├── docs ├── ToC.md ├── iSpindHub.jpg ├── SettingsHome.jpg ├── iSpindHubConf1.jpg ├── iSpindHubConf2.jpg ├── HomePageISpindHub.PNG ├── HomePageiSpindHub.jpg ├── iSpindHubDisplay.jpg ├── iSpindHubBaseSettings.jpg ├── BrewFatherTargetSettings.jpg ├── start.md ├── MakeRelease.py ├── screens.md └── readme.md ├── src ├── screen_colour.h ├── filemgr.h ├── ispindel.h ├── cronpush.h ├── filemgr.cpp ├── pushhelper.h ├── Flash.bat ├── wifi.h ├── config.h ├── main.h ├── screen.h ├── tools.h ├── ispindel.cpp ├── templatescreen.h ├── web.h ├── uptime.h ├── jsonconfig.h ├── pushtarget.h ├── target.h ├── brewfather.h ├── ntp.h ├── tools.cpp ├── ntp.cpp ├── uptime.cpp ├── cronpush.cpp ├── resetreasons.h ├── pushhelper.cpp ├── screen.cpp ├── main.cpp ├── target.cpp ├── wifi.cpp ├── jsonconfig.cpp └── templatescreen.cpp ├── data ├── TMS10.vlw ├── TMS12.vlw ├── Arial12.vlw ├── Arial20.vlw ├── Arial9.vlw ├── RBold20.vlw ├── RThin12.vlw ├── RThin20.vlw ├── RThin9.vlw ├── favicon.ico ├── FreeSans12.vlw ├── FreeSans20.vlw ├── FreeSans9.vlw ├── SegLight20.vlw ├── favicon-16x16.png ├── favicon-32x32.png ├── hop-icon-16.xcf ├── hop-icon-24.png ├── hop-icon-24.xcf ├── hop-icon-32.xcf ├── FreeSansGras12.vlw ├── mstile-144x144.png ├── apple-touch-icon.png ├── README.md ├── browserconfig.xml ├── site.webmanifest ├── data │ ├── iSpindel032.csv │ └── templates │ │ ├── default_2.json │ │ ├── korev.json │ │ └── default.json ├── wifi2.htm ├── wifi.htm ├── help.htm ├── license.htm ├── about.js ├── about.htm ├── reset.htm ├── index.htm ├── safari-pinned-tab.svg ├── ota.htm └── settings.js ├── .gitattributes ├── STL └── iSpindHub v12.stl ├── pictures ├── iSpindHub2.jpg ├── 1_4_TFT_RedTab_Back.jpg └── 1_4_TFT_RedTab_Front.jpg ├── releases ├── Common │ └── littlefs.bin └── 1_4_RedBand │ ├── firmware.bin │ └── littlefs.bin ├── .gitignore ├── iSpindHub.code-workspace ├── .vscode └── extensions.json ├── test └── README ├── ToDo.md ├── lib └── README ├── include └── README ├── platformio.ini └── README.md /docs/ToC.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/screen_colour.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/TMS10.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/TMS10.vlw -------------------------------------------------------------------------------- /data/TMS12.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/TMS12.vlw -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /data/Arial12.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/Arial12.vlw -------------------------------------------------------------------------------- /data/Arial20.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/Arial20.vlw -------------------------------------------------------------------------------- /data/Arial9.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/Arial9.vlw -------------------------------------------------------------------------------- /data/RBold20.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/RBold20.vlw -------------------------------------------------------------------------------- /data/RThin12.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/RThin12.vlw -------------------------------------------------------------------------------- /data/RThin20.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/RThin20.vlw -------------------------------------------------------------------------------- /data/RThin9.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/RThin9.vlw -------------------------------------------------------------------------------- /data/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/favicon.ico -------------------------------------------------------------------------------- /data/FreeSans12.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/FreeSans12.vlw -------------------------------------------------------------------------------- /data/FreeSans20.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/FreeSans20.vlw -------------------------------------------------------------------------------- /data/FreeSans9.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/FreeSans9.vlw -------------------------------------------------------------------------------- /data/SegLight20.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/SegLight20.vlw -------------------------------------------------------------------------------- /docs/iSpindHub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/iSpindHub.jpg -------------------------------------------------------------------------------- /STL/iSpindHub v12.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/STL/iSpindHub v12.stl -------------------------------------------------------------------------------- /data/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/favicon-16x16.png -------------------------------------------------------------------------------- /data/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/favicon-32x32.png -------------------------------------------------------------------------------- /data/hop-icon-16.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/hop-icon-16.xcf -------------------------------------------------------------------------------- /data/hop-icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/hop-icon-24.png -------------------------------------------------------------------------------- /data/hop-icon-24.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/hop-icon-24.xcf -------------------------------------------------------------------------------- /data/hop-icon-32.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/hop-icon-32.xcf -------------------------------------------------------------------------------- /docs/SettingsHome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/SettingsHome.jpg -------------------------------------------------------------------------------- /data/FreeSansGras12.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/FreeSansGras12.vlw -------------------------------------------------------------------------------- /data/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/mstile-144x144.png -------------------------------------------------------------------------------- /docs/iSpindHubConf1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/iSpindHubConf1.jpg -------------------------------------------------------------------------------- /docs/iSpindHubConf2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/iSpindHubConf2.jpg -------------------------------------------------------------------------------- /pictures/iSpindHub2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/pictures/iSpindHub2.jpg -------------------------------------------------------------------------------- /data/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/data/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/HomePageISpindHub.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/HomePageISpindHub.PNG -------------------------------------------------------------------------------- /docs/HomePageiSpindHub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/HomePageiSpindHub.jpg -------------------------------------------------------------------------------- /docs/iSpindHubDisplay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/iSpindHubDisplay.jpg -------------------------------------------------------------------------------- /releases/Common/littlefs.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/releases/Common/littlefs.bin -------------------------------------------------------------------------------- /docs/iSpindHubBaseSettings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/iSpindHubBaseSettings.jpg -------------------------------------------------------------------------------- /pictures/1_4_TFT_RedTab_Back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/pictures/1_4_TFT_RedTab_Back.jpg -------------------------------------------------------------------------------- /docs/BrewFatherTargetSettings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/docs/BrewFatherTargetSettings.jpg -------------------------------------------------------------------------------- /pictures/1_4_TFT_RedTab_Front.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/pictures/1_4_TFT_RedTab_Front.jpg -------------------------------------------------------------------------------- /releases/1_4_RedBand/firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/releases/1_4_RedBand/firmware.bin -------------------------------------------------------------------------------- /releases/1_4_RedBand/littlefs.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeSlammy/iSpindHub/HEAD/releases/1_4_RedBand/littlefs.bin -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /src/filemgr.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "tools.h" 3 | #include 4 | #include 5 | void get_files_info(); -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Web Pages 2 | 3 | These are the pages that are loaded onto the controller that provide user interaction functionality with the iSpindHub 4 | -------------------------------------------------------------------------------- /iSpindHub.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | "functional": "cpp" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/ispindel.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "tools.h" 3 | #include 4 | #include "screen.h" 5 | #include "templatescreen.h" 6 | int handle_spindel_data(String iSpinData, int delay_loop,int last_seen_ms); 7 | -------------------------------------------------------------------------------- /src/cronpush.h: -------------------------------------------------------------------------------- 1 | #ifndef CRONPUSH_H 2 | #define CRONPUSH_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "wifi.h" 9 | 10 | #endif 11 | void pushBrewFather(); -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/filemgr.cpp: -------------------------------------------------------------------------------- 1 | #include "filemgr.h" 2 | 3 | void get_files_info() 4 | { 5 | FSInfo fs_info; 6 | LittleFS.info(fs_info); 7 | 8 | Dir dir = LittleFS.openDir("/data"); 9 | // or Dir dir = LittleFS.openDir("/data"); 10 | while (dir.next()) 11 | { 12 | // Open iSpindel Data file 13 | Serial.println("File Name : " + dir.fileName()); 14 | } 15 | } -------------------------------------------------------------------------------- /src/pushhelper.h: -------------------------------------------------------------------------------- 1 | #ifndef _PUSHHELPER_H 2 | #define _PUSHHELPER_H 3 | 4 | #include "pushtarget.h" 5 | #include "target.h" 6 | #include "brewfather.h" 7 | #include 8 | 9 | IPAddress resolveHost(const char hostname[129]); 10 | bool pushToTarget(PushTarget *, IPAddress, int); 11 | void updateLoop(); 12 | void setDoURLTarget(); 13 | void setDoBFTarget(); 14 | void setDoBrewfTarget(); 15 | 16 | #endif // _PUSHHELPER_H 17 | -------------------------------------------------------------------------------- /data/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #00aba9 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /data/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iSpindHub", 3 | "short_name": "iSPindHub", 4 | "icons": [{ 5 | "src": "/icons/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/icons/android-chrome-512x512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone" 18 | } -------------------------------------------------------------------------------- /src/Flash.bat: -------------------------------------------------------------------------------- 1 | "c:\users\noutrey\.platformio\penv\scripts\python.exe" "C:\Users\noutrey\.platformio\packages\tool-esptoolpy@1.30000.201119\esptool.py" --before default_reset --after hard_reset --chip esp8266 --port "COM24" --baud 115200 write_flash 0x0 ..\.pio\build\2_inches\firmware.bin 2 | "c:\users\noutrey\.platformio\penv\scripts\python.exe" "C:\Users\noutrey\.platformio\packages\tool-esptoolpy@1.30000.201119\esptool.py" --before default_reset --after hard_reset --chip esp8266 --port "COM24" --baud 115200 write_flash 1048576 ..\.pio\build\2_inches\littlefs.bin -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | -------------------------------------------------------------------------------- /src/wifi.h: -------------------------------------------------------------------------------- 1 | #ifndef _WIFI_H 2 | #define _WIFI_H 3 | #define WM_ASYNC // Turn on Async mode 4 | 5 | #include 6 | // #include 7 | #include "tools.h" 8 | #include 9 | #include "config.h" 10 | #include "jsonconfig.h" 11 | #include 12 | // #define WEBSERVER_H 13 | // #include 14 | void doWiFi(); 15 | void doWiFi(bool dontUseStoredCreds); 16 | void resetWifi(); 17 | extern struct Config config; 18 | // WiFiManager Callbacks 19 | // void apCallback(AsyncWiFiManager *myWiFiManager); 20 | void apCallback(ESPAsync_WiFiManager *myWiFiManager); 21 | void saveConfigCallback(); 22 | void saveParamsCallback(); 23 | void WiFiEvent(WiFiEvent_t event); 24 | 25 | #endif // _WIFI_H -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | #ifndef APNAME 4 | #define APNAME "iSpindHub" 5 | #endif 6 | #ifndef APPWD 7 | #define APPWD "" 8 | #endif 9 | #ifndef APCONFNAME 10 | #define APCONFNAME "iSpindHubConfig" 11 | #endif 12 | #ifndef APCONFPWD 13 | #define APCONFPWD "" 14 | #endif 15 | #ifndef VERSION_STRING 16 | #define VERSION_STRING PIO_SRC_TAG 17 | #endif 18 | #ifndef WIFI_CHAN 19 | #define WIFI_CHAN 1 20 | #endif // WIFI_CHAN 21 | #ifndef HOSTNAME 22 | #define HOSTNAME "iSpindHub" 23 | #endif // HOSTNAME 24 | #ifndef TIMESERVER 25 | #define TIMESERVER "pool.ntp.org", "time.nist.gov" 26 | #define THISTZ TZ_Etc_GMT 27 | #endif // TIMESERVER 28 | 29 | #ifndef LOG_LEVEL 30 | #define LOG_LEVEL LOG_LEVEL_VERBOSE 31 | #endif // LOG_LEVEL 32 | 33 | 34 | #endif -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | #ifndef _MAIN_H 2 | #define _MAIN_H 3 | #include 4 | #define FILESYSTEM LittleFS 5 | #define FLashFS LitteFS 6 | #include // time() ctime() 7 | #ifdef ESP8266 8 | #include // struct timeval 9 | #endif 10 | #include //https://github.com/esp8266/Arduino 11 | //needed for library 12 | #include 13 | #include 14 | #include "TickTwo.h" 15 | #include 16 | #include 17 | #include "wifi.h" 18 | #include "tools.h" 19 | #include "web.h" 20 | //#include 21 | #include 22 | #include "ntp.h" 23 | #include "ispindel.h" 24 | #include "cronpush.h" 25 | 26 | #endif // _MAIN_H 27 | -------------------------------------------------------------------------------- /data/data/iSpindel032.csv: -------------------------------------------------------------------------------- 1 | 2021-05-02T12:12:22Z,iSpindel032,63.3900,18.26,4.2000,1.0560,900.0,-57.0,, 2 | 2021-05-02T12:27:22Z,iSpindel032,63.1900,18.25,4.2000,1.054,900.0,-57.0,, 3 | 2021-05-02T12:42:22Z,iSpindel032,63.0900,18.25,4.2000,1.0520,900.0,-57.0,, 4 | 2021-05-02T12:57:22Z,iSpindel032,63.0900,18.25,4.2000,1.0520,900.0,-57.0,, 5 | 2021-05-02T13:12:22Z,iSpindel032,62.9900,18.22,4.15,1.0500,900.0,-57.0,, 6 | 2021-05-02T13:27:22Z,iSpindel032,62.9900,18.22,4.15,1.0500,900.0,-57.0,, 7 | 2021-05-02T13:42:22Z,iSpindel032,62.7900,18.20,4.15,1.0480,900.0,-57.0,, 8 | 2021-05-02T13:57:22Z,iSpindel032,62.7900,18.18,4.15,1.0480,900.0,-57.0,, 9 | 2021-05-02T14:12:22Z,iSpindel032,62.6900,18.12,4.15,1.0460,900.0,-57.0,, 10 | 2021-05-02T14:27:22Z,iSpindel032,62.5900,18.08,4.15,1.0440,900.0,-57.0,, 11 | 2021-05-02T14:42:22Z,iSpindel032,62.3900,17.99,4.14,1.0420,900.0,-57.0,, 12 | -------------------------------------------------------------------------------- /ToDo.md: -------------------------------------------------------------------------------- 1 | # To Do List 2 | 3 | - [x] HomePage showing info about iSpindels 4 | 5 | - [ ] Configuration Page for Little Bock 6 | 7 | - [X] Configuration Page for BrewFather 8 | 9 | - [X] Configuration Page for URL 10 | 11 | - [X] Jobs that send info to LittleBock / BrewFather / etc (Done for BF) 12 | 13 | - [ ] Page to manage log files (Delete / Trim) 14 | 15 | - [ ] Historical graphs 16 | 17 | - [X] Handle multiple screens through multiple releases 18 | 19 | - [ ] Handle multiple screens through options in configuration 20 | 21 | - [ ] BrewPiless as a target 22 | 23 | - [X] Change the name of the Broadcasted network 24 | 25 | - [X] Change colors (ultimately allow people to make their own screens without recompiling) 26 | 27 | - [ ] Handle error on "No Json file" 28 | 29 | - [ ] Handle config to be persistent in between flashes 30 | 31 | - [X] Handle TimeZones (Require reboot for the time being) 32 | 33 | - [ ] Maybe handle file downloading 34 | -------------------------------------------------------------------------------- /src/screen.h: -------------------------------------------------------------------------------- 1 | //#include // Core graphics library 2 | //#include // Hardware-specific library 3 | #include 4 | #include 5 | #include 6 | #include "tools.h" 7 | #include 8 | #define ST7735_GRAY 0x8410 9 | #define ST7735_LIME 0x07FF 10 | #define ST7735_AQUA 0x04FF 11 | //#define ST7735_PINK 0xF8FF 12 | #define FreeSans9 "FreeSans20" 13 | //#define FreeSansGras9 "FreeSansGras9" 14 | #define FreeSans12 "FreeSans12" 15 | #define FreeSansGras12 "SegLight20" 16 | #define Arial9 "Arial9" 17 | #define Arial12 "Arial12" 18 | #define Arial20 "Arial20" 19 | #include 20 | #include 21 | #define FlashFS LittleFS 22 | //#define TFT_CS 2 23 | //#define TFT_RST -1 // you can also connect this to the Arduino reset 24 | // in which case, set this #define pin to -1! 25 | //#define TFT_DC 0 26 | void displaydata(String array_data[10],int last_seen_ms); -------------------------------------------------------------------------------- /docs/start.md: -------------------------------------------------------------------------------- 1 | # iSpindHub Startup & Configuration 2 | ## _Start here once you've flashed your device_ 3 | 4 | iSpindHub needs to be connected to a Wifi network in order to work properly. 5 | Once it is, the display changes and starts displaying iSpindle info. 6 | 7 | - Set Up using the WiFi config wizard 8 | - Restarts 9 | - ✨Magic ✨ 10 | 11 | ## How it should work 12 | 13 | - When you start the D1 mini for the first time, it should display 14 | iSpinHub x.y.z 15 | by Slammy 16 | - While not configured, that's all it will display 17 | - If from your PC or phone you look for Wifi networks, you should see one call iSpindHub 18 | - Connect to it, it should bring you to the config page 19 | - Chose the Wifi you want your iSpindHub to connect to and enter password 20 | - Once done, the D1 Mini will reboot 21 | - The screen will then display 22 | iSpinHub x.y.z 23 | by Slammy 24 | Connected 25 | - Then after a while (5 seconds) will display iSpindel Information (I put some dummy file in the image) 26 | -------------------------------------------------------------------------------- /src/tools.h: -------------------------------------------------------------------------------- 1 | #ifndef _TOOLS_H 2 | #define _TOOLS_H 3 | #include 4 | #include 5 | //#include // Core graphics library 6 | //#include // Hardware-specific library 7 | #include 8 | void _delay(unsigned long); 9 | #define DRD_TIMEOUT 3.0 10 | #define DRD_ADDRESS 0x00 11 | void centerString(String buf, int x, int y); 12 | /* Useful Constants */ 13 | #define SECS_PER_MIN (60UL) 14 | #define SECS_PER_HOUR (3600UL) 15 | #define SECS_PER_DAY (SECS_PER_HOUR * 24L) 16 | #include 17 | /* Useful Macros for getting elapsed time */ 18 | #define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) 19 | #define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) 20 | #define numberOfHours(_time_) (( _time_% SECS_PER_DAY) / SECS_PER_HOUR) 21 | #define elapsedDays(_time_) ( _time_ / SECS_PER_DAY) 22 | String pretty_time(long val); 23 | void printDigits(byte digits); 24 | String get_last_value(String iSpinData); 25 | uint32_t get_color(String colorString); 26 | #endif -------------------------------------------------------------------------------- /src/ispindel.cpp: -------------------------------------------------------------------------------- 1 | #include "ispindel.h" 2 | //extern Adafruit_ST7735 tft; 3 | extern TFT_eSPI tft; 4 | //extern int delay_loop; 5 | 6 | int handle_spindel_data(String iSpinData,int delay_loop,int last_seen_ms){ 7 | //Serial.println("delay loop en entrant"); 8 | //Serial.println(delay_loop); 9 | //String lastData = get_last_value(iSpinData); 10 | String lastData = iSpinData; 11 | //Serial.println(lastData); 12 | int str_len = lastData.length() +1; 13 | int count = 0; 14 | int idx; 15 | int mov_idx = 0; 16 | String array_data[10] = {}; 17 | for (idx = 0; idx <= str_len; idx++) 18 | { 19 | if (lastData[idx] == ',') 20 | { //splitData[count] = lastData.substring(mov_idx,idx-1); 21 | array_data[count] = lastData.substring(mov_idx,idx); 22 | mov_idx = idx+1; 23 | count++; 24 | } 25 | } 26 | //Serial.println("delay loop en sortant"); 27 | //Serial.println(delay_loop); 28 | wdt_disable(); 29 | String screen_template = "korev"; 30 | parse_screen_template(screen_template,array_data,last_seen_ms); 31 | //delay(5000); 32 | //displaydata(array_data,last_seen_ms); 33 | 34 | return(delay_loop); 35 | } -------------------------------------------------------------------------------- /src/templatescreen.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "tools.h" 5 | #include 6 | #include 7 | #define ST7735_GRAY 0x8410 8 | #define ST7735_LIME 0x07FF 9 | #define ST7735_AQUA 0x04FF 10 | #define ST7735_PINK 0xF8FF 11 | #define FreeSans9 "FreeSans20" 12 | //#define FreeSansGras9 "FreeSansGras9" 13 | #define FreeSans12 "FreeSans12" 14 | #define Arial9 "Arial9" 15 | #define Arial12 "Arial12" 16 | #define Arial20 "Arial20" 17 | #define RThin9 "RThin9" 18 | #define RThin12 "RThin12" 19 | #define RThin20 "RThin20" 20 | #define RBold20 "RBold20" 21 | #define TMS10 "TMS10" 22 | #define TMS12 "TMS12" 23 | #define FreeSansGras12 "SegLight20" 24 | #define SegLight20 "SegLight20" 25 | #include 26 | #include 27 | #define FlashFS LittleFS 28 | // void displaydata(String array_data[10],int last_seen_ms,String model); 29 | void parse_screen_template(String screen_template,String array_data[10],int last_seen_ms); 30 | void handle_global(String global_json); 31 | void handle_line(String line_json); 32 | void make_text_line(JsonObject text_line_json); 33 | void make_line_line(JsonObject line_line_json); 34 | char* get_font(String font); 35 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /docs/MakeRelease.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | build_root = "C:\\Users\\noutrey\\Documents\\Perso\\PlatformIO\\Projects\\iSpindHub\\.pio\\build\\" 5 | releases_root = "C:\\Users\\noutrey\\Documents\\Perso\\Brassage\\iSpindHub\\Releases" 6 | release_index = "0.0.7" 7 | 8 | list_firmwares = os.scandir(build_root) 9 | for el in list_firmwares: 10 | if not(el.name == 'project.checksum' ): 11 | rel_name = el.name 12 | # Build All FileSystems 13 | base_cmd = 'C:\\Users\\noutrey\\.platformio\\penv\\Scripts\\platformio.exe run --target buildfs --environment ' + rel_name 14 | os.system(base_cmd) 15 | #Get FW File, Copy it to release dir and rename it 16 | fw_file_source = build_root + rel_name + "\\firmware.bin" 17 | 18 | fw_file_target = releases_root + "\\" + release_index + "\\" + rel_name + "_fw.bin" 19 | shutil.copy2(fw_file_source,fw_file_target) 20 | #Get FS File, Copy it to release dir and rename it 21 | fs_file_source = build_root + rel_name + "\\littlefs.bin" 22 | fs_file_target = releases_root + "\\" + release_index + "\\" + rel_name + "_littlefs.bin" 23 | shutil.copy2(fs_file_source,fs_file_target) 24 | print(el) 25 | 26 | -------------------------------------------------------------------------------- /data/data/templates/default_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "back_color": "TFT_BLACK" , 4 | "font_color": "TFT_BLUE", 5 | "text_wrap" : false 6 | 7 | }, 8 | "line": { 9 | "type": "text", 10 | "font": "", 11 | "color": "", 12 | "x": 0, 13 | "y": 18, 14 | "center": true, 15 | "text": "SG: #SG" 16 | }, 17 | "line": { 18 | "type": "text", 19 | "font": "FreeSans9", 20 | "color": "TFT_BLUE", 21 | "x": 87, 22 | "y": 50, 23 | "center": true, 24 | "text": "Voltage: #Voltage" 25 | }, 26 | "line 2": { 27 | "type": "variable_text", 28 | "font": "FreeSansGras12", 29 | "variable" : "#RSSI", 30 | "colors": { 31 | "65": "TFT_GREEN", 32 | "70": "TFT_YELLOW", 33 | "80": "TFT_ORANGE", 34 | "default": "TFT_RED" 35 | }, 36 | "x": 0, 37 | "y": 18, 38 | "center": true, 39 | "text": "Signal: #RSSI dB" 40 | }, 41 | "line 3": { 42 | "type": "rectangle", 43 | "color": "TFT_MAGENTA", 44 | "x_0": 0, 45 | "y_0": 0, 46 | "x_1": 128, 47 | "y_1": 128 48 | }, 49 | "line 4": { 50 | "type": "line", 51 | "color": "TFT_YELLOW", 52 | "x_0": 0, 53 | "y_0": 0 54 | } 55 | } -------------------------------------------------------------------------------- /src/web.h: -------------------------------------------------------------------------------- 1 | #ifndef WEB_H 2 | #define WEB_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #define WEBSERVER_H 10 | #include 11 | // #include 12 | #include "screen.h" 13 | #include 14 | #include "ntp.h" 15 | #include 16 | #include 17 | #include "wifi.h" 18 | #include "uptime.h" 19 | #ifndef USE_LITTLEFS 20 | #define USE_LITTLEFS 21 | #endif 22 | void initWebServer(); 23 | void setRegPageAliases(); 24 | void setActionPageHandlers(); 25 | void setJsonHandlers(); 26 | void setSettingsAliases(); 27 | 28 | bool handleiSpindHubPost(AsyncWebServerRequest *request); 29 | bool handleURLTargetPost(AsyncWebServerRequest *request); 30 | bool handleBrewfatherTargetPost(AsyncWebServerRequest *request); 31 | bool handleBPiLessPost(AsyncWebServerRequest *request); 32 | 33 | #define LOG_LEVEL LOG_LEVEL_VERBOSE 34 | extern struct Config config; 35 | extern const size_t capacityDeserial; 36 | extern const size_t capacitySerial; 37 | extern const char *resetReason[7]; 38 | extern const char *resetDescription[7]; 39 | 40 | bool saveConfig(); 41 | bool saveFile(); 42 | bool serializeConfig(Print &); 43 | bool deserializeConfig(Stream &); 44 | #define stringify(s) _stringifyDo(s) 45 | #define _stringifyDo(s) #s 46 | 47 | const char *build(); 48 | const char *branch(); 49 | const char *version(); 50 | 51 | #endif // -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /src/uptime.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2021 Lee C. Bussy (@LBussy) 2 | 3 | This file is part of Lee Bussy's Brew Bubbles (brew-bubbles). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. */ 22 | 23 | #ifndef _UPTIME_H 24 | #define _UPTIME_H 25 | 26 | #include 27 | 28 | #define UPTIME_REFRESH 1 29 | 30 | #define DAY_MILLIS 86400000 31 | #define HOUR_MILLIS 3600000 32 | #define MIN_MILLIS 60000 33 | #define SEC_MILLIS 1000 34 | 35 | void getNow(); 36 | void setValues(); 37 | const int uptimeDays(bool refr = false); 38 | const int uptimeHours(bool refr = false); 39 | const int uptimeMinutes(bool refr = false); 40 | const int uptimeSeconds(bool refr = false); 41 | const int uptimeMillis(bool refr = false); 42 | 43 | #endif // _UPTIME_H 44 | -------------------------------------------------------------------------------- /src/jsonconfig.h: -------------------------------------------------------------------------------- 1 | #ifndef _JSONCONFIG_H 2 | #define _JSONCONFIG_H 3 | #define ARDUINOJSON_ENABLE_ARDUINO_STRING 1 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "config.h" 10 | 11 | struct iSpindHub 12 | { 13 | // Stores iSpindHub configuration 14 | char name[32]; 15 | char TZ[32]; 16 | void load(JsonObjectConst); 17 | void save(JsonObject) const; 18 | }; 19 | 20 | struct ApConfig 21 | { 22 | // Stores Access Point configuration 23 | char ssid[32]; 24 | char passphrase[64]; 25 | 26 | void load(JsonObjectConst); 27 | void save(JsonObject) const; 28 | }; 29 | 30 | struct URLTarget 31 | { 32 | // Stores URL Target configuration 33 | char url[128]; 34 | int freq; 35 | bool update; 36 | 37 | void load(JsonObjectConst); 38 | void save(JsonObject) const; 39 | }; 40 | 41 | struct KeyTarget 42 | { 43 | // Stores Key Target configurations 44 | char key[64]; 45 | int channel; 46 | int freq; 47 | bool update; 48 | 49 | void load(JsonObjectConst); 50 | void save(JsonObject) const; 51 | }; 52 | 53 | struct Config 54 | { 55 | // Stores the complete configuration 56 | char hostname[32]; 57 | iSpindHub ispindhub; 58 | ApConfig apconfig; 59 | URLTarget urltarget; 60 | URLTarget bpiless; 61 | KeyTarget brewersfriend; 62 | KeyTarget brewfather; 63 | bool nodrd; 64 | 65 | void load(JsonObjectConst); 66 | void save(JsonObject) const; 67 | }; 68 | 69 | bool deleteConfigFile(); 70 | bool loadConfig(); 71 | bool saveConfig(); 72 | bool loadFile(); 73 | bool saveFile(); 74 | bool printConfig(); 75 | bool printFile(); 76 | bool serializeConfig(Print &); 77 | bool deserializeConfig(Stream &); 78 | bool merge(JsonVariant, JsonVariantConst); 79 | bool mergeJsonObject(JsonVariantConst); 80 | bool mergeJsonString(String); 81 | 82 | #endif // _JSONCONFIG_H -------------------------------------------------------------------------------- /data/data/templates/korev.json: -------------------------------------------------------------------------------- 1 | { 2 | "g": { 3 | "bc": "TFT_BLACK", 4 | "fc": "TFT_BLUE", 5 | "tw": false 6 | 7 | }, 8 | "line 1": { 9 | "t": "text", 10 | "f": "SegLight20", 11 | "c": "TFT_YELLOW", 12 | "x": 0, 13 | "y": 6, 14 | "ctr": true, 15 | "text": "#SG °P" 16 | }, 17 | "line 2": { 18 | "t": "text", 19 | "f": "SegLight20", 20 | "c": "TFT_RED", 21 | "x": 0, 22 | "y": 26, 23 | "ctr": true, 24 | "text": "T°: #Temp °" 25 | }, 26 | "line 3": { 27 | "t": "text", 28 | "f": "SegLight20", 29 | "c": "TFT_LIGHTGREY", 30 | "x": 0, 31 | "y": 48, 32 | "ctr": true, 33 | "text": "#Angle ° / #VoltageV" 34 | }, 35 | "line 4": { 36 | "t": "text", 37 | "f": "", 38 | "s": 1.5, 39 | "c": "TFT_WHITE", 40 | "x": 0, 41 | "y": 74, 42 | "ctr": true, 43 | "text": "#deviceName" 44 | }, 45 | "line 5": { 46 | "t": "text", 47 | "f": "", 48 | "s": 1, 49 | "c": "TFT_ORANGE", 50 | "x": 0, 51 | "y": "MAX", 52 | "y_delta" : -40, 53 | "ctr": true, 54 | "text": "Last Seen" 55 | }, 56 | "line 6": { 57 | "t": "text", 58 | "f": "", 59 | "s": 1, 60 | "c": "TFT_ORANGE", 61 | "x": 0, 62 | "y": "MAX", 63 | "y_delta" : -30, 64 | "ctr": true, 65 | "text": "#LastSeen" 66 | }, 67 | "line 7": { 68 | "t": "text", 69 | "f": "", 70 | "s": 1, 71 | "var": "#RSSI", 72 | "cs": [{ 73 | "val": 65, 74 | "col": "TFT_GREEN" 75 | }, 76 | { 77 | "val": 70, 78 | "col": "TFT_YELLOW" 79 | }, 80 | { 81 | "val": 85, 82 | "col": "TFT_ORANGE" 83 | } 84 | ], 85 | "def_col": "TFT_RED", 86 | "x": 0, 87 | "y": "MAX", 88 | "y_delta" : -20, 89 | "ctr": true, 90 | "text": "Signal: #RSSI dB" 91 | }, 92 | "line ": { 93 | "t": "text", 94 | "f": "", 95 | "s": 1, 96 | "c": "TFT_WHITE", 97 | "x": 0, 98 | "y": "MAX", 99 | "y_delta" : -10, 100 | "ctr": true, 101 | "text": "IP: #IP" 102 | } 103 | 104 | 105 | } -------------------------------------------------------------------------------- /data/data/templates/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "g": { 3 | "bc": "TFT_BLACK", 4 | "fc": "TFT_BLUE", 5 | "tw": false 6 | 7 | }, 8 | "line 1": { 9 | "t": "text", 10 | "f": "RBold20", 11 | "c": "TFT_BLUE", 12 | "x": 0, 13 | "y": 0, 14 | "ctr": true, 15 | "text": "SG: #SG" 16 | }, 17 | "line 2": { 18 | "t": "line", 19 | "c": "TFT_CYAN", 20 | "x_0": 1, 21 | "y_0": 20, 22 | "x_1": "MAX", 23 | "y_1": 20 24 | 25 | }, 26 | "line 3": { 27 | "t": "text", 28 | "f": "Arial20", 29 | "c": "TFT_YELLOW", 30 | "x": 0, 31 | "y": 21, 32 | "ctr": true, 33 | "text": "T°: #Temp °" 34 | }, 35 | "line 4": { 36 | "t": "line", 37 | "c": "TFT_CYAN", 38 | "x_0": 1, 39 | "y_0": 36, 40 | "x_1": "MAX", 41 | "y_1": 36 42 | 43 | }, 44 | "line 5": { 45 | "t": "text", 46 | "f": "Arial20", 47 | "c": "TFT_MAGENTA", 48 | "x": 0, 49 | "y": 38, 50 | "ctr": true, 51 | "text": "#Angle ° / #VoltageV" 52 | }, 53 | "line 6": { 54 | "t": "line", 55 | "c": "TFT_CYAN", 56 | "x_0": 1, 57 | "y_0": 55, 58 | "x_1": "MAX", 59 | "y_1": 55 60 | 61 | }, 62 | "line 7": { 63 | "t": "text", 64 | "f": "", 65 | "s": 1, 66 | "var": "#RSSI", 67 | "cs": [{ 68 | "val": 65, 69 | "col": "TFT_GREEN" 70 | }, 71 | { 72 | "val": 70, 73 | "col": "TFT_YELLOW" 74 | }, 75 | { 76 | "val": 85, 77 | "col": "TFT_ORANGE" 78 | } 79 | ], 80 | "def_col": "TFT_RED", 81 | "x": 0, 82 | "y": "MAX", 83 | "y_delta" : -20, 84 | "ctr": true, 85 | "text": "Signal: #RSSI dB" 86 | }, 87 | "line 8": { 88 | "t": "rectangle", 89 | "c": "TFT_MAGENTA", 90 | "x_0": 0, 91 | "y_0": 0, 92 | "x_1": 128, 93 | "y_1": 128 94 | }, 95 | "line 9": { 96 | "t": "line", 97 | "c": "TFT_YELLOW", 98 | "x_0": 0, 99 | "y_0": 55, 100 | "x_1": "MAX", 101 | "y_1": 55 102 | }, 103 | "line10": { 104 | "t": "text", 105 | "f": "RThin20", 106 | "c": "TFT_RED", 107 | "x": 0, 108 | "y": 69, 109 | "ctr": true, 110 | "text": "#deviceName" 111 | } 112 | } -------------------------------------------------------------------------------- /src/pushtarget.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2021 Lee C. Bussy (@LBussy) 2 | 3 | This file is part of Lee Bussy's Brew Bubbles (brew-bubbles). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. */ 22 | 23 | #ifndef _PUSHTARGET_H 24 | #define _PUSHTARGET_H 25 | 26 | #include 27 | 28 | struct pushPoint 29 | { 30 | bool enabled; // Whether to send or not 31 | char name[129]; // What to call the data point 32 | }; 33 | 34 | class PushTarget 35 | { 36 | private: 37 | public: 38 | PushTarget(){}; 39 | pushPoint target; // Target enabled and name 40 | pushPoint checkBody; // Check return body for success 41 | char url[129]; // URL of target 42 | pushPoint key; // API Key 43 | IPAddress ip; // Resolved address of target 44 | pushPoint apiName; // i.e. "Brew Bubbles" 45 | pushPoint bubName; // mDNS name i.e. "brewbubbles" 46 | pushPoint lastTime; // Time of last send 47 | pushPoint tempFormat; // F or C 48 | pushPoint ambientTemp; // Room or chamber temp 49 | pushPoint vesselTemp; // Brew temp 50 | pushPoint bpm; // Bubbles per minute 51 | }; 52 | 53 | #endif // _PUSHTARGET_H 54 | -------------------------------------------------------------------------------- /src/target.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _URLTARGET_H 3 | #define _URLTARGET_H 4 | 5 | #include "pushtarget.h" 6 | #include "pushhelper.h" 7 | #include "jsonconfig.h" 8 | #include 9 | #include 10 | #include 11 | 12 | class Target 13 | { 14 | private: 15 | // Singleton Declarations 16 | Target() {} 17 | static Target *single; 18 | // External Declarations 19 | PushTarget *target; 20 | // Private Methods 21 | 22 | // Private Properties 23 | 24 | ///////////////////////////////////////////////////////////////////// 25 | // Configure Target - Below are configuration items per target type 26 | ///////////////////////////////////////////////////////////////////// 27 | 28 | // Enable target and target name 29 | const bool target_enabled = true; 30 | String target_name = "URL Target"; 31 | // 32 | // Check return body for success 33 | const bool checkbody_enabled = false; 34 | String checkbody_name = ""; 35 | // 36 | // Turn JSON points on/off and provide JSON field name per target type 37 | // 38 | const bool apiname_enabled = true; 39 | String apiname_name = "api_name"; 40 | // 41 | const bool bubname_enabled = true; 42 | String bubname_name = "name"; 43 | // 44 | const bool bpm_enabled = true; 45 | String bpm_name = "bpm"; 46 | // 47 | const bool ambienttemp_enabled = true; 48 | String ambienttemp_name = "ambient"; 49 | // 50 | const bool vesseltemp_enabled = true; 51 | String vesseltemp_name = "temp"; 52 | // 53 | const bool tempformat_enabled = true; 54 | String tempformat_name = "temp_unit"; 55 | // 56 | // Main URL for endpoint 57 | String targeturl = ""; 58 | // 59 | const bool apikey_enabled = true; 60 | String apikey_name = ""; // Will pick this up from config 61 | 62 | ///////////////////////////////////////////////////////////////////// 63 | // Configure Target - Above are configuration items per target type 64 | ///////////////////////////////////////////////////////////////////// 65 | 66 | public: 67 | // Singleton Declarations 68 | static Target *getInstance(); 69 | ~Target() { single = NULL; } 70 | // External Declarations 71 | 72 | // Public Methods 73 | bool push(); 74 | // Public Properties 75 | }; 76 | 77 | extern struct Config config; 78 | 79 | #endif // _URLTARGET_H 80 | -------------------------------------------------------------------------------- /src/brewfather.h: -------------------------------------------------------------------------------- 1 | #ifndef _BREWFTARGET_H 2 | #define _BREWFTARGET_H 3 | 4 | #include "pushtarget.h" 5 | #include "pushhelper.h" 6 | #include "jsonconfig.h" 7 | #include 8 | #include 9 | #include 10 | 11 | class BrewfTarget 12 | { 13 | private: 14 | // Singleton Declarations 15 | BrewfTarget() {} 16 | static BrewfTarget *single; 17 | // External Declarations 18 | PushTarget *target; 19 | // Private Methods 20 | 21 | // Private Properties 22 | 23 | ///////////////////////////////////////////////////////////////////// 24 | // Configure Target - Below are configuration items per target type 25 | ///////////////////////////////////////////////////////////////////// 26 | 27 | // Enable target and target name 28 | const bool target_enabled = true; 29 | String target_name = "Brewfather"; 30 | // 31 | // Check return body for success 32 | const bool checkbody_enabled = true; 33 | String checkbody_name = "200"; 34 | // 35 | // Turn JSON points on/off and provide JSON field name per target type 36 | // 37 | const bool apiname_enabled = false; 38 | String apiname_name = ""; 39 | // 40 | const bool bubname_enabled = true; 41 | String bubname_name = "name"; 42 | // 43 | const bool bpm_enabled = true; 44 | String bpm_name = "bpm"; 45 | // 46 | const bool ambienttemp_enabled = true; 47 | String ambienttemp_name = "aux_temp"; 48 | // 49 | const bool vesseltemp_enabled = true; 50 | String vesseltemp_name = "temp"; 51 | // 52 | const bool tempformat_enabled = true; 53 | String tempformat_name = "temp_unit"; 54 | // 55 | // Main URL for endpoint 56 | String targeturl = "http://log.brewfather.net/stream?id="; 57 | // 58 | const bool apikey_enabled = true; 59 | String apikey_name = ""; // Will pick this up from config 60 | 61 | ///////////////////////////////////////////////////////////////////// 62 | // Configure Target - Above are configuration items per target type 63 | ///////////////////////////////////////////////////////////////////// 64 | 65 | public: 66 | // Singleton Declarations 67 | static BrewfTarget *getInstance(); 68 | ~BrewfTarget() { single = NULL; } 69 | // External Declarations 70 | 71 | // Public Methods 72 | bool push(); 73 | // Public Properties 74 | }; 75 | 76 | extern struct Config config; 77 | 78 | #endif // _BREWFTARGET_H 79 | -------------------------------------------------------------------------------- /docs/screens.md: -------------------------------------------------------------------------------- 1 | # All About Screens 2 | 3 | ## Why is there a screen format ? 4 | Instead of having people tring to find their way through my mucky code, I thought it'd be easier to have a screen format allowing for everybody to design the perfect screen layout for them. 5 | Here it is. 6 | 7 | ## Overall Format 8 | 9 | ### It's JSON DUH 10 | Sounded like the easiest way to have a clean structure and also I finally know how to parse JSON on an ESP8266 11 | 12 | ### The Structure 13 | - 2 types of entries 14 | - global 15 | Gives generic information about the screen (orientation, background color,fonts etc) 16 | - lines 17 | Doesn't have to be A line but it's the best I could came up with. For those, there are 4 possible types (at the moment) that I'll detail just after. 18 | Lines do not have to be put in the order they will appear on the screen but it's easier I guess 19 | - For the lines, various variables are available 20 | - Specific Gravity #SG 21 | - Signal Strength in dB #RSSI 22 | - Temperature #Temp 23 | - Angle of the iSpindel #Angle 24 | - Voltage of the battery #Voltage 25 | - Name of the iSpindel #deviceName 26 | - Duration since last seen #LastSeen 27 | - Local IP adress of the iSpindHub #IP 28 | 29 | #### Global 30 | - In here you'll define everyting that'll be true for the whole screen 31 | - Background Color (bc) 32 | - Font Color (fc) 33 | - Whether or not the text is wrapped (tw) 34 | - Generic/Default Font (default_font) 35 | - Screen Orientation (r) 36 | 37 | #### Generic for Lines 38 | - When passing a position (x or y), you can use the keyword "MAX" and use x_delta or y_delta. This will replace the MAX par TFT_WIDTH and TFT_HEIGHT respectfully and apply the delta to it (say you want it 20 pixels from the bottom, you'll write "y" : "MAX" and "y_delta" : -20 in the JSON Template) 39 | - Same applies for lines (2 points so x_0/y_0 and x_1,y_1) 40 | 41 | #### Text Lines 42 | - To define a text line you need : 43 | - a font (f) 44 | - Can be empty to use System font 45 | - a color (c) 46 | - can be empty 47 | - a position (x and y) 48 | - a centered (ctr) attribute 49 | - a text (obviously) 50 | - You can have the color changing based on values of your variable. 51 | - You need a var key in which you put the name of the variable you want to use (ex : "var" : "#RSSI") 52 | - You need to build a "cs" entry that list in order the 3 couples threshold/color you want to use (low/medium/high) 53 | - A couple is defined by a "val" and a "col" attribute 54 | - You need a default color (def_col) 55 | 56 | -------------------------------------------------------------------------------- /src/ntp.h: -------------------------------------------------------------------------------- 1 | #ifndef _NTP_H 2 | #define _NTP_H 3 | 4 | #ifdef ESP8266 5 | #include 6 | #elif defined ESP32 7 | #include 8 | #endif 9 | 10 | #include 11 | #include "jsonconfig.h" 12 | 13 | void setClock(); 14 | String getDTS(); 15 | int getYear(); // tm_year 16 | int getMonth(); // tm_mon 17 | int getDate(); // tm_mday 18 | int getWday(); // tm_wday 19 | int getHour(); // tm_hour 20 | int getMinute(); // tm_min 21 | int getSecond(); // tm_sec 22 | int getYDay(); // tm_yday 23 | void ntpBlinker(); 24 | 25 | static const float __attribute__((unused)) GMT = 0; 26 | static const float __attribute__((unused)) UTC = 0; 27 | static const float __attribute__((unused)) ECT = 1.00; 28 | static const float __attribute__((unused)) EET = 2.00; 29 | static const float __attribute__((unused)) ART = 2.00; 30 | static const float __attribute__((unused)) EAT = 3.00; 31 | static const float __attribute__((unused)) MET = 3.30; 32 | static const float __attribute__((unused)) NET = 4.00; 33 | static const float __attribute__((unused)) PLT = 5.00; 34 | static const float __attribute__((unused)) IST = 5.30; 35 | static const float __attribute__((unused)) BST = 6.00; 36 | static const float __attribute__((unused)) VST = 7.00; 37 | static const float __attribute__((unused)) CTT = 8.00; 38 | static const float __attribute__((unused)) JST = 9.00; 39 | static const float __attribute__((unused)) ACT = 9.30; 40 | static const float __attribute__((unused)) AET = 10.00; 41 | static const float __attribute__((unused)) SST = 11.00; 42 | static const float __attribute__((unused)) NST = 12.00; 43 | static const float __attribute__((unused)) MIT = -11.00; 44 | static const float __attribute__((unused)) HST = -10.00; 45 | static const float __attribute__((unused)) AST = -9.00; 46 | static const float __attribute__((unused)) PST = -8.00; 47 | static const float __attribute__((unused)) PNT = -7.00; 48 | static const float __attribute__((unused)) MST = -7.00; 49 | static const float __attribute__((unused)) CST = -6.00; 50 | static const float __attribute__((unused)) EST = -5.00; 51 | static const float __attribute__((unused)) IET = -5.00; 52 | static const float __attribute__((unused)) PRT = -4.00; 53 | static const float __attribute__((unused)) CNT = -3.30; 54 | static const float __attribute__((unused)) AGT = -3.00; 55 | static const float __attribute__((unused)) BET = -3.00; 56 | static const float __attribute__((unused)) CAT = -1.00; 57 | 58 | static const int __attribute__((unused)) EPOCH_1_1_2019 = 1546300800; //1546300800 = 01/01/2019 @ 12:00am (UTC) 59 | static const char __attribute__((unused)) *DAYS_OF_WEEK[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; 60 | static const char __attribute__((unused)) *DAYS_OF_WEEK_3[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; 61 | extern struct Config config; 62 | #endif // _NTP_H -------------------------------------------------------------------------------- /src/tools.cpp: -------------------------------------------------------------------------------- 1 | #include "tools.h" 2 | //extern Adafruit_ST7735 tft; 3 | extern TFT_eSPI tft; 4 | void _delay(unsigned long ulDelay) { 5 | // Safe semi-blocking delay 6 | #ifdef ESP32 7 | vTaskDelay(ulDelay); // Builtin to ESP32 8 | #elif defined ESP8266 9 | unsigned long ulNow = millis(); 10 | unsigned long ulThen = ulNow + ulDelay; 11 | while (ulThen > millis()) { 12 | yield(); // ESP8266 needs to yield() 13 | } 14 | #endif 15 | } 16 | 17 | void centerString(String buf, int x, int y){ 18 | //int16_t x1, y1; 19 | uint16_t w, h; 20 | //tft.getTextBounds(buf, x, y, &x1, &y1, &w, &h); //calc width of new string 21 | w = tft.textWidth(buf); 22 | //Serial.println(buf); 23 | //Serial.println(tft.fontHeight()); 24 | /* 25 | Serial.println("x" + (String)x); 26 | Serial.println("y" + (String)y); 27 | Serial.println("w" + (String)w); 28 | Serial.print(w); 29 | */ 30 | tft.setCursor(x - (w / 2), y); 31 | tft.print(buf); 32 | return; 33 | } 34 | 35 | String pretty_time(long val){ 36 | int days = elapsedDays(val); 37 | int hours = numberOfHours(val); 38 | int minutes = numberOfMinutes(val); 39 | int seconds = numberOfSeconds(val); 40 | 41 | // digital clock display of current time 42 | //Serial.print(days,DEC); 43 | //printDigits(hours); 44 | //printDigits(minutes); 45 | //printDigits(seconds); 46 | //Serial.println(); 47 | // Build the string for "last seen" 48 | String last_seen = ""; 49 | if (days > 0){ 50 | last_seen = last_seen + days; 51 | last_seen = last_seen + "d,"; 52 | } 53 | if (hours > 0){ 54 | last_seen = last_seen + hours; 55 | last_seen = last_seen + "h,"; 56 | } 57 | if (minutes > 0){ 58 | last_seen = last_seen + minutes; 59 | last_seen = last_seen + + "m,"; 60 | } 61 | if (seconds < 10){ 62 | last_seen = last_seen + "0"; 63 | last_seen = last_seen + seconds; 64 | } 65 | else { 66 | last_seen = last_seen + seconds; 67 | } 68 | last_seen = last_seen + "s ago"; 69 | 70 | return last_seen; 71 | } 72 | 73 | void printDigits(byte digits){ 74 | // utility function for digital clock display: prints colon and leading 0 75 | Serial.print(":"); 76 | if(digits < 10) 77 | Serial.print('0'); 78 | Serial.print(digits,DEC); 79 | } 80 | 81 | 82 | String get_last_value(String iSpinData){ 83 | int str_len = iSpinData.length() +1; 84 | //char charData[str_len]; 85 | //iSpinData.toCharArray(charData,str_len); 86 | //Split by the \r char 87 | int line_len = iSpinData.indexOf("\r"); 88 | //char* splitData = strtok(charData,"\r"); 89 | //int line_len = strlen(splitData); 90 | // Get to the LAST record 91 | Serial.println("Position, Longueur "); 92 | Serial.println(str_len); 93 | Serial.println(line_len); 94 | String lastData = iSpinData.substring(str_len-line_len,str_len); 95 | return lastData; 96 | } 97 | 98 | uint32_t get_color(String colorString){ 99 | if (colorString =="TFT_BLACK"){ 100 | return TFT_BLACK; 101 | } 102 | else if (colorString =="TFT_GREEN"){ 103 | return TFT_GREEN; 104 | } 105 | else if (colorString =="TFT_BLUE"){ 106 | return TFT_BLUE; 107 | } 108 | else if (colorString =="TFT_WHITE"){ 109 | return TFT_WHITE; 110 | } 111 | else if (colorString =="TFT_RED"){ 112 | return TFT_RED; 113 | } 114 | else if (colorString =="TFT_YELLOW"){ 115 | return TFT_YELLOW; 116 | } 117 | else if (colorString =="TFT_CYAN"){ 118 | return TFT_CYAN; 119 | } 120 | else if (colorString =="TFT_MAGENTA"){ 121 | return TFT_MAGENTA; 122 | } 123 | else if (colorString =="TFT_LIGHTGREY"){ 124 | return TFT_LIGHTGREY; 125 | } 126 | else if (colorString =="TFT_ORANGE"){ 127 | return TFT_ORANGE; 128 | } 129 | else { 130 | return TFT_WHITE; 131 | } 132 | 133 | } 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/ntp.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "ntp.h" 3 | 4 | void setClock() { 5 | Log.notice(F("Entering blocking loop to get NTP time.")); 6 | char* TMZ = config.ispindhub.TZ; 7 | Log.notice(TMZ); 8 | Serial.println(TMZ); 9 | if (strlen(TMZ) == 0) 10 | { 11 | TMZ = "CET"; 12 | } 13 | Serial.println(TMZ); 14 | Log.notice("Time Zone used is %", TMZ); 15 | configTime(TMZ,"pool.ntp.org", "time.nist.gov"); 16 | time_t nowSecs = time(nullptr); 17 | time_t startSecs = time(nullptr); 18 | int cycle = 0; 19 | while (nowSecs < EPOCH_1_1_2019) { 20 | if (nowSecs - startSecs > 9) { 21 | if (cycle > 9) { 22 | Log.warning(F("Unable to get time hack from %s, rebooting." CR)); 23 | ESP.restart(); 24 | } 25 | #ifdef LOG_LEVEL 26 | Serial.println(); 27 | #endif 28 | Log.verbose(F("Re-requesting time hack.")); 29 | configTime(TMZ, "pool.ntp.org", "time.nist.gov"); 30 | startSecs = time(nullptr); 31 | cycle++; 32 | } 33 | #ifdef LOG_LEVEL 34 | Serial.print(F(".")); 35 | #endif 36 | delay(1000); 37 | yield(); 38 | nowSecs = time(nullptr); 39 | } 40 | #ifdef LOG_LEVEL 41 | Serial.println(); 42 | #endif 43 | Log.notice(F("NTP time set." CR)); 44 | struct tm timeinfo; 45 | gmtime_r(&nowSecs, &timeinfo); 46 | } 47 | 48 | String getDTS() { 49 | // Returns JSON-type string = 2019-12-20T13:59:39Z 50 | /// Also: 51 | // sprintf(dts, "%04u-%02u-%02uT%02u:%02u:%02uZ", getYear(), getMonth(), getDate(), getHour(), getMinute(), getSecond()); 52 | time_t now; 53 | time_t rawtime = time(&now); 54 | struct tm ts; 55 | ts = *localtime(&rawtime); 56 | char dta[21] = {'\0'}; 57 | strftime(dta, sizeof(dta), "%FT%TZ", &ts); 58 | String dateTimeString = String(dta); 59 | return dateTimeString; 60 | } 61 | 62 | int getYear() { 63 | // tm_year = years since 1900 64 | time_t rawtime; 65 | struct tm * ts; 66 | time ( &rawtime ); 67 | ts = gmtime ( &rawtime ); 68 | int year = 1900 + ts->tm_year; 69 | return year; 70 | } 71 | 72 | int getMonth() { 73 | // tm_mon = months since January (0-11) 74 | time_t rawtime; 75 | struct tm * ts; 76 | time ( &rawtime ); 77 | ts = gmtime ( &rawtime ); 78 | int month = ts->tm_mon; 79 | return month; 80 | } 81 | 82 | int getDate() { 83 | // tm_mday = day of the month (1-31) 84 | time_t rawtime; 85 | struct tm * ts; 86 | time ( &rawtime ); 87 | ts = gmtime ( &rawtime ); 88 | int day = ts->tm_mday; 89 | return day; 90 | } 91 | 92 | int getWday() { 93 | // tm_wday = days since Sunday (0-6) 94 | time_t rawtime; 95 | struct tm * ts; 96 | time ( &rawtime ); 97 | ts = gmtime ( &rawtime ); 98 | int wday = 1 + ts->tm_wday; 99 | return wday; 100 | } 101 | 102 | int getHour() { 103 | // tm_hour = hours since midnight (0-23) 104 | time_t rawtime; 105 | struct tm * ts; 106 | time ( &rawtime ); 107 | ts = gmtime ( &rawtime ); 108 | int hour = ts->tm_hour; 109 | return hour; 110 | } 111 | 112 | int getMinute() { 113 | // tm_min = minutes after the hour (0-59) 114 | time_t rawtime; 115 | struct tm * ts; 116 | time ( &rawtime ); 117 | ts = gmtime ( &rawtime ); 118 | int min = ts->tm_min; 119 | return min; 120 | } 121 | 122 | int getSecond() { 123 | // tm_sec = seconds after the minute (0-60) 124 | time_t rawtime; 125 | struct tm * ts; 126 | time ( &rawtime ); 127 | ts = gmtime ( &rawtime ); 128 | int sec = ts->tm_sec; 129 | return sec; 130 | } 131 | 132 | int getYDay() { 133 | // tm_yday = days since January 1 (0-365) 134 | time_t rawtime; 135 | struct tm * ts; 136 | time ( &rawtime ); 137 | ts = gmtime ( &rawtime ); 138 | int yday = ts->tm_yday; 139 | return yday; 140 | } -------------------------------------------------------------------------------- /src/uptime.cpp: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2021 Lee C. Bussy (@LBussy) 2 | 3 | This file is part of Lee Bussy's Brew Bubbles (brew-bubbles). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. */ 22 | 23 | #include "uptime.h" 24 | 25 | static int refresh = UPTIME_REFRESH * 1000; 26 | static unsigned long uptimeNow; 27 | static int days; 28 | static int hours; 29 | static int minutes; 30 | static int seconds; 31 | static int mills; 32 | 33 | void getNow() 34 | { 35 | // Set the uptime values if refresh time is expired 36 | if ((int)(millis() - uptimeNow) > refresh) 37 | { 38 | setValues(); 39 | } 40 | // Reset timer for another period to avoid a really unlikely situation 41 | // where the timer expires in between grabbing two parts 42 | uptimeNow = millis(); 43 | } 44 | 45 | void setValues() 46 | { 47 | // Call this only by getNow() 48 | // Using refr = true forces recalculation 49 | uptimeNow = millis(); 50 | days = uptimeDays(true); 51 | hours = uptimeHours(true); 52 | minutes = uptimeMinutes(true); 53 | seconds = uptimeSeconds(true); 54 | mills = uptimeMillis(true); 55 | } 56 | 57 | const int uptimeDays(bool refr) 58 | { 59 | getNow(); // Make sure we are current 60 | if (refr) 61 | { 62 | // Calculate full days from uptime 63 | days = (int)floor(uptimeNow / DAY_MILLIS); 64 | } 65 | return days; 66 | } 67 | 68 | const int uptimeHours(bool refr) 69 | { 70 | getNow(); // Make sure we are current 71 | if (refr) 72 | { 73 | // Refresh values: 74 | // Subtract millis value for any full days via modulo 75 | // Calculate full hours from remainder 76 | hours = (int)floor((uptimeNow % DAY_MILLIS) / HOUR_MILLIS); 77 | } 78 | return hours; 79 | } 80 | 81 | const int uptimeMinutes(bool refr) 82 | { 83 | getNow(); // Make sure we are current 84 | if (refr) 85 | { 86 | // Refresh values: 87 | // Subtract millis value for any full hours via modulo 88 | // Calculate full minutes from remainder 89 | minutes = (int)floor((uptimeNow % HOUR_MILLIS) / MIN_MILLIS); 90 | } 91 | return minutes; 92 | } 93 | 94 | const int uptimeSeconds(bool refr) 95 | { 96 | getNow(); // Make sure we are current 97 | if (refr) 98 | { 99 | // Refresh values: 100 | // Subtract millis value for any full minutes via modulo 101 | // Calculate full seconds from remainder 102 | seconds = (int)floor((uptimeNow % MIN_MILLIS) / SEC_MILLIS); 103 | } 104 | return seconds; 105 | } 106 | 107 | const int uptimeMillis(bool refr) 108 | { 109 | getNow(); // Make sure we are current 110 | if (refr) 111 | { 112 | // Refresh values: 113 | // Subtract millis value for any full seconds via modulo 114 | // Return remainder millis 115 | mills = (int)floor((uptimeNow % SEC_MILLIS)); 116 | } 117 | return mills; 118 | } 119 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | **iSpindHub Documentation** 2 | 3 | (Loads of information is coming from info put together by [korev on the HBT Forum](https://www.homebrewtalk.com/threads/ispindle-concentrators-repeaters-bridges-hubs.693125/post-9237012) Many thanks for the time he took to put it together ) 4 | 5 | iSpindHub is a relay/repeater allowing to forward information to serveral cloud services to track your fermentation. On top of it, the screen is there to give a local display of the information coming from your iSpindels. 6 | 7 | 8 | * 1. [1.1. iSpindHub Setup](#iSpindHubSetup) 9 | * 2. [1.2. iSpindHub Navigation](#iSpindHubNavigation) 10 | * 2.1. [ Home Screen](#HomeScreen) 11 | * 2.2. [ Settings](#Settings) 12 | * 2.2.1. [Base Settings](#BaseSettings) 13 | * 2.3. [ Targets](#Targets) 14 | * 2.3.1. [ BrewFather](#BrewFather) 15 | * 3. [1.3. iSpindel Setup](#iSpindelSetup) 16 | 17 | 21 | 22 | 23 | 24 | # 1. All you need to know to get started with iSpindHub 25 | - Build it 26 | - Flash FirmWare 27 | - Connect to Wifi and do Setup 28 | - SetUp your iSpindel to talk to iSpindHub 29 | - PROFIT (or something) 30 | 31 | ## 1. 1.1. iSpindHub Setup 32 | On first start (or as long as it is not connected to a WiFi network), the iSpindHub will broadcast a local AP (access Point) called iSpindHubConfig or iSpindHub. 33 | ![WiFi Networks showing the iSpindHub](iSpindHub.jpg) 34 | 35 | Connect to said WiFi then access ip 192.168.4.1 (if not offered to connect directly) 36 | You'll see a "WiFi Manager" page with a Configure WiFi Option 37 | ![First Config Page](iSpindHubConf1.jpg) 38 | 39 | Pick it 40 | ![Proper configuration Page](iSpindHubConf2.jpg) 41 | 42 | Chose a WiFi Network, enter the password for it and save. 43 | Should be enough. 44 | If there is no DHCP on your network, then you'll have to set up the gateway IP, the SubNet as well as the static DNS. 45 | 46 | Once it restarts, you should now have both an AP call iSpindHub and an iSpindHub on your network. 47 | Ip to connect to it should be on the last line of the display 48 | ![iSpindHub Display with default screen and dummy info](iSpindHubDisplay.jpg) 49 | 50 | ## 2. 1.2. iSpindHub Navigation 51 | ### 2.1. Home Screen 52 | When you connect to the iSpindHub, you see a list of logs from iSpindels communicating (or having communicated) with the iSpindHub. 53 | ![HomePage](HomePageiSpindHub.jpg) 54 | 55 | Clicking on the trash bin will delete the log file. 56 | Clicking on the logfile name will try and download it (**==not working for the moment==**) 57 | 58 | ### 2.2. Settings 59 | Landing gives some explanation and presents you with 3 tabs 60 | ![Settings Landing page](SettingsHome.jpg) 61 | #### 2.2.1. Base Settings 62 | 63 | This allows to change the iSpindHub ID (hence the broadcast Access Point) as well as the Time Zone for the iSpindHub. 64 | You need to click Update (**==and to reboot for the time being, working on it==**) 65 | ![iSdpinHub Base Settings](iSpindHubBaseSettings.jpg) 66 | 67 | ### 2.3. Targets 68 | ==For the time being, only BrewFather is working== 69 | #### 2.3.1. BrewFather 70 | Easy settings. Enter your BrewFather key, enter the frequency you want the information to be pushed to BrewFather in minutes, hit Update and you're good to go. 71 | ![BrewFather Target Settings](BrewFatherTargetSettings.jpg) 72 | 73 | ## 3. 1.3. iSpindel Setup 74 | - Put you iSpindel in configuration mode 75 | - Get onto the iSpindel network 76 | - You should see an iSpindHub (or however you renamed it) network 77 | - Connect your iSpindel to it 78 | - set ssid for iSpindHub (or whatever the SSID is) 79 | - No Password (==Work in Progress==) 80 | - Use the HTTP Protocol 81 | - Token : empty 82 | - Port : 80 83 | - IP to use : 192.168.4.1 84 | - Path : /ispindel 85 | 86 | Save and you're good to go. 87 | -------------------------------------------------------------------------------- /src/cronpush.cpp: -------------------------------------------------------------------------------- 1 | #include "cronpush.h" 2 | // Recurring Jobs Functions 3 | void pushBrewFather() 4 | { // Check if we have a BF Key 5 | if (config.brewfather.key != "") 6 | { 7 | 8 | // create payload 9 | StaticJsonDocument<400> payload; 10 | FSInfo fs_info; 11 | LittleFS.info(fs_info); 12 | Dir dir = LittleFS.openDir("/data"); 13 | //const char *bf_id = "SYuAmrrHxyGMsx"; 14 | String bf_id = config.brewfather.key; 15 | String bf_server = "http://log.brewfather.net/ispindel?id="; 16 | String bf_url; 17 | bf_url = bf_server + bf_id; 18 | Serial.println(bf_url); 19 | WiFiClient client; 20 | HTTPClient http; 21 | 22 | while (dir.next()) 23 | { 24 | // PrOcessing One file at at time 25 | http.begin(client, bf_url); 26 | http.addHeader("Content-Type", "application/json"); 27 | http.addHeader("Cache-Control", "no-cache"); 28 | String json; 29 | String f_name = dir.fileName(); 30 | f_name.remove(f_name.length() - 4); 31 | // JsonObject f_name_json = payload.createNestedObject(f_name); 32 | if (dir.fileSize()) 33 | { 34 | File file = dir.openFile("r"); 35 | String temp = file.readStringUntil('\r'); 36 | int line_len = temp.length() + 1; 37 | //int file_size = file.size(); 38 | //float num_line = file_size / line_len; 39 | file.seek(line_len, SeekEnd); 40 | String lastData = file.readString(); 41 | file.close(); 42 | // Store Last Readings 43 | int str_len = lastData.length() + 1; 44 | int count = 0; 45 | int idx; 46 | int mov_idx = 0; 47 | String array_data[10] = {}; 48 | for (idx = 0; idx <= str_len; idx++) 49 | { 50 | if (lastData[idx] == ',') 51 | { // splitData[count] = lastData.substring(mov_idx,idx-1); 52 | array_data[count] = lastData.substring(mov_idx, idx); 53 | mov_idx = idx + 1; 54 | count++; 55 | } 56 | } 57 | /* array_data : 2021-05-02T12:12:22Z,iSpindel032,63.3900,18.26,4.2000,1.0560,900.0,-57.0,, 58 | 1 : Date created 59 | 2 : name 60 | 3 : Angle 61 | 4 : Temp 62 | 5 : Battery 63 | 6 : Gravity 64 | 7 : Frequency 65 | 8 : RSSI 66 | 9 : Temp Unit 67 | */ 68 | /*f_name_json["name"] = array_data[1]; 69 | f_name_json["ID"] = array_data[1]; 70 | f_name_json["angle"] = array_data[2]; 71 | f_name_json["temperature"] = array_data[3]; 72 | f_name_json["battery"] = array_data[4]; 73 | f_name_json["gravity"] = array_data[5]; 74 | f_name_json["interval"] = array_data[6]; 75 | f_name_json["RSSI"] = array_data[7]; 76 | */ 77 | payload["name"] = array_data[1]; 78 | payload["ID"] = array_data[1]; 79 | payload["angle"] = array_data[2]; 80 | payload["temperature"] = array_data[3]; 81 | payload["battery"] = array_data[4]; 82 | payload["gravity"] = array_data[5]; 83 | payload["interval"] = array_data[6]; 84 | payload["RSSI"] = array_data[7]; 85 | char buffer[900]; 86 | serializeJsonPretty(payload, buffer); 87 | Serial.println(buffer); 88 | serializeJson(payload, json); 89 | http.POST(json); 90 | // Read response 91 | Serial.print(http.getString()); 92 | delay(5000); 93 | http.end(); 94 | } 95 | } 96 | 97 | // Disconnect 98 | 99 | } 100 | else{ 101 | Serial.println("No BF Key, no Push"); 102 | } 103 | } -------------------------------------------------------------------------------- /src/resetreasons.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2021 Lee C. Bussy (@LBussy) 2 | 3 | This file is part of Lee Bussy's Brew Bubbles (brew-bubbles). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. */ 22 | 23 | #ifndef _RESETREASONS_H 24 | #define _RESETREASONS_H 25 | 26 | #if defined ESP8266 27 | const char *resetReason[7] = { 28 | "REASON_DEFAULT_RST", // = 0, /* normal startup by power on */ 29 | "REASON_WDT_RST", // = 1, /* hardware watch dog reset */ 30 | "REASON_EXCEPTION_RST", // = 2, /* exception reset, GPIO status won’t change */ 31 | "REASON_SOFT_WDT_RST", // = 3, /* software watch dog reset, GPIO status won’t change */ 32 | "REASON_SOFT_RESTART", // = 4, /* software restart ,system_restart , GPIO status won’t change */ 33 | "REASON_DEEP_SLEEP_AWAKE", // = 5, /* wake up from deep-sleep */ 34 | "REASON_EXT_SYS_RST" // = 6 /* external system reset */ 35 | }; 36 | 37 | const char *resetDescription[7] = { 38 | "Normal startup by power on", 39 | "Hardware watch dog reset", 40 | "Exception reset, GPIO status won’t change", 41 | "Software watch dog reset, GPIO status won’t change", 42 | "Software restart, system_restart, GPIO status won’t change", 43 | "Wake up from deep-sleep", 44 | "External system reset"}; 45 | 46 | #elif ESP32 47 | 48 | const char *resetReason[10] = { 49 | "ESP_RST_UNKNOWN", //!< Reset reason can not be determined 50 | "ESP_RST_POWERON", //!< Reset due to power-on event 51 | "ESP_RST_EXT", //!< Reset by external pin (not applicable for ESP32) 52 | "ESP_RST_SW", //!< Software reset via esp_restart 53 | "ESP_RST_PANIC", //!< Software reset due to exception/panic 54 | "ESP_RST_INT_WDT", //!< Reset (software or hardware) due to interrupt watchdog 55 | "ESP_RST_TASK_WDT", //!< Reset due to task watchdog 56 | "ESP_RST_WDT", //!< Reset due to other watchdogs 57 | "ESP_RST_DEEPSLEEP", //!< Reset after exiting deep sleep mode 58 | "ESP_RST_BROWNOUT", //!< Brownout reset (software or hardware) 59 | "ESP_RST_SDIO" //!< Reset over SDIO 60 | }; 61 | 62 | const char *resetDescription[10] = { 63 | "Reset reason can not be determined", 64 | "Reset due to power-on event", 65 | "Reset by external pin (not applicable for ESP32)", 66 | "Software reset via esp_restart", 67 | "Software reset due to exception/panic", 68 | "Reset (software or hardware) due to interrupt watchdog", 69 | "Reset due to task watchdog", 70 | "Reset due to other watchdogs", 71 | "Reset after exiting deep sleep mode", 72 | "Brownout reset (software or hardware)", 73 | "Reset over SDIO" 74 | }; 75 | 76 | typedef enum { 77 | ESP_RST_UNKNOWN, //!< Reset reason can not be determined 78 | ESP_RST_POWERON, //!< Reset due to power-on event 79 | ESP_RST_EXT, //!< Reset by external pin (not applicable for ESP32) 80 | ESP_RST_SW, //!< Software reset via esp_restart 81 | ESP_RST_PANIC, //!< Software reset due to exception/panic 82 | ESP_RST_INT_WDT, //!< Reset (software or hardware) due to interrupt watchdog 83 | ESP_RST_TASK_WDT, //!< Reset due to task watchdog 84 | ESP_RST_WDT, //!< Reset due to other watchdogs 85 | ESP_RST_DEEPSLEEP, //!< Reset after exiting deep sleep mode 86 | ESP_RST_BROWNOUT, //!< Brownout reset (software or hardware) 87 | ESP_RST_SDIO, //!< Reset over SDIO 88 | } esp_reset_reason_t; 89 | 90 | #endif 91 | 92 | #endif // _RESETREASONS_H 93 | -------------------------------------------------------------------------------- /src/pushhelper.cpp: -------------------------------------------------------------------------------- 1 | #include "pushhelper.h" 2 | 3 | IPAddress resolveHost(const char *hostname) 4 | { 5 | Log.verbose(F("Host lookup: %s." CR), hostname); 6 | IPAddress returnIP = INADDR_NONE; 7 | if (WiFi.hostByName(hostname, returnIP, 10000) == 0) 8 | { 9 | Log.error(F("Host lookup error." CR)); 10 | returnIP = INADDR_NONE; 11 | } 12 | return returnIP; 13 | } 14 | 15 | bool pushToTarget(PushTarget *target, IPAddress targetIP, int port) 16 | { 17 | LCBUrl lcburl; 18 | lcburl.setUrl(String(target->url) + String(target->key.name)); 19 | 20 | Log.notice(F("Posting to: %s" CR), lcburl.getHost().c_str()); 21 | 22 | const size_t capacity = JSON_OBJECT_SIZE(8) + 210; 23 | StaticJsonDocument doc; 24 | 25 | String json; 26 | serializeJson(doc, json); 27 | 28 | // Use the IP address we resolved (necessary for mDNS) 29 | Log.verbose(F("Connecting to: %s at %s on port %l" CR), 30 | lcburl.getHost().c_str(), 31 | targetIP.toString().c_str(), 32 | port); 33 | 34 | WiFiClient client; 35 | // 1 = SUCCESS 36 | // 0 = FAILED 37 | // -1 = TIMED_OUT 38 | // -2 = INVALID_SERVER 39 | // -3 = TRUNCATED 40 | // -4 = INVALID_RESPONSE 41 | client.setNoDelay(true); 42 | client.setTimeout(10000); 43 | if (client.connect(targetIP, port)) 44 | { 45 | Log.notice(F("Connected to: %s." CR), target->target.name); 46 | 47 | // Open POST connection 48 | if (lcburl.getAfterPath().length() > 0) 49 | { 50 | Log.verbose(F("POST /%s%s HTTP/1.1" CR), 51 | lcburl.getPath().c_str(), 52 | lcburl.getAfterPath().c_str()); 53 | } 54 | else 55 | { 56 | Log.verbose(F("POST /%s HTTP/1.1" CR), lcburl.getPath().c_str()); 57 | } 58 | client.print(F("POST /")); 59 | client.print(lcburl.getPath().c_str()); 60 | if (lcburl.getAfterPath().length() > 0) 61 | { 62 | client.print(lcburl.getAfterPath().c_str()); 63 | } 64 | client.println(F(" HTTP/1.1")); 65 | 66 | // Begin headers 67 | // 68 | // Host 69 | Log.verbose(F("Host: %s" CR), lcburl.getHost().c_str()); 70 | client.print(F("Host: ")); 71 | client.println(lcburl.getHost().c_str()); 72 | // 73 | Log.verbose(F("Connection: close" CR)); 74 | client.println(F("Connection: close")); 75 | // Content 76 | Log.verbose(F("Content-Length: %l" CR), json.length()); 77 | client.print(F("Content-Length: ")); 78 | client.println(json.length()); 79 | // Content Type 80 | Log.verbose(F("Content-Type: application/json" CR)); 81 | client.println(F("Content-Type: application/json")); 82 | // Terminate headers with a blank line 83 | Log.verbose(F("End headers." CR)); 84 | client.println(); 85 | // 86 | // End Headers 87 | 88 | // Post JSON 89 | client.println(json); 90 | // Check the HTTP status (should be "HTTP/1.1 200 OK") 91 | char status[32] = {0}; 92 | client.readBytesUntil('\r', status, sizeof(status)); 93 | client.stop(); 94 | Log.verbose(F("Status: %s" CR), status); 95 | if (strcmp(status + 9, "200 OK") == 0) 96 | { 97 | if (target->checkBody.enabled == true) 98 | { 99 | // Check body 100 | String response = String(status); 101 | if (response.indexOf(target->checkBody.name) >= 0) 102 | { 103 | Log.verbose(F("Response body ok." CR)); 104 | return true; 105 | } 106 | else 107 | { 108 | Log.error(F("Unexpected body content: %s" CR), response.c_str()); 109 | return false; 110 | } 111 | } 112 | else 113 | { 114 | return true; 115 | } 116 | } 117 | else 118 | { 119 | Log.error(F("Unexpected status: %s" CR), status); 120 | return false; 121 | } 122 | } 123 | else 124 | { 125 | Log.warning(F("Connection failed, Host: %s, Port: %l (Err: %d)" CR), 126 | lcburl.getHost().c_str(), port, client.connected()); 127 | return false; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/screen.cpp: -------------------------------------------------------------------------------- 1 | #include "screen.h" 2 | // #include 3 | // #include 4 | // #include 5 | // #include 6 | // Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); 7 | TFT_eSPI tft = TFT_eSPI(); 8 | extern String SG; 9 | extern String Voltage; 10 | extern String Temp; 11 | extern String Angle; 12 | extern String deviceName; 13 | extern String RSSI; 14 | extern String IP; 15 | extern String LastSeen; 16 | 17 | void displaydata(String array_data[10], int last_seen_ms) 18 | { 19 | wdt_disable(); 20 | SG = array_data[5]; 21 | Voltage = array_data[4]; 22 | Temp = array_data[3]; 23 | Angle = array_data[2]; 24 | deviceName = array_data[1]; 25 | RSSI = array_data[7]; 26 | IP = WiFi.localIP().toString(); 27 | LastSeen = String(pretty_time(last_seen_ms)); 28 | 29 | tft.fillScreen(TFT_BLACK); 30 | tft.setTextColor(TFT_BLUE); 31 | // tft.setTextDatum(BL_DATUM); 32 | tft.setTextWrap(false); 33 | // Cadre tout autour 34 | tft.drawRect(0, 0, TFT_WIDTH, TFT_HEIGHT, TFT_WHITE); 35 | tft.setFreeFont(&FreeSansBold12pt7b); 36 | // centerString("SG: "+ array_data[5],(TFT_WIDTH-2)/2,TFT_HEIGHT/8-2); 37 | centerString("SG: " + SG, (TFT_WIDTH - 2) / 2, 18); 38 | // Separator 39 | // tft.drawLine(1,TFT_HEIGHT/8 +2 ,TFT_WIDTH-1,TFT_HEIGHT/8 + 2,ST7735_LIME); 40 | tft.drawLine(1, 20, TFT_WIDTH - 1, 20, ST7735_LIME); 41 | // Temperature 42 | tft.setTextColor(TFT_YELLOW); 43 | tft.loadFont(FreeSansGras12, LittleFS); 44 | // centerString("T° : " + array_data[3] + " °" + array_data[8],(TFT_WIDTH/2),TFT_HEIGHT/4-2); 45 | centerString("T° : " + Temp + " °" + array_data[8], (TFT_WIDTH / 2), 21); 46 | tft.unloadFont(); 47 | // tft.drawLine(1,TFT_HEIGHT/4 +2,TFT_WIDTH-1,TFT_HEIGHT/4 +2,ST7735_LIME); 48 | tft.drawLine(1, 36, TFT_WIDTH - 1, 36, ST7735_LIME); 49 | // Battery 50 | // tft.setFreeFont(&FreeSans9pt7b); 51 | tft.loadFont(FreeSans9, LittleFS); 52 | tft.setTextColor(TFT_MAGENTA); 53 | // centerString("Battery : " + array_data[4].substring(0,4) + " V",(TFT_WIDTH/2),3*TFT_HEIGHT/8-2); 54 | // centerString(array_data[2].substring(0,5) +"° /" + array_data[4].substring(0,4) + " V",(TFT_WIDTH/2),3*TFT_HEIGHT/8-2); 55 | // centerString(array_data[2].substring(0,5) +"° - " + array_data[4].substring(0,4) + "V",(TFT_WIDTH/2),TFT_HEIGHT/4+4); 56 | centerString(Angle.substring(0, 5) + "°-" + Voltage.substring(0, 4) + "V", (TFT_WIDTH / 2), 38); 57 | // tft.drawLine(1,3*TFT_HEIGHT/8+2,TFT_WIDTH,3*TFT_HEIGHT/8+2,ST7735_LIME); 58 | tft.drawLine(1, 55, TFT_WIDTH, 55, ST7735_LIME); 59 | tft.unloadFont(); 60 | // iSpindel Name 61 | tft.setFreeFont(&FreeSans9pt7b); 62 | tft.setTextColor(TFT_RED); 63 | // centerString(array_data[1],(TFT_WIDTH/2),TFT_HEIGHT/2-2); 64 | centerString(deviceName, (TFT_WIDTH / 2), 69); 65 | // tft.drawLine(1,TFT_HEIGHT/2+2,TFT_WIDTH,TFT_HEIGHT/2+2,ST7735_LIME); 66 | tft.drawLine(1, 72, TFT_WIDTH, 72, ST7735_LIME); 67 | // Last Seen 68 | tft.unloadFont(); 69 | tft.setFreeFont(); 70 | tft.setTextSize(1); 71 | tft.setTextColor(TFT_ORANGE); 72 | // centerString("Last seen ",(TFT_WIDTH/2),9*TFT_HEIGHT/16-2); 73 | centerString("Last seen ", (TFT_WIDTH / 2), TFT_HEIGHT - 40); 74 | // centerString(String(last_seen),(TFT_WIDTH/2),10*TFT_HEIGHT/16-2); 75 | centerString(LastSeen, (TFT_WIDTH / 2), TFT_HEIGHT - 30); 76 | // Signal Strength 77 | // Let's color depending on the signal strength 78 | int sign_strength = RSSI.substring(1, 3).toInt(); 79 | if (sign_strength < 65) 80 | { 81 | // tft.setTextColor(ST7735_GREEN); 82 | tft.setTextColor(TFT_GREEN); 83 | } 84 | else if (sign_strength < 70) 85 | { 86 | // tft.setTextColor(ST7735_YELLOW); 87 | tft.setTextColor(TFT_YELLOW); 88 | } 89 | else if (sign_strength < 80) 90 | { 91 | // tft.setTextColor(ST7735_ORANGE); 92 | tft.setTextColor(TFT_ORANGE); 93 | } 94 | // else tft.setTextColor(ST7735_RED); 95 | else 96 | tft.setTextColor(TFT_RED); 97 | 98 | // centerString("Signal : " + String(sign_strength) + "dB",(TFT_WIDTH/2),6*TFT_HEIGHT/8-2); 99 | centerString("Signal : " + String(sign_strength) + "dB", (TFT_WIDTH / 2), TFT_HEIGHT - 20); 100 | // Local IP 101 | // tft.setFont(); 102 | tft.setFreeFont(); 103 | // tft.setTextColor(ST7735_BLUE); 104 | tft.setTextColor(TFT_BLUE); 105 | // centerString("IP : " + WiFi.localIP().toString(),(TFT_WIDTH/2),7*TFT_HEIGHT/8); 106 | // centerString("IP : " + WiFi.localIP().toString(),(TFT_WIDTH/2),TFT_HEIGHT-10); 107 | if (WiFi.isConnected()) 108 | { 109 | centerString("IP : " + WiFi.localIP().toString(), (TFT_WIDTH / 2), TFT_HEIGHT - 10); 110 | } 111 | else 112 | { 113 | centerString("WIFI DISCONNECTED", (TFT_WIDTH / 2), TFT_HEIGHT - 10); 114 | } 115 | // tft.print("IP : " + WiFi.localIP().toString()); 116 | // tft.drawLine(1,120,127,120,ST7735_LIME); 117 | wdt_enable(WDTO_8S); 118 | return; 119 | } 120 | -------------------------------------------------------------------------------- /data/wifi2.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | iSpindHub 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 76 |
77 | 78 |
79 | 80 |
81 | 82 | 83 |
84 | 85 | 86 |
87 |

