diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc31c01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 uko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 377d457..6f7b169 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,94 @@ -# tiny-key-value-store - A simple key-value store library using generic FS (SD/SPIFFS/FatFS/LittleFS/etc) functions for Arduino family boards. +# Tiny Key Value Store Arduino Library + +A simple key-value store library using generic FS (SD/SPIFFS/FatFS/LittleFS/etc) functions for Arduino family boards. + +## Getting Started + +### Global (sketch beginning) part + +#### AVR family boards + +Use with SD shield. + +```cpp +#include +#include +#include + +TinyKeyValueStore store = TinyKeyValueStore(SD); +``` + +#### ESP8266 / ESP32 boards + +Use SPIFFS recommended. + +```cpp +#include +#include + +TinyKeyValueStore store = TinyKeyValueStore(SPIFFS); +``` + +Note: Don't forget to set partition scheme setting as `*** with spiffs` before uploading the sketch. + +### Common part + +```cpp +void setup() { + // 1. Begin based FS class + if (!SD.begin(SS, SPI)) Serial.println("SD Mount Failed"); + // please replace above if you use any other FS libraries + + // 2. Begin tiny-key-value-store + store.begin("config.txt"); + + // 3. Read and Write the values associated with the keys + store.set("KEY_NAME", "hello this is VALUE"); + String val = store.get("KEY_NAME"); + + Serial.begin(38400); + Serial.println(val); // hello this is VALUE +} +``` + +## Dependency + +Requires at least one generic FS extended library like SD, SPIFFS, FatFS, LittleFS, Seeed_FS, etc. + +## API + +### TinyKeyValueStore(fs::FS& fs_obj) + +`fs_obj` : SPIFFS / FFat / LittleFS / SD / and ... + +### void begin(const char* read_write_filename, const char* read_only_filename = "") + +`read_write_filename` : Required. File name for storing key-value data. +`read_only_filename` : Optional. The keys stored in this file are readable, but cannot update value, cannot create additional keys in the sketch. + +### String get(const String key) + +Get the value as String. + +### void getCharArray(const String key, char* charArray) + +Get the value as char*. + +```cpp +char mes[30]; +store.getCharArray("KEY_NAME", mes); +Serial.printf("Message: %s\n", mes); // Message: hello this is VALUE +``` + +### bool set(const String key, const String value) + +Set the value. +Returns true if the value added or updated successfully, otherwise returns false. + +### bool setIfFalse(const String key, const String value) + +Set the argument as value only if existing value equals to FALSE (or 0, empty string). + +## License + +This project is licensed under the MIT License - see the LICENSE.md file for details. \ No newline at end of file diff --git a/examples/BasicSerialString/BasicSerialString.ino b/examples/BasicSerialString/BasicSerialString.ino new file mode 100644 index 0000000..2362210 --- /dev/null +++ b/examples/BasicSerialString/BasicSerialString.ino @@ -0,0 +1,94 @@ +/** + * Tiny Key Value Store + * Basic usage example: storing 2 messages alternatively + * Please open serial monitor and try to send a string message after uploading this sketch + */ + +#ifdef ARDUINO_ARCH_ESP32 +#include +#include +#include +#include +#include +#elif ARDUINO_ARCH_ESP8266 +#include +#include +#elif __SEEED_FS__ +#include +#include +#else +#include +#include +#endif + +/* Library */ +#include +#define MESSAGE_KEY_1 "sample_message_1" +#define MESSAGE_KEY_2 "sample_message_2" +#define STORE_FILE_NAME "/config.txt" // filename must starts with "/" + +bool store1or2 = true; // true: 1, false:2 + +/** + * Choose one FS library as arguments for storing data + * Note: Please check "Tools -> Partition Scheme" settings on ESP32 if happens mount failure + * (when using SPIFFS/FatFS/LittleFS) + */ +TinyKeyValueStore store = TinyKeyValueStore(SPIFFS); +//TinyKeyValueStore store = TinyKeyValueStore(FFat); +//TinyKeyValueStore store = TinyKeyValueStore(LittleFS); +//TinyKeyValueStore store = TinyKeyValueStore(SD); + + +void setup() { + Serial.begin(38400); + + /* first, begin a FS class you chose */ + if (!SPIFFS.begin(true)) Serial.println("SPIFFS Mount Failed"); + //if (!FFat.begin(true)) Serial.println("FFat Mount Failed"); + //if (!LittleFS.begin(true)) Serial.println("LittleFS Mount Failed"); + //if (!SD.begin(SS, SPI)) Serial.println("SD Mount Failed"); // replace SS and SPI to appropriate PINs with your board + + /* second, begin TinyKeyValueStore after beginning FS class */ + store.begin(STORE_FILE_NAME); + + /* message1: get as string class */ + String sample_message_1 = store.get(MESSAGE_KEY_1); + Serial.print("Stored message 1: "); Serial.println(sample_message_1); + /* message2: get as char* */ + char sample_message_2[100]; + store.getCharArray(MESSAGE_KEY_2, sample_message_2); + Serial.printf("Stored message 2: %s\n", sample_message_2); + + Serial.println("=============================="); + Serial.println("Send me something...\n"); +} + + +void loop() { + if (Serial.available() > 0) { + String buf = Serial.readStringUntil('\n'); + buf.trim(); + Serial.printf("Received \"%s\"", buf.c_str()); + + /* store as message 1 or 2 */ + if (store1or2) { + Serial.println(" -> message 1"); + store.set(MESSAGE_KEY_1, buf); + } else { + Serial.println(" -> message 2"); + store.set(MESSAGE_KEY_2, buf); + } + store1or2 = !store1or2; // for next + + String sample_message_1 = store.get(MESSAGE_KEY_1); + Serial.print("Stored message 1: "); Serial.println(sample_message_1); + char sample_message_2[100]; + store.getCharArray(MESSAGE_KEY_2, sample_message_2); + Serial.printf("Stored message 2: %s\n", sample_message_2); + + Serial.println("=============================="); + Serial.println("Send me something...\n"); + } + delay(1); +} diff --git a/examples/WiFiConfig/WiFiConfig.ino b/examples/WiFiConfig/WiFiConfig.ino new file mode 100644 index 0000000..98f6e88 --- /dev/null +++ b/examples/WiFiConfig/WiFiConfig.ino @@ -0,0 +1,179 @@ +/** + * Tiny Key Value Store + * Advanced usage example: Edit WiFi info with your smartphone and keep the configuration + * ================================================================================ + * Note: THIS SKETCH IS DESIGNED ONLY FOR M5STACK FAMILY DEVICES WITH DISPLAY + * ================================================================================ + * 1. Write this sketch and boot your device + * 2. Please press any button or touch display just after boot up the device to be config mode + * 3. Scan QR code by your smartphone to join the device's wifi access point + * 4. Wait some seconds or following the smartphone's instruction to access the WiFi config page (Captive Portal Web) + * 5. Please input WiFi config information and NTP hostname + * 6. Tap "Update & Reboot" button to reboot your device + * 7. If all information is correct, your device will be connected WiFi and got NTP server time + */ + +/* M5 */ +#include +#include +#include +#include +#include +#include + +/* WiFi */ +#include +#include +#include +#include +#include +WiFiClientSecure client; + +/* Library */ +#include +#define STORE_FILE_NAME "/config.txt" // filename must starts with "/" + +/* Keys & Default values */ +#define KEY_WIFI_SSID "wifi_ssid" +#define KEY_WIFI_PSWD "wifi_passwd" +#define KEY_NTPD_HOST "ntp_host" +char ssid[64]; +char pswd[64]; +char host[128]; + +/* Mode flag (Normal or Config) */ +bool config_mode = false; +/* Config Web page HTML */ +const char* config_html_template = +#include "config.html.h" +; +String configHTML = String(""); // create later from template with some vars +/* Config Web Server */ +const char* config_ssid = "TinyKeyValueStoreDevice"; +WebServer configServer(80); +const byte DNS_PORT = 53; +IPAddress dnsIP(8, 8, 4, 4); +DNSServer dnsServer; + +/** + * Choose one FS library as arguments for storing data + * Note: Please check "Tools -> Partition Scheme" settings on ESP32 if happens mount failure + * (when using SPIFFS/FatFS/LittleFS) + */ +TinyKeyValueStore store = TinyKeyValueStore(SPIFFS); +//TinyKeyValueStore store = TinyKeyValueStore(FFat); +//TinyKeyValueStore store = TinyKeyValueStore(LittleFS); +//TinyKeyValueStore store = TinyKeyValueStore(SD); + +/* Reading config vars and create HTML */ +void configLoad(bool updated = false) { + store.getCharArray(KEY_WIFI_SSID, ssid); + store.getCharArray(KEY_WIFI_PSWD, pswd); + store.getCharArray(KEY_NTPD_HOST, host); + char buf[3000]; + const char* updated_notice = updated ? "Updated" : ""; + sprintf(buf, config_html_template, updated_notice, ssid, pswd, host); + configHTML = String(buf); +} + +/* Handler function for accessing the config web server */ +void configWeb() { + if (configServer.method() == HTTP_POST) { + /* POST access: updating vars */ + if (configServer.arg(KEY_WIFI_SSID).length() != 0) store.set(KEY_WIFI_SSID, configServer.arg(KEY_WIFI_SSID)); + if (configServer.arg(KEY_WIFI_PSWD).length() != 0) store.set(KEY_WIFI_PSWD, configServer.arg(KEY_WIFI_PSWD)); + if (configServer.arg(KEY_NTPD_HOST).length() != 0) store.set(KEY_NTPD_HOST, configServer.arg(KEY_NTPD_HOST)); + if (configServer.arg("reboot").equals("yes")) ESP.restart(); // when reboot button clicked + configLoad(true); // updated + } else { + /* GET access: only show current config values */ + configLoad(false); // not updated + } + configServer.send(200, "text/html", configHTML); +} + +void setup() { + auto cfg = M5.config(); + M5.begin(cfg); + M5.Display.setTextColor(0xFFFFFFU, 0x000000U); + M5.Display.setTextFont(2); + + /* first, begin a FS class you chose */ + if (!SPIFFS.begin(true)) Serial.println("SPIFFS Mount Failed"); + //if (!FFat.begin(true)) Serial.println("FFat Mount Failed"); + //if (!LittleFS.begin(true)) Serial.println("LittleFS Mount Failed"); + //if (!SD.begin(SS, SPI)) Serial.println("SD Mount Failed"); // replace SS and SPI to appropriate PINs with your board + + /* second, begin TinyKeyValueStore after beginning FS class */ + store.begin(STORE_FILE_NAME); + + /* reading char variables from store */ + store.getCharArray(KEY_WIFI_SSID, ssid); + store.getCharArray(KEY_WIFI_PSWD, pswd); + store.getCharArray(KEY_NTPD_HOST, host); + + /* Check pressing BtnA or not when M5 just booting up */ + M5.Display.printf("To configure WiFi,\nPlease press any button or touch display ..."); + for (int i=0; i<500; i++) { + delay(10); + M5.update(); + if (M5.BtnA.isPressed() || M5.BtnB.isPressed() || M5.BtnC.isPressed() || M5.Touch.getCount() > 0) { + config_mode = true; + break; + } + } + M5.Display.clear(); + M5.Display.setCursor(0, 0); + + if (config_mode) { + /* Pressed: WiFi config mode - Start WiFi AP and Web server */ + WiFi.softAP(config_ssid); + WiFi.softAPConfig(dnsIP, dnsIP, IPAddress(255, 255, 255, 0)); + dnsServer.start(DNS_PORT, "*", dnsIP); + /* Serve as CaptivePortal */ + configServer.onNotFound(configWeb); + configServer.begin(); + /** + * show ZXing's style WiFi connection QR + * see: https://zxing.appspot.com/generator + */ + String qrStr = "WIFI:S:" + String(config_ssid) + ";;"; + M5.Display.qrcode(qrStr); + } else { + /* Not: Normal mode - NTP Client */ + WiFi.mode(WIFI_STA); WiFi.disconnect(); + M5.Display.printf("WiFi SSID: %s\n", ssid); + WiFi.begin(ssid, pswd); + while (WiFi.status() != WL_CONNECTED) { + if (millis() >= 60000UL) ESP.restart(); // reboot when couldnt connect within 1 min + M5.Display.print("."); delay(1000); + } + M5.Display.printf("\nConnected\nIP: %s\n", WiFi.localIP().toString().c_str()); + /* Get time from NTP */ + M5.Display.printf("\nNTP Host: %s\n", host); + configTime(0, 0, host); + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + M5.Display.printf("Time: failed to get\n"); + } else { + M5.Display.printf("%04d-%02d-%02d %02d:%02d:%02d" + ,timeinfo.tm_year + 1900 + ,timeinfo.tm_mon + 1 + ,timeinfo.tm_mday + ,timeinfo.tm_hour + ,timeinfo.tm_min + ,timeinfo.tm_sec + ); + } + } +} + + +void loop() { + /* Keep handling CaptivePortal when config mode */ + if (config_mode) { + dnsServer.processNextRequest(); + configServer.handleClient(); + } + delay(1); +} diff --git a/examples/WiFiConfig/config.html.h b/examples/WiFiConfig/config.html.h new file mode 100644 index 0000000..96230bd --- /dev/null +++ b/examples/WiFiConfig/config.html.h @@ -0,0 +1,17 @@ +"" +"" +"" +"" +"WiFi Config" +"

