Proivdes data log data structure for FRAM, EPROM chip with functions to read chip and send back on serial data string.

Dependencies:   W25Q80BV multi-serial-command-listener

Dependents:   xj-data-log-test-and-example

Data Logging Data structure

Both Read and write seem to be working fine but testing has been limited.

Motivation

I needed a flexible data log structure that could tolerate evolving data structures as I discovered more things that needed to be measured. I also wanted something that is mostly human readable while remaining sufficiently concise to make efficient use of expensive storage resources.

I found it challenging to track everything needed to perform after the fact analysis we need to improve our state machine. In addition what I wanted to measure changed with time and I needed a robust way to log this data so we could analyze it latter. without breaking or converting all the old data. A self describing data format like JSON or XML would work but FRAM is expensive so I wanted something flexible but still concise.

I am working on A2WH which is a electronic controller for a sophisticated product that balances many sensors, battery charging from photo voltaic panels, controlling speed of many different fans, humidity and environmental data. Our main challenge is we never have enough battery power to run everything so we have to make decisions about what to run in an effort to produce the maximum amount of water from the available solar power resource. Our 2nd challenge is that balancing system actions such as increasing or decreasing fan speeds is driven by a complex internal prediction model that attempts balance many competing thermodynamic requirements. To get all this right requires substantial after the fact analysis and that requires logging a large amount of evolving data.

Design Notes

See: data-log-read.me.txt in the same project

Sample Use and Basic Test

Serial Command Interface

COMMANDS
  readall= send entire contents of log
  readlast 999
     999 = number of bytes from tail of log to retrieve
  tread 333 444
     333 = starting offset to start reading log
     444 = number of bytes to retrieve from log
  erase = erase log and start a new one
  help  = display this help

Other Chips

For legacy reasons I am using the library for "W25Q80BV.h" simply because I started with it. The actual FRAM chip I am using is 2 MBit FRAM MB85RS2MTPH-G-JNE I also tested it with SRAM 23LCV1024-I/P

Simplifying Design Decision

I made a simplifying assumption that every-time we generate a log entry I record the offset of the next write at a specific location in the chip. This works and is fast but it causes lots of updates against a single location. I prefer FRAM because this would rapidly fatigue FLASH chips like the W25Q80BV. Storing this pointer data in the CPU has the same fatigue problem.

Another other option would be to store this offset and our other critical configuration data in the clock chip but it is susceptible to loosing power and loosing this critical data.

One reason I don't log directly to the micro-sd is for the same fatigue problem but it is mostly for power management.

The FRAM chip provides adequate durability and data retention through power outage. The power outage retention is critical because the A2WH systems can be buried under feet of snow in the winter and solar panels do not provide much recharge under that condition.

One design option I have considered but not yet implemented is using a much smaller FRAM chip critical configuration data and rapid update data and then log directly to a larger and less expensive FLASH chip .

Journaling to micro-SD

I latter decided to add features to allow after the fact copying of the data to micro-sd cards to obtain larger log storage without soldering in more chips. I found the micro-sd consume quite a lot of power so I still want to log direct to the FRAM then copy to the micro-sd when I have surplus power available. Still thinking about consolidation tactics to allow re-use of FRAM after the data has been copied ot micro-sd.

Future

  • Support fast indexing by date to only pull back log entries between two dates.
  • Record most recent record headers for each record types where they are fast to access so we can send them with the data when only sending back portions of the data.
  • Support wrap around use of data log to re-use storage on chip.
  • Copy Data to micro SD card and consolidate FRAM chip for re-use.

License

By Joseph Ellsworth CTO of A2WH Take a look at A2WH.com Producing Water from Air using Solar Energy March-2016 License: https://developer.mbed.org/handbook/MIT-Licence Please contact us http://a2wh.com for help with custom design projects.

Committer:
joeata2wh
Date:
Wed Mar 30 21:44:22 2016 +0000
Revision:
3:5550814cc21c
Parent:
2:8d06af2f1fcc
Child:
4:fa5bbe31a039
; data logger test and sample use code.

Who changed what in which revision?

