├── .gitignore ├── .travis.yml ├── README.md ├── lib └── MPPTLib │ ├── powerSupplies.cpp │ ├── powerSupplies.h │ ├── publishable.cpp │ ├── publishable.h │ ├── solar.cpp │ ├── solar.h │ ├── utils.cpp │ └── utils.h ├── platformio.ini ├── src ├── main.cpp └── version.h └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .piolibdeps 3 | .vscode 4 | .DS_Store 5 | wiki/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.9" 4 | sudo: false 5 | cache: 6 | ccache: true 7 | directories: 8 | - "~/.platformio" 9 | - "~/.buildcache" 10 | install: 11 | - pip install -U platformio 12 | - platformio update 13 | script: 14 | - platformio run 15 | after_success: 16 | - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh 17 | - chmod +x send.sh 18 | - ./send.sh success $DISCORD_WEBHOOK_URL 19 | after_failure: 20 | - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh 21 | - chmod +x send.sh 22 | - ./send.sh failure $DISCORD_WEBHOOK_URL 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # OSP Controller ☀️🕹 _now on [discord](https://discord.gg/GtR3JShfGu)_ 4 | 5 | _DC -> DC -> DC_ Solar. With a single used solar panel, a few used batteries, and $40 in parts you can power your life, transportation and all. Add an ESP32 Arduino to a 95% efficient DC-DC buck converter controlled over serial and you get an internet-connected, privately hosted smart solar MPPT power system. [Parts list](https://github.com/opensolarproject/OSPController/wiki/Step-1-Parts-List). [Instructions](https://github.com/opensolarproject/OSPController/wiki). [About](https://github.com/opensolarproject/OSPController/wiki/About). Go build one! (And reach out! I'm happy to help) 6 |
7 | 8 | [![GitHub version](https://img.shields.io/github/release/opensolarproject/OSPController.svg?style=flat-square)](https://github.com/opensolarproject/OSPController/releases/latest) 9 | [![version since](https://img.shields.io/github/commits-since/opensolarproject/OSPController/latest.svg?style=flat-square&color=green)](https://github.com/opensolarproject/OSPController/commits) 10 | [![version date](https://img.shields.io/github/release-date/opensolarproject/OSPController.svg?style=flat-square)](https://github.com/opensolarproject/OSPController/commits) 11 | [![GitHub download](https://img.shields.io/github/downloads/opensolarproject/OSPController/total.svg?style=flat-square&color=green)](https://github.com/opensolarproject/OSPController/releases/latest) 12 | [![build](https://img.shields.io/travis/opensolarproject/OSPController.svg?style=flat-square)](https://travis-ci.com/github/opensolarproject/OSPController) 13 | 14 | [![Language Type](https://img.shields.io/github/languages/top/opensolarproject/OSPController?style=flat-square)](https://github.com/opensolarproject/OSPController/commits) 15 | [![GitHub stars](https://img.shields.io/github/stars/opensolarproject/OSPController.svg?style=flat-square&label=Star)](https://github.com/arendst/Tasmota/stargazers) 16 | [![GitHub forks](https://img.shields.io/github/forks/opensolarproject/OSPController.svg?style=flat-square&label=Fork)](https://github.com/opensolarproject/OSPController/network) 17 | [![Issues open](https://img.shields.io/github/issues/opensolarproject/OSPController?style=flat-square)](https://github.com/opensolarproject/OSPController/issues) 18 | [![Issues closed](https://img.shields.io/github/issues-closed/opensolarproject/OSPController?style=flat-square&color=green)](https://github.com/opensolarproject/OSPController/issues) 19 | [![Chat](https://img.shields.io/discord/720686061159841852.svg?style=flat-square&color=blueviolet)](https://discord.gg/GtR3JShfGu) 20 | 21 | | ![dashboard view](https://raw.githubusercontent.com/wiki/opensolarproject/OSPController/images/charts-grafana.png) | 22 | :-------------------------:| 23 | | A dashboard view Grafana (optional). More details & options [here](https://github.com/opensolarproject/OSPController/wiki/Step-4-Data-Visualization) | 24 | 25 | ### This solar controller: 26 | - Costs less than $35 in [total parts](https://github.com/opensolarproject/OSPController/wiki/Step-1-Parts-List) 27 | - Works with 12 - 82VDC Solar Panels, _(enabling big and efficient strings of panels!)_ 28 | - Works with 4.2 - 60VDC batteries. Directly charge your high-voltage eBike batteries! 29 | - Is open source, modify it as you wish! 30 | - Connects to your MQTT smart home 31 | - Lets you own your own data 32 | - Gives you [graphs and charts](https://github.com/opensolarproject/OSPController/wiki/Step-4:-Data-Visualization) about your system from anywhere 33 | 34 | ### But really, head over [to the wiki](https://github.com/opensolarproject/OSPController/wiki) for 35 | 36 | - [Background & About](https://github.com/opensolarproject/OSPController/wiki/About) 37 | - [Part 1:Parts](https://github.com/opensolarproject/OSPController/wiki/Step-1-Parts-List) 38 | - [Part 2:Hardware](https://github.com/opensolarproject/OSPController/wiki/Step-2-Hardware-Build) 39 | - [Part 3:Software](https://github.com/opensolarproject/OSPController/wiki/Step-3-Software-Setup) 40 | - [Part 4:Data](https://github.com/opensolarproject/OSPController/wiki/Step-4-Data-Visualization) 41 | - [Part 5:Wiring](https://github.com/opensolarproject/OSPController/wiki/Step-5-Wiring-Things) 42 | 43 | ## Also join the [Discord Channel](https://discord.gg/GtR3JShfGu) 44 | It's the discussion board to talk shop, get ideas, get help, triage issues, and share success! [discord.gg/MRQvKR](https://discord.gg/GtR3JShfGu) 45 | 46 | -------------------------------------------------------------------------------- /lib/MPPTLib/powerSupplies.cpp: -------------------------------------------------------------------------------- 1 | #include "powerSupplies.h" 2 | #include 3 | #include 4 | #include // ModbusMaster 5 | #include "utils.h" 6 | 7 | //form: rxpin,txpin[sw]:baud 8 | Stream* makeStream(String s, int baud) { 9 | auto sp1 = split(s, ":"); 10 | if (sp1.second.length()) //specify baud rate 11 | baud = sp1.second.toInt(); 12 | int rx = -1, tx = -1; 13 | if (sp1.first.length()) { //specify pins 14 | bool useSw = suffixed(& sp1.first, "sw"); 15 | auto pins = split(sp1.first, ","); 16 | rx = pins.first.length()? pins.first.toInt() : -1; 17 | tx = pins.second.length()? pins.second.toInt() : -1; 18 | if (useSw) { 19 | auto ret = new SoftwareSerial; 20 | ret->begin(baud, SWSERIAL_8N1, rx, tx, false); 21 | return ret; 22 | } 23 | } 24 | Serial2.begin(baud, SERIAL_8N1, rx, tx, false, 1000); 25 | return &Serial2; 26 | } 27 | 28 | PowerSupply* PowerSupply::make(String type) { 29 | type.toLowerCase(); 30 | auto sp1 = split(type, ":"); 31 | PowerSupply* ret = NULL; 32 | String typeUp = type; 33 | typeUp.toUpperCase(); 34 | if (typeUp.startsWith("DP")) { 35 | ret = new DPS(makeStream(sp1.second, 19200)); 36 | } else if (typeUp.startsWith("DROK")) { 37 | ret = new Drok(makeStream(sp1.second, 4800)); 38 | } else { //default 39 | ret = NULL; 40 | } 41 | if (ret) ret->type_ = type; 42 | return ret; 43 | } 44 | 45 | 46 | // ----------------------- // 47 | // ----- PowerSupply ----- // 48 | // ----------------------- // 49 | 50 | PowerSupply::PowerSupply() { } 51 | PowerSupply::~PowerSupply() { 52 | String ret; 53 | if (auto hw = dynamic_cast(port_)) { 54 | hw->end(); ret += "ended HW "; 55 | } else if (auto sw = dynamic_cast(port_)) { 56 | sw->end(); ret += "ended SW "; 57 | } 58 | if ((port_ != &Serial) && (port_ != &Serial1) && (port_ != &Serial2)) { 59 | delete(port_); 60 | ret += "deleted "; 61 | } 62 | log("~PowerSupply " + ret); 63 | } 64 | 65 | String PowerSupply::toString() const { 66 | return str("PSU-out[%0.2fV %0.2fA]-lim[%0.2fV %0.2fA]", outVolt_, outCurr_, limitVolt_, limitCurr_) 67 | + (outEn_? " ENABLED":"") + (isCV()? " CV":"") + (isCC()? " CC":"") + (isCollapsed()? " CLPS":""); 68 | } 69 | 70 | bool PowerSupply::isCV() const { return ((limitVolt_ - outVolt_) / limitVolt_) < 0.004; } 71 | bool PowerSupply::isCC() const { return ((limitCurr_ - outCurr_) / limitCurr_) < 0.02; } 72 | bool PowerSupply::isCollapsed() const { return outEn_ && !isCV() && !isCC(); } 73 | 74 | void PowerSupply::doTotals() { 75 | wh_ += outVolt_ * outCurr_ * (millis() - lastAmpUpdate_) / 1000.0 / 60 / 60; 76 | currFilt_ = currFilt_ - 0.1 * (currFilt_ - outCurr_); 77 | lastAmpUpdate_ = millis(); 78 | } 79 | 80 | 81 | // ---------------------- // 82 | // -------- Drok -------- // 83 | // ---------------------- // 84 | 85 | Drok::Drok(Stream* port) : PowerSupply() { port_ = port; } 86 | Drok::~Drok() { } 87 | 88 | bool Drok::begin() { 89 | flush(); 90 | return doUpdate(); 91 | } 92 | 93 | bool Drok::doUpdate() { 94 | bool res = readVoltage() && 95 | readCurrent() && 96 | readOutputEnabled(); 97 | if (res && !limitVolt_) { 98 | handleReply(cmdReply("arc")); //read current limit 99 | handleReply(cmdReply("arv")); //read voltage limit 100 | log(getType() + str(" finished begin, got %0.3fV %0.3fA limits\n", limitVolt_, limitCurr_)); 101 | } 102 | return res; 103 | } 104 | 105 | bool Drok::readVoltage() { return handleReply(cmdReply("aru")); } 106 | bool Drok::readCurrent() { return handleReply(cmdReply("ari")); } 107 | bool Drok::readOutputEnabled() { return handleReply(cmdReply("aro")); } 108 | 109 | templatevoid setCheck(T &save, float in, float max) { if (in < max) save = in; } 110 | 111 | bool Drok::handleReply(const String &msg) { 112 | if (!msg.length()) return false; 113 | String hdr = msg.substring(0, 3); 114 | String body = msg.substring(3); 115 | if (hdr == "#ro") setCheck(outEn_, (body.toInt() == 1), 2); 116 | else if (hdr == "#ru") setCheck(outVolt_, body.toFloat() / 100.0, 80); 117 | else if (hdr == "#rv") setCheck(limitVolt_, body.toFloat() / 100.0, 80); 118 | else if (hdr == "#ra") setCheck(limitCurr_, body.toFloat() / 100.0, 15); 119 | else if (hdr == "#ri") { 120 | setCheck(outCurr_, body.toFloat() / 100.0, 15); 121 | doTotals(); 122 | } else { 123 | log(getType() + " got unknown msg > '" + hdr + "' / '" + body + "'"); 124 | return false; 125 | } 126 | lastSuccess_ = millis(); 127 | return true; 128 | } 129 | 130 | void Drok::flush() { 131 | port_->flush(); 132 | } 133 | 134 | String Drok::cmdReply(const String &cmd) { 135 | port_->print(cmd + "\r\n"); 136 | String tolog; 137 | if (debug_) tolog += " > '" + cmd + "CRLF'"; 138 | String reply; 139 | uint32_t start = millis(); 140 | char c; 141 | while ((millis() - start) < 1000 && !reply.endsWith("\n")) 142 | if (port_->readBytes(&c, 1)) 143 | reply.concat(c); 144 | if (debug_ && reply.length()) { 145 | tolog += " < '" + reply + "'"; 146 | tolog.replace("\r", "CR"); 147 | tolog.replace("\n", "NL"); 148 | log(getType() + tolog); 149 | } 150 | if (!reply.length() && debug_ && port_->available()) 151 | log(getType() + " nothing read.. stuff available!? " + String(port_->available())); 152 | reply.trim(); 153 | return reply; 154 | } 155 | 156 | bool Drok::enableOutput(bool status) { 157 | String r = cmdReply(str("awo%d\r\n", status ? 1 : 0)); 158 | if (r == "#wook") { 159 | outEn_ = status; 160 | return true; 161 | } else return false; 162 | } 163 | 164 | String Drok::fourCharStr(uint16_t input) { 165 | char buf[] = " "; 166 | // Iterate through units, tens, hundreds etc 167 | for (int digit = 0; digit < 4; digit++) 168 | buf[3 - digit] = ((input / ((int) pow(10, digit))) % 10) + '0'; 169 | return String(buf); 170 | } 171 | 172 | bool Drok::setVoltage(float v) { 173 | limitVolt_ = v; 174 | String r = cmdReply("awu" + fourCharStr(v * 100.0)); 175 | return (r == "#wuok"); 176 | } 177 | 178 | bool Drok::setCurrent(float v) { 179 | limitCurr_ = v; 180 | String r = cmdReply("awi" + fourCharStr(v * 100.0)); 181 | return (r == "#wiok"); 182 | } 183 | 184 | 185 | // ----------------------- // 186 | // --------- DPS --------- // 187 | // ----------------------- // 188 | 189 | DPS::DPS(Stream* port) : PowerSupply(), bus_(new ModbusMaster), dps5020_(false) { port_ = port; } 190 | DPS::~DPS() { } 191 | 192 | bool DPS::begin() { 193 | bus_->begin(1, *port_); 194 | if (doUpdate()) { 195 | if (bus_->readHoldingRegisters(0x000B, 2) == bus_->ku8MBSuccess) { 196 | uint16_t model = bus_->getResponseBuffer(0); 197 | uint16_t version = bus_->getResponseBuffer(1); 198 | dps5020_ = (model == 5020); 199 | log(getType() + str(" begin model/version %d %d %d", model, version, dps5020_)); 200 | return true; 201 | } 202 | } 203 | return false; 204 | } 205 | 206 | bool DPS::doUpdate() { 207 | //read a range of 16-bit registers starting at register 0 to 10 208 | try { 209 | if (bus_->readHoldingRegisters(0x0000, 10) == bus_->ku8MBSuccess) { 210 | limitVolt_ = ((float)bus_->getResponseBuffer(0) / 100 ); 211 | limitCurr_ = ((float)bus_->getResponseBuffer(1) / (dps5020_? 100 : 1000) ); 212 | outVolt_ = ((float)bus_->getResponseBuffer(2) / 100 ); 213 | outCurr_ = ((float)bus_->getResponseBuffer(3) / (dps5020_? 100 : 1000) ); 214 | // float power = ((float)bus_->getResponseBuffer(4) / 100 ); 215 | inputVolts_ = ((float)bus_->getResponseBuffer(5) / 100 ); 216 | cc_ = ((bool)bus_->getResponseBuffer(8) ); 217 | outEn_ = ((bool)bus_->getResponseBuffer(9) ); 218 | doTotals(); 219 | lastSuccess_ = millis(); 220 | return true; 221 | } else log(getType() + " error fetching registers"); 222 | } catch (std::runtime_error e) { 223 | log(getType() + " caught exception in DPS::update " + String(e.what())); 224 | } catch (...) { 225 | log(getType() + " caught unknown exception in DPS::update"); 226 | } 227 | return false; 228 | } 229 | bool DPS::enableOutput(bool en) { 230 | return bus_->writeSingleRegister(0x0009, en) == bus_->ku8MBSuccess; 231 | } 232 | 233 | bool DPS::setVoltage(float v) { 234 | return bus_->writeSingleRegister(0x0000, ((limitVolt_ = v)) * 100) == bus_->ku8MBSuccess; 235 | } 236 | bool DPS::setCurrent(float c) { 237 | return bus_->writeSingleRegister(0x0001, ((limitCurr_ = c)) * (dps5020_? 100 : 1000)) == bus_->ku8MBSuccess; 238 | } 239 | 240 | bool DPS::isCC() const { return dps5020_? PowerSupply::isCC() : cc_; } //5020 cc doesn't report correctly 241 | 242 | bool DPS::getInputVolt(float* v) const { 243 | if (v) *v = inputVolts_; 244 | return true; 245 | } 246 | -------------------------------------------------------------------------------- /lib/MPPTLib/powerSupplies.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | class Stream; 6 | 7 | class PowerSupply { 8 | public: 9 | String type_; 10 | Stream *port_ = NULL; 11 | bool debug_ = false; 12 | float outVolt_ = 0, outCurr_ = 0; 13 | float limitVolt_ = 0, limitCurr_ = 0; 14 | float currFilt_ = 0.0, wh_ = 0; 15 | bool outEn_ = false; 16 | uint32_t lastSuccess_ = 0, lastAmpUpdate_ = 0; 17 | 18 | static PowerSupply* make(String type); 19 | PowerSupply(); 20 | virtual ~PowerSupply(); 21 | virtual bool begin() = 0; 22 | virtual bool doUpdate() = 0; 23 | virtual bool readCurrent() { return doUpdate(); }; 24 | 25 | virtual bool setVoltage(float) = 0; 26 | virtual bool setCurrent(float) = 0; 27 | virtual bool enableOutput(bool) = 0; 28 | 29 | virtual bool isCV() const; 30 | virtual bool isCC() const; 31 | virtual bool isCollapsed() const; 32 | virtual bool getInputVolt(float* v) const { return false; } 33 | virtual String toString() const; 34 | String getType() const { return type_; } 35 | virtual bool isDrok() const { return true; } 36 | protected: 37 | void doTotals(); 38 | }; 39 | 40 | class Drok : public PowerSupply { 41 | public: 42 | Drok(Stream*); 43 | ~Drok(); 44 | bool begin() override; 45 | 46 | String cmdReply(const String &cmd); 47 | bool setVoltage(float) override; 48 | bool setCurrent(float) override; 49 | bool enableOutput(bool) override; 50 | 51 | bool doUpdate() override; //runs these next three: 52 | bool readCurrent() override; 53 | bool readVoltage(); 54 | bool readOutputEnabled(); 55 | void flush(); 56 | 57 | private: 58 | bool handleReply(const String &); 59 | String fourCharStr(uint16_t input); 60 | }; 61 | 62 | class ModbusMaster; 63 | 64 | class DPS : public PowerSupply { 65 | ModbusMaster* bus_; 66 | public: 67 | float inputVolts_ = 0; 68 | bool cc_ = false; 69 | bool dps5020_ = false; 70 | 71 | DPS(Stream*); 72 | ~DPS(); 73 | bool begin() override; 74 | 75 | bool setVoltage(float) override; 76 | bool setCurrent(float) override; 77 | bool enableOutput(bool) override; 78 | 79 | bool doUpdate() override; //runs these next three: 80 | 81 | bool isCC() const override; 82 | bool getInputVolt(float* v) const override; 83 | bool isDrok() const override { return false; } 84 | }; 85 | -------------------------------------------------------------------------------- /lib/MPPTLib/publishable.cpp: -------------------------------------------------------------------------------- 1 | #include "publishable.h" 2 | #include "utils.h" 3 | #include 4 | #include 5 | 6 | template 7 | struct Pub : PubItem { 8 | T value; 9 | Pub(String k, T v, int p) : PubItem(k,p), value(v) { } 10 | ~Pub() { } 11 | String toString() const override { return String(*value); } 12 | String jsonValue() const override { return toString(); } 13 | String set(String v) override { *value = v.toFloat(); return toString(); } 14 | void const* val() const override { return value; } 15 | void save(Preferences&p) override { p.putBytes(key.c_str(), value, sizeof(*value)); } 16 | void load(Preferences&p) override { p.getBytes(key.c_str(), value, sizeof(*value)); } 17 | bool isAction() const override { return false; } 18 | }; 19 | 20 | String prefGetString(Preferences&p, String key) { 21 | char buf[128]; 22 | size_t l = p.getBytes(key.c_str(), buf, 128); 23 | if (l == 0) return ""; 24 | buf[l] = 0; //null terminate 25 | return String(buf); 26 | } 27 | 28 | template<> String Pub::toString() const { return String(*value, 3); } 29 | template<> String Pub::toString() const { return (*value)? "true":"false"; } 30 | template<> String Pub::toString() const { return (value)(""); } 31 | template<> String Pub::jsonValue() const { return "\"" + toString() + "\""; } 32 | template<> String Pub::set(String v) { (*value) = v=="on" || v=="true" || v=="1"; return toString(); } 33 | template<> String Pub::set(String v) { return (value)(v); } 34 | template<> void const* Pub::val() const { return &value; } 35 | 36 | template<> bool Pub::isAction()const { return true; } 37 | template<> void Pub::save(Preferences&p) { String v = (value)(""); p.putBytes(key.c_str(), v.c_str(), v.length()); } 38 | template<> void Pub::load(Preferences&p) { String v = prefGetString(p, key); if (v.length()) try { (value)(v); } catch(...) { } } 39 | template<> String Pub::set(String v) { return (*value) = v; } 40 | template<> String Pub::toString() const { return (*value); } 41 | template<> String Pub::jsonValue() const { return "\"" + toString() + "\""; } 42 | template<> void Pub::save(Preferences&p) { p.putBytes(key.c_str(), value->c_str(), value->length()); } 43 | template<> void Pub::load(Preferences&p) { 44 | (*value) = prefGetString(p, key); 45 | } 46 | 47 | Publishable::Publishable() : lock_(xSemaphoreCreateMutex()) { 48 | add("save", [this](String s){ 49 | return str("saved %d prefs", this->savePrefs()); 50 | }).hide(); 51 | add("load", [this](String s){ 52 | return str("loaded %d prefs", this->loadPrefs()); 53 | }).hide(); 54 | add("help", [this](String s){ printHelp(); return ""; }).hide(); 55 | add("list", [this](String s){ printHelp(); return ""; }).hide(); 56 | } 57 | 58 | void Publishable::log(const String &s) { 59 | Serial.println(s); 60 | if (xSemaphoreTake(lock_, (TickType_t) 100) == pdTRUE) { 61 | logPub_.push_back(s); 62 | xSemaphoreGive(lock_); 63 | } 64 | } 65 | bool Publishable::popLog(String *s) { 66 | if (xSemaphoreTake(lock_, (TickType_t) 100) == pdTRUE) { 67 | bool got = logPub_.size() > 0; 68 | if (got) (*s) = logPub_.pop_front(); 69 | xSemaphoreGive(lock_); 70 | return got; 71 | } 72 | return false; 73 | } 74 | void Publishable::logNote(const String &s) { 75 | if (xSemaphoreTake(lock_, (TickType_t) 100) == pdTRUE) { 76 | logNote_ += " " + s; 77 | xSemaphoreGive(lock_); 78 | } else Serial.println("LOGNOTE couldn't get mutex! " + s); 79 | } 80 | String Publishable::popNotes() { 81 | if (xSemaphoreTake(lock_, (TickType_t) 100) == pdTRUE) { 82 | String ret = logNote_; 83 | logNote_ = ""; 84 | xSemaphoreGive(lock_); 85 | return ret; 86 | } 87 | return ""; 88 | } 89 | 90 | PubItem& Publishable::add(PubItem* p) { items_[p->key] = p; return *p; } 91 | PubItem& Publishable::add(String k, double &v, int p) { return add(new Pub(k,&v,p)); } 92 | PubItem& Publishable::add(String k, float &v, int p) { return add(new Pub (k,&v,p)); } 93 | PubItem& Publishable::add(String k, int &v, int p) { return add(new Pub (k,&v,p)); } 94 | PubItem& Publishable::add(String k, bool &v, int p) { return add(new Pub (k,&v,p)); } 95 | PubItem& Publishable::add(String k, String &v, int p) { return add(new Pub(k,&v,p)); } 96 | PubItem& Publishable::add(String k, Action v, int p) { return add(new Pub(k, v,p)); } 97 | 98 | int Publishable::loadPrefs() { 99 | Preferences prefs; //destructor calls end() 100 | prefs.begin("Publishable", true); //read only 101 | int ret = 0; 102 | for (const auto & i : items_) 103 | if (i.second->pref_) { 104 | i.second->load(prefs); 105 | i.second->dirty_ = true; 106 | Serial.println("loaded key " + i.first + " to " + i.second->toString()); 107 | ret++; 108 | } 109 | return ret; 110 | } 111 | int Publishable::savePrefs() { 112 | Preferences prefs; 113 | prefs.begin("Publishable", false); //read-write 114 | int ret = 0; 115 | for (const auto & i : items_) 116 | if (i.second->pref_) { 117 | i.second->save(prefs); 118 | Serial.println("saved key " + i.first + " to " + i.second->toString() + str(" (%d free)", prefs.freeEntries())); 119 | ret++; 120 | } 121 | return ret; 122 | } 123 | bool Publishable::clearPrefs() { 124 | Preferences prefs; 125 | prefs.begin("Publishable", false); //read-write 126 | return prefs.clear(); 127 | } 128 | 129 | String Publishable::handleCmd(String cmd) { 130 | cmd.trim(); 131 | int pivot = cmd.indexOf('='); 132 | if (pivot < 0) pivot = cmd.indexOf(' '); 133 | return handleSet(cmd.substring(0,pivot), cmd.substring(pivot + 1)); 134 | } 135 | 136 | String Publishable::handleSet(String key, String val) { 137 | for (auto i : items_) 138 | if (i.first == key) { 139 | try { 140 | String ret = i.second->set(val); 141 | i.second->dirty_ = true; 142 | return (ret.length())? ret : ("set " + key + " to " + val); 143 | } catch (std::runtime_error e) { 144 | return "error setting '" + key + "' to '" + val + "': " + String(e.what()); 145 | } 146 | } 147 | return "unknown key " + key; 148 | } 149 | 150 | std::list Publishable::items(bool dirtyOnly) const { 151 | std::list ret; 152 | for (const auto & i : items_) 153 | if (!dirtyOnly || i.second->dirty_) 154 | if (! i.second->hidden_) 155 | ret.push_back(i.second); 156 | return ret; 157 | } 158 | 159 | void Publishable::clearDirty() { for (auto &i : items_) i.second->dirty_ = false; } 160 | void Publishable::setDirty(std::listdlist) { for (auto i : dlist) setDirty(i); } 161 | void Publishable::setDirty(String key) { 162 | auto it = items_.find(key); 163 | if (it != items_.end()) it->second->dirty_ = true; 164 | else Serial.println("Pub::setDirty missing key" + key); 165 | } 166 | void Publishable::setDirtyAddr(void const* v) { 167 | for (const auto & i : items_) 168 | if (v == i.second->val()) 169 | { i.second->dirty_ = true; return; } 170 | Serial.printf("Pub::setDirty missing addr %p\n", v); 171 | } 172 | 173 | void Publishable::poll(Stream* stream) { 174 | static String buff; 175 | if (stream->available()) { //cmd val 176 | buff += stream->readString(); 177 | int end = -1; 178 | while ((end = buff.indexOf('\n')) > 0) { 179 | stream->println(handleCmd(buff.substring(0, end))); //TODO - 1 to exclude newline? 180 | buff = buff.substring(end + 1); 181 | buff.trim(); 182 | } 183 | } 184 | } 185 | 186 | void Publishable::printHelp() const { 187 | Serial.println("help:"); 188 | std::list sorted; 189 | for (auto i : items_) 190 | if (i.second->pref_) sorted.push_front(i.second); 191 | else sorted.push_back(i.second); 192 | for (auto i : sorted) 193 | if (i->isAction()) Serial.println("- " + i->key + " [action]"); 194 | else Serial.println("- " + i->key + " = " + i->toString()); 195 | if (WiFi.isConnected()) 196 | Serial.println("** IP: " + WiFi.localIP().toString()); 197 | } 198 | 199 | String Publishable::toJson() const { 200 | String ret = "{\n"; 201 | for (const auto & i : items_) 202 | if (!i.second->hidden_ && !i.second->pref_) 203 | ret += " \"" + i.first + "\":" + i.second->jsonValue() + ",\n"; 204 | ret += "\"prefs\":{\n"; 205 | for (const auto & i : items_) 206 | if (!i.second->hidden_ && i.second->pref_) 207 | ret += " \"" + i.first + "\":" + i.second->jsonValue() + ",\n"; 208 | ret.remove(ret.length() - 2, 2); //remove trailing comma + LF 209 | ret += " }\n"; 210 | return ret + "\n}\n"; 211 | } 212 | -------------------------------------------------------------------------------- /lib/MPPTLib/publishable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "utils.h" 9 | 10 | class Stream; 11 | class PubSubClient; 12 | class WebServer; 13 | class Preferences; 14 | typedef std::function Action; 15 | typedef std::function StrFn; 16 | typedef std::function SetFn; 17 | 18 | #define DEFAULT_PERIOD -1 19 | 20 | struct PubItem { 21 | String key; 22 | int period; 23 | bool pref_, hidden_, dirty_; 24 | PubItem(String k, int p) : key(k), period(p), pref_(false), hidden_(false), dirty_(false) { } 25 | virtual ~PubItem() { } 26 | virtual String toString() const = 0; 27 | virtual String jsonValue() const = 0; 28 | virtual String set(String v) = 0; 29 | virtual void const* val() const = 0; 30 | virtual void save(Preferences&) = 0; 31 | virtual void load(Preferences&) = 0; 32 | virtual PubItem& pref() { pref_ = true; return *this; } 33 | virtual PubItem& hide() { hidden_ = true; return *this; } 34 | virtual bool isAction() const = 0; 35 | }; 36 | 37 | class Publishable { 38 | public: 39 | Publishable(); 40 | 41 | //TODO support publishing cap in msgs/minute 42 | 43 | PubItem& add(String name, double &, int pubPeriod = DEFAULT_PERIOD); 44 | PubItem& add(String name, float &, int pubPeriod = DEFAULT_PERIOD); 45 | PubItem& add(String name, int &, int pubPeriod = DEFAULT_PERIOD); 46 | PubItem& add(String name, bool &, int pubPeriod = DEFAULT_PERIOD); 47 | PubItem& add(String name, String &, int pubPeriod = DEFAULT_PERIOD); 48 | PubItem& add(String name, Action, int pubPeriod = DEFAULT_PERIOD); 49 | 50 | void poll(Stream*); 51 | String handleCmd(String cmd); 52 | String handleSet(String key, String val); 53 | String toJson() const; 54 | int loadPrefs(); 55 | int savePrefs(); 56 | bool clearPrefs(); 57 | std::list items(bool dirtyOnly=true) const; 58 | void setDirty(String key); 59 | void setDirtyAddr(void const*); 60 | void setDirty(std::list); 61 | void clearDirty(); 62 | void printHelp() const; 63 | 64 | void log(const String &); 65 | bool popLog(String*); 66 | void logNote(const String &); //adds note to next status 67 | String popNotes(); 68 | // void log(const char *fmtStr, ...); 69 | 70 | private: 71 | PubItem& add(PubItem*); 72 | std::map items_; 73 | int defaultPeriod_ = 12000; 74 | String logNote_; 75 | CircularArray logPub_; 76 | SemaphoreHandle_t lock_; 77 | }; 78 | 79 | -------------------------------------------------------------------------------- /lib/MPPTLib/solar.cpp: -------------------------------------------------------------------------------- 1 | #include "solar.h" 2 | #include "utils.h" 3 | #include "powerSupplies.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace std::placeholders; 12 | #define ckPSUs() if (!psu_) { return String("no psu"); } 13 | 14 | WiFiClient espClient; 15 | 16 | Solar::~Solar() { } 17 | 18 | Solar::Solar(String version) : 19 | version_(version), 20 | state_(States::off), 21 | server_(80), 22 | pub_() { 23 | db_.client.setClient(espClient); 24 | } 25 | 26 | // void runLoop(void*c) { ((Solar*)c)->loopTask(); } 27 | void runPubt(void*c) { ((Solar*)c)->publishTask(); } 28 | 29 | //TODO: make these class members instead? 30 | uint32_t nextVmeas_ = 0, nextPub_ = 20000, nextPrint_ = 0; 31 | uint32_t nextPSUpdate_ = 0, nextSolarAdjust_ = 1000; 32 | uint32_t nextAutoSweep_ = 0, lastAutoSweep_ = 0; 33 | extern const String updateIndex; 34 | String doOTAUpdate_ = ""; 35 | uint32_t espSketchSize_ = 0; 36 | 37 | class Backoff : public std::runtime_error { public: 38 | Backoff(String s) : std::runtime_error(s.c_str()) { } 39 | }; 40 | 41 | void Solar::setup() { 42 | Serial.begin(115200); 43 | Serial.setTimeout(10); //very fast, need to keep the ctrl loop running 44 | addLogger(&pub_); //sets global context 45 | espSketchSize_ = ESP.getSketchSize(); 46 | delay(100); 47 | log(getResetReasons()); 48 | uint64_t fusemac = ESP.getEfuseMac(); 49 | uint8_t* chipid = (uint8_t*) & fusemac; 50 | String mac = str("%02x:%02x:%02x:%02x:%02x:%02x", chipid[0], chipid[1], chipid[2], chipid[3], chipid[4], chipid[5]); 51 | log("startup, MAC " + id_); 52 | id_ = "mppt-" + str("%02x", chipid[5]); 53 | log("startup, ID " + id_); 54 | //TODO analogSetCycles(32); <- removed in recent version. test if needs replacing 55 | 56 | pub_.add("wifiap", wifiap).hide().pref(); 57 | pub_.add("wifipass", wifipass).hide().pref(); 58 | pub_.add("mqttServ", db_.serv).hide().pref(); 59 | pub_.add("mqttUser", db_.user).hide().pref(); 60 | pub_.add("mqttPass", db_.pass).hide().pref(); 61 | pub_.add("mqttFeed", db_.feed).hide().pref(); 62 | pub_.add("inPin", pinInvolt_).pref(); 63 | pub_.add("lvProtect", std::bind(&Solar::setLVProtect, this, _1)).pref(); 64 | pub_.add("psu", std::bind(&Solar::setPSU, this, _1)).pref(); 65 | pub_.add("outputEN",[=](String s){ ckPSUs(); if (s.length()) psu_->enableOutput(s == "on"); return String(psu_->outEn_); }); 66 | pub_.add("outvolt", [=](String s){ ckPSUs(); if (s.length()) psu_->setVoltage(s.toFloat()); return String(psu_->outVolt_); }); 67 | pub_.add("outcurr", [=](String s){ ckPSUs(); if (s.length()) psu_->setCurrent(s.toFloat()); return String(psu_->outCurr_); }); 68 | pub_.add("outpower",[=](String){ ckPSUs(); return String(psu_->outVolt_ * psu_->outCurr_); }); 69 | pub_.add("currFilt",[=](String){ ckPSUs(); return String(psu_->currFilt_); }); 70 | pub_.add("state", state_ ); 71 | pub_.add("pgain", pgain_ ).pref(); 72 | pub_.add("ramplimit", ramplimit_ ).pref(); 73 | pub_.add("setpoint", setpoint_ ).pref(); 74 | pub_.add("vadjust", vadjust_ ).pref(); 75 | pub_.add("printperiod",printPeriod_ ).pref(); 76 | pub_.add("pubperiod", db_.period ).pref(); 77 | pub_.add("adjustperiod",adjustPeriod_ ).pref(); 78 | pub_.add("measperiod", measperiod_ ).pref(); 79 | pub_.add("autosweep", autoSweep_ ).pref(); 80 | pub_.add("currentcap", currentCap_ ).pref(); 81 | pub_.add("offthreshold",offThreshold_ ).pref(); 82 | pub_.add("involt", inVolt_); 83 | pub_.add("wh", [=](String s) { ckPSUs(); if (s.length()) psu_->wh_ = s.toFloat(); return String(psu_->wh_); }); 84 | pub_.add("collapses", [=](String) { return String(getCollapses()); }); 85 | pub_.add("sweep",[=](String){ startSweep(); return "starting sweep"; }).hide(); 86 | pub_.add("connect",[=](String s){ doConnect(); return "connected"; }).hide(); 87 | pub_.add("disconnect",[=](String s){ db_.client.disconnect(); WiFi.disconnect(); return "dissed"; }).hide(); 88 | pub_.add("restart",[](String s){ ESP.restart(); return ""; }).hide(); 89 | pub_.add("clear",[=](String s){ pub_.clearPrefs(); return "cleared"; }).hide(); 90 | pub_.add("debug",[=](String s){ ckPSUs(); psu_->debug_ = !(s == "off"); return String(psu_->debug_); }).hide(); 91 | pub_.add("version",[=](String){ log("Version " + version_); return version_; }).hide(); 92 | pub_.add("update",[=](String s){ doOTAUpdate_ = s; return "OK, will try "+s; }).hide(); 93 | pub_.add("uptime",[=](String){ String ret = "Uptime " + timeAgo(millis()/1000); log(ret); return ret; }).hide(); 94 | 95 | server_.on("/", HTTP_ANY, [=]() { 96 | log("got req " + server_.uri() + " -> " + server_.hostHeader()); 97 | String ret; 98 | for (int i = 0; i < server_.args(); i++) 99 | ret += pub_.handleSet(server_.argName(i), server_.arg(i)) + "\n"; 100 | server_.sendHeader("Connection", "close"); 101 | if (! ret.length()) ret = pub_.toJson(); 102 | server_.send(200, "application/json", ret.c_str()); 103 | }); 104 | 105 | server_.on("/update", HTTP_GET, [this](){ 106 | server_.sendHeader("Connection", "close"); 107 | server_.send(200, "text/html", updateIndex); 108 | }); 109 | server_.on("/update", HTTP_POST, [this](){ 110 | server_.sendHeader("Connection", "close"); 111 | server_.send(200, "text/plain", (Update.hasError())?"FAIL":"OK"); 112 | ESP.restart(); 113 | },[=](){ 114 | HTTPUpload& upload = server_.upload(); 115 | if (upload.status == UPLOAD_FILE_START){ 116 | log(str("Update: %s\n", upload.filename.c_str())); 117 | doOTAUpdate_ = " "; //stops tasks 118 | db_.client.disconnect(); //helps reliability 119 | esp_task_wdt_init(120, true); //slows watchdog 120 | if (!Update.begin(UPDATE_SIZE_UNKNOWN))//start with max available size 121 | Update.printError(Serial); 122 | } else if (upload.status == UPLOAD_FILE_WRITE){ 123 | log(str("OTA upload at %dKB ~%0.1f%%", Update.progress() / 1000, Update.progress() * 100.0 / (float)espSketchSize_)); 124 | if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) 125 | Update.printError(Serial); 126 | } else if(upload.status == UPLOAD_FILE_END){ 127 | if (Update.end(true)) 128 | log(str("Update Success: %u\nRebooting...\n", upload.totalSize)); 129 | else Update.printError(Serial); 130 | } else if (upload.status == UPLOAD_FILE_ABORTED){ 131 | log("Update ABORTED, rebooting."); 132 | Update.abort(); 133 | delay(500); 134 | ESP.restart(); 135 | } else log(str("Update ELSE %d", upload.status)); 136 | }); 137 | 138 | pub_.loadPrefs(); 139 | // wifi & mqtt is connected by pubsubConnect below 140 | 141 | if (digitalPinToAnalogChannel(pinInvolt_) < 0) 142 | log(str("ERROR, inPin %d isn't actually an ADC pin", pinInvolt_)); 143 | if (digitalPinToAnalogChannel(pinInvolt_) > 7) 144 | log(str("ERROR, inPin %d is an ADC2 pin and WILL NOT WORK", pinInvolt_)); 145 | 146 | //fn, name, stack size, parameter, priority, handle 147 | xTaskCreate(runPubt, "publish", 10000, this, 1, NULL); 148 | 149 | if (!psu_) log("no PSU set"); 150 | else if (!psu_->begin()) log("PSU begin failed"); 151 | else if (psu_) { 152 | psu_->currFilt_ = psu_->limitCurr_ = psu_->outCurr_; 153 | log(str("startup current is %0.3fAfilt/%0.3fAout", psu_->currFilt_, psu_->outCurr_)); 154 | } 155 | if (autoSweep_ > 0) nextAutoSweep_ = millis() + 10000; 156 | log("finished setup"); 157 | log("OSPController Version " + version_); 158 | } 159 | 160 | String Solar::setLVProtect(String s) { 161 | if (s.length()) { 162 | lvProtect_.reset(new LowVoltageProtect(s)); //may throw! 163 | log("low-voltage cutoff enabled: " + lvProtect_->toString() + " (pin[i]:cutoff:recovery)"); 164 | lvProtect_->nextCheck_ = millis() + 5000; //don't check right away 165 | return "new " + lvProtect_->toString() + " ok"; 166 | } else return lvProtect_? lvProtect_->toString() : ""; 167 | } 168 | 169 | String Solar::setPSU(String s) { 170 | if (s.length() || !psu_) { 171 | log("setPSU " + s); 172 | //TODO Parse softserial pins, bluetooth comms, and moar. 173 | psu_.reset(PowerSupply::make(s)); 174 | if (psu_ && ! psu_->isDrok() && (measperiod_ == 200)) //default 175 | measperiod_ = 500; //slow down, DSP5005 meas does full update() 176 | ckPSUs(); 177 | psu_->begin(); 178 | return "created psu " + psu_->getType(); 179 | } 180 | return psu_->getType(); 181 | } 182 | 183 | void Solar::doConnect() { 184 | if (! WiFi.isConnected()) { 185 | if (wifiap.length() && wifipass.length()) { 186 | WiFi.begin(wifiap.c_str(), wifipass.c_str()); 187 | WiFi.setHostname(id_.c_str()); 188 | if (WiFi.waitForConnectResult() == WL_CONNECTED) { 189 | log("Wifi connected! hostname: " + id_); 190 | log("IP: " + WiFi.localIP().toString()); 191 | MDNS.begin(id_.c_str()); 192 | MDNS.addService("http", "tcp", 80); 193 | server_.begin(); 194 | lastConnected_ = millis(); 195 | } 196 | } else log("no wifiap or wifipass set!"); 197 | } 198 | if (WiFi.isConnected() && !db_.client.connected()) { 199 | if (db_.serv.length() && db_.feed.length()) { 200 | log("Connecting MQTT to " + db_.user + "@" + db_.serv + " as " + id_); 201 | db_.client.setServer(db_.getEndpoint().c_str(), db_.getPort()); 202 | if (db_.client.connect(id_.c_str(), db_.user.c_str(), db_.pass.c_str())) { 203 | log("PubSub connect success! " + db_.client.state()); 204 | db_.client.subscribe((db_.feed + "/cmd").c_str()); //subscribe to cmd topic for any actions 205 | lastConnected_ = millis(); 206 | } else pub_.logNote("[PubSub connect ERROR]" + db_.client.state()); 207 | } else pub_.logNote("[no MQTT user/pass/serv/feed set up]"); 208 | } else pub_.logNote(str("[can't pub connect, wifi %d pub %d]", WiFi.isConnected(), db_.client.connected())); 209 | } 210 | 211 | String SPoint::toString() const { 212 | return str("[%0.2fVin %0.2fVout %0.2fAout", input, v, i) + (collapsed? " CLPS]" : " ]"); 213 | } 214 | 215 | void Solar::applyAdjustment(float current) { 216 | if (psu_ && current != psu_->limitCurr_) { 217 | if (psu_->setCurrent(current)) 218 | pub_.logNote(str("[adjusting %0.3fA (from %0.3fA)]", current - psu_->limitCurr_, psu_->limitCurr_)); 219 | else log("error setting current"); 220 | delay(50); 221 | psu_->readCurrent(); 222 | pub_.setDirty({"outcurr", "outpower"}); 223 | printStatus(); 224 | } 225 | } 226 | 227 | void Solar::startSweep() { 228 | if (state_ == States::error) 229 | return log("can't sweep, system is in error state"); 230 | psu_->setCurrent(psu_->currFilt_* 0.90); //back off a little to start 231 | log(str("SWEEP START c=%0.3f, (setpoint was %0.3f)", psu_->limitCurr_, setpoint_)); 232 | if ((psu_ && state_ == States::collapsemode) || hasCollapsed()) { 233 | log(str("First coming out of collapse-mode to clim of %0.2fA", psu_->limitCurr_)); 234 | restoreFromCollapse(psu_->currFilt_* 0.75); 235 | } 236 | setState(States::sweeping); 237 | if (psu_ && !psu_->outEn_) 238 | psu_->enableOutput(true); 239 | lastAutoSweep_ = millis(); 240 | } 241 | 242 | void Solar::doSweepStep() { 243 | if (!psu_) return; 244 | if (!psu_->outEn_) 245 | return setState(States::mppt); 246 | 247 | updatePSU(); 248 | 249 | bool isCollapsed = hasCollapsed(); 250 | sweepPoints_.push_back({v: psu_->outVolt_, i: psu_->outCurr_, input: inVolt_, collapsed: isCollapsed}); 251 | int collapsedPoints = 0, nonCollapsedPoints = 0; 252 | for (int i = 0; i < sweepPoints_.size(); i++) { 253 | if (sweepPoints_[i].collapsed) collapsedPoints++; 254 | else nonCollapsedPoints++; 255 | } 256 | if (isCollapsed) pub_.logNote(str("COLLAPSED[%d]", collapsedPoints)); 257 | 258 | if (isCollapsed && collapsedPoints >= 2) { //great, sweep finished 259 | if (!nonCollapsedPoints) { 260 | log("SWEEP DONE but zero un-collapsed points. aborting."); 261 | restoreFromCollapse(psu_->currFilt_* 0.5); 262 | return setState(States::mppt); 263 | } 264 | int maxIndex = 0; 265 | SPoint collapsePoint = sweepPoints_.back(); 266 | 267 | for (int i = 0; i < sweepPoints_.size(); i++) { 268 | log(str("point %i = ", i) + sweepPoints_[i].toString()); 269 | if (!sweepPoints_[i].collapsed && sweepPoints_[i].p() > sweepPoints_[maxIndex].p()) 270 | maxIndex = i; //find max 271 | } 272 | String tolog = "SWEEP DONE. max = " + sweepPoints_[maxIndex].toString(); 273 | if (sweepPoints_[maxIndex].p() < collapsePoint.p()) { 274 | log(tolog + str(" will run collapsed! (next sweep in %0.1fm)", ((float)autoSweep_) / 3.0 / 60.0)); 275 | setState(States::collapsemode); 276 | psu_->setCurrent(currentCap_ > 0? currentCap_ : 10); 277 | nextAutoSweep_ = millis() + autoSweep_ * 1000 / 3; //reschedule soon 278 | setpoint_ = collapsePoint.input; 279 | } else { 280 | maxIndex = max(0, maxIndex - 2); 281 | log(tolog + str(" new setpoint = %0.3f (was %0.3f)", sweepPoints_[maxIndex].input, setpoint_)); 282 | setState(States::mppt); 283 | restoreFromCollapse(sweepPoints_[maxIndex].i * (0.98 - 0.04 * min(getCollapses(), 8))); //more collapses, more backoff 284 | setpoint_ = sweepPoints_[maxIndex].input; 285 | } 286 | pub_.setDirtyAddr(&setpoint_); 287 | nextSolarAdjust_ = millis() + 1000; //don't recheck the voltage too quickly 288 | sweepPoints_.clear(); 289 | //the output should be re-enabled below 290 | } 291 | 292 | if (psu_->limitCurr_ >= currentCap_) { 293 | setpoint_ = inVolt_ - (pgain_ * 4); 294 | setpoint_ = sweepPoints_.back().input; 295 | setState(States::mppt); 296 | log(str("SWEEP DONE, currentcap of %0.1fA reached (setpoint=%0.3f)", currentCap_, setpoint_)); 297 | return applyAdjustment(currentCap_); 298 | } else if (psu_->isCV()) { 299 | setState(States::full_cv); 300 | return log("SWEEP DONE, constant-voltage state reached"); 301 | } 302 | 303 | applyAdjustment(min(psu_->limitCurr_ + (inVolt_ * 0.001), currentCap_ + 0.001)); //speed porportional to input voltage 304 | } 305 | 306 | bool Solar::hasCollapsed() const { 307 | if (!psu_ || !psu_->outEn_) return false; 308 | if (!psu_->isDrok() && psu_->isCollapsed()) //DP* psu is darn accurate 309 | return true; 310 | bool simpleClps = (inVolt_ < (psu_->outVolt_ * 1.11)); //simple voltage match method 311 | float collapsePct = (inVolt_ - psu_->outVolt_) / psu_->outVolt_; 312 | if (simpleClps && psu_->isCollapsed()) 313 | return true; 314 | if ((collapsePct < 0.05) && psu_->isCollapsed()) { //secondary method 315 | log(str("hasCollapsed used secondary method. collapse %0.3f%%", collapsePct)); 316 | return true; 317 | } 318 | return false; 319 | } 320 | 321 | int Solar::getCollapses() const { return collapses_.size(); } 322 | 323 | bool Solar::updatePSU() { 324 | uint32_t start = millis(); 325 | if (psu_ && psu_->doUpdate()) { 326 | pub_.setDirty({"outvolt", "outcurr", "outputEN", "outpower", "currFilt"}); 327 | if (psu_->wh_ > 2.0 || (millis() - lastConnected_) > 60000) 328 | pub_.setDirty("wh"); //don't publish for a while after reboot 329 | if (psu_->debug_) log(psu_->getType() + str(" updated in %d ms: ", millis() - start) + psu_->toString()); 330 | return true; 331 | } 332 | return false; 333 | } 334 | 335 | float Solar::measureInvolt() { 336 | if (psu_ && psu_->getInputVolt(&inVolt_)) { 337 | //excellent, we could read the input voltage! nothing else required 338 | if ((millis() - psu_->lastSuccess_) > 600) { 339 | updatePSU(); //seems to take ~400ms for a DP 340 | psu_->getInputVolt(&inVolt_); 341 | } 342 | } else { 343 | int analogval = analogRead(pinInvolt_); 344 | inVolt_ = analogval * 3.3 * (vadjust_ / 3.3) / 4096.0; 345 | } 346 | pub_.setDirtyAddr(&inVolt_); 347 | return inVolt_; 348 | } 349 | 350 | void Solar::restoreFromCollapse(float restoreCurrent) { 351 | psu_->setCurrent(0.01); //some PSU's don't disable without crashing (cough5020cough) 352 | uint32_t start = millis(); 353 | while ((millis() - start) < 8000 && measureInvolt() < offThreshold_) 354 | delay(25); 355 | float in = measureInvolt(); 356 | if (offThreshold_ >= 1000) { //startup condition 357 | offThreshold_ = 0.992 * in; 358 | log(str("restore threshold now set to %0.2fV", offThreshold_)); 359 | pub_.setDirtyAddr(&offThreshold_); 360 | } 361 | log(str("restore took %0.1fs to reach %0.1fV [goal %0.1f], setting %0.1fA", (millis() - start) / 1000.0, in, offThreshold_, restoreCurrent)); 362 | psu_->setCurrent(restoreCurrent); 363 | } 364 | 365 | float Solar::doMeasure() { 366 | measureInvolt(); 367 | if (state_ == States::sweeping) { 368 | doSweepStep(); 369 | } else if (setpoint_ > 0 && psu_ && psu_->outEn_) { //corrections enabled 370 | double error = inVolt_ - setpoint_; 371 | double dcurr = constrain(error * pgain_, -ramplimit_ * 2, ramplimit_); //limit ramping speed 372 | if (error > 0.3 || (-error > 0.2)) { //adjustment deadband, more sensitive when needing to ramp down 373 | if ((error < 0.6) && (state_ == States::mppt)) { //ramp down, quick! 374 | pub_.logNote("[QUICK]"); 375 | nextSolarAdjust_ = millis(); 376 | } 377 | return min(psu_->limitCurr_ + dcurr, currentCap_); 378 | } 379 | } 380 | return psu_? psu_->limitCurr_ : 0; 381 | } 382 | 383 | void Solar::doUpdateState() { 384 | if (!psu_) { 385 | setState(States::error); 386 | } else if (state_ != States::sweeping && state_ != States::collapsemode) { 387 | int lastPSUsecs = (millis() - psu_->lastSuccess_) / 1000; 388 | if (psu_->outEn_) { 389 | if (lastPSUsecs > 11) setState(States::error, "enabled but no PSU comms"); 390 | else if (psu_->outCurr_ > (currentCap_ * 0.95 )) setState(States::capped); 391 | else if (psu_->isCV()) setState(States::full_cv); 392 | else setState(States::mppt); 393 | } else { //disabled 394 | if ((inVolt_ > 1) && lastPSUsecs > 120) //psu active at least every 2m when shut down 395 | setState(States::error, "inactive PSU"); 396 | else setState(States::off); 397 | } 398 | } 399 | } 400 | 401 | void Solar::doAdjust(float desired) { 402 | uint32_t now = millis(); 403 | try { 404 | if (state_ == States::error) { 405 | if (psu_ && (now - psu_->lastSuccess_) < 30000) { //for 30s after failure try and shut it down 406 | psu_->enableOutput(false); 407 | psu_->setCurrent(0); 408 | throw Backoff("PSU failure, disabling"); 409 | } 410 | } else if (setpoint_ > 0 && (state_ != States::sweeping)) { 411 | if (hasCollapsed() && state_ != States::collapsemode) { 412 | collapses_.push_back(now); 413 | pub_.setDirty("collapses"); 414 | log(str("collapsed! %0.2fV ", inVolt_) + psu_->toString()); 415 | restoreFromCollapse(psu_->currFilt_ * 0.95); //restore at 90% of previous point 416 | } else if (psu_ && !psu_->outEn_) { //power supply is off. let's check about turning it on 417 | if (inVolt_ < psu_->outVolt_ || psu_->outVolt_ < 0.1) { 418 | throw Backoff("not starting up, input voltage too low (is it dark?)"); 419 | } else if ((psu_->outVolt_ > psu_->limitVolt_) || (psu_->outVolt_ < (psu_->limitVolt_ * 0.60) && psu_->outVolt_ > 1)) { 420 | //li-ion 4.1-2.5 is 60% of range. the last && condition allows system to work with battery drain diode in place 421 | throw Backoff(str("not starting up, battery %0.1fV too far from Supply limit %0.1fV. ", psu_->outVolt_, psu_->limitVolt_) + 422 | "Use outvolt command (or PSU buttons) to set your appropiate battery voltage and restart"); 423 | } else { 424 | log("restoring from collapse"); 425 | psu_->enableOutput(true); 426 | } 427 | } 428 | if (psu_ && psu_->outEn_ && state_ != States::collapsemode) { 429 | applyAdjustment(desired); 430 | } 431 | } 432 | backoffLevel_ = max(backoffLevel_ - 1, 0); //successes means less backoff 433 | } catch (const Backoff &b) { 434 | backoffLevel_ = min(backoffLevel_ + 1, 8); 435 | log(str("backoff now at %ds: ", getBackoff(adjustPeriod_) / 1000) + String(b.what())); 436 | } 437 | if (collapses_.size() && (millis() - collapses_.front()) > (5 * 60000)) { //5m age 438 | pub_.logNote(str("[clear collapse (%ds ago)]", (now - collapses_.pop_front())/1000)); 439 | pub_.setDirty("collapses"); 440 | } 441 | } 442 | 443 | void Solar::loop() { 444 | uint32_t now = millis(); 445 | if (doOTAUpdate_.length()) 446 | return delay(100); 447 | 448 | if (now > nextVmeas_) { 449 | doMeasure(); //may set nextSolarAdjust sooner 450 | doUpdateState(); 451 | nextVmeas_ = now + ((state_ == States::sweeping)? measperiod_ * 2 : measperiod_); 452 | } 453 | 454 | if (now > nextSolarAdjust_) { 455 | doAdjust(doMeasure()); 456 | heap_caps_check_integrity_all(true); 457 | nextSolarAdjust_ = now + getBackoff(adjustPeriod_); 458 | } 459 | 460 | if (now > nextPrint_) { 461 | printStatus(); 462 | nextPrint_ = now + printPeriod_; 463 | } 464 | 465 | if (psu_ && now > nextPSUpdate_) { 466 | if (!updatePSU()) { 467 | log("psu update fail" + String(psu_->debug_? " serial debug output enabled" : "")); 468 | psu_->begin(); //try and reconnect 469 | } 470 | if ((inVolt_ > 1) && ((millis() - psu_->lastSuccess_) > 5 * 60 * 1000)) { //5m 471 | log("VERY UNRESPONSIVE PSU, RESTARTING"); 472 | nextPub_ = now; 473 | delay(1000); 474 | ESP.restart(); 475 | } 476 | nextPSUpdate_ = now + min(getBackoff(5000), 100000); //100s 477 | } 478 | 479 | if (lvProtect_ && now > lvProtect_->nextCheck_) { 480 | if (!lvProtect_->isTriggered() && psu_ && psu_->outVolt_ < lvProtect_->threshold_) { 481 | log(str("LOW VOLTAGE PROTECT TRIGGERED (now at %0.2fV)", psu_->outVolt_)); 482 | sendOutgoingLogs(); //send logs, tripping this relay may power us down 483 | delay(200); 484 | lvProtect_->trigger(true); 485 | lvProtect_->nextCheck_ = now + 5 * 1000; 486 | } else if (lvProtect_->isTriggered() && psu_ && psu_->outVolt_ > lvProtect_->threshRecovery_) { 487 | log("low voltage recovery, re-enabling."); 488 | lvProtect_->trigger(false); 489 | lvProtect_->nextCheck_ = now + 10000; 490 | } 491 | } 492 | 493 | if (getCollapses() > 2) 494 | nextAutoSweep_ = lastAutoSweep_ + autoSweep_ / 3.0 * 1000; 495 | 496 | if (autoSweep_ > 0 && (now > nextAutoSweep_)) { 497 | if (state_ == States::capped) { 498 | log(str("Skipping auto-sweep. Already at currentCap (%0.1fA)", currentCap_)); 499 | } else if (state_ == States::full_cv) { 500 | log(str("Skipping auto-sweep. Battery-full voltage reached (%0.1fV)", psu_->outVolt_)); 501 | } else if (state_ == States::mppt || state_ == States::collapsemode) { 502 | log(str("Starting AUTO-SWEEP (last run %0.1f mins ago)", (now - lastAutoSweep_)/1000.0/60.0)); 503 | startSweep(); 504 | } 505 | nextAutoSweep_ = now + autoSweep_ * 1000; 506 | lastAutoSweep_ = now; 507 | } 508 | } 509 | 510 | void Solar::sendOutgoingLogs() { 511 | String s; 512 | while (db_.client.connected() && pub_.popLog(&s)) 513 | db_.client.publish((db_.feed + "/log").c_str(), s.c_str(), false); 514 | } 515 | 516 | void Solar::publishTask() { 517 | doConnect(); 518 | db_.client.loop(); 519 | db_.client.setCallback([=](char*topicbuf, uint8_t*buf, unsigned int len){ 520 | String topic(topicbuf), val = str(std::string((char*)buf, len)); 521 | log("got sub value " + topic + " -> " + val); 522 | if (topic == (db_.feed + "/wh") && psu_) { 523 | psu_->wh_ = (psu_->wh_ > 2.0)? val.toFloat() : psu_->wh_ + val.toFloat(); 524 | log("restored wh value to " + val); 525 | db_.client.unsubscribe((db_.feed + "/wh").c_str()); 526 | } else if (topic == db_.feed + "/cmd") { 527 | log("MQTT cmd " + topic + ":" + val + " -> " + pub_.handleCmd(val)); 528 | } else { 529 | log("MQTT unknown message " + topic + ":" + val); 530 | } 531 | }); 532 | db_.client.subscribe((db_.feed + "/wh").c_str()); 533 | 534 | while (true) { 535 | uint32_t now = millis(); 536 | if (now > nextPub_) { 537 | while (doOTAUpdate_ == " ") //stops this task while an upload-OTA is running 538 | delay(1000); 539 | if (doOTAUpdate_.length()) { 540 | doOTA(doOTAUpdate_); 541 | doOTAUpdate_ = ""; 542 | } 543 | if (db_.client.connected()) { 544 | int wins = 0; 545 | auto pubs = pub_.items(true); 546 | for (auto i : pubs) 547 | wins += db_.client.publish((db_.feed + "/" + (i->pref_? "prefs/":"") + i->key).c_str(), i->toString().c_str(), true)? 1 : 0; 548 | pub_.logNote(str("[pub-%d]", wins)); 549 | pub_.clearDirty(); 550 | } else { 551 | pub_.logNote("[pub disconnected]"); 552 | doConnect(); 553 | } 554 | sendOutgoingLogs(); 555 | heap_caps_check_integrity_all(true); 556 | nextPub_ = now + ((psu_ && psu_->outEn_)? db_.period : db_.period * 4); //slower when disabled 557 | } 558 | db_.client.loop(); 559 | pub_.poll(&Serial); 560 | server_.handleClient(); 561 | delay(1); 562 | } 563 | } 564 | 565 | void Solar::printStatus() { 566 | String s = state_; 567 | s.toUpperCase(); 568 | s += str(" %0.1fVin -> %0.2fWh ", inVolt_, psu_? psu_->wh_ : 0) + (psu_? psu_->toString() : "[no PSU]"); 569 | if (lvProtect_ && lvProtect_->isTriggered()) s += " [LV PROTECTED]"; 570 | s += pub_.popNotes(); 571 | if (psu_ && psu_->debug_) log(s); 572 | else Serial.println(s); 573 | } 574 | 575 | int Solar::getBackoff(int period) const { 576 | if (backoffLevel_ <= 0) return period; 577 | return ((backoffLevel_ * backoffLevel_ + 2) / 2) * period; 578 | } 579 | 580 | void Solar::setState(const String state, String reason) { 581 | if (state_ != state) { 582 | pub_.setDirty("state"); 583 | log("state change to " + state + " (from " + state_ + ") " + reason); 584 | } 585 | state_ = state; 586 | } 587 | 588 | int DBConnection::getPort() const { 589 | int sep = serv.indexOf(':'); 590 | return (sep >= 0)? serv.substring(sep + 1).toInt() : 1883; 591 | } 592 | String DBConnection::getEndpoint() const { 593 | int sep = serv.indexOf(':'); 594 | return (sep >= 0)? serv.substring(0, sep) : serv; 595 | } 596 | 597 | void Solar::doOTA(String url) { 598 | log("[OTA] running from " + url); 599 | sendOutgoingLogs(); //send any outstanding log() messages 600 | db_.client.disconnect(); //helps to disconnect everything 601 | esp_task_wdt_init(120, true); //way longer watchdog timeout 602 | t_httpUpdate_return ret = httpUpdate.update(espClient, url, version_); 603 | if (ret == HTTP_UPDATE_FAILED) { 604 | log(str("[OTA] Error (%d):", httpUpdate.getLastError()) + httpUpdate.getLastErrorString()); 605 | } else if (ret == HTTP_UPDATE_NO_UPDATES) { 606 | log("[OTA] no updates"); 607 | } else if (ret == HTTP_UPDATE_OK) { 608 | log("[OTA] SUCCESS!!! restarting"); 609 | delay(100); 610 | ESP.restart(); 611 | } 612 | } 613 | 614 | 615 | String LowVoltageProtect::toString() const { 616 | return String(pin_) + (invert_? "i" : "") + str(":%0.2f:%0.2f", threshold_, threshRecovery_); 617 | } 618 | LowVoltageProtect::~LowVoltageProtect() { log("~LVProtect " + toString()); } 619 | LowVoltageProtect::LowVoltageProtect(String config) { 620 | StringPair sp1 = split(config, ":"); 621 | invert_ = suffixed(& sp1.first, "i"); 622 | pin_ = sp1.first.length()? sp1.first.toInt() : 22; 623 | if (digitalPinToAnalogChannel(pin_) > 7) 624 | throw std::runtime_error("sorry, lv-protect pin can't use an ADC2 pin"); 625 | if (sp1.second.length()) { 626 | StringPair sp2 = split(sp1.second, ":"); 627 | threshold_ = sp2.first.toFloat(); 628 | if (sp2.second.length()) 629 | threshRecovery_ = sp2.second.toFloat(); 630 | else threshRecovery_ = threshold_ * 1.08; 631 | } 632 | log("created lvProtect=" + toString()); 633 | } 634 | 635 | void LowVoltageProtect::trigger(bool trigger) { 636 | pinMode(pin_, OUTPUT); 637 | digitalWrite(pin_, !(trigger ^ invert_)); 638 | } 639 | 640 | bool LowVoltageProtect::isTriggered() const { 641 | return !(digitalRead(pin_) ^ invert_); 642 | } 643 | 644 | //page styling 645 | const String style = 646 | ""; 652 | 653 | // Update page 654 | const String updateIndex = 655 | "" 656 | "
" 657 | "" 658 | "" 659 | "" 660 | "

" 661 | "
" 662 | "

" 663 | "" + style; 689 | -------------------------------------------------------------------------------- /lib/MPPTLib/solar.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "publishable.h" 3 | #include 4 | #include 5 | #include 6 | 7 | class PowerSupply; 8 | struct LowVoltageProtect; 9 | 10 | struct DBConnection { 11 | String serv, user, pass, feed; 12 | PubSubClient client; 13 | int32_t period = 1000; 14 | int getPort() const; 15 | String getEndpoint() const; 16 | }; 17 | 18 | struct SPoint { 19 | double v, i, input; bool collapsed; 20 | String toString() const; 21 | double p() const { return v * i; } 22 | }; 23 | 24 | class Solar { 25 | public: 26 | Solar(String version); 27 | ~Solar(); 28 | void setup(); 29 | String setLVProtect(String); 30 | String setPSU(String); 31 | 32 | void loop(); 33 | float doMeasure(); 34 | void doUpdateState(); 35 | void doAdjust(float desired); 36 | 37 | bool updatePSU(); 38 | float measureInvolt(); 39 | void sendOutgoingLogs(); 40 | void publishTask(); 41 | void doConnect(); 42 | void applyAdjustment(float current); 43 | void printStatus(); 44 | void startSweep(); 45 | void doSweepStep(); 46 | bool hasCollapsed() const; 47 | int getCollapses() const; 48 | void restoreFromCollapse(float restoreCurrent); 49 | void doOTA(String url); 50 | 51 | int getBackoff(int period) const; 52 | void setState(const String state, String reason=""); 53 | 54 | const String version_; 55 | String id_; 56 | String state_; 57 | int pinInvolt_ = 32; 58 | float inVolt_ = 0; 59 | double setpoint_ = 0, pgain_ = 0.005, ramplimit_ = 12; 60 | double currentCap_ = 8.5; 61 | CircularArray collapses_; 62 | int measperiod_ = 200, printPeriod_ = 1000, adjustPeriod_ = 2000; 63 | int autoSweep_ = 10 * 60; //every 10m 64 | float vadjust_ = 116.50; 65 | float offThreshold_ = 1000.0; //starts high to force update 66 | CircularArray sweepPoints_; //size here is important, larger == more stable setpoint 67 | String wifiap, wifipass; 68 | uint32_t lastConnected_ = 0; 69 | int8_t backoffLevel_ = 0; 70 | std::unique_ptr lvProtect_; 71 | 72 | std::unique_ptr psu_; 73 | WebServer server_; 74 | Publishable pub_; 75 | DBConnection db_; 76 | }; 77 | 78 | #define STATE(x) static constexpr const char* x = #x 79 | 80 | struct States { 81 | STATE(error); 82 | STATE(off); 83 | STATE(mppt); 84 | STATE(sweeping); 85 | STATE(full_cv); 86 | STATE(capped); 87 | STATE(collapsemode); 88 | }; 89 | 90 | 91 | struct LowVoltageProtect { 92 | uint8_t pin_ = 22; 93 | float threshold_ = 12.0; 94 | float threshRecovery_ = 13.0; 95 | bool invert_ = false; 96 | uint32_t nextCheck_ = 0; 97 | String toString() const; 98 | LowVoltageProtect(String configuration); 99 | ~LowVoltageProtect(); 100 | void trigger(bool trigger=true); 101 | bool isTriggered() const; 102 | }; 103 | -------------------------------------------------------------------------------- /lib/MPPTLib/utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "publishable.h" 4 | #include "utils.h" 5 | 6 | 7 | float mapfloat(long x, long in_min, long in_max, long out_min, long out_max) { 8 | return (float)(x - in_min) * (out_max - out_min) / (float)(in_max - in_min) + out_min; 9 | } 10 | 11 | String str(const char *fmtStr, ...) { 12 | static char buf[201] = {'\0'}; 13 | va_list arg_ptr; 14 | va_start(arg_ptr, fmtStr); 15 | vsnprintf(buf, 200, fmtStr, arg_ptr); 16 | va_end(arg_ptr); 17 | return String(buf); 18 | } 19 | String str(const std::string &s) { 20 | return String(s.c_str()); 21 | } 22 | String str(bool v) { 23 | return v ? " WIN" : " FAIL"; 24 | } 25 | 26 | StringPair split(const String &str, const String &del) { 27 | int at = str.indexOf(del); 28 | if (at >= 0) return StringPair(str.substring(0, at), str.substring(at + del.length())); 29 | return StringPair(str, ""); 30 | } 31 | 32 | bool suffixed(String *str, const String &suff) { 33 | if (!str) return false; 34 | bool res = str->endsWith(suff); 35 | if (res) str->remove(str->lastIndexOf(suff)); 36 | return res; 37 | } 38 | 39 | // float lifepo4_soc[] = {13.4, 13.3, 13.28, 13.}; 40 | 41 | Publishable* pub_; //static 42 | void log(const String &s) { pub_->log(s); } 43 | void addLogger(Publishable* p) { pub_ = p; } 44 | 45 | String timeAgo(int sec) { 46 | int days = (sec / (3600 * 24)); 47 | String ret = str("%ds", sec % 60); 48 | if (sec >= 60 ) ret = str("%dm ", (sec % 3600) / 60) + ret; 49 | if (sec >= 3600) ret = str("%dh ", ((sec % (3600 * 24)) / 3600)) + ret; 50 | if (days) ret = str("%dd ", days % 365) + ret; 51 | if (days >= 365) ret = str("%dy ", days / 365) + ret; 52 | return ret; 53 | } 54 | 55 | String getResetReason(RESET_REASON r) { 56 | switch (r) { 57 | case NO_MEAN: return ""; 58 | case POWERON_RESET : return "Vbat power on reset"; 59 | case SW_RESET : return "Software reset digital core"; 60 | case OWDT_RESET : return "Legacy watch dog reset digital core"; 61 | case DEEPSLEEP_RESET : return "Deep Sleep reset digital core"; 62 | case SDIO_RESET : return "Reset by SLC module, reset digital core"; 63 | case TG0WDT_SYS_RESET : return "Timer Group0 Watch dog reset digital core"; 64 | case TG1WDT_SYS_RESET : return "Timer Group1 Watch dog reset digital core"; 65 | case RTCWDT_SYS_RESET : return "RTC Watch dog Reset digital core"; 66 | case INTRUSION_RESET : return "Instrusion tested to reset CPU"; 67 | case TGWDT_CPU_RESET : return "Time Group reset CPU"; 68 | case SW_CPU_RESET : return "Software reset CPU"; 69 | case RTCWDT_CPU_RESET : return "RTC Watch dog Reset CPU"; 70 | case EXT_CPU_RESET : return "for APP CPU, reseted by PRO CPU"; 71 | case RTCWDT_BROWN_OUT_RESET: return "Reset when the vdd voltage is not stable"; 72 | case RTCWDT_RTC_RESET : return "RTC Watch dog reset digital core and rtc module"; 73 | } 74 | return ""; 75 | } 76 | 77 | String getResetReasons() { 78 | return "Reset0: " + getResetReason(rtc_get_reset_reason(0)) + ". Reason1: " + getResetReason(rtc_get_reset_reason(1)); 79 | } -------------------------------------------------------------------------------- /lib/MPPTLib/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Publishable; 4 | 5 | void log(const String &); 6 | void addLogger(Publishable*); 7 | 8 | String getResetReasons(); 9 | String timeAgo(int seconds); 10 | 11 | float mapfloat(long x, long in_min, long in_max, long out_min, long out_max); 12 | 13 | extern const char* adafruitRootCert; 14 | 15 | String str(const char *fmtStr, ...); 16 | String str(const std::string &s); 17 | String str(bool v); 18 | 19 | typedef std::pair StringPair; 20 | StringPair split(const String &str, const String &del); 21 | bool suffixed(String *str, const String &suff); 22 | 23 | template 24 | class CircularArray { 25 | T buf_[Size]; 26 | T *head_, *tail_; 27 | uint16_t count_; 28 | public: 29 | CircularArray() : head_(buf_), tail_(buf_), count_(0) { } 30 | ~CircularArray() { } 31 | 32 | bool push_front(T v) { 33 | if (head_ == buf_) head_ = buf_ + Size; 34 | *--head_ = v; 35 | if (count_ == Size) { 36 | if (tail_-- == buf_) tail_ = buf_ + Size - 1; 37 | return false; 38 | } else { 39 | if (count_++ == 0) tail_ = head_; 40 | return true; 41 | } 42 | } 43 | 44 | bool push_back(T v) { 45 | if (++tail_ == buf_ + Size) tail_ = buf_; 46 | *tail_ = v; 47 | if (count_ == Size) { 48 | if (++head_ == buf_ + Size) head_ = buf_; 49 | return false; 50 | } else { 51 | if (count_++ == 0) head_ = tail_; 52 | return true; 53 | } 54 | } 55 | 56 | T pop_front() { 57 | T res = *head_++; 58 | if (head_ == buf_ + Size) head_ = buf_; 59 | count_--; 60 | return res; 61 | } 62 | 63 | T pop_end() { 64 | T res = *tail_--; 65 | if (tail_ == buf_) tail_ = buf_ + Size - 1; 66 | count_--; 67 | return res; 68 | } 69 | 70 | T& operator [] (uint16_t index) { return *(buf_ + ((head_ - buf_ + index) % Size)); } 71 | T& front() { return *head_; } //TODO: add bounds checks 72 | T& back() { return *tail_; } 73 | 74 | uint16_t inline size() const { return count_; } 75 | uint16_t inline available() const { return Size - count_; } 76 | bool inline empty() const { return count_ == 0; } 77 | bool inline isFull() const { return count_ == Size; } 78 | 79 | void inline clear() { 80 | head_ = tail_ = buf_; 81 | count_ = 0; 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | 2 | [env:nodemcu-32s] 3 | platform = espressif32 4 | board = nodemcu-32s 5 | framework = arduino 6 | monitor_speed = 115200 7 | lib_deps = 8 | PubSubClient 9 | ModbusMaster 10 | plerup/espsoftwareserial 11 | extra_scripts = pre:utils.py ;injects version into main 12 | build_unflags = -fno-rtti ;allow dynamic_cast 13 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "version.h" 3 | 4 | //GIT_VERSION pulled from platformio.ini src_build_flags, only for this file 5 | Solar controller(GIT_VERSION); 6 | 7 | void setup() { 8 | controller.setup(); 9 | } 10 | void loop() { 11 | controller.loop(); 12 | } 13 | -------------------------------------------------------------------------------- /src/version.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //version.cpp file written by utils.py as a pio pre-script 4 | extern const char* GIT_VERSION; 5 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os, sys, subprocess 3 | 4 | def shellCmd(cmd): 5 | return subprocess.check_output(cmd.split(' ')).strip().decode("utf-8") 6 | 7 | def getDescribe(): 8 | return shellCmd("git describe --long --tags --always --dirty") 9 | def getGitDate(): 10 | return shellCmd("git log -1 --date=format:%Y%m%d --format=%ad") 11 | def getVersion(): 12 | try: 13 | return getDescribe().replace("-dirty", ".d") + "-" + str(getGitDate()) 14 | except Exception as e: 15 | return os.path.basename(os.getcwd()) 16 | def prettyPrint(): 17 | try: #optional colorful output 18 | from colorama import Fore, Back, Style 19 | print(Back.YELLOW + Fore.BLACK + " git version " + Back.BLACK + Fore.YELLOW + " " + getVersion() + " " + Style.RESET_ALL) 20 | except Exception: 21 | print("git version " + getVersion()) 22 | 23 | 24 | arg = sys.argv[1] if len(sys.argv) > 1 else "" 25 | 26 | if arg == "version": 27 | prettyPrint() 28 | elif arg == "simple": 29 | print(getVersion()) 30 | 31 | else: 32 | prettyPrint() 33 | 34 | try: #if running inside platformio 35 | Import("env") 36 | # print(env.Dump()) # <- can use this to see what's available 37 | bpath = os.path.join(env.subst("$BUILD_DIR"), "generated") 38 | print(" - version injection to " + bpath) 39 | 40 | if not os.path.exists(bpath): os.makedirs(bpath) 41 | with open(os.path.join(bpath, "version.cpp"), 'w+') as ofile: 42 | ofile.write("const char* GIT_VERSION(\"" + getVersion() + "\");" + os.linesep) 43 | env.BuildSources(os.path.join(bpath, "build"), bpath) 44 | except NameError: 45 | pass 46 | --------------------------------------------------------------------------------