├── .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 | [](https://github.com/opensolarproject/OSPController/releases/latest)
9 | [](https://github.com/opensolarproject/OSPController/commits)
10 | [](https://github.com/opensolarproject/OSPController/commits)
11 | [](https://github.com/opensolarproject/OSPController/releases/latest)
12 | [](https://travis-ci.com/github/opensolarproject/OSPController)
13 |
14 | [](https://github.com/opensolarproject/OSPController/commits)
15 | [](https://github.com/arendst/Tasmota/stargazers)
16 | [](https://github.com/opensolarproject/OSPController/network)
17 | [](https://github.com/opensolarproject/OSPController/issues)
18 | [](https://github.com/opensolarproject/OSPController/issues)
19 | [](https://discord.gg/GtR3JShfGu)
20 |
21 | |  |
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