Resetting WiFi Configuration

88 |
89 |
90 |

91 | Your iSpindHub WiFi connection settings are being reset which will cause it to forget your network configuration. After this is complete, it will reboot, and will create the configuration Access Point. To reconnect your Brew Bubbles to your network you 92 | will need to connect to the configuration Access Point and provide new WiFi settings. 93 |

94 |
95 |
96 | 97 | 98 |
99 | 100 | 101 |
102 | 103 |
104 |
105 | Copyright © 2020-2021, Nicolas Outrey 106 |
107 |
108 | 109 | 111 | 113 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /data/wifi.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | iSpindHub 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 76 |
77 | 78 |
79 | 80 |
81 | 82 | 83 |
84 | 85 | 86 |
87 |

Reset WiFi: Confirmation

88 |
89 |
90 |

91 | If you are sure you want to completely reset your iSpindHub' WiFi configuration, click the red "Reset WiFi" button below. You will have to re-configure via the Access Point to join this device back to your local WiFi network. 92 |

93 |

94 | Reset WiFi 95 |

96 |
97 |
98 | 99 | 100 |
101 | 102 | 103 |
104 | 105 |
106 |
107 | Copyright © 2020-2021, Nicolas Outrey 108 |
109 |
110 | 111 | 113 | 115 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /data/help.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | iSpindHub 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 76 |
77 | 78 |
79 | 80 |
81 | 82 | 83 |
84 | 85 | 86 |
87 |