WiFi Config

" +"

%s

" +"

" +"" +"
WiFi SSID
" +"
WiFi Password
" +"
NTP Host Name
" +"" +"
" +"
" +"
" +"

"; \ No newline at end of file diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..69eb02a --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=Tiny Key Value Store +version=1.0.0 +author=uko +maintainer=uko +sentence=A simple key-value store library based on FileSystem +paragraph=This library stores data as config file using FileSystem API that compatibles with the SD/File API from Arduino official library, reading and writing a value as generic string class. Also available on SPIFFS, FatFS, LittleFS and similar FS classes. +category=Data Storage +url=https://github.com/ukkz/tiny-key-value-store +architectures=* \ No newline at end of file diff --git a/src/TinyKeyValueStore.h b/src/TinyKeyValueStore.h new file mode 100644 index 0000000..ce5b7a4 --- /dev/null +++ b/src/TinyKeyValueStore.h @@ -0,0 +1,140 @@ +#pragma once +#ifdef ARDUINO_ARCH_ESP32 +#include +#include +#include +#include +#include +#elif ARDUINO_ARCH_ESP8266 +#include +#include +#elif __SEEED_FS__ +#include +#include +#else +#include +#include +#endif + +class TinyKeyValueStore { + private: + fs::FS& fs; + const char* rofile; + const char* rwfile; + const String KEYVALUE_SEPARATOR = ":"; + const char ENTRY_SEPARATOR = '\n'; + bool exist(const String filename, const String key) { + bool result = false; + File f = fs.open(filename); + while (true) { + // read 1 line / 1 loop + String line = f.readStringUntil(ENTRY_SEPARATOR); + // break if no any lines + if (!line.length()) break; + // looking up the key string + int i = line.indexOf(key + KEYVALUE_SEPARATOR); + if (i == 0) result = true; + } + f.close(); + return result; + } + String read(const String filename, const String key) { + String val = String(""); + File f = fs.open(filename); + while (true) { + // read 1 line / 1 loop + String line = f.readStringUntil(ENTRY_SEPARATOR); + // break if no any lines + if (!line.length()) break; + // looking up the key string + int i = line.indexOf(key + KEYVALUE_SEPARATOR); + if (i == 0) { + // HEAD index of the value + int s = line.indexOf(KEYVALUE_SEPARATOR) + 1; + // get to TAIL of the value + val = line.substring(s); + break; + } + } + val.trim(); + f.close(); + return val; + } + + public: + TinyKeyValueStore(fs::FS& fs_obj) : fs(fs_obj) {} + void begin(const char* read_write_filename, const char* read_only_filename = "") { + rwfile = read_write_filename; + rofile = read_only_filename; + // Create file if not exists (ReadWriteFile only) + if (!fs.exists(rwfile)) { + // WIP: output debug info + //Serial.printf("File \"%s\" not found - created\n", rwfile); + File f = fs.open(rwfile, "w"); + f.write(ENTRY_SEPARATOR); + f.close(); + } + } + String get(const String key) { + // return the value from ReadOnlyFile preferentially (in case duplicate key written in ReadWriteFile) + if (!String(rofile).equals("")) { + String ro = read(rofile, key); + if (!ro.equals("")) return ro; + } + // return the value from ReadWriteFile in case ReadOnlyFile is not set or the key is not exist + String rw = read(rwfile, key); + if (!rw.equals("")) return rw; + return ""; + } + void getCharArray(const String key, char* charArray) { + // use if need char* instead of this.get().c_str() + String val = get(key); + val.toCharArray(charArray, val.length()+1); + } + bool set(const String key, const String value) { + // Reading the entire of ReadWriteFile except for the lines to be updated + String buf = String(""); + File f = fs.open(rwfile); + while (true) { + // read 1 line / 1 loop + String line = f.readStringUntil(ENTRY_SEPARATOR); + // break if no any lines + if (!line.length()) break; + // looking up the key string + if (line.indexOf(key) == -1) { + // not: line -> buffer + buf = buf + line + ENTRY_SEPARATOR; + } else { + // exist: ignore current line and add new value to buffer with this key + buf = buf + key + KEYVALUE_SEPARATOR + value + ENTRY_SEPARATOR; + } + } + f.close(); + // add new key + if (buf.indexOf(key + KEYVALUE_SEPARATOR) == -1) buf = buf + key + KEYVALUE_SEPARATOR + value + ENTRY_SEPARATOR; + // Delete ReadWriteFile + bool d = fs.remove(rwfile); + if (!d) { + //Serial.printf("Couldn't delete: %s\n", rwfile); + return false; + } + // Re-generate ReadWriteFile and write the buffer + File rw = fs.open(rwfile, "w"); + if (!rw) { + //Serial.printf("Couldn't open: %s\n", rwfile); + return false; + } + rw.print(buf); + rw.close(); + return true; + } + bool setIfFalse(const String key, const String value) { + // set the argument as value only if existing value equals with FALSE (or 0, empty string) + String target_value = read(rwfile, key); + if (target_value.equals("") || target_value.toInt() == 0 || target_value.toFloat() == 0.0) { + return set(key, value); + } else { + return false; + } + } +}; \ No newline at end of file