UserRevisionLine numberNew contents of line
joeata2wh 1:b2e12bf6b4aa 1 /* DataLog.h - Data logger for logging enviornmental
joeata2wh 1:b2e12bf6b4aa 2 data to EPROM, SRAM or FRAM chip without a file system
joeata2wh 3:5550814cc21c 3 inteface. Supports multiple and evolving records types
joeata2wh 3:5550814cc21c 4 and trnsport back to serial
joeata2wh 3:5550814cc21c 5
joeata2wh 3:5550814cc21c 6 See data-log-text.txt for detailed design and layout notes
joeata2wh 3:5550814cc21c 7 See: xj-data-log-test-and-example.c for example use.
joeata2wh 1:b2e12bf6b4aa 8
joeata2wh 1:b2e12bf6b4aa 9 By Joseph Ellsworth CTO of A2WH
joeata2wh 1:b2e12bf6b4aa 10 Take a look at A2WH.com Producing Water from Air using Solar Energy
joeata2wh 1:b2e12bf6b4aa 11 March-2016 License: https://developer.mbed.org/handbook/MIT-Licence
joeata2wh 1:b2e12bf6b4aa 12 Please contact us http://a2wh.com for help with custom design projects.
joeata2wh 3:5550814cc21c 13
joeata2wh 1:b2e12bf6b4aa 14 Before you complain about not using proper C++ classes, I intend to
joeata2wh 1:b2e12bf6b4aa 15 port the entire A2WH project to PSoc where I may or may not be able to
joeata2wh 1:b2e12bf6b4aa 16 use the full set of C++ features. I am using techniques that should be
joeata2wh 1:b2e12bf6b4aa 17 easier to port to a ANSI C enviornment. You may think this is a wierd
joeata2wh 1:b2e12bf6b4aa 18 decision but the PSoC enviornments gives me transparant support for
joeata2wh 1:b2e12bf6b4aa 19 differential ADC and very low power analog comparators that remain active
joeata2wh 1:b2e12bf6b4aa 20 when CPU is in deep sleep and which can wake the CPU up from deep sleep
joeata2wh 1:b2e12bf6b4aa 21 which makes very low power designs easier. mBed is still weak for this
joeata2wh 1:b2e12bf6b4aa 22 kind of advanced peripherial support.
joeata2wh 1:b2e12bf6b4aa 23
joeata2wh 1:b2e12bf6b4aa 24 */
joeata2wh 1:b2e12bf6b4aa 25 #ifndef DataLog_H
joeata2wh 1:b2e12bf6b4aa 26 #define DataLog_H
joeata2wh 1:b2e12bf6b4aa 27 #include "mbed.h"
joeata2wh 3:5550814cc21c 28
joeata2wh 1:b2e12bf6b4aa 29 #include "W25Q80BV.h" // Note: We are using this library because we started with it but
joeata2wh 1:b2e12bf6b4aa 30 // the actual chip we are using is a 2 MBit FRAM MB85RS2MTPH-G-JNE
joeata2wh 1:b2e12bf6b4aa 31 // also tested with SRAM 23LCV1024-I/P Prefer SRAM or FRAM because
joeata2wh 1:b2e12bf6b4aa 32 // we made a simplifying assumption that we could write next log
joeata2wh 1:b2e12bf6b4aa 33 // address to the same position over and over. Without wear leveling
joeata2wh 1:b2e12bf6b4aa 34 // this could rapidly wear out a e-prom chip. Could have written
joeata2wh 1:b2e12bf6b4aa 35 // this to the clock chip which uses SRAM but FRAM has such high write
joeata2wh 1:b2e12bf6b4aa 36 // durability that we don't have to.
joeata2wh 1:b2e12bf6b4aa 37
joeata2wh 1:b2e12bf6b4aa 38 #define DataLogChipType W25Q80BV
joeata2wh 1:b2e12bf6b4aa 39
joeata2wh 1:b2e12bf6b4aa 40 // TODO: Add the SDIO link here to copy data from dlog chip to
joeata2wh 1:b2e12bf6b4aa 41 // SD card when available.
joeata2wh 1:b2e12bf6b4aa 42
joeata2wh 2:8d06af2f1fcc 43
joeata2wh 1:b2e12bf6b4aa 44 const long dlChipMaxAddr = 250000; // 2 mBit 2000000 / 8
joeata2wh 1:b2e12bf6b4aa 45 #define dlChipFullErr -2
joeata2wh 1:b2e12bf6b4aa 46 #define dlAddressTooLarge -3
joeata2wh 1:b2e12bf6b4aa 47 #define dlMaxOperationSize -4;
joeata2wh 1:b2e12bf6b4aa 48 // Chip DLog Chip Memory Layout
joeata2wh 1:b2e12bf6b4aa 49 const long dlAddrInitByte = 1500; // data before this is assumed to be used for system config variables
joeata2wh 1:b2e12bf6b4aa 50 const char dlInitByteValue = 213;
joeata2wh 1:b2e12bf6b4aa 51 const int dlMaxReadWriteSize = 32000; // limit imposed by streaming interface for the chip
joeata2wh 1:b2e12bf6b4aa 52 const long dlAddrNextWritePos = dlInitByteValue + 1;
joeata2wh 1:b2e12bf6b4aa 53 const long dlAddrNextWritePosSize = 4;
joeata2wh 1:b2e12bf6b4aa 54 const long dlAddrCurrYDay = dlAddrNextWritePos + dlAddrNextWritePosSize;
joeata2wh 1:b2e12bf6b4aa 55 const long dlAddrCurrYDaySize = 2;
joeata2wh 1:b2e12bf6b4aa 56 const long dlAddrHeaders = dlAddrCurrYDay + dlAddrCurrYDaySize ;
joeata2wh 1:b2e12bf6b4aa 57 const long dlHeadersLen = 256;
joeata2wh 1:b2e12bf6b4aa 58 const long dlDateIndex = dlAddrHeaders + dlHeadersLen + 1;
joeata2wh 1:b2e12bf6b4aa 59 const long dlDateIndexLen= 1000;
joeata2wh 1:b2e12bf6b4aa 60 const long dlFirstLogEntry= dlDateIndexLen + dlDateIndexLen + 1;
joeata2wh 1:b2e12bf6b4aa 61 const long dlBuffLen = 256;
joeata2wh 1:b2e12bf6b4aa 62 const char dlEmpty[] = {0,0,0,0,0,0,0};
joeata2wh 1:b2e12bf6b4aa 63 const long dlMaxLogSize = dlChipMaxAddr - dlFirstLogEntry;
joeata2wh 1:b2e12bf6b4aa 64 #define MIN(X,Y) X <? Y
joeata2wh 1:b2e12bf6b4aa 65 #define MAX(X,Y) X >? Y
joeata2wh 1:b2e12bf6b4aa 66
joeata2wh 1:b2e12bf6b4aa 67 struct DLOG {
joeata2wh 1:b2e12bf6b4aa 68 DataLogChipType *chip;
joeata2wh 1:b2e12bf6b4aa 69 long nextWritePos;
joeata2wh 1:b2e12bf6b4aa 70 int currYDay; // tm_yday from gmtime only log date date when date changes
joeata2wh 1:b2e12bf6b4aa 71 char *buff;
joeata2wh 1:b2e12bf6b4aa 72 int buffLen;
joeata2wh 1:b2e12bf6b4aa 73 };
joeata2wh 1:b2e12bf6b4aa 74
joeata2wh 1:b2e12bf6b4aa 75
joeata2wh 1:b2e12bf6b4aa 76 // save the current nextWritePos to the chip so we hav it
joeata2wh 1:b2e12bf6b4aa 77 // just in case of a reboot
joeata2wh 1:b2e12bf6b4aa 78 void dlSaveNextWritePos(struct DLOG *wrk) {
joeata2wh 1:b2e12bf6b4aa 79 wrk->chip->writeStream(dlAddrNextWritePos,(char *) &wrk->nextWritePos,dlAddrNextWritePosSize); // write next write postion
joeata2wh 1:b2e12bf6b4aa 80 }
joeata2wh 1:b2e12bf6b4aa 81
joeata2wh 1:b2e12bf6b4aa 82 long dlReadNextWritePos(struct DLOG *wrk) {
joeata2wh 1:b2e12bf6b4aa 83 wrk->chip->readStream(dlAddrNextWritePos, (char *) &wrk->nextWritePos, dlAddrNextWritePosSize);
joeata2wh 1:b2e12bf6b4aa 84 return wrk->nextWritePos;
joeata2wh 1:b2e12bf6b4aa 85 }
joeata2wh 1:b2e12bf6b4aa 86
joeata2wh 1:b2e12bf6b4aa 87 int dlReadCurrYDay(struct DLOG *wrk) {
joeata2wh 1:b2e12bf6b4aa 88 wrk->chip->readStream(dlAddrNextWritePos, (char *) &wrk->currYDay, dlAddrCurrYDaySize);
joeata2wh 1:b2e12bf6b4aa 89 return wrk->currYDay;
joeata2wh 1:b2e12bf6b4aa 90 }
joeata2wh 1:b2e12bf6b4aa 91
joeata2wh 1:b2e12bf6b4aa 92 void dlUpdateCurrDate(struct DLOG *wrk, int newYDay) {
joeata2wh 1:b2e12bf6b4aa 93 wrk->currYDay = newYDay;
joeata2wh 1:b2e12bf6b4aa 94 wrk->chip->writeStream(dlAddrCurrYDay,(char *) &wrk->currYDay, dlAddrCurrYDaySize); // write next write postion
joeata2wh 1:b2e12bf6b4aa 95 }
joeata2wh 1:b2e12bf6b4aa 96
joeata2wh 1:b2e12bf6b4aa 97 // New data log chip detected write data to initialize it.
joeata2wh 1:b2e12bf6b4aa 98 long dlInitializeChip(struct DLOG *wrk) {
joeata2wh 1:b2e12bf6b4aa 99 wrk->nextWritePos = dlFirstLogEntry;
joeata2wh 1:b2e12bf6b4aa 100 wrk->chip->writeStream(dlAddrInitByte, (char *) &dlInitByteValue,1); // write init byte
joeata2wh 1:b2e12bf6b4aa 101 wrk->chip->writeStream(dlAddrNextWritePos,(char *) &wrk->nextWritePos,dlAddrNextWritePosSize); // write next write postion
joeata2wh 1:b2e12bf6b4aa 102
joeata2wh 1:b2e12bf6b4aa 103 memset(wrk->buff,0,wrk->buffLen);
joeata2wh 1:b2e12bf6b4aa 104 wrk->chip->writeStream(dlAddrHeaders,wrk->buff,MIN(wrk->buffLen,dlHeadersLen)); // nulls over the header region
joeata2wh 1:b2e12bf6b4aa 105 wrk->chip->writeStream(dlDateIndex,wrk->buff,MIN(wrk->buffLen,dlDateIndexLen)); // nulls over the header region
joeata2wh 1:b2e12bf6b4aa 106
joeata2wh 1:b2e12bf6b4aa 107 wrk->chip->writeStream(dlDateIndex,wrk->buff,MIN(wrk->buffLen,dlDateIndexLen)); // nulls over the header region
joeata2wh 1:b2e12bf6b4aa 108 wrk->currYDay = -99;
joeata2wh 1:b2e12bf6b4aa 109 return wrk->nextWritePos;
joeata2wh 1:b2e12bf6b4aa 110 }
joeata2wh 1:b2e12bf6b4aa 111
joeata2wh 1:b2e12bf6b4aa 112
joeata2wh 1:b2e12bf6b4aa 113 /* read a initialization byte from chip. If the byte
joeata2wh 1:b2e12bf6b4aa 114 doesn't contain the expected value then write one
joeata2wh 1:b2e12bf6b4aa 115 and assume that we are starting our log ad the beginning */
joeata2wh 1:b2e12bf6b4aa 116 long dlCheckChipInit(struct DLOG *wrk){
joeata2wh 1:b2e12bf6b4aa 117 wrk->buff[0] = 0;
joeata2wh 1:b2e12bf6b4aa 118 wrk->chip->readStream(dlInitByteValue, wrk->buff, 1);
joeata2wh 1:b2e12bf6b4aa 119 if (wrk->buff[0] != dlInitByteValue)
joeata2wh 1:b2e12bf6b4aa 120 return dlInitializeChip(wrk);
joeata2wh 1:b2e12bf6b4aa 121 else {
joeata2wh 1:b2e12bf6b4aa 122 dlReadCurrYDay(wrk);
joeata2wh 1:b2e12bf6b4aa 123 return dlReadNextWritePos(wrk);
joeata2wh 1:b2e12bf6b4aa 124 }
joeata2wh 1:b2e12bf6b4aa 125 }
joeata2wh 1:b2e12bf6b4aa 126
joeata2wh 1:b2e12bf6b4aa 127
joeata2wh 1:b2e12bf6b4aa 128 // make and instance of our dlog structure fill it in
joeata2wh 1:b2e12bf6b4aa 129 // in and load any current data such as next write postion
joeata2wh 1:b2e12bf6b4aa 130 // already loaded in the chip.
joeata2wh 1:b2e12bf6b4aa 131 struct DLOG *dlMake(DataLogChipType *dataLogMem, char *buff, short buffLen) {
joeata2wh 1:b2e12bf6b4aa 132 struct DLOG *tout = (struct DLOG *) malloc(sizeof(struct DLOG));
joeata2wh 1:b2e12bf6b4aa 133 tout->chip = dataLogMem;
joeata2wh 1:b2e12bf6b4aa 134 tout->nextWritePos = dlFirstLogEntry;
joeata2wh 1:b2e12bf6b4aa 135 tout->buff = buff;
joeata2wh 1:b2e12bf6b4aa 136 tout->buffLen = buffLen;
joeata2wh 1:b2e12bf6b4aa 137 dlCheckChipInit(tout);
joeata2wh 1:b2e12bf6b4aa 138 return tout;
joeata2wh 1:b2e12bf6b4aa 139 }
joeata2wh 1:b2e12bf6b4aa 140
joeata2wh 1:b2e12bf6b4aa 141 // writes log stream entry to chip and updates the next write
joeata2wh 1:b2e12bf6b4aa 142 // position. Also adds a null terminator to data on chip
joeata2wh 1:b2e12bf6b4aa 143 // log entries should not contain null characters because we
joeata2wh 1:b2e12bf6b4aa 144 // eventually plant to delay flush of nextWritePos and use
joeata2wh 1:b2e12bf6b4aa 145 // scan forware to find the end when a crash occurs.
joeata2wh 1:b2e12bf6b4aa 146 // returns -2 if the write request would go beyond chip size.
joeata2wh 3:5550814cc21c 147 long dlWrite(struct DLOG *wrk, char *aStr) {
joeata2wh 1:b2e12bf6b4aa 148 int slen = strlen(aStr);
joeata2wh 1:b2e12bf6b4aa 149 if ((wrk->nextWritePos + slen) >= dlChipMaxAddr) {
joeata2wh 1:b2e12bf6b4aa 150 return dlChipFullErr;
joeata2wh 1:b2e12bf6b4aa 151 }
joeata2wh 1:b2e12bf6b4aa 152 wrk->chip->writeStream(wrk->nextWritePos, aStr,slen);
joeata2wh 1:b2e12bf6b4aa 153 wrk->nextWritePos += slen;
joeata2wh 1:b2e12bf6b4aa 154 wrk->chip->writeStream(wrk->nextWritePos, (char *)dlEmpty, 1); // add terminating null
joeata2wh 1:b2e12bf6b4aa 155
joeata2wh 1:b2e12bf6b4aa 156 dlSaveNextWritePos(wrk); // WARNING THIS IS THE LINE THAT WILL KILL EPROM CHIPS
joeata2wh 1:b2e12bf6b4aa 157 // with over-write fatigue.
joeata2wh 1:b2e12bf6b4aa 158 // TODO: add err check read first and last bytes.
joeata2wh 1:b2e12bf6b4aa 159 // compare to what was written.
joeata2wh 1:b2e12bf6b4aa 160 return wrk->nextWritePos;
joeata2wh 1:b2e12bf6b4aa 161 }
joeata2wh 1:b2e12bf6b4aa 162
joeata2wh 3:5550814cc21c 163 long dlLog(struct DLOG *wrk, char *recType, char *str) {
joeata2wh 1:b2e12bf6b4aa 164 time_t seconds = time(NULL);
joeata2wh 1:b2e12bf6b4aa 165 tm *ptm = gmtime ( &seconds );
joeata2wh 1:b2e12bf6b4aa 166 if (ptm->tm_yday != wrk->currYDay) {
joeata2wh 1:b2e12bf6b4aa 167 dlUpdateCurrDate(wrk, ptm->tm_yday);
joeata2wh 1:b2e12bf6b4aa 168 sprintf(wrk->buff,"DATE\t000000\n\000");
joeata2wh 3:5550814cc21c 169 dlWrite(wrk, wrk->buff);
joeata2wh 1:b2e12bf6b4aa 170 }
joeata2wh 1:b2e12bf6b4aa 171 memset(wrk->buff,12,0);
joeata2wh 1:b2e12bf6b4aa 172 sprintf(wrk->buff,"%s\t%2d%2d%2d\t", recType, ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
joeata2wh 3:5550814cc21c 173 dlWrite(wrk, wrk->buff);
joeata2wh 1:b2e12bf6b4aa 174 return write(wrk, str);
joeata2wh 1:b2e12bf6b4aa 175 }
joeata2wh 1:b2e12bf6b4aa 176
joeata2wh 1:b2e12bf6b4aa 177 // read a block of bytes from log starting at offset
joeata2wh 1:b2e12bf6b4aa 178 // for len bytes placed in buffer. If offset is >
joeata2wh 1:b2e12bf6b4aa 179 // log size then return dlAddressTooLarge if len would
joeata2wh 1:b2e12bf6b4aa 180 // be greate than log size only return that available.
joeata2wh 3:5550814cc21c 181 long dlRead(struct DLOG *wrk, char *buff, long offset, int len) {
joeata2wh 1:b2e12bf6b4aa 182 long addr = dlFirstLogEntry + offset;
joeata2wh 1:b2e12bf6b4aa 183 if ((addr + len) >= dlChipMaxAddr)
joeata2wh 1:b2e12bf6b4aa 184 return dlAddressTooLarge;
joeata2wh 1:b2e12bf6b4aa 185 wrk->chip->readStream(offset, buff, len);
joeata2wh 1:b2e12bf6b4aa 186 return 1;
joeata2wh 1:b2e12bf6b4aa 187 }
joeata2wh 1:b2e12bf6b4aa 188
joeata2wh 3:5550814cc21c 189 long dlReadSend(struct DLOG *wrk, Serial *dest, long offset, long len) {
joeata2wh 1:b2e12bf6b4aa 190 long addr = dlFirstLogEntry + offset;
joeata2wh 1:b2e12bf6b4aa 191 long maxAddr = MIN(addr + len, dlChipMaxAddr); // no overflow past end of chip
joeata2wh 1:b2e12bf6b4aa 192 maxAddr = MIN(maxAddr, wrk->nextWritePos); // no overlow pas end of log
joeata2wh 1:b2e12bf6b4aa 193 int chunkSize = dlBuffLen -1;
joeata2wh 1:b2e12bf6b4aa 194 if (addr >= maxAddr)
joeata2wh 1:b2e12bf6b4aa 195 return dlAddressTooLarge;
joeata2wh 1:b2e12bf6b4aa 196 long endAdd = MIN(addr + len, maxAddr);
joeata2wh 1:b2e12bf6b4aa 197
joeata2wh 1:b2e12bf6b4aa 198 do {
joeata2wh 1:b2e12bf6b4aa 199 memset(wrk->buff, chunkSize, 0);
joeata2wh 1:b2e12bf6b4aa 200 if (addr + chunkSize > endAdd)
joeata2wh 1:b2e12bf6b4aa 201 chunkSize = endAdd - addr;
joeata2wh 1:b2e12bf6b4aa 202 wrk->chip->readStream(addr, wrk->buff, chunkSize);
joeata2wh 1:b2e12bf6b4aa 203 dest->printf("%s", wrk->buff);
joeata2wh 1:b2e12bf6b4aa 204 addr += chunkSize;
joeata2wh 1:b2e12bf6b4aa 205 } while (addr < endAdd);
joeata2wh 2:8d06af2f1fcc 206 return -1;
joeata2wh 1:b2e12bf6b4aa 207 }
joeata2wh 1:b2e12bf6b4aa 208
joeata2wh 1:b2e12bf6b4aa 209
joeata2wh 1:b2e12bf6b4aa 210 //if (wroteValue == 0) {
joeata2wh 1:b2e12bf6b4aa 211 // memset(buff,0,60);
joeata2wh 1:b2e12bf6b4aa 212 // strcpy(buff, "This is a Test of writting ");
joeata2wh 1:b2e12bf6b4aa 213 // dataLogMem.writeStream(read_addr,buff,35);
joeata2wh 1:b2e12bf6b4aa 214 // wroteValue = 1;
joeata2wh 1:b2e12bf6b4aa 215
joeata2wh 1:b2e12bf6b4aa 216 #endif