iSpindHub Support

88 |
89 |
90 |

iSpindHub documentation, issue reporting and support can be found at the following links:
91 |

    92 |
  • Visit my Blog
  • 93 |
  • Visit project on GitHub
  • 94 | 95 |
96 |

97 |
98 |
99 | 100 | 101 |
102 | 103 | 104 |
105 | 106 |
107 |
108 | Copyright © 2020-2021, Nicolas Outrey 109 |
110 |
111 | 112 | 114 | 116 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env] 12 | platform = espressif8266 13 | board = d1_mini 14 | board_build.ldscript = eagle.flash.4m3m.ld 15 | framework = arduino 16 | upload_speed = 115200 17 | monitor_speed = 115200 18 | lib_deps = 19 | https://github.com/bblanchon/ArduinoJson.git 20 | # https://github.com/alanswx/ESPAsyncWiFiManager.git 21 | https://github.com/esphome/ESPAsyncWebServer.git 22 | # https://github.com/yubox-node-org/ESPAsyncWebServer#yuboxfixes-0xFEEDC0DE64-cleanup 23 | # https://github.com/lbussy/AsyncWiFiManager.git 24 | https://github.com/lbussy/Arduino-Log.git 25 | https://github.com/lbussy/AsyncTCP/ 26 | https://github.com/lbussy/LCBUrl.git 27 | https://github.com/sstaub/TickTwo 28 | https://github.com/me-no-dev/ESPAsyncUDP.git 29 | https://github.com/devyte/ESPAsyncDNSServer.git 30 | khoih-prog/ESP_DoubleResetDetector 31 | arduino-libraries/NTPClient@^3.1.0 32 | paulstoffregen/Time@^1.6 33 | bblanchon/StreamUtils@^1.6.3 34 | bodmer/TFT_eSPI@^2.3.67 35 | arkhipenko/Dictionary @ ^3.5.0 36 | khoih-prog/ESPAsync_WiFiManager 37 | lib_ldf_mode = chain 38 | board_build.filesystem = littlefs 39 | build_flags = 40 | -DPIO_SRC_TAG="0.0.7" 41 | -DPIO_SRC_REV="Dev" 42 | 43 | [env:2_inches] 44 | board_build.filesystem = littlefs 45 | build_flags = 46 | ${env.build_flags} 47 | -DPIO_SRC_BRH="2 Inches" 48 | -DUSER_SETUP_LOADED=1 49 | -DLOAD_GLCD=1 50 | -DLOAD_FONT2=1 51 | -DLOAD_FONT4=1 52 | -DLOAD_FONT6=1 53 | -DLOAD_FONT7=1 54 | -DLOAD_FONT8=1 55 | -DLOAD_GFXFF=1 56 | -DSMOOTH_FONT=1 57 | -DSPI_FREQUENCY=27000000 58 | -DILI9225_DRIVER=1 59 | -DTFT_WIDTH=176 60 | -DTFT_HEIGHT=220 61 | -DTFT_MOSI=PIN_D7 62 | -DTFT_SCLK=PIN_D5 63 | -DTFT_CS=PIN_D8 64 | -DTFT_DC=PIN_D1 65 | -DTFT_RST=PIN_D2 66 | -DTFT_BL=PIN_D4 67 | -DTFT_BACKLIGHT_ON=LOW 68 | -D USE_LITTLEFS 69 | -std=c99 70 | 71 | [env:1_7_inches] 72 | board_build.filesystem = littlefs 73 | build_flags = 74 | ${env.build_flags} 75 | -DPIO_SRC_BRH="1.7 Inches" 76 | -DUSER_SETUP_LOADED=1 77 | -DST7735_DRIVER=1 78 | -DLOAD_GLCD=1 79 | -DLOAD_FONT2=1 80 | -DLOAD_FONT4=1 81 | -DLOAD_FONT6=1 82 | -DLOAD_FONT7=1 83 | -DLOAD_FONT8=1 84 | -DLOAD_GFXFF=1 85 | -DSMOOTH_FONT=1 86 | -DSPI_FREQUENCY=27000000 87 | -DTFT_WIDTH=128 88 | -DTFT_HEIGHT=160 89 | -DTFT_MOSI=PIN_D7 90 | -DTFT_SCLK=PIN_D5 91 | -DTFT_CS=PIN_D8 92 | -DTFT_DC=PIN_D1 93 | -DTFT_RST=PIN_D2 94 | -DTFT_BL=PIN_D4 95 | -DTFT_BACKLIGHT_ON=HIGH 96 | -DTFT_RGB_ORDER=TFT_RGB 97 | -D USE_LITTLEFS 98 | 99 | 100 | [env:1_4_inches_red] 101 | board_build.filesystem = littlefs 102 | build_flags = 103 | ${env.build_flags} 104 | -DPIO_SRC_BRH="1.4 Inches Red Tag" 105 | -DUSER_SETUP_LOADED=1 106 | -DST7735_DRIVER=1 107 | -DST7735_GREENTAB3=1 108 | -DLOAD_GLCD=1 109 | -DLOAD_FONT2=1 110 | -DLOAD_FONT4=1 111 | -DLOAD_FONT6=1 112 | -DLOAD_FONT7=1 113 | -DLOAD_FONT8=1 114 | -DLOAD_GFXFF=1 115 | -DSMOOTH_FONT=1 116 | -DSPI_FREQUENCY=27000000 117 | -DTFT_WIDTH=128 118 | -DTFT_HEIGHT=128 119 | -DTFT_MOSI=PIN_D7 120 | -DTFT_SCLK=PIN_D5 121 | -DTFT_CS=PIN_D8 122 | -DTFT_DC=PIN_D3 123 | -DTFT_RST=PIN_D2 124 | -DTFT_BL=PIN_D4 125 | -DTFT_BACKLIGHT_ON=HIGH 126 | -DTFT_RGB_ORDER=TFT_BGR 127 | -D USE_LITTLEFS 128 | 129 | 130 | [env:1_4_inches_green] 131 | board_build.filesystem = littlefs 132 | build_flags = 133 | ${env.build_flags} 134 | -DPIO_SRC_BRH="1.4 Inches Green Tag" 135 | -DUSER_SETUP_LOADED=1 136 | -DST7735_DRIVER=1 137 | -DST7735_GREENTAB=1 138 | -DLOAD_GLCD=1 139 | -DLOAD_FONT2=1 140 | -DLOAD_FONT4=1 141 | -DLOAD_FONT6=1 142 | -DLOAD_FONT7=1 143 | -DLOAD_FONT8=1 144 | -DLOAD_GFXFF=1 145 | -DSMOOTH_FONT=1 146 | -DSPI_FREQUENCY=27000000 147 | -DTFT_WIDTH=128 148 | -DTFT_HEIGHT=128 149 | -DTFT_MOSI=PIN_D7 150 | -DTFT_SCLK=PIN_D5 151 | -DTFT_CS=PIN_D8 152 | -DTFT_DC=PIN_D3 153 | -DTFT_RST=PIN_D2 154 | -DTFT_BL=PIN_D4 155 | -DTFT_BACKLIGHT_ON=HIGH 156 | -DTFT_RGB_ORDER=TFT_BGR 157 | -D USE_LITTLEFS 158 | 159 | [env:1_4_inches_direct] 160 | board_build.filesystem = littlefs 161 | build_flags = 162 | ${env.build_flags} 163 | -DPIO_SRC_BRH="1.4 Inches Direct Plug" 164 | -DUSER_SETUP_LOADED=1 165 | -DST7735_DRIVER=1 166 | -DST7735_GREENTAB3=1 167 | -DLOAD_GLCD=1 168 | -DLOAD_FONT2=1 169 | -DLOAD_FONT4=1 170 | -DLOAD_FONT6=1 171 | -DLOAD_FONT7=1 172 | -DLOAD_FONT8=1 173 | -DLOAD_GFXFF=1 174 | -DSMOOTH_FONT=1 175 | -DSPI_FREQUENCY=27000000 176 | -DTFT_WIDTH=128 177 | -DTFT_HEIGHT=128 178 | -DTFT_MOSI=PIN_D7 179 | -DTFT_SCLK=PIN_D5 180 | -DTFT_CS=PIN_D4 181 | -DTFT_DC=PIN_D3 182 | -DTFT_RST=PIN_D2 183 | -DTFT_BACKLIGHT_ON=HIGH 184 | -DTFT_RGB_ORDER=TFT_BGR 185 | -D USE_LITTLEFS 186 | 187 | [env:2_4_inches_ILI9341] 188 | board_build.filesystem = littlefs 189 | build_flags = 190 | ${env.build_flags} 191 | -DPIO_SRC_BRH="2.4 Inches ILI9341" 192 | -DUSER_SETUP_LOADED=1 193 | -DLOAD_GLCD=1 194 | -DLOAD_FONT2=1 195 | -DLOAD_FONT4=1 196 | -DLOAD_FONT6=1 197 | -DLOAD_FONT7=1 198 | -DLOAD_FONT8=1 199 | -DLOAD_GFXFF=1 200 | -DSMOOTH_FONT=1 201 | -DSPI_FREQUENCY=27000000 202 | -DILI9341_DRIVER=1 203 | -DTFT_WIDTH=240 204 | -DTFT_HEIGHT=320 205 | -DTFT_MOSI=PIN_D7 206 | -DTFT_SCLK=PIN_D5 207 | -DTFT_CS=PIN_D8 208 | -DTFT_DC=PIN_D1 209 | -DTFT_RST=PIN_D2 210 | -DTFT_BL=PIN_D4 211 | -DTFT_BACKLIGHT_ON=LOW 212 | -D USE_LITTLEFS 213 | -std=c99 214 | 215 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | // extern Adafruit_ST7735 tft; 3 | extern TFT_eSPI tft; 4 | 5 | // DoubleResetDetect drd(DRD_TIMEOUT, DRD_ADDRESS); 6 | 7 | IPAddress local_IP(192, 168, 4, 1); 8 | IPAddress gateway(192, 168, 4, 22); 9 | IPAddress subnet(255, 255, 255, 0); 10 | 11 | int delay_loop = 30000; 12 | // String version = "0.0.6"; 13 | String vers = String(version()); 14 | 15 | // Let's put Global variable because why not 16 | String SG; 17 | String Voltage; 18 | String Temp; 19 | String Angle; 20 | String deviceName; 21 | String RSSI; 22 | String IP; 23 | String LastSeen; 24 | 25 | // Recurring jobs setup 26 | TickTwo BFa_timer(pushBrewFather, 900); 27 | 28 | void setup() 29 | { 30 | // tft.initR(INITR_144GREENTAB); // initialise ST7735S chip, green tab 31 | tft.init(); 32 | tft.setTextWrap(false); // Allow text to run off right edge 33 | // tft.setRotation(1); 34 | // Display Boot 35 | // tft.fillScreen(ST7735_BLACK); 36 | tft.fillScreen(TFT_BLACK); 37 | tft.setCursor(0, 0); 38 | // tft.drawRect(0,0,128,128,ST7735_WHITE); 39 | tft.drawRect(0, 0, TFT_WIDTH, TFT_HEIGHT, TFT_WHITE); 40 | tft.setTextColor(ST7735_AQUA); 41 | centerString("iSpind Hub " + vers, (TFT_WIDTH / 2), 10); 42 | // tft.setTextColor(ST7735_YELLOW); 43 | tft.setTextColor(TFT_YELLOW); 44 | centerString("by Slammy", (TFT_WIDTH / 2), 25); 45 | // File System 46 | LittleFS.begin(); 47 | // LittleFS.format(); 48 | Serial.begin(115200); 49 | wdt_enable(WDTO_8S); 50 | Log.begin(LOG_LEVEL_VERBOSE, &Serial); 51 | if (loadConfig()) 52 | {Log.notice(F("Configuration loaded." CR)); 53 | printConfig(); 54 | } 55 | else 56 | {Log.error(F("Unable to load configuration." CR));} 57 | // WiFi.mode(WIFI_STA); 58 | // bool rst = drd.detect(); // Check for double-reset 59 | bool rst = false; 60 | bool isconnected; 61 | bool isdeployed; 62 | if (rst == true) 63 | { 64 | // Serial.println("DRD Triggered, launch Config Portal"); 65 | doWiFi(true); 66 | } 67 | else 68 | { 69 | doWiFi(); 70 | } 71 | // AsyncWiFiManager wm; 72 | 73 | // isconnected = wm.autoConnect("iSpindHubConfig"); 74 | isconnected = WiFi.isConnected(); 75 | if (!isconnected) 76 | { 77 | tft.setCursor(2, 40); 78 | // tft.setTextColor(ST7735_RED); 79 | tft.setTextColor(TFT_RED); 80 | tft.setTextSize(1); 81 | tft.print("Failed to connect :("); 82 | // Serial.println("Failed to connect."); 83 | } 84 | else 85 | { 86 | tft.setCursor(2, 40); 87 | // tft.setTextColor(ST7735_GREEN); 88 | tft.setTextColor(TFT_GREEN); 89 | tft.setTextSize(1); 90 | tft.print("Connected !"); 91 | // Serial.println("Connected"); 92 | // Serial.println("Setting soft-AP configuration ... "); 93 | // Serial.println(WiFi.softAPConfig(local_IP, gateway, subnet) ? "Ready" : "Failed!"); 94 | setClock(); // Set NTP Time 95 | Serial.println(WiFi.softAPConfig(local_IP, gateway, subnet) ? "Ready" : "Failed!"); 96 | //isdeployed = WiFi.softAP("iSpindHub"); 97 | if (strlen(config.ispindhub.name) == 0) 98 | { 99 | isdeployed = WiFi.softAP(APNAME); 100 | } 101 | else{ 102 | isdeployed = WiFi.softAP(config.ispindhub.name); 103 | } 104 | 105 | if (isdeployed) 106 | { 107 | // tft.setTextColor(ST7735_BLUE); 108 | tft.setTextColor(TFT_BLUE); 109 | tft.setTextSize(1); 110 | tft.setCursor(2, 50); 111 | tft.print("SubNetWork deployed ! "); 112 | tft.setCursor(2, 80); 113 | tft.print("Soft-AP IP address = "); 114 | tft.setCursor(2, 110); 115 | tft.print(WiFi.localIP().toString()); 116 | // Serial.print("Soft-AP IP address = "); 117 | // Serial.println(WiFi.softAPIP()); 118 | initWebServer(); 119 | // Serial.print("Web Server Launched"); 120 | BFa_timer.start(); 121 | } 122 | else 123 | { 124 | // tft.setTextColor(ST7735_RED); 125 | tft.setTextColor(TFT_RED); 126 | tft.setTextSize(1); 127 | tft.setCursor(2, 50); 128 | tft.print("Failed to Deploy SubNetwork "); 129 | } 130 | } 131 | } 132 | 133 | 134 | void loop() 135 | { 136 | // put your main code here, to run repeatedly: 137 | Serial.println("Début du Loop"); 138 | BFa_timer.update(); 139 | FSInfo fs_info; 140 | LittleFS.info(fs_info); 141 | Dir dir = LittleFS.openDir("/data"); 142 | Serial.println("On a ouvert le Dir"); 143 | // or Dir dir = LittleFS.openDir("/data"); 144 | // while(true){ 145 | while (dir.next()) 146 | { 147 | // Open iSpindel Data file 148 | // Serial.println("File Name : " + dir.fileName()); 149 | if (dir.fileSize()) 150 | { 151 | wdt_reset(); 152 | File f = dir.openFile("r"); 153 | /*Serial.println("File Name :"); 154 | Serial.println(f.name()); 155 | Serial.println("File Size :"); 156 | Serial.println(f.size()); 157 | */ 158 | time_t now = time(nullptr); 159 | String temp = f.readStringUntil('\r'); 160 | int line_len = temp.length() + 1; 161 | f.seek(line_len, SeekEnd); 162 | String iSpinData = f.readString(); 163 | // Serial.println(iSpinData); 164 | delay_loop = handle_spindel_data(iSpinData, delay_loop, now - f.getLastWrite()); 165 | f.close(); 166 | //printConfig(); 167 | BFa_timer.interval(config.brewfather.freq * 60 * 1000); 168 | // free(iSpinData); 169 | //Serial.println("Avant delay"); 170 | //Serial.println(BFa_timer.elapsed()); 171 | //Serial.println(BFa_timer.state()); 172 | delay(delay_loop); 173 | //Serial.println("Après delay"); 174 | } 175 | } 176 | dir.rewind(); 177 | //} 178 | // return; 179 | } 180 | -------------------------------------------------------------------------------- /src/target.cpp: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2021 Lee C. Bussy (@LBussy) 2 | 3 | This file is part of Lee Bussy's Brew Bubbles (brew-bubbles). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. */ 22 | 23 | #include "target.h" 24 | 25 | Target *Target::single = NULL; 26 | 27 | Target *Target::getInstance() 28 | { 29 | if (!single) 30 | { 31 | single = new Target(); 32 | single->target = new PushTarget; 33 | single->target->ip = INADDR_NONE; 34 | 35 | // Enable target and target name 36 | single->target->target.enabled = (single->targeturl.length() > 3); 37 | strlcpy(single->target->target.name, single->target_name.c_str(), single->target_name.length() + 1); 38 | // 39 | // Check return body for success 40 | single->target->checkBody.enabled = single->checkbody_enabled; 41 | strlcpy(single->target->checkBody.name, single->checkbody_name.c_str(), single->checkbody_name.length() + 1); 42 | // 43 | // Change JSON point enabled and name for target type 44 | single->target->apiName.enabled = single->apiname_enabled; 45 | strlcpy(single->target->apiName.name, single->apiname_name.c_str(), single->apiname_name.length() + 1); 46 | // 47 | single->target->bubName.enabled = single->bubname_enabled; 48 | strlcpy(single->target->bubName.name, single->bubname_name.c_str(), single->bubname_name.length() + 1); 49 | // 50 | single->target->bpm.enabled = single->bpm_enabled; 51 | strlcpy(single->target->bpm.name, single->bpm_name.c_str(), single->bpm_name.length() + 1); 52 | // 53 | single->target->ambientTemp.enabled = single->ambienttemp_enabled; 54 | strlcpy(single->target->ambientTemp.name, single->ambienttemp_name.c_str(), single->ambienttemp_name.length() + 1); 55 | // 56 | single->target->vesselTemp.enabled = single->vesseltemp_enabled; 57 | strlcpy(single->target->vesselTemp.name, single->vesseltemp_name.c_str(), single->vesseltemp_name.length() + 1); 58 | // 59 | single->target->tempFormat.enabled = single->tempformat_enabled; 60 | strlcpy(single->target->tempFormat.name, single->tempformat_name.c_str(), single->tempformat_name.length() + 1); 61 | // 62 | // Grab correct URL for target type 63 | strlcpy(single->target->url, config.urltarget.url, sizeof(config.urltarget.url)); // Unique to URL Target 64 | // 65 | // API Key handling parameters 66 | single->target->key.enabled = single->apikey_enabled; 67 | strlcpy(single->target->key.name, single->apikey_name.c_str(), single->apikey_name.length()); 68 | } 69 | return single; 70 | } 71 | 72 | bool Target::push() 73 | { 74 | Log.verbose(F("Triggered %s push." CR), single->target->target.name); 75 | strlcpy(single->target->url, config.urltarget.url, sizeof(config.urltarget.url)); // Unique to URL Target 76 | single->target->target.enabled = (String(single->target->url).length() > 3); // Unique to URL Target 77 | LCBUrl lcburl; 78 | if (single->target->target.enabled) 79 | { 80 | if (lcburl.setUrl(String(single->target->url))) 81 | { 82 | IPAddress resolvedIP = resolveHost(lcburl.getHost().c_str()); 83 | if (resolvedIP == INADDR_NONE) 84 | { 85 | if (single->target->ip == INADDR_NONE) 86 | { 87 | Log.error(F("Unable to resolve host %s to IP address." CR), lcburl.getHost().c_str()); 88 | return false; 89 | } 90 | else 91 | { 92 | Log.verbose(F("Using cached information for host %s at IP %s." CR), lcburl.getHost().c_str(), single->target->ip.toString().c_str()); 93 | } 94 | } 95 | else 96 | { 97 | Log.verbose(F("Resolved host %s to IP %s." CR), lcburl.getHost().c_str(), resolvedIP.toString().c_str()); 98 | single->target->ip = resolvedIP; 99 | } 100 | } 101 | else 102 | { 103 | Log.error(F("Invalid URL in %s configuration: %s" CR), single->target->target.name, single->target->url); 104 | return false; 105 | } 106 | } 107 | else 108 | { 109 | Log.verbose(F("%s not enabled, skipping." CR), single->target->target.name); 110 | return true; 111 | } 112 | 113 | if (pushToTarget(single->target, target->ip, lcburl.getPort())) 114 | { 115 | Log.notice(F("%s post ok." CR), single->target->target.name); 116 | return true; 117 | } 118 | else 119 | { 120 | Log.error(F("%s post failed." CR), single->target->target.name); 121 | return false; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /data/license.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | iSpindHub 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 76 |
77 | 78 |
79 | 80 |
81 | 82 |
83 | 84 | 85 |
86 |

