123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689 |
- // 1-channel LoRa Gateway for ESP8266
- // Copyright (c) 2016-2020 Maarten Westenberg version for ESP8266
- //
- // based on work done by Thomas Telkamp for Raspberry PI 1ch gateway
- // and many others.
- //
- // All rights reserved. This program and the accompanying materials
- // are made available under the terms of the MIT License
- // which accompanies this distribution, and is available at
- // https://opensource.org/licenses/mit-license.php
- //
- // NO WARRANTY OF ANY KIND IS PROVIDED
- //
- // Author: Maarten Westenberg (mw12554@hotmail.com)
- //
- // This file contains the LoRa filesystem specific code
- #if _MONITOR>=1
- // ----------------------------------------------------------------------------
- // LoRa Monitor logging code.
- // Define one print function and depending on the logging parameter output
- // to _USB of to the www screen function
- // ----------------------------------------------------------------------------
- int initMonitor(struct moniLine *monitor)
- {
- for (int i=0; i< _MAXMONITOR; i++) {
- monitor[i].txt= "-"; // Make all lines empty
- }
- iMoni=0; // Init the index
- return(1);
- }
- #endif //_MONITOR
- // ============================================================================
- // LORA SPIFFS FILESYSTEM FUNCTIONS
- //
- // The LoRa supporting functions are in the section below
- // ----------------------------------------------------------------------------
- // Supporting function to readConfig
- // ----------------------------------------------------------------------------
- void id_print (String id, String val)
- {
- #if _MONITOR>=1
- if (( debug>=0 ) && ( pdebug & P_MAIN )) {
- Serial.print(id);
- Serial.print(F("=\t"));
- Serial.println(val);
- }
- #endif //_MONITOR
- }
- // ============================================================================
- // config functions
- //
- // ----------------------------------------------------------------------------
- // INITCONFIG; Init the gateway configuration file
- // Espcecially when calling SPIFFS.format() the gateway is left in an init state
- // which is not very well defined. This function will init some of the settings
- // to well known settings.
- // ----------------------------------------------------------------------------
- void initConfig(struct espGwayConfig *c)
- {
- (*c).ch = 0;
- (*c).sf = _SPREADING;
- (*c).debug = 1; // debug level is 1
- (*c).pdebug = P_GUI | P_MAIN;
- (*c).cad = _CAD;
- (*c).hop = false;
- (*c).seen = true; // Seen interface is ON
- (*c).expert = false; // Expert interface is OFF
- (*c).monitor = true; // Monitoring is ON
- (*c).trusted = 1;
- (*c).txDelay = 0; // First Value without saving is 0;
- (*c).dusbStat = true;
- } // initConfig()
- // ----------------------------------------------------------------------------
- // Read the config file and fill the (copied) variables
- // ----------------------------------------------------------------------------
- int readGwayCfg(const char *fn, struct espGwayConfig *c)
- {
- if (readConfig(fn, c)<0) {
- # if _MONITOR>=1
- mPrint("readConfig:: Error reading config file");
- return 0;
- # endif //_MONITOR
- }
- if (gwayConfig.sf != (uint8_t) 0) {
- sf = (sf_t) gwayConfig.sf;
- }
- debug = (*c).debug;
- pdebug = (*c).pdebug;
- (*c).boots++; // Increment Boot Counter
- # if _GATEWAYNODE==1
- if (gwayConfig.fcnt != (uint8_t) 0) {
- frameCount = gwayConfig.fcnt+10;
- }
- # endif
- writeGwayCfg(CONFIGFILE, &gwayConfig ); // And writeback the configuration, not to miss a boot
- return 1;
-
- } // readGwayCfg()
- // ----------------------------------------------------------------------------
- // Read the gateway configuration file
- // ----------------------------------------------------------------------------
- int readConfig(const char *fn, struct espGwayConfig *c)
- {
-
- int tries = 0;
- if (!SPIFFS.exists(fn)) {
- # if _MONITOR>=1
- mPrint("readConfig ERR:: file="+String(fn)+" does not exist ..");
- # endif //_MONITOR
- initConfig(c); // If we cannot read the config, at least init known values
- return(-1);
- }
- File f = SPIFFS.open(fn, "r");
- if (!f) {
- # if _MONITOR>=1
- Serial.println(F("ERROR:: SPIFFS open failed"));
- # endif //_MONITOR
- return(-1);
- }
- while (f.available()) {
-
- # if _MONITOR>=1
- if (( debug>=0 ) && ( pdebug & P_MAIN )) {
- Serial.print('.');
- }
- # endif //_MONITOR
- // If we wait for more than 15 times, reformat the filesystem
- // We do this so that the system will be responsive (over OTA for example).
- //
- if (tries >= 15) {
- f.close();
- # if _MONITOR>=1
- if (debug>=0) {
- mPrint("readConfig:: Formatting");
- }
- # endif //_MONITOR
- SPIFFS.format();
- f = SPIFFS.open(fn, "r");
- tries = 0;
- initSeen(listSeen);
- }
- initConfig(c); // Even if we do not read a value, give a default
-
- String id =f.readStringUntil('='); // Read keyword until '=', C++ thing
- String val=f.readStringUntil('\n'); // Read value until End of Line (EOL)
- if (id == "MONITOR") { // MONITOR button setting
- id_print(id, val);
- (*c).monitor = (bool) val.toInt();
- }
- else if (id == "CH") { // Frequency Channel
- id_print(id,val);
- (*c).ch = (uint8_t) val.toInt();
- }
- else if (id == "SF") { // Spreading Factor
- id_print(id, val);
- (*c).sf = (uint8_t) val.toInt();
- }
- else if (id == "FCNT") { // Frame Counter
- id_print(id, val);
- (*c).fcnt = (uint16_t) val.toInt();
- }
- else if (id == "DEBUG") { // Debug Level
- id_print(id, val);
- (*c).debug = (uint8_t) val.toInt();
- }
- else if (id == "PDEBUG") { // pDebug Pattern
- id_print(id, val);
- (*c).pdebug = (uint8_t) val.toInt();
- }
- else if (id == "CAD") { // CAD setting
- id_print(id, val);
- (*c).cad = (bool) val.toInt();
- }
- else if (id == "HOP") { // HOP setting
- id_print(id, val);
- (*c).hop = (bool) val.toInt();
- }
- else if (id == "BOOTS") { // BOOTS setting
- id_print(id, val);
- (*c).boots = (uint16_t) val.toInt();
- }
- else if (id == "RESETS") { // RESET setting
- id_print(id, val);
- (*c).resets = (uint16_t) val.toInt();
- }
- else if (id == "WIFIS") { // WIFIS setting
- id_print(id, val);
- (*c).wifis = (uint16_t) val.toInt();
- }
- else if (id == "VIEWS") { // VIEWS setting
- id_print(id, val);
- (*c).views = (uint16_t) val.toInt();
- }
- else if (id == "NODE") { // NODE setting
- id_print(id, val);
- (*c).isNode = (bool) val.toInt();
- }
- else if (id == "REFR") { // REFR setting
- id_print(id, val);
- (*c).refresh = (bool) val.toInt();
- }
- else if (id == "REENTS") { // REENTS setting
- id_print(id, val);
- (*c).reents = (uint16_t) val.toInt();
- }
- else if (id == "NTPERR") { // NTPERR setting
- id_print(id, val);
- (*c).ntpErr = (uint16_t) val.toInt();
- }
- else if (id == "WAITERR") { // WAITERR setting
- id_print(id, val);
- (*c).waitErr = (uint16_t) val.toInt();
- }
- else if (id == "WAITOK") { // WAITOK setting
- id_print(id, val);
- (*c).waitOk = (uint16_t) val.toInt();
- }
- else if (id == "NTPETIM") { // NTPERR setting
- id_print(id, val);
- (*c).ntpErrTime = (uint32_t) val.toInt();
- }
- else if (id == "NTPS") { // NTPS setting
- id_print(id, val);
- (*c).ntps = (uint16_t) val.toInt();
- }
- else if (id == "FILENO") { // FILENO setting
- id_print(id, val);
- (*c).logFileNo = (uint16_t) val.toInt();
- }
- else if (id == "FILEREC") { // FILEREC setting
- id_print(id, val);
- (*c).logFileRec = (uint16_t) val.toInt();
- }
- else if (id == "FILENUM") { // FILEREC setting
- id_print(id, val);
- (*c).logFileNum = (uint16_t) val.toInt();
- }
- else if (id == "EXPERT") { // EXPERT button setting
- id_print(id, val);
- (*c).expert = (bool) val.toInt();
- }
- else if (id == "SEEN") { // SEEN button setting
- id_print(id, val);
- (*c).seen = (bool) val.toInt();
- }
- else if (id == "DELAY") { // DELAY setting
- id_print(id, val);
- (*c).txDelay = (int32_t) val.toInt();
- }
- else if (id == "TRUSTED") { // TRUSTED setting
- id_print(id, val);
- (*c).trusted= (int8_t) val.toInt();
- }
- else if (id == "FORMAT") { // TRUSTED setting
- id_print(id, val);
- (*c).formatCntr= (int8_t) val.toInt();
- }
- else {
- # if _MONITOR>=1
- mPrint(F("readConfig:: tries++"));
- # endif //_MONITOR
- tries++;
- }
- }
- f.close();
- return(1);
-
- } // readConfig()
- // ----------------------------------------------------------------------------
- // Write the current gateway configuration to SPIFFS. First copy all the
- // separate data items to the gwayConfig structure
- //
- // Note: gwayConfig.expert contains the expert setting already
- // gwayConfig.txDelay
- // ----------------------------------------------------------------------------
- int writeGwayCfg(const char *fn, struct espGwayConfig *c)
- {
-
- (*c).sf = (uint8_t) sf; // Spreading Factor
- (*c).debug = debug;
- (*c).pdebug = pdebug;
- # if _GATEWAYNODE==1
- (*c).fcnt = frameCount;
- # endif //_GATEWAYNODE
- return(writeConfig(fn, c));
- } // writeGwayCfg
- // ----------------------------------------------------------------------------
- // Write the configuration as found in the espGwayConfig structure
- // to SPIFFS
- // Parameters:
- // fn; Filename
- // c; struct config
- // Returns:
- // 1 when successful, -1 on error
- // ----------------------------------------------------------------------------
- int writeConfig(const char *fn, struct espGwayConfig *c)
- {
- // Assuming the config file is the first we write...
-
- File f = SPIFFS.open(fn, "w");
- if (!f) {
- #if _MONITOR>=1
- mPrint("writeConfig: ERROR open file="+String(fn));
- #endif //_MONITOR
- return(-1);
- }
- f.print("CH"); f.print('='); f.print((*c).ch); f.print('\n');
- f.print("SF"); f.print('='); f.print((*c).sf); f.print('\n');
- f.print("FCNT"); f.print('='); f.print((*c).fcnt); f.print('\n');
- f.print("DEBUG"); f.print('='); f.print((*c).debug); f.print('\n');
- f.print("PDEBUG"); f.print('='); f.print((*c).pdebug); f.print('\n');
- f.print("CAD"); f.print('='); f.print((*c).cad); f.print('\n');
- f.print("HOP"); f.print('='); f.print((*c).hop); f.print('\n');
- f.print("NODE"); f.print('='); f.print((*c).isNode); f.print('\n');
- f.print("BOOTS"); f.print('='); f.print((*c).boots); f.print('\n');
- f.print("RESETS"); f.print('='); f.print((*c).resets); f.print('\n');
- f.print("WIFIS"); f.print('='); f.print((*c).wifis); f.print('\n');
- f.print("VIEWS"); f.print('='); f.print((*c).views); f.print('\n');
- f.print("REFR"); f.print('='); f.print((*c).refresh); f.print('\n');
- f.print("REENTS"); f.print('='); f.print((*c).reents); f.print('\n');
- f.print("NTPETIM"); f.print('='); f.print((*c).ntpErrTime); f.print('\n');
- f.print("NTPERR"); f.print('='); f.print((*c).ntpErr); f.print('\n');
- f.print("WAITERR"); f.print('='); f.print((*c).waitErr); f.print('\n');
- f.print("WAITOK"); f.print('='); f.print((*c).waitOk); f.print('\n');
- f.print("NTPS"); f.print('='); f.print((*c).ntps); f.print('\n');
- f.print("FILEREC"); f.print('='); f.print((*c).logFileRec); f.print('\n');
- f.print("FILENO"); f.print('='); f.print((*c).logFileNo); f.print('\n');
- f.print("FILENUM"); f.print('='); f.print((*c).logFileNum); f.print('\n');
- f.print("FORMAT"); f.print('='); f.print((*c).formatCntr); f.print('\n');
- f.print("DELAY"); f.print('='); f.print((*c).txDelay); f.print('\n');
- f.print("TRUSTED"); f.print('='); f.print((*c).trusted); f.print('\n');
- f.print("EXPERT"); f.print('='); f.print((*c).expert); f.print('\n');
- f.print("SEEN"); f.print('='); f.print((*c).seen); f.print('\n');
- f.print("MONITOR"); f.print('='); f.print((*c).monitor); f.print('\n');
-
- f.close();
- return(1);
- } // writeConfig()
- // ----------------------------------------------------------------------------
- // Add a line with statistics to the log.
- //
- // We put the check in the function to protect against calling
- // the function without _STAT_LOG being proper defined
- // ToDo: Store the fileNo and the fileRec in the status file to save for
- // restarts
- //
- // Parameters:
- // line; char array with characters to write to log
- // cnt;
- // Returns:
- // <none>
- // ----------------------------------------------------------------------------
- int addLog(const unsigned char * line, int cnt)
- {
- # if _STAT_LOG==1
- char fn[16];
-
- if (gwayConfig.logFileRec > LOGFILEREC) { // Have to make define for this
- gwayConfig.logFileRec = 0; // In new logFile start with record 0
- gwayConfig.logFileNo++; // Increase file ID
- gwayConfig.logFileNum++; // Increase number of log files
- }
- gwayConfig.logFileRec++;
-
- // If we have too many logfies, delete the oldest
- //
- if (gwayConfig.logFileNum > LOGFILEMAX){
- sprintf(fn,"/log-%d", gwayConfig.logFileNo - LOGFILEMAX);
- # if _MONITOR>=1
- if (( debug>=2 ) && ( pdebug & P_GUI )) {
- mPrint("addLog:: Too many logfiles, deleting="+String(fn));
- }
- # endif //_MONITOR
- SPIFFS.remove(fn);
- gwayConfig.logFileNum--;
- }
-
- // Make sure we have the right fileno
- sprintf(fn,"/log-%d", gwayConfig.logFileNo);
-
- // If there is no SPIFFS, Error
- // Make sure to write the config record/line also
- if (!SPIFFS.exists(fn)) {
- # if _MONITOR>=1
- if (( debug >= 2 ) && ( pdebug & P_GUI )) {
- mPrint("addLog:: WARNING file="+String(fn)+" does not exist .. rec="+String(gwayConfig.logFileRec) );
- }
- # endif //_MONITOR
- }
-
- File f = SPIFFS.open(fn, "a");
- if (!f) {
- # if _MONITOR>=1
- if (( debug>=1 ) && ( pdebug & P_GUI )) {
- mPrint("addLOG:: ERROR file open failed="+String(fn));
- }
- # endif //_MONITOR
- return(0); // If file open failed, return
- }
-
- int i=0;
- # if _MONITOR>=1
- if (( debug>=2 ) && ( pdebug & P_GUI )) {
- Serial.print(F("addLog:: fileno="));
- Serial.print(gwayConfig.logFileNo);
- Serial.print(F(", rec="));
- Serial.print(gwayConfig.logFileRec);
- Serial.print(F(": "));
- # if _MONITOR>=2
- {
- for (i=0; i< 12; i++) { // The first 12 bytes contain non printable characters
- Serial.print(line[i],HEX);
- Serial.print(' ');
- }
- }
- # else //_MONITOR>=2
- i+=12;
- # endif //_DUSB>=2
- Serial.print((char *) &line[i]); // The rest if the buffer contains ascii
- Serial.println();
- }
- # endif //_MONITOR
- for (i=0; i< 12; i++) { // The first 12 bytes contain non printable characters
- // f.print(line[i],HEX);
- f.print('*');
- }
- f.write(&(line[i]), cnt-12); // write/append the line to the file
- f.print('\n');
- f.close(); // Close the file after appending to it
- # endif //_STAT_LOG
- return(1);
- } //addLog()
- // ============================================================================
- // Below are the xxxSeen() functions. These functions keep track of the kast
- // time a device was seen bij the gateway.
- // These functions are not round-robin and they do not need to be.
- // ----------------------------------------------------------------------------
- // initSeen
- // Init the lisrScreen array
- // Return:
- // 1: Success
- // Parameters:
- // listSeen: array of Seen data
- // ----------------------------------------------------------------------------
- int initSeen(struct nodeSeen *listSeen)
- {
- #if _MAXSEEN >= 1
- for (int i=0; i< _MAXSEEN; i++) {
- listSeen[i].idSeen=0;
- listSeen[i].sfSeen=0;
- listSeen[i].cntSeen=0;
- listSeen[i].chnSeen=0;
- listSeen[i].timSeen=(time_t) 0; // 1 jan 1970 0:00:00 hrs
- }
- iSeen= 0; // Init index to 0
- #endif // _MAXSEEN
- return(1);
- } // initSeen()
- // ----------------------------------------------------------------------------
- // readSeen
- // This function read the information stored by writeSeen from the file.
- // The file is read as String() values and converted to int after.
- // Parameters:
- // fn: Filename
- // listSeen: Array of all last seen nodes on the LoRa network
- // Return:
- // 1: When successful
- // ----------------------------------------------------------------------------
- int readSeen(const char *fn, struct nodeSeen *listSeen)
- {
- #if _MAXSEEN >= 1
- int i;
- iSeen= 0; // Init the index at 0
-
- if (!SPIFFS.exists(fn)) { // Does listSeen file exist
- # if _MONITOR>=1
- mPrint("WARNING:: readSeen, history file not exists "+String(fn) );
- # endif //_MONITOR
- initSeen(listSeen); // XXX make all initial declarations here if config vars need to have a value
- return(-1);
- }
-
- File f = SPIFFS.open(fn, "r");
- if (!f) {
- # if _MONITOR>=1
- mPrint("readSeen:: ERROR open file=" + String(fn));
- # endif //_MONITOR
- return(-1);
- }
- delay(1000);
-
- for (i=0; i<_MAXSEEN; i++) {
- delay(200);
- String val="";
-
- if (!f.available()) {
- # if _MONITOR>=2
- mPrint("readSeen:: No more info left in file, i=" + String(i));
- # endif //_MONITOR
- break;
- }
- val=f.readStringUntil('\t'); listSeen[i].timSeen = (time_t) val.toInt();
- val=f.readStringUntil('\t'); listSeen[i].idSeen = (int64_t) val.toInt();
- val=f.readStringUntil('\t'); listSeen[i].cntSeen = (uint32_t) val.toInt();
- val=f.readStringUntil('\t'); listSeen[i].chnSeen = (uint8_t) val.toInt();
- val=f.readStringUntil('\n'); listSeen[i].sfSeen = (uint8_t) val.toInt();
-
- # if _MONITOR>=1
- if ((debug>=2) && (pdebug & P_MAIN)) {
- mPrint("readSeen:: idSeen ="+String(listSeen[i].idSeen,HEX)+", i="+String(i));
- }
- # endif
- iSeen++; // Increase index, new record read
- }
- f.close();
- #endif // _MAXSEEN
- // So we read iSeen records
- return 1;
-
- } // readSeen()
- // ----------------------------------------------------------------------------
- // writeSeen
- // Once every few messages, update the SPIFFS file and write the array.
- // Parameters:
- // - fn contains the filename to write
- // - listSeen contains the _MAXSEEN array of list structures
- // Return values:
- // - return 1 on success
- // ----------------------------------------------------------------------------
- int writeSeen(const char *fn, struct nodeSeen *listSeen)
- {
- #if _MAXSEEN >= 1
- int i;
- if (!SPIFFS.exists(fn)) {
- # if _MONITOR>=1
- mPrint("WARNING:: writeSeen, file not exists="+String(fn));
- # endif //_MONITOR
- //initSeen(listSeen); // XXX make all initial declarations here if config vars need to have a value
- }
-
- File f = SPIFFS.open(fn, "w");
- if (!f) {
- # if _MONITOR>=1
- mPrint("writeSeen:: ERROR open file="+String(fn)+" for writing");
- # endif //_MONITOR
- return(-1);
- }
- delay(500);
- for (i=0; i<iSeen; i++) { // For all records indexed
- f.print((time_t)listSeen[i].timSeen); f.print('\t');
- f.print((int32_t)listSeen[i].idSeen); f.print('\t'); // Typecast to avoid errors in unsigned conversion!
- f.print((uint32_t)listSeen[i].cntSeen); f.print('\t');
- f.print((uint8_t)listSeen[i].chnSeen); f.print('\t');
- f.print((uint8_t)listSeen[i].sfSeen); f.print('\n');
- }
-
- f.close();
- #endif // _MAXSEEN
- return(1);
- }
- // ----------------------------------------------------------------------------
- // addSeen
- // With every message received:
- // - Look whether message is already in the array, if so update existing message.
- // - If not, create new record.
- // - With this record, update the SF settings
- //
- // Parameters:
- // listSeen: The array of records of nodes we have seen
- // stat: one record
- // Returns:
- // 1 when successful
- // ----------------------------------------------------------------------------
- int addSeen(struct nodeSeen *listSeen, struct stat_t stat)
- {
- #if _MAXSEEN >= 1
- int i;
- for (i=0; i<iSeen; i++) { // For all known records
- // If the record node is equal, we found the record already.
- // So increment cntSeen
- if (listSeen[i].idSeen==stat.node) {
-
- listSeen[i].timSeen = (time_t)stat.time;
- listSeen[i].cntSeen++; // Not included on function para
- //listSeen[i].idSeen = stat.node; // Not necessary, is the same
- listSeen[i].chnSeen = stat.ch;
- listSeen[i].sfSeen = stat.sf; // The SF argument
- // writeSeen(_SEENFILE, listSeen);
-
- # if _MONITOR>=2
- if ((debug>=1) && (pdebug & P_MAIN)) {
- mPrint("addSeen:: adding i="+String(i)+", node="+String(stat.node,HEX));
- }
- # endif
- return 1;
- }
- }
-
- // else: We did not find the current record so make a new Seen entry
- if ((i>=iSeen) && (i<_MAXSEEN)) {
- listSeen[i].idSeen = stat.node;
- listSeen[i].chnSeen = stat.ch;
- listSeen[i].sfSeen = stat.sf; // The SF argument
- listSeen[i].timSeen = (time_t)stat.time; // Timestamp correctly
- listSeen[i].cntSeen = 1; // We see this for the first time
- iSeen++;
- }
- # if _MONITOR>=1
- if ((debug>=2) && (pdebug & P_MAIN)) {
- String response= "addSeen:: i=";
- response += i;
- response += ", tim=";
- stringTime(stat.time, response);
- response += ", iSeen=";
- response += String(iSeen);
- response += ", node=";
- response += String(stat.node,HEX);
- response += ", listSeen[0]=";
- printHex(listSeen[0].idSeen,':',response);
- mPrint(response);
- }
- # endif // _MONITOR
- #endif //_MAXSEEN>=1
- return 1;
-
- } // addSeen()
- // End of File
|