Brew Bubbles License

87 |
88 |
89 |

Copyright 2019-2020 Lee C. Bussy

90 | 91 |

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 92 | 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:

93 | 94 |

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

95 | 96 |

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 97 | 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. 98 |

99 | 100 |
101 |
102 | 103 | 104 |
105 | 106 | 107 |
108 | 109 |
110 |
111 | Copyright © 2020-2021, Nicolas Outrey 112 |
113 |
114 | 115 | 117 | 119 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /data/about.js: -------------------------------------------------------------------------------- 1 | // Supports About Page 2 | var unloadingState = false; 3 | var numReq = 3; 4 | var loaded = 0; 5 | var heapReloadTimer = 60000; 6 | 7 | // Detect unloading state during getJSON 8 | $(window).bind("beforeunload", function () { 9 | unloadingState = true; 10 | }); 11 | 12 | function populatePage() { // Get page data 13 | $(document).tooltip({ // Enable tooltips 14 | 'selector': '[data-toggle=tooltip]', 15 | 'toggleEnabled': true 16 | }); 17 | 18 | heapToolTip(); // Set up tooltip for debug info 19 | 20 | loadThisVersion(); // Populate form with controller settings 21 | 22 | loadUptime(); // Load uptime information 23 | loadHeap(); // Load heap information 24 | loadResetReason(); // Load last reset reason 25 | 26 | pollComplete(); 27 | } 28 | 29 | function heapToolTip() { 30 | var heapToolTip = "Heap Information:
"; 31 | heapToolTip += "
    "; 32 | heapToolTip += "
  • Free Heap = Total free bytes in the heap"; 33 | heapToolTip += "
  • Max = Size of largest free block in the heap"; 34 | heapToolTip += "
  • Frags = 100 - (max * 100) / free"; 35 | heapToolTip += "
"; 36 | $("#uptime").attr("data-original-title", "Time since last controller (re)start"); 37 | $("#resetreason").attr("data-original-title", "Reason for last (re)start"); 38 | $("#heap").attr("data-original-title", heapToolTip); 39 | } 40 | 41 | function loadThisVersion() { // Get current parameters 42 | var thisVersionJson = "/thisVersion/"; 43 | var thisVersion = $.getJSON(thisVersionJson, function () { 44 | }) 45 | .done(function (thisVersion) { 46 | try { 47 | $('#thisVersion').text("v" + thisVersion.version); 48 | $('#thisBranch').text(thisVersion.branch); 49 | $('#thisBuild').text(thisVersion.build); 50 | } 51 | catch { 52 | $('#thisVersion').html("").html('Error parsing version.'); 53 | $('#thisBranch').text(); 54 | $('#thisBuild').text(); 55 | } 56 | }) 57 | .fail(function () { 58 | $('#thisVersion').html("").html('Error loading version.'); 59 | $('#thisBranch').text(); 60 | $('#thisBuild').text(); 61 | }) 62 | .always(function () { 63 | // Can post-process here 64 | }); 65 | } 66 | 67 | function loadUptime(callback = null) { // Get uptime information 68 | var uptimeJson = "/uptime/"; 69 | var uptime = $.getJSON(uptimeJson, function () { 70 | }) 71 | .done(function (uptime) { 72 | try { 73 | var days = uptime.u.days.toString(); 74 | var hours = uptime.u.hours.toString(); 75 | var minutes = uptime.u.minutes.toString(); 76 | var seconds = uptime.u.seconds.toString(); 77 | 78 | var uptime = "Days: " + days + ", Hours: " + hours + ", Minutes: " + minutes + ", Seconds: " + seconds; 79 | $('#uptime').text(uptime); 80 | } 81 | catch { 82 | $('#uptime').text("(Error parsing uptime.)"); 83 | } 84 | }) 85 | .fail(function () { 86 | $('#uptime').text("(Error loading uptime.)"); 87 | }) 88 | .always(function () { 89 | if (loaded < numReq) { 90 | loaded++; 91 | } 92 | if (typeof callback == "function") { 93 | callback(); 94 | } 95 | }); 96 | } 97 | 98 | function loadHeap(callback = null) { // Get heap information 99 | var heapJson = "/heap/"; 100 | var heap = $.getJSON(heapJson, function () { 101 | }) 102 | .done(function (heap) { 103 | try { 104 | var free = heap.h.free; 105 | var max = heap.h.max; 106 | var frag = heap.h.frag; 107 | 108 | var heapinfo = "Free Heap: " + free + ", Max: " + max + ", Frags: " + frag; 109 | $('#heap').text(heapinfo); 110 | } 111 | catch { 112 | $('#heap').text("(Error parsing heap.)"); 113 | } 114 | }) 115 | .fail(function () { 116 | $('#heap').text("(Error loading heap.)"); 117 | }) 118 | .always(function () { 119 | if (loaded < numReq) { 120 | loaded++; 121 | } 122 | if (typeof callback == "function") { 123 | callback(); 124 | } 125 | }); 126 | } 127 | 128 | function loadResetReason(callback = null) { // Get last reset reason 129 | var resetJson = "/resetreason/"; 130 | var reset = $.getJSON(resetJson, function () { 131 | }) 132 | .done(function (reset) { 133 | try { 134 | var resetReason = reset.r.reason; 135 | var resetDescription = reset.r.description; 136 | 137 | var resetText = "Reason: " + resetReason + ", Description: " + resetDescription; 138 | $('#resetreason').text(resetText); 139 | } 140 | catch { 141 | $('#resetreason').text("(Error parsing version.)"); 142 | } 143 | }) 144 | .fail(function () { 145 | $('#resetreason').text("(Error loading version.)"); 146 | }) 147 | .always(function () { 148 | if (loaded < numReq) { 149 | loaded++; 150 | } 151 | if (typeof callback == "function") { 152 | callback(); 153 | } 154 | }); 155 | } 156 | 157 | function pollComplete() { 158 | if (loaded == numReq) { 159 | finishPage(); 160 | } else { 161 | setTimeout(pollComplete, 300); // try again in 300 milliseconds 162 | } 163 | } 164 | 165 | function heapReload() { 166 | loadHeap(function callFunction() { 167 | setTimeout(heapReload, heapReloadTimer); 168 | }); 169 | } 170 | 171 | function uptimeReload() { 172 | loadUptime(function callFunction() { 173 | setTimeout(uptimeReload, heapReloadTimer); 174 | }); 175 | } 176 | 177 | function reasonReload() { 178 | loadResetReason(function callFunction() { 179 | setTimeout(reasonReload, heapReloadTimer); 180 | }); 181 | } 182 | 183 | function finishPage() { // Display page 184 | setTimeout(heapReload, heapReloadTimer); 185 | setTimeout(uptimeReload, heapReloadTimer); 186 | setTimeout(reasonReload, heapReloadTimer); 187 | } 188 | -------------------------------------------------------------------------------- /src/wifi.cpp: -------------------------------------------------------------------------------- 1 | #include "wifi.h" 2 | bool shouldSaveConfig = false; 3 | const byte DNS_PORT = 53; 4 | #define HTTP_PORT 80 5 | IPAddress apIP(192, 168, 4, 1); 6 | AsyncWebServer webServer(HTTP_PORT); 7 | AsyncDNSServer dnsServer; 8 | 9 | ESPAsync_WiFiManager wm(&webServer, &dnsServer, "iSpindHub"); 10 | 11 | void doWiFi() 12 | { 13 | doWiFi(false); 14 | } 15 | void doWiFi(bool dontUseStoredCreds) 16 | { 17 | // wm.setConfigPortalBlocking(false); 18 | // wm.setConfigPortalBlocking(true); 19 | WiFi.mode(WIFI_AP_STA); // explicitly set mode, esp defaults to STA+AP 20 | WiFi.setAutoReconnect(true); 21 | // AsyncWiFiManager Callbacks 22 | wm.setAPCallback(apCallback); // Called after AP has started 23 | // wm.setConfigResetCallback(configResetCallback); // Called after settings are reset 24 | // wm.setPreSaveConfigCallback(preSaveConfigCallback); // Called before saving wifi creds 25 | wm.setSaveConfigCallback(saveConfigCallback); // Called only if wifi is saved/changed, or setBreakAfterConfig(true) 26 | // wm.setSaveParamsCallback(saveParamsCallback); // Called after parameters are saved via params menu or wifi config 27 | // wm.setWebServerCallback(webServerCallback); // Called after webserver is setup 28 | wm.setDebugOutput(true); // Verbose debug is enabled by default 29 | std::vector _wfmPortalMenu = { 30 | "wifi", 31 | "wifinoscan", 32 | "sep", 33 | "info", 34 | //"param", 35 | //"close", 36 | "erase", 37 | "restart", 38 | "exit"}; 39 | // wm.setMenu(_wfmPortalMenu); // Set menu items 40 | // wm.setCountry(WIFI_COUNTRY); // Setting wifi country seems to improve OSX soft ap connectivity 41 | Log.notice(F("Set COUNTRY OK" CR)); 42 | // wm.setWiFiAPChannel(WIFI_CHAN); // Set WiFi channel 43 | // wm.setShowStaticFields(true); // Force show static ip fields 44 | // wm.setShowDnsFields(true); 45 | if (dontUseStoredCreds) 46 | { 47 | // Voluntary portal 48 | // blinker.attach_ms(APBLINK, wifiBlinker); 49 | wm.setConfigPortalTimeout(120); 50 | 51 | if (wm.startConfigPortal(APCONFNAME, APCONFPWD)) 52 | { 53 | // We finished with portal, do we need this? 54 | } 55 | else 56 | { 57 | // Hit timeout on voluntary portal 58 | delay(3000); 59 | Log.notice(F("Hit timeout for on-demand portal, exiting." CR)); 60 | // ESP.restart(); 61 | } 62 | } 63 | else 64 | { // Normal WiFi connection attempt 65 | // blinker.attach_ms(STABLINK, wifiBlinker); 66 | wm.setConnectTimeout(30); 67 | wm.setConfigPortalTimeout(120); 68 | if (!wm.autoConnect(APNAME, APPWD)) 69 | { 70 | Log.warning(F("Failed to connect and/or hit timeout." CR)); 71 | Log.warning(F("Restarting." CR)); 72 | ESP.restart(); 73 | } 74 | else 75 | { 76 | // We finished with portal (We were configured) 77 | Log.notice(F("Get In Set HostName" CR)); 78 | if (strlen(config.ispindhub.name) == 0) 79 | { 80 | Log.notice(F(HOSTNAME CR)); 81 | // WiFi.setHostname(HOSTNAME); 82 | WiFi.hostname(HOSTNAME); 83 | WiFi.softAP(HOSTNAME); 84 | } 85 | else 86 | { 87 | Log.notice("We use the information from the config file"); 88 | Log.notice(F(CR)); 89 | Log.notice(config.ispindhub.name); 90 | Log.notice(F(CR)); 91 | WiFi.hostname(config.ispindhub.name); 92 | WiFi.softAP(config.ispindhub.name); 93 | } 94 | 95 | Log.notice(F("Get Out Set HostName" CR)); 96 | saveConfig(); 97 | } 98 | } 99 | if (shouldSaveConfig) 100 | { // Save configuration 101 | // Save configuration 102 | } 103 | 104 | Log.notice(F("Connected. IP address: %s." CR), WiFi.localIP().toString().c_str()); 105 | dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); 106 | Log.verbose(F("Soft AP started, IP: %s" CR), WiFi.softAPIP().toString().c_str()); 107 | WiFi.onEvent(WiFiEvent); 108 | } 109 | 110 | void resetWifi() 111 | { // Wipe WiFi settings and reset controller 112 | WiFi.disconnect(); 113 | wm.resetSettings(); 114 | Log.notice(F("Restarting after clearing wifi settings." CR)); 115 | saveConfig(); 116 | _delay(100); 117 | ESP.restart(); 118 | } 119 | 120 | void apCallback(ESPAsync_WiFiManager *asyncWiFiManager) 121 | { // Entered Access Point mode 122 | Log.verbose(F("[CALLBACK]: setAPCallback fired." CR)); 123 | Log.notice(F("Entered portal mode; name: %s, IP: %s." CR), 124 | asyncWiFiManager->getConfigPortalSSID().c_str(), 125 | WiFi.localIP().toString().c_str()); 126 | } 127 | 128 | void saveConfigCallback() 129 | { 130 | Log.verbose(F("[CALLBACK]: setSaveConfigCallback fired." CR)); 131 | shouldSaveConfig = true; 132 | } 133 | 134 | void saveParamsCallback() 135 | { 136 | Log.verbose(F("[CALLBACK]: setSaveParamsCallback fired." CR)); 137 | } 138 | 139 | // void webServerCallback() { 140 | // Log.verbose(F("[CALLBACK]: setWebServerCallback fired." CR)); 141 | // } 142 | 143 | void WiFiEvent(WiFiEvent_t event) 144 | { 145 | Log.notice(F("[WiFi-event] event: %d" CR), event); 146 | if (!WiFi.isConnected()) 147 | { 148 | Log.warning(F("WiFi lost connection..")); 149 | WiFi.begin(); 150 | 151 | int WLcount = 0; 152 | while (!WiFi.isConnected() && WLcount < 190) 153 | { 154 | delay(100); 155 | Serial.print("."); 156 | ++WLcount; 157 | } 158 | 159 | if (!WiFi.isConnected()) 160 | { 161 | // We failed to reconnect. 162 | Log.error(F("Unable to reconnect WiFI, restarting." CR)); 163 | delay(1000); 164 | ESP.restart(); 165 | } 166 | else 167 | { 168 | bool isdeployed; 169 | if (strlen(config.ispindhub.name) == 0) 170 | { 171 | isdeployed = WiFi.softAP(APNAME); 172 | } 173 | else 174 | { 175 | isdeployed = WiFi.softAP(config.ispindhub.name); 176 | } 177 | 178 | if (!isdeployed) 179 | { 180 | Log.error(F("Unable to start softAP, restarting." CR)); 181 | delay(1000); 182 | ESP.restart(); 183 | } 184 | } 185 | Serial.println(); 186 | } 187 | } -------------------------------------------------------------------------------- /data/about.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | iSpindHub 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | 81 |
82 | 83 |
84 | 85 |
86 | 87 | 88 |
89 | 90 |
91 |

92 | About iSpindHub: 93 | (version loading...) 94 | [] 95 | () 96 |

97 |
98 |
99 |

iSpindHub is a project developed by Nicolas Outrey

100 |
    101 |
  • 102 | Uptime: 103 | ...loading 104 |
  • 105 |
106 |
107 |
108 | 109 | 110 |
111 | 112 | 113 |
114 | 115 |
116 |
117 | Copyright © 2010-2021, Nicolas Outrey 118 |
119 |
120 | 121 | 123 | 125 | 127 | 128 | 129 | 130 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /data/reset.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | iSpindHub 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 76 |
77 | 78 |
79 | 80 |
81 | 82 |
83 | 84 | 85 |
86 |

Controller Reset

87 |
88 |
89 |

90 | Your Brew Bubbles' controller is being reset and will return momentarily. You will be redirected to the home page when that is complete. 91 |

92 |
93 |
94 | 95 | 96 |
97 | 98 | 99 |
100 | 101 |
102 |
103 | Copyright © 2020-2021, Nicolas Outrey 104 |
105 |
106 | 107 | 109 | 111 | 113 | 114 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /data/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | iSpindHub 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 | 50 | 52 | 54 | 55 | 56 | 57 |
58 | 82 |
83 | 84 |
85 | 86 |
87 | 88 |
89 | 90 | 91 |
92 |

Available iSpindel(s)

93 |
94 |
95 |
    96 |
97 |
98 |
99 | 100 | 101 |
102 | 103 | 104 |
105 | 106 |
107 |
108 | Copyright © 2021, Nicolas Outrey 109 |
110 |
111 | 112 | 157 | 158 | -------------------------------------------------------------------------------- /data/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/ota.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | iSpindHub 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 76 |
77 | 78 |
79 | 80 |
81 | 82 | 83 |
84 | 85 | 86 |
87 |

Firmware Update In Progress, please Wait ...

88 |
89 |
90 |

91 | Your Brew Bubbles' firmware is being updated to the latest version. This can take up to 5 minutes, during which your Brew Bubbles will be unresponsive. Please do not disconnect the power or reset your Brew Bubbles while this process is taking place. 92 |

93 |

94 | If you wish to observe your controller's LED during the process, the LED will flash as the update is in progress. As soon as the LED is steady - either off if blocked or on if not blocked, the process is complete. 95 |

96 |

97 | When the update is complete, the controller will re-load your original application settings. If this step fails, you will need to manually reconfigure all application settings. WiFi settings will not be affected. 98 |

99 |

100 |

101 | 102 | Do not refresh this page. If you do, you will not be able to 103 | track the upgrade process. 104 | 105 |
106 |

107 |
108 |
109 | 110 | 111 |
112 | 113 | 114 |
115 | 116 |
117 |
118 | Copyright © 2020-2021, Nicolas Outrey 119 |
120 |
121 | 122 | 124 | 126 | 128 | 129 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **iSpindHub** 2 | - [**iSpindHub**](#ispindhub) 3 | - [Overall Project Information](#overall-project-information) 4 | - [Major Thanks](#major-thanks) 5 | - [Documentation is in progress](#documentation-is-in-progress) 6 | - [Build your Own !](#build-your-own-) 7 | - [Generic stuff](#generic-stuff) 8 | - [1.4 TFT Screen](#14-tft-screen) 9 | - [1.77" TFT Screen](#177-tft-screen) 10 | - [2" TFT Screen](#2-tft-screen) 11 | - [2.4" TFT Screen](#24-tft-screen) 12 | 13 | 14 | 15 | # Overall Project Information 16 | 17 | This project is widely inspired by [**TiltBridge**](https://www.tiltbridge.com/) and [**Nautilis' iSpindel Relay**](http://www.nautilis.eu/en/portfolio-item/nautilis-ispindel-relay/). 18 | 19 | Easiest way to install is through [**BrewFlasher**](https://github.com/thorrak/brewflasher/releases), project is iSpindHub. 20 | 21 | 22 | It is basically a receiver to get information from your iSpindels and display it allowing you to use your iSpindel(s) even if you do not have WiFi available in your brew cave. 23 | 24 | It also acts as a WiFi repetiter and will push iSpindel information to various services (BrewFather, LittleBock, CraftBeerPi etc). 25 | 26 | ![Image of a iSpindhub Display](https://raw.githubusercontent.com/ZeSlammy/iSpindHub/master/pictures/iSpindHub2.jpg) 27 | 28 | 29 | 30 | It's a work in Progress :D 31 | 32 | # Major Thanks 33 | I cannot thank enough [**Thorrak**](https://github.com/thorrak) and [**lbussy**](https://github.com/lbussy) for helping me during my dumb moments. 34 | 35 | # Documentation is in progress 36 | Check it on in the [**docs folder**](https://github.com/ZeSlammy/iSpindHub/tree/master/docs) ! 37 | 38 | # Build your Own ! 39 | ## Generic stuff 40 | I will try and list the various screens I have tested and am sure are working (this might lead to multiple releases unless I find an elegant way to have this as a selectable option) 41 | I might have to rewrite my code to use a generic library and not a specific one ... 42 | 43 | ## 1.4 TFT Screen 44 | This one is an easy to find on Ali Express and is nice because it plugs directly onto a D1 Mini. 45 | Display is square 128*128 46 | 47 | /!\ I have not tested it (recently) directly plugged onto the D1 Mini. 48 | I used the bottom connection and soldered pins. 49 | 50 | | Marking on screen | D1 Mini PIN | Alternative names | Comments | 51 | | :---------------: | :---------: | :---------------: | :---------------------------------------: | 52 | | LED | 3.3V | LCD | Could be on 5V if your screen is tolerant | 53 | | GND | GND | G | | 54 | | RST | D2 | | | 55 | | DC | D3 | | | 56 | | MOSI | D7 | SDI | | 57 | | SCK | D5 | | | 58 | | 3V | 3V3 | | | 59 | | CS | D4 | | | 60 | 61 | 62 | If you want to use the existing pins at the back of the screen do the following 63 | 64 | | Marking on screen | D1 Mini PIN | Alternative names | Comments | 65 | | :---------------: | :---------: | :---------------: | :----------------------------------------------------: | 66 | | D4 | D8 | | | 67 | | G | GND | | | 68 | | D7 | D7 | | It might be marked D4. It is the one between D6 and D8 | 69 | | D5 | D5 | | | 70 | | 3V | 3V3 | | | 71 | | RST | D2 | | | 72 | | D3 | D3 | | | 73 | 74 | 75 | 76 | ![Back of the Red Tab 1.4 TFT](https://github.com/ZeSlammy/iSpindHub/blob/master/pictures/1_4_TFT_RedTab_Back.jpg?raw=true "Back of the Red Tab 1.4 TFT") 77 | ![Front of the Red Tab TFT](https://github.com/ZeSlammy/iSpindHub/blob/master/pictures/1_4_TFT_RedTab_Front.jpg?raw=true "Front of the Red Tab TFT") 78 | 79 | [**Find it on Ali Express**](https://s.click.aliexpress.com/e/_An4AxM) 80 | 81 | [**find it on Amazon.com**](https://www.amazon.com/1-44Inch-Display-Resolution-Peripheral-Interface/dp/B08135JYP4/) 82 | 83 | [**find it on Amazon.fr**](https://amzn.to/3DwjqVb) REFERRAL LINK 84 | 85 | 86 | ## 1.77" TFT Screen 87 | Display is 160*128 88 | | Marking on screen | D1 Mini PIN | Alternative names | Comments | 89 | | :---------------: | :---------: | :---------------: | :---------------------------------------: | 90 | | LEDA | D4 | 8 | Could be on 5V if your screen is tolerant | 91 | | GND | GND | 1 | | 92 | | RES | D2 | 5 | | 93 | | RS | D1 | 6 | | 94 | | SDA | D7 | 4 | | 95 | | SCK | D5 | 3 | | 96 | | VCC | 3V3 | 2 | | 97 | | CS | D8 | 7 | | 98 | 99 | 100 | 101 | [**Find it on AliExpress**](https://s.click.aliexpress.com/e/_98DPi6) 102 | 103 | [**Find it on Amazon.fr**](https://amzn.to/3gOuxPC) REFERRAL LINK 104 | 105 | ## 2" TFT Screen 106 | Red PCB with Red Tab 107 | Display is 176*220 108 | 109 | 110 | | Marking on screen | D1 Mini PIN | Alternative names | Comments | 111 | | :---------------: | :---------: | :---------------: | :---------------------------------------: | 112 | | LED | D4 | LCD | Could be on 5V if your screen is tolerant | 113 | | GND | GND | G | | 114 | | RST | D2 | | | 115 | | DC | D1 | RS | | 116 | | MOSI | D7 | SDI | | 117 | | SCK | D5 | | | 118 | | 3V | 3V3 | | | 119 | | CS | D8 | | | 120 | 121 | [**Find it on AliExpress**](https://s.click.aliexpress.com/e/_9JS0cI) 122 | 123 | [**Find it on Amazon.fr**](https://amzn.to/3mOLFIX) REFERRAL LINK 124 | 125 | [**Another one on amazon**](https://amzn.to/3Cfpq2E) REFERRAL LINK 126 | For this one, connections are as follow 127 | | Marking on screen | D1 Mini PIN | Alternative names | Comments | 128 | | :---------------: | :---------: | :---------------: | :------: | 129 | | VCC | Vin | | | 130 | | GND | GND | | | 131 | | CLK | D5 | | | 132 | | SDA | D7 | | | 133 | | RS | D1 | | | 134 | | RST | RST | | | 135 | | CS | D8 | | | 136 | | | 137 | 138 | ## 2.4" TFT Screen 139 | Red PCB with Red Tab 140 | Display is 240*320 141 | 142 | 143 | | Marking on screen | D1 Mini PIN | Alternative names | Comments | 144 | | :---------------: | :---------: | :---------------: | :---------------------------------------: | 145 | | LED | D4 | LCD | Could be on 5V if your screen is tolerant | 146 | | GND | GND | G | | 147 | | RST | D2 | RESET | | 148 | | DC | D1 | RS | | 149 | | MOSI | D7 | SDI | | 150 | | SCK | D5 | | | 151 | | VCC | 3V3 | | | 152 | | CS | D8 | | | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /src/jsonconfig.cpp: -------------------------------------------------------------------------------- 1 | #include "jsonconfig.h" 2 | const char *filename = "/config.json"; 3 | Config config; 4 | 5 | extern const size_t capacitySerial = 1536; 6 | extern const size_t capacityDeserial = 800; 7 | 8 | bool deleteConfigFile() 9 | { 10 | if (!LittleFS.begin()) 11 | { 12 | return false; 13 | } 14 | return LittleFS.remove(filename); 15 | } 16 | 17 | bool loadConfig() 18 | { 19 | // Manage loading the configuration 20 | if (!loadFile()) 21 | { 22 | return false; 23 | } 24 | else 25 | { 26 | saveFile(); 27 | return true; 28 | } 29 | } 30 | 31 | bool loadFile() 32 | { 33 | if (!LittleFS.begin()) 34 | { 35 | return false; 36 | } 37 | // Loads the configuration from a file on File System 38 | File file = LittleFS.open(filename, "r"); 39 | if (!LittleFS.exists(filename) || !file) 40 | { 41 | // Unable to open the file 42 | file.close(); 43 | File file = LittleFS.open(filename, "w"); 44 | if (!LittleFS.exists(filename) || !file) 45 | { 46 | // Still could not create a file 47 | return false; 48 | } 49 | else 50 | { 51 | file.close(); 52 | file = LittleFS.open(filename, "r"); 53 | if (!LittleFS.exists(filename) || !file) 54 | { 55 | // Unable to open a file we created in the previous block 56 | return false; 57 | } 58 | } 59 | } 60 | 61 | if (!deserializeConfig(file)) 62 | { 63 | file.close(); 64 | return false; 65 | } 66 | else 67 | { 68 | file.close(); 69 | return true; 70 | } 71 | } 72 | 73 | bool saveConfig() 74 | { 75 | return saveFile(); 76 | } 77 | 78 | bool saveFile() 79 | { 80 | // Saves the configuration to a file on File System 81 | File file = LittleFS.open(filename, "w"); 82 | if (!file) 83 | { 84 | file.close(); 85 | return false; 86 | } 87 | 88 | // Serialize JSON to file 89 | if (!serializeConfig(file)) 90 | { 91 | file.close(); 92 | return false; 93 | } 94 | file.close(); 95 | return true; 96 | } 97 | 98 | bool deserializeConfig(Stream &src) 99 | { 100 | // Deserialize configuration 101 | JsonDocument doc; 102 | 103 | // Parse the JSON object in the file 104 | DeserializationError err = deserializeJson(doc, src); 105 | 106 | if (err) 107 | { 108 | // We really don;t care if there's an err, the file should be created anyway 109 | config.load(doc.as()); 110 | return true; 111 | } 112 | else 113 | { 114 | config.load(doc.as()); 115 | return true; 116 | } 117 | } 118 | 119 | bool serializeConfig(Print &dst) 120 | { 121 | // Serialize configuration 122 | JsonDocument doc; 123 | 124 | // Create an object at the root 125 | JsonObject root = doc.to(); 126 | 127 | // Fill the object 128 | config.save(root); 129 | 130 | // Serialize JSON to file 131 | return serializeJsonPretty(doc, dst) > 0; 132 | } 133 | 134 | bool printFile() 135 | { 136 | // Prints the content of a file to the Serial 137 | File file = LittleFS.open(filename, "r"); 138 | if (!file) 139 | return false; 140 | 141 | while (file.available()) 142 | Serial.print((char)file.read()); 143 | 144 | Serial.println(); 145 | file.close(); 146 | return true; 147 | } 148 | 149 | bool printConfig() 150 | { 151 | // Serialize configuration 152 | JsonDocument doc; 153 | 154 | // Create an object at the root 155 | JsonObject root = doc.to(); 156 | 157 | // Fill the object 158 | config.save(root); 159 | 160 | // Serialize JSON to file 161 | bool retval = serializeJsonPretty(doc, Serial) > 0; 162 | Serial.println(); 163 | return retval; 164 | } 165 | bool mergeJsonString(String newJson) 166 | { 167 | // Serialize configuration 168 | JsonDocument doc; 169 | 170 | // Parse directly from file 171 | DeserializationError err = deserializeJson(doc, newJson); 172 | if (err) 173 | Serial.println(err.c_str()); 174 | 175 | return mergeJsonObject(doc); 176 | } 177 | 178 | bool mergeJsonObject(JsonVariantConst src) 179 | { 180 | // Serialize configuration 181 | JsonDocument doc; 182 | 183 | // Create an object at the root 184 | JsonObject root = doc.to(); 185 | 186 | // Fill the object 187 | config.save(root); 188 | 189 | // Merge in the configuration 190 | if (merge(root, src)) 191 | { 192 | // Move new object to config 193 | config.load(root); 194 | saveFile(); 195 | return true; 196 | } 197 | 198 | return false; 199 | } 200 | 201 | bool merge(JsonVariant dst, JsonVariantConst src) 202 | { 203 | if (src.is()) 204 | { 205 | for (auto kvp : src.as()) 206 | { 207 | 208 | merge(dst[kvp.key()], kvp.value()); 209 | } 210 | } 211 | else 212 | { 213 | dst.set(src); 214 | } 215 | return true; 216 | } 217 | 218 | void ApConfig::save(JsonObject obj) const 219 | { 220 | obj["ssid"] = ssid; 221 | obj["passphrase"] = passphrase; 222 | } 223 | 224 | void ApConfig::load(JsonObjectConst obj) 225 | { 226 | // Load Access Point configuration 227 | // 228 | if (obj["ssid"].isNull()) 229 | { 230 | strlcpy(ssid, APNAME, sizeof(ssid)); 231 | } 232 | else 233 | { 234 | const char *sd = obj["ssid"]; 235 | strlcpy(ssid, sd, sizeof(ssid)); 236 | } 237 | 238 | if (obj["passphrase"].isNull()) 239 | { 240 | strlcpy(passphrase, APPWD, sizeof(passphrase)); 241 | } 242 | else 243 | { 244 | const char *ps = obj["passphrase"]; 245 | strlcpy(passphrase, ps, sizeof(passphrase)); 246 | } 247 | } 248 | 249 | void URLTarget::save(JsonObject obj) const 250 | { 251 | obj["url"] = url; 252 | obj["freq"] = freq; 253 | obj["update"] = update; 254 | } 255 | 256 | void URLTarget::load(JsonObjectConst obj) 257 | { 258 | // Load URL Target configuration 259 | // 260 | if (obj["url"].isNull()) 261 | { 262 | strlcpy(url, "", sizeof(url)); 263 | } 264 | else 265 | { 266 | const char *tu = obj["url"]; 267 | strlcpy(url, tu, sizeof(url)); 268 | } 269 | 270 | if (obj["freq"].isNull()) 271 | { 272 | freq = 2; 273 | } 274 | else 275 | { 276 | int f = obj["freq"]; 277 | freq = f; 278 | } 279 | 280 | if (obj["update"].isNull()) 281 | { 282 | update = false; 283 | } 284 | else 285 | { 286 | bool u = obj["update"]; 287 | update = u; 288 | } 289 | } 290 | 291 | void KeyTarget::save(JsonObject obj) const 292 | { 293 | obj["channel"] = channel; 294 | obj["key"] = key; 295 | obj["freq"] = freq; 296 | obj["update"] = update; 297 | } 298 | 299 | void KeyTarget::load(JsonObjectConst obj) 300 | { 301 | // Load Key-type configuration 302 | // 303 | if (obj["channel"].isNull()) 304 | { 305 | channel = 0; 306 | } 307 | else 308 | { 309 | int c = obj["channel"]; 310 | channel = c; 311 | } 312 | 313 | if (obj["key"].isNull()) 314 | { 315 | strlcpy(key, "", sizeof(key)); 316 | } 317 | else 318 | { 319 | const char *k = obj["key"]; 320 | strlcpy(key, k, sizeof(key)); 321 | } 322 | 323 | if (obj["freq"].isNull()) 324 | { 325 | freq = 15; 326 | } 327 | else 328 | { 329 | int f = obj["freq"]; 330 | freq = f; 331 | } 332 | 333 | if (obj["update"].isNull()) 334 | { 335 | update = false; 336 | } 337 | else 338 | { 339 | bool u = obj["update"]; 340 | update = u; 341 | } 342 | } 343 | void iSpindHub::save(JsonObject obj) const 344 | { 345 | obj["name"] = name; 346 | obj["TZ"] = TZ; 347 | } 348 | 349 | void iSpindHub::load(JsonObjectConst obj) 350 | { 351 | // Load iSpindHub configuration 352 | // 353 | 354 | if (obj["name"].isNull()) 355 | { 356 | strlcpy(name, APNAME, sizeof(name)); 357 | } 358 | else 359 | { 360 | const char *nm = obj["name"]; 361 | strlcpy(name, nm, sizeof(name)); 362 | } 363 | 364 | if (obj["TZ"].isNull()) 365 | { 366 | strlcpy(TZ, "CEST", sizeof(TZ)); 367 | } 368 | else 369 | { 370 | const char *tm = obj["TZ"]; 371 | strlcpy(TZ, tm, sizeof(TZ)); 372 | } 373 | } 374 | 375 | void Config::load(JsonObjectConst obj) 376 | { 377 | // Load all config objects 378 | // 379 | 380 | apconfig.load(obj["apconfig"]); 381 | ispindhub.load(obj["ispindhub"]); 382 | urltarget.load(obj["urltarget"]); 383 | brewersfriend.load(obj["brewersfriend"]); 384 | brewfather.load(obj["brewfather"]); 385 | } 386 | 387 | void Config::save(JsonObject obj) const 388 | { 389 | // Add Access Point object 390 | // apconfig.save(obj.createNestedObject("apconfig")); 391 | apconfig.save(obj["apconfig"].to()); 392 | // Add iSpindHub object 393 | ispindhub.save(obj["ispindhub"].to()); 394 | // Add Target object 395 | urltarget.save(obj["urltarget"].to()); 396 | // Add BrewPiLess object 397 | bpiless.save(obj["bpiless"].to()); 398 | // Add Brewer's Friend object 399 | brewersfriend.save(obj["brewersfriend"].to()); 400 | // Add Brewfather object 401 | brewfather.save(obj["brewfather"].to()); 402 | } -------------------------------------------------------------------------------- /data/settings.js: -------------------------------------------------------------------------------- 1 | // Supports Settings Page 2 | 3 | var posted = false; 4 | var unloadingState = false; 5 | var loaded = 0; // Hold data load status 6 | var numReq = 2; // Number of JSON required 7 | 8 | // Tab tracking 9 | var previousTab = ""; 10 | var currentTab = ""; 11 | 12 | // Handle unloading page while making a getJSON call 13 | $(window).bind("beforeunload", function() { 14 | unloadingState = true; 15 | }); 16 | 17 | function loadHash() { // Link to tab via hash value 18 | var url = document.location.toString(); 19 | if (url.match('#')) { 20 | $('.nav-tabs a[href="#' + url.split('#')[1] + '"]').tab('show'); 21 | } 22 | 23 | // Change hash for page-reload 24 | $('.nav-tabs a').on('shown.bs.tab', function(e) { 25 | window.location.hash = e.target.hash; 26 | }); 27 | } 28 | 29 | function populatePage() { 30 | $(document).tooltip({ 31 | 'selector': '[data-toggle=tooltip]', 32 | 'toggleEnabled': true 33 | }); 34 | loadHash(); 35 | populateForm(); 36 | loadThisVersion(); 37 | loadTab(); 38 | pollComplete(); 39 | } 40 | 41 | function loadTab() { 42 | // Javascript to enable linking to tab 43 | var url = document.location.toString(); 44 | if (url.match('#')) { 45 | $('.nav-tabs a[href="#' + url.split('#')[1] + '"]').tab('show'); 46 | } 47 | 48 | // Change hash for page-reload 49 | $('.nav-tabs a').on('shown.bs.tab', function(e) { 50 | window.location.hash = e.target.hash; 51 | }); 52 | } 53 | 54 | function loadThisVersion() { // Get current parameters 55 | var thisVersionJson = "/thisVersion/"; 56 | var thisVersion = $.getJSON(thisVersionJson, function () { 57 | }) 58 | .done(function (thisVersion) { 59 | try { 60 | $('#thisVersion').text(thisVersion.version); 61 | } 62 | catch { 63 | if (!unloadingState) { 64 | $('#thisVersion').text("Error parsing."); 65 | } 66 | } 67 | }) 68 | .fail(function () { 69 | if (!unloadingState) { 70 | $('#thisVersion').text("Error loading."); 71 | } 72 | }) 73 | .always(function () { 74 | // Can post-process here 75 | if (loaded < numReq) { 76 | loaded++; 77 | } 78 | if (typeof callback == "function") { 79 | callback(); 80 | } 81 | }); 82 | } 83 | 84 | function populateForm() { // Get current parameters 85 | var url = "/config/"; 86 | var config = $.getJSON(url, function() {}) 87 | .done(function(config) { 88 | try { 89 | $('#mdnsid').val(config.hostname); 90 | $('#ispindhubname').val(config.ispindhub.name); 91 | $('#ispindhubTZInfo').val(config.ispindhub.TZ); 92 | $('#urltargeturl').val(config.urltarget.url); 93 | $('#urlfreq').val(config.urltarget.freq); 94 | $('#brewersfriendkey').val(config.brewersfriend.key); 95 | $('#brewersfriendfreq').val(config.brewersfriend.freq); 96 | $('#brewfatherkey').val(config.brewfather.key); 97 | $('#brewfatherfreq').val(config.brewfather.freq); 98 | } catch { 99 | if (!unloadingState) { 100 | alert("Unable to parse configuration data."); 101 | } 102 | } 103 | }) 104 | .fail(function() { 105 | if (!unloadingState) { 106 | alert("Unable to retrieve configuration data."); 107 | } 108 | }) 109 | .always(function() { 110 | // Can post-process here 111 | }); 112 | } 113 | 114 | 115 | function pollComplete() { // Poll to see if entire page is loaded 116 | if (loaded == numReq) { 117 | posted = true; 118 | // finishPage(); 119 | } else { 120 | setTimeout(pollComplete, 300); // try again in 300 milliseconds 121 | } 122 | } 123 | 124 | // POST Handlers: 125 | 126 | function processPost(obj) { 127 | posted = false; 128 | event.preventDefault(); 129 | hashLoc = window.location.hash; 130 | console.log(hashLoc); 131 | var $form = $(obj); 132 | url = $form.attr("action"); 133 | 134 | $("button[id='submitSettings']").prop('disabled', true); 135 | $("button[id='submitSettings']").html(' Updating'); 136 | 137 | // Switch here for hashLoc 138 | switch (hashLoc) { 139 | case "#ispindhub": 140 | processiSpindHubPost(url, obj); 141 | break; 142 | case "#temperature": 143 | processTemperaturePost(url, obj); 144 | break; 145 | case "#urltarget": 146 | processURLTargetPost(url, obj); 147 | break; 148 | case "#brewpiles": 149 | processBPiLessPost(url, obj); 150 | break; 151 | case "#brewersfriend": 152 | processBrewersFriendPost(url, obj); 153 | break; 154 | case "#brewfather": 155 | processBrewfatherPost(url, obj); 156 | break; 157 | case "#thingspeak": 158 | processThingSpeakPost(url, obj, 8); 159 | break; 160 | default: 161 | // Unknown hash location passed 162 | break; 163 | } 164 | buttonClearDelay(); 165 | } 166 | 167 | function buttonClearDelay() { // Poll to see if entire page is loaded 168 | if (posted) { 169 | $("button[id='submitSettings']").prop('disabled', false); 170 | $("button[id='submitSettings']").html('Update'); 171 | posted = false; 172 | } else { 173 | setTimeout(buttonClearDelay, 500); // try again in 300 milliseconds 174 | } 175 | } 176 | 177 | function processiSpindHubPost(url, obj) { 178 | // Handle Controller settings posts 179 | 180 | // Get form data 181 | var $form = $(obj), 182 | ispindhubname = $form.find("input[name='ispindhubname']").val(); 183 | ispindhubTZ = $form.find("select[name='ispindhubTZ'").val(); 184 | 185 | // Process post 186 | data = { 187 | ispindhubname: ispindhubname, 188 | ispindhubTZ : ispindhubTZ 189 | }; 190 | postData(url, data); 191 | } 192 | 193 | 194 | function processURLTargetPost(url, obj) { 195 | // Handle URL target posts 196 | 197 | // Get form data 198 | var $form = $(obj), 199 | urltargeturl = $form.find("input[name='urltargeturl']").val(), 200 | urlfreq = $form.find("input[name='urlfreq']").val(); 201 | 202 | // Process post 203 | data = { 204 | urltargeturl: urltargeturl, 205 | urlfreq: urlfreq 206 | }; 207 | postData(url, data); 208 | } 209 | 210 | function processBPiLessPost(url, obj) { 211 | // Handle URL target posts 212 | 213 | // Get form data 214 | var $form = $(obj), 215 | urltargeturl = $form.find("input[name='bpilessurl']").val(), 216 | urlfreq = $form.find("input[name='bpilessfreq']").val(); 217 | 218 | // Process post 219 | data = { 220 | urltargeturl: urltargeturl, 221 | urlfreq: urlfreq 222 | }; 223 | postData(url, data); 224 | } 225 | 226 | function processBrewersFriendPost(url, obj) { 227 | // Handle Brewer's Friend target posts 228 | 229 | // Get form data 230 | var $form = $(obj), 231 | brewersfriendkey = $form.find("input[name='brewersfriendkey']").val(), 232 | brewersfriendfreq = $form.find("input[name='brewersfriendfreq']").val(); 233 | 234 | // Process post 235 | data = { 236 | brewersfriendkey: brewersfriendkey, 237 | brewersfriendfreq: brewersfriendfreq 238 | }; 239 | postData(url, data); 240 | } 241 | 242 | function processBrewfatherPost(url, obj) { 243 | // Handle Brewfather target posts 244 | 245 | // Get form data 246 | var $form = $(obj), 247 | brewfatherkey = $form.find("input[name='brewfatherkey']").val(), 248 | brewfatherfreq = $form.find("input[name='brewfatherfreq']").val(); 249 | 250 | // Process post 251 | data = { 252 | brewfatherkey: brewfatherkey, 253 | brewfatherfreq: brewfatherfreq 254 | }; 255 | postData(url, data); 256 | } 257 | 258 | function processThingSpeakPost(url, obj) { 259 | // Handle ThingSpeak target posts 260 | 261 | // Get form data 262 | var $form = $(obj), 263 | thingspeakchannel = $form.find("input[name='thingspeakchannel']").val(), 264 | thingspeakkey = $form.find("input[name='thingspeakkey']").val(); 265 | thingspeakfreq = $form.find("input[name='thingspeakfreq']").val(); 266 | 267 | // Process post 268 | data = { 269 | thingspeakchannel: thingspeakchannel, 270 | thingspeakkey: thingspeakkey, 271 | thingspeakfreq: thingspeakfreq 272 | }; 273 | postData(url, data); 274 | } 275 | 276 | function postData(url, data, newpage = false, newdata = false, callback = null) { 277 | var loadNew = (newpage.length > 0); 278 | $.ajax({ 279 | url: url, 280 | type: 'POST', 281 | data: data, 282 | success: function(data) { 283 | // No alert 284 | }, 285 | error: function(data) { 286 | alert("Settings update failed."); 287 | }, 288 | complete: function(data) { 289 | if (loadNew) { 290 | window.location.href = newpage; 291 | } else if (newdata) { 292 | repopulatePage(true); 293 | } 294 | posted = true; 295 | if (typeof callback == "function") { 296 | callback(); 297 | } 298 | } 299 | }); 300 | } -------------------------------------------------------------------------------- /src/templatescreen.cpp: -------------------------------------------------------------------------------- 1 | #include "templatescreen.h" 2 | extern TFT_eSPI tft; 3 | extern String SG; 4 | extern String Voltage; 5 | extern String Temp; 6 | extern String Angle; 7 | extern String deviceName; 8 | extern String RSSI; 9 | extern String IP; 10 | extern String LastSeen; 11 | void parse_screen_template(String screen_template, String array_data[10], int last_seen_ms) 12 | { 13 | wdt_disable(); 14 | // Set Up the Scene with variables 15 | SG = array_data[5]; 16 | Voltage = array_data[4].substring(0, 4); 17 | Temp = array_data[3]; 18 | Angle = array_data[2].substring(0, 5); 19 | deviceName = array_data[1]; 20 | RSSI = array_data[7]; 21 | IP = WiFi.localIP().toString(); 22 | LastSeen = String(pretty_time(last_seen_ms)); 23 | 24 | // Open the Screen Template 25 | String fname = String("/data/templates/") + String(screen_template) + String(".json"); 26 | 27 | File file = LittleFS.open(fname, "r"); 28 | String parsingScreen = file.readString(); 29 | JsonDocument parsedScreen; 30 | // ReadLoggingStream loggingStream(parsingScreen, Serial); 31 | // ReadLoggingStream loggingStream(file, Serial); 32 | // DeserializationError errPars = deserializeJson(parsedScreen, loggingStream); 33 | // DeserializationError errPars = deserializeJson(parsedScreen,file); 34 | DeserializationError errPars = deserializeJson(parsedScreen, parsingScreen); 35 | if (errPars) 36 | { 37 | Serial.print(F("deserializeJson() for template failed: ")); 38 | Serial.println(errPars.f_str()); 39 | return; 40 | } 41 | else 42 | { 43 | JsonObject ScreenTemp = parsedScreen.as(); 44 | for (JsonPair p : ScreenTemp) 45 | { 46 | Serial.println(p.key().c_str()); // is a JsonString 47 | // Serial.println(p.value().as()); // is a JsonVariant 48 | String key_json = p.key().c_str(); 49 | if (key_json == "g") 50 | { 51 | // Serial.println("Let's handle Global parameters"); 52 | handle_global(p.value().as()); 53 | } 54 | else if (key_json.substring(0, 4) == "line") 55 | { 56 | // Serial.println("Let's hanndle parameters for a line"); 57 | handle_line(p.value().as()); 58 | } 59 | else 60 | { 61 | Serial.println("Entry in template not recognized"); 62 | } 63 | } 64 | } 65 | wdt_enable(WDTO_8S); 66 | parsedScreen.clear(); 67 | return; 68 | }; 69 | 70 | void handle_global(String global_json) 71 | { 72 | JsonDocument parsedGlobal; 73 | DeserializationError errPars = deserializeJson(parsedGlobal, global_json); 74 | if (errPars) 75 | { 76 | Serial.print(F("deserializeJson() for Global failed: ")); 77 | Serial.println(errPars.f_str()); 78 | return; 79 | } 80 | else 81 | { 82 | JsonObject globalTemp = parsedGlobal.as(); 83 | for (JsonPair p : globalTemp) 84 | { 85 | String key_json = p.key().c_str(); 86 | if (key_json == "bc") 87 | { 88 | String my_col = p.value().as(); 89 | uint32_t back_col = get_color(my_col); 90 | // Serial.println("We found a back color"); 91 | tft.fillScreen(back_col); 92 | } 93 | else if (key_json == "fc") 94 | { 95 | String my_col = p.value().as(); 96 | uint32_t font_col = get_color(my_col); 97 | // Serial.println("We found a font color"); 98 | tft.setTextColor(font_col); 99 | } 100 | else if (key_json == "tw") 101 | { 102 | bool wrap_flag = p.value().as(); 103 | // Serial.println("We found a Wrap Flag"); 104 | tft.setTextWrap(wrap_flag); 105 | } 106 | else if (key_json == "r") 107 | { 108 | int screen_rotation = p.value().as(); 109 | // Serial.println("We found a rotation flag"); 110 | tft.setRotation(screen_rotation); 111 | } 112 | else if (key_json == "f") 113 | { 114 | // uint8_t default_font = p.value().as(); 115 | // String def_font = p.value().as; 116 | // Serial.println("We found default Font"); 117 | if (p.value()) 118 | { 119 | tft.loadFont(p.value(), LittleFS); 120 | } 121 | 122 | // tft.setTextFont(default_font); 123 | } 124 | } 125 | } 126 | parsedGlobal.clear(); 127 | }; 128 | 129 | void handle_line(String line_json) 130 | { 131 | JsonDocument parsedLine; 132 | DeserializationError errPars = deserializeJson(parsedLine, line_json); 133 | if (errPars) 134 | { 135 | Serial.print(F("deserializeJson() for Line failed: ")); 136 | Serial.println(errPars.f_str()); 137 | return; 138 | } 139 | else 140 | { 141 | JsonObject lineTemp = parsedLine.as(); 142 | if (lineTemp.containsKey("t")) 143 | { 144 | if (lineTemp["t"] == "text") 145 | { 146 | // Serial.println("Let's deal with a text line"); 147 | make_text_line(lineTemp); 148 | } 149 | else if (lineTemp["t"] == "var_text") 150 | { 151 | Serial.println("Let's deal with a VARIABLE text line"); 152 | } 153 | else if (lineTemp["t"] == "line") 154 | { 155 | Serial.println("Let's deal with a line line"); 156 | make_line_line(lineTemp); 157 | } 158 | } 159 | } 160 | // parsedLine.clear(); 161 | return; 162 | } 163 | 164 | void make_text_line(JsonObject text_line_json) 165 | { 166 | // {"type":"text","font":"","color":"","x":0,"y":18,"center":false,"text":"SG: #SG"} 167 | String line_text = text_line_json["text"]; 168 | line_text.replace("#SG", SG); 169 | line_text.replace("#Temp", Temp); 170 | line_text.replace("#Angle", Angle); 171 | line_text.replace("#RSSI", RSSI); 172 | line_text.replace("#Voltage", Voltage); 173 | line_text.replace("#IP", IP); 174 | line_text.replace("#LastSeen", LastSeen); 175 | line_text.replace("#deviceName", deviceName); 176 | bool center_flag = text_line_json["ctr"]; 177 | int x_pos; 178 | int y_pos; 179 | if (text_line_json["x"] == "MAX") 180 | { 181 | x_pos = TFT_WIDTH; 182 | } 183 | else 184 | { 185 | x_pos = text_line_json["x"]; 186 | } 187 | if (text_line_json["y"] == "MAX") 188 | { 189 | y_pos = TFT_HEIGHT; 190 | } 191 | else 192 | { 193 | y_pos = text_line_json["y"]; 194 | } 195 | if (text_line_json.containsKey("x_delta")) 196 | { 197 | x_pos = x_pos + text_line_json["x_delta"].as(); 198 | } 199 | if (text_line_json.containsKey("y_delta")) 200 | { 201 | y_pos = y_pos + text_line_json["y_delta"].as(); 202 | } 203 | String text_font = text_line_json["f"]; 204 | String text_color = text_line_json["c"]; 205 | if (text_font.length() > 0) 206 | { 207 | char *da_font = get_font(text_font); 208 | tft.loadFont(da_font, LittleFS); 209 | } 210 | if (text_line_json.containsKey("s")) 211 | { 212 | int text_size = text_line_json["s"]; 213 | tft.setFreeFont(); 214 | tft.setTextSize(text_size); 215 | } 216 | 217 | if (text_line_json.containsKey("var")) 218 | { 219 | if (text_line_json.containsKey("cs")) 220 | { 221 | String tmp_var = text_line_json["var"]; 222 | int val_check; 223 | if (tmp_var == "#SG") 224 | { 225 | val_check = SG.toInt(); 226 | } 227 | else if ((tmp_var == "#RSSI")) 228 | { 229 | val_check = RSSI.substring(1, 3).toInt(); 230 | } 231 | else if ((tmp_var == "#Voltage")) 232 | { 233 | val_check = Voltage.toInt(); 234 | } 235 | else if ((tmp_var == "#Temp")) 236 | { 237 | val_check = Temp.toInt(); 238 | } 239 | else if ((tmp_var == "#Angle")) 240 | { 241 | val_check = Angle.toInt(); 242 | } 243 | 244 | int val_low = text_line_json["cs"][0]["val"].as(); 245 | String col_low = text_line_json["cs"][0]["col"]; 246 | int val_medium = text_line_json["cs"][1]["val"].as(); 247 | String col_medium = text_line_json["cs"][1]["col"]; 248 | int val_high = text_line_json["cs"][2]["val"].as(); 249 | String col_high = text_line_json["cs"][2]["col"]; 250 | String col_def = text_line_json["def_col"]; 251 | /* 252 | Serial.println(val_check); 253 | Serial.println(val_low); 254 | Serial.println(val_medium); 255 | Serial.println(val_high); 256 | */ 257 | if (val_check < val_low) 258 | { 259 | text_color = col_low; 260 | } 261 | else if (val_check < val_medium) 262 | { 263 | text_color = col_medium; 264 | } 265 | else if (val_check < val_high) 266 | { 267 | text_color = col_high; 268 | } 269 | else 270 | { 271 | text_color = col_def; 272 | } 273 | // Serial.println(text_color); 274 | uint16_t t_col = get_color(text_color); 275 | tft.setTextColor(t_col); 276 | } 277 | else 278 | { 279 | if (text_color.length() > 0) 280 | { 281 | // Serial.println("Let's change the Color for this line"); 282 | uint16_t t_col = get_color(text_color); 283 | tft.setTextColor(t_col); 284 | } 285 | } 286 | } 287 | else 288 | { 289 | if (text_color.length() > 0) 290 | { 291 | // Serial.println("Let's change the Color for this line"); 292 | uint16_t t_col = get_color(text_color); 293 | tft.setTextColor(t_col); 294 | } 295 | } 296 | if (center_flag) 297 | { 298 | // Serial.println("Printing a Centered Line"); 299 | centerString(line_text, (TFT_WIDTH / 2), y_pos); 300 | } 301 | else 302 | { 303 | // Serial.println("Printing a regular line"); 304 | tft.setCursor(x_pos, y_pos); 305 | tft.print(line_text); 306 | } 307 | // Serial.println(line_text); 308 | tft.unloadFont(); 309 | return; 310 | } 311 | 312 | void make_line_line(JsonObject line_line_json) 313 | { 314 | // Serial.println(line_line_json); 315 | int x_0 = line_line_json["x_0"]; 316 | int y_0 = line_line_json["y_0"]; 317 | int x_1; 318 | if (line_line_json["x_1"] == "MAX") 319 | { 320 | x_1 = TFT_WIDTH; 321 | } 322 | else 323 | { 324 | x_1 = line_line_json["x_1"]; 325 | } 326 | int y_1; 327 | if (line_line_json["y_1"] == "MAX") 328 | { 329 | y_1 = TFT_HEIGHT; 330 | } 331 | else 332 | { 333 | y_1 = line_line_json["y_1"]; 334 | } 335 | String line_color = line_line_json["c"]; 336 | uint16_t l_col = get_color(line_color); 337 | tft.drawLine(x_0, y_0, x_1, y_1, l_col); 338 | } 339 | 340 | char *get_font(String font) 341 | { 342 | if (font == "FreeSansGras12") 343 | { 344 | return FreeSansGras12; 345 | } 346 | else if (font == "FreeSans9") 347 | { 348 | return FreeSans9; 349 | } 350 | else if (font == "Arial9") 351 | { 352 | return Arial9; 353 | } 354 | else if (font == "Arial12") 355 | { 356 | return Arial12; 357 | } 358 | else if (font == "Arial20") 359 | { 360 | return Arial20; 361 | } 362 | else if (font == "RThin9") 363 | { 364 | return RThin9; 365 | } 366 | else if (font == "RThin12") 367 | { 368 | return RThin12; 369 | } 370 | else if (font == "RThin20") 371 | { 372 | return RThin20; 373 | } 374 | else if (font == "RBold20") 375 | { 376 | return RBold20; 377 | } 378 | else if (font == "TMS10") 379 | { 380 | return TMS10; 381 | } 382 | else if (font == "TMS12") 383 | { 384 | return TMS12; 385 | } 386 | else if (font == "SegLight20") 387 | { 388 | return SegLight20; 389 | } 390 | return RThin20; 391 | } 392 | --------------------------------------------------------------------------------