mirror of https://github.com/lumapu/ahoy.git
126 changed files with 12672 additions and 6436 deletions
@ -1,24 +1,15 @@ |
|||
CMakeLists.txt.user |
|||
CMakeCache.txt |
|||
CMakeFiles |
|||
CMakeScripts |
|||
Testing |
|||
Makefile |
|||
cmake_install.cmake |
|||
install_manifest.txt |
|||
compile_commands.json |
|||
CTestTestfile.cmake |
|||
_deps |
|||
build |
|||
tools/esp8266/tmp |
|||
tools/esp8266/binaries |
|||
.pio |
|||
.vscode/.browse.c_cpp.db* |
|||
.vscode/c_cpp_properties.json |
|||
.vscode/launch.json |
|||
.vscode/ipch |
|||
.vscode/extensions.json |
|||
src/config/config_override.h |
|||
src/web/html/h/* |
|||
src/web/html/tmp/* |
|||
/**/Debug |
|||
/**/v16/* |
|||
*.db |
|||
*.suo |
|||
*.ipch |
|||
tools/esp8266/.vscode/extensions.json |
|||
.DS_Store |
|||
.vscode |
|||
tools/esp8266/platformio-device-monitor-*.log |
|||
tools/esp8266/html/h/* |
|||
src/output.map |
|||
|
@ -0,0 +1,437 @@ |
|||
Attribution-NonCommercial-ShareAlike 4.0 International |
|||
|
|||
======================================================================= |
|||
|
|||
Creative Commons Corporation ("Creative Commons") is not a law firm and |
|||
does not provide legal services or legal advice. Distribution of |
|||
Creative Commons public licenses does not create a lawyer-client or |
|||
other relationship. Creative Commons makes its licenses and related |
|||
information available on an "as-is" basis. Creative Commons gives no |
|||
warranties regarding its licenses, any material licensed under their |
|||
terms and conditions, or any related information. Creative Commons |
|||
disclaims all liability for damages resulting from their use to the |
|||
fullest extent possible. |
|||
|
|||
Using Creative Commons Public Licenses |
|||
|
|||
Creative Commons public licenses provide a standard set of terms and |
|||
conditions that creators and other rights holders may use to share |
|||
original works of authorship and other material subject to copyright |
|||
and certain other rights specified in the public license below. The |
|||
following considerations are for informational purposes only, are not |
|||
exhaustive, and do not form part of our licenses. |
|||
|
|||
Considerations for licensors: Our public licenses are |
|||
intended for use by those authorized to give the public |
|||
permission to use material in ways otherwise restricted by |
|||
copyright and certain other rights. Our licenses are |
|||
irrevocable. Licensors should read and understand the terms |
|||
and conditions of the license they choose before applying it. |
|||
Licensors should also secure all rights necessary before |
|||
applying our licenses so that the public can reuse the |
|||
material as expected. Licensors should clearly mark any |
|||
material not subject to the license. This includes other CC- |
|||
licensed material, or material used under an exception or |
|||
limitation to copyright. More considerations for licensors: |
|||
wiki.creativecommons.org/Considerations_for_licensors |
|||
|
|||
Considerations for the public: By using one of our public |
|||
licenses, a licensor grants the public permission to use the |
|||
licensed material under specified terms and conditions. If |
|||
the licensor's permission is not necessary for any reason--for |
|||
example, because of any applicable exception or limitation to |
|||
copyright--then that use is not regulated by the license. Our |
|||
licenses grant only permissions under copyright and certain |
|||
other rights that a licensor has authority to grant. Use of |
|||
the licensed material may still be restricted for other |
|||
reasons, including because others have copyright or other |
|||
rights in the material. A licensor may make special requests, |
|||
such as asking that all changes be marked or described. |
|||
Although not required by our licenses, you are encouraged to |
|||
respect those requests where reasonable. More considerations |
|||
for the public: |
|||
wiki.creativecommons.org/Considerations_for_licensees |
|||
|
|||
======================================================================= |
|||
|
|||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International |
|||
Public License |
|||
|
|||
By exercising the Licensed Rights (defined below), You accept and agree |
|||
to be bound by the terms and conditions of this Creative Commons |
|||
Attribution-NonCommercial-ShareAlike 4.0 International Public License |
|||
("Public License"). To the extent this Public License may be |
|||
interpreted as a contract, You are granted the Licensed Rights in |
|||
consideration of Your acceptance of these terms and conditions, and the |
|||
Licensor grants You such rights in consideration of benefits the |
|||
Licensor receives from making the Licensed Material available under |
|||
these terms and conditions. |
|||
|
|||
|
|||
Section 1 -- Definitions. |
|||
|
|||
a. Adapted Material means material subject to Copyright and Similar |
|||
Rights that is derived from or based upon the Licensed Material |
|||
and in which the Licensed Material is translated, altered, |
|||
arranged, transformed, or otherwise modified in a manner requiring |
|||
permission under the Copyright and Similar Rights held by the |
|||
Licensor. For purposes of this Public License, where the Licensed |
|||
Material is a musical work, performance, or sound recording, |
|||
Adapted Material is always produced where the Licensed Material is |
|||
synched in timed relation with a moving image. |
|||
|
|||
b. Adapter's License means the license You apply to Your Copyright |
|||
and Similar Rights in Your contributions to Adapted Material in |
|||
accordance with the terms and conditions of this Public License. |
|||
|
|||
c. BY-NC-SA Compatible License means a license listed at |
|||
creativecommons.org/compatiblelicenses, approved by Creative |
|||
Commons as essentially the equivalent of this Public License. |
|||
|
|||
d. Copyright and Similar Rights means copyright and/or similar rights |
|||
closely related to copyright including, without limitation, |
|||
performance, broadcast, sound recording, and Sui Generis Database |
|||
Rights, without regard to how the rights are labeled or |
|||
categorized. For purposes of this Public License, the rights |
|||
specified in Section 2(b)(1)-(2) are not Copyright and Similar |
|||
Rights. |
|||
|
|||
e. Effective Technological Measures means those measures that, in the |
|||
absence of proper authority, may not be circumvented under laws |
|||
fulfilling obligations under Article 11 of the WIPO Copyright |
|||
Treaty adopted on December 20, 1996, and/or similar international |
|||
agreements. |
|||
|
|||
f. Exceptions and Limitations means fair use, fair dealing, and/or |
|||
any other exception or limitation to Copyright and Similar Rights |
|||
that applies to Your use of the Licensed Material. |
|||
|
|||
g. License Elements means the license attributes listed in the name |
|||
of a Creative Commons Public License. The License Elements of this |
|||
Public License are Attribution, NonCommercial, and ShareAlike. |
|||
|
|||
h. Licensed Material means the artistic or literary work, database, |
|||
or other material to which the Licensor applied this Public |
|||
License. |
|||
|
|||
i. Licensed Rights means the rights granted to You subject to the |
|||
terms and conditions of this Public License, which are limited to |
|||
all Copyright and Similar Rights that apply to Your use of the |
|||
Licensed Material and that the Licensor has authority to license. |
|||
|
|||
j. Licensor means the individual(s) or entity(ies) granting rights |
|||
under this Public License. |
|||
|
|||
k. NonCommercial means not primarily intended for or directed towards |
|||
commercial advantage or monetary compensation. For purposes of |
|||
this Public License, the exchange of the Licensed Material for |
|||
other material subject to Copyright and Similar Rights by digital |
|||
file-sharing or similar means is NonCommercial provided there is |
|||
no payment of monetary compensation in connection with the |
|||
exchange. |
|||
|
|||
l. Share means to provide material to the public by any means or |
|||
process that requires permission under the Licensed Rights, such |
|||
as reproduction, public display, public performance, distribution, |
|||
dissemination, communication, or importation, and to make material |
|||
available to the public including in ways that members of the |
|||
public may access the material from a place and at a time |
|||
individually chosen by them. |
|||
|
|||
m. Sui Generis Database Rights means rights other than copyright |
|||
resulting from Directive 96/9/EC of the European Parliament and of |
|||
the Council of 11 March 1996 on the legal protection of databases, |
|||
as amended and/or succeeded, as well as other essentially |
|||
equivalent rights anywhere in the world. |
|||
|
|||
n. You means the individual or entity exercising the Licensed Rights |
|||
under this Public License. Your has a corresponding meaning. |
|||
|
|||
|
|||
Section 2 -- Scope. |
|||
|
|||
a. License grant. |
|||
|
|||
1. Subject to the terms and conditions of this Public License, |
|||
the Licensor hereby grants You a worldwide, royalty-free, |
|||
non-sublicensable, non-exclusive, irrevocable license to |
|||
exercise the Licensed Rights in the Licensed Material to: |
|||
|
|||
a. reproduce and Share the Licensed Material, in whole or |
|||
in part, for NonCommercial purposes only; and |
|||
|
|||
b. produce, reproduce, and Share Adapted Material for |
|||
NonCommercial purposes only. |
|||
|
|||
2. Exceptions and Limitations. For the avoidance of doubt, where |
|||
Exceptions and Limitations apply to Your use, this Public |
|||
License does not apply, and You do not need to comply with |
|||
its terms and conditions. |
|||
|
|||
3. Term. The term of this Public License is specified in Section |
|||
6(a). |
|||
|
|||
4. Media and formats; technical modifications allowed. The |
|||
Licensor authorizes You to exercise the Licensed Rights in |
|||
all media and formats whether now known or hereafter created, |
|||
and to make technical modifications necessary to do so. The |
|||
Licensor waives and/or agrees not to assert any right or |
|||
authority to forbid You from making technical modifications |
|||
necessary to exercise the Licensed Rights, including |
|||
technical modifications necessary to circumvent Effective |
|||
Technological Measures. For purposes of this Public License, |
|||
simply making modifications authorized by this Section 2(a) |
|||
(4) never produces Adapted Material. |
|||
|
|||
5. Downstream recipients. |
|||
|
|||
a. Offer from the Licensor -- Licensed Material. Every |
|||
recipient of the Licensed Material automatically |
|||
receives an offer from the Licensor to exercise the |
|||
Licensed Rights under the terms and conditions of this |
|||
Public License. |
|||
|
|||
b. Additional offer from the Licensor -- Adapted Material. |
|||
Every recipient of Adapted Material from You |
|||
automatically receives an offer from the Licensor to |
|||
exercise the Licensed Rights in the Adapted Material |
|||
under the conditions of the Adapter's License You apply. |
|||
|
|||
c. No downstream restrictions. You may not offer or impose |
|||
any additional or different terms or conditions on, or |
|||
apply any Effective Technological Measures to, the |
|||
Licensed Material if doing so restricts exercise of the |
|||
Licensed Rights by any recipient of the Licensed |
|||
Material. |
|||
|
|||
6. No endorsement. Nothing in this Public License constitutes or |
|||
may be construed as permission to assert or imply that You |
|||
are, or that Your use of the Licensed Material is, connected |
|||
with, or sponsored, endorsed, or granted official status by, |
|||
the Licensor or others designated to receive attribution as |
|||
provided in Section 3(a)(1)(A)(i). |
|||
|
|||
b. Other rights. |
|||
|
|||
1. Moral rights, such as the right of integrity, are not |
|||
licensed under this Public License, nor are publicity, |
|||
privacy, and/or other similar personality rights; however, to |
|||
the extent possible, the Licensor waives and/or agrees not to |
|||
assert any such rights held by the Licensor to the limited |
|||
extent necessary to allow You to exercise the Licensed |
|||
Rights, but not otherwise. |
|||
|
|||
2. Patent and trademark rights are not licensed under this |
|||
Public License. |
|||
|
|||
3. To the extent possible, the Licensor waives any right to |
|||
collect royalties from You for the exercise of the Licensed |
|||
Rights, whether directly or through a collecting society |
|||
under any voluntary or waivable statutory or compulsory |
|||
licensing scheme. In all other cases the Licensor expressly |
|||
reserves any right to collect such royalties, including when |
|||
the Licensed Material is used other than for NonCommercial |
|||
purposes. |
|||
|
|||
|
|||
Section 3 -- License Conditions. |
|||
|
|||
Your exercise of the Licensed Rights is expressly made subject to the |
|||
following conditions. |
|||
|
|||
a. Attribution. |
|||
|
|||
1. If You Share the Licensed Material (including in modified |
|||
form), You must: |
|||
|
|||
a. retain the following if it is supplied by the Licensor |
|||
with the Licensed Material: |
|||
|
|||
i. identification of the creator(s) of the Licensed |
|||
Material and any others designated to receive |
|||
attribution, in any reasonable manner requested by |
|||
the Licensor (including by pseudonym if |
|||
designated); |
|||
|
|||
ii. a copyright notice; |
|||
|
|||
iii. a notice that refers to this Public License; |
|||
|
|||
iv. a notice that refers to the disclaimer of |
|||
warranties; |
|||
|
|||
v. a URI or hyperlink to the Licensed Material to the |
|||
extent reasonably practicable; |
|||
|
|||
b. indicate if You modified the Licensed Material and |
|||
retain an indication of any previous modifications; and |
|||
|
|||
c. indicate the Licensed Material is licensed under this |
|||
Public License, and include the text of, or the URI or |
|||
hyperlink to, this Public License. |
|||
|
|||
2. You may satisfy the conditions in Section 3(a)(1) in any |
|||
reasonable manner based on the medium, means, and context in |
|||
which You Share the Licensed Material. For example, it may be |
|||
reasonable to satisfy the conditions by providing a URI or |
|||
hyperlink to a resource that includes the required |
|||
information. |
|||
3. If requested by the Licensor, You must remove any of the |
|||
information required by Section 3(a)(1)(A) to the extent |
|||
reasonably practicable. |
|||
|
|||
b. ShareAlike. |
|||
|
|||
In addition to the conditions in Section 3(a), if You Share |
|||
Adapted Material You produce, the following conditions also apply. |
|||
|
|||
1. The Adapter's License You apply must be a Creative Commons |
|||
license with the same License Elements, this version or |
|||
later, or a BY-NC-SA Compatible License. |
|||
|
|||
2. You must include the text of, or the URI or hyperlink to, the |
|||
Adapter's License You apply. You may satisfy this condition |
|||
in any reasonable manner based on the medium, means, and |
|||
context in which You Share Adapted Material. |
|||
|
|||
3. You may not offer or impose any additional or different terms |
|||
or conditions on, or apply any Effective Technological |
|||
Measures to, Adapted Material that restrict exercise of the |
|||
rights granted under the Adapter's License You apply. |
|||
|
|||
|
|||
Section 4 -- Sui Generis Database Rights. |
|||
|
|||
Where the Licensed Rights include Sui Generis Database Rights that |
|||
apply to Your use of the Licensed Material: |
|||
|
|||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right |
|||
to extract, reuse, reproduce, and Share all or a substantial |
|||
portion of the contents of the database for NonCommercial purposes |
|||
only; |
|||
|
|||
b. if You include all or a substantial portion of the database |
|||
contents in a database in which You have Sui Generis Database |
|||
Rights, then the database in which You have Sui Generis Database |
|||
Rights (but not its individual contents) is Adapted Material, |
|||
including for purposes of Section 3(b); and |
|||
|
|||
c. You must comply with the conditions in Section 3(a) if You Share |
|||
all or a substantial portion of the contents of the database. |
|||
|
|||
For the avoidance of doubt, this Section 4 supplements and does not |
|||
replace Your obligations under this Public License where the Licensed |
|||
Rights include other Copyright and Similar Rights. |
|||
|
|||
|
|||
Section 5 -- Disclaimer of Warranties and Limitation of Liability. |
|||
|
|||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE |
|||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS |
|||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF |
|||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, |
|||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, |
|||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR |
|||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, |
|||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT |
|||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT |
|||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. |
|||
|
|||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE |
|||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, |
|||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, |
|||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, |
|||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR |
|||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN |
|||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR |
|||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR |
|||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. |
|||
|
|||
c. The disclaimer of warranties and limitation of liability provided |
|||
above shall be interpreted in a manner that, to the extent |
|||
possible, most closely approximates an absolute disclaimer and |
|||
waiver of all liability. |
|||
|
|||
|
|||
Section 6 -- Term and Termination. |
|||
|
|||
a. This Public License applies for the term of the Copyright and |
|||
Similar Rights licensed here. However, if You fail to comply with |
|||
this Public License, then Your rights under this Public License |
|||
terminate automatically. |
|||
|
|||
b. Where Your right to use the Licensed Material has terminated under |
|||
Section 6(a), it reinstates: |
|||
|
|||
1. automatically as of the date the violation is cured, provided |
|||
it is cured within 30 days of Your discovery of the |
|||
violation; or |
|||
|
|||
2. upon express reinstatement by the Licensor. |
|||
|
|||
For the avoidance of doubt, this Section 6(b) does not affect any |
|||
right the Licensor may have to seek remedies for Your violations |
|||
of this Public License. |
|||
|
|||
c. For the avoidance of doubt, the Licensor may also offer the |
|||
Licensed Material under separate terms or conditions or stop |
|||
distributing the Licensed Material at any time; however, doing so |
|||
will not terminate this Public License. |
|||
|
|||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public |
|||
License. |
|||
|
|||
|
|||
Section 7 -- Other Terms and Conditions. |
|||
|
|||
a. The Licensor shall not be bound by any additional or different |
|||
terms or conditions communicated by You unless expressly agreed. |
|||
|
|||
b. Any arrangements, understandings, or agreements regarding the |
|||
Licensed Material not stated herein are separate from and |
|||
independent of the terms and conditions of this Public License. |
|||
|
|||
|
|||
Section 8 -- Interpretation. |
|||
|
|||
a. For the avoidance of doubt, this Public License does not, and |
|||
shall not be interpreted to, reduce, limit, restrict, or impose |
|||
conditions on any use of the Licensed Material that could lawfully |
|||
be made without permission under this Public License. |
|||
|
|||
b. To the extent possible, if any provision of this Public License is |
|||
deemed unenforceable, it shall be automatically reformed to the |
|||
minimum extent necessary to make it enforceable. If the provision |
|||
cannot be reformed, it shall be severed from this Public License |
|||
without affecting the enforceability of the remaining terms and |
|||
conditions. |
|||
|
|||
c. No term or condition of this Public License will be waived and no |
|||
failure to comply consented to unless expressly agreed to by the |
|||
Licensor. |
|||
|
|||
d. Nothing in this Public License constitutes or may be interpreted |
|||
as a limitation upon, or waiver of, any privileges and immunities |
|||
that apply to the Licensor or You, including from the legal |
|||
processes of any jurisdiction or authority. |
|||
|
|||
======================================================================= |
|||
|
|||
Creative Commons is not a party to its public |
|||
licenses. Notwithstanding, Creative Commons may elect to apply one of |
|||
its public licenses to material it publishes and in those instances |
|||
will be considered the “Licensor.” The text of the Creative Commons |
|||
public licenses is dedicated to the public domain under the CC0 Public |
|||
Domain Dedication. Except for the limited purpose of indicating that |
|||
material is shared under a Creative Commons public license or as |
|||
otherwise permitted by the Creative Commons policies published at |
|||
creativecommons.org/policies, Creative Commons does not authorize the |
|||
use of the trademark "Creative Commons" or any other trademark or logo |
|||
of Creative Commons without its prior written consent including, |
|||
without limitation, in connection with any unauthorized modifications |
|||
to any of its public licenses or any other arrangements, |
|||
understandings, or agreements concerning use of licensed material. For |
|||
the avoidance of doubt, this paragraph does not form part of the |
|||
public licenses. |
|||
|
|||
Creative Commons may be contacted at creativecommons.org. |
After Width: | Height: | Size: 650 KiB |
After Width: | Height: | Size: 66 KiB |
@ -0,0 +1,51 @@ |
|||
# Prometheus Endpoint |
|||
Metrics available for AhoyDTU device, inverters and channels. |
|||
|
|||
Prometheus metrics provided at `/metrics`. |
|||
|
|||
## Labels |
|||
| Label name | Description | |
|||
|:-------------|:--------------------------------------| |
|||
| version | current installed version of AhoyDTU | |
|||
| image | currently not used | |
|||
| devicename | Device name from setup | |
|||
| name | Inverter name from setup | |
|||
| serial | Serial number of inverter | |
|||
| inverter | Inverter name from setup | |
|||
| channel | Channel name from setup | |
|||
|
|||
## Exported Metrics |
|||
| Metric name | Type | Description | Labels | |
|||
|----------------------------------------|---------|--------------------------------------------------------|--------------| |
|||
| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | |
|||
| `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename | |
|||
| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | |
|||
| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial | |
|||
| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | |
|||
| `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | |
|||
| `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | |
|||
| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | |
|||
| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | |
|||
| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | |
|||
| `ahoy_solar_Q_AC_var` | Gauge | AC reactive power[var] | inverter | |
|||
| `ahoy_solar_F_AC_hertz` | Gauge | AC frequency [Hz] | inverter | |
|||
| `ahoy_solar_PF_AC` | Gauge | AC Power factor | inverter | |
|||
| `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter | |
|||
| `ahoy_solar_ALARM_MES_ID` | Gauge | Alarm message index of inverter | inverter | |
|||
| `ahoy_solar_LastAlarmCode` | Gauge | Last alarm code from inverter | inverter | |
|||
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter | |
|||
| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter | |
|||
| `ahoy_solar_P_DC_watt` | Gauge | DC power of inverter [W] | inverter | |
|||
| `ahoy_solar_Efficiency_ratio` | Gauge | ration AC Power over DC Power [%] | inverter | |
|||
| `ahoy_solar_U_DC_volt` | Gauge | DC voltage of channel [V] | inverter, channel | |
|||
| `ahoy_solar_I_DC_ampere` | Gauge | DC current of channel [A] | inverter, channel | |
|||
| `ahoy_solar_P_DC_watt` | Gauge | DC power of channel [P] | inverter, channel | |
|||
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel | |
|||
| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel | |
|||
| `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel | |
|||
| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | | |
|||
| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | | |
|||
| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | | |
|||
| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | | |
|||
| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | | |
|||
|
@ -0,0 +1,55 @@ |
|||
import os |
|||
from datetime import date |
|||
import json |
|||
|
|||
def readVersion(path, infile): |
|||
f = open(path + infile, "r") |
|||
lines = f.readlines() |
|||
f.close() |
|||
|
|||
today = date.today() |
|||
search = ["_MAJOR", "_MINOR", "_PATCH"] |
|||
version = today.strftime("%y%m%d") + "_ahoy_" |
|||
versionnumber = ""# "ahoy_v" |
|||
for line in lines: |
|||
if(line.find("VERSION_") != -1): |
|||
for s in search: |
|||
p = line.find(s) |
|||
if(p != -1): |
|||
version += line[p+13:].rstrip() + "." |
|||
versionnumber += line[p+13:].rstrip() + "." |
|||
|
|||
return [versionnumber[:-1], version[:-1]] |
|||
|
|||
def buildManifest(path, infile, outfile): |
|||
version = readVersion(path, infile) |
|||
sha = os.getenv("SHA",default="sha") |
|||
data = {} |
|||
data["name"] = "AhoyDTU - Development" |
|||
data["version"] = version[0] |
|||
data["new_install_prompt_erase"] = 1 |
|||
data["builds"] = [] |
|||
|
|||
esp32 = {} |
|||
esp32["chipFamily"] = "ESP32" |
|||
esp32["parts"] = [] |
|||
esp32["parts"].append({"path": "bootloader.bin", "offset": 4096}) |
|||
esp32["parts"].append({"path": "partitions.bin", "offset": 32768}) |
|||
esp32["parts"].append({"path": "ota.bin", "offset": 57344}) |
|||
esp32["parts"].append({"path": version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) |
|||
data["builds"].append(esp32) |
|||
|
|||
esp8266 = {} |
|||
esp8266["chipFamily"] = "ESP8266" |
|||
esp8266["parts"] = [] |
|||
esp8266["parts"].append({"path": version[1] + "_" + sha + "_esp8266.bin", "offset": 0}) |
|||
data["builds"].append(esp8266) |
|||
|
|||
jsonString = json.dumps(data, indent=2) |
|||
|
|||
fp = open(path + "firmware/" + outfile, "w") |
|||
fp.write(jsonString) |
|||
fp.close() |
|||
|
|||
|
|||
buildManifest("", "defines.h", "manifest.json") |
@ -0,0 +1,97 @@ |
|||
import os |
|||
import shutil |
|||
import gzip |
|||
from datetime import date |
|||
|
|||
def genOtaBin(path): |
|||
arr = [] |
|||
arr.append(1) |
|||
arr.append(0) |
|||
arr.append(0) |
|||
arr.append(0) |
|||
for x in range(24): |
|||
arr.append(255) |
|||
arr.append(154) |
|||
arr.append(152) |
|||
arr.append(67) |
|||
arr.append(71) |
|||
for x in range(4064): |
|||
arr.append(255) |
|||
arr.append(0) |
|||
arr.append(0) |
|||
arr.append(0) |
|||
arr.append(0) |
|||
for x in range(4092): |
|||
arr.append(255) |
|||
with open(path + "ota.bin", "wb") as f: |
|||
f.write(bytearray(arr)) |
|||
|
|||
# write gzip firmware file |
|||
def gzip_bin(bin_file, gzip_file): |
|||
with open(bin_file,"rb") as fp: |
|||
with gzip.open(gzip_file, "wb", compresslevel = 9) as f: |
|||
shutil.copyfileobj(fp, f) |
|||
|
|||
def readVersion(path, infile): |
|||
f = open(path + infile, "r") |
|||
lines = f.readlines() |
|||
f.close() |
|||
|
|||
today = date.today() |
|||
search = ["_MAJOR", "_MINOR", "_PATCH"] |
|||
version = today.strftime("%y%m%d") + "_ahoy_" |
|||
versionnumber = "ahoy_v" |
|||
for line in lines: |
|||
if(line.find("VERSION_") != -1): |
|||
for s in search: |
|||
p = line.find(s) |
|||
if(p != -1): |
|||
version += line[p+13:].rstrip() + "." |
|||
versionnumber += line[p+13:].rstrip() + "." |
|||
|
|||
os.mkdir(path + "firmware/") |
|||
sha = os.getenv("SHA",default="sha") |
|||
|
|||
versionout = version[:-1] + "_" + sha + "_esp8266.bin" |
|||
src = path + ".pio/build/esp8266-release/firmware.bin" |
|||
dst = path + "firmware/" + versionout |
|||
os.rename(src, dst) |
|||
|
|||
versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin" |
|||
src = path + ".pio/build/esp8266-release-prometheus/firmware.bin" |
|||
dst = path + "firmware/" + versionout |
|||
os.rename(src, dst) |
|||
|
|||
versionout = version[:-1] + "_" + sha + "_esp8285.bin" |
|||
src = path + ".pio/build/esp8285-release/firmware.bin" |
|||
dst = path + "firmware/" + versionout |
|||
os.rename(src, dst) |
|||
gzip_bin(dst, dst + ".gz") |
|||
|
|||
versionout = version[:-1] + "_" + sha + "_esp32.bin" |
|||
src = path + ".pio/build/esp32-wroom32-release/firmware.bin" |
|||
dst = path + "firmware/" + versionout |
|||
os.rename(src, dst) |
|||
|
|||
versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin" |
|||
src = path + ".pio/build/esp32-wroom32-release-prometheus/firmware.bin" |
|||
dst = path + "firmware/" + versionout |
|||
os.rename(src, dst) |
|||
|
|||
versionout = version[:-1] + "_" + sha + "_esp32s3.bin" |
|||
src = path + ".pio/build/opendtufusionv1-release/firmware.bin" |
|||
dst = path + "firmware/" + versionout |
|||
os.rename(src, dst) |
|||
|
|||
# other ESP32 bin files |
|||
src = path + ".pio/build/esp32-wroom32-release/" |
|||
dst = path + "firmware/" |
|||
os.rename(src + "bootloader.bin", dst + "bootloader.bin") |
|||
os.rename(src + "partitions.bin", dst + "partitions.bin") |
|||
genOtaBin(path + "firmware/") |
|||
os.rename("../scripts/gh-action-dev-build-flash.html", path + "install.html") |
|||
|
|||
print("name=" + versionnumber[:-1] ) |
|||
|
|||
|
|||
readVersion("", "defines.h") |
@ -0,0 +1,93 @@ |
|||
<!doctype html> |
|||
<html lang="en" class="h-100"> |
|||
<body class="d-flex flex-column h-100"> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
|||
|
|||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> |
|||
|
|||
<title>Flash | AhoyDTU</title> |
|||
</head> |
|||
<main> |
|||
<div class="alert alert-secondary my-3" role="alert"> |
|||
Ahoy ist unter <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/de/" target="_blank">CC-BY-NC-SA 4.0</a> lizensiert. |
|||
</div> |
|||
<div class="container col-xxl-8 px-4 py-5"> |
|||
<h3>Development Build (ESP8266 / ESP32)</h3> |
|||
<p class="lead"> |
|||
|
|||
</p> |
|||
<p class="lead" > |
|||
Hierzu die Ahoy-Hardware per USB Kabel an den PC stecken und evtl. warten, bis die Treiber installiert sind. Anschließend auf den ensprechenden <strong>connect</strong> Button klicken. |
|||
</p> |
|||
|
|||
<esp-web-install-button manifest="firmware/manifest.json"> |
|||
<button class="btn btn-primary" slot="activate">Development Build installieren</button> |
|||
<span slot="unsupported"> |
|||
<div class="alert alert-warning" role="alert"> |
|||
Your browser does not support installing things on ESP devices. Use Google Chrome or Microsoft Edge. |
|||
</div> |
|||
</span> |
|||
<span slot="not-allowed"> |
|||
<div class="alert alert-danger" role="alert"> |
|||
You are not allowed to do this using http! |
|||
</div> |
|||
</span> |
|||
</esp-web-install-button> |
|||
<div class="alert alert-secondary mt-3" role="alert"> |
|||
Der Online Installer wird von <a href="https://esphome.github.io/esp-web-tools/" target="_blank">ESP Home Web Tools</a> bereitgestellt |
|||
</div> |
|||
|
|||
<h3>Release Build</h3> |
|||
<p class="lead" > |
|||
Die Release Builds werden auf <a href="https://ahoydtu.de" target="_blank">ahoyDtu.de</a> veröffentlicht. |
|||
</p> |
|||
</div> |
|||
|
|||
<div class="container col-xxl-8 px-4 py-5"> |
|||
<h3>Vorbereitungen Google Chrome</h3> |
|||
<p class="lead"> |
|||
Bekommt man nach der Auswahl des COM-Ports einen Fehler <i>Failed to download manifest</i> muss man Chrome mit einem Parameter starten: |
|||
</p> |
|||
<p class="lead"> |
|||
<div class="row lead mb-2"> |
|||
<div class="col col-md-2 col-sm-12"> |
|||
Windows |
|||
</div> |
|||
<div class="col mx-sm-4"> |
|||
<code>start chrome --allow-file-access-from-files</code> |
|||
</div> |
|||
</div> |
|||
<div class="row lead mb-2"> |
|||
<div class="col col-md-2 col-sm-12"> |
|||
Linux |
|||
</div> |
|||
<div class="col mx-sm-4"> |
|||
<code>google-chrome --allow-file-access-from-files</code> |
|||
</div> |
|||
</div> |
|||
<div class="row lead"> |
|||
<div class="col col-md-2 col-sm-12"> |
|||
OS X |
|||
</div> |
|||
<div class="col mx-sm-4"> |
|||
<code>open -a 'Google Chrome' --args -allow-file-access-from-files</code> |
|||
</div> |
|||
</div> |
|||
</p> |
|||
<div class="alert alert-warning" role="alert"> |
|||
<strong>Wichtig: </strong>es darf keine weitere Instanz von Chrome offen sein, sonst funktionert der Parameter nicht. |
|||
</div> |
|||
</div> |
|||
</main> |
|||
<footer class="footer mt-auto text-center text-lg-start text-white" style="background-color: #45526e"> |
|||
<div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.2);"> |
|||
© 2022 |
|||
<a class="text-white" href="https://ahoydtu.de">AhoyDTU.de</a> |
|||
</div> |
|||
</footer> |
|||
<script type="module" src="https://unpkg.com/esp-web-tools@9.0.3/dist/web/install-button.js?module"></script> |
|||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,10 @@ |
|||
{ |
|||
// See http://go.microsoft.com/fwlink/?LinkId=827846 |
|||
// for the documentation about the extensions.json format |
|||
"recommendations": [ |
|||
"platformio.platformio-ide" |
|||
], |
|||
"unwantedRecommendations": [ |
|||
"ms-vscode.cpptools-extension-pack" |
|||
] |
|||
} |
@ -0,0 +1,86 @@ |
|||
// Place your settings in this file to overwrite default and user settings. |
|||
{ |
|||
// identify that settings is loaded |
|||
"workbench.colorCustomizations": { |
|||
"editorLineNumber.foreground": "#00ff00" |
|||
}, |
|||
"editor.wordWrap": "off", |
|||
"files.eol": "\n", |
|||
"files.trimTrailingWhitespace": true, |
|||
"diffEditor.ignoreTrimWhitespace": true, |
|||
"files.autoSave": "afterDelay", |
|||
"editor.tabSize": 4, |
|||
"editor.insertSpaces": true, |
|||
// `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents. |
|||
// Set to false to keep the values you've explicitly set, above. |
|||
"editor.detectIndentation": false, |
|||
// https://clang.llvm.org/docs/ClangFormatStyleOptions.html |
|||
"C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}", |
|||
"files.associations": { |
|||
"typeinfo": "cpp", |
|||
"string": "cpp", |
|||
"istream": "cpp", |
|||
"ostream": "cpp", |
|||
"array": "cpp", |
|||
"atomic": "cpp", |
|||
"*.tcc": "cpp", |
|||
"bitset": "cpp", |
|||
"cctype": "cpp", |
|||
"chrono": "cpp", |
|||
"clocale": "cpp", |
|||
"cmath": "cpp", |
|||
"cstdarg": "cpp", |
|||
"cstddef": "cpp", |
|||
"cstdint": "cpp", |
|||
"cstdio": "cpp", |
|||
"cstdlib": "cpp", |
|||
"cstring": "cpp", |
|||
"ctime": "cpp", |
|||
"cwchar": "cpp", |
|||
"cwctype": "cpp", |
|||
"deque": "cpp", |
|||
"list": "cpp", |
|||
"unordered_map": "cpp", |
|||
"unordered_set": "cpp", |
|||
"vector": "cpp", |
|||
"exception": "cpp", |
|||
"algorithm": "cpp", |
|||
"functional": "cpp", |
|||
"iterator": "cpp", |
|||
"map": "cpp", |
|||
"memory": "cpp", |
|||
"memory_resource": "cpp", |
|||
"numeric": "cpp", |
|||
"optional": "cpp", |
|||
"random": "cpp", |
|||
"ratio": "cpp", |
|||
"regex": "cpp", |
|||
"string_view": "cpp", |
|||
"system_error": "cpp", |
|||
"tuple": "cpp", |
|||
"type_traits": "cpp", |
|||
"utility": "cpp", |
|||
"fstream": "cpp", |
|||
"initializer_list": "cpp", |
|||
"iomanip": "cpp", |
|||
"iosfwd": "cpp", |
|||
"limits": "cpp", |
|||
"new": "cpp", |
|||
"sstream": "cpp", |
|||
"stdexcept": "cpp", |
|||
"streambuf": "cpp", |
|||
"cinttypes": "cpp", |
|||
"bit": "cpp", |
|||
"compare": "cpp", |
|||
"concepts": "cpp", |
|||
"condition_variable": "cpp", |
|||
"set": "cpp", |
|||
"iostream": "cpp", |
|||
"mutex": "cpp", |
|||
"ranges": "cpp", |
|||
"stop_token": "cpp", |
|||
"thread": "cpp" |
|||
}, |
|||
"cmake.configureOnOpen": false, |
|||
"editor.formatOnSave": false, |
|||
} |
@ -0,0 +1,33 @@ |
|||
Changelog v0.6.0 |
|||
|
|||
## General |
|||
* improved night time calculation time to 1 minute after last communication pause #515 |
|||
* refactored code for better readability |
|||
* improved Hoymiles communication (retransmits, immediate power limit transmission, timing at all) |
|||
* renamed firmware binaries |
|||
* add login / logout to menu |
|||
* add display support for `SH1106`, `SSD1306`, `Nokia` and `ePaper 1.54"` (ESP32 only) |
|||
* add yield total correction - move your yield to a new inverter or correct an already used inverter |
|||
* added import / export feature |
|||
* added `Prometheus` endpoints |
|||
* improved wifi connection and stability (connect to strongest AP) |
|||
* addded Hoymiles alarm IDs to log |
|||
* improved `System` information page (eg. radio statitistics) |
|||
* improved UI (responsive design, (optional) dark mode) |
|||
* improved system stability (reduced `heap-fragmentation`, don't break settings on failure) #644, #645 |
|||
* added support for 2nd generation of Hoymiles inverters, MI series |
|||
* improved JSON API for more stable WebUI |
|||
* added option to disable input display in `/live` (`max-power` has to be set to `0`) |
|||
* updated documentation |
|||
* improved settings on ESP32 devices while setting SPI pins (for `NRF24` radio) |
|||
|
|||
## MqTT |
|||
* added `comm_disabled` #529 |
|||
* added fixed interval option #542, #523 |
|||
* improved communication, only required publishes |
|||
* improved retained flags |
|||
* added `set_power_limit` acknowledge MQTT publish #553 |
|||
* added feature to reset values on midnight, communication pause or if the inverters are not available |
|||
* partially added Hoymiles alarm ID |
|||
* improved autodiscover (added total values on multi-inverter setup) |
|||
* improved `clientID` a part of the MAC address is added to have an unique name |
@ -0,0 +1,416 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#include "app.h" |
|||
#include <ArduinoJson.h> |
|||
#include "utils/sun.h" |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
app::app() : ah::Scheduler() {} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::setup() { |
|||
Serial.begin(115200); |
|||
while (!Serial) |
|||
yield(); |
|||
|
|||
ah::Scheduler::setup(); |
|||
|
|||
resetSystem(); |
|||
|
|||
mSettings.setup(); |
|||
mSettings.getPtr(mConfig); |
|||
DPRINT(DBG_INFO, F("Settings valid: ")); |
|||
if (mSettings.getValid()) |
|||
DBGPRINTLN(F("true")); |
|||
else |
|||
DBGPRINTLN(F("false")); |
|||
|
|||
mSys.enableDebug(); |
|||
mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); |
|||
|
|||
#if defined(AP_ONLY) |
|||
mInnerLoopCb = std::bind(&app::loopStandard, this); |
|||
#else |
|||
mInnerLoopCb = std::bind(&app::loopWifi, this); |
|||
#endif |
|||
|
|||
mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onWifi, this, std::placeholders::_1)); |
|||
#if !defined(AP_ONLY) |
|||
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); |
|||
#endif |
|||
|
|||
mSys.addInverters(&mConfig->inst); |
|||
|
|||
mPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); |
|||
mPayload.enableSerialDebug(mConfig->serial.debug); |
|||
mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); |
|||
|
|||
mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); |
|||
mMiPayload.enableSerialDebug(mConfig->serial.debug); |
|||
|
|||
// DBGPRINTLN("--- after payload");
|
|||
// DBGPRINTLN(String(ESP.getFreeHeap()));
|
|||
// DBGPRINTLN(String(ESP.getHeapFragmentation()));
|
|||
// DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));
|
|||
|
|||
if (!mSys.Radio.isChipConnected()) |
|||
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); |
|||
|
|||
// when WiFi is in client mode, then enable mqtt broker
|
|||
#if !defined(AP_ONLY) |
|||
mMqttEnabled = (mConfig->mqtt.broker[0] > 0); |
|||
if (mMqttEnabled) { |
|||
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp); |
|||
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); |
|||
mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); |
|||
} |
|||
#endif |
|||
setupLed(); |
|||
|
|||
mWeb.setup(this, &mSys, mConfig); |
|||
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); |
|||
|
|||
mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); |
|||
|
|||
// Plugins
|
|||
if (mConfig->plugin.display.type != 0) |
|||
mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion); |
|||
|
|||
mPubSerial.setup(mConfig, &mSys, &mTimestamp); |
|||
|
|||
regularTickers(); |
|||
|
|||
|
|||
// DBGPRINTLN("--- end setup");
|
|||
// DBGPRINTLN(String(ESP.getFreeHeap()));
|
|||
// DBGPRINTLN(String(ESP.getHeapFragmentation()));
|
|||
// DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));
|
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::loop(void) { |
|||
mInnerLoopCb(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::loopStandard(void) { |
|||
ah::Scheduler::loop(); |
|||
|
|||
if (mSys.Radio.loop()) { |
|||
while (!mSys.Radio.mBufCtrl.empty()) { |
|||
packet_t *p = &mSys.Radio.mBufCtrl.front(); |
|||
|
|||
if (mConfig->serial.debug) { |
|||
DPRINT(DBG_INFO, F("RX ")); |
|||
DBGPRINT(String(p->len)); |
|||
DBGPRINT(F("B Ch")); |
|||
DBGPRINT(String(p->ch)); |
|||
DBGPRINT(F(" | ")); |
|||
mSys.Radio.dumpBuf(p->packet, p->len); |
|||
} |
|||
mStat.frmCnt++; |
|||
|
|||
Inverter<> *iv = mSys.findInverter(&p->packet[1]); |
|||
if (NULL != iv) { |
|||
if (IV_HM == iv->ivGen) |
|||
mPayload.add(iv, p); |
|||
else |
|||
mMiPayload.add(iv, p); |
|||
} |
|||
mSys.Radio.mBufCtrl.pop(); |
|||
yield(); |
|||
} |
|||
mPayload.process(true); |
|||
mMiPayload.process(true); |
|||
} |
|||
mPayload.loop(); |
|||
mMiPayload.loop(); |
|||
|
|||
if (mMqttEnabled) |
|||
mMqtt.loop(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::loopWifi(void) { |
|||
ah::Scheduler::loop(); |
|||
yield(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::onWifi(bool gotIp) { |
|||
DPRINTLN(DBG_DEBUG, F("onWifi")); |
|||
ah::Scheduler::resetTicker(); |
|||
regularTickers(); // reinstall regular tickers
|
|||
if (gotIp) { |
|||
mInnerLoopCb = std::bind(&app::loopStandard, this); |
|||
every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); |
|||
mMqttReconnect = true; |
|||
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
|
|||
once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); |
|||
if (WIFI_AP == WiFi.getMode()) { |
|||
mMqttEnabled = false; |
|||
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); |
|||
} |
|||
} else { |
|||
mInnerLoopCb = std::bind(&app::loopWifi, this); |
|||
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::regularTickers(void) { |
|||
DPRINTLN(DBG_DEBUG, F("regularTickers")); |
|||
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); |
|||
// Plugins
|
|||
if (mConfig->plugin.display.type != 0) |
|||
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); |
|||
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickNtpUpdate(void) { |
|||
uint32_t nxtTrig = 5; // default: check again in 5 sec
|
|||
bool isOK = mWifi.getNtpTime(); |
|||
if (isOK || mTimestamp != 0) { |
|||
if (mMqttReconnect && mMqttEnabled) { |
|||
mMqtt.tickerSecond(); |
|||
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); |
|||
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); |
|||
} |
|||
|
|||
// only install schedulers once even if NTP wasn't successful in first loop
|
|||
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
|
|||
if (mConfig->inst.rstValsNotAvail) |
|||
everyMin(std::bind(&app::tickMinute, this), "tMin"); |
|||
if (mConfig->inst.rstYieldMidNight) { |
|||
uint32_t localTime = gTimezone.toLocal(mTimestamp); |
|||
uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
|
|||
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); |
|||
} |
|||
} |
|||
|
|||
nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min
|
|||
|
|||
if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { |
|||
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; |
|||
tickCalcSunrise(); |
|||
} |
|||
|
|||
// immediately start communicating
|
|||
// @TODO: leads to reboot loops? not sure #674
|
|||
if (isOK && mSendFirst) { |
|||
mSendFirst = false; |
|||
once(std::bind(&app::tickSend, this), 2, "senOn"); |
|||
} |
|||
|
|||
mMqttReconnect = false; |
|||
} |
|||
once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickCalcSunrise(void) { |
|||
if (mSunrise == 0) // on boot/reboot calc sun values for current time
|
|||
ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); |
|||
|
|||
if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day
|
|||
ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); |
|||
|
|||
tickIVCommunication(); |
|||
|
|||
uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop
|
|||
onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); |
|||
if (mMqttEnabled) |
|||
tickSun(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickIVCommunication(void) { |
|||
mIVCommunicationOn = !mConfig->sun.disNightCom; // if sun.disNightCom is false, communication is always on
|
|||
if (!mIVCommunicationOn) { // inverter communication only during the day
|
|||
uint32_t nxtTrig; |
|||
if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start
|
|||
nxtTrig = mSunrise - mConfig->sun.offsetSec; |
|||
} else { |
|||
if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
|
|||
nxtTrig = 0; |
|||
} else { // current time lies within communication start/stop time, set next trigger to communication stop
|
|||
mIVCommunicationOn = true; |
|||
nxtTrig = mSunset + mConfig->sun.offsetSec; |
|||
} |
|||
} |
|||
if (nxtTrig != 0) |
|||
onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); |
|||
} |
|||
tickComm(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickSun(void) { |
|||
// only used and enabled by MQTT (see setup())
|
|||
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) |
|||
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
|
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickComm(void) { |
|||
if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) |
|||
once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); |
|||
|
|||
if (mMqttEnabled) { |
|||
if (!mMqtt.tickerComm(!mIVCommunicationOn)) |
|||
once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s
|
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickZeroValues(void) { |
|||
Inverter<> *iv; |
|||
// set values to zero, except yields
|
|||
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { |
|||
iv = mSys.getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
|
|||
mPayload.zeroInverterValues(iv); |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickMinute(void) { |
|||
// only triggered if 'reset values on no avail is enabled'
|
|||
|
|||
Inverter<> *iv; |
|||
// set values to zero, except yields
|
|||
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { |
|||
iv = mSys.getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
|
|||
if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) |
|||
mPayload.zeroInverterValues(iv); |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickMidnight(void) { |
|||
// only triggered if 'reset values at midnight is enabled'
|
|||
uint32_t localTime = gTimezone.toLocal(mTimestamp); |
|||
uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
|
|||
onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); |
|||
|
|||
Inverter<> *iv; |
|||
// set values to zero, except yield total
|
|||
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { |
|||
iv = mSys.getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
|
|||
mPayload.zeroInverterValues(iv); |
|||
mPayload.zeroYieldDay(iv); |
|||
} |
|||
|
|||
if (mMqttEnabled) |
|||
mMqtt.tickerMidnight(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::tickSend(void) { |
|||
if (!mSys.Radio.isChipConnected()) { |
|||
DPRINTLN(DBG_WARN, F("NRF24 not connected!")); |
|||
return; |
|||
} |
|||
if (mIVCommunicationOn) { |
|||
if (!mSys.Radio.mBufCtrl.empty()) { |
|||
if (mConfig->serial.debug) { |
|||
DPRINT(DBG_DEBUG, F("recbuf not empty! #")); |
|||
DBGPRINTLN(String(mSys.Radio.mBufCtrl.size())); |
|||
} |
|||
} |
|||
|
|||
int8_t maxLoop = MAX_NUM_INVERTERS; |
|||
Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); |
|||
do { |
|||
mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; |
|||
iv = mSys.getInverterByPos(mSendLastIvId); |
|||
} while ((NULL == iv) && ((maxLoop--) > 0)); |
|||
|
|||
if (NULL != iv) { |
|||
if (iv->config->enabled) { |
|||
if (iv->ivGen == IV_HM) |
|||
mPayload.ivSend(iv); |
|||
else |
|||
mMiPayload.ivSend(iv); |
|||
} |
|||
} |
|||
} else { |
|||
if (mConfig->serial.debug) |
|||
DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!")); |
|||
} |
|||
yield(); |
|||
|
|||
updateLed(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::resetSystem(void) { |
|||
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); |
|||
|
|||
#ifdef AP_ONLY |
|||
mTimestamp = 1; |
|||
#endif |
|||
|
|||
mSendFirst = true; |
|||
|
|||
mSunrise = 0; |
|||
mSunset = 0; |
|||
|
|||
mMqttEnabled = false; |
|||
|
|||
mSendLastIvId = 0; |
|||
mShowRebootRequest = false; |
|||
mIVCommunicationOn = true; |
|||
mSavePending = false; |
|||
mSaveReboot = false; |
|||
|
|||
memset(&mStat, 0, sizeof(statistics_t)); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::mqttSubRxCb(JsonObject obj) { |
|||
mApi.ctrlRequest(obj); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::setupLed(void) { |
|||
/** LED connection diagram
|
|||
* \\ |
|||
* PIN ---- |<----- 3.3V |
|||
* |
|||
* */ |
|||
if (mConfig->led.led0 != 0xff) { |
|||
pinMode(mConfig->led.led0, OUTPUT); |
|||
digitalWrite(mConfig->led.led0, HIGH); // LED off
|
|||
} |
|||
if (mConfig->led.led1 != 0xff) { |
|||
pinMode(mConfig->led.led1, OUTPUT); |
|||
digitalWrite(mConfig->led.led1, HIGH); // LED off
|
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::updateLed(void) { |
|||
if (mConfig->led.led0 != 0xff) { |
|||
Inverter<> *iv = mSys.getInverterByPos(0); |
|||
if (NULL != iv) { |
|||
if (iv->isProducing(mTimestamp)) |
|||
digitalWrite(mConfig->led.led0, LOW); // LED on
|
|||
else |
|||
digitalWrite(mConfig->led.led0, HIGH); // LED off
|
|||
} |
|||
} |
|||
} |
@ -0,0 +1,297 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __APP_H__ |
|||
#define __APP_H__ |
|||
|
|||
#include <Arduino.h> |
|||
#include <ArduinoJson.h> |
|||
#include <RF24.h> |
|||
#include <RF24_config.h> |
|||
|
|||
#include "appInterface.h" |
|||
#include "config/settings.h" |
|||
#include "defines.h" |
|||
#include "hm/hmPayload.h" |
|||
#include "hm/hmSystem.h" |
|||
#include "hm/miPayload.h" |
|||
#include "publisher/pubMqtt.h" |
|||
#include "publisher/pubSerial.h" |
|||
#include "utils/crc.h" |
|||
#include "utils/dbg.h" |
|||
#include "utils/scheduler.h" |
|||
#include "web/RestApi.h" |
|||
#include "web/web.h" |
|||
#include "wifi/ahoywifi.h" |
|||
|
|||
// convert degrees and radians for sun calculation
|
|||
#define SIN(x) (sin(radians(x))) |
|||
#define COS(x) (cos(radians(x))) |
|||
#define ASIN(x) (degrees(asin(x))) |
|||
#define ACOS(x) (degrees(acos(x))) |
|||
|
|||
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType; |
|||
typedef HmPayload<HmSystemType> PayloadType; |
|||
typedef MiPayload<HmSystemType> MiPayloadType; |
|||
typedef Web<HmSystemType> WebType; |
|||
typedef RestApi<HmSystemType> RestApiType; |
|||
typedef PubMqtt<HmSystemType> PubMqttType; |
|||
typedef PubSerial<HmSystemType> PubSerialType; |
|||
|
|||
// PLUGINS
|
|||
#include "plugins/Display/Display.h" |
|||
typedef Display<HmSystemType> DisplayType; |
|||
|
|||
class app : public IApp, public ah::Scheduler { |
|||
public: |
|||
app(); |
|||
~app() {} |
|||
|
|||
void setup(void); |
|||
void loop(void); |
|||
void loopStandard(void); |
|||
void loopWifi(void); |
|||
void onWifi(bool gotIp); |
|||
void regularTickers(void); |
|||
|
|||
void handleIntr(void) { |
|||
mSys.Radio.handleIntr(); |
|||
} |
|||
|
|||
uint32_t getUptime() { |
|||
return Scheduler::getUptime(); |
|||
} |
|||
|
|||
uint32_t getTimestamp() { |
|||
return Scheduler::getTimestamp(); |
|||
} |
|||
|
|||
bool saveSettings(bool reboot) { |
|||
mShowRebootRequest = true; // only message on index, no reboot
|
|||
mSavePending = true; |
|||
mSaveReboot = reboot; |
|||
once(std::bind(&app::tickSave, this), 3, "save"); |
|||
return true; |
|||
} |
|||
|
|||
bool readSettings(const char *path) { |
|||
return mSettings.readSettings(path); |
|||
} |
|||
|
|||
bool eraseSettings(bool eraseWifi = false) { |
|||
return mSettings.eraseSettings(eraseWifi); |
|||
} |
|||
|
|||
bool getSavePending() { |
|||
return mSavePending; |
|||
} |
|||
|
|||
bool getLastSaveSucceed() { |
|||
return mSettings.getLastSaveSucceed(); |
|||
} |
|||
|
|||
statistics_t *getStatistics() { |
|||
return &mStat; |
|||
} |
|||
|
|||
void scanAvailNetworks() { |
|||
mWifi.scanAvailNetworks(); |
|||
} |
|||
|
|||
void getAvailNetworks(JsonObject obj) { |
|||
mWifi.getAvailNetworks(obj); |
|||
} |
|||
|
|||
void setOnUpdate() { |
|||
onWifi(false); |
|||
} |
|||
|
|||
void setRebootFlag() { |
|||
once(std::bind(&app::tickReboot, this), 3, "rboot"); |
|||
} |
|||
|
|||
const char *getVersion() { |
|||
return mVersion; |
|||
} |
|||
|
|||
uint32_t getSunrise() { |
|||
return mSunrise; |
|||
} |
|||
|
|||
uint32_t getSunset() { |
|||
return mSunset; |
|||
} |
|||
|
|||
bool getSettingsValid() { |
|||
return mSettings.getValid(); |
|||
} |
|||
|
|||
bool getRebootRequestState() { |
|||
return mShowRebootRequest; |
|||
} |
|||
|
|||
void setMqttDiscoveryFlag() { |
|||
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); |
|||
} |
|||
|
|||
void setMqttPowerLimitAck(Inverter<> *iv) { |
|||
mMqtt.setPowerLimitAck(iv); |
|||
} |
|||
|
|||
void ivSendHighPrio(Inverter<> *iv) { |
|||
if(mIVCommunicationOn) // only send commands if communcation is enabled
|
|||
mPayload.ivSendHighPrio(iv); |
|||
} |
|||
|
|||
bool getMqttIsConnected() { |
|||
return mMqtt.isConnected(); |
|||
} |
|||
|
|||
uint32_t getMqttTxCnt() { |
|||
return mMqtt.getTxCnt(); |
|||
} |
|||
|
|||
uint32_t getMqttRxCnt() { |
|||
return mMqtt.getRxCnt(); |
|||
} |
|||
|
|||
bool getProtection() { |
|||
return mWeb.getProtection(); |
|||
} |
|||
|
|||
uint8_t getIrqPin(void) { |
|||
return mConfig->nrf.pinIrq; |
|||
} |
|||
|
|||
String getTimeStr(uint32_t offset = 0) { |
|||
char str[10]; |
|||
if(0 == mTimestamp) |
|||
sprintf(str, "n/a"); |
|||
else |
|||
sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp + offset), minute(mTimestamp + offset), second(mTimestamp + offset)); |
|||
return String(str); |
|||
} |
|||
|
|||
uint32_t getTimezoneOffset() { |
|||
return mApi.getTimezoneOffset(); |
|||
} |
|||
|
|||
void getSchedulerInfo(uint8_t *max) { |
|||
getStat(max); |
|||
} |
|||
|
|||
void getSchedulerNames(void) { |
|||
printSchedulers(); |
|||
} |
|||
|
|||
void setTimestamp(uint32_t newTime) { |
|||
DPRINT(DBG_DEBUG, F("setTimestamp: ")); |
|||
DBGPRINTLN(String(newTime)); |
|||
if(0 == newTime) |
|||
mWifi.getNtpTime(); |
|||
else |
|||
Scheduler::setTimestamp(newTime); |
|||
} |
|||
|
|||
HmSystemType mSys; |
|||
|
|||
private: |
|||
typedef std::function<void()> innerLoopCb; |
|||
|
|||
void resetSystem(void); |
|||
|
|||
void payloadEventListener(uint8_t cmd) { |
|||
#if !defined(AP_ONLY) |
|||
if (mMqttEnabled) |
|||
mMqtt.payloadEventListener(cmd); |
|||
#endif |
|||
if(mConfig->plugin.display.type != 0) |
|||
mDisplay.payloadEventListener(cmd); |
|||
} |
|||
|
|||
void mqttSubRxCb(JsonObject obj); |
|||
|
|||
void setupLed(void); |
|||
void updateLed(void); |
|||
|
|||
void tickReboot(void) { |
|||
DPRINTLN(DBG_INFO, F("Rebooting...")); |
|||
onWifi(false); |
|||
ah::Scheduler::resetTicker(); |
|||
WiFi.disconnect(); |
|||
delay(200); |
|||
ESP.restart(); |
|||
} |
|||
|
|||
void tickSave(void) { |
|||
if(!mSettings.saveSettings()) |
|||
mSaveReboot = false; |
|||
mSavePending = false; |
|||
|
|||
if(mSaveReboot) |
|||
setRebootFlag(); |
|||
} |
|||
|
|||
void tickNtpUpdate(void); |
|||
void tickCalcSunrise(void); |
|||
void tickIVCommunication(void); |
|||
void tickSun(void); |
|||
void tickComm(void); |
|||
void tickSend(void); |
|||
void tickMinute(void); |
|||
void tickZeroValues(void); |
|||
void tickMidnight(void); |
|||
/*void tickSerial(void) {
|
|||
if(Serial.available() == 0) |
|||
return; |
|||
|
|||
uint8_t buf[80]; |
|||
uint8_t len = Serial.readBytes(buf, 80); |
|||
DPRINTLN(DBG_INFO, "got serial data, len: " + String(len)); |
|||
for(uint8_t i = 0; i < len; i++) { |
|||
if((0 != i) && (i % 8 == 0)) |
|||
DBGPRINTLN(""); |
|||
DBGPRINT(String(buf[i], HEX) + " "); |
|||
} |
|||
DBGPRINTLN(""); |
|||
}*/ |
|||
|
|||
innerLoopCb mInnerLoopCb; |
|||
|
|||
bool mShowRebootRequest; |
|||
bool mIVCommunicationOn; |
|||
|
|||
ahoywifi mWifi; |
|||
WebType mWeb; |
|||
RestApiType mApi; |
|||
PayloadType mPayload; |
|||
MiPayloadType mMiPayload; |
|||
PubSerialType mPubSerial; |
|||
|
|||
char mVersion[12]; |
|||
settings mSettings; |
|||
settings_t *mConfig; |
|||
bool mSavePending; |
|||
bool mSaveReboot; |
|||
|
|||
uint8_t mSendLastIvId; |
|||
bool mSendFirst; |
|||
|
|||
statistics_t mStat; |
|||
|
|||
// mqtt
|
|||
PubMqttType mMqtt; |
|||
bool mMqttReconnect; |
|||
bool mMqttEnabled; |
|||
|
|||
// sun
|
|||
int32_t mCalculatedTimezoneOffset; |
|||
uint32_t mSunrise, mSunset; |
|||
|
|||
// plugins
|
|||
DisplayType mDisplay; |
|||
}; |
|||
|
|||
#endif /*__APP_H__*/ |
@ -0,0 +1,53 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __IAPP_H__ |
|||
#define __IAPP_H__ |
|||
|
|||
#include "defines.h" |
|||
#include "hm/hmSystem.h" |
|||
|
|||
// abstract interface to App. Make members of App accessible from child class
|
|||
// like web or API without forward declaration
|
|||
class IApp { |
|||
public: |
|||
virtual ~IApp() {} |
|||
virtual bool saveSettings(bool stopFs) = 0; |
|||
virtual bool readSettings(const char *path) = 0; |
|||
virtual bool eraseSettings(bool eraseWifi) = 0; |
|||
virtual bool getSavePending() = 0; |
|||
virtual bool getLastSaveSucceed() = 0; |
|||
virtual void setOnUpdate() = 0; |
|||
virtual void setRebootFlag() = 0; |
|||
virtual const char *getVersion() = 0; |
|||
virtual statistics_t *getStatistics() = 0; |
|||
virtual void scanAvailNetworks() = 0; |
|||
virtual void getAvailNetworks(JsonObject obj) = 0; |
|||
|
|||
virtual uint32_t getUptime() = 0; |
|||
virtual uint32_t getTimestamp() = 0; |
|||
virtual uint32_t getSunrise() = 0; |
|||
virtual uint32_t getSunset() = 0; |
|||
virtual void setTimestamp(uint32_t newTime) = 0; |
|||
virtual String getTimeStr(uint32_t offset) = 0; |
|||
virtual uint32_t getTimezoneOffset() = 0; |
|||
virtual void getSchedulerInfo(uint8_t *max) = 0; |
|||
virtual void getSchedulerNames() = 0; |
|||
|
|||
virtual bool getRebootRequestState() = 0; |
|||
virtual bool getSettingsValid() = 0; |
|||
virtual void setMqttDiscoveryFlag() = 0; |
|||
virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0; |
|||
|
|||
virtual void ivSendHighPrio(Inverter<> *iv) = 0; |
|||
|
|||
virtual bool getMqttIsConnected() = 0; |
|||
virtual uint32_t getMqttRxCnt() = 0; |
|||
virtual uint32_t getMqttTxCnt() = 0; |
|||
|
|||
virtual bool getProtection() = 0; |
|||
}; |
|||
|
|||
#endif /*__IAPP_H__*/ |
@ -0,0 +1,615 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __SETTINGS_H__ |
|||
#define __SETTINGS_H__ |
|||
|
|||
#include <Arduino.h> |
|||
#include <ArduinoJson.h> |
|||
#include <LittleFS.h> |
|||
|
|||
#include "../defines.h" |
|||
#include "../utils/dbg.h" |
|||
#include "../utils/helper.h" |
|||
|
|||
#if defined(ESP32) |
|||
#define MAX_ALLOWED_BUF_SIZE ESP.getMaxAllocHeap() - 1024 |
|||
#else |
|||
#define MAX_ALLOWED_BUF_SIZE ESP.getMaxFreeBlockSize() - 1024 |
|||
#endif |
|||
|
|||
/**
|
|||
* More info: |
|||
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
|
|||
* */ |
|||
#define DEF_PIN_OFF 255 |
|||
|
|||
|
|||
#define PROT_MASK_INDEX 0x0001 |
|||
#define PROT_MASK_LIVE 0x0002 |
|||
#define PROT_MASK_SERIAL 0x0004 |
|||
#define PROT_MASK_SETUP 0x0008 |
|||
#define PROT_MASK_UPDATE 0x0010 |
|||
#define PROT_MASK_SYSTEM 0x0020 |
|||
#define PROT_MASK_API 0x0040 |
|||
#define PROT_MASK_MQTT 0x0080 |
|||
|
|||
#define DEF_PROT_INDEX 0x0001 |
|||
#define DEF_PROT_LIVE 0x0000 |
|||
#define DEF_PROT_SERIAL 0x0004 |
|||
#define DEF_PROT_SETUP 0x0008 |
|||
#define DEF_PROT_UPDATE 0x0010 |
|||
#define DEF_PROT_SYSTEM 0x0020 |
|||
#define DEF_PROT_API 0x0000 |
|||
#define DEF_PROT_MQTT 0x0000 |
|||
|
|||
|
|||
typedef struct { |
|||
uint8_t ip[4]; // ip address
|
|||
uint8_t mask[4]; // sub mask
|
|||
uint8_t dns1[4]; // dns 1
|
|||
uint8_t dns2[4]; // dns 2
|
|||
uint8_t gateway[4]; // standard gateway
|
|||
} cfgIp_t; |
|||
|
|||
typedef struct { |
|||
char deviceName[DEVNAME_LEN]; |
|||
char adminPwd[PWD_LEN]; |
|||
uint16_t protectionMask; |
|||
bool darkMode; |
|||
|
|||
// wifi
|
|||
char stationSsid[SSID_LEN]; |
|||
char stationPwd[PWD_LEN]; |
|||
|
|||
cfgIp_t ip; |
|||
} cfgSys_t; |
|||
|
|||
typedef struct { |
|||
uint16_t sendInterval; |
|||
uint8_t maxRetransPerPyld; |
|||
uint8_t pinCs; |
|||
uint8_t pinCe; |
|||
uint8_t pinIrq; |
|||
uint8_t pinMiso; |
|||
uint8_t pinMosi; |
|||
uint8_t pinSclk; |
|||
uint8_t amplifierPower; |
|||
} cfgNrf24_t; |
|||
|
|||
typedef struct { |
|||
char addr[NTP_ADDR_LEN]; |
|||
uint16_t port; |
|||
} cfgNtp_t; |
|||
|
|||
typedef struct { |
|||
float lat; |
|||
float lon; |
|||
bool disNightCom; // disable night communication
|
|||
uint16_t offsetSec; |
|||
} cfgSun_t; |
|||
|
|||
typedef struct { |
|||
uint16_t interval; |
|||
bool showIv; |
|||
bool debug; |
|||
} cfgSerial_t; |
|||
|
|||
typedef struct { |
|||
uint8_t led0; // first LED pin
|
|||
uint8_t led1; // second LED pin
|
|||
} cfgLed_t; |
|||
|
|||
typedef struct { |
|||
char broker[MQTT_ADDR_LEN]; |
|||
uint16_t port; |
|||
char user[MQTT_USER_LEN]; |
|||
char pwd[MQTT_PWD_LEN]; |
|||
char topic[MQTT_TOPIC_LEN]; |
|||
uint16_t interval; |
|||
} cfgMqtt_t; |
|||
|
|||
typedef struct { |
|||
bool enabled; |
|||
char name[MAX_NAME_LENGTH]; |
|||
serial_u serial; |
|||
uint16_t chMaxPwr[4]; |
|||
int32_t yieldCor[4]; // signed YieldTotal correction value
|
|||
char chName[4][MAX_NAME_LENGTH]; |
|||
} cfgIv_t; |
|||
|
|||
typedef struct { |
|||
bool enabled; |
|||
cfgIv_t iv[MAX_NUM_INVERTERS]; |
|||
|
|||
bool rstYieldMidNight; |
|||
bool rstValsNotAvail; |
|||
bool rstValsCommStop; |
|||
} cfgInst_t; |
|||
|
|||
typedef struct { |
|||
uint8_t type; |
|||
bool pwrSaveAtIvOffline; |
|||
bool pxShift; |
|||
uint8_t rot; |
|||
//uint16_t wakeUp;
|
|||
//uint16_t sleepAt;
|
|||
uint8_t contrast; |
|||
uint8_t disp_data; |
|||
uint8_t disp_clk; |
|||
uint8_t disp_cs; |
|||
uint8_t disp_reset; |
|||
uint8_t disp_busy; |
|||
uint8_t disp_dc; |
|||
} display_t; |
|||
|
|||
typedef struct { |
|||
display_t display; |
|||
} plugins_t; |
|||
|
|||
typedef struct { |
|||
cfgSys_t sys; |
|||
cfgNrf24_t nrf; |
|||
cfgNtp_t ntp; |
|||
cfgSun_t sun; |
|||
cfgSerial_t serial; |
|||
cfgMqtt_t mqtt; |
|||
cfgLed_t led; |
|||
cfgInst_t inst; |
|||
plugins_t plugin; |
|||
bool valid; |
|||
} settings_t; |
|||
|
|||
class settings { |
|||
public: |
|||
settings() { |
|||
mLastSaveSucceed = false; |
|||
} |
|||
|
|||
void setup() { |
|||
DPRINTLN(DBG_INFO, F("Initializing FS ..")); |
|||
|
|||
mCfg.valid = false; |
|||
#if !defined(ESP32) |
|||
LittleFSConfig cfg; |
|||
cfg.setAutoFormat(false); |
|||
LittleFS.setConfig(cfg); |
|||
#define LITTLFS_TRUE |
|||
#define LITTLFS_FALSE |
|||
#else |
|||
#define LITTLFS_TRUE true |
|||
#define LITTLFS_FALSE false |
|||
#endif |
|||
|
|||
if(!LittleFS.begin(LITTLFS_FALSE)) { |
|||
DPRINTLN(DBG_INFO, F(".. format ..")); |
|||
LittleFS.format(); |
|||
if(LittleFS.begin(LITTLFS_TRUE)) { |
|||
DPRINTLN(DBG_INFO, F(".. success")); |
|||
} else { |
|||
DPRINTLN(DBG_INFO, F(".. failed")); |
|||
} |
|||
|
|||
} |
|||
else |
|||
DPRINTLN(DBG_INFO, F(" .. done")); |
|||
|
|||
readSettings("/settings.json"); |
|||
} |
|||
|
|||
// should be used before OTA
|
|||
void stop() { |
|||
LittleFS.end(); |
|||
DPRINTLN(DBG_INFO, F("FS stopped")); |
|||
} |
|||
|
|||
void getPtr(settings_t *&cfg) { |
|||
cfg = &mCfg; |
|||
} |
|||
|
|||
bool getValid(void) { |
|||
return mCfg.valid; |
|||
} |
|||
|
|||
inline bool getLastSaveSucceed() { |
|||
return mLastSaveSucceed; |
|||
} |
|||
|
|||
void getInfo(uint32_t *used, uint32_t *size) { |
|||
#if !defined(ESP32) |
|||
FSInfo info; |
|||
LittleFS.info(info); |
|||
*used = info.usedBytes; |
|||
*size = info.totalBytes; |
|||
|
|||
DPRINTLN(DBG_INFO, F("-- FILESYSTEM INFO --")); |
|||
DPRINTLN(DBG_INFO, String(info.usedBytes) + F(" of ") + String(info.totalBytes) + F(" used")); |
|||
#else |
|||
DPRINTLN(DBG_WARN, F("not supported by ESP32")); |
|||
#endif |
|||
} |
|||
|
|||
bool readSettings(const char* path) { |
|||
loadDefaults(); |
|||
File fp = LittleFS.open(path, "r"); |
|||
if(!fp) |
|||
DPRINTLN(DBG_WARN, F("failed to load json, using default config")); |
|||
else { |
|||
//DPRINTLN(DBG_INFO, fp.readString());
|
|||
//fp.seek(0, SeekSet);
|
|||
DynamicJsonDocument root(MAX_ALLOWED_BUF_SIZE); |
|||
DeserializationError err = deserializeJson(root, fp); |
|||
root.shrinkToFit(); |
|||
if(!err && (root.size() > 0)) { |
|||
mCfg.valid = true; |
|||
jsonWifi(root[F("wifi")]); |
|||
jsonNrf(root[F("nrf")]); |
|||
jsonNtp(root[F("ntp")]); |
|||
jsonSun(root[F("sun")]); |
|||
jsonSerial(root[F("serial")]); |
|||
jsonMqtt(root[F("mqtt")]); |
|||
jsonLed(root[F("led")]); |
|||
jsonPlugin(root[F("plugin")]); |
|||
jsonInst(root[F("inst")]); |
|||
} |
|||
else { |
|||
Serial.println(F("failed to parse json, using default config")); |
|||
} |
|||
|
|||
fp.close(); |
|||
} |
|||
return mCfg.valid; |
|||
} |
|||
|
|||
bool saveSettings() { |
|||
DPRINTLN(DBG_DEBUG, F("save settings")); |
|||
|
|||
DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); |
|||
JsonObject root = json.to<JsonObject>(); |
|||
jsonWifi(root.createNestedObject(F("wifi")), true); |
|||
jsonNrf(root.createNestedObject(F("nrf")), true); |
|||
jsonNtp(root.createNestedObject(F("ntp")), true); |
|||
jsonSun(root.createNestedObject(F("sun")), true); |
|||
jsonSerial(root.createNestedObject(F("serial")), true); |
|||
jsonMqtt(root.createNestedObject(F("mqtt")), true); |
|||
jsonLed(root.createNestedObject(F("led")), true); |
|||
jsonPlugin(root.createNestedObject(F("plugin")), true); |
|||
jsonInst(root.createNestedObject(F("inst")), true); |
|||
|
|||
DPRINT(DBG_INFO, F("memory usage: ")); |
|||
DBGPRINTLN(String(json.memoryUsage())); |
|||
DPRINT(DBG_INFO, F("capacity: ")); |
|||
DBGPRINTLN(String(json.capacity())); |
|||
DPRINT(DBG_INFO, F("max alloc: ")); |
|||
DBGPRINTLN(String(MAX_ALLOWED_BUF_SIZE)); |
|||
|
|||
if(json.overflowed()) { |
|||
DPRINTLN(DBG_ERROR, F("buffer too small!")); |
|||
mLastSaveSucceed = false; |
|||
return false; |
|||
} |
|||
|
|||
File fp = LittleFS.open("/settings.json", "w"); |
|||
if(!fp) { |
|||
DPRINTLN(DBG_ERROR, F("can't open settings file!")); |
|||
mLastSaveSucceed = false; |
|||
return false; |
|||
} |
|||
|
|||
if(0 == serializeJson(root, fp)) { |
|||
DPRINTLN(DBG_ERROR, F("can't write settings file!")); |
|||
mLastSaveSucceed = false; |
|||
return false; |
|||
} |
|||
fp.close(); |
|||
|
|||
DPRINTLN(DBG_INFO, F("settings saved")); |
|||
mLastSaveSucceed = true; |
|||
return true; |
|||
} |
|||
|
|||
bool eraseSettings(bool eraseWifi = false) { |
|||
if(true == eraseWifi) |
|||
return LittleFS.format(); |
|||
loadDefaults(!eraseWifi); |
|||
return saveSettings(); |
|||
} |
|||
|
|||
private: |
|||
void loadDefaults(bool keepWifi = false) { |
|||
DPRINTLN(DBG_VERBOSE, F("loadDefaults")); |
|||
|
|||
cfgSys_t tmp; |
|||
if(keepWifi) { |
|||
// copy contents which should not be deleted
|
|||
memset(&tmp.adminPwd, 0, PWD_LEN); |
|||
memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t)); |
|||
} |
|||
// erase all settings and reset to default
|
|||
memset(&mCfg, 0, sizeof(settings_t)); |
|||
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP |
|||
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; |
|||
mCfg.sys.darkMode = false; |
|||
// restore temp settings
|
|||
if(keepWifi) |
|||
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); |
|||
else { |
|||
snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); |
|||
snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); |
|||
} |
|||
|
|||
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); |
|||
|
|||
mCfg.nrf.sendInterval = SEND_INTERVAL; |
|||
mCfg.nrf.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; |
|||
mCfg.nrf.pinCs = DEF_CS_PIN; |
|||
mCfg.nrf.pinCe = DEF_CE_PIN; |
|||
mCfg.nrf.pinIrq = DEF_IRQ_PIN; |
|||
mCfg.nrf.pinMiso = DEF_MISO_PIN; |
|||
mCfg.nrf.pinMosi = DEF_MOSI_PIN; |
|||
mCfg.nrf.pinSclk = DEF_SCLK_PIN; |
|||
|
|||
mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; |
|||
|
|||
snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); |
|||
mCfg.ntp.port = DEF_NTP_PORT; |
|||
|
|||
mCfg.sun.lat = 0.0; |
|||
mCfg.sun.lon = 0.0; |
|||
mCfg.sun.disNightCom = false; |
|||
mCfg.sun.offsetSec = 0; |
|||
|
|||
mCfg.serial.interval = SERIAL_INTERVAL; |
|||
mCfg.serial.showIv = false; |
|||
mCfg.serial.debug = false; |
|||
|
|||
mCfg.mqtt.port = DEF_MQTT_PORT; |
|||
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); |
|||
snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER); |
|||
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); |
|||
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); |
|||
mCfg.mqtt.interval = 0; // off
|
|||
|
|||
mCfg.inst.rstYieldMidNight = false; |
|||
mCfg.inst.rstValsNotAvail = false; |
|||
mCfg.inst.rstValsCommStop = false; |
|||
|
|||
mCfg.led.led0 = DEF_PIN_OFF; |
|||
mCfg.led.led1 = DEF_PIN_OFF; |
|||
|
|||
memset(&mCfg.inst, 0, sizeof(cfgInst_t)); |
|||
|
|||
mCfg.plugin.display.pwrSaveAtIvOffline = false; |
|||
mCfg.plugin.display.contrast = 60; |
|||
mCfg.plugin.display.pxShift = true; |
|||
mCfg.plugin.display.rot = 0; |
|||
mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA
|
|||
mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL
|
|||
mCfg.plugin.display.disp_cs = DEF_PIN_OFF; |
|||
mCfg.plugin.display.disp_reset = DEF_PIN_OFF; |
|||
mCfg.plugin.display.disp_busy = DEF_PIN_OFF; |
|||
mCfg.plugin.display.disp_dc = DEF_PIN_OFF; |
|||
} |
|||
|
|||
void jsonWifi(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
char buf[16]; |
|||
obj[F("ssid")] = mCfg.sys.stationSsid; |
|||
obj[F("pwd")] = mCfg.sys.stationPwd; |
|||
obj[F("dev")] = mCfg.sys.deviceName; |
|||
obj[F("adm")] = mCfg.sys.adminPwd; |
|||
obj[F("prot_mask")] = mCfg.sys.protectionMask; |
|||
obj[F("dark")] = mCfg.sys.darkMode; |
|||
ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf); |
|||
ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf); |
|||
ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); |
|||
ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf); |
|||
ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf); |
|||
} else { |
|||
snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as<const char*>()); |
|||
snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as<const char*>()); |
|||
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>()); |
|||
snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>()); |
|||
mCfg.sys.protectionMask = obj[F("prot_mask")]; |
|||
mCfg.sys.darkMode = obj[F("dark")]; |
|||
ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>()); |
|||
ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>()); |
|||
ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>()); |
|||
ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as<const char*>()); |
|||
ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as<const char*>()); |
|||
|
|||
if(mCfg.sys.protectionMask == 0) |
|||
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP |
|||
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; |
|||
} |
|||
} |
|||
|
|||
void jsonNrf(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
obj[F("intvl")] = mCfg.nrf.sendInterval; |
|||
obj[F("maxRetry")] = mCfg.nrf.maxRetransPerPyld; |
|||
obj[F("cs")] = mCfg.nrf.pinCs; |
|||
obj[F("ce")] = mCfg.nrf.pinCe; |
|||
obj[F("irq")] = mCfg.nrf.pinIrq; |
|||
obj[F("sclk")] = mCfg.nrf.pinSclk; |
|||
obj[F("mosi")] = mCfg.nrf.pinMosi; |
|||
obj[F("miso")] = mCfg.nrf.pinMiso; |
|||
obj[F("pwr")] = mCfg.nrf.amplifierPower; |
|||
} else { |
|||
mCfg.nrf.sendInterval = obj[F("intvl")]; |
|||
mCfg.nrf.maxRetransPerPyld = obj[F("maxRetry")]; |
|||
mCfg.nrf.pinCs = obj[F("cs")]; |
|||
mCfg.nrf.pinCe = obj[F("ce")]; |
|||
mCfg.nrf.pinIrq = obj[F("irq")]; |
|||
mCfg.nrf.pinSclk = obj[F("sclk")]; |
|||
mCfg.nrf.pinMosi = obj[F("mosi")]; |
|||
mCfg.nrf.pinMiso = obj[F("miso")]; |
|||
mCfg.nrf.amplifierPower = obj[F("pwr")]; |
|||
if((obj[F("cs")] == obj[F("ce")])) { |
|||
mCfg.nrf.pinCs = DEF_CS_PIN; |
|||
mCfg.nrf.pinCe = DEF_CE_PIN; |
|||
mCfg.nrf.pinIrq = DEF_IRQ_PIN; |
|||
mCfg.nrf.pinSclk = DEF_SCLK_PIN; |
|||
mCfg.nrf.pinMosi = DEF_MOSI_PIN; |
|||
mCfg.nrf.pinMiso = DEF_MISO_PIN; |
|||
} |
|||
} |
|||
} |
|||
|
|||
void jsonNtp(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
obj[F("addr")] = mCfg.ntp.addr; |
|||
obj[F("port")] = mCfg.ntp.port; |
|||
} else { |
|||
snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", obj[F("addr")].as<const char*>()); |
|||
mCfg.ntp.port = obj[F("port")]; |
|||
} |
|||
} |
|||
|
|||
void jsonSun(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
obj[F("lat")] = mCfg.sun.lat; |
|||
obj[F("lon")] = mCfg.sun.lon; |
|||
obj[F("dis")] = mCfg.sun.disNightCom; |
|||
obj[F("offs")] = mCfg.sun.offsetSec; |
|||
} else { |
|||
mCfg.sun.lat = obj[F("lat")]; |
|||
mCfg.sun.lon = obj[F("lon")]; |
|||
mCfg.sun.disNightCom = obj[F("dis")]; |
|||
mCfg.sun.offsetSec = obj[F("offs")]; |
|||
} |
|||
} |
|||
|
|||
void jsonSerial(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
obj[F("intvl")] = mCfg.serial.interval; |
|||
obj[F("show")] = mCfg.serial.showIv; |
|||
obj[F("debug")] = mCfg.serial.debug; |
|||
} else { |
|||
mCfg.serial.interval = obj[F("intvl")]; |
|||
mCfg.serial.showIv = obj[F("show")]; |
|||
mCfg.serial.debug = obj[F("debug")]; |
|||
} |
|||
} |
|||
|
|||
void jsonMqtt(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
obj[F("broker")] = mCfg.mqtt.broker; |
|||
obj[F("port")] = mCfg.mqtt.port; |
|||
obj[F("user")] = mCfg.mqtt.user; |
|||
obj[F("pwd")] = mCfg.mqtt.pwd; |
|||
obj[F("topic")] = mCfg.mqtt.topic; |
|||
obj[F("intvl")] = mCfg.mqtt.interval; |
|||
|
|||
} else { |
|||
mCfg.mqtt.port = obj[F("port")]; |
|||
mCfg.mqtt.interval = obj[F("intvl")]; |
|||
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as<const char*>()); |
|||
snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as<const char*>()); |
|||
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as<const char*>()); |
|||
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", obj[F("topic")].as<const char*>()); |
|||
} |
|||
} |
|||
|
|||
void jsonLed(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
obj[F("0")] = mCfg.led.led0; |
|||
obj[F("1")] = mCfg.led.led1; |
|||
} else { |
|||
mCfg.led.led0 = obj[F("0")]; |
|||
mCfg.led.led1 = obj[F("1")]; |
|||
} |
|||
} |
|||
|
|||
void jsonPlugin(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
JsonObject disp = obj.createNestedObject("disp"); |
|||
disp[F("type")] = mCfg.plugin.display.type; |
|||
disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; |
|||
disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; |
|||
disp[F("rotation")] = mCfg.plugin.display.rot; |
|||
//disp[F("wake")] = mCfg.plugin.display.wakeUp;
|
|||
//disp[F("sleep")] = mCfg.plugin.display.sleepAt;
|
|||
disp[F("contrast")] = mCfg.plugin.display.contrast; |
|||
disp[F("data")] = mCfg.plugin.display.disp_data; |
|||
disp[F("clock")] = mCfg.plugin.display.disp_clk; |
|||
disp[F("cs")] = mCfg.plugin.display.disp_cs; |
|||
disp[F("reset")] = mCfg.plugin.display.disp_reset; |
|||
disp[F("busy")] = mCfg.plugin.display.disp_busy; |
|||
disp[F("dc")] = mCfg.plugin.display.disp_dc; |
|||
} else { |
|||
JsonObject disp = obj["disp"]; |
|||
mCfg.plugin.display.type = disp[F("type")]; |
|||
mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")]; |
|||
mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")]; |
|||
mCfg.plugin.display.rot = disp[F("rotation")]; |
|||
//mCfg.plugin.display.wakeUp = disp[F("wake")];
|
|||
//mCfg.plugin.display.sleepAt = disp[F("sleep")];
|
|||
mCfg.plugin.display.contrast = disp[F("contrast")]; |
|||
mCfg.plugin.display.disp_data = disp[F("data")]; |
|||
mCfg.plugin.display.disp_clk = disp[F("clock")]; |
|||
mCfg.plugin.display.disp_cs = disp[F("cs")]; |
|||
mCfg.plugin.display.disp_reset = disp[F("reset")]; |
|||
mCfg.plugin.display.disp_busy = disp[F("busy")]; |
|||
mCfg.plugin.display.disp_dc = disp[F("dc")]; |
|||
} |
|||
} |
|||
|
|||
void jsonInst(JsonObject obj, bool set = false) { |
|||
if(set) { |
|||
obj[F("en")] = (bool)mCfg.inst.enabled; |
|||
obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; |
|||
obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; |
|||
obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; |
|||
} |
|||
else { |
|||
mCfg.inst.enabled = (bool)obj[F("en")]; |
|||
mCfg.inst.rstYieldMidNight = (bool)obj["rstMidNight"]; |
|||
mCfg.inst.rstValsNotAvail = (bool)obj["rstNotAvail"]; |
|||
mCfg.inst.rstValsCommStop = (bool)obj["rstComStop"]; |
|||
} |
|||
|
|||
JsonArray ivArr; |
|||
if(set) |
|||
ivArr = obj.createNestedArray(F("iv")); |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
if(set) { |
|||
if(mCfg.inst.iv[i].serial.u64 != 0ULL) |
|||
jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); |
|||
} |
|||
else { |
|||
if(!obj[F("iv")][i].isNull()) |
|||
jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) { |
|||
if(set) { |
|||
obj[F("en")] = (bool)cfg->enabled; |
|||
obj[F("name")] = cfg->name; |
|||
obj[F("sn")] = cfg->serial.u64; |
|||
for(uint8_t i = 0; i < 4; i++) { |
|||
obj[F("yield")][i] = cfg->yieldCor[i]; |
|||
obj[F("pwr")][i] = cfg->chMaxPwr[i]; |
|||
obj[F("chName")][i] = cfg->chName[i]; |
|||
} |
|||
} else { |
|||
cfg->enabled = (bool)obj[F("en")]; |
|||
snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as<const char*>()); |
|||
cfg->serial.u64 = obj[F("sn")]; |
|||
for(uint8_t i = 0; i < 4; i++) { |
|||
cfg->yieldCor[i] = obj[F("yield")][i]; |
|||
cfg->chMaxPwr[i] = obj[F("pwr")][i]; |
|||
snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as<const char*>()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
settings_t mCfg; |
|||
bool mLastSaveSucceed; |
|||
}; |
|||
|
|||
#endif /*__SETTINGS_H__*/ |
@ -0,0 +1,105 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __DEFINES_H__ |
|||
#define __DEFINES_H__ |
|||
|
|||
#include "config/config.h" |
|||
|
|||
//-------------------------------------
|
|||
// VERSION
|
|||
//-------------------------------------
|
|||
#define VERSION_MAJOR 0 |
|||
#define VERSION_MINOR 6 |
|||
#define VERSION_PATCH 0 |
|||
|
|||
//-------------------------------------
|
|||
typedef struct { |
|||
uint8_t ch; |
|||
uint8_t len; |
|||
uint8_t packet[MAX_RF_PAYLOAD_SIZE]; |
|||
} packet_t; |
|||
|
|||
typedef enum { |
|||
InverterDevInform_Simple = 0, // 0x00
|
|||
InverterDevInform_All = 1, // 0x01
|
|||
GridOnProFilePara = 2, // 0x02
|
|||
HardWareConfig = 3, // 0x03
|
|||
SimpleCalibrationPara = 4, // 0x04
|
|||
SystemConfigPara = 5, // 0x05
|
|||
RealTimeRunData_Debug = 11, // 0x0b
|
|||
RealTimeRunData_Reality = 12, // 0x0c
|
|||
RealTimeRunData_A_Phase = 13, // 0x0d
|
|||
RealTimeRunData_B_Phase = 14, // 0x0e
|
|||
RealTimeRunData_C_Phase = 15, // 0x0f
|
|||
AlarmData = 17, // 0x11, Alarm data - all unsent alarms
|
|||
AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms
|
|||
RecordData = 19, // 0x13
|
|||
InternalData = 20, // 0x14
|
|||
GetLossRate = 21, // 0x15
|
|||
GetSelfCheckState = 30, // 0x1e
|
|||
InitDataState = 0xff |
|||
} InfoCmdType; |
|||
|
|||
typedef enum { |
|||
TurnOn = 0, // 0x00
|
|||
TurnOff = 1, // 0x01
|
|||
Restart = 2, // 0x02
|
|||
Lock = 3, // 0x03
|
|||
Unlock = 4, // 0x04
|
|||
ActivePowerContr = 11, // 0x0b
|
|||
ReactivePowerContr = 12, // 0x0c
|
|||
PFSet = 13, // 0x0d
|
|||
CleanState_LockAndAlarm = 20, // 0x14
|
|||
SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files
|
|||
Init = 0xff |
|||
} DevControlCmdType; |
|||
|
|||
typedef enum { |
|||
AbsolutNonPersistent = 0UL, // 0x0000
|
|||
RelativNonPersistent = 1UL, // 0x0001
|
|||
AbsolutPersistent = 256UL, // 0x0100
|
|||
RelativPersistent = 257UL // 0x0101
|
|||
} PowerLimitControlType; |
|||
|
|||
union serial_u { |
|||
uint64_t u64; |
|||
uint8_t b[8]; |
|||
}; |
|||
|
|||
#define MIN_SERIAL_INTERVAL 2 // 5
|
|||
#define MIN_SEND_INTERVAL 15 |
|||
#define MIN_MQTT_INTERVAL 60 |
|||
|
|||
|
|||
#define MQTT_STATUS_NOT_AVAIL_NOT_PROD 0 |
|||
#define MQTT_STATUS_AVAIL_NOT_PROD 1 |
|||
#define MQTT_STATUS_AVAIL_PROD 2 |
|||
|
|||
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; |
|||
|
|||
//-------------------------------------
|
|||
// EEPROM
|
|||
//-------------------------------------
|
|||
#define SSID_LEN 32 |
|||
#define PWD_LEN 64 |
|||
#define DEVNAME_LEN 16 |
|||
#define NTP_ADDR_LEN 32 // DNS Name
|
|||
|
|||
#define MQTT_ADDR_LEN 64 // DNS Name
|
|||
#define MQTT_USER_LEN 65 // there is another byte necessary for \0
|
|||
#define MQTT_PWD_LEN 65 |
|||
#define MQTT_TOPIC_LEN 65 |
|||
|
|||
#define MQTT_MAX_PACKET_SIZE 384 |
|||
|
|||
typedef struct { |
|||
uint32_t rxFail; |
|||
uint32_t rxFailNoAnser; |
|||
uint32_t rxSuccess; |
|||
uint32_t frmCnt; |
|||
} statistics_t; |
|||
|
|||
#endif /*__DEFINES_H__*/ |
@ -0,0 +1,419 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __HM_PAYLOAD_H__ |
|||
#define __HM_PAYLOAD_H__ |
|||
|
|||
#include "../utils/dbg.h" |
|||
#include "../utils/crc.h" |
|||
#include "../config/config.h" |
|||
#include <Arduino.h> |
|||
|
|||
typedef struct { |
|||
uint8_t txCmd; |
|||
uint8_t txId; |
|||
uint8_t invId; |
|||
uint32_t ts; |
|||
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; |
|||
uint8_t len[MAX_PAYLOAD_ENTRIES]; |
|||
bool complete; |
|||
uint8_t maxPackId; |
|||
bool lastFound; |
|||
uint8_t retransmits; |
|||
bool requested; |
|||
bool gotFragment; |
|||
} invPayload_t; |
|||
|
|||
|
|||
typedef std::function<void(uint8_t)> payloadListenerType; |
|||
typedef std::function<void(uint16_t alarmCode, uint32_t start, uint32_t end)> alarmListenerType; |
|||
|
|||
|
|||
template<class HMSYSTEM> |
|||
class HmPayload { |
|||
public: |
|||
HmPayload() {} |
|||
|
|||
void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { |
|||
mApp = app; |
|||
mSys = sys; |
|||
mStat = stat; |
|||
mMaxRetrans = maxRetransmits; |
|||
mTimestamp = timestamp; |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
reset(i); |
|||
} |
|||
mSerialDebug = false; |
|||
mHighPrioIv = NULL; |
|||
mCbAlarm = NULL; |
|||
mCbPayload = NULL; |
|||
} |
|||
|
|||
void enableSerialDebug(bool enable) { |
|||
mSerialDebug = enable; |
|||
} |
|||
|
|||
void addPayloadListener(payloadListenerType cb) { |
|||
mCbPayload = cb; |
|||
} |
|||
|
|||
void addAlarmListener(alarmListenerType cb) { |
|||
mCbAlarm = cb; |
|||
} |
|||
|
|||
void loop() { |
|||
if(NULL != mHighPrioIv) { |
|||
ivSend(mHighPrioIv, true); |
|||
mHighPrioIv = NULL; |
|||
} |
|||
} |
|||
|
|||
void zeroYieldDay(Inverter<> *iv) { |
|||
DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
uint8_t pos; |
|||
for(uint8_t ch = 0; ch < iv->channels; ch++) { |
|||
pos = iv->getPosByChFld(CH0, FLD_YD, rec); |
|||
iv->setValue(pos, rec, 0.0f); |
|||
} |
|||
} |
|||
|
|||
void zeroInverterValues(Inverter<> *iv) { |
|||
DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
for(uint8_t ch = 0; ch <= iv->channels; ch++) { |
|||
uint8_t pos = 0; |
|||
for(uint8_t fld = 0; fld < FLD_EVT; fld++) { |
|||
switch(fld) { |
|||
case FLD_YD: |
|||
case FLD_YT: |
|||
continue; |
|||
} |
|||
pos = iv->getPosByChFld(ch, fld, rec); |
|||
iv->setValue(pos, rec, 0.0f); |
|||
} |
|||
} |
|||
|
|||
notify(RealTimeRunData_Debug); |
|||
} |
|||
|
|||
void ivSendHighPrio(Inverter<> *iv) { |
|||
mHighPrioIv = iv; |
|||
} |
|||
|
|||
void ivSend(Inverter<> *iv, bool highPrio = false) { |
|||
if(!highPrio) { |
|||
if (mPayload[iv->id].requested) { |
|||
if (!mPayload[iv->id].complete) |
|||
process(false); // no retransmit
|
|||
|
|||
if (!mPayload[iv->id].complete) { |
|||
if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) |
|||
mStat->rxFailNoAnser++; // got nothing
|
|||
else |
|||
mStat->rxFail++; // got fragments but not complete response
|
|||
|
|||
iv->setQueuedCmdFinished(); // command failed
|
|||
if (mSerialDebug) { |
|||
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("no Payload received! (retransmits: ")); |
|||
DBGPRINT(String(mPayload[iv->id].retransmits)); |
|||
DBGPRINTLN(F(")")); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
reset(iv->id); |
|||
mPayload[iv->id].requested = true; |
|||
|
|||
yield(); |
|||
if (mSerialDebug) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("Requesting Inv SN ")); |
|||
DBGPRINTLN(String(iv->config->serial.u64, HEX)); |
|||
} |
|||
|
|||
if (iv->getDevControlRequest()) { |
|||
if (mSerialDebug) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("Devcontrol request 0x")); |
|||
DBGPRINT(String(iv->devControlCmd, HEX)); |
|||
DBGPRINT(F(" power limit ")); |
|||
DBGPRINTLN(String(iv->powerLimit[0])); |
|||
} |
|||
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); |
|||
mPayload[iv->id].txCmd = iv->devControlCmd; |
|||
//iv->clearCmdQueue();
|
|||
//iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
|
|||
} else { |
|||
uint8_t cmd = iv->getQueuedCmd(); |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("prepareDevInformCmd 0x")); |
|||
DBGHEXLN(cmd); |
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); |
|||
mPayload[iv->id].txCmd = cmd; |
|||
} |
|||
} |
|||
|
|||
void add(Inverter<> *iv, packet_t *p) { |
|||
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
|
|||
mPayload[iv->id].txId = p->packet[0]; |
|||
DPRINTLN(DBG_DEBUG, F("Response from info request received")); |
|||
uint8_t *pid = &p->packet[9]; |
|||
if (*pid == 0x00) { |
|||
DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored")); |
|||
} else { |
|||
DPRINT(DBG_DEBUG, F("PID: 0x")); |
|||
DPRINTLN(DBG_DEBUG, String(*pid, HEX)); |
|||
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { |
|||
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); |
|||
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; |
|||
mPayload[iv->id].gotFragment = true; |
|||
} |
|||
|
|||
if ((*pid & ALL_FRAMES) == ALL_FRAMES) { |
|||
// Last packet
|
|||
if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { |
|||
mPayload[iv->id].maxPackId = (*pid & 0x7f); |
|||
if (*pid > 0x81) |
|||
mPayload[iv->id].lastFound = true; |
|||
} |
|||
} |
|||
} |
|||
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
|
|||
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); |
|||
|
|||
mPayload[iv->id].txId = p->packet[0]; |
|||
iv->clearDevControlRequest(); |
|||
|
|||
if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { |
|||
bool ok = true; |
|||
if((p->packet[10] == 0x00) && (p->packet[11] == 0x00)) |
|||
mApp->setMqttPowerLimitAck(iv); |
|||
else |
|||
ok = false; |
|||
|
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("has ")); |
|||
if(!ok) DBGPRINT(F("not ")); |
|||
DBGPRINT(F("accepted power limit set point ")); |
|||
DBGPRINT(String(iv->powerLimit[0])); |
|||
DBGPRINT(F(" with PowerLimitControl ")); |
|||
DBGPRINTLN(String(iv->powerLimit[1])); |
|||
|
|||
iv->clearCmdQueue(); |
|||
iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
|
|||
} |
|||
iv->devControlCmd = Init; |
|||
} |
|||
} |
|||
|
|||
void process(bool retransmit) { |
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
|
|||
if (IV_MI == iv->ivGen) // only process HM inverters
|
|||
continue; // skip to next inverter
|
|||
|
|||
if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { |
|||
// no processing needed if txId is not 0x95
|
|||
mPayload[iv->id].complete = true; |
|||
continue; // skip to next inverter
|
|||
} |
|||
|
|||
if (!mPayload[iv->id].complete) { |
|||
bool crcPass, pyldComplete; |
|||
crcPass = build(iv->id, &pyldComplete); |
|||
if (!crcPass && !pyldComplete) { // payload not complete
|
|||
if ((mPayload[iv->id].requested) && (retransmit)) { |
|||
if (mPayload[iv->id].retransmits < mMaxRetrans) { |
|||
mPayload[iv->id].retransmits++; |
|||
if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { |
|||
// This is required to prevent retransmissions without answer.
|
|||
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); |
|||
mPayload[iv->id].retransmits = mMaxRetrans; |
|||
} else if(iv->devControlCmd == ActivePowerContr) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DPRINTLN(DBG_INFO, F("retransmit power limit")); |
|||
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); |
|||
} else { |
|||
if(false == mPayload[iv->id].gotFragment) { |
|||
/*
|
|||
DPRINTLN(DBG_WARN, F("nothing received: Request Complete Retransmit")); |
|||
mPayload[iv->id].txCmd = iv->getQueuedCmd(); |
|||
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); |
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); |
|||
*/ |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINTLN(F("nothing received")); |
|||
mPayload[iv->id].retransmits = mMaxRetrans; |
|||
} else { |
|||
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { |
|||
if (mPayload[iv->id].len[i] == 0) { |
|||
DPRINT_IVID(DBG_WARN, iv->id); |
|||
DBGPRINT(F("Frame ")); |
|||
DBGPRINT(String(i + 1)); |
|||
DBGPRINTLN(F(" missing: Request Retransmit")); |
|||
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); |
|||
break; // only request retransmit one frame per loop
|
|||
} |
|||
yield(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} else if(!crcPass && pyldComplete) { // crc error on complete Payload
|
|||
if (mPayload[iv->id].retransmits < mMaxRetrans) { |
|||
mPayload[iv->id].retransmits++; |
|||
DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); |
|||
mPayload[iv->id].txCmd = iv->getQueuedCmd(); |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("prepareDevInformCmd 0x")); |
|||
DBGHEXLN(mPayload[iv->id].txCmd); |
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); |
|||
} |
|||
} else { // payload complete
|
|||
DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); |
|||
DBGHEXLN(mPayload[iv->id].txCmd); |
|||
DPRINT(DBG_INFO, F("procPyld: txid: 0x")); |
|||
DBGHEXLN(mPayload[iv->id].txId); |
|||
DPRINT(DBG_DEBUG, F("procPyld: max: ")); |
|||
DPRINTLN(DBG_DEBUG, String(mPayload[iv->id].maxPackId)); |
|||
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
|
|||
mPayload[iv->id].complete = true; |
|||
|
|||
uint8_t payload[128]; |
|||
uint8_t payloadLen = 0; |
|||
|
|||
memset(payload, 0, 128); |
|||
|
|||
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { |
|||
memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); |
|||
payloadLen += (mPayload[iv->id].len[i]); |
|||
yield(); |
|||
} |
|||
payloadLen -= 2; |
|||
|
|||
if (mSerialDebug) { |
|||
DPRINT(DBG_INFO, F("Payload (")); |
|||
DBGPRINT(String(payloadLen)); |
|||
DBGPRINT(F("): ")); |
|||
mSys->Radio.dumpBuf(payload, payloadLen); |
|||
} |
|||
|
|||
if (NULL == rec) { |
|||
DPRINTLN(DBG_ERROR, F("record is NULL!")); |
|||
} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { |
|||
if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) |
|||
mStat->rxSuccess++; |
|||
|
|||
rec->ts = mPayload[iv->id].ts; |
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
iv->addValue(i, payload, rec); |
|||
yield(); |
|||
} |
|||
iv->doCalculations(); |
|||
notify(mPayload[iv->id].txCmd); |
|||
|
|||
if(AlarmData == mPayload[iv->id].txCmd) { |
|||
uint8_t i = 0; |
|||
uint16_t code; |
|||
uint32_t start, end; |
|||
while(1) { |
|||
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); |
|||
if(0 == code) |
|||
break; |
|||
if (NULL != mCbAlarm) |
|||
(mCbAlarm)(code, start, end); |
|||
yield(); |
|||
} |
|||
} |
|||
} else { |
|||
DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); |
|||
DBGPRINT(String(rec->pyldLen)); |
|||
DBGPRINTLN(F(" bytes")); |
|||
mStat->rxFail++; |
|||
} |
|||
|
|||
iv->setQueuedCmdFinished(); |
|||
} |
|||
} |
|||
yield(); |
|||
} |
|||
} |
|||
|
|||
private: |
|||
void notify(uint8_t val) { |
|||
if(NULL != mCbPayload) |
|||
(mCbPayload)(val); |
|||
} |
|||
|
|||
void notify(uint16_t code, uint32_t start, uint32_t endTime) { |
|||
if (NULL != mCbAlarm) |
|||
(mCbAlarm)(code, start, endTime); |
|||
} |
|||
|
|||
bool build(uint8_t id, bool *complete) { |
|||
DPRINTLN(DBG_VERBOSE, F("build")); |
|||
uint16_t crc = 0xffff, crcRcv = 0x0000; |
|||
if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) |
|||
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; |
|||
|
|||
// check if all fragments are there
|
|||
*complete = true; |
|||
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { |
|||
if(mPayload[id].len[i] == 0) |
|||
*complete = false; |
|||
} |
|||
if(!*complete) |
|||
return false; |
|||
|
|||
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { |
|||
if (mPayload[id].len[i] > 0) { |
|||
if (i == (mPayload[id].maxPackId - 1)) { |
|||
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); |
|||
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); |
|||
} else |
|||
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); |
|||
} |
|||
yield(); |
|||
} |
|||
|
|||
return (crc == crcRcv) ? true : false; |
|||
} |
|||
|
|||
void reset(uint8_t id) { |
|||
DPRINT(DBG_INFO, "resetPayload: id: "); |
|||
DBGPRINTLN(String(id)); |
|||
memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); |
|||
mPayload[id].txCmd = 0; |
|||
mPayload[id].gotFragment = false; |
|||
mPayload[id].retransmits = 0; |
|||
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; |
|||
mPayload[id].lastFound = false; |
|||
mPayload[id].complete = false; |
|||
mPayload[id].requested = false; |
|||
mPayload[id].ts = *mTimestamp; |
|||
} |
|||
|
|||
IApp *mApp; |
|||
HMSYSTEM *mSys; |
|||
statistics_t *mStat; |
|||
uint8_t mMaxRetrans; |
|||
uint32_t *mTimestamp; |
|||
invPayload_t mPayload[MAX_NUM_INVERTERS]; |
|||
bool mSerialDebug; |
|||
Inverter<> *mHighPrioIv; |
|||
|
|||
alarmListenerType mCbAlarm; |
|||
payloadListenerType mCbPayload; |
|||
}; |
|||
|
|||
#endif /*__HM_PAYLOAD_H__*/ |
@ -0,0 +1,371 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://github.com/lumpapu/ahoy
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __RADIO_H__ |
|||
#define __RADIO_H__ |
|||
|
|||
#include "../utils/dbg.h" |
|||
#include <RF24.h> |
|||
#include "../utils/crc.h" |
|||
#include "../config/config.h" |
|||
#include "SPI.h" |
|||
|
|||
#define SPI_SPEED 1000000 |
|||
|
|||
#define RF_CHANNELS 5 |
|||
|
|||
#define TX_REQ_INFO 0x15 |
|||
#define TX_REQ_DEVCONTROL 0x51 |
|||
#define ALL_FRAMES 0x80 |
|||
#define SINGLE_FRAME 0x81 |
|||
|
|||
const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
// MACROS
|
|||
//-----------------------------------------------------------------------------
|
|||
#define CP_U32_LittleEndian(buf, v) ({ \ |
|||
uint8_t *b = buf; \ |
|||
b[0] = ((v >> 24) & 0xff); \ |
|||
b[1] = ((v >> 16) & 0xff); \ |
|||
b[2] = ((v >> 8) & 0xff); \ |
|||
b[3] = ((v ) & 0xff); \ |
|||
}) |
|||
|
|||
#define CP_U32_BigEndian(buf, v) ({ \ |
|||
uint8_t *b = buf; \ |
|||
b[3] = ((v >> 24) & 0xff); \ |
|||
b[2] = ((v >> 16) & 0xff); \ |
|||
b[1] = ((v >> 8) & 0xff); \ |
|||
b[0] = ((v ) & 0xff); \ |
|||
}) |
|||
|
|||
#define BIT_CNT(x) ((x)<<3) |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
// HM Radio class
|
|||
//-----------------------------------------------------------------------------
|
|||
template <uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW, uint8_t SCLK_PIN = DEF_SCLK_PIN, uint8_t MOSI_PIN = DEF_MOSI_PIN, uint8_t MISO_PIN = DEF_MISO_PIN> |
|||
class HmRadio { |
|||
public: |
|||
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { |
|||
DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); |
|||
DPRINT(DBG_VERBOSE, String(CE_PIN)); |
|||
DPRINT(DBG_VERBOSE, F(", CS_PIN: ")); |
|||
DPRINT(DBG_VERBOSE, String(CS_PIN)); |
|||
DPRINT(DBG_VERBOSE, F(", SPI_SPEED: ")); |
|||
DPRINTLN(DBG_VERBOSE, String(SPI_SPEED) + ")"); |
|||
|
|||
// Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz.
|
|||
// Channel List 2403, 2423, 2440, 2461, 2475MHz
|
|||
mRfChLst[0] = 03; |
|||
mRfChLst[1] = 23; |
|||
mRfChLst[2] = 40; |
|||
mRfChLst[3] = 61; |
|||
mRfChLst[4] = 75; |
|||
|
|||
// default channels
|
|||
mTxChIdx = 2; // Start TX with 40
|
|||
mRxChIdx = 0; // Start RX with 03
|
|||
|
|||
mSendCnt = 0; |
|||
mRetransmits = 0; |
|||
|
|||
mSerialDebug = false; |
|||
mIrqRcvd = false; |
|||
} |
|||
~HmRadio() {} |
|||
|
|||
void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); |
|||
pinMode(irq, INPUT_PULLUP); |
|||
|
|||
uint32_t dtuSn = 0x87654321; |
|||
uint32_t chipID = 0; // will be filled with last 3 bytes of MAC
|
|||
#ifdef ESP32 |
|||
uint64_t MAC = ESP.getEfuseMac(); |
|||
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); |
|||
#else |
|||
chipID = ESP.getChipId(); |
|||
#endif |
|||
if(chipID) { |
|||
dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
|
|||
for(int i = 0; i < 7; i++) { |
|||
dtuSn |= (chipID % 10) << (i * 4); |
|||
chipID /= 10; |
|||
} |
|||
} |
|||
// change the byte order of the DTU serial number and append the required 0x01 at the end
|
|||
DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; |
|||
|
|||
#ifdef ESP32 |
|||
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 |
|||
mSpi = new SPIClass(FSPI); |
|||
#else |
|||
mSpi = new SPIClass(VSPI); |
|||
#endif |
|||
mSpi->begin(sclk, miso, mosi, cs); |
|||
#else |
|||
//the old ESP82xx cannot freely place their SPI pins
|
|||
mSpi = new SPIClass(); |
|||
mSpi->begin(); |
|||
#endif |
|||
mNrf24.begin(mSpi, ce, cs); |
|||
mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
|
|||
|
|||
mNrf24.setChannel(mRfChLst[mRxChIdx]); |
|||
mNrf24.startListening(); |
|||
mNrf24.setDataRate(RF24_250KBPS); |
|||
mNrf24.setAutoAck(true); |
|||
mNrf24.enableDynamicPayloads(); |
|||
mNrf24.setCRCLength(RF24_CRC_16); |
|||
mNrf24.setAddressWidth(5); |
|||
mNrf24.openReadingPipe(1, DTU_RADIO_ID); |
|||
|
|||
// enable all receiving interrupts
|
|||
mNrf24.maskIRQ(false, false, false); |
|||
|
|||
DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); |
|||
DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); |
|||
mNrf24.setPALevel(ampPwr & 0x03); |
|||
|
|||
if(mNrf24.isChipConnected()) { |
|||
DPRINTLN(DBG_INFO, F("Radio Config:")); |
|||
mNrf24.printPrettyDetails(); |
|||
} |
|||
else |
|||
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); |
|||
} |
|||
|
|||
bool loop(void) { |
|||
if (!mIrqRcvd) |
|||
return false; // nothing to do
|
|||
mIrqRcvd = false; |
|||
bool tx_ok, tx_fail, rx_ready; |
|||
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
|
|||
mNrf24.flush_tx(); // empty TX FIFO
|
|||
//DBGPRINTLN("TX whatHappened Ch" + String(mRfChLst[mTxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready));
|
|||
|
|||
// start listening on the default RX channel
|
|||
mRxChIdx = 0; |
|||
mNrf24.setChannel(mRfChLst[mRxChIdx]); |
|||
mNrf24.startListening(); |
|||
|
|||
//uint32_t debug_ms = millis();
|
|||
uint16_t cnt = 300; // that is 60 times 5 channels
|
|||
while (0 < cnt--) { |
|||
uint32_t startMillis = millis(); |
|||
while (millis()-startMillis < 4) { // listen 4ms to each channel
|
|||
if (mIrqRcvd) { |
|||
mIrqRcvd = false; |
|||
if (getReceived()) { // everything received
|
|||
//DBGPRINTLN("RX finished Cnt: " + String(300-cnt) + " time used: " + String(millis()-debug_ms)+ " ms");
|
|||
return true; |
|||
} |
|||
} |
|||
yield(); |
|||
} |
|||
switchRxCh(); // switch to next RX channel
|
|||
yield(); |
|||
} |
|||
// not finished but time is over
|
|||
//DBGPRINTLN("RX not finished: 300 time used: " + String(millis()-debug_ms)+ " ms");
|
|||
return true; |
|||
} |
|||
|
|||
void handleIntr(void) { |
|||
mIrqRcvd = true; |
|||
} |
|||
|
|||
bool isChipConnected(void) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
|
|||
return mNrf24.isChipConnected(); |
|||
} |
|||
void enableDebug() { |
|||
mSerialDebug = true; |
|||
} |
|||
|
|||
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { |
|||
DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); |
|||
DBGHEXLN(cmd); |
|||
initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); |
|||
uint8_t cnt = 10; |
|||
if (isNoMI) { |
|||
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
|
|||
mTxBuf[cnt++] = 0x00; |
|||
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
|
|||
mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
|
|||
mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit
|
|||
mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
|
|||
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
|
|||
} |
|||
} else { //MI 2nd gen. specific
|
|||
switch (cmd) { |
|||
case TurnOn: |
|||
mTxBuf[9] = 0x55; |
|||
mTxBuf[10] = 0xaa; |
|||
break; |
|||
case TurnOff: |
|||
mTxBuf[9] = 0xaa; |
|||
mTxBuf[10] = 0x55; |
|||
break; |
|||
case ActivePowerContr: |
|||
cnt++; |
|||
mTxBuf[9] = 0x5a; |
|||
mTxBuf[10] = 0x5a; |
|||
mTxBuf[11] = data[0]; // power limit
|
|||
break; |
|||
default: |
|||
return; |
|||
} |
|||
cnt++; |
|||
} |
|||
sendPacket(invId, cnt, isRetransmit, true); |
|||
} |
|||
|
|||
void prepareDevInformCmd(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg.
|
|||
DPRINTLN(DBG_DEBUG, F("prepareDevInformCmd 0x") + String(cmd, HEX)); |
|||
initPacket(invId, reqfld, ALL_FRAMES); |
|||
mTxBuf[10] = cmd; // cid
|
|||
mTxBuf[11] = 0x00; |
|||
CP_U32_LittleEndian(&mTxBuf[12], ts); |
|||
if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { |
|||
mTxBuf[18] = (alarmMesId >> 8) & 0xff; |
|||
mTxBuf[19] = (alarmMesId ) & 0xff; |
|||
} |
|||
sendPacket(invId, 24, isRetransmit, true); |
|||
} |
|||
|
|||
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit) { |
|||
initPacket(invId, mid, pid); |
|||
sendPacket(invId, 10, isRetransmit, false); |
|||
} |
|||
|
|||
void dumpBuf(uint8_t buf[], uint8_t len) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf"));
|
|||
for(uint8_t i = 0; i < len; i++) { |
|||
DHEX(buf[i]); |
|||
DBGPRINT(" "); |
|||
} |
|||
DBGPRINTLN(""); |
|||
} |
|||
|
|||
uint8_t getDataRate(void) { |
|||
if(!mNrf24.isChipConnected()) |
|||
return 3; // unkown
|
|||
return mNrf24.getDataRate(); |
|||
} |
|||
|
|||
bool isPVariant(void) { |
|||
return mNrf24.isPVariant(); |
|||
} |
|||
|
|||
std::queue<packet_t> mBufCtrl; |
|||
|
|||
uint32_t mSendCnt; |
|||
uint32_t mRetransmits; |
|||
|
|||
bool mSerialDebug; |
|||
|
|||
private: |
|||
bool getReceived(void) { |
|||
bool tx_ok, tx_fail, rx_ready; |
|||
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
|
|||
//DBGPRINTLN("RX whatHappened Ch" + String(mRfChLst[mRxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready));
|
|||
|
|||
bool isLastPackage = false; |
|||
while(mNrf24.available()) { |
|||
uint8_t len; |
|||
len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed
|
|||
if (len > 0) { |
|||
packet_t p; |
|||
p.ch = mRfChLst[mRxChIdx]; |
|||
p.len = len; |
|||
mNrf24.read(p.packet, len); |
|||
mBufCtrl.push(p); |
|||
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
|
|||
isLastPackage = (p.packet[9] > 0x81); // > 0x81 indicates last packet received
|
|||
else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command
|
|||
isLastPackage = (p.packet[9] > 0x11); // > 0x11 indicates last packet received
|
|||
else if (p.packet[0] != 0x00 && p.packet[0] != 0x88 && p.packet[0] != 0x92) |
|||
// ignore fragment number zero and MI status messages
|
|||
isLastPackage = true; // response from dev control command
|
|||
yield(); |
|||
} |
|||
} |
|||
return isLastPackage; |
|||
} |
|||
|
|||
void switchRxCh() { |
|||
mNrf24.stopListening(); |
|||
// get next channel index
|
|||
if(++mRxChIdx >= RF_CHANNELS) |
|||
mRxChIdx = 0; |
|||
mNrf24.setChannel(mRfChLst[mRxChIdx]); |
|||
mNrf24.startListening(); |
|||
} |
|||
|
|||
void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { |
|||
DPRINTLN(DBG_VERBOSE, F("initPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX)); |
|||
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); |
|||
mTxBuf[0] = mid; // message id
|
|||
CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); |
|||
CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); |
|||
mTxBuf[9] = pid; |
|||
} |
|||
|
|||
void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool clear=false) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket"));
|
|||
//DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt));
|
|||
|
|||
// append crc's
|
|||
if (len > 10) { |
|||
// crc control data
|
|||
uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); |
|||
mTxBuf[len++] = (crc >> 8) & 0xff; |
|||
mTxBuf[len++] = (crc ) & 0xff; |
|||
} |
|||
// crc over all
|
|||
mTxBuf[len] = ah::crc8(mTxBuf, len); |
|||
len++; |
|||
|
|||
if(mSerialDebug) { |
|||
DPRINT(DBG_INFO, F("TX ")); |
|||
DBGPRINT(String(len)); |
|||
DBGPRINT("B Ch"); |
|||
DBGPRINT(String(mRfChLst[mTxChIdx])); |
|||
DBGPRINT(F(" | ")); |
|||
dumpBuf(mTxBuf, len); |
|||
} |
|||
|
|||
mNrf24.stopListening(); |
|||
mNrf24.setChannel(mRfChLst[mTxChIdx]); |
|||
mNrf24.openWritingPipe(reinterpret_cast<uint8_t*>(&invId)); |
|||
mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response
|
|||
|
|||
// switch TX channel for next packet
|
|||
if(++mTxChIdx >= RF_CHANNELS) |
|||
mTxChIdx = 0; |
|||
|
|||
if(isRetransmit) |
|||
mRetransmits++; |
|||
else |
|||
mSendCnt++; |
|||
} |
|||
|
|||
volatile bool mIrqRcvd; |
|||
uint64_t DTU_RADIO_ID; |
|||
|
|||
uint8_t mRfChLst[RF_CHANNELS]; |
|||
uint8_t mTxChIdx; |
|||
uint8_t mRxChIdx; |
|||
|
|||
SPIClass* mSpi; |
|||
RF24 mNrf24; |
|||
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; |
|||
}; |
|||
|
|||
#endif /*__RADIO_H__*/ |
@ -0,0 +1,136 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://github.com/lumpapu/ahoy
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __HM_SYSTEM_H__ |
|||
#define __HM_SYSTEM_H__ |
|||
|
|||
#include "hmInverter.h" |
|||
#include "hmRadio.h" |
|||
|
|||
template <uint8_t MAX_INVERTER=3, class INVERTERTYPE=Inverter<float>> |
|||
class HmSystem { |
|||
public: |
|||
HmRadio<> Radio; |
|||
|
|||
HmSystem() {} |
|||
|
|||
void setup() { |
|||
mNumInv = 0; |
|||
Radio.setup(); |
|||
} |
|||
|
|||
void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin, uint8_t sclkPin, uint8_t mosiPin, uint8_t misoPin) { |
|||
mNumInv = 0; |
|||
Radio.setup(ampPwr, irqPin, cePin, csPin, sclkPin, mosiPin, misoPin); |
|||
} |
|||
|
|||
void addInverters(cfgInst_t *config) { |
|||
Inverter<> *iv; |
|||
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
iv = addInverter(&config->iv[i]); |
|||
if (0ULL != config->iv[i].serial.u64) { |
|||
if (NULL != iv) { |
|||
DPRINT(DBG_INFO, "added inverter "); |
|||
if(iv->config->serial.b[5] == 0x11) |
|||
DBGPRINT("HM"); |
|||
else { |
|||
DBGPRINT(((iv->config->serial.b[4] & 0x03) == 0x01) ? " (2nd Gen) " : " (3rd Gen) "); |
|||
} |
|||
|
|||
DBGPRINTLN(String(iv->config->serial.u64, HEX)); |
|||
|
|||
if((iv->config->serial.b[5] == 0x10) && ((iv->config->serial.b[4] & 0x03) == 0x01)) |
|||
DPRINTLN(DBG_WARN, F("MI Inverter are not fully supported now!!!")); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
INVERTERTYPE *addInverter(cfgIv_t *config) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); |
|||
if(MAX_INVERTER <= mNumInv) { |
|||
DPRINT(DBG_WARN, F("max number of inverters reached!")); |
|||
return NULL; |
|||
} |
|||
INVERTERTYPE *p = &mInverter[mNumInv]; |
|||
p->id = mNumInv; |
|||
p->config = config; |
|||
DPRINT(DBG_VERBOSE, "SERIAL: " + String(p->config->serial.b[5], HEX)); |
|||
DPRINTLN(DBG_VERBOSE, " " + String(p->config->serial.b[4], HEX)); |
|||
if((p->config->serial.b[5] == 0x11) || (p->config->serial.b[5] == 0x10)) { |
|||
switch(p->config->serial.b[4]) { |
|||
case 0x22: |
|||
case 0x21: p->type = INV_TYPE_1CH; break; |
|||
case 0x42: |
|||
case 0x41: p->type = INV_TYPE_2CH; break; |
|||
case 0x62: |
|||
case 0x61: p->type = INV_TYPE_4CH; break; |
|||
default: |
|||
DPRINTLN(DBG_ERROR, F("unknown inverter type")); |
|||
break; |
|||
} |
|||
|
|||
if(p->config->serial.b[5] == 0x11) |
|||
p->ivGen = IV_HM; |
|||
else if((p->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM
|
|||
p->ivGen = IV_HM; |
|||
else // MI 2nd Gen
|
|||
p->ivGen = IV_MI; |
|||
} |
|||
else if(p->config->serial.u64 != 0ULL) |
|||
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); |
|||
|
|||
p->init(); |
|||
|
|||
mNumInv ++; |
|||
return p; |
|||
} |
|||
|
|||
INVERTERTYPE *findInverter(uint8_t buf[]) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); |
|||
INVERTERTYPE *p; |
|||
for(uint8_t i = 0; i < mNumInv; i++) { |
|||
p = &mInverter[i]; |
|||
if((p->config->serial.b[3] == buf[0]) |
|||
&& (p->config->serial.b[2] == buf[1]) |
|||
&& (p->config->serial.b[1] == buf[2]) |
|||
&& (p->config->serial.b[0] == buf[3])) |
|||
return p; |
|||
} |
|||
return NULL; |
|||
} |
|||
|
|||
INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); |
|||
if(pos >= MAX_INVERTER) |
|||
return NULL; |
|||
else if((mInverter[pos].initialized && mInverter[pos].config->serial.u64 != 0ULL) || false == check) |
|||
return &mInverter[pos]; |
|||
else |
|||
return NULL; |
|||
} |
|||
|
|||
uint8_t getNumInverters(void) { |
|||
/*uint8_t num = 0;
|
|||
INVERTERTYPE *p; |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
p = &mInverter[i]; |
|||
if(p->config->serial.u64 != 0ULL) |
|||
num++; |
|||
} |
|||
return num;*/ |
|||
return MAX_NUM_INVERTERS; |
|||
} |
|||
|
|||
void enableDebug() { |
|||
Radio.enableDebug(); |
|||
} |
|||
|
|||
private: |
|||
INVERTERTYPE mInverter[MAX_INVERTER]; |
|||
uint8_t mNumInv; |
|||
}; |
|||
|
|||
#endif /*__HM_SYSTEM_H__*/ |
@ -0,0 +1,825 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __MI_PAYLOAD_H__ |
|||
#define __MI_PAYLOAD_H__ |
|||
|
|||
//#include "hmInverter.h"
|
|||
#include "../utils/dbg.h" |
|||
#include "../utils/crc.h" |
|||
#include "../config/config.h" |
|||
#include <Arduino.h> |
|||
|
|||
typedef struct { |
|||
uint32_t ts; |
|||
bool requested; |
|||
bool limitrequested; |
|||
uint8_t txCmd; |
|||
uint8_t len[MAX_PAYLOAD_ENTRIES]; |
|||
bool complete; |
|||
bool dataAB[3]; |
|||
bool stsAB[3]; |
|||
uint16_t sts[6]; |
|||
uint8_t txId; |
|||
uint8_t invId; |
|||
uint8_t retransmits; |
|||
//uint8_t skipfirstrepeat;
|
|||
bool gotFragment; |
|||
/*
|
|||
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; |
|||
uint8_t maxPackId; |
|||
bool lastFound;*/ |
|||
} miPayload_t; |
|||
|
|||
|
|||
typedef std::function<void(uint8_t)> miPayloadListenerType; |
|||
|
|||
|
|||
template<class HMSYSTEM> |
|||
class MiPayload { |
|||
public: |
|||
MiPayload() {} |
|||
|
|||
void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { |
|||
mApp = app; |
|||
mSys = sys; |
|||
mStat = stat; |
|||
mMaxRetrans = maxRetransmits; |
|||
mTimestamp = timestamp; |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
reset(i, true); |
|||
mPayload[i].limitrequested = true; |
|||
} |
|||
mSerialDebug = false; |
|||
mHighPrioIv = NULL; |
|||
mCbMiPayload = NULL; |
|||
} |
|||
|
|||
void enableSerialDebug(bool enable) { |
|||
mSerialDebug = enable; |
|||
} |
|||
|
|||
void addPayloadListener(miPayloadListenerType cb) { |
|||
mCbMiPayload = cb; |
|||
} |
|||
|
|||
void addAlarmListener(alarmListenerType cb) { |
|||
mCbMiAlarm = cb; |
|||
} |
|||
|
|||
void loop() { |
|||
if(NULL != mHighPrioIv) { // && mHighPrioIv->ivGen == IV_MI) {
|
|||
ivSend(mHighPrioIv, true); // for devcontrol commands?
|
|||
mHighPrioIv = NULL; |
|||
} |
|||
} |
|||
|
|||
void ivSendHighPrio(Inverter<> *iv) { |
|||
mHighPrioIv = iv; |
|||
} |
|||
|
|||
void ivSend(Inverter<> *iv, bool highPrio = false) { |
|||
if(!highPrio) { |
|||
if (mPayload[iv->id].requested) { |
|||
if (!mPayload[iv->id].complete) |
|||
process(false); // no retransmit
|
|||
|
|||
if (!mPayload[iv->id].complete) { |
|||
if (!mPayload[iv->id].gotFragment) |
|||
mStat->rxFailNoAnser++; // got nothing
|
|||
else |
|||
mStat->rxFail++; // got fragments but not complete response
|
|||
|
|||
iv->setQueuedCmdFinished(); // command failed
|
|||
if (mSerialDebug) |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINTLN(F("enqueued cmd failed/timeout")); |
|||
if (mSerialDebug) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("no Payload received! (retransmits: ")); |
|||
DBGPRINT(String(mPayload[iv->id].retransmits)); |
|||
DBGPRINTLN(F(")")); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
reset(iv->id); |
|||
mPayload[iv->id].requested = true; |
|||
|
|||
yield(); |
|||
if (mSerialDebug){ |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("Requesting Inv SN ")); |
|||
DBGPRINTLN(String(iv->config->serial.u64, HEX)); |
|||
} |
|||
|
|||
if (iv->getDevControlRequest()) { |
|||
if (mSerialDebug) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("Devcontrol request 0x")); |
|||
DHEX(iv->devControlCmd); |
|||
DBGPRINT(F(" power limit ")); |
|||
DBGPRINTLN(String(iv->powerLimit[0])); |
|||
} |
|||
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false); |
|||
mPayload[iv->id].txCmd = iv->devControlCmd; |
|||
mPayload[iv->id].limitrequested = true; |
|||
|
|||
iv->clearCmdQueue(); |
|||
iv->enqueCommand<InfoCommand>(SystemConfigPara); // try to read back power limit
|
|||
} else { |
|||
uint8_t cmd = iv->getQueuedCmd(); |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("prepareDevInformCmd 0x")); |
|||
DBGHEXLN(cmd); |
|||
uint8_t cmd2 = cmd; |
|||
if ( cmd == SystemConfigPara ) { //0x05 for HM-types
|
|||
if (!mPayload[iv->id].limitrequested) { // only do once at startup
|
|||
iv->setQueuedCmdFinished(); |
|||
cmd = iv->getQueuedCmd(); |
|||
} else { |
|||
mPayload[iv->id].limitrequested = false; |
|||
} |
|||
} |
|||
|
|||
if (cmd == 0x01 || cmd == SystemConfigPara ) { //0x1 and 0x05 for HM-types
|
|||
cmd = 0x0f; // for MI, these seem to make part of the Polling the device software and hardware version number command
|
|||
cmd2 = cmd == SystemConfigPara ? 0x01 : 0x00; //perhaps we can only try to get second frame?
|
|||
mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false); |
|||
} else { |
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); |
|||
}; |
|||
|
|||
mPayload[iv->id].txCmd = cmd; |
|||
if (iv->type == INV_TYPE_1CH || iv->type == INV_TYPE_2CH) { |
|||
mPayload[iv->id].dataAB[CH1] = false; |
|||
mPayload[iv->id].stsAB[CH1] = false; |
|||
mPayload[iv->id].dataAB[CH0] = false; |
|||
mPayload[iv->id].stsAB[CH0] = false; |
|||
} |
|||
|
|||
if (iv->type == INV_TYPE_2CH) { |
|||
mPayload[iv->id].dataAB[CH2] = false; |
|||
mPayload[iv->id].stsAB[CH2] = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
void add(Inverter<> *iv, packet_t *p) { |
|||
//DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX));
|
|||
if (p->packet[0] == (0x08 + ALL_FRAMES)) { // 0x88; MI status response to 0x09
|
|||
miStsDecode(iv, p); |
|||
} |
|||
|
|||
else if (p->packet[0] == (0x11 + SINGLE_FRAME)) { // 0x92; MI status response to 0x11
|
|||
miStsDecode(iv, p, CH2); |
|||
} |
|||
|
|||
else if ( p->packet[0] == 0x09 + ALL_FRAMES || |
|||
p->packet[0] == 0x11 + ALL_FRAMES || |
|||
( p->packet[0] >= (0x36 + ALL_FRAMES) && p->packet[0] < (0x39 + SINGLE_FRAME) |
|||
&& mPayload[iv->id].txCmd != 0x0f) ) { // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
|
|||
mPayload[iv->id].txId = p->packet[0]; |
|||
miDataDecode(iv,p); |
|||
} |
|||
|
|||
else if (p->packet[0] == ( 0x0f + ALL_FRAMES)) { |
|||
// MI response from get hardware information request
|
|||
record_t<> *rec = iv->getRecordStruct(InverterDevInform_All); // choose the record structure
|
|||
rec->ts = mPayload[iv->id].ts; |
|||
mPayload[iv->id].gotFragment = true; |
|||
|
|||
/*
|
|||
Polling the device software and hardware version number command |
|||
start byte Command word routing address target address User data check end byte |
|||
byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] |
|||
0x7e 0x0f xx xx xx xx YY YY YY YY 0x00 CRC 0x7f |
|||
Command Receipt - First Frame |
|||
start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte |
|||
byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] |
|||
0x7e 0x8f YY YY YY YY xx xx xx xx 0x00 USFWBuild_VER APPFWBuild_VER APPFWBuild_YYYY APPFWBuild_MMDD APPFWBuild_HHMM APPFW_PN HW_VER CRC 0x7f |
|||
Command Receipt - Second Frame |
|||
start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte |
|||
byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] |
|||
0x7e 0x8f YY YY YY YY xx xx xx xx 0x01 HW_PN HW_FB_TLmValue HW_FB_ReSPRT HW_GridSamp_ResValule HW_ECapValue Matching_APPFW_PN CRC 0x7f |
|||
Command receipt - third frame |
|||
start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data check end byte |
|||
byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[15] byte[16] byte[17] byte[18] |
|||
0x7e 0x8f YY YY YY YY xx xx xx xx 0x12 APPFW_MINVER HWInfoAddr PNInfoCRC_gusv PNInfoCRC_gusv CRC 0x7f |
|||
*/ |
|||
|
|||
/*
|
|||
case InverterDevInform_All: |
|||
rec->length = (uint8_t)(HMINFO_LIST_LEN); |
|||
rec->assign = (byteAssign_t *)InfoAssignment; |
|||
rec->pyldLen = HMINFO_PAYLOAD_LEN; |
|||
break; |
|||
const byteAssign_t InfoAssignment[] = { |
|||
{ FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, |
|||
{ FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, |
|||
{ FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, |
|||
{ FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 }, |
|||
{ FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } |
|||
}; |
|||
*/ |
|||
|
|||
if ( p->packet[9] == 0x00 ) {//first frame
|
|||
//FLD_FW_VERSION
|
|||
for (uint8_t i = 0; i < 5; i++) { |
|||
iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); |
|||
} |
|||
iv->isConnected = true; |
|||
if(mSerialDebug) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DPRINT(DBG_INFO,F("HW_VER is ")); |
|||
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25])); |
|||
} |
|||
/*iv->setQueuedCmdFinished();
|
|||
mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x01, false);*/ |
|||
} else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10
|
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
if ( p->packet[9] == 0x01 ) { |
|||
DBGPRINTLN(F("got 2nd frame (hw info)")); |
|||
} else { |
|||
DBGPRINTLN(F("3rd gen. inverter!")); // see table in OpenDTU code, DevInfoParser.cpp devInfo[]
|
|||
} |
|||
// xlsx: HW_ECapValue is total energy?!? (data coll. inst. #154)
|
|||
DPRINT(DBG_INFO,F("HW_PartNo ")); |
|||
DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])); |
|||
//DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13]));
|
|||
if ( p->packet[9] == 0x01 ) { |
|||
iv->setValue(iv->getPosByChFld(0, FLD_YT, rec), rec, (float) ((p->packet[20] << 8) + p->packet[21])/1); |
|||
if(mSerialDebug) { |
|||
DPRINT(DBG_INFO,F("HW_ECapValue ")); |
|||
DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21])); |
|||
|
|||
DPRINT(DBG_INFO,F("HW_FB_TLmValue ")); |
|||
DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15])); |
|||
DPRINT(DBG_INFO,F("HW_FB_ReSPRT ")); |
|||
DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17])); |
|||
DPRINT(DBG_INFO,F("HW_GridSamp_ResValule ")); |
|||
DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19])); |
|||
} |
|||
} |
|||
} else if ( p->packet[9] == 0x12 ) {//3rd frame
|
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINTLN(F("got 3rd frame (hw info)")); |
|||
iv->setQueuedCmdFinished(); |
|||
mStat->rxSuccess++; |
|||
} |
|||
|
|||
} else if ( p->packet[0] == (TX_REQ_INFO + ALL_FRAMES) // response from get information command
|
|||
|| (p->packet[0] == 0xB6 && mPayload[iv->id].txCmd != 0x36)) { // strange short response from MI-1500 3rd gen; might be missleading!
|
|||
// atm, we just do nothing else than print out what we got...
|
|||
// for decoding see xls- Data collection instructions - #147ff
|
|||
//mPayload[iv->id].txId = p->packet[0];
|
|||
DPRINTLN(DBG_DEBUG, F("Response from info request received")); |
|||
uint8_t *pid = &p->packet[9]; |
|||
if (*pid == 0x00) { |
|||
DPRINT(DBG_DEBUG, F("fragment number zero received")); |
|||
iv->setQueuedCmdFinished(); |
|||
} else if (p->packet[9] == 0x81) { // might need some additional check, as this is only ment for short answers!
|
|||
DPRINT_IVID(DBG_WARN, iv->id); |
|||
DBGPRINTLN(F("seems to use 3rd gen. protocol - switching ivGen!")); |
|||
iv->ivGen = IV_HM; |
|||
iv->setQueuedCmdFinished(); |
|||
iv->clearCmdQueue(); |
|||
//DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX));
|
|||
/* (old else-tree)
|
|||
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {^ |
|||
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); |
|||
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; |
|||
mPayload[iv->id].gotFragment = true; |
|||
} |
|||
if ((*pid & ALL_FRAMES) == ALL_FRAMES) { |
|||
// Last packet
|
|||
if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { |
|||
mPayload[iv->id].maxPackId = (*pid & 0x7f); |
|||
if (*pid > 0x81) |
|||
mPayload[iv->id].lastFound = true; |
|||
} |
|||
}*/ |
|||
} |
|||
//}
|
|||
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES ) // response from dev control command
|
|||
|| p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES -1)) { // response from DRED instruction
|
|||
DPRINT_IVID(DBG_DEBUG, iv->id); |
|||
DBGPRINTLN(F("Response from devcontrol request received")); |
|||
|
|||
mPayload[iv->id].txId = p->packet[0]; |
|||
iv->clearDevControlRequest(); |
|||
|
|||
if ((p->packet[9] == 0x5a) && (p->packet[10] == 0x5a)) { |
|||
mApp->setMqttPowerLimitAck(iv); |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("has accepted power limit set point ")); |
|||
DBGPRINT(String(iv->powerLimit[0])); |
|||
DBGPRINT(F(" with PowerLimitControl ")); |
|||
DBGPRINTLN(String(iv->powerLimit[1])); |
|||
|
|||
iv->clearCmdQueue(); |
|||
iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
|
|||
} |
|||
iv->devControlCmd = Init; |
|||
} else { // some other response; copied from hmPayload:process; might not be correct to do that here!!!
|
|||
DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); |
|||
DBGHEXLN(mPayload[iv->id].txCmd); |
|||
DPRINT(DBG_INFO, F("procPyld: txid: 0x")); |
|||
DBGHEXLN(mPayload[iv->id].txId); |
|||
//DPRINT(DBG_DEBUG, F("procPyld: max: "));
|
|||
//DBGPRINTLN(String(mPayload[iv->id].maxPackId));
|
|||
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
|
|||
mPayload[iv->id].complete = true; |
|||
|
|||
uint8_t payload[128]; |
|||
uint8_t payloadLen = 0; |
|||
|
|||
memset(payload, 0, 128); |
|||
|
|||
/*for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) {
|
|||
memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); |
|||
payloadLen += (mPayload[iv->id].len[i]); |
|||
yield(); |
|||
}*/ |
|||
payloadLen -= 2; |
|||
|
|||
if (mSerialDebug) { |
|||
DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); |
|||
mSys->Radio.dumpBuf(payload, payloadLen); |
|||
} |
|||
|
|||
if (NULL == rec) { |
|||
DPRINTLN(DBG_ERROR, F("record is NULL!")); |
|||
} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { |
|||
if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) |
|||
mStat->rxSuccess++; |
|||
|
|||
rec->ts = mPayload[iv->id].ts; |
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
iv->addValue(i, payload, rec); |
|||
yield(); |
|||
} |
|||
iv->doCalculations(); |
|||
notify(mPayload[iv->id].txCmd); |
|||
|
|||
if(AlarmData == mPayload[iv->id].txCmd) { |
|||
uint8_t i = 0; |
|||
uint16_t code; |
|||
uint32_t start, end; |
|||
while(1) { |
|||
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); |
|||
if(0 == code) |
|||
break; |
|||
if (NULL != mCbMiAlarm) |
|||
(mCbMiAlarm)(code, start, end); |
|||
yield(); |
|||
} |
|||
} |
|||
} else { |
|||
DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); |
|||
mStat->rxFail++; |
|||
} |
|||
|
|||
iv->setQueuedCmdFinished(); |
|||
} |
|||
} |
|||
|
|||
void process(bool retransmit) { |
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
|
|||
if (IV_HM == iv->ivGen) // only process MI inverters
|
|||
continue; // skip to next inverter
|
|||
|
|||
if ( !mPayload[iv->id].complete && |
|||
(mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && |
|||
(mPayload[iv->id].txId < (0x36 + ALL_FRAMES)) && |
|||
(mPayload[iv->id].txId > (0x39 + ALL_FRAMES)) && |
|||
(mPayload[iv->id].txId != (0x09 + ALL_FRAMES)) && |
|||
(mPayload[iv->id].txId != (0x11 + ALL_FRAMES)) && |
|||
(mPayload[iv->id].txId != (0x88)) && |
|||
(mPayload[iv->id].txId != (0x92)) && |
|||
(mPayload[iv->id].txId != 0 )) { |
|||
// no processing needed if txId is not one of 0x95, 0x88, 0x89, 0x91, 0x92 or resonse to 0x36ff
|
|||
mPayload[iv->id].complete = true; |
|||
continue; // skip to next inverter
|
|||
} |
|||
|
|||
//delayed next message?
|
|||
//mPayload[iv->id].skipfirstrepeat++;
|
|||
/*if (mPayload[iv->id].skipfirstrepeat) {
|
|||
mPayload[iv->id].skipfirstrepeat = 0; //reset counter
|
|||
continue; // skip to next inverter
|
|||
}*/ |
|||
|
|||
if (!mPayload[iv->id].complete) { |
|||
//DPRINTLN(DBG_INFO, F("Pyld incompl code")); //info for testing only
|
|||
bool crcPass, pyldComplete; |
|||
crcPass = build(iv->id, &pyldComplete); |
|||
if (!crcPass && !pyldComplete) { // payload not complete
|
|||
if ((mPayload[iv->id].requested) && (retransmit)) { |
|||
if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { |
|||
// This is required to prevent retransmissions without answer.
|
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINTLN(F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); |
|||
mPayload[iv->id].retransmits = mMaxRetrans; |
|||
} else if(iv->devControlCmd == ActivePowerContr) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINTLN(F("retransmit power limit")); |
|||
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false); |
|||
} else { |
|||
uint8_t cmd = mPayload[iv->id].txCmd; |
|||
if (mPayload[iv->id].retransmits < mMaxRetrans) { |
|||
mPayload[iv->id].retransmits++; |
|||
if( !mPayload[iv->id].gotFragment ) { |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINTLN(F("nothing received")); |
|||
mPayload[iv->id].retransmits = mMaxRetrans; |
|||
} else if ( cmd == 0x0f ) { |
|||
//hard/firmware request
|
|||
mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true); |
|||
//iv->setQueuedCmdFinished();
|
|||
//cmd = iv->getQueuedCmd();
|
|||
} else { |
|||
bool change = false; |
|||
if ( cmd >= 0x36 && cmd < 0x39 ) { // MI-1500 Data command
|
|||
//cmd++; // just request the next channel
|
|||
//change = true;
|
|||
} else if ( cmd == 0x09 ) {//MI single or dual channel device
|
|||
if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) { |
|||
if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {} |
|||
//first try to get missing sts for first channel a second time
|
|||
else if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) { |
|||
cmd = 0x11; |
|||
change = true; |
|||
mPayload[iv->id].retransmits = 0; //reset counter
|
|||
} |
|||
} |
|||
} else if ( cmd == 0x11) { |
|||
if ( mPayload[iv->id].dataAB[CH2] ) { // data + status ch2 are there?
|
|||
if (mPayload[iv->id].stsAB[CH2] && (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1])) { |
|||
cmd = 0x09; |
|||
change = true; |
|||
} |
|||
} |
|||
} |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
if (change) { |
|||
DBGPRINT(F("next request is")); |
|||
//mPayload[iv->id].skipfirstrepeat = 0;
|
|||
mPayload[iv->id].txCmd = cmd; |
|||
} else { |
|||
DBGPRINT(F("sth.")); |
|||
DBGPRINT(F(" missing: Request Retransmit")); |
|||
} |
|||
DBGPRINT(F(" 0x")); |
|||
DBGHEXLN(cmd); |
|||
//mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true);
|
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd); |
|||
yield(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} else if(!crcPass && pyldComplete) { // crc error on complete Payload
|
|||
if (mPayload[iv->id].retransmits < mMaxRetrans) { |
|||
mPayload[iv->id].retransmits++; |
|||
DPRINT_IVID(DBG_WARN, iv->id); |
|||
DBGPRINTLN(F("CRC Error: Request Complete Retransmit")); |
|||
mPayload[iv->id].txCmd = iv->getQueuedCmd(); |
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
|
|||
DBGPRINT(F("prepareDevInformCmd 0x")); |
|||
DBGHEXLN(mPayload[iv->id].txCmd); |
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); |
|||
} |
|||
} |
|||
/*else { // payload complete
|
|||
//This tree is not really tested, most likely it's not truly complete....
|
|||
DPRINTLN(DBG_INFO, F("procPyld: cmd: 0x") + String(mPayload[iv->id].txCmd, HEX)); |
|||
DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); |
|||
//DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId));
|
|||
//record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
|
|||
//uint8_t payload[128];
|
|||
//uint8_t payloadLen = 0;
|
|||
//memset(payload, 0, 128);
|
|||
//for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) {
|
|||
// memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i]));
|
|||
// payloadLen += (mPayload[iv->id].len[i]);
|
|||
// yield();
|
|||
//}
|
|||
//payloadLen -= 2;
|
|||
//if (mSerialDebug) {
|
|||
// DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): ");
|
|||
// mSys->Radio.dumpBuf(payload, payloadLen);
|
|||
//}
|
|||
//if (NULL == rec) {
|
|||
// DPRINTLN(DBG_ERROR, F("record is NULL!"));
|
|||
//} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) {
|
|||
// if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES))
|
|||
// mStat->rxSuccess++;
|
|||
// rec->ts = mPayload[iv->id].ts;
|
|||
// for (uint8_t i = 0; i < rec->length; i++) {
|
|||
// iv->addValue(i, payload, rec);
|
|||
// yield();
|
|||
// }
|
|||
// iv->doCalculations();
|
|||
// notify(mPayload[iv->id].txCmd);
|
|||
// if(AlarmData == mPayload[iv->id].txCmd) {
|
|||
// uint8_t i = 0;
|
|||
// uint16_t code;
|
|||
// uint32_t start, end;
|
|||
// while(1) {
|
|||
// code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end);
|
|||
// if(0 == code)
|
|||
// break;
|
|||
// if (NULL != mCbAlarm)
|
|||
// (mCbAlarm)(code, start, end);
|
|||
// yield();
|
|||
// }
|
|||
// }
|
|||
//} else {
|
|||
// DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes"));
|
|||
// mStat->rxFail++;
|
|||
//}
|
|||
//iv->setQueuedCmdFinished();
|
|||
//}*/
|
|||
} |
|||
yield(); |
|||
} |
|||
} |
|||
|
|||
private: |
|||
void notify(uint8_t val) { |
|||
if(NULL != mCbMiPayload) |
|||
(mCbMiPayload)(val); |
|||
} |
|||
|
|||
void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) { |
|||
//DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") status msg 0x") + String(p->packet[0], HEX));
|
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
|
|||
rec->ts = mPayload[iv->id].ts; |
|||
mPayload[iv->id].gotFragment = true; |
|||
mPayload[iv->id].txId = p->packet[0]; |
|||
miStsConsolidate(iv, stschan, rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]); |
|||
mPayload[iv->id].stsAB[stschan] = true; |
|||
if (mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].stsAB[CH2]) |
|||
mPayload[iv->id].stsAB[CH0] = true; |
|||
//mPayload[iv->id].skipfirstrepeat = 1;
|
|||
if (mPayload[iv->id].stsAB[CH0] && mPayload[iv->id].dataAB[CH0] && !mPayload[iv->id].complete) { |
|||
miComplete(iv); |
|||
} |
|||
} |
|||
|
|||
void miStsConsolidate(Inverter<> *iv, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) { |
|||
//uint8_t status = (p->packet[11] << 8) + p->packet[12];
|
|||
uint16_t status = 3; // regular status for MI, change to 1 later?
|
|||
if ( uState == 2 ) { |
|||
status = 5050 + stschan; //first approach, needs review!
|
|||
if (lState) |
|||
status += lState*10; |
|||
} else if ( uState > 3 ) { |
|||
status = uState*1000 + uEnum*10; |
|||
if (lState) |
|||
status += lState*100; //needs review, esp. for 4ch-8310 state!
|
|||
//if (lEnum)
|
|||
status += lEnum; |
|||
if (uEnum < 6) { |
|||
status += stschan; |
|||
} |
|||
if (status == 8000) |
|||
status = 8310; //trick?
|
|||
} |
|||
|
|||
uint16_t prntsts = status == 3 ? 1 : status; |
|||
if ( status != mPayload[iv->id].sts[stschan] ) { //sth.'s changed?
|
|||
mPayload[iv->id].sts[stschan] = status; |
|||
DPRINT(DBG_WARN, F("Status change for CH")); |
|||
DBGPRINT(String(stschan)); DBGPRINT(F(" (")); |
|||
DBGPRINT(String(prntsts)); DBGPRINT(F("): ")); |
|||
DBGPRINTLN(iv->getAlarmStr(prntsts)); |
|||
} |
|||
|
|||
if ( !mPayload[iv->id].sts[0] || prntsts < mPayload[iv->id].sts[0] ) { |
|||
mPayload[iv->id].sts[0] = prntsts; |
|||
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); |
|||
} |
|||
|
|||
if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ |
|||
iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!?
|
|||
|
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT(F("alarm ID incremented to ")); |
|||
DBGPRINTLN(String(iv->alarmMesIndex)); |
|||
} |
|||
/*if(AlarmData == mPayload[iv->id].txCmd) {
|
|||
uint8_t i = 0; |
|||
uint16_t code; |
|||
uint32_t start, end; |
|||
while(1) { |
|||
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); |
|||
if(0 == code) |
|||
break; |
|||
if (NULL != mCbAlarm) |
|||
(mCbAlarm)(code, start, end); |
|||
yield(); |
|||
} |
|||
}*/ |
|||
} |
|||
|
|||
void miDataDecode(Inverter<> *iv, packet_t *p) { |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser
|
|||
rec->ts = mPayload[iv->id].ts; |
|||
mPayload[iv->id].gotFragment = true; |
|||
|
|||
uint8_t datachan = ( p->packet[0] == 0x89 || p->packet[0] == (0x36 + ALL_FRAMES) ) ? CH1 : |
|||
( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 : |
|||
p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 : |
|||
CH4; |
|||
//DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan);
|
|||
// count in RF_communication_protocol.xlsx is with offset = -1
|
|||
iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10); |
|||
yield(); |
|||
iv->setValue(iv->getPosByChFld(datachan, FLD_IDC, rec), rec, (float)((p->packet[11] << 8) + p->packet[12])/10); |
|||
yield(); |
|||
iv->setValue(iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[13] << 8) + p->packet[14])/10); |
|||
yield(); |
|||
iv->setValue(iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[15] << 8) + p->packet[16])/100); |
|||
iv->setValue(iv->getPosByChFld(datachan, FLD_PDC, rec), rec, (float)((p->packet[17] << 8) + p->packet[18])/10); |
|||
yield(); |
|||
iv->setValue(iv->getPosByChFld(datachan, FLD_YD, rec), rec, (float)((p->packet[19] << 8) + p->packet[20])/1); |
|||
yield(); |
|||
iv->setValue(iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[21] << 8) + p->packet[22])/10); |
|||
iv->setValue(iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(iv, datachan))); |
|||
//AC Power is missing; we may have to calculate, as no respective data is in payload
|
|||
|
|||
if ( datachan < 3 ) { |
|||
mPayload[iv->id].dataAB[datachan] = true; |
|||
} |
|||
if ( !mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].dataAB[CH2] && mPayload[iv->id].dataAB[CH2] ) { |
|||
mPayload[iv->id].dataAB[CH0] = true; |
|||
} |
|||
|
|||
if (p->packet[0] >= (0x36 + ALL_FRAMES) ) { |
|||
|
|||
/*For MI1500:
|
|||
if (MI1500) { |
|||
STAT = (uint8_t)(p->packet[25] ); |
|||
FCNT = (uint8_t)(p->packet[26]); |
|||
FCODE = (uint8_t)(p->packet[27]); |
|||
}*/ |
|||
|
|||
/*uint16_t status = (uint8_t)(p->packet[23]);
|
|||
mPayload[iv->id].sts[datachan] = status; |
|||
if ( !mPayload[iv->id].sts[0] || status < mPayload[iv->id].sts[0]) { |
|||
mPayload[iv->id].sts[0] = status; |
|||
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, status); |
|||
}*/ |
|||
miStsConsolidate(iv, datachan, rec, p->packet[23], p->packet[24]); |
|||
|
|||
if (p->packet[0] < (0x39 + ALL_FRAMES) ) { |
|||
/*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1;
|
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); |
|||
mPayload[iv->id].txCmd = cmd;*/ |
|||
mPayload[iv->id].txCmd++; |
|||
if (mPayload[iv->id].retransmits) |
|||
mPayload[iv->id].retransmits--; // reserve retransmissions for each response
|
|||
mPayload[iv->id].complete = false; |
|||
} |
|||
|
|||
else if (p->packet[0] == (0x39 + ALL_FRAMES) ) { |
|||
/*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1;
|
|||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); |
|||
mPayload[iv->id].txCmd = cmd;*/ |
|||
mPayload[iv->id].complete = true; |
|||
} |
|||
|
|||
/*if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){
|
|||
iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; |
|||
|
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINT_TXT(TXT_INCRALM); |
|||
DBGPRINTLN(String(iv->alarmMesIndex)); |
|||
}*/ |
|||
|
|||
} |
|||
|
|||
if ( mPayload[iv->id].complete || //4ch device
|
|||
(iv->type != INV_TYPE_4CH //other devices
|
|||
&& mPayload[iv->id].dataAB[CH0] |
|||
&& mPayload[iv->id].stsAB[CH0])) { |
|||
miComplete(iv); |
|||
} |
|||
|
|||
|
|||
|
|||
/*
|
|||
if(AlarmData == mPayload[iv->id].txCmd) { |
|||
uint8_t i = 0; |
|||
uint16_t code; |
|||
uint32_t start, end; |
|||
while(1) { |
|||
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); |
|||
if(0 == code) |
|||
break; |
|||
if (NULL != mCbMiAlarm) |
|||
(mCbAlarm)(code, start, end); |
|||
yield(); |
|||
} |
|||
}*/ |
|||
} |
|||
|
|||
void miComplete(Inverter<> *iv) { |
|||
if (mPayload[iv->id].complete) |
|||
return; //if we got second message as well in repreated attempt
|
|||
mPayload[iv->id].complete = true; // For 2 CH devices, this might be too short...
|
|||
DPRINT_IVID(DBG_INFO, iv->id); |
|||
DBGPRINTLN(F("got all msgs")); |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); |
|||
|
|||
//preliminary AC calculation...
|
|||
float ac_pow = 0; |
|||
for(uint8_t i = 1; i <= iv->channels; i++) { |
|||
if (mPayload[iv->id].sts[i] == 3) { |
|||
uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); |
|||
ac_pow += iv->getValue(pos, rec); |
|||
} |
|||
} |
|||
ac_pow = (int) (ac_pow*9.5); |
|||
iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10); |
|||
|
|||
iv->doCalculations(); |
|||
iv->setQueuedCmdFinished(); |
|||
mStat->rxSuccess++; |
|||
yield(); |
|||
notify(mPayload[iv->id].txCmd); |
|||
} |
|||
|
|||
bool build(uint8_t id, bool *complete) { |
|||
DPRINTLN(DBG_VERBOSE, F("build")); |
|||
// check if all messages are there
|
|||
|
|||
*complete = mPayload[id].complete; |
|||
uint8_t txCmd = mPayload[id].txCmd; |
|||
|
|||
if(!*complete) { |
|||
DPRINTLN(DBG_VERBOSE, F("incomlete, txCmd is 0x") + String(txCmd, HEX)); |
|||
//DBGHEXLN(txCmd);
|
|||
if (txCmd == 0x09 || txCmd == 0x11 || (txCmd >= 0x36 && txCmd <= 0x39)) |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void reset(uint8_t id, bool clrSts = false) { |
|||
DPRINT_IVID(DBG_INFO, id); |
|||
DBGPRINTLN(F("resetPayload")); |
|||
memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); |
|||
mPayload[id].gotFragment = false; |
|||
/*mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
|
|||
mPayload[id].lastFound = false;*/ |
|||
mPayload[id].retransmits = 0; |
|||
mPayload[id].complete = false; |
|||
mPayload[id].dataAB[CH0] = true; //required for 1CH and 2CH devices
|
|||
mPayload[id].dataAB[CH1] = true; //required for 1CH and 2CH devices
|
|||
mPayload[id].dataAB[CH2] = true; //only required for 2CH devices
|
|||
mPayload[id].stsAB[CH0] = true; //required for 1CH and 2CH devices
|
|||
mPayload[id].stsAB[CH1] = true; //required for 1CH and 2CH devices
|
|||
mPayload[id].stsAB[CH2] = true; //only required for 2CH devices
|
|||
mPayload[id].txCmd = 0; |
|||
//mPayload[id].skipfirstrepeat = 0;
|
|||
mPayload[id].requested = false; |
|||
mPayload[id].ts = *mTimestamp; |
|||
mPayload[id].sts[0] = 0; |
|||
if (clrSts) { // only clear channel states at startup
|
|||
mPayload[id].sts[CH1] = 0; |
|||
mPayload[id].sts[CH2] = 0; |
|||
mPayload[id].sts[CH3] = 0; |
|||
mPayload[id].sts[CH4] = 0; |
|||
mPayload[id].sts[5] = 0; //remember last summarized state
|
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
IApp *mApp; |
|||
HMSYSTEM *mSys; |
|||
statistics_t *mStat; |
|||
uint8_t mMaxRetrans; |
|||
uint32_t *mTimestamp; |
|||
miPayload_t mPayload[MAX_NUM_INVERTERS]; |
|||
bool mSerialDebug; |
|||
|
|||
Inverter<> *mHighPrioIv; |
|||
alarmListenerType mCbMiAlarm; |
|||
payloadListenerType mCbMiPayload; |
|||
}; |
|||
|
|||
#endif /*__MI_PAYLOAD_H__*/ |
@ -0,0 +1,146 @@ |
|||
; PlatformIO Project Configuration File |
|||
; |
|||
; Build options: build flags, source filter |
|||
; Upload options: custom upload port, speed and extra flags |
|||
; Library options: dependencies, extra library storages |
|||
; Advanced options: extra scripting |
|||
; |
|||
; Please visit documentation for the other options and examples |
|||
; https://docs.platformio.org/page/projectconf.html |
|||
|
|||
[platformio] |
|||
src_dir = . |
|||
include_dir = . |
|||
|
|||
[env] |
|||
framework = arduino |
|||
board_build.filesystem = littlefs |
|||
upload_speed = 921600 |
|||
|
|||
;build_flags = |
|||
; ;;;;; Possible Debug options ;;;;;; |
|||
; https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level |
|||
;-DDEBUG_ESP_PORT=Serial |
|||
;-DDEBUG_ESP_CORE |
|||
;-DDEBUG_ESP_WIFI |
|||
;-DDEBUG_ESP_HTTP_CLIENT |
|||
;-DDEBUG_ESP_HTTP_SERVER |
|||
;-DDEBUG_ESP_OOM |
|||
|
|||
monitor_speed = 115200 |
|||
|
|||
extra_scripts = |
|||
pre:../scripts/auto_firmware_version.py |
|||
pre:web/html/convert.py |
|||
|
|||
lib_deps = |
|||
https://github.com/yubox-node-org/ESPAsyncWebServer |
|||
nrf24/RF24 @ ^1.4.5 |
|||
paulstoffregen/Time @ ^1.6.1 |
|||
https://github.com/bertmelis/espMqttClient#v1.4.2 |
|||
bblanchon/ArduinoJson @ ^6.21.0 |
|||
https://github.com/JChristensen/Timezone @ ^1.2.4 |
|||
olikraus/U8g2 @ ^2.34.16 |
|||
zinggjm/GxEPD2 @ ^1.5.0 |
|||
|
|||
|
|||
[env:esp8266-release] |
|||
platform = espressif8266 |
|||
board = esp12e |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -D RELEASE |
|||
;-Wl,-Map,output.map |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
;time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
esp8266_exception_decoder |
|||
|
|||
|
|||
[env:esp8266-release-prometheus] |
|||
platform = espressif8266 |
|||
board = esp12e |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -D RELEASE -DENABLE_PROMETHEUS_EP |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
;time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
esp8266_exception_decoder |
|||
|
|||
[env:esp8266-debug] |
|||
platform = espressif8266 |
|||
board = esp12e |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 |
|||
build_type = debug |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp8285-release] |
|||
platform = espressif8266 |
|||
board = esp8285 |
|||
board_build.ldscript = eagle.flash.1m64.ld |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -D RELEASE |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp8285-debug] |
|||
platform = espressif8266 |
|||
board = esp8285 |
|||
board_build.ldscript = eagle.flash.1m64.ld |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial |
|||
build_type = debug |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp32-wroom32-release] |
|||
platform = espressif32 |
|||
board = lolin_d32 |
|||
build_flags = -D RELEASE -std=gnu++14 |
|||
build_unflags = -std=gnu++11 |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
;time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
esp32_exception_decoder |
|||
|
|||
[env:esp32-wroom32-release-prometheus] |
|||
platform = espressif32 |
|||
board = lolin_d32 |
|||
build_flags = -D RELEASE -std=gnu++14 -DENABLE_PROMETHEUS_EP |
|||
build_unflags = -std=gnu++11 |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
;time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
esp32_exception_decoder |
|||
|
|||
[env:esp32-wroom32-debug] |
|||
platform = espressif32 |
|||
board = lolin_d32 |
|||
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -std=gnu++14 |
|||
build_unflags = -std=gnu++11 |
|||
build_type = debug |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:opendtufusionv1-release] |
|||
platform = espressif32 |
|||
board = esp32-s3-devkitc-1 |
|||
build_flags = -D RELEASE -std=gnu++14 |
|||
build_unflags = -std=gnu++11 |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
@ -0,0 +1,114 @@ |
|||
#ifndef __DISPLAY__ |
|||
#define __DISPLAY__ |
|||
|
|||
#include <Timezone.h> |
|||
#include <U8g2lib.h> |
|||
|
|||
#include "../../hm/hmSystem.h" |
|||
#include "../../utils/helper.h" |
|||
#include "Display_Mono.h" |
|||
#include "Display_ePaper.h" |
|||
|
|||
template <class HMSYSTEM> |
|||
class Display { |
|||
public: |
|||
Display() {} |
|||
|
|||
void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { |
|||
mCfg = cfg; |
|||
mSys = sys; |
|||
mUtcTs = utcTs; |
|||
mNewPayload = false; |
|||
mLoopCnt = 0; |
|||
mVersion = version; |
|||
|
|||
if (mCfg->type == 0) |
|||
return; |
|||
|
|||
if ((0 < mCfg->type) && (mCfg->type < 10)) { |
|||
mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); |
|||
mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); |
|||
} else if (mCfg->type >= 10) { |
|||
#if defined(ESP32) |
|||
mRefreshCycle = 0; |
|||
mEpaper.config(mCfg->rot); |
|||
mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); |
|||
#endif |
|||
} |
|||
} |
|||
|
|||
void payloadEventListener(uint8_t cmd) { |
|||
mNewPayload = true; |
|||
} |
|||
|
|||
void tickerSecond() { |
|||
mMono.loop(); |
|||
if (mNewPayload || ((++mLoopCnt % 10) == 0)) { |
|||
mNewPayload = false; |
|||
mLoopCnt = 0; |
|||
DataScreen(); |
|||
} |
|||
} |
|||
|
|||
private: |
|||
void DataScreen() { |
|||
if (mCfg->type == 0) |
|||
return; |
|||
if (*mUtcTs == 0) |
|||
return; |
|||
|
|||
float totalPower = 0; |
|||
float totalYieldDay = 0; |
|||
float totalYieldTotal = 0; |
|||
|
|||
uint8_t isprod = 0; |
|||
|
|||
Inverter<> *iv; |
|||
record_t<> *rec; |
|||
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { |
|||
iv = mSys->getInverterByPos(i); |
|||
rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
if (iv == NULL) |
|||
continue; |
|||
|
|||
if (iv->isProducing(*mUtcTs)) |
|||
isprod++; |
|||
|
|||
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); |
|||
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); |
|||
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); |
|||
} |
|||
|
|||
if ((0 < mCfg->type) && (mCfg->type < 10)) { |
|||
mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod); |
|||
} else if (mCfg->type >= 10) { |
|||
#if defined(ESP32) |
|||
mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); |
|||
mRefreshCycle++; |
|||
#endif |
|||
} |
|||
|
|||
#if defined(ESP32) |
|||
if (mRefreshCycle > 480) { |
|||
mEpaper.fullRefresh(); |
|||
mRefreshCycle = 0; |
|||
} |
|||
#endif |
|||
} |
|||
|
|||
// private member variables
|
|||
bool mNewPayload; |
|||
uint8_t mLoopCnt; |
|||
uint32_t *mUtcTs; |
|||
const char *mVersion; |
|||
display_t *mCfg; |
|||
HMSYSTEM *mSys; |
|||
uint16_t mRefreshCycle; |
|||
|
|||
#if defined(ESP32) |
|||
DisplayEPaper mEpaper; |
|||
#endif |
|||
DisplayMono mMono; |
|||
}; |
|||
|
|||
#endif /*__DISPLAY__*/ |
@ -0,0 +1,157 @@ |
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
#include "Display_Mono.h" |
|||
|
|||
#ifdef ESP8266 |
|||
#include <ESP8266WiFi.h> |
|||
#elif defined(ESP32) |
|||
#include <WiFi.h> |
|||
#endif |
|||
#include "../../utils/helper.h" |
|||
|
|||
//#ifdef U8X8_HAVE_HW_SPI
|
|||
//#include <SPI.h>
|
|||
//#endif
|
|||
//#ifdef U8X8_HAVE_HW_I2C
|
|||
//#include <Wire.h>
|
|||
//#endif
|
|||
|
|||
DisplayMono::DisplayMono() { |
|||
mEnPowerSafe = true; |
|||
mEnScreenSaver = true; |
|||
mLuminance = 60; |
|||
_dispY = 0; |
|||
mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds)
|
|||
mUtcTs = NULL; |
|||
mType = 0; |
|||
} |
|||
|
|||
|
|||
|
|||
void DisplayMono::init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { |
|||
if ((0 < type) && (type < 4)) { |
|||
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); |
|||
mType = type; |
|||
switch(type) { |
|||
case 1: |
|||
mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); |
|||
break; |
|||
default: |
|||
case 2: |
|||
mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); |
|||
break; |
|||
case 3: |
|||
mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); |
|||
break; |
|||
} |
|||
|
|||
mUtcTs = utcTs; |
|||
|
|||
mDisplay->begin(); |
|||
|
|||
mIsLarge = (mDisplay->getWidth() > 120); |
|||
calcLineHeights(); |
|||
|
|||
mDisplay->clearBuffer(); |
|||
if (3 != mType) |
|||
mDisplay->setContrast(mLuminance); |
|||
printText("AHOY!", 0, 35); |
|||
printText("ahoydtu.de", 2, 20); |
|||
printText(version, 3, 46); |
|||
mDisplay->sendBuffer(); |
|||
} |
|||
} |
|||
|
|||
void DisplayMono::config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { |
|||
mEnPowerSafe = enPowerSafe; |
|||
mEnScreenSaver = enScreenSaver; |
|||
mLuminance = lum; |
|||
} |
|||
|
|||
void DisplayMono::loop(void) { |
|||
if (mEnPowerSafe) |
|||
if(mTimeout != 0) |
|||
mTimeout--; |
|||
} |
|||
|
|||
void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { |
|||
|
|||
|
|||
mDisplay->clearBuffer(); |
|||
|
|||
// set Contrast of the Display to raise the lifetime
|
|||
if (3 != mType) |
|||
mDisplay->setContrast(mLuminance); |
|||
|
|||
if ((totalPower > 0) && (isprod > 0)) { |
|||
mTimeout = DISP_DEFAULT_TIMEOUT; |
|||
mDisplay->setPowerSave(false); |
|||
if (totalPower > 999) { |
|||
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); |
|||
} else { |
|||
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); |
|||
} |
|||
printText(_fmtText, 0); |
|||
} else { |
|||
printText("offline", 0, 25); |
|||
// check if it's time to enter power saving mode
|
|||
if (mTimeout == 0) |
|||
mDisplay->setPowerSave(mEnPowerSafe); |
|||
} |
|||
|
|||
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); |
|||
printText(_fmtText, 1); |
|||
|
|||
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); |
|||
printText(_fmtText, 2); |
|||
|
|||
IPAddress ip = WiFi.localIP(); |
|||
if (!(_mExtra % 10) && (ip)) { |
|||
printText(ip.toString().c_str(), 3); |
|||
} else if (!(_mExtra % 5)) { |
|||
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); |
|||
printText(_fmtText, 3); |
|||
} else { |
|||
if(mIsLarge && (NULL != mUtcTs)) |
|||
printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); |
|||
else |
|||
printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); |
|||
} |
|||
|
|||
mDisplay->sendBuffer(); |
|||
|
|||
_dispY = 0; |
|||
_mExtra++; |
|||
} |
|||
|
|||
void DisplayMono::calcLineHeights() { |
|||
uint8_t yOff = 0; |
|||
for (uint8_t i = 0; i < 4; i++) { |
|||
setFont(i); |
|||
yOff += (mDisplay->getMaxCharHeight()); |
|||
mLineOffsets[i] = yOff; |
|||
} |
|||
} |
|||
|
|||
inline void DisplayMono::setFont(uint8_t line) { |
|||
switch (line) { |
|||
case 0: |
|||
mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr); |
|||
break; |
|||
case 3: |
|||
mDisplay->setFont(u8g2_font_5x8_tr); |
|||
break; |
|||
default: |
|||
mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void DisplayMono::printText(const char* text, uint8_t line, uint8_t dispX) { |
|||
if (!mIsLarge) { |
|||
dispX = (line == 0) ? 10 : 5; |
|||
} |
|||
setFont(line); |
|||
|
|||
dispX += (mEnScreenSaver) ? (_mExtra % 7) : 0; |
|||
mDisplay->drawStr(dispX, mLineOffsets[line], text); |
|||
} |
@ -0,0 +1,38 @@ |
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
#pragma once |
|||
|
|||
#include <U8g2lib.h> |
|||
#define DISP_DEFAULT_TIMEOUT 60 // in seconds
|
|||
#define DISP_FMT_TEXT_LEN 32 |
|||
|
|||
class DisplayMono { |
|||
public: |
|||
DisplayMono(); |
|||
|
|||
void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version); |
|||
void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum); |
|||
void loop(void); |
|||
void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); |
|||
|
|||
private: |
|||
void calcLineHeights(); |
|||
void setFont(uint8_t line); |
|||
void printText(const char* text, uint8_t line, uint8_t dispX = 5); |
|||
|
|||
U8G2* mDisplay; |
|||
|
|||
uint8_t mType; |
|||
bool mEnPowerSafe, mEnScreenSaver; |
|||
uint8_t mLuminance; |
|||
|
|||
bool mIsLarge = false; |
|||
uint8_t mLoopCnt; |
|||
uint32_t* mUtcTs; |
|||
uint8_t mLineOffsets[5]; |
|||
|
|||
uint16_t _dispY; |
|||
|
|||
uint8_t _mExtra; |
|||
uint16_t mTimeout; |
|||
char _fmtText[DISP_FMT_TEXT_LEN]; |
|||
}; |
@ -0,0 +1,197 @@ |
|||
#include "Display_ePaper.h" |
|||
|
|||
#ifdef ESP8266 |
|||
#include <ESP8266WiFi.h> |
|||
#elif defined(ESP32) |
|||
#include <WiFi.h> |
|||
#endif |
|||
#include "../../utils/helper.h" |
|||
#include "imagedata.h" |
|||
|
|||
#if defined(ESP32) |
|||
|
|||
static const uint32_t spiClk = 4000000; // 4 MHz
|
|||
|
|||
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) |
|||
SPIClass hspi(HSPI); |
|||
#endif |
|||
|
|||
DisplayEPaper::DisplayEPaper() { |
|||
mDisplayRotation = 2; |
|||
mHeadFootPadding = 16; |
|||
} |
|||
|
|||
|
|||
//***************************************************************************
|
|||
void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char *version) { |
|||
mUtcTs = utcTs; |
|||
|
|||
if (type > 9) { |
|||
Serial.begin(115200); |
|||
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); |
|||
hspi.begin(_SCK, _BUSY, _MOSI, _CS); |
|||
|
|||
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) |
|||
_display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); |
|||
#endif |
|||
_display->init(115200, true, 2, false); |
|||
_display->setRotation(mDisplayRotation); |
|||
_display->setFullWindow(); |
|||
|
|||
// Logo
|
|||
_display->fillScreen(GxEPD_BLACK); |
|||
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); |
|||
while (_display->nextPage()) |
|||
; |
|||
|
|||
// clean the screen
|
|||
delay(2000); |
|||
_display->fillScreen(GxEPD_WHITE); |
|||
while (_display->nextPage()) |
|||
; |
|||
|
|||
headlineIP(); |
|||
|
|||
// call the PowerPage to change the PV Power Values
|
|||
actualPowerPaged(0, 0, 0, 0); |
|||
} |
|||
} |
|||
|
|||
void DisplayEPaper::config(uint8_t rotation) { |
|||
mDisplayRotation = rotation; |
|||
} |
|||
|
|||
//***************************************************************************
|
|||
void DisplayEPaper::fullRefresh() { |
|||
// screen complete black
|
|||
_display->fillScreen(GxEPD_BLACK); |
|||
while (_display->nextPage()) |
|||
; |
|||
delay(2000); |
|||
// screen complete white
|
|||
_display->fillScreen(GxEPD_WHITE); |
|||
while (_display->nextPage()) |
|||
; |
|||
} |
|||
//***************************************************************************
|
|||
void DisplayEPaper::headlineIP() { |
|||
int16_t tbx, tby; |
|||
uint16_t tbw, tbh; |
|||
|
|||
_display->setFont(&FreeSans9pt7b); |
|||
_display->setTextColor(GxEPD_WHITE); |
|||
|
|||
_display->setPartialWindow(0, 0, _display->width(), mHeadFootPadding); |
|||
_display->fillScreen(GxEPD_BLACK); |
|||
|
|||
do { |
|||
if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { |
|||
snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str()); |
|||
} else { |
|||
snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected"); |
|||
} |
|||
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); |
|||
uint16_t x = ((_display->width() - tbw) / 2) - tbx; |
|||
|
|||
_display->setCursor(x, (mHeadFootPadding - 2)); |
|||
_display->println(_fmtText); |
|||
} while (_display->nextPage()); |
|||
} |
|||
//***************************************************************************
|
|||
void DisplayEPaper::lastUpdatePaged() { |
|||
int16_t tbx, tby; |
|||
uint16_t tbw, tbh; |
|||
|
|||
_display->setFont(&FreeSans9pt7b); |
|||
_display->setTextColor(GxEPD_WHITE); |
|||
|
|||
_display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); |
|||
_display->fillScreen(GxEPD_BLACK); |
|||
do { |
|||
if (NULL != mUtcTs) { |
|||
snprintf(_fmtText, sizeof(_fmtText), ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str()); |
|||
|
|||
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); |
|||
uint16_t x = ((_display->width() - tbw) / 2) - tbx; |
|||
|
|||
_display->setCursor(x, (_display->height() - 3)); |
|||
_display->println(_fmtText); |
|||
} |
|||
} while (_display->nextPage()); |
|||
} |
|||
//***************************************************************************
|
|||
void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) { |
|||
int16_t tbx, tby; |
|||
uint16_t tbw, tbh, x, y; |
|||
|
|||
_display->setFont(&FreeSans24pt7b); |
|||
_display->setTextColor(GxEPD_BLACK); |
|||
|
|||
_display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2)); |
|||
_display->fillScreen(GxEPD_WHITE); |
|||
do { |
|||
if (_totalPower > 9999) { |
|||
snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000)); |
|||
_changed = true; |
|||
} else if ((_totalPower > 0) && (_totalPower <= 9999)) { |
|||
snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower); |
|||
_changed = true; |
|||
} else { |
|||
snprintf(_fmtText, sizeof(_fmtText), "offline"); |
|||
} |
|||
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); |
|||
x = ((_display->width() - tbw) / 2) - tbx; |
|||
_display->setCursor(x, mHeadFootPadding + tbh + 10); |
|||
_display->print(_fmtText); |
|||
|
|||
_display->setFont(&FreeSans12pt7b); |
|||
y = _display->height() / 2; |
|||
_display->setCursor(5, y); |
|||
_display->print("today:"); |
|||
snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay); |
|||
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); |
|||
x = ((_display->width() - tbw) / 2) - tbx; |
|||
_display->setCursor(x, y); |
|||
_display->print(_fmtText); |
|||
_display->setCursor(_display->width() - 38, y); |
|||
_display->println("Wh"); |
|||
|
|||
y = y + tbh + 7; |
|||
_display->setCursor(5, y); |
|||
_display->print("total:"); |
|||
snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal); |
|||
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); |
|||
x = ((_display->width() - tbw) / 2) - tbx; |
|||
_display->setCursor(x, y); |
|||
_display->print(_fmtText); |
|||
_display->setCursor(_display->width() - 50, y); |
|||
_display->println("kWh"); |
|||
|
|||
_display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); |
|||
snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); |
|||
_display->println(_fmtText); |
|||
|
|||
} while (_display->nextPage()); |
|||
} |
|||
//***************************************************************************
|
|||
void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { |
|||
// check if the IP has changed
|
|||
if (_settedIP != WiFi.localIP().toString().c_str()) { |
|||
// save the new IP and call the Headline Funktion to adapt the Headline
|
|||
_settedIP = WiFi.localIP().toString().c_str(); |
|||
headlineIP(); |
|||
} |
|||
|
|||
// call the PowerPage to change the PV Power Values
|
|||
actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod); |
|||
|
|||
// if there was an change and the Inverter is producing set a new Timestam in the footline
|
|||
if ((isprod > 0) && (_changed)) { |
|||
_changed = false; |
|||
lastUpdatePaged(); |
|||
} |
|||
|
|||
_display->powerOff(); |
|||
} |
|||
//***************************************************************************
|
|||
#endif // ESP32
|
@ -0,0 +1,52 @@ |
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
#pragma once |
|||
|
|||
#if defined(ESP32) |
|||
|
|||
// uncomment next line to use HSPI for EPD (and VSPI for SD), e.g. with Waveshare ESP32 Driver Board
|
|||
#define USE_HSPI_FOR_EPD |
|||
|
|||
/// uncomment next line to use class GFX of library GFX_Root instead of Adafruit_GFX, to use less code and ram
|
|||
// #include <GFX.h>
|
|||
// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code
|
|||
// enable GxEPD2_GFX base class
|
|||
#define ENABLE_GxEPD2_GFX 1 |
|||
|
|||
#include <GxEPD2_3C.h> |
|||
#include <GxEPD2_BW.h> |
|||
#include <SPI.h> |
|||
|
|||
#include <map> |
|||
// FreeFonts from Adafruit_GFX
|
|||
#include <Fonts/FreeSans12pt7b.h> |
|||
#include <Fonts/FreeSans18pt7b.h> |
|||
#include <Fonts/FreeSans24pt7b.h> |
|||
#include <Fonts/FreeSans9pt7b.h> |
|||
|
|||
// GDEW027C44 2.7 " b/w/r 176x264, IL91874
|
|||
// GDEH0154D67 1.54" b/w 200x200
|
|||
|
|||
class DisplayEPaper { |
|||
public: |
|||
DisplayEPaper(); |
|||
void fullRefresh(); |
|||
void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char* version); |
|||
void config(uint8_t rotation); |
|||
void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); |
|||
|
|||
|
|||
private: |
|||
void headlineIP(); |
|||
void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); |
|||
void lastUpdatePaged(); |
|||
|
|||
uint8_t mDisplayRotation; |
|||
bool _changed = false; |
|||
char _fmtText[35]; |
|||
const char* _settedIP; |
|||
uint8_t mHeadFootPadding; |
|||
GxEPD2_GFX* _display; |
|||
uint32_t *mUtcTs; |
|||
}; |
|||
|
|||
#endif // ESP32
|
@ -0,0 +1,329 @@ |
|||
// GxEPD2_ESP32_ESP8266_WifiData_V1_und_V2
|
|||
|
|||
#ifndef __IMAGEDATA_H__ |
|||
#define __IMAGEDATA_H__ |
|||
|
|||
#if defined(__AVR__) || defined(ARDUINO_ARCH_SAMD) |
|||
#include <avr/pgmspace.h> |
|||
#elif defined(ESP8266) || defined(ESP32) |
|||
#include <pgmspace.h> |
|||
#endif |
|||
|
|||
// 'Logo', 200x200px
|
|||
const unsigned char logo[] PROGMEM = { |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, |
|||
0x0b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x06, |
|||
0x0f, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x7e, 0x0f, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, |
|||
0x03, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x19, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfe, |
|||
0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xe0, 0x70, 0x7f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe0, 0x3f, 0x07, 0xff, 0xff, |
|||
0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xfc, 0x0f, 0xe0, 0x3f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xe0, 0x1f, 0x83, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x1f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, |
|||
0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, |
|||
0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, |
|||
0xff, 0xc1, 0x07, 0x80, 0x07, 0xfe, 0xff, 0xff, 0xfc, 0x07, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xe1, 0x07, 0xc0, 0x01, 0xe0, 0x0f, |
|||
0xff, 0xfc, 0x0f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xfc, 0xff, 0xe1, 0x83, 0xc0, 0x01, 0xc0, 0x07, 0xff, 0xf8, 0x0f, 0xfc, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe1, 0x83, 0xc0, 0x00, |
|||
0xc0, 0x07, 0x8f, 0xf8, 0x1f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe0, 0x01, 0xc0, 0x00, 0x81, 0x83, 0x07, 0xf0, 0x3f, 0xf9, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xe0, 0x01, |
|||
0xe0, 0xe0, 0x87, 0xe3, 0x0f, 0xf0, 0x3f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xe0, 0x00, 0xe0, 0xe0, 0x87, 0xe1, 0x0c, 0x60, 0x7f, |
|||
0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, |
|||
0xe0, 0x00, 0xe1, 0xf0, 0x87, 0xe1, 0x08, 0x60, 0x7f, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xe0, 0xe0, 0xe0, 0xe0, 0x87, 0xc2, 0x00, |
|||
0x40, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0x8f, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, 0x82, 0x00, 0xc0, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, |
|||
0x06, 0x01, 0x81, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xcf, 0xe0, 0xe0, 0x20, 0xe0, 0xe0, 0x0c, 0x03, 0x81, 0xff, 0x1f, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x30, |
|||
0xe1, 0xf8, 0x18, 0x07, 0xe1, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x7f, 0xff, 0xff, 0xf0, 0x1f, 0xf3, 0xfe, 0x01, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xc0, |
|||
0xfb, 0xff, 0xff, 0xff, 0xe0, 0x3e, 0x1f, 0xfc, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xfc, 0x0f, |
|||
0xf8, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, |
|||
0x33, 0xef, 0xff, 0xff, 0xff, 0xff, 0x81, 0xfc, 0x0f, 0xf1, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xf1, 0xff, 0xff, 0xa0, 0x00, 0x7f, 0xe3, |
|||
0xfc, 0x0f, 0xf3, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xf1, 0xf9, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x0f, 0xe7, 0xff, 0xe0, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf9, 0xff, 0x80, 0x3f, 0xff, |
|||
0xe0, 0x0f, 0xfe, 0x1f, 0xc7, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xcf, 0xf8, 0xf0, 0x07, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0x8f, 0xff, 0xfc, |
|||
0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x70, 0x3f, |
|||
0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0x1f, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, 0x63, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0x3f, |
|||
0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfe, |
|||
0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7e, 0x3f, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, |
|||
0x0c, 0x7f, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, |
|||
0x7f, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xf8, |
|||
0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x87, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xfc, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xfc, 0x00, 0x00, 0x01, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x55, 0x00, 0x3f, 0xf8, 0x00, |
|||
0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0x01, 0xff, 0xff, 0xf8, 0x0f, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0x9f, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0x03, |
|||
0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe3, 0xf1, 0xff, |
|||
0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe7, 0xf9, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, |
|||
0xff, 0xf8, 0x7e, 0x06, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xcf, |
|||
0xf8, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0x03, 0x3f, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0x1f, 0x23, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, |
|||
0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xf3, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xf1, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xf8, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x0f, 0xff, 0x8f, 0xf1, 0xff, 0xff, 0xff, 0xfe, 0xf5, 0x90, 0x07, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x03, 0xff, |
|||
0x1f, 0xe3, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0xfe, 0x31, 0xfe, 0x7f, 0xe7, 0xff, 0x80, 0x00, 0x40, 0x00, |
|||
0x07, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0x3c, |
|||
0xf9, 0xfc, 0xff, 0xe7, 0xfe, 0x3f, 0xc9, 0xff, 0xf1, 0x1f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x3c, 0xf9, 0xf9, 0xff, 0xc7, 0xfc, 0xff, 0x90, |
|||
0x7f, 0xf3, 0x03, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, |
|||
0x3f, 0x39, 0xfd, 0xf3, 0xff, 0xcf, 0xfc, 0xff, 0x90, 0x3f, 0xf3, 0x83, 0xf8, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x39, 0xf9, 0xc7, 0xff, 0xcf, 0xfc, |
|||
0xff, 0x32, 0x7f, 0xe4, 0x77, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, |
|||
0xff, 0xff, 0x7f, 0x33, 0xf9, 0x8f, 0xff, 0xcf, 0xf9, 0xff, 0x00, 0x7f, 0xe0, 0x67, 0xfc, 0x7f, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xb3, 0xf3, 0xbf, 0xff, |
|||
0xcf, 0xf9, 0xff, 0x00, 0xff, 0xfe, 0x47, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xf9, 0xff, 0xff, 0x7f, 0xf7, 0xf3, 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe0, 0xff, 0xfc, 0x0f, |
|||
0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xe7, 0xe7, |
|||
0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe1, 0xff, 0xfc, 0x1f, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xe7, 0xef, 0xff, 0xc7, 0xf9, 0xff, 0xc3, 0xff, |
|||
0xfc, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, |
|||
0xef, 0xef, 0xc0, 0xff, 0xe7, 0xf9, 0xff, 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xcf, 0xf0, 0x01, 0xe7, 0xf1, 0xff, |
|||
0x87, 0xff, 0xf8, 0x7f, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, |
|||
0xff, 0xbf, 0xcf, 0xe7, 0xff, 0xc1, 0xe3, 0xe1, 0xff, 0x8f, 0xff, 0xf0, 0xff, 0xff, 0x9f, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x9f, 0xef, 0xe7, 0xff, 0xff, 0xf3, |
|||
0xc1, 0xff, 0x96, 0xaf, 0xf9, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xf9, 0xff, 0xff, 0x9f, 0xe7, 0xe3, 0xff, 0xff, 0xf1, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, |
|||
0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xcf, 0xe7, 0xf3, 0xff, |
|||
0xff, 0xf8, 0xc0, 0x00, 0x4a, 0x90, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xf9, 0xff, 0xff, 0xef, 0xf3, 0xf3, 0x9f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, |
|||
0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe7, 0xf1, |
|||
0xe7, 0xc7, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf3, 0xf0, 0x07, 0xe3, 0xff, 0xff, 0x81, 0xff, 0xff, |
|||
0xff, 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, |
|||
0xf8, 0x07, 0x1f, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xfc, 0x1f, 0x9f, 0xf8, 0xff, 0xff, 0xc3, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, |
|||
0xff, 0xff, 0xf8, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf9, 0xff, 0x9f, 0xfe, 0x3f, |
|||
0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xfd, 0xff, 0xff, 0xf1, 0xff, 0x9f, 0xff, 0x9f, 0xff, 0xf3, 0xff, 0x3f, 0x3f, 0xff, 0xff, |
|||
0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe1, 0xff, 0xcf, |
|||
0xff, 0xc7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xe1, 0xff, 0x8f, 0xff, 0xe7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, |
|||
0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xc1, |
|||
0xff, 0xcf, 0xff, 0xf3, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x81, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xf3, 0xff, |
|||
0x3f, 0x9f, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, |
|||
0xff, 0x91, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0x11, 0xff, 0x9f, 0xff, 0xff, 0xff, |
|||
0xf3, 0xff, 0x1f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xfe, 0x7f, 0xff, 0x21, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xbf, 0x9f, 0xff, 0xff, 0xfe, |
|||
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x20, 0xff, 0x9f, 0xff, |
|||
0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x60, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0x64, 0x3f, |
|||
0x1f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0xe7, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, |
|||
0xff, 0x3f, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, |
|||
0xe7, 0x80, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xf8, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, |
|||
0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0x9f, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xe7, 0xff, 0xfe, 0x7f, 0xff, 0xc3, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xf3, 0xf3, 0xff, 0xfc, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xff, 0xf8, 0xff, 0xff, |
|||
0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xf9, 0xe7, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xf3, 0xf9, 0xff, 0xe1, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xe7, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0x3f, 0x07, |
|||
0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf3, 0xe7, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x00, 0x1f, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, |
|||
0xe0, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, |
|||
0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xf7, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xf8, 0x83, 0xe7, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x13, 0xe7, 0xff, 0xfc, 0x03, |
|||
0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xfc, 0x31, 0xe7, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfe, |
|||
0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x39, 0xe3, 0xff, |
|||
0xfc, 0x00, 0x1f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x31, 0xf3, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xc7, 0xff, 0xff, |
|||
0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x19, |
|||
0xf3, 0xff, 0xfc, 0x00, 0x07, 0xff, 0x87, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xf3, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, |
|||
0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0x83, 0xf3, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xf3, 0xff, 0xff, 0xff, 0xff, |
|||
0xf8, 0x07, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xe3, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xfe, 0x1f, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe1, 0xff, 0xfe, |
|||
0x01, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xf0, 0x00, 0x3f, 0x80, 0x07, 0xff, 0xff, 0xf0, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x4c, |
|||
0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0c, 0xff, 0xf0, 0x00, 0x00, 0x0b, 0x87, 0xff, |
|||
0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0x0e, 0x7f, 0xf8, 0x00, 0x3f, 0xff, 0xc7, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x86, 0x7f, 0xfe, 0x00, 0xff, 0xff, |
|||
0xc3, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0x80, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xf3, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, |
|||
0xff, 0xff, 0xf3, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x07, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, |
|||
0xff, 0xff, 0xff, 0xff, 0xf3, 0xf0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xf3, 0xc0, 0x7f, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xe3, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, 0xe0, |
|||
0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x03, 0xff, |
|||
0xfe, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xf0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x17, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
|||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff |
|||
}; |
|||
|
|||
#endif /*__IMAGEDATA_H__*/ |
@ -0,0 +1,669 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
// https://bert.emelis.net/espMqttClient/
|
|||
|
|||
#ifndef __PUB_MQTT_H__ |
|||
#define __PUB_MQTT_H__ |
|||
|
|||
#ifdef ESP8266 |
|||
#include <ESP8266WiFi.h> |
|||
#elif defined(ESP32) |
|||
#include <WiFi.h> |
|||
#endif |
|||
|
|||
#include "../utils/dbg.h" |
|||
#include "../config/config.h" |
|||
#include <espMqttClient.h> |
|||
#include <ArduinoJson.h> |
|||
#include "../defines.h" |
|||
#include "../hm/hmSystem.h" |
|||
|
|||
#include "pubMqttDefs.h" |
|||
|
|||
#define QOS_0 0 |
|||
|
|||
typedef std::function<void(JsonObject)> subscriptionCb; |
|||
|
|||
struct alarm_t { |
|||
uint16_t code; |
|||
uint32_t start; |
|||
uint32_t end; |
|||
alarm_t(uint16_t c, uint32_t s, uint32_t e) : code(c), start(s), end(e) {} |
|||
}; |
|||
|
|||
typedef struct { |
|||
bool running; |
|||
uint8_t lastIvId; |
|||
uint8_t sub; |
|||
uint8_t foundIvCnt; |
|||
} discovery_t; |
|||
|
|||
template<class HMSYSTEM> |
|||
class PubMqtt { |
|||
public: |
|||
PubMqtt() { |
|||
mRxCnt = 0; |
|||
mTxCnt = 0; |
|||
mSubscriptionCb = NULL; |
|||
memset(mLastIvState, MQTT_STATUS_NOT_AVAIL_NOT_PROD, MAX_NUM_INVERTERS); |
|||
mLastAnyAvail = false; |
|||
} |
|||
|
|||
~PubMqtt() { } |
|||
|
|||
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) { |
|||
mCfgMqtt = cfg_mqtt; |
|||
mDevName = devName; |
|||
mVersion = version; |
|||
mSys = sys; |
|||
mUtcTimestamp = utcTs; |
|||
mIntervalTimeout = 1; |
|||
|
|||
mDiscovery.running = false; |
|||
|
|||
snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic); |
|||
|
|||
if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0)) |
|||
mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd); |
|||
snprintf(mClientId, 24, "%s-", mDevName); |
|||
uint8_t pos = strlen(mClientId); |
|||
mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0]; |
|||
mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0]; |
|||
mClientId[pos++] = WiFi.macAddress().substring(12, 13).c_str()[0]; |
|||
mClientId[pos++] = WiFi.macAddress().substring(13, 14).c_str()[0]; |
|||
mClientId[pos++] = WiFi.macAddress().substring(15, 16).c_str()[0]; |
|||
mClientId[pos++] = WiFi.macAddress().substring(16, 17).c_str()[0]; |
|||
mClientId[pos++] = '\0'; |
|||
|
|||
mClient.setClientId(mClientId); |
|||
mClient.setServer(mCfgMqtt->broker, mCfgMqtt->port); |
|||
mClient.setWill(mLwtTopic, QOS_0, true, mqttStr[MQTT_STR_LWT_NOT_CONN]); |
|||
mClient.onConnect(std::bind(&PubMqtt::onConnect, this, std::placeholders::_1)); |
|||
mClient.onDisconnect(std::bind(&PubMqtt::onDisconnect, this, std::placeholders::_1)); |
|||
mClient.onMessage(std::bind(&PubMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); |
|||
} |
|||
|
|||
void loop() { |
|||
#if defined(ESP8266) |
|||
mClient.loop(); |
|||
yield(); |
|||
#endif |
|||
|
|||
if(mDiscovery.running) |
|||
discoveryConfigLoop(); |
|||
} |
|||
|
|||
|
|||
void tickerSecond() { |
|||
if (mIntervalTimeout > 0) |
|||
mIntervalTimeout--; |
|||
|
|||
if(mClient.disconnected()) { |
|||
mClient.connect(); |
|||
return; // next try in a second
|
|||
} |
|||
|
|||
if(0 == mCfgMqtt->interval) // no fixed interval, publish once new data were received (from inverter)
|
|||
sendIvData(); |
|||
else { // send mqtt data in a fixed interval
|
|||
if(mIntervalTimeout == 0) { |
|||
mIntervalTimeout = mCfgMqtt->interval; |
|||
mSendList.push(RealTimeRunData_Debug); |
|||
sendIvData(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void tickerMinute() { |
|||
snprintf(mVal, 40, "%ld", millis() / 1000); |
|||
publish(subtopics[MQTT_UPTIME], mVal); |
|||
publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str()); |
|||
publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str()); |
|||
#ifndef ESP32 |
|||
publish(subtopics[MQTT_HEAP_FRAG], String(ESP.getHeapFragmentation()).c_str()); |
|||
#endif |
|||
} |
|||
|
|||
bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { |
|||
if (!mClient.connected()) |
|||
return false; |
|||
|
|||
publish(subtopics[MQTT_SUNRISE], String(sunrise).c_str(), true); |
|||
publish(subtopics[MQTT_SUNSET], String(sunset).c_str(), true); |
|||
publish(subtopics[MQTT_COMM_START], String(sunrise - offs).c_str(), true); |
|||
publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true); |
|||
publish(subtopics[MQTT_DIS_NIGHT_COMM], ((disNightCom) ? dict[STR_TRUE] : dict[STR_FALSE]), true); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
bool tickerComm(bool disabled) { |
|||
if (!mClient.connected()) |
|||
return false; |
|||
|
|||
publish(subtopics[MQTT_COMM_DISABLED], ((disabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true); |
|||
publish(subtopics[MQTT_COMM_DIS_TS], String(*mUtcTimestamp).c_str(), true); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void tickerMidnight() { |
|||
// set Total YieldDay to zero
|
|||
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[FLD_YD]); |
|||
snprintf(mVal, 2, "0"); |
|||
publish(mSubTopic, mVal, true); |
|||
} |
|||
|
|||
void payloadEventListener(uint8_t cmd) { |
|||
if(mClient.connected()) { // prevent overflow if MQTT broker is not reachable but set
|
|||
if((0 == mCfgMqtt->interval) || (RealTimeRunData_Debug != cmd)) // no interval or no live data
|
|||
mSendList.push(cmd); |
|||
} |
|||
} |
|||
|
|||
void alarmEventListener(uint16_t code, uint32_t start, uint32_t endTime) { |
|||
if(mClient.connected()) { |
|||
mAlarmList.push(alarm_t(code, start, endTime)); |
|||
} |
|||
} |
|||
|
|||
void publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) { |
|||
if(!mClient.connected()) |
|||
return; |
|||
|
|||
if(addTopic){ |
|||
snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic); |
|||
} else { |
|||
snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s", subTopic); |
|||
} |
|||
|
|||
do { |
|||
if(0 != mClient.publish(mTopic, QOS_0, retained, payload)) |
|||
break; |
|||
if(!mClient.connected()) |
|||
break; |
|||
#if defined(ESP8266) |
|||
mClient.loop(); |
|||
#endif |
|||
yield(); |
|||
} while(1); |
|||
|
|||
mTxCnt++; |
|||
} |
|||
|
|||
void subscribe(const char *subTopic) { |
|||
char topic[MQTT_TOPIC_LEN + 20]; |
|||
snprintf(topic, (MQTT_TOPIC_LEN + 20), "%s/%s", mCfgMqtt->topic, subTopic); |
|||
mClient.subscribe(topic, QOS_0); |
|||
} |
|||
|
|||
void setSubscriptionCb(subscriptionCb cb) { |
|||
mSubscriptionCb = cb; |
|||
} |
|||
|
|||
inline bool isConnected() { |
|||
return mClient.connected(); |
|||
} |
|||
|
|||
inline uint32_t getTxCnt(void) { |
|||
return mTxCnt; |
|||
} |
|||
|
|||
inline uint32_t getRxCnt(void) { |
|||
return mRxCnt; |
|||
} |
|||
|
|||
void sendDiscoveryConfig(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig")); |
|||
mDiscovery.running = true; |
|||
mDiscovery.lastIvId = 0; |
|||
mDiscovery.sub = 0; |
|||
mDiscovery.foundIvCnt = 0; |
|||
} |
|||
|
|||
void setPowerLimitAck(Inverter<> *iv) { |
|||
if (NULL != iv) { |
|||
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]); |
|||
publish(mSubTopic, "true", true); |
|||
} |
|||
} |
|||
|
|||
private: |
|||
void onConnect(bool sessionPreset) { |
|||
DPRINTLN(DBG_INFO, F("MQTT connected")); |
|||
|
|||
publish(subtopics[MQTT_VERSION], mVersion, true); |
|||
publish(subtopics[MQTT_DEVICE], mDevName, true); |
|||
publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true); |
|||
tickerMinute(); |
|||
publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false); |
|||
|
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
snprintf(mVal, 20, "ctrl/limit/%d", i); |
|||
subscribe(mVal); |
|||
snprintf(mVal, 20, "ctrl/restart/%d", i); |
|||
subscribe(mVal); |
|||
} |
|||
subscribe(subscr[MQTT_SUBS_SET_TIME]); |
|||
} |
|||
|
|||
void onDisconnect(espMqttClientTypes::DisconnectReason reason) { |
|||
DPRINT(DBG_INFO, F("MQTT disconnected, reason: ")); |
|||
switch (reason) { |
|||
case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED: |
|||
DBGPRINTLN(F("TCP disconnect")); |
|||
break; |
|||
case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: |
|||
DBGPRINTLN(F("wrong protocol version")); |
|||
break; |
|||
case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED: |
|||
DBGPRINTLN(F("identifier rejected")); |
|||
break; |
|||
case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE: |
|||
DBGPRINTLN(F("broker unavailable")); |
|||
break; |
|||
case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS: |
|||
DBGPRINTLN(F("malformed credentials")); |
|||
break; |
|||
case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED: |
|||
DBGPRINTLN(F("not authorized")); |
|||
break; |
|||
default: |
|||
DBGPRINTLN(F("unknown")); |
|||
} |
|||
} |
|||
|
|||
void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { |
|||
if(len == 0) |
|||
return; |
|||
DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]); |
|||
DBGPRINTLN(String(topic)); |
|||
if(NULL == mSubscriptionCb) |
|||
return; |
|||
|
|||
DynamicJsonDocument json(128); |
|||
JsonObject root = json.to<JsonObject>(); |
|||
|
|||
bool limitAbs = false; |
|||
if(len > 0) { |
|||
char *pyld = new char[len + 1]; |
|||
strncpy(pyld, (const char*)payload, len); |
|||
pyld[len] = '\0'; |
|||
root[F("val")] = atoi(pyld); |
|||
if(pyld[len-1] == 'W') |
|||
limitAbs = true; |
|||
delete[] pyld; |
|||
} |
|||
|
|||
const char *p = topic; |
|||
uint8_t pos = 0; |
|||
uint8_t elm = 0; |
|||
char tmp[30]; |
|||
|
|||
while(1) { |
|||
if(('/' == p[pos]) || ('\0' == p[pos])) { |
|||
strncpy(tmp, p, pos); |
|||
tmp[pos] = '\0'; |
|||
switch(elm++) { |
|||
case 1: root[F("path")] = String(tmp); break; |
|||
case 2: |
|||
if(strncmp("limit", tmp, 5) == 0) { |
|||
if(limitAbs) |
|||
root[F("cmd")] = F("limit_nonpersistent_absolute"); |
|||
else |
|||
root[F("cmd")] = F("limit_nonpersistent_relative"); |
|||
} |
|||
else |
|||
root[F("cmd")] = String(tmp); |
|||
break; |
|||
case 3: root[F("id")] = atoi(tmp); break; |
|||
default: break; |
|||
} |
|||
if('\0' == p[pos]) |
|||
break; |
|||
p = p + pos + 1; |
|||
pos = 0; |
|||
} |
|||
pos++; |
|||
} |
|||
|
|||
/*char out[128];
|
|||
serializeJson(root, out, 128); |
|||
DPRINTLN(DBG_INFO, "json: " + String(out));*/ |
|||
(mSubscriptionCb)(root); |
|||
|
|||
mRxCnt++; |
|||
} |
|||
|
|||
void discoveryConfigLoop(void) { |
|||
char topic[64], name[32], uniq_id[32], buf[350]; |
|||
DynamicJsonDocument doc(256); |
|||
|
|||
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC}; |
|||
const char* unitTotal[4] = {"W", "kWh", "Wh", "W"}; |
|||
|
|||
String node_id = String(mDevName) + "_TOTAL"; |
|||
bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS); |
|||
|
|||
Inverter<> *iv = mSys->getInverterByPos(mDiscovery.lastIvId); |
|||
record_t<> *rec = NULL; |
|||
if (NULL != iv) { |
|||
rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
if(0 == mDiscovery.sub) |
|||
mDiscovery.foundIvCnt++; |
|||
} |
|||
|
|||
if ((NULL != iv) || total) { |
|||
if (!total) { |
|||
doc[F("name")] = iv->config->name; |
|||
doc[F("ids")] = String(iv->config->serial.u64, HEX); |
|||
doc[F("mdl")] = iv->config->name; |
|||
} |
|||
else { |
|||
doc[F("name")] = node_id; |
|||
doc[F("ids")] = node_id; |
|||
doc[F("mdl")] = node_id; |
|||
} |
|||
|
|||
doc[F("cu")] = F("http://") + String(WiFi.localIP().toString()); |
|||
doc[F("mf")] = F("Hoymiles"); |
|||
JsonObject deviceObj = doc.as<JsonObject>(); // deviceObj is only pointer!?
|
|||
|
|||
const char *devCls, *stateCls; |
|||
if (!total) { |
|||
if (rec->assign[mDiscovery.sub].ch == CH0) |
|||
snprintf(name, 32, "%s %s", iv->config->name, iv->getFieldName(mDiscovery.sub, rec)); |
|||
else |
|||
snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); |
|||
snprintf(topic, 64, "/ch%d/%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); |
|||
snprintf(uniq_id, 32, "ch%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); |
|||
|
|||
devCls = getFieldDeviceClass(rec->assign[mDiscovery.sub].fieldId); |
|||
stateCls = getFieldStateClass(rec->assign[mDiscovery.sub].fieldId); |
|||
} |
|||
|
|||
else { // total values
|
|||
snprintf(name, 32, "Total %s", fields[fldTotal[mDiscovery.sub]]); |
|||
snprintf(topic, 64, "/%s", fields[fldTotal[mDiscovery.sub]]); |
|||
snprintf(uniq_id, 32, "total_%s", fields[fldTotal[mDiscovery.sub]]); |
|||
devCls = getFieldDeviceClass(fldTotal[mDiscovery.sub]); |
|||
stateCls = getFieldStateClass(fldTotal[mDiscovery.sub]); |
|||
} |
|||
|
|||
DynamicJsonDocument doc2(512); |
|||
doc2[F("name")] = name; |
|||
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic); |
|||
doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub])); |
|||
doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id; |
|||
doc2[F("dev")] = deviceObj; |
|||
if (!(String(stateCls) == String("total_increasing"))) |
|||
doc2[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
|
|||
if (devCls != NULL) |
|||
doc2[F("dev_cla")] = String(devCls); |
|||
if (stateCls != NULL) |
|||
doc2[F("stat_cla")] = String(stateCls); |
|||
|
|||
if (!total) |
|||
snprintf(topic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); |
|||
else // total values
|
|||
snprintf(topic, 64, "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]); |
|||
size_t size = measureJson(doc2) + 1; |
|||
memset(buf, 0, size); |
|||
serializeJson(doc2, buf, size); |
|||
publish(topic, buf, true, false); |
|||
|
|||
if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) { |
|||
mDiscovery.sub = 0; |
|||
checkDiscoveryEnd(); |
|||
} |
|||
} else { |
|||
mDiscovery.sub = 0; |
|||
checkDiscoveryEnd(); |
|||
} |
|||
|
|||
yield(); |
|||
} |
|||
|
|||
void checkDiscoveryEnd(void) { |
|||
if(++mDiscovery.lastIvId == MAX_NUM_INVERTERS) { |
|||
// check if only one inverter was found, then don't create 'total' sensor
|
|||
if(mDiscovery.foundIvCnt == 1) |
|||
mDiscovery.running = false; |
|||
} else if(mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1)) |
|||
mDiscovery.running = false; |
|||
} |
|||
|
|||
const char *getFieldDeviceClass(uint8_t fieldId) { |
|||
uint8_t pos = 0; |
|||
for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { |
|||
if (deviceFieldAssignment[pos].fieldId == fieldId) |
|||
break; |
|||
} |
|||
return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : deviceClasses[deviceFieldAssignment[pos].deviceClsId]; |
|||
} |
|||
|
|||
const char *getFieldStateClass(uint8_t fieldId) { |
|||
uint8_t pos = 0; |
|||
for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { |
|||
if (deviceFieldAssignment[pos].fieldId == fieldId) |
|||
break; |
|||
} |
|||
return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId]; |
|||
} |
|||
|
|||
bool processIvStatus() { |
|||
// returns true if any inverter is available
|
|||
bool allAvail = true; // shows if all enabled inverters are available
|
|||
bool anyAvail = false; // shows if at least one enabled inverter is available
|
|||
bool changed = false; |
|||
Inverter<> *iv; |
|||
record_t<> *rec; |
|||
|
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
iv = mSys->getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
if (!iv->config->enabled) |
|||
continue; // skip to next inverter
|
|||
|
|||
rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
|
|||
// inverter status
|
|||
uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; |
|||
if (iv->isAvailable(*mUtcTimestamp)) { |
|||
anyAvail = true; |
|||
status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD; |
|||
} |
|||
else // inverter is enabled but not available
|
|||
allAvail = false; |
|||
|
|||
if(mLastIvState[id] != status) { |
|||
// if status changed from producing to not producing send last data immediately
|
|||
if (MQTT_STATUS_AVAIL_PROD == mLastIvState[id]) |
|||
sendData(iv, RealTimeRunData_Debug); |
|||
|
|||
mLastIvState[id] = status; |
|||
changed = true; |
|||
|
|||
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); |
|||
snprintf(mVal, 40, "%d", status); |
|||
publish(mSubTopic, mVal, true); |
|||
|
|||
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name); |
|||
snprintf(mVal, 40, "%d", iv->getLastTs(rec)); |
|||
publish(mSubTopic, mVal, true); |
|||
} |
|||
} |
|||
|
|||
if(changed) { |
|||
snprintf(mVal, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE))); |
|||
publish("status", mVal, true); |
|||
} |
|||
|
|||
return anyAvail; |
|||
} |
|||
|
|||
void sendAlarmData() { |
|||
if(mAlarmList.empty()) |
|||
return; |
|||
Inverter<> *iv = mSys->getInverterByPos(0, false); |
|||
while(!mAlarmList.empty()) { |
|||
alarm_t alarm = mAlarmList.front(); |
|||
publish(subtopics[MQTT_ALARM], iv->getAlarmStr(alarm.code).c_str()); |
|||
publish(subtopics[MQTT_ALARM_START], String(alarm.start).c_str()); |
|||
publish(subtopics[MQTT_ALARM_END], String(alarm.end).c_str()); |
|||
mAlarmList.pop(); |
|||
} |
|||
} |
|||
|
|||
void sendData(Inverter<> *iv, uint8_t curInfoCmd) { |
|||
record_t<> *rec = iv->getRecordStruct(curInfoCmd); |
|||
|
|||
if (iv->getLastTs(rec) > 0) { |
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
bool retained = false; |
|||
if (curInfoCmd == RealTimeRunData_Debug) { |
|||
switch (rec->assign[i].fieldId) { |
|||
case FLD_YT: |
|||
case FLD_YD: |
|||
if ((rec->assign[i].ch == CH0) && (!iv->isProducing(*mUtcTimestamp))) // avoids returns to 0 on restart
|
|||
continue; |
|||
retained = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); |
|||
snprintf(mVal, 40, "%g", ah::round3(iv->getValue(i, rec))); |
|||
publish(mSubTopic, mVal, retained); |
|||
|
|||
yield(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void sendIvData() { |
|||
bool anyAvail = processIvStatus(); |
|||
if (mLastAnyAvail != anyAvail) |
|||
mSendList.push(RealTimeRunData_Debug); // makes shure that total values are calculated
|
|||
|
|||
if(mSendList.empty()) |
|||
return; |
|||
|
|||
float total[4]; |
|||
bool RTRDataHasBeenSent = false; |
|||
|
|||
while(!mSendList.empty()) { |
|||
memset(total, 0, sizeof(float) * 4); |
|||
uint8_t curInfoCmd = mSendList.front(); |
|||
|
|||
if ((curInfoCmd != RealTimeRunData_Debug) || !RTRDataHasBeenSent) { // send RTR Data only once
|
|||
bool sendTotals = (curInfoCmd == RealTimeRunData_Debug); |
|||
|
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
if (!iv->config->enabled) |
|||
continue; // skip to next inverter
|
|||
|
|||
// send RTR Data only if status is available
|
|||
if ((curInfoCmd != RealTimeRunData_Debug) || (MQTT_STATUS_NOT_AVAIL_NOT_PROD != mLastIvState[id])) |
|||
sendData(iv, curInfoCmd); |
|||
|
|||
// calculate total values for RealTimeRunData_Debug
|
|||
if (sendTotals) { |
|||
record_t<> *rec = iv->getRecordStruct(curInfoCmd); |
|||
|
|||
sendTotals &= (iv->getLastTs(rec) > 0); |
|||
if (sendTotals) { |
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
if (CH0 == rec->assign[i].ch) { |
|||
switch (rec->assign[i].fieldId) { |
|||
case FLD_PAC: |
|||
total[0] += iv->getValue(i, rec); |
|||
break; |
|||
case FLD_YT: |
|||
total[1] += iv->getValue(i, rec); |
|||
break; |
|||
case FLD_YD: |
|||
total[2] += iv->getValue(i, rec); |
|||
break; |
|||
case FLD_PDC: |
|||
total[3] += iv->getValue(i, rec); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
yield(); |
|||
} |
|||
|
|||
if (sendTotals) { |
|||
uint8_t fieldId; |
|||
for (uint8_t i = 0; i < 4; i++) { |
|||
bool retained = true; |
|||
switch (i) { |
|||
default: |
|||
case 0: |
|||
fieldId = FLD_PAC; |
|||
retained = false; |
|||
break; |
|||
case 1: |
|||
fieldId = FLD_YT; |
|||
break; |
|||
case 2: |
|||
fieldId = FLD_YD; |
|||
break; |
|||
case 3: |
|||
fieldId = FLD_PDC; |
|||
retained = false; |
|||
break; |
|||
} |
|||
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); |
|||
snprintf(mVal, 40, "%g", ah::round3(total[i])); |
|||
publish(mSubTopic, mVal, retained); |
|||
} |
|||
RTRDataHasBeenSent = true; |
|||
yield(); |
|||
} |
|||
} |
|||
|
|||
mSendList.pop(); // remove from list once all inverters were processed
|
|||
} |
|||
|
|||
mLastAnyAvail = anyAvail; |
|||
} |
|||
|
|||
espMqttClient mClient; |
|||
cfgMqtt_t *mCfgMqtt; |
|||
#if defined(ESP8266) |
|||
WiFiEventHandler mHWifiCon, mHWifiDiscon; |
|||
#endif |
|||
|
|||
HMSYSTEM *mSys; |
|||
uint32_t *mUtcTimestamp; |
|||
uint32_t mRxCnt, mTxCnt; |
|||
std::queue<uint8_t> mSendList; |
|||
std::queue<alarm_t> mAlarmList; |
|||
subscriptionCb mSubscriptionCb; |
|||
bool mLastAnyAvail; |
|||
uint8_t mLastIvState[MAX_NUM_INVERTERS]; |
|||
uint16_t mIntervalTimeout; |
|||
|
|||
// last will topic and payload must be available trough lifetime of 'espMqttClient'
|
|||
char mLwtTopic[MQTT_TOPIC_LEN+5]; |
|||
const char *mDevName, *mVersion; |
|||
char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT
|
|||
// global buffer for mqtt topic. Used when publishing mqtt messages.
|
|||
char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; |
|||
char mSubTopic[32 + MAX_NAME_LENGTH + 1]; |
|||
char mVal[40]; |
|||
discovery_t mDiscovery; |
|||
}; |
|||
|
|||
#endif /*__PUB_MQTT_H__*/ |
@ -0,0 +1,96 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __PUB_MQTT_DEFS_H__ |
|||
#define __PUB_MQTT_DEFS_H__ |
|||
|
|||
#include <Arduino.h> |
|||
|
|||
enum { |
|||
STR_TRUE, |
|||
STR_FALSE |
|||
}; |
|||
|
|||
const char* const dict[] PROGMEM = { |
|||
"true", |
|||
"false" |
|||
}; |
|||
|
|||
enum { |
|||
MQTT_STR_LWT_CONN, |
|||
MQTT_STR_LWT_NOT_CONN, |
|||
MQTT_STR_AVAILABLE, |
|||
MQTT_STR_LAST_SUCCESS, |
|||
MQTT_STR_TOTAL, |
|||
MQTT_STR_GOT_TOPIC |
|||
}; |
|||
|
|||
const char* const mqttStr[] PROGMEM = { |
|||
"connected", |
|||
"not connected", |
|||
"available", |
|||
"last_success", |
|||
"total", |
|||
"MQTT got topic: " |
|||
}; |
|||
|
|||
|
|||
enum { |
|||
MQTT_UPTIME = 0, |
|||
MQTT_RSSI, |
|||
MQTT_FREE_HEAP, |
|||
MQTT_HEAP_FRAG, |
|||
MQTT_SUNRISE, |
|||
MQTT_SUNSET, |
|||
MQTT_COMM_START, |
|||
MQTT_COMM_STOP, |
|||
MQTT_DIS_NIGHT_COMM, |
|||
MQTT_COMM_DISABLED, |
|||
MQTT_COMM_DIS_TS, |
|||
MQTT_VERSION, |
|||
MQTT_DEVICE, |
|||
MQTT_IP_ADDR, |
|||
MQTT_STATUS, |
|||
MQTT_ALARM, |
|||
MQTT_ALARM_START, |
|||
MQTT_ALARM_END, |
|||
MQTT_LWT_ONLINE, |
|||
MQTT_LWT_OFFLINE, |
|||
MQTT_ACK_PWR_LMT |
|||
}; |
|||
|
|||
const char* const subtopics[] PROGMEM = { |
|||
"uptime", |
|||
"wifi_rssi", |
|||
"free_heap", |
|||
"heap_frag", |
|||
"sunrise", |
|||
"sunset", |
|||
"comm_start", |
|||
"comm_stop", |
|||
"dis_night_comm", |
|||
"comm_disabled", |
|||
"comm_dis_ts", |
|||
"version", |
|||
"device", |
|||
"ip_addr", |
|||
"status", |
|||
"alarm", |
|||
"alarm_start", |
|||
"alarm_end", |
|||
"connected", |
|||
"not_connected", |
|||
"ack_pwr_limit" |
|||
}; |
|||
|
|||
enum { |
|||
MQTT_SUBS_SET_TIME |
|||
}; |
|||
|
|||
const char* const subscr[] PROGMEM = { |
|||
"setup/set_time" |
|||
}; |
|||
|
|||
#endif /*__PUB_MQTT_DEFS_H__*/ |
@ -0,0 +1,55 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __PUB_SERIAL_H__ |
|||
#define __PUB_SERIAL_H__ |
|||
|
|||
#include "../utils/dbg.h" |
|||
#include "../config/settings.h" |
|||
#include "../hm/hmSystem.h" |
|||
|
|||
template<class HMSYSTEM> |
|||
class PubSerial { |
|||
public: |
|||
PubSerial() {} |
|||
|
|||
void setup(settings_t *cfg, HMSYSTEM *sys, uint32_t *utcTs) { |
|||
mCfg = cfg; |
|||
mSys = sys; |
|||
mUtcTimestamp = utcTs; |
|||
} |
|||
|
|||
void tick(void) { |
|||
if (mCfg->serial.showIv) { |
|||
char topic[32 + MAX_NAME_LENGTH], val[40]; |
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL != iv) { |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
if (iv->isAvailable(*mUtcTimestamp)) { |
|||
DPRINTLN(DBG_INFO, "Iv: " + String(id)); |
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
if (0.0f != iv->getValue(i, rec)) { |
|||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec)); |
|||
snprintf(val, 40, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec)); |
|||
DPRINTLN(DBG_INFO, String(topic) + ": " + String(val)); |
|||
} |
|||
yield(); |
|||
} |
|||
DPRINTLN(DBG_INFO, ""); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private: |
|||
settings_t *mCfg; |
|||
HMSYSTEM *mSys; |
|||
uint32_t *mUtcTimestamp; |
|||
}; |
|||
|
|||
|
|||
#endif /*__PUB_SERIAL_H__*/ |
@ -0,0 +1,67 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://github.com/lumpapu/ahoy
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#include "helper.h" |
|||
|
|||
namespace ah { |
|||
void ip2Arr(uint8_t ip[], const char *ipStr) { |
|||
uint8_t p = 1; |
|||
memset(ip, 0, 4); |
|||
for(uint8_t i = 0; i < 16; i++) { |
|||
if(ipStr[i] == 0) |
|||
return; |
|||
if(0 == i) |
|||
ip[0] = atoi(ipStr); |
|||
else if(ipStr[i] == '.') |
|||
ip[p++] = atoi(&ipStr[i+1]); |
|||
} |
|||
} |
|||
|
|||
// note: char *str needs to be at least 16 bytes long
|
|||
void ip2Char(uint8_t ip[], char *str) { |
|||
if(0 == ip[0]) |
|||
str[0] = '\0'; |
|||
else |
|||
snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); |
|||
} |
|||
|
|||
double round3(double value) { |
|||
return (int)(value * 1000 + 0.5) / 1000.0; |
|||
} |
|||
|
|||
String getDateTimeStr(time_t t) { |
|||
char str[20]; |
|||
if(0 == t) |
|||
sprintf(str, "n/a"); |
|||
else |
|||
sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t)); |
|||
return String(str); |
|||
} |
|||
|
|||
String getTimeStr(time_t t) { |
|||
char str[9]; |
|||
if(0 == t) |
|||
sprintf(str, "n/a"); |
|||
else |
|||
sprintf(str, "%02d:%02d:%02d", hour(t), minute(t), second(t)); |
|||
return String(str); |
|||
} |
|||
|
|||
uint64_t Serial2u64(const char *val) { |
|||
char tmp[3]; |
|||
uint64_t ret = 0ULL; |
|||
uint64_t u64; |
|||
memset(tmp, 0, 3); |
|||
for(uint8_t i = 0; i < 6; i++) { |
|||
tmp[0] = val[i*2]; |
|||
tmp[1] = val[i*2 + 1]; |
|||
if((tmp[0] == '\0') || (tmp[1] == '\0')) |
|||
break; |
|||
u64 = strtol(tmp, NULL, 16); |
|||
ret |= (u64 << ((5-i) << 3)); |
|||
} |
|||
return ret; |
|||
} |
|||
} |
@ -0,0 +1,32 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __HELPER_H__ |
|||
#define __HELPER_H__ |
|||
|
|||
#include <Arduino.h> |
|||
#include <cstdint> |
|||
#include <cstring> |
|||
#include <stdio.h> |
|||
#include <stdlib.h> |
|||
#include <Timezone.h> |
|||
|
|||
static TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
|
|||
static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time
|
|||
static Timezone gTimezone(CEST, CET); |
|||
|
|||
|
|||
#define CHECK_MASK(a,b) ((a & b) == b) |
|||
|
|||
namespace ah { |
|||
void ip2Arr(uint8_t ip[], const char *ipStr); |
|||
void ip2Char(uint8_t ip[], char *str); |
|||
double round3(double value); |
|||
String getDateTimeStr(time_t t); |
|||
String getTimeStr(time_t t); |
|||
uint64_t Serial2u64(const char *val); |
|||
} |
|||
|
|||
#endif /*__HELPER_H__*/ |
@ -0,0 +1,171 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Lukas Pusch, lukas@lpusch.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __SCHEDULER_H__ |
|||
#define __SCHEDULER_H__ |
|||
|
|||
#include <functional> |
|||
#include "dbg.h" |
|||
|
|||
namespace ah { |
|||
typedef std::function<void()> scdCb; |
|||
|
|||
enum {SCD_SEC = 1, SCD_MIN = 60, SCD_HOUR = 3600, SCD_12H = 43200, SCD_DAY = 86400}; |
|||
|
|||
struct sP { |
|||
scdCb c; |
|||
uint32_t timeout; |
|||
uint32_t reload; |
|||
bool isTimestamp; |
|||
char name[6]; |
|||
sP() : c(NULL), timeout(0), reload(0), isTimestamp(false), name("\n") {} |
|||
sP(scdCb a, uint32_t tmt, uint32_t rl, bool its) : c(a), timeout(tmt), reload(rl), isTimestamp(its), name("\n") {} |
|||
}; |
|||
|
|||
#define MAX_NUM_TICKER 30 |
|||
|
|||
class Scheduler { |
|||
public: |
|||
Scheduler() {} |
|||
|
|||
void setup() { |
|||
mUptime = 0; |
|||
mTimestamp = 0; |
|||
mMax = 0; |
|||
mPrevMillis = millis(); |
|||
resetTicker(); |
|||
} |
|||
|
|||
void loop(void) { |
|||
mMillis = millis(); |
|||
mDiff = mMillis - mPrevMillis; |
|||
if (mDiff < 1000) |
|||
return; |
|||
|
|||
mDiffSeconds = 1; |
|||
if (mDiff < 2000) |
|||
mPrevMillis += 1000; |
|||
else { |
|||
if (mMillis < mPrevMillis) { // overflow
|
|||
mDiff = mMillis; |
|||
if (mDiff < 1000) |
|||
return; |
|||
} |
|||
mDiffSeconds = mDiff / 1000; |
|||
mPrevMillis += (mDiffSeconds * 1000); |
|||
} |
|||
|
|||
mUptime += mDiffSeconds; |
|||
if(0 != mTimestamp) |
|||
mTimestamp += mDiffSeconds; |
|||
checkTicker(); |
|||
|
|||
} |
|||
|
|||
void once(scdCb c, uint32_t timeout, const char *name) { addTicker(c, timeout, 0, false, name); } |
|||
void onceAt(scdCb c, uint32_t timestamp, const char *name) { addTicker(c, timestamp, 0, true, name); } |
|||
uint8_t every(scdCb c, uint32_t interval, const char *name){ return addTicker(c, interval, interval, false, name); } |
|||
|
|||
void everySec(scdCb c, const char *name) { every(c, SCD_SEC, name); } |
|||
void everyMin(scdCb c, const char *name) { every(c, SCD_MIN, name); } |
|||
void everyHour(scdCb c, const char *name) { every(c, SCD_HOUR, name); } |
|||
void every12h(scdCb c, const char *name) { every(c, SCD_12H, name); } |
|||
void everyDay(scdCb c, const char *name) { every(c, SCD_DAY, name); } |
|||
|
|||
virtual void setTimestamp(uint32_t ts) { |
|||
mTimestamp = ts; |
|||
} |
|||
|
|||
bool resetEveryById(uint8_t id) { |
|||
if (mTickerInUse[id] == false) |
|||
return false; |
|||
mTicker[id].timeout = mTicker[id].reload; |
|||
return true; |
|||
} |
|||
|
|||
uint32_t getUptime(void) { |
|||
return mUptime; |
|||
} |
|||
|
|||
uint32_t getTimestamp(void) { |
|||
return mTimestamp; |
|||
} |
|||
|
|||
inline void resetTicker(void) { |
|||
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) |
|||
mTickerInUse[i] = false; |
|||
} |
|||
|
|||
void getStat(uint8_t *max) { |
|||
*max = mMax; |
|||
} |
|||
|
|||
void printSchedulers() { |
|||
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { |
|||
if (mTickerInUse[i]) { |
|||
DPRINT(DBG_INFO, String(mTicker[i].name)); |
|||
DBGPRINT(", tmt: "); |
|||
DBGPRINT(String(mTicker[i].timeout)); |
|||
DBGPRINT(", rel: "); |
|||
DBGPRINTLN(String(mTicker[i].reload)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected: |
|||
uint32_t mTimestamp; |
|||
|
|||
private: |
|||
inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) { |
|||
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { |
|||
if (!mTickerInUse[i]) { |
|||
mTickerInUse[i] = true; |
|||
mTicker[i].c = c; |
|||
mTicker[i].timeout = timeout; |
|||
mTicker[i].reload = reload; |
|||
mTicker[i].isTimestamp = isTimestamp; |
|||
memset(mTicker[i].name, 0, 6); |
|||
strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5); |
|||
if(mMax == i) |
|||
mMax = i + 1; |
|||
return i; |
|||
} |
|||
} |
|||
return 0xff; |
|||
} |
|||
|
|||
inline void checkTicker(void) { |
|||
bool inUse[MAX_NUM_TICKER]; |
|||
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) |
|||
inUse[i] = mTickerInUse[i]; |
|||
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { |
|||
if (inUse[i]) { |
|||
if (mTicker[i].timeout <= ((mTicker[i].isTimestamp) ? mTimestamp : mDiffSeconds)) { // expired
|
|||
if(0 == mTicker[i].reload) |
|||
mTickerInUse[i] = false; |
|||
else |
|||
mTicker[i].timeout = mTicker[i].reload; |
|||
//DPRINTLN(DBG_INFO, "checkTick " + String(i) + " reload: " + String(mTicker[i].reload) + ", timeout: " + String(mTicker[i].timeout));
|
|||
(mTicker[i].c)(); |
|||
yield(); |
|||
} |
|||
else // not expired
|
|||
if (!mTicker[i].isTimestamp) |
|||
mTicker[i].timeout -= mDiffSeconds; |
|||
} |
|||
} |
|||
} |
|||
|
|||
sP mTicker[MAX_NUM_TICKER]; |
|||
bool mTickerInUse[MAX_NUM_TICKER]; |
|||
uint32_t mMillis, mPrevMillis, mDiff; |
|||
uint32_t mUptime; |
|||
uint8_t mDiffSeconds; |
|||
uint8_t mMax; |
|||
}; |
|||
} |
|||
|
|||
#endif /*__SCHEDULER_H__*/ |
@ -0,0 +1,38 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __SUN_H__ |
|||
#define __SUN_H__ |
|||
|
|||
namespace ah { |
|||
void calculateSunriseSunset(uint32_t utcTs, uint32_t offset, float lat, float lon, uint32_t *sunrise, uint32_t *sunset) { |
|||
// Source: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
|
|||
|
|||
// Julian day since 1.1.2000 12:00
|
|||
double n_JulianDay = (utcTs + offset) / 86400 - 10957.0; |
|||
// Mean solar time
|
|||
double J = n_JulianDay - lon / 360; |
|||
// Solar mean anomaly
|
|||
double M = fmod((357.5291 + 0.98560028 * J), 360); |
|||
// Equation of the center
|
|||
double C = 1.9148 * SIN(M) + 0.02 * SIN(2 * M) + 0.0003 * SIN(3 * M); |
|||
// Ecliptic longitude
|
|||
double lambda = fmod((M + C + 180 + 102.9372), 360); |
|||
// Solar transit
|
|||
double Jtransit = 2451545.0 + J + 0.0053 * SIN(M) - 0.0069 * SIN(2 * lambda); |
|||
// Declination of the sun
|
|||
double delta = ASIN(SIN(lambda) * SIN(23.44)); |
|||
// Hour angle
|
|||
double omega = ACOS((SIN(-0.83) - SIN(lat) * SIN(delta)) / (COS(lat) * COS(delta))); |
|||
// Calculate sunrise and sunset
|
|||
double Jrise = Jtransit - omega / 360; |
|||
double Jset = Jtransit + omega / 360; |
|||
// Julian sunrise/sunset to UTC unix timestamp (days incl. fraction to seconds + unix offset 1.1.2000 12:00)
|
|||
*sunrise = (Jrise - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line
|
|||
*sunset = (Jset - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line
|
|||
} |
|||
} |
|||
|
|||
#endif /*__SUN_H__*/ |
@ -0,0 +1,617 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __WEB_API_H__ |
|||
#define __WEB_API_H__ |
|||
|
|||
#include "../utils/dbg.h" |
|||
#ifdef ESP32 |
|||
#include "AsyncTCP.h" |
|||
#else |
|||
#include "ESPAsyncTCP.h" |
|||
#endif |
|||
#include "../appInterface.h" |
|||
#include "../hm/hmSystem.h" |
|||
#include "../utils/helper.h" |
|||
#include "AsyncJson.h" |
|||
#include "ESPAsyncWebServer.h" |
|||
|
|||
#if defined(F) && defined(ESP32) |
|||
#undef F |
|||
#define F(sl) (sl) |
|||
#endif |
|||
|
|||
const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; |
|||
const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR}; |
|||
|
|||
template <class HMSYSTEM> |
|||
class RestApi { |
|||
public: |
|||
RestApi() { |
|||
mTimezoneOffset = 0; |
|||
mHeapFree = 0; |
|||
mHeapFreeBlk = 0; |
|||
mHeapFrag = 0; |
|||
nr = 0; |
|||
} |
|||
|
|||
void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { |
|||
mApp = app; |
|||
mSrv = srv; |
|||
mSys = sys; |
|||
mConfig = config; |
|||
mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); |
|||
mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( |
|||
std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); |
|||
|
|||
mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); |
|||
} |
|||
|
|||
uint32_t getTimezoneOffset(void) { |
|||
return mTimezoneOffset; |
|||
} |
|||
|
|||
void ctrlRequest(JsonObject obj) { |
|||
/*char out[128];
|
|||
serializeJson(obj, out, 128); |
|||
DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ |
|||
DynamicJsonDocument json(128); |
|||
JsonObject dummy = json.as<JsonObject>(); |
|||
if(obj[F("path")] == "ctrl") |
|||
setCtrl(obj, dummy); |
|||
else if(obj[F("path")] == "setup") |
|||
setSetup(obj, dummy); |
|||
} |
|||
|
|||
private: |
|||
void onApi(AsyncWebServerRequest *request) { |
|||
mHeapFree = ESP.getFreeHeap(); |
|||
#ifndef ESP32 |
|||
mHeapFreeBlk = ESP.getMaxFreeBlockSize(); |
|||
mHeapFrag = ESP.getHeapFragmentation(); |
|||
#endif |
|||
|
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); |
|||
JsonObject root = response->getRoot(); |
|||
|
|||
String path = request->url().substring(5); |
|||
if(path == "html/system") getHtmlSystem(root); |
|||
else if(path == "html/logout") getHtmlLogout(root); |
|||
else if(path == "html/reboot") getHtmlReboot(root); |
|||
else if(path == "html/save") getHtmlSave(root); |
|||
else if(path == "system") getSysInfo(root); |
|||
else if(path == "generic") getGeneric(root); |
|||
else if(path == "reboot") getReboot(root); |
|||
else if(path == "statistics") getStatistics(root); |
|||
else if(path == "inverter/list") getInverterList(root); |
|||
else if(path == "index") getIndex(root); |
|||
else if(path == "setup") getSetup(root); |
|||
else if(path == "setup/networks") getNetworks(root); |
|||
else if(path == "live") getLive(root); |
|||
else if(path == "record/info") getRecord(root, InverterDevInform_All); |
|||
else if(path == "record/alarm") getRecord(root, AlarmData); |
|||
else if(path == "record/config") getRecord(root, SystemConfigPara); |
|||
else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); |
|||
else { |
|||
if(path.substring(0, 12) == "inverter/id/") |
|||
getInverter(root, request->url().substring(17).toInt()); |
|||
else |
|||
getNotFound(root, F("http://") + request->host() + F("/api/")); |
|||
} |
|||
|
|||
//DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage()));
|
|||
response->addHeader("Access-Control-Allow-Origin", "*"); |
|||
response->addHeader("Access-Control-Allow-Headers", "content-type"); |
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onApiPost(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, "onApiPost"); |
|||
} |
|||
|
|||
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { |
|||
DPRINTLN(DBG_VERBOSE, "onApiPostBody"); |
|||
DynamicJsonDocument json(200); |
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); |
|||
JsonObject root = response->getRoot(); |
|||
|
|||
DeserializationError err = deserializeJson(json, (const char *)data, len); |
|||
JsonObject obj = json.as<JsonObject>(); |
|||
root[F("success")] = (err) ? false : true; |
|||
if(!err) { |
|||
String path = request->url().substring(5); |
|||
if(path == "ctrl") |
|||
root[F("success")] = setCtrl(obj, root); |
|||
else if(path == "setup") |
|||
root[F("success")] = setSetup(obj, root); |
|||
else { |
|||
root[F("success")] = false; |
|||
root[F("error")] = "Path not found: " + path; |
|||
} |
|||
} |
|||
else { |
|||
switch (err.code()) { |
|||
case DeserializationError::Ok: break; |
|||
case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; |
|||
case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; |
|||
default: root[F("error")] = F("Deserialization failed"); break; |
|||
} |
|||
} |
|||
|
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
|||
|
|||
void getNotFound(JsonObject obj, String url) { |
|||
JsonObject ep = obj.createNestedObject("avail_endpoints"); |
|||
ep[F("system")] = url + F("system"); |
|||
ep[F("statistics")] = url + F("statistics"); |
|||
ep[F("inverter/list")] = url + F("inverter/list"); |
|||
ep[F("index")] = url + F("index"); |
|||
ep[F("setup")] = url + F("setup"); |
|||
ep[F("live")] = url + F("live"); |
|||
ep[F("record/info")] = url + F("record/info"); |
|||
ep[F("record/alarm")] = url + F("record/alarm"); |
|||
ep[F("record/config")] = url + F("record/config"); |
|||
ep[F("record/live")] = url + F("record/live"); |
|||
} |
|||
|
|||
|
|||
void onDwnldSetup(AsyncWebServerRequest *request) { |
|||
AsyncWebServerResponse *response; |
|||
|
|||
File fp = LittleFS.open("/settings.json", "r"); |
|||
if(!fp) { |
|||
DPRINTLN(DBG_ERROR, F("failed to load settings")); |
|||
response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); |
|||
} |
|||
else { |
|||
String tmp = fp.readString(); |
|||
int i = 0; |
|||
// remove all passwords
|
|||
while (i != -1) { |
|||
i = tmp.indexOf("\"pwd\":", i); |
|||
if(-1 != i) { |
|||
i+=7; |
|||
tmp.remove(i, tmp.indexOf("\"", i)-i); |
|||
} |
|||
} |
|||
response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); |
|||
} |
|||
|
|||
response->addHeader("Content-Type", "application/octet-stream"); |
|||
response->addHeader("Content-Description", "File Transfer"); |
|||
response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); |
|||
request->send(response); |
|||
fp.close(); |
|||
} |
|||
|
|||
void getGeneric(JsonObject obj) { |
|||
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); |
|||
obj[F("ts_uptime")] = mApp->getUptime(); |
|||
obj[F("menu_prot")] = mApp->getProtection(); |
|||
obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); |
|||
obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); |
|||
|
|||
#if defined(ESP32) |
|||
obj[F("esp_type")] = F("ESP32"); |
|||
#else |
|||
obj[F("esp_type")] = F("ESP8266"); |
|||
#endif |
|||
} |
|||
|
|||
void getSysInfo(JsonObject obj) { |
|||
obj[F("ssid")] = mConfig->sys.stationSsid; |
|||
obj[F("device_name")] = mConfig->sys.deviceName; |
|||
obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; |
|||
|
|||
obj[F("mac")] = WiFi.macAddress(); |
|||
obj[F("hostname")] = mConfig->sys.deviceName; |
|||
obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); |
|||
obj[F("prot_mask")] = mConfig->sys.protectionMask; |
|||
|
|||
obj[F("sdk")] = ESP.getSdkVersion(); |
|||
obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); |
|||
obj[F("heap_free")] = mHeapFree; |
|||
obj[F("sketch_total")] = ESP.getFreeSketchSpace(); |
|||
obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb
|
|||
getGeneric(obj); |
|||
|
|||
getRadio(obj.createNestedObject(F("radio"))); |
|||
getStatistics(obj.createNestedObject(F("statistics"))); |
|||
|
|||
#if defined(ESP32) |
|||
obj[F("heap_total")] = ESP.getHeapSize(); |
|||
obj[F("chip_revision")] = ESP.getChipRevision(); |
|||
obj[F("chip_model")] = ESP.getChipModel(); |
|||
obj[F("chip_cores")] = ESP.getChipCores(); |
|||
//obj[F("core_version")] = F("n/a");
|
|||
//obj[F("flash_size")] = F("n/a");
|
|||
//obj[F("heap_frag")] = F("n/a");
|
|||
//obj[F("max_free_blk")] = F("n/a");
|
|||
//obj[F("reboot_reason")] = F("n/a");
|
|||
#else |
|||
//obj[F("heap_total")] = F("n/a");
|
|||
//obj[F("chip_revision")] = F("n/a");
|
|||
//obj[F("chip_model")] = F("n/a");
|
|||
//obj[F("chip_cores")] = F("n/a");
|
|||
obj[F("core_version")] = ESP.getCoreVersion(); |
|||
obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb
|
|||
obj[F("heap_frag")] = mHeapFrag; |
|||
obj[F("max_free_blk")] = mHeapFreeBlk; |
|||
obj[F("reboot_reason")] = ESP.getResetReason(); |
|||
#endif |
|||
//obj[F("littlefs_total")] = LittleFS.totalBytes();
|
|||
//obj[F("littlefs_used")] = LittleFS.usedBytes();
|
|||
|
|||
uint8_t max; |
|||
mApp->getSchedulerInfo(&max); |
|||
obj[F("schMax")] = max; |
|||
} |
|||
|
|||
void getHtmlSystem(JsonObject obj) { |
|||
getSysInfo(obj.createNestedObject(F("system"))); |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>"); |
|||
} |
|||
|
|||
void getHtmlLogout(JsonObject obj) { |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
obj[F("refresh")] = 3; |
|||
obj[F("refresh_url")] = "/"; |
|||
obj[F("html")] = F("succesfully logged out"); |
|||
} |
|||
|
|||
void getHtmlReboot(JsonObject obj) { |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
obj[F("refresh")] = 20; |
|||
obj[F("refresh_url")] = "/"; |
|||
obj[F("html")] = F("rebooting ..."); |
|||
} |
|||
|
|||
void getHtmlSave(JsonObject obj) { |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
obj["pending"] = (bool)mApp->getSavePending(); |
|||
obj["success"] = (bool)mApp->getLastSaveSucceed(); |
|||
} |
|||
|
|||
void getReboot(JsonObject obj) { |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
obj[F("refresh")] = 10; |
|||
obj[F("refresh_url")] = "/"; |
|||
obj[F("html")] = F("reboot. Autoreload after 10 seconds"); |
|||
} |
|||
|
|||
void getStatistics(JsonObject obj) { |
|||
statistics_t *stat = mApp->getStatistics(); |
|||
obj[F("rx_success")] = stat->rxSuccess; |
|||
obj[F("rx_fail")] = stat->rxFail; |
|||
obj[F("rx_fail_answer")] = stat->rxFailNoAnser; |
|||
obj[F("frame_cnt")] = stat->frmCnt; |
|||
obj[F("tx_cnt")] = mSys->Radio.mSendCnt; |
|||
obj[F("retransmits")] = mSys->Radio.mRetransmits; |
|||
} |
|||
|
|||
void getInverterList(JsonObject obj) { |
|||
JsonArray invArr = obj.createNestedArray(F("inverter")); |
|||
|
|||
Inverter<> *iv; |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |
|||
iv = mSys->getInverterByPos(i); |
|||
if(NULL != iv) { |
|||
JsonObject obj2 = invArr.createNestedObject(); |
|||
obj2[F("enabled")] = (bool)iv->config->enabled; |
|||
obj2[F("id")] = i; |
|||
obj2[F("name")] = String(iv->config->name); |
|||
obj2[F("serial")] = String(iv->config->serial.u64, HEX); |
|||
obj2[F("channels")] = iv->channels; |
|||
obj2[F("version")] = String(iv->getFwVersion()); |
|||
|
|||
for(uint8_t j = 0; j < iv->channels; j ++) { |
|||
obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j]; |
|||
obj2[F("ch_name")][j] = iv->config->chName[j]; |
|||
obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; |
|||
} |
|||
} |
|||
} |
|||
obj[F("interval")] = String(mConfig->nrf.sendInterval); |
|||
obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld); |
|||
obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; |
|||
obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; |
|||
obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; |
|||
obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; |
|||
} |
|||
|
|||
void getInverter(JsonObject obj, uint8_t id) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if(NULL != iv) { |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
obj[F("id")] = id; |
|||
obj[F("enabled")] = (bool)iv->config->enabled; |
|||
obj[F("name")] = String(iv->config->name); |
|||
obj[F("serial")] = String(iv->config->serial.u64, HEX); |
|||
obj[F("version")] = String(iv->getFwVersion()); |
|||
obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); |
|||
obj[F("ts_last_success")] = rec->ts; |
|||
|
|||
JsonArray ch = obj.createNestedArray("ch"); |
|||
|
|||
// AC
|
|||
uint8_t pos; |
|||
obj[F("ch_name")][0] = "AC"; |
|||
JsonArray ch0 = ch.createNestedArray(); |
|||
for (uint8_t fld = 0; fld < sizeof(acList); fld++) { |
|||
pos = (iv->getPosByChFld(CH0, acList[fld], rec)); |
|||
ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; |
|||
} |
|||
|
|||
// DC
|
|||
for(uint8_t j = 0; j < iv->channels; j ++) { |
|||
obj[F("ch_name")][j+1] = iv->config->chName[j]; |
|||
obj[F("ch_max_pwr")][j+1] = iv->config->chMaxPwr[j]; |
|||
JsonArray cur = ch.createNestedArray(); |
|||
for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { |
|||
pos = (iv->getPosByChFld((j+1), dcList[fld], rec)); |
|||
cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
void getMqtt(JsonObject obj) { |
|||
obj[F("broker")] = String(mConfig->mqtt.broker); |
|||
obj[F("port")] = String(mConfig->mqtt.port); |
|||
obj[F("user")] = String(mConfig->mqtt.user); |
|||
obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); |
|||
obj[F("topic")] = String(mConfig->mqtt.topic); |
|||
obj[F("interval")] = String(mConfig->mqtt.interval); |
|||
} |
|||
|
|||
void getNtp(JsonObject obj) { |
|||
obj[F("addr")] = String(mConfig->ntp.addr); |
|||
obj[F("port")] = String(mConfig->ntp.port); |
|||
} |
|||
|
|||
void getSun(JsonObject obj) { |
|||
obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; |
|||
obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; |
|||
obj[F("disnightcom")] = mConfig->sun.disNightCom; |
|||
obj[F("offs")] = mConfig->sun.offsetSec; |
|||
} |
|||
|
|||
void getPinout(JsonObject obj) { |
|||
obj[F("cs")] = mConfig->nrf.pinCs; |
|||
obj[F("ce")] = mConfig->nrf.pinCe; |
|||
obj[F("irq")] = mConfig->nrf.pinIrq; |
|||
obj[F("sclk")] = mConfig->nrf.pinSclk; |
|||
obj[F("mosi")] = mConfig->nrf.pinMosi; |
|||
obj[F("miso")] = mConfig->nrf.pinMiso; |
|||
obj[F("led0")] = mConfig->led.led0; |
|||
obj[F("led1")] = mConfig->led.led1; |
|||
} |
|||
|
|||
void getRadio(JsonObject obj) { |
|||
obj[F("power_level")] = mConfig->nrf.amplifierPower; |
|||
obj[F("isconnected")] = mSys->Radio.isChipConnected(); |
|||
obj[F("DataRate")] = mSys->Radio.getDataRate(); |
|||
obj[F("isPVariant")] = mSys->Radio.isPVariant(); |
|||
} |
|||
|
|||
void getSerial(JsonObject obj) { |
|||
obj[F("interval")] = (uint16_t)mConfig->serial.interval; |
|||
obj[F("show_live_data")] = mConfig->serial.showIv; |
|||
obj[F("debug")] = mConfig->serial.debug; |
|||
} |
|||
|
|||
void getStaticIp(JsonObject obj) { |
|||
char buf[16]; |
|||
ah::ip2Char(mConfig->sys.ip.ip, buf); obj[F("ip")] = String(buf); |
|||
ah::ip2Char(mConfig->sys.ip.mask, buf); obj[F("mask")] = String(buf); |
|||
ah::ip2Char(mConfig->sys.ip.dns1, buf); obj[F("dns1")] = String(buf); |
|||
ah::ip2Char(mConfig->sys.ip.dns2, buf); obj[F("dns2")] = String(buf); |
|||
ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf); |
|||
} |
|||
|
|||
void getDisplay(JsonObject obj) { |
|||
obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; |
|||
obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; |
|||
obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift; |
|||
obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; |
|||
obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; |
|||
obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; |
|||
obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data; |
|||
obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; |
|||
obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; |
|||
obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; |
|||
obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; |
|||
} |
|||
|
|||
void getIndex(JsonObject obj) { |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
obj[F("ts_now")] = mApp->getTimestamp(); |
|||
obj[F("ts_sunrise")] = mApp->getSunrise(); |
|||
obj[F("ts_sunset")] = mApp->getSunset(); |
|||
obj[F("ts_offset")] = mConfig->sun.offsetSec; |
|||
obj[F("disNightComm")] = mConfig->sun.disNightCom; |
|||
|
|||
JsonArray inv = obj.createNestedArray(F("inverter")); |
|||
Inverter<> *iv; |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |
|||
iv = mSys->getInverterByPos(i); |
|||
if(NULL != iv) { |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
JsonObject invObj = inv.createNestedObject(); |
|||
invObj[F("enabled")] = (bool)iv->config->enabled; |
|||
invObj[F("id")] = i; |
|||
invObj[F("name")] = String(iv->config->name); |
|||
invObj[F("version")] = String(iv->getFwVersion()); |
|||
invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp()); |
|||
invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp()); |
|||
invObj[F("ts_last_success")] = iv->getLastTs(rec); |
|||
} |
|||
} |
|||
|
|||
JsonArray warn = obj.createNestedArray(F("warnings")); |
|||
if(!mSys->Radio.isChipConnected()) |
|||
warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); |
|||
else if(!mSys->Radio.isPVariant()) |
|||
warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); |
|||
if(!mApp->getSettingsValid()) |
|||
warn.add(F("your settings are invalid")); |
|||
if(mApp->getRebootRequestState()) |
|||
warn.add(F("reboot your ESP to apply all your configuration changes")); |
|||
if(0 == mApp->getTimestamp()) |
|||
warn.add(F("time not set. No communication to inverter possible")); |
|||
/*if(0 == mSys->getNumInverters())
|
|||
warn.add(F("no inverter configured"));*/ |
|||
|
|||
if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0)) |
|||
warn.add(F("MQTT is not connected")); |
|||
|
|||
JsonArray info = obj.createNestedArray(F("infos")); |
|||
if(mApp->getMqttIsConnected()) |
|||
info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); |
|||
if(mConfig->mqtt.interval > 0) |
|||
info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds")); |
|||
} |
|||
|
|||
void getSetup(JsonObject obj) { |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
getSysInfo(obj.createNestedObject(F("system"))); |
|||
//getInverterList(obj.createNestedObject(F("inverter")));
|
|||
getMqtt(obj.createNestedObject(F("mqtt"))); |
|||
getNtp(obj.createNestedObject(F("ntp"))); |
|||
getSun(obj.createNestedObject(F("sun"))); |
|||
getPinout(obj.createNestedObject(F("pinout"))); |
|||
getRadio(obj.createNestedObject(F("radio"))); |
|||
getSerial(obj.createNestedObject(F("serial"))); |
|||
getStaticIp(obj.createNestedObject(F("static_ip"))); |
|||
getDisplay(obj.createNestedObject(F("display"))); |
|||
} |
|||
|
|||
void getNetworks(JsonObject obj) { |
|||
mApp->getAvailNetworks(obj); |
|||
} |
|||
|
|||
void getLive(JsonObject obj) { |
|||
getGeneric(obj.createNestedObject(F("generic"))); |
|||
obj[F("refresh")] = mConfig->nrf.sendInterval; |
|||
|
|||
for (uint8_t fld = 0; fld < sizeof(acList); fld++) { |
|||
obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); |
|||
obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); |
|||
} |
|||
for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { |
|||
obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); |
|||
obj[F("fld_names")][fld] = String(fields[dcList[fld]]); |
|||
} |
|||
|
|||
Inverter<> *iv; |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |
|||
iv = mSys->getInverterByPos(i); |
|||
bool parse = false; |
|||
if(NULL != iv) |
|||
parse = iv->config->enabled; |
|||
obj[F("iv")][i] = parse; |
|||
} |
|||
} |
|||
|
|||
void getRecord(JsonObject obj, uint8_t recType) { |
|||
JsonArray invArr = obj.createNestedArray(F("inverter")); |
|||
|
|||
Inverter<> *iv; |
|||
record_t<> *rec; |
|||
uint8_t pos; |
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |
|||
iv = mSys->getInverterByPos(i); |
|||
if(NULL != iv) { |
|||
rec = iv->getRecordStruct(recType); |
|||
JsonArray obj2 = invArr.createNestedArray(); |
|||
for(uint8_t j = 0; j < rec->length; j++) { |
|||
byteAssign_t *assign = iv->getByteAssign(j, rec); |
|||
pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); |
|||
obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; |
|||
obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; |
|||
obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { |
|||
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); |
|||
bool accepted = true; |
|||
if(NULL == iv) { |
|||
jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>(); |
|||
return false; |
|||
} |
|||
|
|||
if(F("power") == jsonIn[F("cmd")]) |
|||
accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); |
|||
else if(F("restart") == jsonIn[F("restart")]) |
|||
accepted = iv->setDevControlRequest(Restart); |
|||
else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) { |
|||
iv->powerLimit[0] = jsonIn["val"]; |
|||
if(F("limit_persistent_relative") == jsonIn[F("cmd")]) |
|||
iv->powerLimit[1] = RelativPersistent; |
|||
else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) |
|||
iv->powerLimit[1] = AbsolutPersistent; |
|||
else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")]) |
|||
iv->powerLimit[1] = RelativNonPersistent; |
|||
else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) |
|||
iv->powerLimit[1] = AbsolutNonPersistent; |
|||
|
|||
accepted = iv->setDevControlRequest(ActivePowerContr); |
|||
} |
|||
else if(F("dev") == jsonIn[F("cmd")]) { |
|||
DPRINTLN(DBG_INFO, F("dev cmd")); |
|||
iv->enqueCommand<InfoCommand>(jsonIn[F("val")].as<int>()); |
|||
} |
|||
else { |
|||
jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'"; |
|||
return false; |
|||
} |
|||
|
|||
if(!accepted) { |
|||
jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); |
|||
return false; |
|||
} else |
|||
mApp->ivSendHighPrio(iv); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { |
|||
if(F("scan_wifi") == jsonIn[F("cmd")]) |
|||
mApp->scanAvailNetworks(); |
|||
else if(F("set_time") == jsonIn[F("cmd")]) |
|||
mApp->setTimestamp(jsonIn[F("val")]); |
|||
else if(F("sync_ntp") == jsonIn[F("cmd")]) |
|||
mApp->setTimestamp(0); // 0: update ntp flag
|
|||
else if(F("serial_utc_offset") == jsonIn[F("cmd")]) |
|||
mTimezoneOffset = jsonIn[F("val")]; |
|||
else if(F("discovery_cfg") == jsonIn[F("cmd")]) { |
|||
mApp->setMqttDiscoveryFlag(); // for homeassistant
|
|||
} else { |
|||
jsonOut[F("error")] = F("unknown cmd"); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
IApp *mApp; |
|||
HMSYSTEM *mSys; |
|||
AsyncWebServer *mSrv; |
|||
settings_t *mConfig; |
|||
|
|||
uint32_t mTimezoneOffset; |
|||
uint32_t mHeapFree, mHeapFreeBlk; |
|||
uint8_t mHeapFrag; |
|||
uint16_t nr; |
|||
}; |
|||
|
|||
#endif /*__WEB_API_H__*/ |
@ -0,0 +1,57 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>About</title> |
|||
{#HTML_HEADER} |
|||
</head> |
|||
<body> |
|||
{#HTML_NAV} |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<div class="my-3"><h2>About AhoyDTU</h2></div> |
|||
<div class="my-3"> |
|||
<div class="row my-3 head"> |
|||
<div class="p-2">Used Libraries</div> |
|||
</div> |
|||
<div class="row"><a href="https://github.com/bertmelis/espMqttClient" target="_blank">bertmelis/espMqttClient</a></div> |
|||
<div class="row"><a href="https://github.com/yubox-node-org/ESPAsyncWebServer" target="_blank">yubox-node-org/ESPAsyncWebServer</a></div> |
|||
<div class="row"><a href="https://github.com/bblanchon/ArduinoJson" target="_blank">bblanchon/ArduinoJson</a></div> |
|||
<div class="row"><a href="https://github.com/nrf24/RF24" target="_blank">nrf24/RF24</a></div> |
|||
<div class="row"><a href="https://github.com/paulstoffregen/Time" target="_blank">paulstoffregen/Time</a></div> |
|||
<div class="row"><a href="https://github.com/olikraus/U8g2" target="_blank">olikraus/U8g2</a></div> |
|||
<div class="row"><a href="https://github.com/zinggjm/GxEPD2" target="_blank">zinggjm/GxEPD2</a></div> |
|||
|
|||
<div class="row my-3 head"> |
|||
<div class="p-2">Contact Information</div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-5 col-sm-3">Github Repository</div> |
|||
<div class="col-7 col-sm-9"><a href="https://github.com/lumapu/ahoy">https://github.com/lumapu/ahoy</a></div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-5 col-sm-3">Discord Chat</div> |
|||
<div class="col-7 col-sm-9"><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-5 col-sm-3">E-Mail</div> |
|||
<div class="col-7 col-sm-9"><a href="mailto:contact@ahoydtu.de">contact@ahoydtu.de</a></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
<script type="text/javascript"> |
|||
function parseGeneric(obj) { |
|||
parseNav(obj); |
|||
parseESP(obj); |
|||
parseRssi(obj); |
|||
} |
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
parseGeneric(obj["generic"]); |
|||
} |
|||
} |
|||
getAjax("/api/html/save", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,265 @@ |
|||
/** |
|||
* SVG ICONS |
|||
*/ |
|||
|
|||
iconWifi1 = [ |
|||
"M11.046 10.454c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.708-.707z" |
|||
]; |
|||
|
|||
iconWifi2 = [ |
|||
"M13.229 8.271c.216-.216.194-.578-.063-.745A9.456 9.456 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.577 1.336c.205.132.48.108.652-.065zm-2.183 2.183c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.408.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .708 0l.707-.707z" |
|||
]; |
|||
|
|||
iconWifi3 = [ |
|||
"M15.384 6.115a.485.485 0 0 0-.047-.736A12.444 12.444 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.518.518 0 0 0 .668.05A11.448 11.448 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049z", |
|||
"M13.229 8.271a.482.482 0 0 0-.063-.745A9.455 9.455 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065zm-2.183 2.183c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.61-.091l.016-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.707-.707z" |
|||
]; |
|||
|
|||
iconWarn = [ |
|||
"M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z", |
|||
"M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z" |
|||
]; |
|||
|
|||
iconInfo = [ |
|||
"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z", |
|||
"m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" |
|||
]; |
|||
|
|||
iconSuccess = [ |
|||
"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z", |
|||
"M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" |
|||
]; |
|||
|
|||
/** |
|||
* GENERIC FUNCTIONS |
|||
*/ |
|||
function ml(tagName, ...args) { |
|||
var el = document.createElement(tagName); |
|||
if(args[0]) { |
|||
for(var name in args[0]) { |
|||
if(name.indexOf("on") === 0) { |
|||
el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false) |
|||
} else { |
|||
el.setAttribute(name, args[0][name]); |
|||
} |
|||
} |
|||
} |
|||
if (!args[1]) { |
|||
return el; |
|||
} |
|||
return nester(el, args[1]) |
|||
} |
|||
|
|||
function nester(el, n) { |
|||
if (typeof n === "string") { |
|||
var t = document.createTextNode(n); |
|||
el.appendChild(t); |
|||
} else if (n instanceof Array) { |
|||
for(var i = 0; i < n.length; i++) { |
|||
if (typeof n[i] === "string") { |
|||
var t = document.createTextNode(n[i]); |
|||
el.appendChild(t); |
|||
} else if (n[i] instanceof Node){ |
|||
el.appendChild(n[i]); |
|||
} |
|||
} |
|||
} else if (n instanceof Node){ |
|||
el.appendChild(n) |
|||
} |
|||
return el; |
|||
} |
|||
|
|||
function topnav() { |
|||
toggle("topnav", "mobile"); |
|||
} |
|||
|
|||
function parseNav(obj) { |
|||
for(i = 0; i < 11; i++) { |
|||
if(i == 2) |
|||
continue; |
|||
var l = document.getElementById("nav"+i); |
|||
if(window.location.pathname == "/" + l.href.split('/').pop()) |
|||
l.classList.add("active"); |
|||
|
|||
if(obj["menu_protEn"]) { |
|||
if(obj["menu_prot"]) { |
|||
if(0 == i) |
|||
l.classList.remove("hide"); |
|||
else if(i > 2) { |
|||
if(((obj["menu_mask"] >> (i-2)) & 0x01) == 0x00) |
|||
l.classList.remove("hide"); |
|||
} |
|||
} else if(0 != i) |
|||
l.classList.remove("hide"); |
|||
} else if(i > 1) |
|||
l.classList.remove("hide"); |
|||
} |
|||
} |
|||
|
|||
function parseVersion(obj) { |
|||
document.getElementById("version").appendChild( |
|||
link("https://github.com/lumapu/ahoy/commits/" + obj["build"], "Git SHA: " + obj["build"] + " :: " + obj["version"], "_blank") |
|||
); |
|||
} |
|||
|
|||
function parseESP(obj) { |
|||
document.getElementById("esp_type").append( |
|||
document.createTextNode("Board: " + obj["esp_type"]) |
|||
); |
|||
} |
|||
|
|||
function parseRssi(obj) { |
|||
var icon = iconWifi3; |
|||
if(obj["wifi_rssi"] <= -80) |
|||
icon = iconWifi1; |
|||
else if(obj["wifi_rssi"] <= -70) |
|||
icon = iconWifi2; |
|||
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"])); |
|||
} |
|||
|
|||
function setHide(id, hide) { |
|||
var elm = document.getElementById(id); |
|||
if(hide) { |
|||
if(!elm.classList.contains("hide")) |
|||
elm.classList.add("hide"); |
|||
} |
|||
else |
|||
elm.classList.remove('hide'); |
|||
} |
|||
|
|||
function toggle(id, cl="hide") { |
|||
var e = document.getElementById(id); |
|||
if(!e.classList.contains(cl)) |
|||
e.classList.add(cl); |
|||
else |
|||
e.classList.remove(cl); |
|||
} |
|||
|
|||
function getAjax(url, ptr, method="GET", json=null) { |
|||
var xhr = new XMLHttpRequest(); |
|||
if(xhr != null) { |
|||
xhr.open(method, url, true); |
|||
xhr.onreadystatechange = p; |
|||
if("POST" == method) |
|||
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); |
|||
xhr.send(json); |
|||
} |
|||
function p() { |
|||
if(xhr.readyState == 4) { |
|||
if(null != xhr.responseText) { |
|||
if(null != ptr) |
|||
ptr(JSON.parse(xhr.responseText)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* CREATE DOM FUNCTIONS |
|||
*/ |
|||
|
|||
function des(val) { |
|||
e = document.createElement('p'); |
|||
e.classList.add("subdes"); |
|||
e.innerHTML = val; |
|||
return e; |
|||
} |
|||
|
|||
function lbl(htmlfor, val, cl=null, id=null) { |
|||
e = document.createElement('label'); |
|||
e.htmlFor = htmlfor; |
|||
e.innerHTML = val; |
|||
if(null != cl) e.classList.add(...cl); |
|||
if(null != id) e.id = id; |
|||
return e; |
|||
} |
|||
|
|||
function inp(name, val, max=32, cl=["text"], id=null, type=null, pattern=null, title=null, checked=null) { |
|||
e = document.createElement('input'); |
|||
e.classList.add(...cl); |
|||
e.name = name; |
|||
if(null != val) e.value = val; |
|||
if(null != max) e.maxLength = max; |
|||
if(null != id) e.id = id; |
|||
if(null != type) e.type = type; |
|||
if(null != pattern) e.pattern = pattern; |
|||
if(null != title) e.title = title; |
|||
if(null != checked) e.checked = checked; |
|||
return e; |
|||
} |
|||
|
|||
function sel(name, options, selId) { |
|||
e = document.createElement('select'); |
|||
e.name = name; |
|||
for(it of options) { |
|||
o = opt(it[0], it[1], (it[0] == selId)); |
|||
e.appendChild(o); |
|||
} |
|||
return e; |
|||
} |
|||
|
|||
function selDelAllOpt(sel) { |
|||
var i, l = sel.options.length - 1; |
|||
for(i = l; i >= 0; i--) { |
|||
sel.remove(i); |
|||
} |
|||
} |
|||
|
|||
function opt(val, html, sel=false) { |
|||
o = document.createElement('option'); |
|||
o.value = val; |
|||
o.innerHTML = html; |
|||
if(sel) |
|||
o.selected = true; |
|||
return o; |
|||
} |
|||
|
|||
function div(cl, h=null) { |
|||
e = document.createElement('div'); |
|||
e.classList.add(...cl); |
|||
if(null != h) e.innerHTML = h; |
|||
return e; |
|||
} |
|||
|
|||
function span(val, cl=null, id=null) { |
|||
e = document.createElement('span'); |
|||
e.innerHTML = val; |
|||
if(null != cl) e.classList.add(...cl); |
|||
if(null != id) e.id = id; |
|||
return e; |
|||
} |
|||
|
|||
function br() { |
|||
return document.createElement('br'); |
|||
} |
|||
|
|||
function link(dst, text, target=null) { |
|||
var a = document.createElement('a'); |
|||
var t = document.createTextNode(text); |
|||
a.href = dst; |
|||
if(null != target) |
|||
a.target = target; |
|||
a.appendChild(t); |
|||
return a; |
|||
} |
|||
|
|||
function svg(data=null, w=24, h=24, cl=null, tooltip=null) { |
|||
var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); |
|||
s.setAttribute('width', w); |
|||
s.setAttribute('height', h); |
|||
s.setAttribute('viewBox', '0 0 16 16'); |
|||
if(null != cl) s.setAttribute('class', cl); |
|||
if(null != data) { |
|||
for(const e of data) { |
|||
const i = document.createElementNS('http://www.w3.org/2000/svg', 'path'); |
|||
i.setAttribute('d', e); |
|||
s.appendChild(i); |
|||
} |
|||
} |
|||
if(null != tooltip) { |
|||
const t = document.createElement("title"); |
|||
t.appendChild(document.createTextNode(tooltip)); |
|||
s.appendChild(t); |
|||
} |
|||
return s; |
|||
} |
@ -0,0 +1,27 @@ |
|||
:root { |
|||
--bg: #fff; |
|||
--fg: #000; |
|||
--fg2: #fff; |
|||
|
|||
--info: #0000dd; |
|||
--warn: #ff7700; |
|||
--success: #009900; |
|||
|
|||
--input-bg: #eee; |
|||
|
|||
--nav-bg: #333; |
|||
--primary: #006ec0; |
|||
--primary-hover: #044e86; |
|||
--secondary: #0072c8; |
|||
--nav-active: #555; |
|||
--footer-bg: #282828; |
|||
|
|||
--total-head-title: #8e5903; |
|||
--total-bg: #b06e04; |
|||
--iv-head-title: #1c6800; |
|||
--iv-head-bg: #32b004; |
|||
--ch-head-title: #003c80; |
|||
--ch-head-bg: #006ec0; |
|||
--ts-head: #333; |
|||
--ts-bg: #555; |
|||
} |
@ -0,0 +1,27 @@ |
|||
:root { |
|||
--bg: #222; |
|||
--fg: #ccc; |
|||
--fg2: #fff; |
|||
|
|||
--info: #0072c8; |
|||
--warn: #ffaa00; |
|||
--success: #00bb00; |
|||
|
|||
--input-bg: #333; |
|||
|
|||
--nav-bg: #333; |
|||
--primary: #004d87; |
|||
--primary-hover: #023155; |
|||
--secondary: #0072c8; |
|||
--nav-active: #555; |
|||
--footer-bg: #282828; |
|||
|
|||
--total-head-title: #555511; |
|||
--total-bg: #666622; |
|||
--iv-head-title: #115511; |
|||
--iv-head-bg: #226622; |
|||
--ch-head-title: #112255; |
|||
--ch-head-bg: #223366; |
|||
--ts-head: #333; |
|||
--ts-bg: #555; |
|||
} |
@ -0,0 +1,148 @@ |
|||
import re |
|||
import os |
|||
import gzip |
|||
import glob |
|||
import shutil |
|||
from datetime import date |
|||
from pathlib import Path |
|||
import subprocess |
|||
|
|||
|
|||
def get_git_sha(): |
|||
try: |
|||
return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip() |
|||
except: |
|||
return "0000000" |
|||
|
|||
def readVersion(path): |
|||
f = open(path, "r") |
|||
lines = f.readlines() |
|||
f.close() |
|||
|
|||
today = date.today() |
|||
search = ["_MAJOR", "_MINOR", "_PATCH"] |
|||
version = today.strftime("%y%m%d") + "_ahoy_" |
|||
ver = "" |
|||
for line in lines: |
|||
if(line.find("VERSION_") != -1): |
|||
for s in search: |
|||
p = line.find(s) |
|||
if(p != -1): |
|||
version += line[p+13:].rstrip() + "." |
|||
ver += line[p+13:].rstrip() + "." |
|||
return ver[:-1] |
|||
|
|||
def htmlParts(file, header, nav, footer, version): |
|||
p = ""; |
|||
f = open(file, "r") |
|||
lines = f.readlines() |
|||
f.close(); |
|||
|
|||
f = open(header, "r") |
|||
h = f.read().strip() |
|||
f.close() |
|||
|
|||
f = open(nav, "r") |
|||
n = f.read().strip() |
|||
f.close() |
|||
|
|||
f = open(footer, "r") |
|||
fo = f.read().strip() |
|||
f.close() |
|||
|
|||
for line in lines: |
|||
line = line.replace("{#HTML_HEADER}", h) |
|||
line = line.replace("{#HTML_NAV}", n) |
|||
line = line.replace("{#HTML_FOOTER}", fo) |
|||
p += line |
|||
|
|||
#placeholders |
|||
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>' |
|||
p = p.replace("{#VERSION}", version) |
|||
p = p.replace("{#VERSION_GIT}", link) |
|||
f = open("tmp/" + file, "w") |
|||
f.write(p); |
|||
f.close(); |
|||
return p |
|||
|
|||
def convert2Header(inFile, version): |
|||
fileType = inFile.split(".")[1] |
|||
define = inFile.split(".")[0].upper() |
|||
define2 = inFile.split(".")[1].upper() |
|||
inFileVarName = inFile.replace(".", "_") |
|||
|
|||
if os.getcwd()[-4:] != "html": |
|||
outName = "html/" + "h/" + inFileVarName + ".h" |
|||
inFile = "html/" + inFile |
|||
Path("html/h").mkdir(exist_ok=True) |
|||
else: |
|||
outName = "h/" + inFileVarName + ".h" |
|||
|
|||
data = "" |
|||
if fileType == "ico": |
|||
f = open(inFile, "rb") |
|||
data = f.read() |
|||
f.close() |
|||
else: |
|||
if fileType == "html": |
|||
data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version) |
|||
else: |
|||
f = open(inFile, "r") |
|||
data = f.read() |
|||
f.close() |
|||
|
|||
if fileType == "css": |
|||
data = data.replace('\n', '') |
|||
data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css |
|||
|
|||
length = len(data) |
|||
|
|||
f = open(outName, "w") |
|||
f.write("#ifndef __{}_{}_H__\n".format(define, define2)) |
|||
f.write("#define __{}_{}_H__\n".format(define, define2)) |
|||
|
|||
if fileType == "ico": |
|||
zipped = gzip.compress(bytes(data)) |
|||
else: |
|||
zipped = gzip.compress(bytes(data, 'utf-8')) |
|||
zippedStr = "" |
|||
for i in range(len(zipped)): |
|||
zippedStr += "0x{:02x}".format(zipped[i]) #hex(zipped[i]) |
|||
if (i + 1) != len(zipped): |
|||
zippedStr += ", " |
|||
if (i + 1) % 16 == 0 and i != 0: |
|||
zippedStr += "\n" |
|||
f.write("#define {}_len {}\n".format(inFileVarName, len(zipped))) |
|||
f.write("const uint8_t {}[] PROGMEM = {{\n{}}};\n".format(inFileVarName, zippedStr)) |
|||
f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) |
|||
f.close() |
|||
|
|||
# delete all files in the 'h' dir |
|||
wd = 'h' |
|||
if os.getcwd()[-4:] != "html": |
|||
wd = "web/html/" + wd |
|||
|
|||
if os.path.exists(wd): |
|||
for f in os.listdir(wd): |
|||
os.remove(os.path.join(wd, f)) |
|||
wd += "/tmp" |
|||
if os.path.exists(wd): |
|||
for f in os.listdir(wd): |
|||
os.remove(os.path.join(wd, f)) |
|||
|
|||
# grab all files with following extensions |
|||
if os.getcwd()[-4:] != "html": |
|||
os.chdir('./web/html') |
|||
types = ('*.html', '*.css', '*.js', '*.ico') # the tuple of file types |
|||
files_grabbed = [] |
|||
for files in types: |
|||
files_grabbed.extend(glob.glob(files)) |
|||
|
|||
Path("h").mkdir(exist_ok=True) |
|||
Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements |
|||
shutil.copyfile("style.css", "tmp/style.css") |
|||
version = readVersion("../../defines.h") |
|||
|
|||
# go throw the array |
|||
for val in files_grabbed: |
|||
convert2Header(val, version) |
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,16 @@ |
|||
<div id="footer"> |
|||
<div class="left"> |
|||
<a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2023</a> |
|||
<ul> |
|||
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> |
|||
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="right"> |
|||
<ul> |
|||
<li>{#VERSION_GIT}</li> |
|||
<li id="esp_type"></li> |
|||
<li><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed" target="_blank" >CC BY-NC-SA 4.0</a></li> |
|||
</ul> |
|||
</div> |
|||
</div> |
@ -0,0 +1,5 @@ |
|||
<link rel="stylesheet" type="text/css" href="colors.css"/> |
|||
<link rel="stylesheet" type="text/css" href="style.css"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<meta charset="UTF-8"> |
|||
<script type="text/javascript" src="api.js"></script> |
@ -0,0 +1,24 @@ |
|||
<div class="topnav"> |
|||
<a href="/" class="title">AhoyDTU</a> |
|||
<a href="javascript:void(0);" class="icon" onclick="topnav()"> |
|||
<span></span> |
|||
<span></span> |
|||
<span></span> |
|||
</a> |
|||
<div id="topnav" class="mobile"> |
|||
<a id="nav3" class="hide" href="/live">Live</a> |
|||
<a id="nav4" class="hide" href="/serial">Serial / Control</a> |
|||
<a id="nav5" class="hide" href="/setup">Settings</a> |
|||
<span class="seperator"></span> |
|||
<a id="nav6" class="hide" href="/update">Update</a> |
|||
<a id="nav7" class="hide" href="/system">System</a> |
|||
<span class="seperator"></span> |
|||
<a id="nav8" href="/api" target="_blank">REST API</a> |
|||
<a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a> |
|||
<a id="nav10" href="/about">About</a> |
|||
<span class="seperator"></span> |
|||
<a id="nav0" class="hide" href="/login">Login</a> |
|||
<a id="nav1" class="hide" href="/logout">Logout</a> |
|||
</div> |
|||
<div id="wifiicon" class="info"></div> |
|||
</div> |
@ -0,0 +1,225 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Index</title> |
|||
{#HTML_HEADER} |
|||
</head> |
|||
<body> |
|||
{#HTML_NAV} |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<p> |
|||
<span class="des">Uptime: </span><span id="uptime"></span><br/> |
|||
<span class="des">ESP-Time: </span><span id="date"></span> |
|||
</p> |
|||
<p> |
|||
<span class="des">System Infos:</span> |
|||
<div id="iv"></div> |
|||
<div class="hr"></div> |
|||
<div id="warn_info"></div> |
|||
</p> |
|||
|
|||
<div class="hr"></div> |
|||
<div id="note"> |
|||
<h3>Support this project:</h3> |
|||
<ul> |
|||
<li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">Changelog</a></li> |
|||
<li>Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a></li> |
|||
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li> |
|||
<li>Contribute to <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">documentation</a></li> |
|||
<li><a href="https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip" target="_blank">Download</a> & Test development firmware, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">Development Changelog</a></li> |
|||
<li>make a <a href="https://paypal.me/lupusch" target="_blank">donation</a></li> |
|||
</ul> |
|||
<p class="lic"> |
|||
This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
<script type="text/javascript"> |
|||
var exeOnce = true; |
|||
var tickCnt = 0; |
|||
var ts = 0; |
|||
var commInfo = ""; |
|||
var release = null; |
|||
|
|||
function apiCb(obj) { |
|||
var e = document.getElementById("apiResult"); |
|||
if(obj["success"]) { |
|||
e.innerHTML = " command excuted"; |
|||
getAjax("/api/index", parse); |
|||
} |
|||
else |
|||
e.innerHTML = " Error: " + obj["error"]; |
|||
} |
|||
|
|||
function setTime() { |
|||
var date = new Date(); |
|||
var obj = new Object(); |
|||
obj.cmd = "set_time"; |
|||
obj.val = parseInt(date.getTime() / 1000); |
|||
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function ts2Span(ts) { |
|||
return span(new Date(ts * 1000).toLocaleString('de-DE')); |
|||
} |
|||
|
|||
function parseGeneric(obj) { |
|||
if(exeOnce) |
|||
parseESP(obj); |
|||
parseRssi(obj); |
|||
} |
|||
|
|||
function parseSys(obj) { |
|||
ts = obj["ts_now"]; |
|||
var date = new Date(obj["ts_now"] * 1000); |
|||
var up = obj["generic"]["ts_uptime"]; |
|||
var days = parseInt(up / 86400) % 365; |
|||
var hrs = parseInt(up / 3600) % 24; |
|||
var min = parseInt(up / 60) % 60; |
|||
var sec = up % 60; |
|||
var e = document.getElementById("uptime"); |
|||
e.innerHTML = days + " Day"; |
|||
if(1 != days) |
|||
e.innerHTML += "s"; |
|||
e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":" |
|||
+ ("0"+min).substr(-2) + ":" |
|||
+ ("0"+sec).substr(-2); |
|||
var dSpan = document.getElementById("date"); |
|||
if(0 != obj["ts_now"]) |
|||
dSpan.innerHTML = date.toLocaleString('de-DE'); |
|||
else { |
|||
dSpan.innerHTML = ""; |
|||
var e = inp("set", "sync from browser", 0, ["btn"], "set", "button"); |
|||
dSpan.appendChild(span("NTP timeserver unreachable. ")); |
|||
dSpan.appendChild(e); |
|||
dSpan.appendChild(span("", ["span"], "apiResult")); |
|||
e.addEventListener("click", setTime); |
|||
} |
|||
|
|||
if(obj["disNightComm"]) { |
|||
if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"]) |
|||
&& ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) { |
|||
commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); |
|||
} |
|||
else { |
|||
commInfo = "Night time, inverter polling disabled, "; |
|||
if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) { |
|||
commInfo += "paused at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); |
|||
} |
|||
else { |
|||
commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
function parseIv(obj) { |
|||
var p = div(["none"]); |
|||
for(var i of obj) { |
|||
var icon = iconWarn; |
|||
var cl = "icon-warn"; |
|||
avail = ""; |
|||
if(false == i["enabled"]) { |
|||
avail = "disabled"; |
|||
} |
|||
else if(false == i["is_avail"]) { |
|||
icon = iconInfo; |
|||
cl = "icon-info"; |
|||
avail = "not yet available"; |
|||
} |
|||
else if(0 == i["ts_last_success"]) { |
|||
icon = iconSuccess; |
|||
avail = "available but no data was received until now"; |
|||
} |
|||
else { |
|||
icon = iconSuccess; |
|||
avail = "available and is "; |
|||
if(false == i["is_producing"]) |
|||
avail += "not "; |
|||
else |
|||
cl = "icon-success"; |
|||
avail += "producing"; |
|||
} |
|||
|
|||
p.append( |
|||
svg(icon, 30, 30, "icon " + cl), |
|||
span("Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is " + avail), |
|||
br() |
|||
); |
|||
|
|||
if(false == i["is_avail"]) { |
|||
if(i["ts_last_success"] > 0) { |
|||
var date = new Date(i["ts_last_success"] * 1000); |
|||
p.append(span("-> last successful transmission: " + date.toLocaleString('de-DE')), br()); |
|||
} |
|||
} |
|||
} |
|||
document.getElementById("iv").replaceChildren(p); |
|||
} |
|||
|
|||
function parseWarnInfo(warn, success) { |
|||
var p = div(["none"]); |
|||
for(var w of warn) { |
|||
p.append(svg(iconWarn, 30, 30, "icon icon-warn"), span(w), br()); |
|||
} |
|||
for(var i of success) { |
|||
p.append(svg(iconSuccess, 30, 30, "icon icon-success"), span(i), br()); |
|||
} |
|||
|
|||
if(commInfo.length > 0) |
|||
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span(commInfo), br()); |
|||
|
|||
if(null != release) { |
|||
if(getVerInt("{#VERSION}") < getVerInt(release)) |
|||
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Update available, current released version: " + release), br()); |
|||
else if(getVerInt("{#VERSION}") > getVerInt(release)) |
|||
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br()); |
|||
else |
|||
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using the current stable release: " + release), br()); |
|||
} |
|||
|
|||
document.getElementById("warn_info").replaceChildren(p); |
|||
} |
|||
|
|||
function tick() { |
|||
if(0 != ts) |
|||
document.getElementById("date").innerHTML = (new Date((++ts) * 1000)).toLocaleString('de-DE'); |
|||
if(++tickCnt >= 10) { |
|||
tickCnt = 0; |
|||
getAjax('/api/index', parse); |
|||
} |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
if(exeOnce) |
|||
parseNav(obj["generic"]); |
|||
parseGeneric(obj["generic"]); |
|||
parseSys(obj); |
|||
parseIv(obj["inverter"]); |
|||
parseWarnInfo(obj["warnings"], obj["infos"]); |
|||
if(exeOnce) { |
|||
window.setInterval("tick()", 1000); |
|||
exeOnce = false; |
|||
getAjax("https://api.github.com/repos/lumapu/ahoy/releases/latest", parseRelease); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
function getVerInt(ver) { |
|||
var a = ver.split('.'); |
|||
return (a[0] << 24) | (a[1] << 16) | a[2]; |
|||
} |
|||
|
|||
function parseRelease(obj) { |
|||
release = obj["name"].substring(6); |
|||
} |
|||
|
|||
getAjax("/api/index", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,23 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Login</title> |
|||
{#HTML_HEADER} |
|||
</head> |
|||
<body> |
|||
<div id="wrapper"> |
|||
<div id="login"> |
|||
<div class="p-4"> |
|||
<form action="/login" method="post"> |
|||
<div class="row"><h2>AhoyDTU</h2></div> |
|||
<div class="row"> |
|||
<div class="col-8"><input type="password" name="pwd" autofocus></div> |
|||
<div class="col-4"><input type="submit" name="login" value="login" class="btn"></div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
</body> |
|||
</html> |
@ -0,0 +1,51 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Save</title> |
|||
{#HTML_HEADER} |
|||
</head> |
|||
<body> |
|||
{#HTML_NAV} |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<div id="html" class="mt-3 mb-3"></div> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
<script type="text/javascript"> |
|||
function parseGeneric(obj) { |
|||
parseNav(obj); |
|||
parseESP(obj); |
|||
parseRssi(obj); |
|||
} |
|||
|
|||
function parseHtml(obj) { |
|||
var html = ""; |
|||
if(obj.pending) |
|||
html = "saving settings ..."; |
|||
else { |
|||
if(obj.success) |
|||
html = "settings successfully saved"; |
|||
else |
|||
html = "failed saving settings"; |
|||
|
|||
var meta = document.createElement('meta'); |
|||
meta.httpEquiv = "refresh" |
|||
meta.content = 1 + "; URL=/setup"; |
|||
document.getElementsByTagName('head')[0].appendChild(meta); |
|||
} |
|||
document.getElementById("html").innerHTML = html; |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
parseGeneric(obj["generic"]); |
|||
parseHtml(obj); |
|||
window.setInterval("getAjax('/api/html/save', parse)", 1100); |
|||
} |
|||
} |
|||
|
|||
getAjax("/api/html/save", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,806 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Setup</title> |
|||
{#HTML_HEADER} |
|||
<script type="text/javascript"> |
|||
function load() { |
|||
for(it of document.getElementsByClassName("s_collapsible")) { |
|||
it.addEventListener("click", function() { |
|||
this.classList.toggle("active"); |
|||
var content = this.nextElementSibling; |
|||
content.style.display = (content.style.display === "block") ? "none" : "block"; |
|||
}); |
|||
} |
|||
} |
|||
</script> |
|||
</head> |
|||
<body onload="load()"> |
|||
{#HTML_NAV} |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<form method="post" action="/save"> |
|||
<button type="button" class="s_collapsible mt-4">System Config</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-2"> |
|||
<legend class="des">Device Host Name</legend> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3">Device Name</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="device"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3">Dark Mode</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div> |
|||
</div> |
|||
</fieldset> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">System Config</legend> |
|||
<p class="des">Pinout</p> |
|||
<div id="pinout"></div> |
|||
|
|||
<p class="des">Radio (NRF24L01+)</p> |
|||
<div id="rf24"></div> |
|||
|
|||
<p class="des">Serial Console</p> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3">print inverter data</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="serEn"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3">Serial Debug</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="serDbg"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Interval [s]</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="serIntvl" pattern="[0-9]+" title="Invalid input"/></div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">Network</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-2"> |
|||
<legend class="des">WiFi</legend> |
|||
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p> |
|||
|
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Search Networks</div> |
|||
<div class="col-12 col-sm-9"><input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/></div> |
|||
</div> |
|||
|
|||
<div class="row mb-2 mb-sm-3"> |
|||
<div class="col-12 col-sm-3 my-2">Avail Networks</div> |
|||
<div class="col-12 col-sm-9"> |
|||
<select name="networks" id="networks" onChange="selNet()"> |
|||
<option value="-1" selected disabled hidden>not scanned</option> |
|||
</select> |
|||
</div> |
|||
</div> |
|||
<div class="row mb-2 mb-sm-3"> |
|||
<div class="col-12 col-sm-3 my-2">SSID</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ssid"/></div> |
|||
</div> |
|||
<div class="row mb-2 mb-sm-3"> |
|||
<div class="col-12 col-sm-3 my-2">Password</div> |
|||
<div class="col-12 col-sm-9"><input type="password" name="pwd" value="{PWD}"/></div> |
|||
</div> |
|||
</fieldset> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">Static IP (optional)</legend> |
|||
<p> |
|||
Leave fields blank for DHCP<br/> |
|||
The following fields are parsed in this format: 192.168.4.1 |
|||
</p> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">IP Address</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ipAddr" maxlength="15" /></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Submask</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ipMask" maxlength="15" /></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">DNS 1</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ipDns1" maxlength="15" /></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">DNS 2</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ipDns2" maxlength="15" /></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Gateway</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ipGateway" maxlength="15" /></div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">Protection</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des mx-2">Protection</legend> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 mb-2 mt-2">Admin Password</div> |
|||
<div class="col-12 col-sm-9"><input type="password" name="adminpwd" value="{PWD}"/></div> |
|||
</div> |
|||
<p>Select pages which should be protected by password</p> |
|||
<div id="prot_mask"></div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">Inverter</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">Inverter</legend> |
|||
<div id="inverter"></div> |
|||
<div class="row mb-2"> |
|||
<div class="col-12 col-sm-3"></div> |
|||
<div class="col-12 col-sm-9"><input type="button" id="btnAdd" class="btn" value="Add Inverter"/></div> |
|||
</div> |
|||
<div class="row mb-2"> |
|||
<div class="col-12 col-sm-3"><p class="subdes">Note</p></div> |
|||
<div class="col-12 col-sm-9"><p>A 'max module power' value of '0' disables the channel in 'live' view</p></div> |
|||
</div> |
|||
<div class="row mb-2"> |
|||
<div class="col-12 col-sm-3"><p class="subdes">General</p></div> |
|||
<div class="col-12 col-sm-9"></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Interval [s]</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Max retries per Payload</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="invRetry"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3 mb-2">Reset values and YieldDay at midnight</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstMid"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3 mb-2">Reset values when inverter polling pauses at sunset</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstComStop"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3">Reset values when inverter status is 'not available'</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstNotAvail"/></div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">NTP Server</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">NTP Server</legend> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">NTP Server / IP</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ntpAddr"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">NTP Port</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="ntpPort"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">set system time</div> |
|||
<div class="col-12 col-sm-9"> |
|||
<input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/> |
|||
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/> |
|||
<span id="apiResultNtp"></span> |
|||
</div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">Sunrise & Sunset</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">Sunrise & Sunset</legend> |
|||
<p>Use a decimal separator: '.' (dot) for Latitude and Longitude</p> |
|||
|
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Latitude (decimal)</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="sunLat"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Longitude (decimal)</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="sunLon"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Offset (pre sunrise, post sunset)</div> |
|||
<div class="col-12 col-sm-9"><select name="sunOffs"></select></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3">Pause polling inverters during night</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="sunDisNightCom"/></div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">MQTT</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">MQTT</legend> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Broker / Server IP</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="mqttAddr"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Port</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="mqttPort"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Username (optional)</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="mqttUser"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Password (optional)</div> |
|||
<div class="col-12 col-sm-9"><input type="password" name="mqttPwd"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Topic</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" /></div> |
|||
</div> |
|||
<p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Interval [s]</div> |
|||
<div class="col-12 col-sm-9"><input type="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" /></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div> |
|||
<div class="col-12 col-sm-9"> |
|||
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/> |
|||
<span id="apiResultMqtt"></span> |
|||
</div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">Display Config</button> |
|||
<div class="s_content"> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">Display Config</legend> |
|||
<div id="dispType"></div> |
|||
<div id="dispRot"></div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3">Turn off while inverters are offline</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pwr"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-8 col-sm-3">Enable Screensaver (pixel shifting, OLED only)</div> |
|||
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pxshift"/></div> |
|||
</div> |
|||
<div class="row mb-3"> |
|||
<div class="col-12 col-sm-3 my-2">Luminance</div> |
|||
<div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="100"></select></div> |
|||
</div> |
|||
<p class="des">Pinout</p> |
|||
<div id="dispPins"></div> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<div class="row mb-4 mt-4"> |
|||
<div class="col-8 col-sm-3">Reboot device after successful save</div> |
|||
<div class="col-4 col-sm-9"> |
|||
<input type="checkbox" name="reboot" checked /> |
|||
<input type="submit" value="save" class="btn right"/> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
<div class="hr mb-3 mt-3"></div> |
|||
<div class="mb-4 mt-4"> |
|||
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a> |
|||
<fieldset class="mb-4"> |
|||
<legend class="des">Import / Export JSON Settings</legend> |
|||
<div class="row mb-4 mt-4"> |
|||
<div class="col-12 col-sm-3 my-2">Import</div> |
|||
<div class="col-12 col-sm-9"> |
|||
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8"> |
|||
<input type="file" name="upload"> |
|||
<input type="button" class="btn" value="Import" onclick="hide()"> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
<div class="row mb-4 mt-4"> |
|||
<div class="col-12 col-sm-3 my-2">Export</div> |
|||
<div class="col-12 col-sm-9"> |
|||
<a class="btn" href="/get_setup" target="_blank">Export settings (JSON file)</a><span> (only values, passwords will be removed!)</span> |
|||
</div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
<script type="text/javascript"> |
|||
var highestId = 0; |
|||
var maxInv = 0; |
|||
|
|||
var esp8266pins = [ |
|||
[255, "off / default"], |
|||
[0, "D3 (GPIO0)"], |
|||
[1, "TX (GPIO1)"], |
|||
[2, "D4 (GPIO2)"], |
|||
[3, "RX (GPIO3)"], |
|||
[4, "D2 (GPIO4, SDA)"], |
|||
[5, "D1 (GPIO5, SCL)"], |
|||
[6, "GPIO6"], |
|||
[7, "GPIO7"], |
|||
[8, "GPIO8"], |
|||
[9, "GPIO9"], |
|||
[10, "GPIO10"], |
|||
[11, "GPIO11"], |
|||
[12, "D6 (GPIO12)"], |
|||
[13, "D7 (GPIO13)"], |
|||
[14, "D5 (GPIO14)"], |
|||
[15, "D8 (GPIO15)"], |
|||
[16, "D0 (GPIO16 - no IRQ!)"] |
|||
]; |
|||
var esp32pins = [ |
|||
[255, "off / default"], |
|||
[0, "GPIO0"], |
|||
[1, "TX (GPIO1)"], |
|||
[2, "GPIO2 (LED)"], |
|||
[3, "RX (GPIO3)"], |
|||
[4, "GPIO4"], |
|||
[5, "GPIO5"], |
|||
[12, "GPIO12"], |
|||
[13, "GPIO13"], |
|||
[14, "GPIO14"], |
|||
[15, "GPIO15"], |
|||
[16, "GPIO16"], |
|||
[17, "GPIO17"], |
|||
[18, "GPIO18"], |
|||
[19, "GPIO19"], |
|||
[21, "GPIO21 (SDA)"], |
|||
[22, "GPIO22 (SCL)"], |
|||
[23, "GPIO23"], |
|||
[25, "GPIO25"], |
|||
[26, "GPIO26"], |
|||
[27, "GPIO27"], |
|||
[32, "GPIO32"], |
|||
[33, "GPIO33"], |
|||
[34, "GPIO34"], |
|||
[35, "GPIO35"], |
|||
[36, "VP (GPIO36)"], |
|||
[39, "VN (GPIO39)"] |
|||
]; |
|||
var esp32s3pins = [ |
|||
[255, "off / default"], |
|||
[0, "GPIO0 (DONT USE - BOOT)"], |
|||
[1, "GPIO1"], |
|||
[2, "GPIO2"], |
|||
[3, "GPIO3"], |
|||
[4, "GPIO4"], |
|||
[5, "GPIO5"], |
|||
[6, "GPIO6"], |
|||
[7, "GPIO7"], |
|||
[8, "GPIO8"], |
|||
[9, "GPIO9"], |
|||
[10, "GPIO10"], |
|||
[11, "GPIO11"], |
|||
[12, "GPIO12"], |
|||
[13, "GPIO13"], |
|||
[14, "GPIO14"], |
|||
[15, "GPIO15"], |
|||
[16, "GPIO16"], |
|||
[17, "GPIO17"], |
|||
[18, "GPIO18"], |
|||
[19, "GPIO19 (DONT USE - USB-)"], |
|||
[20, "GPIO20 (DONT USE - USB+)"], |
|||
[21, "GPIO21"], |
|||
[26, "GPIO26 (PSRAM - not available)"], |
|||
[27, "GPIO27 (FLASH - not available)"], |
|||
[28, "GPIO28 (FLASH - not available)"], |
|||
[29, "GPIO29 (FLASH - not available)"], |
|||
[30, "GPIO30 (FLASH - not available)"], |
|||
[31, "GPIO31 (FLASH - not available)"], |
|||
[32, "GPIO32 (FLASH - not available)"], |
|||
[33, "GPIO33 (not exposed on WROOM modules)"], |
|||
[34, "GPIO34 (not exposed on WROOM modules)"], |
|||
[35, "GPIO35"], |
|||
[36, "GPIO36"], |
|||
[37, "GPIO37"], |
|||
[38, "GPIO38"], |
|||
[39, "GPIO39"], |
|||
[40, "GPIO40"], |
|||
[41, "GPIO41"], |
|||
[42, "GPIO42"], |
|||
[43, "GPIO43"], |
|||
[44, "GPIO44"], |
|||
[45, "GPIO45 (DONT USE - STRAPPING PIN)"], |
|||
[46, "GPIO46 (DONT USE - STRAPPING PIN)"], |
|||
[47, "GPIO47"], |
|||
[48, "GPIO48"], |
|||
]; |
|||
|
|||
const re = /11[2,4,6]1.*/; |
|||
|
|||
document.getElementById("btnAdd").addEventListener("click", function() { |
|||
if(highestId <= (maxInv-1)) { |
|||
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_pwr":[0,0,0,0],"ch_name":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId); |
|||
} |
|||
}); |
|||
|
|||
function apiCbWifi(obj) { |
|||
var e = document.getElementById("networks"); |
|||
selDelAllOpt(e); |
|||
if(obj["success"]) |
|||
e.appendChild(opt("-1", "scanning ...")) |
|||
else |
|||
e.appendChild(opt("-1", "Error: " + obj["error"])); |
|||
} |
|||
|
|||
function apiCbNtp(obj) { |
|||
var e = document.getElementById("apiResultNtp"); |
|||
if(obj["success"]) |
|||
e.innerHTML = "command excuted"; |
|||
else |
|||
e.innerHTML = "Error: " + obj["error"]; |
|||
} |
|||
|
|||
function apiCbMqtt(obj) { |
|||
var e = document.getElementById("apiResultMqtt"); |
|||
if(obj["success"]) |
|||
e.innerHTML = "command excuted"; |
|||
else |
|||
e.innerHTML = "Error: " + obj["error"]; |
|||
} |
|||
|
|||
function setTime() { |
|||
var date = new Date(); |
|||
var obj = new Object(); |
|||
obj.cmd = "set_time"; |
|||
obj.val = parseInt(date.getTime() / 1000); |
|||
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function scan() { |
|||
var obj = new Object(); |
|||
obj.cmd = "scan_wifi"; |
|||
getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj)); |
|||
setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000); |
|||
} |
|||
|
|||
function syncTime() { |
|||
var obj = new Object(); |
|||
obj.cmd = "sync_ntp"; |
|||
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function sendDiscoveryConfig() { |
|||
var obj = new Object(); |
|||
obj.cmd = "discovery_cfg"; |
|||
getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function hide() { |
|||
document.getElementById("form").submit(); |
|||
var e = document.getElementById("content"); |
|||
e.replaceChildren(span("upload started")); |
|||
} |
|||
|
|||
function delIv() { |
|||
var id = this.id.substring(0,4); |
|||
var e = document.getElementsByName(id + "Addr")[0]; |
|||
e.value = ""; |
|||
e.dispatchEvent(new Event("keyup")); |
|||
e.dispatchEvent(new Event("change")); |
|||
document.getElementsByName(id + "Name")[0].value = ""; |
|||
} |
|||
|
|||
function mlCb(id, des, chk=false) { |
|||
var cb = ml("input", {type: "checkbox", id: id, name: id}, ""); |
|||
if(chk) |
|||
cb.checked = true; |
|||
return ml("div", {class: "row mb-3"}, [ |
|||
ml("div", {class: "col-8 col-sm-3"}, des), |
|||
ml("div", {class: "col-4 col-sm-9"}, cb) |
|||
]); |
|||
} |
|||
|
|||
function mlE(des, e) { |
|||
return ml("div", {class: "row mb-3"}, [ |
|||
ml("div", {class: "col-12 col-sm-3 my-2"}, des), |
|||
ml("div", {class: "col-12 col-sm-9"}, e) |
|||
]); |
|||
} |
|||
|
|||
function ivHtml(obj, id) { |
|||
highestId = id + 1; |
|||
if(highestId == maxInv) |
|||
setHide("btnAdd", true); |
|||
|
|||
var iv = document.getElementById("inverter"); |
|||
iv.appendChild(des("Inverter " + id)); |
|||
id = "inv" + id; |
|||
|
|||
var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input"); |
|||
iv.append( |
|||
mlCb(id + "Enable", "Communication Enable", obj["enabled"]), |
|||
mlE("Serial Number (12 digits)*", addr) |
|||
); |
|||
|
|||
['keyup', 'change'].forEach(function(evt) { |
|||
addr.addEventListener(evt, (e) => { |
|||
var serial = addr.value.substring(0,4); |
|||
var max = 0; |
|||
for(var i=0;i<4;i++) { |
|||
setHide(id+"ModPwr"+i, true); |
|||
setHide(id+"ModName"+i, true); |
|||
setHide(id+"YieldCor"+i, true); |
|||
} |
|||
setHide("row"+id+"ModPwr", true); |
|||
setHide("row"+id+"ModName", true); |
|||
setHide("row"+id+"YieldCor", true); |
|||
|
|||
if(serial.charAt(0) == 1) { |
|||
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) { |
|||
if((serial.charAt(3) == 1) || (serial.charAt(3) == 2)) { |
|||
switch(serial.charAt(2)) { |
|||
case "2": max = 1; break; |
|||
case "4": max = 2; break; |
|||
case "6": max = 4; break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if(max != 0) { |
|||
for(var i=0;i<max;i++) { |
|||
setHide(id+"ModPwr"+i, false); |
|||
setHide(id+"ModName"+i, false); |
|||
setHide(id+"YieldCor"+i, false); |
|||
} |
|||
setHide("row"+id+"ModPwr", false); |
|||
setHide("row"+id+"ModName", false); |
|||
setHide("row"+id+"YieldCor", false); |
|||
} |
|||
}) |
|||
}); |
|||
|
|||
iv.append(mlE("Name*", inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input"))); |
|||
|
|||
for(var j of [ |
|||
["ModPwr", "ch_max_pwr", "Max Module Power (Wp)", 4, "[0-9]+"], |
|||
["ModName", "ch_name", "Module Name", 15, null], |
|||
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 8, "[0-9-]+"]]) { |
|||
|
|||
var cl = (re.test(obj["serial"])) ? "" : " hide"; |
|||
|
|||
i = 0; |
|||
arrIn = []; |
|||
for(it of obj[j[1]]) { |
|||
arrIn.push(ml("div", {class: "col-3 "}, |
|||
inp(id + j[0] + i, it, j[3], [], id + j[0] + i, "text", j[4], "Invalid input") |
|||
)); |
|||
i++; |
|||
} |
|||
|
|||
iv.append( |
|||
ml("div", {class: "row mb-2 mb-sm-3" + cl, id: "row" + id + j[0]}, [ |
|||
ml("div", {class: "col-12 col-sm-3 my-2"}, j[2]), |
|||
ml("div", {class: "col-12 col-sm-9"}, |
|||
ml("div", {class: "row"}, arrIn) |
|||
) |
|||
]) |
|||
); |
|||
} |
|||
|
|||
var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button"); |
|||
del.addEventListener("click", delIv); |
|||
iv.append(mlE("Delete", del)); |
|||
} |
|||
|
|||
function ivGlob(obj) { |
|||
for(var i of [["invInterval", "interval"], ["invRetry", "retries"]]) |
|||
document.getElementsByName(i[0])[0].value = obj[i[1]]; |
|||
for(var i of [["Mid", "rstMid"], ["ComStop", "rstComStop"], ["NotAvail", "rstNAvail"]]) |
|||
document.getElementsByName("invRst"+i[0])[0].checked = obj[i[1]]; |
|||
} |
|||
|
|||
function parseSys(obj) { |
|||
for(var i of [["device", "device_name"], ["ssid", "ssid"]]) |
|||
document.getElementsByName(i[0])[0].value = obj[i[1]]; |
|||
document.getElementsByName("darkMode")[0].checked = obj["dark_mode"]; |
|||
e = document.getElementsByName("adminpwd")[0]; |
|||
if(!obj["pwd_set"]) |
|||
e.value = ""; |
|||
var d = document.getElementById("prot_mask"); |
|||
var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"]; |
|||
var el = []; |
|||
for(var i = 0; i < 6; i++) { |
|||
var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i)); |
|||
el.push(mlCb("protMask" + i, a[i], chk)) |
|||
} |
|||
d.append(...el); |
|||
} |
|||
|
|||
function parseGeneric(obj) { |
|||
parseNav(obj); |
|||
parseESP(obj); |
|||
parseRssi(obj); |
|||
} |
|||
|
|||
function parseStaticIp(obj) { |
|||
for(var i of [["ipAddr", "ip"], ["ipMask", "mask"], ["ipDns1", "dns1"], ["ipDns2", "dns2"], ["ipGateway", "gateway"]]) |
|||
if(null != obj[i[1]]) |
|||
document.getElementsByName(i[0])[0].value = obj[i[1]]; |
|||
} |
|||
|
|||
function parseIv(obj) { |
|||
for(var i = 0; i < obj.inverter.length; i++) |
|||
ivHtml(obj.inverter[i], i); |
|||
ivGlob(obj); |
|||
maxInv = obj["max_num_inverters"]; |
|||
} |
|||
|
|||
function parseMqtt(obj) { |
|||
for(var i of [["Addr", "broker"], ["Port", "port"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]]) |
|||
document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]]; |
|||
} |
|||
|
|||
function parseNtp(obj) { |
|||
for(var i of [["ntpAddr", "addr"], ["ntpPort", "port"]]) |
|||
document.getElementsByName(i[0])[0].value = obj[i[1]]; |
|||
} |
|||
|
|||
function parseSun(obj) { |
|||
document.getElementsByName("sunLat")[0].value = obj["lat"]; |
|||
document.getElementsByName("sunLon")[0].value = obj["lon"]; |
|||
document.getElementsByName("sunDisNightCom")[0].checked = obj["disnightcom"]; |
|||
const sel = document.getElementsByName("sunOffs")[0]; |
|||
for(var i = 0; i <= 60; i++) { |
|||
sel.appendChild(opt(i, i + " minutes", (i == (obj["offs"] / 60)))); |
|||
} |
|||
} |
|||
|
|||
function parsePinout(obj, type, system) { |
|||
var e = document.getElementById("pinout"); |
|||
if ("ESP8266" == type) { |
|||
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']]; |
|||
} else { |
|||
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso'], ['led0', 'pinLed0'], ['led1', 'pinLed1']]; |
|||
} |
|||
for(p of pins) { |
|||
e.append( |
|||
ml("div", {class: "row mb-3"}, [ |
|||
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), |
|||
ml("div", {class: "col-12 col-sm-9"}, |
|||
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[0]]) |
|||
) |
|||
]) |
|||
); |
|||
} |
|||
} |
|||
|
|||
function parseRadio(obj) { |
|||
var e = document.getElementById("rf24").append( |
|||
ml("div", {class: "row mb-3"}, [ |
|||
ml("div", {class: "col-12 col-sm-3 my-2"}, "Power Level"), |
|||
ml("div", {class: "col-12 col-sm-9"}, |
|||
sel("rf24Power", [ |
|||
[0, "MIN"], |
|||
[1, "LOW"], |
|||
[2, "HIGH"], |
|||
[3, "MAX"] |
|||
], obj["power_level"]) |
|||
) |
|||
]) |
|||
); |
|||
} |
|||
|
|||
function parseSerial(obj) { |
|||
for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"]]) |
|||
document.getElementsByName(i[0])[0].checked = obj[i[1]]; |
|||
document.getElementsByName("serIntvl")[0].value = obj["interval"]; |
|||
} |
|||
|
|||
function parseDisplay(obj, type, system) { |
|||
for(var i of ["disp_pwr", "disp_pxshift"]) |
|||
document.getElementsByName(i)[0].checked = obj[i]; |
|||
|
|||
var e = document.getElementById("dispPins"); |
|||
var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']]; |
|||
if("ESP32" == type) |
|||
pins.push(['busy', 'disp_bsy']); |
|||
for(p of pins) { |
|||
e.append( |
|||
ml("div", {class: "row mb-3", id: "row_" + p[1]}, [ |
|||
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), |
|||
ml("div", {class: "col-12 col-sm-9"}, |
|||
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[1]]) |
|||
) |
|||
]) |
|||
); |
|||
} |
|||
|
|||
var opts = [[0, "None"], [1, "SSD1306 0.96\""], [2, "SH1106 1.3\""], [3, "Nokia5110"]]; |
|||
if("ESP32" == type) |
|||
opts.push([10, "ePaper"]); |
|||
var dispType = sel("disp_typ", opts, obj["disp_typ"]); |
|||
document.getElementById("dispType").append( |
|||
ml("div", {class: "row mb-3"}, [ |
|||
ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"), |
|||
ml("div", {class: "col-12 col-sm-9"}, dispType) |
|||
]) |
|||
); |
|||
dispType.addEventListener('change', (e) => { |
|||
hideDispPins(pins, e.target.value) |
|||
}); |
|||
|
|||
opts = [[0, "0°"], [2, "180°"]]; |
|||
if("ESP32" == type) { |
|||
opts.push([1, "90°"]); |
|||
opts.push([3, "270°"]); |
|||
} |
|||
document.getElementById("dispRot").append( |
|||
ml("div", {class: "row mb-3"}, [ |
|||
ml("div", {class: "col-12 col-sm-3 my-2"}, "Rotation"), |
|||
ml("div", {class: "col-12 col-sm-9"}, sel("disp_rot", opts, obj["disp_rot"])) |
|||
]) |
|||
); |
|||
|
|||
document.getElementsByName("disp_cont")[0].value = obj["disp_cont"]; |
|||
hideDispPins(pins, obj.disp_typ); |
|||
} |
|||
|
|||
function hideDispPins(pins, dispType) { |
|||
for(var i = 0; i < pins.length; i++) { |
|||
var cl = document.getElementById("row_" + pins[i][1]).classList; |
|||
|
|||
if(0 == dispType) |
|||
cl.add("hide"); |
|||
else if(dispType <= 2) { // OLED |
|||
if(i < 2) |
|||
cl.remove("hide"); |
|||
else |
|||
cl.add("hide"); |
|||
} else if(dispType == 3) { // Nokia |
|||
if(i < 4) |
|||
cl.remove("hide"); |
|||
else |
|||
cl.add("hide"); |
|||
} else // ePaper |
|||
cl.remove("hide"); |
|||
} |
|||
} |
|||
|
|||
function parse(root) { |
|||
if(null != root) { |
|||
parseSys(root["system"]); |
|||
parseGeneric(root["generic"]); |
|||
parseStaticIp(root["static_ip"]); |
|||
parseMqtt(root["mqtt"]); |
|||
parseNtp(root["ntp"]); |
|||
parseSun(root["sun"]); |
|||
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]); |
|||
parseRadio(root["radio"]); |
|||
parseSerial(root["serial"]); |
|||
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); |
|||
getAjax("/api/inverter/list", parseIv); |
|||
} |
|||
} |
|||
|
|||
function listNetworks(root) { |
|||
var s = document.getElementById("networks"); |
|||
selDelAllOpt(s); |
|||
if(root["networks"].length > 0) { |
|||
s.appendChild(opt("-1", "please select network")); |
|||
for(i = 0; i < root["networks"].length; i++) { |
|||
s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)")); |
|||
} |
|||
} |
|||
else |
|||
s.appendChild(opt("-1", "no network found")); |
|||
} |
|||
|
|||
function selNet() { |
|||
var s = document.getElementById("networks"); |
|||
var e = document.getElementsByName("ssid")[0]; |
|||
if(-1 != s.value) |
|||
e.value = s.value; |
|||
} |
|||
|
|||
getAjax("/api/setup", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,633 @@ |
|||
html, body { |
|||
font-family: Arial; |
|||
margin: 0; |
|||
padding: 0; |
|||
height: 100%; |
|||
min-height: 100%; |
|||
background-color: var(--bg); |
|||
color: var(--fg); |
|||
} |
|||
|
|||
h2 { |
|||
padding-left: 10px; |
|||
} |
|||
|
|||
span, li, h3, label, fieldset { |
|||
color: var(--fg); |
|||
} |
|||
|
|||
fieldset, input[type=submit], .btn { |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
#live span { |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.topnav { |
|||
background-color: var(--nav-bg); |
|||
position: fixed; |
|||
top: 0; |
|||
width: 100%; |
|||
} |
|||
|
|||
.topnav a { |
|||
color: var(--fg2); |
|||
padding: 14px 14px; |
|||
text-decoration: none; |
|||
font-size: 17px; |
|||
display: block; |
|||
} |
|||
|
|||
#topnav a { |
|||
color: #fff; |
|||
} |
|||
|
|||
.topnav a.icon { |
|||
top: 0; |
|||
left: 0; |
|||
background: var(--nav-bg); |
|||
display: block; |
|||
position: absolute; |
|||
} |
|||
|
|||
.topnav a:hover { |
|||
background-color: var(--primary-hover) !important; |
|||
} |
|||
|
|||
.topnav .info { |
|||
color: var(--fg2); |
|||
position: absolute; |
|||
right: 24px; |
|||
top: 5px; |
|||
} |
|||
|
|||
.topnav .mobile { |
|||
display: none; |
|||
} |
|||
|
|||
svg.icon { |
|||
vertical-align: middle; |
|||
display: inline-block; |
|||
margin-top:-4x; |
|||
padding: 5px 7px 5px 0px; |
|||
} |
|||
|
|||
.icon-info { |
|||
fill: var(--info); |
|||
} |
|||
|
|||
.icon-warn { |
|||
fill: var(--warn); |
|||
} |
|||
|
|||
.icon-success { |
|||
fill: var(--success); |
|||
} |
|||
|
|||
.wifi { |
|||
fill: var(--fg2); |
|||
} |
|||
|
|||
.title { |
|||
background-color: var(--primary); |
|||
color: #fff !important; |
|||
padding-left: 80px !important |
|||
} |
|||
|
|||
.topnav .icon span { |
|||
display: block; |
|||
width: 30px; |
|||
height: 3px; |
|||
margin-bottom: 5px; |
|||
position: relative; |
|||
background: #fff; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
.topnav .active { |
|||
background-color: var(--nav-active); |
|||
} |
|||
|
|||
span.seperator { |
|||
width: 100%; |
|||
height: 1px; |
|||
margin: 5px 0px 5px; |
|||
background-color: #494949; |
|||
display: block; |
|||
} |
|||
|
|||
#content { |
|||
max-width: 1140px; |
|||
} |
|||
|
|||
.total-h { |
|||
background-color: var(--total-head-title); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.total-bg { |
|||
background-color: var(--total-bg); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.iv-h { |
|||
background-color: var(--iv-head-title); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.iv-bg { |
|||
background-color: var(--iv-head-bg); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.ch-h { |
|||
background-color: var(--ch-head-title); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.ch-bg { |
|||
background-color: var(--ch-head-bg); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.ts-h { |
|||
background-color: var(--ts-head); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.ts-bg { |
|||
background-color: var(--ts-bg); |
|||
color: var(--fg2); |
|||
} |
|||
|
|||
.hr { |
|||
border-top: 1px solid var(--iv-head-title); |
|||
margin: 1rem 0 1rem; |
|||
} |
|||
|
|||
p { |
|||
text-align: justify; |
|||
font-size: 13pt; |
|||
color: var(--fg); |
|||
} |
|||
|
|||
#footer { |
|||
background-color: var(--footer-bg); |
|||
} |
|||
|
|||
.row { display: flex; max-width: 100%; flex-wrap: wrap; } |
|||
.col { flex: 1 0 0%; } |
|||
|
|||
.col-1, .col-2, .col-3, .col-4, |
|||
.col-5, .col-6, .col-7, .col-8, |
|||
.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; } |
|||
|
|||
.col-1 { width: 8.333333333%; } |
|||
.col-2 { width: 16.66666667%; } |
|||
.col-3 { width: 25%; } |
|||
.col-4 { width: 33.33333333%; } |
|||
.col-5 { width: 41.66666667%; } |
|||
.col-6 { width: 50%; } |
|||
.col-7 { width: 58.33333333%; } |
|||
.col-8 { width: 66.66666667%; } |
|||
.col-9 { width: 75%; } |
|||
.col-10 { width: 83.33333333%; } |
|||
.col-11 { width: 91.66666667%; } |
|||
.col-12 { width: 100%; } |
|||
|
|||
.p-1 { padding: 0.25rem; } |
|||
.p-2 { padding: 0.5rem; } |
|||
.p-3 { padding: 1rem; } |
|||
.p-4 { padding: 1.5rem; } |
|||
.p-5 { padding: 3rem; } |
|||
|
|||
.px-1 { padding: 0 0.25rem 0 0.25rem; } |
|||
.px-2 { padding: 0 0.5rem 0 0.5rem; } |
|||
.px-3 { padding: 0 1rem 0 1rem; } |
|||
.px-4 { padding: 0 1.5rem 0 1.5rem; } |
|||
.px-5 { padding: 0 3rem 0 3rem; } |
|||
|
|||
.py-1 { padding: 0.25rem 0 0.25rem; } |
|||
.py-2 { padding: 0.5rem 0 0.5rem; } |
|||
.py-3 { padding: 1rem 0 1rem; } |
|||
.py-4 { padding: 1.5rem 0 1.5rem; } |
|||
.py-5 { padding: 3rem 0 3rem; } |
|||
|
|||
.mx-1 { margin: 0 0.25rem 0 0.25rem; } |
|||
.mx-2 { margin: 0 0.5rem 0 0.5rem; } |
|||
.mx-3 { margin: 0 1rem 0 1rem; } |
|||
.mx-4 { margin: 0 1.5rem 0 1.5rem; } |
|||
.mx-5 { margin: 0 3rem 0 3rem; } |
|||
|
|||
.my-1 { margin: 0.25rem 0 0.25rem; } |
|||
.my-2 { margin: 0.5rem 0 0.5rem; } |
|||
.my-3 { margin: 1rem 0 1rem; } |
|||
.my-4 { margin: 1.5rem 0 1.5rem; } |
|||
.my-5 { margin: 3rem 0 3rem; } |
|||
|
|||
.mt-1 { margin-top: 0.25rem } |
|||
.mt-2 { margin-top: 0.5rem } |
|||
.mt-3 { margin-top: 1rem } |
|||
.mt-4 { margin-top: 1.5rem } |
|||
.mt-5 { margin-top: 3rem } |
|||
|
|||
.mb-1 { margin-bottom: 0.25rem } |
|||
.mb-2 { margin-bottom: 0.5rem } |
|||
.mb-3 { margin-bottom: 1rem } |
|||
.mb-4 { margin-bottom: 1.5rem } |
|||
.mb-5 { margin-bottom: 3rem } |
|||
|
|||
.fs-1 { font-size: 3.5rem; } |
|||
.fs-2 { font-size: 3rem; } |
|||
.fs-3 { font-size: 2.5rem; } |
|||
.fs-4 { font-size: 2rem; } |
|||
.fs-5 { font-size: 1.75rem; } |
|||
.fs-6 { font-size: 1.5rem; } |
|||
.fs-7 { font-size: 1.25rem; } |
|||
.fs-8 { font-size: 1rem; } |
|||
.fs-9 { font-size: 0.75rem; } |
|||
.fs-10 { font-size: 0.5rem; } |
|||
|
|||
.a-r { text-align: right; } |
|||
.a-c { text-align: center; } |
|||
|
|||
.row > * { |
|||
padding-left: 0.5rem; |
|||
padding-right: 0.5rem; |
|||
} |
|||
|
|||
*, ::after, ::before { |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
/* sm */ |
|||
@media(min-width: 768px) { |
|||
.col-sm-1 { width: 8.333333333%; } |
|||
.col-sm-2 { width: 16.66666667%; } |
|||
.col-sm-3 { width: 25%; } |
|||
.col-sm-4 { width: 33.33333333%; } |
|||
.col-sm-5 { width: 41.66666667%; } |
|||
.col-sm-6 { width: 50%; } |
|||
.col-sm-7 { width: 58.33333333%; } |
|||
.col-sm-8 { width: 66.66666667%; } |
|||
.col-sm-9 { width: 75%; } |
|||
.col-sm-10 { width: 83.33333333%; } |
|||
.col-sm-11 { width: 91.66666667%; } |
|||
.col-sm-12 { width: 100%; } |
|||
|
|||
.mb-sm-1 { margin-bottom: 0.25rem } |
|||
.mb-sm-2 { margin-bottom: 0.5rem } |
|||
.mb-sm-3 { margin-bottom: 1rem } |
|||
.mb-sm-4 { margin-bottom: 1.5rem } |
|||
.mb-sm-5 { margin-bottom: 3rem } |
|||
|
|||
.fs-sm-1 { font-size: 3.5rem; } |
|||
.fs-sm-2 { font-size: 3rem; } |
|||
.fs-sm-3 { font-size: 2.5rem; } |
|||
.fs-sm-4 { font-size: 2rem; } |
|||
.fs-sm-5 { font-size: 1.75rem; } |
|||
.fs-sm-6 { font-size: 1.5rem; } |
|||
.fs-sm-7 { font-size: 1.25rem; } |
|||
.fs-sm-8 { font-size: 1rem; } |
|||
} |
|||
|
|||
/* md */ |
|||
@media(min-width: 992px) { |
|||
.col-md-1 { width: 8.333333333%; } |
|||
.col-md-2 { width: 16.66666667%; } |
|||
.col-md-3 { width: 25%; } |
|||
.col-md-4 { width: 33.33333333%; } |
|||
.col-md-5 { width: 41.66666667%; } |
|||
.col-md-6 { width: 50%; } |
|||
.col-md-7 { width: 58.33333333%; } |
|||
.col-md-8 { width: 66.66666667%; } |
|||
.col-md-9 { width: 75%; } |
|||
.col-md-10 { width: 83.33333333%; } |
|||
.col-md-11 { width: 91.66666667%; } |
|||
.col-md-12 { width: 100%; } |
|||
} |
|||
|
|||
#wrapper { |
|||
min-height: 100%; |
|||
} |
|||
|
|||
#content { |
|||
padding: 50px 20px 120px 20px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
#footer { |
|||
height: 121px; |
|||
margin-top: -121px; |
|||
width: 100%; |
|||
font-size: 13px; |
|||
} |
|||
|
|||
#footer .right { |
|||
color: #bbb; |
|||
margin: 6px 25px; |
|||
text-align: right; |
|||
} |
|||
|
|||
#footer .left { |
|||
color: #bbb; |
|||
margin: 23px 0px 0px 25px; |
|||
} |
|||
|
|||
#footer ul { |
|||
list-style-type: none; |
|||
margin: 20px auto; |
|||
padding: 0; |
|||
} |
|||
|
|||
#footer ul li, #footer a { |
|||
color: #bbb; |
|||
margin-bottom: 10px; |
|||
padding-left: 5px; |
|||
font-size: 13px; |
|||
} |
|||
|
|||
#footer a:hover { |
|||
color: #fff; |
|||
} |
|||
|
|||
.hide { |
|||
display: none !important; |
|||
} |
|||
|
|||
@media only screen and (min-width: 992px) { |
|||
.topnav { |
|||
width: 230px !important; |
|||
height: 100%; |
|||
} |
|||
|
|||
.topnav a.icon { |
|||
display: none !important; |
|||
} |
|||
|
|||
.topnav a { |
|||
padding: 14px 24px; |
|||
} |
|||
|
|||
.topnav .title { |
|||
padding-left: 24px !important; |
|||
} |
|||
|
|||
.topnav .mobile { |
|||
display: block; |
|||
} |
|||
|
|||
.topnav .info { |
|||
top: auto !important; |
|||
right: auto !important; |
|||
bottom: 14px; |
|||
left: 24px; |
|||
} |
|||
|
|||
#content { |
|||
padding: 15px 15px 120px 250px; |
|||
} |
|||
|
|||
#footer .left { |
|||
margin-left: 250px !important; |
|||
} |
|||
} |
|||
|
|||
p.lic, p.lic a { |
|||
font-size: 8pt; |
|||
color: #999; |
|||
} |
|||
|
|||
.des { |
|||
margin-top: 20px; |
|||
font-size: 13pt; |
|||
color: var(--secondary); |
|||
} |
|||
|
|||
.s_active, .s_collapsible:hover { |
|||
background-color: var(--primary-hover); |
|||
color: #fff; |
|||
} |
|||
|
|||
.s_content { |
|||
display: none; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.s_collapsible { |
|||
background-color: var(--primary); |
|||
color: white; |
|||
cursor: pointer; |
|||
padding: 12px; |
|||
width: 100%; |
|||
border: none; |
|||
text-align: left; |
|||
outline: none; |
|||
font-size: 15px; |
|||
margin-bottom: 5px; |
|||
} |
|||
|
|||
.subdes { |
|||
font-size: 12pt; |
|||
color: var(--secondary); |
|||
margin-left: 7px; |
|||
} |
|||
|
|||
.subsubdes { |
|||
font-size:12pt; |
|||
color:var(--secondary); |
|||
margin: 0 0 7px 12px; |
|||
} |
|||
|
|||
a:link, a:visited { |
|||
text-decoration: none; |
|||
font-size: 13pt; |
|||
color: var(--secondary); |
|||
} |
|||
|
|||
a:hover, a:focus { |
|||
color: #f00; |
|||
} |
|||
|
|||
a.btn { |
|||
background-color: var(--primary); |
|||
color: #fff; |
|||
padding: 7px 15px 7px 15px; |
|||
display: inline-block; |
|||
} |
|||
|
|||
a.btn:hover { |
|||
background-color: var(--primary-hover) !important; |
|||
} |
|||
|
|||
input, select { |
|||
padding: 7px; |
|||
font-size: 13pt; |
|||
} |
|||
|
|||
input[type=text], input[type=password], select, input[type=number] { |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
border: 1px solid #ccc; |
|||
border-radius: 4px; |
|||
background-color: var(--input-bg); |
|||
color: var(--fg); |
|||
} |
|||
|
|||
input.sh { |
|||
max-width: 150px !important; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
input.btnDel { |
|||
background-color: #c00 !important; |
|||
} |
|||
|
|||
input.btn { |
|||
background-color: var(--primary); |
|||
color: #fff; |
|||
border: 0px; |
|||
padding: 7px 20px 7px 20px; |
|||
margin-bottom: 10px; |
|||
text-transform: uppercase; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
input.btn:hover { |
|||
background-color: #044e86; |
|||
} |
|||
|
|||
input.cb { |
|||
margin-bottom: 15px; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
label { |
|||
width: 20%; |
|||
display: inline-block; |
|||
font-size: 12pt; |
|||
padding-right: 10px; |
|||
margin: 10px 0px 0px 15px; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
pre { |
|||
white-space: pre-wrap; |
|||
} |
|||
|
|||
.left { |
|||
float: left; |
|||
} |
|||
|
|||
.right { |
|||
float: right; |
|||
} |
|||
|
|||
.subgrp { |
|||
float: left; |
|||
width: 220px; |
|||
} |
|||
|
|||
div.ModPwr, div.ModName, div.YieldCor { |
|||
width:70%; |
|||
display: inline-block; |
|||
} |
|||
|
|||
div.hr { |
|||
height: 1px; |
|||
border-top: 1px solid #ccc; |
|||
margin: 10px 0px 10px; |
|||
} |
|||
|
|||
#note { |
|||
margin: 10px 10px 10px 10px; |
|||
padding-top: 10px; |
|||
width: 100%; |
|||
} |
|||
|
|||
@media(max-width: 500px) { |
|||
div.ch .unit, div.ch-iv .unit { |
|||
font-size: 18px; |
|||
} |
|||
|
|||
div.ch { |
|||
width: 170px; |
|||
min-height: 100px |
|||
} |
|||
|
|||
.subgrp { |
|||
width: 180px; |
|||
} |
|||
} |
|||
|
|||
#serial { |
|||
width: 100%; |
|||
} |
|||
|
|||
#content .serial { |
|||
max-width: 1000px; |
|||
} |
|||
|
|||
.dot { |
|||
height: 15px; |
|||
width: 15px; |
|||
background-color: #f00; |
|||
border-radius: 50%; |
|||
display: inline-block; |
|||
margin-top: 15px; |
|||
} |
|||
|
|||
#login { |
|||
width: 450px; |
|||
height: 200px; |
|||
border: 1px solid #ccc; |
|||
background-color: var(--input-bg); |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
margin-top: -160px; |
|||
margin-left: -225px; |
|||
} |
|||
|
|||
|
|||
.head { |
|||
background-color: var(--primary); |
|||
color: #fff; |
|||
} |
|||
|
|||
|
|||
.css-tooltip{ |
|||
position: relative; |
|||
} |
|||
.css-tooltip:hover:after{ |
|||
content:attr(data-tooltip); |
|||
background:#000; |
|||
padding:5px; |
|||
border-radius:3px; |
|||
display: inline-block; |
|||
position: absolute; |
|||
transform: translate(-50%,-100%); |
|||
margin:0 auto; |
|||
color:#FFF; |
|||
min-width:100px; |
|||
min-width:150px; |
|||
top:-5px; |
|||
left: 50%; |
|||
text-align:center; |
|||
} |
|||
.css-tooltip:hover:before { |
|||
top:-5px; |
|||
left: 50%; |
|||
border: solid transparent; |
|||
content: " "; |
|||
height: 0; |
|||
width: 0; |
|||
position: absolute; |
|||
pointer-events: none; |
|||
border-color: rgba(0, 0, 0, 0); |
|||
border-top-color: #000; |
|||
border-width: 5px; |
|||
margin-left: -5px; |
|||
transform: translate(0,0px); |
|||
} |
@ -0,0 +1,120 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>System</title> |
|||
{#HTML_HEADER} |
|||
</head> |
|||
<body> |
|||
{#HTML_NAV} |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<pre id="stat"></pre> |
|||
<div id="info" class="col-sm-12 col-md-6 mt-3"></div> |
|||
<div id="radio" class="col-sm-12 col-md-6 mt-3"></div> |
|||
<div id="sun" class="col-sm-12 col-md-6 mt-3"></div> |
|||
<div id="html" class="mt-3 mb-3"></div> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
<script type="text/javascript"> |
|||
function parseGeneric(obj) { |
|||
parseNav(obj); |
|||
parseESP(obj); |
|||
parseRssi(obj); |
|||
} |
|||
|
|||
function genTabRow(key, value) { |
|||
var r = div(["row", "p-1"]); |
|||
r.appendChild(div(["col"], key)); |
|||
r.appendChild(div(["col"], value)); |
|||
return r; |
|||
} |
|||
|
|||
function parseSysInfo(obj) { |
|||
const data = ["sdk", "cpu_freq", "chip_revision", |
|||
"chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime", |
|||
"flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag", |
|||
"max_free_blk", "version", "core_version", "reboot_reason"]; |
|||
|
|||
var main = document.getElementById("info"); |
|||
var h = div(["head", "p-2"]); |
|||
var r = div(["row"]); |
|||
r.appendChild(div(["col", "a-c"], "System Information")); |
|||
h.appendChild(r); |
|||
main.appendChild(h); |
|||
|
|||
for (const [key, value] of Object.entries(obj)) { |
|||
if(!data.includes(key) || (typeof value == 'undefined')) continue; |
|||
main.appendChild(genTabRow(key, value)); |
|||
} |
|||
} |
|||
|
|||
function parseRadio(obj, stat) { |
|||
const pa = ["MIN", "LOW", "HIGH", "MAX"]; |
|||
const datarate = ["1 MBps", "2 MBps", "250 kbps"]; |
|||
|
|||
var main = document.getElementById("radio"); |
|||
var h = div(["head", "p-2"]); |
|||
var r = div(["row"]); |
|||
r.appendChild(div(["col", "a-c"], "Radio")); |
|||
h.appendChild(r); |
|||
main.appendChild(h); |
|||
|
|||
main.appendChild(genTabRow("nrf24l01" + (obj["isPVariant"] ? "+ " : ""), (obj["isconnected"] ? "is connected " : "is not connected "))); |
|||
|
|||
if(obj["isconnected"]) { |
|||
main.appendChild(genTabRow("Datarate", datarate[obj["DataRate"]])); |
|||
main.appendChild(genTabRow("Power Level", pa[obj["power_level"]])); |
|||
} |
|||
|
|||
main.append( |
|||
genTabRow("TX count", stat["tx_cnt"]), |
|||
genTabRow("RX success", stat["rx_success"]), |
|||
genTabRow("RX fail", stat["rx_fail"]), |
|||
genTabRow("RX no answer", stat["rx_fail_answer"]), |
|||
genTabRow("RX fragments", stat["frame_cnt"]), |
|||
genTabRow("TX retransmits", stat["retransmits"]) |
|||
); |
|||
} |
|||
|
|||
function parseIndex(obj) { |
|||
if(obj["ts_sunrise"] > 0) { |
|||
var h = div(["head", "p-2"]); |
|||
var r = div(["row"]); |
|||
r.appendChild(div(["col", "a-c"], "Sun")); |
|||
h.appendChild(r); |
|||
|
|||
document.getElementById("sun").append ( |
|||
h, |
|||
genTabRow("Sunrise", new Date(obj["ts_sunrise"] * 1000).toLocaleString('de-DE')), |
|||
genTabRow("Sunset", new Date(obj["ts_sunset"] * 1000).toLocaleString('de-DE')), |
|||
genTabRow("Communication start", new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')), |
|||
genTabRow("Communication stop", new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')), |
|||
genTabRow("Night Communication", ((obj["disNightComm"]) ? "disabled" : "enabled")) |
|||
); |
|||
} |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
parseGeneric(obj["generic"]); |
|||
|
|||
if(null != obj["refresh"]) { |
|||
var meta = document.createElement('meta'); |
|||
meta.httpEquiv = "refresh" |
|||
meta.content = obj["refresh"] + "; URL=" + obj["refresh_url"]; |
|||
document.getElementsByTagName('head')[0].appendChild(meta); |
|||
} |
|||
else { |
|||
parseSysInfo(obj["system"]); |
|||
parseRadio(obj["system"]["radio"], obj["system"]["statistics"]); |
|||
getAjax('/api/index', parseIndex); |
|||
} |
|||
document.getElementById("html").innerHTML = obj["html"]; |
|||
} |
|||
} |
|||
|
|||
getAjax("/api/html" + window.location.pathname, parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,37 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Update</title> |
|||
{#HTML_HEADER} |
|||
</head> |
|||
<body> |
|||
{#HTML_NAV} |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<fieldset> |
|||
<legend class="des">Select firmware file (*.bin)</legend> |
|||
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> |
|||
<input type="file" name="update"> |
|||
<input type="button" class="btn" value="Update" onclick="hide()"> |
|||
</form> |
|||
</fieldset> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
<script type="text/javascript"> |
|||
function parseGeneric(obj) { |
|||
parseNav(obj); |
|||
parseESP(obj); |
|||
parseRssi(obj); |
|||
} |
|||
|
|||
function hide() { |
|||
document.getElementById("form").submit(); |
|||
var e = document.getElementById("content"); |
|||
e.replaceChildren(span("update started")); |
|||
} |
|||
|
|||
getAjax("/api/generic", parseGeneric); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,238 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Live</title> |
|||
{#HTML_HEADER} |
|||
<meta name="apple-mobile-web-app-capable" content="yes"> |
|||
</head> |
|||
<body> |
|||
{#HTML_NAV} |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<div id="live"></div> |
|||
<p>Every <span id="refresh"></span> seconds the values are updated</p> |
|||
</div> |
|||
</div> |
|||
{#HTML_FOOTER} |
|||
<script type="text/javascript"> |
|||
var exeOnce = true; |
|||
var units, ivEn; |
|||
var mIvHtml = []; |
|||
var mNum = 0; |
|||
var names = ["Voltage", "Current", "Power", "Yield Day", "Yield Total", "Irradiation"]; |
|||
var total = Array(5).fill(0); |
|||
|
|||
function parseGeneric(obj) { |
|||
if(true == exeOnce){ |
|||
parseNav(obj); |
|||
parseESP(obj); |
|||
} |
|||
parseRssi(obj); |
|||
} |
|||
|
|||
function numBig(val, unit, des) { |
|||
return ml("div", {class: "col-6 col-sm-4 a-c"}, [ |
|||
ml("div", {class: "row"}, |
|||
ml("div", {class: "col"}, [ |
|||
ml("span", {class: "fs-5 fs-md-4"}, String(Math.round(val * 100) / 100)), |
|||
ml("span", {class: "fs-6 fs-md-7 mx-1"}, unit) |
|||
])), |
|||
ml("div", {class: "row"}, |
|||
ml("div", {class: "col"}, |
|||
ml("span", {class: "fs-9 px-1"}, des) |
|||
) |
|||
) |
|||
]); |
|||
} |
|||
|
|||
function numMid(val, unit, des) { |
|||
return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [ |
|||
ml("div", {class: "row"}, |
|||
ml("div", {class: "col"}, [ |
|||
ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)), |
|||
ml("span", {class: "fs-8 mx-1"}, unit) |
|||
])), |
|||
ml("div", {class: "row"}, |
|||
ml("div", {class: "col"}, |
|||
ml("span", {class: "fs-9"}, des) |
|||
) |
|||
) |
|||
]); |
|||
} |
|||
|
|||
function totals() { |
|||
for(var i = 0; i < 5; i++) { |
|||
total[i] = Math.round(total[i] * 100) / 100; |
|||
} |
|||
|
|||
return ml("div", {class: "row mt-3 mb-5"}, |
|||
ml("div", {class: "col"}, [ |
|||
ml("div", {class: "p-2 total-h"}, |
|||
ml("div", {class: "row"}, |
|||
ml("div", {class: "col mx-2 mx-md-1"}, "TOTAL") |
|||
), |
|||
), |
|||
ml("div", {class: "p-2 total-bg"}, [ |
|||
ml("div", {class: "row"}, [ |
|||
numBig(total[0], "W", "AC Power"), |
|||
numBig(total[1], "Wh", "Yield Day"), |
|||
numBig(total[2], "kWh", "Yield Total") |
|||
]), |
|||
ml("div", {class: "hr"}), |
|||
ml("div", {class: "row"}, [ |
|||
numMid(total[3], "W", "DC Power"), |
|||
numMid(total[4], "var", "Reactive Power") |
|||
]) |
|||
]) |
|||
]) |
|||
); |
|||
} |
|||
function ivHead(obj) { |
|||
total[0] += obj.ch[0][2]; // P_AC |
|||
total[1] += obj.ch[0][7]; // YieldDay |
|||
total[2] += obj.ch[0][6]; // YieldTotal |
|||
total[3] += obj.ch[0][8]; // P_DC |
|||
total[4] += obj.ch[0][10]; // Q_AC |
|||
var t = span(" ° C"); |
|||
return ml("div", {class: "row mt-2"}, |
|||
ml("div", {class: "col"}, [ |
|||
ml("div", {class: "p-2 iv-h"}, |
|||
ml("div", {class: "row"}, [ |
|||
ml("div", {class: "col mx-2 mx-md-1"}, obj.name), |
|||
ml("div", {class: "col a-c"}, "Power limit " + ((obj.power_limit_read == 65535) ? "n/a" : (obj.power_limit_read + " %"))), |
|||
ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5]) + t.innerHTML) |
|||
]) |
|||
), |
|||
ml("div", {class: "p-2 iv-bg"}, [ |
|||
ml("div", {class: "row"},[ |
|||
numBig(obj.ch[0][2], "W", "AC Power"), |
|||
numBig(obj.ch[0][7], "Wh", "Yield Day"), |
|||
numBig(obj.ch[0][6], "kWh", "Yield Total") |
|||
]), |
|||
ml("div", {class: "hr"}), |
|||
ml("div", {class: "row mt-2"},[ |
|||
numMid(obj.ch[0][8], "W", "DC Power"), |
|||
numMid(obj.ch[0][0], "V", "Voltage"), |
|||
numMid(obj.ch[0][1], "A", "Current"), |
|||
numMid(obj.ch[0][3], "Hz", "Frequency"), |
|||
numMid(obj.ch[0][9], "%", "Efficiency"), |
|||
numMid(obj.ch[0][10], "var", "Reactive Power"), |
|||
numMid(obj.ch[0][4], "", "Power Factor") |
|||
]) |
|||
]) |
|||
]) |
|||
); |
|||
} |
|||
|
|||
function numCh(val, unit, des) { |
|||
return ml("div", {class: "col-12 col-sm-6 col-md-12 mb-2"}, [ |
|||
ml("div", {class: "row"}, |
|||
ml("div", {class: "col"}, [ |
|||
ml("span", {class: "fs-6 fs-md-7"}, String(Math.round(val * 100) / 100)), |
|||
ml("span", {class: "fs-8 mx-2"}, unit) |
|||
])), |
|||
ml("div", {class: "row"}, |
|||
ml("div", {class: "col"}, |
|||
ml("span", {class: "fs-9"}, des) |
|||
) |
|||
) |
|||
]); |
|||
} |
|||
|
|||
function ch(name, vals) { |
|||
return ml("div", {class: "col-6 col-md-3 mt-2"}, [ |
|||
ml("div", {class: "ch-h p-2 a-c"}, name), |
|||
ml("div", {class: "p-2 ch-bg"}, [ |
|||
ml("div", {class: "row"}, [ |
|||
numCh(vals[2], units[2], "Power"), |
|||
numCh(vals[5], units[5], "Irradiation"), |
|||
numCh(vals[3], units[3], "Yield Day"), |
|||
numCh(vals[4], units[4], "Yield Total"), |
|||
numCh(vals[0], units[0], "Voltage"), |
|||
numCh(vals[1], units[1], "Current") |
|||
]) |
|||
]) |
|||
]); |
|||
} |
|||
|
|||
function tsInfo(ts) { |
|||
var ageInfo = "Last received data requested at: "; |
|||
if(ts > 0) { |
|||
var date = new Date(ts * 1000); |
|||
ageInfo += date.toLocaleString('de-DE'); |
|||
} |
|||
else |
|||
ageInfo += "nothing received"; |
|||
|
|||
return ml("div", {class: "mb-5"}, [ |
|||
ml("div", {class: "row p-1 ts-h mx-2"}, |
|||
ml("div", {class: "col"}, "") |
|||
), |
|||
ml("div", {class: "row p-2 ts-bg mx-2"}, |
|||
ml("div", {class: "col mx-2"}, ageInfo) |
|||
) |
|||
]); |
|||
} |
|||
|
|||
function parseIv(obj) { |
|||
mNum++; |
|||
|
|||
var chn = []; |
|||
for(var i = 1; i < obj.ch.length; i++) { |
|||
var name = obj.ch_name[i]; |
|||
if(name.length == 0) |
|||
name = "CHANNEL " + i; |
|||
if(obj.ch_max_pwr[i] > 0) // show channel only if max mod pwr |
|||
chn.push(ch(name, obj.ch[i])); |
|||
} |
|||
mIvHtml.push( |
|||
ml("div", {}, [ |
|||
ivHead(obj), |
|||
ml("div", {class: "row mb-2"}, chn), |
|||
tsInfo(obj.ts_last_success) |
|||
]) |
|||
); |
|||
|
|||
var last = true; |
|||
for(var i = obj.id + 1; i < ivEn.length; i++) { |
|||
if((i != ivEn.length) && ivEn[i]) { |
|||
last = false; |
|||
getAjax("/api/inverter/id/" + i, parseIv); |
|||
break; |
|||
} |
|||
} |
|||
if(last) { |
|||
if(mNum > 1) |
|||
mIvHtml.unshift(totals()); |
|||
document.getElementById("live").replaceChildren(...mIvHtml); |
|||
} |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
parseGeneric(obj["generic"]); |
|||
units = Object.assign({}, obj["fld_units"]); |
|||
ivEn = Object.values(Object.assign({}, obj["iv"])); |
|||
mIvHtml = []; |
|||
mNum = 0; |
|||
total.fill(0); |
|||
for(var i = 0; i < obj.iv.length; i++) { |
|||
if(obj.iv[i]) { |
|||
getAjax("/api/inverter/id/" + i, parseIv); |
|||
break; |
|||
} |
|||
} |
|||
document.getElementById("refresh").innerHTML = obj["refresh"]; |
|||
if(true == exeOnce) { |
|||
window.setInterval("getAjax('/api/live', parse)", obj["refresh"] * 1000); |
|||
exeOnce = false; |
|||
} |
|||
} |
|||
else |
|||
document.getElementById("refresh").innerHTML = "n/a"; |
|||
} |
|||
|
|||
getAjax("/api/live", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,856 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __WEB_H__ |
|||
#define __WEB_H__ |
|||
|
|||
#include "../utils/dbg.h" |
|||
#ifdef ESP32 |
|||
#include "AsyncTCP.h" |
|||
#include "Update.h" |
|||
#else |
|||
#include "ESPAsyncTCP.h" |
|||
#endif |
|||
#include "../appInterface.h" |
|||
#include "../hm/hmSystem.h" |
|||
#include "../utils/helper.h" |
|||
#include "ESPAsyncWebServer.h" |
|||
#include "html/h/api_js.h" |
|||
#include "html/h/colorBright_css.h" |
|||
#include "html/h/colorDark_css.h" |
|||
#include "html/h/favicon_ico.h" |
|||
#include "html/h/index_html.h" |
|||
#include "html/h/login_html.h" |
|||
#include "html/h/serial_html.h" |
|||
#include "html/h/setup_html.h" |
|||
#include "html/h/style_css.h" |
|||
#include "html/h/system_html.h" |
|||
#include "html/h/save_html.h" |
|||
#include "html/h/update_html.h" |
|||
#include "html/h/visualization_html.h" |
|||
#include "html/h/about_html.h" |
|||
|
|||
#define WEB_SERIAL_BUF_SIZE 2048 |
|||
|
|||
const char *const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1"}; |
|||
|
|||
template <class HMSYSTEM> |
|||
class Web { |
|||
public: |
|||
Web(void) : mWeb(80), mEvts("/events") { |
|||
mProtected = true; |
|||
mLogoutTimeout = 0; |
|||
|
|||
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); |
|||
mSerialBufFill = 0; |
|||
mSerialAddTime = true; |
|||
mSerialClientConnnected = false; |
|||
} |
|||
|
|||
void setup(IApp *app, HMSYSTEM *sys, settings_t *config) { |
|||
mApp = app; |
|||
mSys = sys; |
|||
mConfig = config; |
|||
|
|||
DPRINTLN(DBG_VERBOSE, F("app::setup-on")); |
|||
mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); |
|||
mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); |
|||
mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); |
|||
mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1)); |
|||
mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1)); |
|||
mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1)); |
|||
mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1)); |
|||
mWeb.onNotFound ( std::bind(&Web::showNotFound, this, std::placeholders::_1)); |
|||
mWeb.on("/reboot", HTTP_ANY, std::bind(&Web::onReboot, this, std::placeholders::_1)); |
|||
mWeb.on("/system", HTTP_ANY, std::bind(&Web::onSystem, this, std::placeholders::_1)); |
|||
mWeb.on("/erase", HTTP_ANY, std::bind(&Web::showErase, this, std::placeholders::_1)); |
|||
mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1)); |
|||
|
|||
mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); |
|||
mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1)); |
|||
|
|||
mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); |
|||
//mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1));
|
|||
|
|||
#ifdef ENABLE_PROMETHEUS_EP |
|||
mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1)); |
|||
#endif |
|||
|
|||
mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1)); |
|||
mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1), |
|||
std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); |
|||
mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1), |
|||
std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); |
|||
mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1)); |
|||
mWeb.on("/about", HTTP_GET, std::bind(&Web::onAbout, this, std::placeholders::_1)); |
|||
mWeb.on("/debug", HTTP_GET, std::bind(&Web::onDebug, this, std::placeholders::_1)); |
|||
|
|||
|
|||
mEvts.onConnect(std::bind(&Web::onConnect, this, std::placeholders::_1)); |
|||
mWeb.addHandler(&mEvts); |
|||
|
|||
mWeb.begin(); |
|||
|
|||
registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h
|
|||
|
|||
mUploadFail = false; |
|||
} |
|||
|
|||
void tickSecond() { |
|||
if (0 != mLogoutTimeout) { |
|||
mLogoutTimeout -= 1; |
|||
if (0 == mLogoutTimeout) { |
|||
if (strlen(mConfig->sys.adminPwd) > 0) |
|||
mProtected = true; |
|||
} |
|||
|
|||
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout)); |
|||
} |
|||
|
|||
if (mSerialClientConnnected) { |
|||
if (mSerialBufFill > 0) { |
|||
mEvts.send(mSerialBuf, "serial", millis()); |
|||
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); |
|||
mSerialBufFill = 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
AsyncWebServer *getWebSrvPtr(void) { |
|||
return &mWeb; |
|||
} |
|||
|
|||
void setProtection(bool protect) { |
|||
mProtected = protect; |
|||
} |
|||
|
|||
bool getProtection() { |
|||
return mProtected; |
|||
} |
|||
|
|||
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { |
|||
mApp->setOnUpdate(); |
|||
|
|||
if (!index) { |
|||
Serial.printf("Update Start: %s\n", filename.c_str()); |
|||
#ifndef ESP32 |
|||
Update.runAsync(true); |
|||
#endif |
|||
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { |
|||
Update.printError(Serial); |
|||
} |
|||
} |
|||
if (!Update.hasError()) { |
|||
if (Update.write(data, len) != len) |
|||
Update.printError(Serial); |
|||
} |
|||
if (final) { |
|||
if (Update.end(true)) |
|||
Serial.printf("Update Success: %uB\n", index + len); |
|||
else |
|||
Update.printError(Serial); |
|||
} |
|||
} |
|||
|
|||
void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { |
|||
if (!index) { |
|||
mUploadFail = false; |
|||
mUploadFp = LittleFS.open("/tmp.json", "w"); |
|||
if (!mUploadFp) { |
|||
DPRINTLN(DBG_ERROR, F("can't open file!")); |
|||
mUploadFail = true; |
|||
mUploadFp.close(); |
|||
return; |
|||
} |
|||
} |
|||
mUploadFp.write(data, len); |
|||
if (final) { |
|||
mUploadFp.close(); |
|||
char pwd[PWD_LEN]; |
|||
strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD
|
|||
if (!mApp->readSettings("/tmp.json")) { |
|||
mUploadFail = true; |
|||
DPRINTLN(DBG_ERROR, F("upload JSON error!")); |
|||
} else { |
|||
LittleFS.remove("/tmp.json"); |
|||
strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD
|
|||
mApp->saveSettings(true); |
|||
} |
|||
if (!mUploadFail) |
|||
DPRINTLN(DBG_INFO, F("upload finished!")); |
|||
} |
|||
} |
|||
|
|||
void serialCb(String msg) { |
|||
if (!mSerialClientConnnected) |
|||
return; |
|||
|
|||
msg.replace("\r\n", "<rn>"); |
|||
if (mSerialAddTime) { |
|||
if ((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { |
|||
if (mApp->getTimestamp() > 0) { |
|||
strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9); |
|||
mSerialBufFill += 9; |
|||
} |
|||
} else { |
|||
mSerialBufFill = 0; |
|||
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis()); |
|||
return; |
|||
} |
|||
mSerialAddTime = false; |
|||
} |
|||
|
|||
if (msg.endsWith("<rn>")) |
|||
mSerialAddTime = true; |
|||
|
|||
uint16_t length = msg.length(); |
|||
if ((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { |
|||
strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length); |
|||
mSerialBufFill += length; |
|||
} else { |
|||
mSerialBufFill = 0; |
|||
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis()); |
|||
} |
|||
} |
|||
|
|||
private: |
|||
void checkRedirect(AsyncWebServerRequest *request) { |
|||
if ((mConfig->sys.protectionMask & PROT_MASK_INDEX) != PROT_MASK_INDEX) |
|||
request->redirect(F("/index")); |
|||
else if ((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE) |
|||
request->redirect(F("/live")); |
|||
else if ((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL) |
|||
request->redirect(F("/serial")); |
|||
else if ((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM) |
|||
request->redirect(F("/system")); |
|||
else |
|||
request->redirect(F("/login")); |
|||
} |
|||
|
|||
void onUpdate(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onUpdate")); |
|||
|
|||
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), update_html, update_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void showUpdate(AsyncWebServerRequest *request) { |
|||
bool reboot = (!Update.hasError()); |
|||
|
|||
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: "); |
|||
if (reboot) |
|||
html += "success"; |
|||
else |
|||
html += "failed"; |
|||
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); |
|||
response->addHeader("Connection", "close"); |
|||
request->send(response); |
|||
mApp->setRebootFlag(); |
|||
} |
|||
|
|||
void onUpload(AsyncWebServerRequest *request) { |
|||
bool reboot = !mUploadFail; |
|||
|
|||
String html = F("<!doctype html><html><head><title>Upload</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Upload: "); |
|||
if (reboot) |
|||
html += "success"; |
|||
else |
|||
html += "failed"; |
|||
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); |
|||
response->addHeader("Connection", "close"); |
|||
request->send(response); |
|||
mApp->setRebootFlag(); |
|||
} |
|||
|
|||
void onConnect(AsyncEventSourceClient *client) { |
|||
DPRINTLN(DBG_VERBOSE, "onConnect"); |
|||
|
|||
mSerialClientConnnected = true; |
|||
|
|||
if (client->lastId()) |
|||
DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId())); |
|||
|
|||
client->send("hello!", NULL, millis(), 1000); |
|||
} |
|||
|
|||
void onIndex(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onIndex")); |
|||
|
|||
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), index_html, index_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onLogin(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onLogin")); |
|||
|
|||
if (request->args() > 0) { |
|||
if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { |
|||
mProtected = false; |
|||
request->redirect("/"); |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), login_html, login_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onLogout(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onLogout")); |
|||
|
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
|
|||
mProtected = true; |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onColor(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onColor")); |
|||
AsyncWebServerResponse *response; |
|||
if (mConfig->sys.darkMode) |
|||
response = request->beginResponse_P(200, F("text/css"), colorDark_css, colorDark_css_len); |
|||
else |
|||
response = request->beginResponse_P(200, F("text/css"), colorBright_css, colorBright_css_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onCss(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onCss")); |
|||
mLogoutTimeout = LOGOUT_TIMEOUT; |
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onApiJs(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onApiJs")); |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onFavicon(AsyncWebServerRequest *request) { |
|||
static const char favicon_type[] PROGMEM = "image/x-icon"; |
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void showNotFound(AsyncWebServerRequest *request) { |
|||
if (mProtected) |
|||
checkRedirect(request); |
|||
else |
|||
request->redirect("/setup"); |
|||
} |
|||
|
|||
void onReboot(AsyncWebServerRequest *request) { |
|||
mApp->setRebootFlag(); |
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void showErase(AsyncWebServerRequest *request) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
|
|||
DPRINTLN(DBG_VERBOSE, F("showErase")); |
|||
mApp->eraseSettings(false); |
|||
onReboot(request); |
|||
} |
|||
|
|||
void showFactoryRst(AsyncWebServerRequest *request) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
|
|||
DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); |
|||
String content = ""; |
|||
int refresh = 3; |
|||
if (request->args() > 0) { |
|||
if (request->arg("reset").toInt() == 1) { |
|||
refresh = 10; |
|||
if (mApp->eraseSettings(true)) |
|||
content = F("factory reset: success\n\nrebooting ... "); |
|||
else |
|||
content = F("factory reset: failed\n\nrebooting ... "); |
|||
} else { |
|||
content = F("factory reset: aborted"); |
|||
refresh = 3; |
|||
} |
|||
} else { |
|||
content = F("<h1>Factory Reset</h1>" |
|||
"<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>"); |
|||
refresh = 120; |
|||
} |
|||
request->send(200, F("text/html; charset=UTF-8"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>")); |
|||
if (refresh == 10) |
|||
onReboot(request); |
|||
} |
|||
|
|||
void onSetup(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onSetup")); |
|||
|
|||
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), setup_html, setup_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void showSave(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("showSave")); |
|||
|
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
|
|||
if (request->args() == 0) |
|||
return; |
|||
|
|||
char buf[20] = {0}; |
|||
|
|||
// general
|
|||
if (request->arg("ssid") != "") |
|||
request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN); |
|||
if (request->arg("pwd") != "{PWD}") |
|||
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); |
|||
if (request->arg("device") != "") |
|||
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); |
|||
mConfig->sys.darkMode = (request->arg("darkMode") == "on"); |
|||
|
|||
// protection
|
|||
if (request->arg("adminpwd") != "{PWD}") { |
|||
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); |
|||
mProtected = (strlen(mConfig->sys.adminPwd) > 0); |
|||
} |
|||
mConfig->sys.protectionMask = 0x0000; |
|||
for (uint8_t i = 0; i < 6; i++) { |
|||
if (request->arg("protMask" + String(i)) == "on") |
|||
mConfig->sys.protectionMask |= (1 << i); |
|||
} |
|||
|
|||
// static ip
|
|||
request->arg("ipAddr").toCharArray(buf, 20); |
|||
ah::ip2Arr(mConfig->sys.ip.ip, buf); |
|||
request->arg("ipMask").toCharArray(buf, 20); |
|||
ah::ip2Arr(mConfig->sys.ip.mask, buf); |
|||
request->arg("ipDns1").toCharArray(buf, 20); |
|||
ah::ip2Arr(mConfig->sys.ip.dns1, buf); |
|||
request->arg("ipDns2").toCharArray(buf, 20); |
|||
ah::ip2Arr(mConfig->sys.ip.dns2, buf); |
|||
request->arg("ipGateway").toCharArray(buf, 20); |
|||
ah::ip2Arr(mConfig->sys.ip.gateway, buf); |
|||
|
|||
// inverter
|
|||
Inverter<> *iv; |
|||
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
iv = mSys->getInverterByPos(i, false); |
|||
// enable communication
|
|||
iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on"); |
|||
// address
|
|||
request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); |
|||
if (strlen(buf) == 0) |
|||
memset(buf, 0, 20); |
|||
iv->config->serial.u64 = ah::Serial2u64(buf); |
|||
switch(iv->config->serial.b[4]) { |
|||
case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break; |
|||
case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break; |
|||
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break; |
|||
default: break; |
|||
} |
|||
|
|||
// name
|
|||
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH); |
|||
|
|||
// max channel power / name
|
|||
for (uint8_t j = 0; j < 4; j++) { |
|||
iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toInt(); |
|||
iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; |
|||
request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH); |
|||
} |
|||
iv->initialized = true; |
|||
} |
|||
|
|||
if (request->arg("invInterval") != "") |
|||
mConfig->nrf.sendInterval = request->arg("invInterval").toInt(); |
|||
if (request->arg("invRetry") != "") |
|||
mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt(); |
|||
mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); |
|||
mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); |
|||
mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); |
|||
|
|||
// pinout
|
|||
uint8_t pin; |
|||
for (uint8_t i = 0; i < 8; i++) { |
|||
pin = request->arg(String(pinArgNames[i])).toInt(); |
|||
switch(i) { |
|||
default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break; |
|||
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break; |
|||
case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break; |
|||
case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_SCLK_PIN); break; |
|||
case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_MOSI_PIN); break; |
|||
case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_MISO_PIN); break; |
|||
case 6: mConfig->led.led0 = pin; break; |
|||
case 7: mConfig->led.led1 = pin; break; |
|||
} |
|||
} |
|||
|
|||
// nrf24 amplifier power
|
|||
mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03; |
|||
|
|||
// ntp
|
|||
if (request->arg("ntpAddr") != "") { |
|||
request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN); |
|||
mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff; |
|||
} |
|||
|
|||
// sun
|
|||
if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { |
|||
mConfig->sun.lat = 0.0; |
|||
mConfig->sun.lon = 0.0; |
|||
mConfig->sun.disNightCom = false; |
|||
mConfig->sun.offsetSec = 0; |
|||
} else { |
|||
mConfig->sun.lat = request->arg("sunLat").toFloat(); |
|||
mConfig->sun.lon = request->arg("sunLon").toFloat(); |
|||
mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on"); |
|||
mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; |
|||
} |
|||
|
|||
// mqtt
|
|||
if (request->arg("mqttAddr") != "") { |
|||
String addr = request->arg("mqttAddr"); |
|||
addr.trim(); |
|||
addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); |
|||
} else |
|||
mConfig->mqtt.broker[0] = '\0'; |
|||
request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); |
|||
if (request->arg("mqttPwd") != "{PWD}") |
|||
request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); |
|||
request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); |
|||
mConfig->mqtt.port = request->arg("mqttPort").toInt(); |
|||
mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); |
|||
|
|||
// serial console
|
|||
if (request->arg("serIntvl") != "") { |
|||
mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff; |
|||
|
|||
mConfig->serial.debug = (request->arg("serDbg") == "on"); |
|||
mConfig->serial.showIv = (request->arg("serEn") == "on"); |
|||
// Needed to log TX buffers to serial console
|
|||
mSys->Radio.mSerialDebug = mConfig->serial.debug; |
|||
} |
|||
|
|||
// display
|
|||
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on"); |
|||
mConfig->plugin.display.pxShift = (request->arg("disp_pxshift") == "on"); |
|||
mConfig->plugin.display.rot = request->arg("disp_rot").toInt(); |
|||
mConfig->plugin.display.type = request->arg("disp_typ").toInt(); |
|||
mConfig->plugin.display.contrast = (mConfig->plugin.display.type == 0) ? 60 : request->arg("disp_cont").toInt(); |
|||
mConfig->plugin.display.disp_data = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_data").toInt(); |
|||
mConfig->plugin.display.disp_clk = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_clk").toInt(); |
|||
mConfig->plugin.display.disp_cs = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_cs").toInt(); |
|||
mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_rst").toInt(); |
|||
mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt(); |
|||
mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt(); |
|||
|
|||
mApp->saveSettings((request->arg("reboot") == "on")); |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), save_html, save_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onLive(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onLive")); |
|||
|
|||
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), visualization_html, visualization_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
response->addHeader(F("content-type"), "text/html; charset=UTF-8"); |
|||
|
|||
request->send(response); |
|||
} |
|||
|
|||
void onAbout(AsyncWebServerRequest *request) { |
|||
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), about_html, about_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
response->addHeader(F("content-type"), "text/html; charset=UTF-8"); |
|||
|
|||
request->send(response); |
|||
} |
|||
|
|||
void onDebug(AsyncWebServerRequest *request) { |
|||
mApp->getSchedulerNames(); |
|||
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onSerial(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onSerial")); |
|||
|
|||
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), serial_html, serial_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
void onSystem(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("onSystem")); |
|||
|
|||
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) { |
|||
if (mProtected) { |
|||
checkRedirect(request); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); |
|||
response->addHeader(F("Content-Encoding"), "gzip"); |
|||
request->send(response); |
|||
} |
|||
|
|||
|
|||
#ifdef ENABLE_PROMETHEUS_EP |
|||
enum { |
|||
metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd |
|||
} metricsStep; |
|||
int metricsInverterId,metricsChannelId; |
|||
|
|||
void showMetrics(AsyncWebServerRequest *request) { |
|||
DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); |
|||
|
|||
metricsStep = metricsStateStart; |
|||
AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), |
|||
[this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t |
|||
{ |
|||
Inverter<> *iv; |
|||
record_t<> *rec; |
|||
statistics_t *stat; |
|||
String promUnit, promType; |
|||
String metrics; |
|||
char type[60], topic[100], val[25]; |
|||
size_t len = 0; |
|||
int alarmChannelId; |
|||
|
|||
switch (metricsStep) { |
|||
case metricsStateStart: // System Info & NRF Statistics : fit to one packet
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n", |
|||
mApp->getVersion(), mConfig->sys.deviceName); |
|||
metrics = String(type) + String(topic); |
|||
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap()); |
|||
metrics += String(type) + String(topic); |
|||
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_uptime counter\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_uptime{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, mApp->getUptime()); |
|||
metrics += String(type) + String(topic); |
|||
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_wifi_rssi_db gauge\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_wifi_rssi_db{devicename=\"%s\"} %d\n", mConfig->sys.deviceName, WiFi.RSSI()); |
|||
metrics += String(type) + String(topic); |
|||
|
|||
// NRF Statistics
|
|||
stat = mApp->getStatistics(); |
|||
metrics += radioStatistic(F("rx_success"), stat->rxSuccess); |
|||
metrics += radioStatistic(F("rx_fail"), stat->rxFail); |
|||
metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser); |
|||
metrics += radioStatistic(F("frame_cnt"), stat->frmCnt); |
|||
metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); |
|||
|
|||
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|||
// Start Inverter loop
|
|||
metricsInverterId = 0; |
|||
metricsStep = metricsStateInverter; |
|||
break; |
|||
|
|||
case metricsStateInverter: // Inverter loop
|
|||
if (metricsInverterId < mSys->getNumInverters()) { |
|||
iv = mSys->getInverterByPos(metricsInverterId); |
|||
if(NULL != iv) { |
|||
// Inverter info : fit to one packet
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", |
|||
iv->config->name, iv->config->serial.u64); |
|||
metrics = String(type) + String(topic); |
|||
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); |
|||
metrics += String(type) + String(topic); |
|||
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); |
|||
metrics += String(type) + String(topic); |
|||
|
|||
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); |
|||
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); |
|||
metrics += String(type) + String(topic); |
|||
|
|||
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|||
|
|||
// Start Realtime Data Channel loop for this inverter
|
|||
metricsChannelId = 0; |
|||
metricsStep = metricStateRealtimeData; |
|||
} |
|||
} else { |
|||
metricsStep = metricsStateEnd; |
|||
} |
|||
break; |
|||
|
|||
case metricStateRealtimeData: // Realtime Data Channel loop
|
|||
iv = mSys->getInverterByPos(metricsInverterId); |
|||
rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
if (metricsChannelId < rec->length) { |
|||
uint8_t channel = rec->assign[metricsChannelId].ch; |
|||
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); |
|||
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); |
|||
if (0 == channel) { |
|||
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); |
|||
} else { |
|||
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); |
|||
} |
|||
snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); |
|||
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); |
|||
|
|||
metricsChannelId++; |
|||
} else { |
|||
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
|
|||
|
|||
// All realtime data channels processed --> try alarm data
|
|||
metricsStep = metricsStateAlarmData; |
|||
} |
|||
break; |
|||
|
|||
case metricsStateAlarmData: // Alarm Info loop
|
|||
iv = mSys->getInverterByPos(metricsInverterId); |
|||
rec = iv->getRecordStruct(AlarmData); |
|||
// simple hack : there is only one channel with alarm data
|
|||
// TODO: find the right one channel with the alarm id
|
|||
alarmChannelId = 0; |
|||
// printf("AlarmData Length %d\n",rec->length);
|
|||
if (alarmChannelId < rec->length) { |
|||
//uint8_t channel = rec->assign[alarmChannelId].ch;
|
|||
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); |
|||
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); |
|||
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); |
|||
snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); |
|||
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); |
|||
} else { |
|||
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
|
|||
} |
|||
// alarm channel processed --> try next inverter
|
|||
metricsInverterId++; |
|||
metricsStep = metricsStateInverter; |
|||
break; |
|||
|
|||
case metricsStateEnd: |
|||
default: // end of transmission
|
|||
len = 0; |
|||
break; |
|||
} |
|||
return len; |
|||
}); |
|||
request->send(response); |
|||
} |
|||
|
|||
String radioStatistic(String statistic, uint32_t value) { |
|||
char type[60], topic[80], val[25]; |
|||
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); |
|||
snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str()); |
|||
snprintf(val, sizeof(val), "%d", value); |
|||
return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n"); |
|||
} |
|||
|
|||
std::pair<String, String> convertToPromUnits(String shortUnit) { |
|||
if(shortUnit == "A") return {"_ampere", "gauge"}; |
|||
if(shortUnit == "V") return {"_volt", "gauge"}; |
|||
if(shortUnit == "%") return {"_ratio", "gauge"}; |
|||
if(shortUnit == "W") return {"_watt", "gauge"}; |
|||
if(shortUnit == "Wh") return {"_wattHours", "counter"}; |
|||
if(shortUnit == "kWh") return {"_kilowattHours", "counter"}; |
|||
if(shortUnit == "°C") return {"_celsius", "gauge"}; |
|||
if(shortUnit == "var") return {"_var", "gauge"}; |
|||
if(shortUnit == "Hz") return {"_hertz", "gauge"}; |
|||
return {"", "gauge"}; |
|||
} |
|||
#endif |
|||
AsyncWebServer mWeb; |
|||
AsyncEventSource mEvts; |
|||
bool mProtected; |
|||
uint32_t mLogoutTimeout; |
|||
IApp *mApp; |
|||
HMSYSTEM *mSys; |
|||
|
|||
settings_t *mConfig; |
|||
|
|||
bool mSerialAddTime; |
|||
char mSerialBuf[WEB_SERIAL_BUF_SIZE]; |
|||
uint16_t mSerialBufFill; |
|||
bool mSerialClientConnnected; |
|||
|
|||
File mUploadFp; |
|||
bool mUploadFail; |
|||
}; |
|||
|
|||
#endif /*__WEB_H__*/ |
@ -0,0 +1,413 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#if defined(ESP32) && defined(F) |
|||
#undef F |
|||
#define F(sl) (sl) |
|||
#endif |
|||
#include "ahoywifi.h" |
|||
|
|||
// NTP CONFIG
|
|||
#define NTP_PACKET_SIZE 48 |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
ahoywifi::ahoywifi() : mApIp(192, 168, 4, 1) {} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) { |
|||
mConfig = config; |
|||
mUtcTimestamp = utcTimestamp; |
|||
mAppWifiCb = cb; |
|||
|
|||
mStaConn = DISCONNECTED; |
|||
mCnt = 0; |
|||
mScanActive = false; |
|||
|
|||
#if defined(ESP8266) |
|||
wifiConnectHandler = WiFi.onStationModeConnected(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1)); |
|||
wifiGotIPHandler = WiFi.onStationModeGotIP(std::bind(&ahoywifi::onGotIP, this, std::placeholders::_1)); |
|||
wifiDisconnectHandler = WiFi.onStationModeDisconnected(std::bind(&ahoywifi::onDisconnect, this, std::placeholders::_1)); |
|||
#else |
|||
WiFi.onEvent(std::bind(&ahoywifi::onWiFiEvent, this, std::placeholders::_1)); |
|||
#endif |
|||
|
|||
setupWifi(true); |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::setupWifi(bool startAP = false) { |
|||
#if !defined(FB_WIFI_OVERRIDDEN) |
|||
if(startAP) { |
|||
setupAp(); |
|||
delay(1000); |
|||
} |
|||
#endif |
|||
#if !defined(AP_ONLY) |
|||
if(mConfig->valid) { |
|||
#if !defined(FB_WIFI_OVERRIDDEN) |
|||
if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) != 0) |
|||
setupStation(); |
|||
#else |
|||
setupStation(); |
|||
#endif |
|||
} |
|||
#endif |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::tickWifiLoop() { |
|||
#if !defined(AP_ONLY) |
|||
if(mStaConn != GOT_IP) { |
|||
if (WiFi.softAPgetStationNum() > 0) { // do not reconnect if any AP connection exists
|
|||
if(mStaConn != IN_AP_MODE) { |
|||
mStaConn = IN_AP_MODE; |
|||
// first time switch to AP Mode
|
|||
if (mScanActive) { |
|||
WiFi.scanDelete(); |
|||
mScanActive = false; |
|||
} |
|||
DBGPRINTLN(F("AP client connected")); |
|||
welcome(mApIp.toString(), ""); |
|||
WiFi.mode(WIFI_AP); |
|||
mDns.start(53, "*", mApIp); |
|||
mAppWifiCb(true); |
|||
} |
|||
mDns.processNextRequest(); |
|||
return; |
|||
} |
|||
else if(mStaConn == IN_AP_MODE) { |
|||
mCnt = 0; |
|||
mDns.stop(); |
|||
WiFi.mode(WIFI_AP_STA); |
|||
mStaConn = DISCONNECTED; |
|||
} |
|||
mCnt++; |
|||
|
|||
uint8_t timeout = (mStaConn == DISCONNECTED) ? 10 : 20; // seconds
|
|||
if (mStaConn == CONNECTED) // connected but no ip
|
|||
timeout = 20; |
|||
|
|||
if(!mScanActive && mBSSIDList.empty() && (mStaConn == DISCONNECTED)) { // start scanning APs with the given SSID
|
|||
DBGPRINT(F("scanning APs with SSID ")); |
|||
DBGPRINTLN(String(mConfig->sys.stationSsid)); |
|||
mScanCnt = 0; |
|||
mScanActive = true; |
|||
#if defined(ESP8266) |
|||
WiFi.scanNetworks(true, false, 0U, (uint8_t *)mConfig->sys.stationSsid); |
|||
#else |
|||
WiFi.scanNetworks(true, false, false, 300U, 0U, mConfig->sys.stationSsid); |
|||
#endif |
|||
return; |
|||
} |
|||
DBGPRINT(F("reconnect in ")); |
|||
DBGPRINT(String(timeout-mCnt)); |
|||
DBGPRINTLN(F(" seconds")); |
|||
if(mScanActive) { |
|||
getBSSIDs(); |
|||
if(!mScanActive) // scan completed
|
|||
if ((mCnt % timeout) < timeout - 2) |
|||
mCnt = timeout - 2; |
|||
} |
|||
if((mCnt % timeout) == 0) { // try to reconnect after x sec without connection
|
|||
mStaConn = CONNECTING; |
|||
WiFi.disconnect(); |
|||
|
|||
if(mBSSIDList.size() > 0) { // get first BSSID in list
|
|||
DBGPRINT(F("try to connect to AP with BSSID:")); |
|||
uint8_t bssid[6]; |
|||
for (int j = 0; j < 6; j++) { |
|||
bssid[j] = mBSSIDList.front(); |
|||
mBSSIDList.pop_front(); |
|||
DBGPRINT(" " + String(bssid[j], HEX)); |
|||
} |
|||
DBGPRINTLN(""); |
|||
WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]); |
|||
} |
|||
else |
|||
mStaConn = DISCONNECTED; |
|||
|
|||
mCnt = 0; |
|||
} |
|||
} |
|||
#endif |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::setupAp(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("wifi::setupAp")); |
|||
|
|||
DBGPRINTLN(F("\n---------\nAhoyDTU Info:")); |
|||
DBGPRINT(F("Version: ")); |
|||
DBGPRINT(String(VERSION_MAJOR)); |
|||
DBGPRINT(F(".")); |
|||
DBGPRINT(String(VERSION_MINOR)); |
|||
DBGPRINT(F(".")); |
|||
DBGPRINTLN(String(VERSION_PATCH)); |
|||
DBGPRINT(F("Github Hash: ")); |
|||
DBGPRINTLN(String(AUTO_GIT_HASH)); |
|||
|
|||
DBGPRINT(F("\n---------\nAP MODE\nSSID: ")); |
|||
DBGPRINTLN(WIFI_AP_SSID); |
|||
DBGPRINT(F("PWD: ")); |
|||
DBGPRINTLN(WIFI_AP_PWD); |
|||
DBGPRINT(F("IP Address: http://")); |
|||
DBGPRINTLN(mApIp.toString()); |
|||
DBGPRINTLN(F("---------\n")); |
|||
|
|||
WiFi.mode(WIFI_AP_STA); |
|||
WiFi.softAPConfig(mApIp, mApIp, IPAddress(255, 255, 255, 0)); |
|||
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PWD); |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::setupStation(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("wifi::setupStation")); |
|||
if(mConfig->sys.ip.ip[0] != 0) { |
|||
IPAddress ip(mConfig->sys.ip.ip); |
|||
IPAddress mask(mConfig->sys.ip.mask); |
|||
IPAddress dns1(mConfig->sys.ip.dns1); |
|||
IPAddress dns2(mConfig->sys.ip.dns2); |
|||
IPAddress gateway(mConfig->sys.ip.gateway); |
|||
if(!WiFi.config(ip, gateway, mask, dns1, dns2)) |
|||
DPRINTLN(DBG_ERROR, F("failed to set static IP!")); |
|||
} |
|||
mBSSIDList.clear(); |
|||
if(String(mConfig->sys.deviceName) != "") |
|||
WiFi.hostname(mConfig->sys.deviceName); |
|||
WiFi.mode(WIFI_AP_STA); |
|||
|
|||
|
|||
DBGPRINT(F("connect to network '")); |
|||
DBGPRINT(mConfig->sys.stationSsid); |
|||
DBGPRINTLN(F("' ...")); |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
bool ahoywifi::getNtpTime(void) { |
|||
if(GOT_IP != mStaConn) |
|||
return false; |
|||
|
|||
IPAddress timeServer; |
|||
uint8_t buf[NTP_PACKET_SIZE]; |
|||
uint8_t retry = 0; |
|||
|
|||
if (WiFi.hostByName(mConfig->ntp.addr, timeServer) != 1) |
|||
return false; |
|||
|
|||
mUdp.begin(mConfig->ntp.port); |
|||
sendNTPpacket(timeServer); |
|||
|
|||
while(retry++ < 5) { |
|||
int wait = 150; |
|||
while(--wait) { |
|||
if(NTP_PACKET_SIZE <= mUdp.parsePacket()) { |
|||
uint64_t secsSince1900; |
|||
mUdp.read(buf, NTP_PACKET_SIZE); |
|||
secsSince1900 = (buf[40] << 24); |
|||
secsSince1900 |= (buf[41] << 16); |
|||
secsSince1900 |= (buf[42] << 8); |
|||
secsSince1900 |= (buf[43] ); |
|||
|
|||
*mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
|
|||
DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC"); |
|||
return true; |
|||
} else |
|||
delay(10); |
|||
} |
|||
} |
|||
|
|||
DPRINTLN(DBG_INFO, F("[NTP]: getNtpTime failed")); |
|||
return false; |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::sendNTPpacket(IPAddress& address) { |
|||
//DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket"));
|
|||
uint8_t buf[NTP_PACKET_SIZE] = {0}; |
|||
|
|||
buf[0] = B11100011; // LI, Version, Mode
|
|||
buf[1] = 0; // Stratum
|
|||
buf[2] = 6; // Max Interval between messages in seconds
|
|||
buf[3] = 0xEC; // Clock Precision
|
|||
// bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset
|
|||
buf[12] = 49; // four-byte reference ID identifying
|
|||
buf[13] = 0x4E; |
|||
buf[14] = 49; |
|||
buf[15] = 52; |
|||
|
|||
mUdp.beginPacket(address, 123); // NTP request, port 123
|
|||
mUdp.write(buf, NTP_PACKET_SIZE); |
|||
mUdp.endPacket(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::sortRSSI(int *sort, int n) { |
|||
for (int i = 0; i < n; i++) |
|||
sort[i] = i; |
|||
for (int i = 0; i < n; i++) |
|||
for (int j = i + 1; j < n; j++) |
|||
if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i])) |
|||
std::swap(sort[i], sort[j]); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::scanAvailNetworks(void) { |
|||
if(!mScanActive) { |
|||
mScanActive = true; |
|||
if(WIFI_AP == WiFi.getMode()) |
|||
WiFi.mode(WIFI_AP_STA); |
|||
WiFi.scanNetworks(true); |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::getAvailNetworks(JsonObject obj) { |
|||
JsonArray nets = obj.createNestedArray("networks"); |
|||
|
|||
int n = WiFi.scanComplete(); |
|||
if (n < 0) |
|||
return; |
|||
if(n > 0) { |
|||
int sort[n]; |
|||
sortRSSI(&sort[0], n); |
|||
for (int i = 0; i < n; ++i) { |
|||
nets[i]["ssid"] = WiFi.SSID(sort[i]); |
|||
nets[i]["rssi"] = WiFi.RSSI(sort[i]); |
|||
} |
|||
} |
|||
mScanActive = false; |
|||
WiFi.scanDelete(); |
|||
if(mStaConn == IN_AP_MODE) |
|||
WiFi.mode(WIFI_AP); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::getBSSIDs() { |
|||
int n = WiFi.scanComplete(); |
|||
if (n < 0) { |
|||
mScanCnt++; |
|||
if (mScanCnt < 20) |
|||
return; |
|||
} |
|||
if(n > 0) { |
|||
mBSSIDList.clear(); |
|||
int sort[n]; |
|||
sortRSSI(&sort[0], n); |
|||
for (int i = 0; i < n; i++) { |
|||
DBGPRINT("BSSID " + String(i) + ":"); |
|||
uint8_t *bssid = WiFi.BSSID(sort[i]); |
|||
for (int j = 0; j < 6; j++){ |
|||
DBGPRINT(" " + String(bssid[j], HEX)); |
|||
mBSSIDList.push_back(bssid[j]); |
|||
} |
|||
DBGPRINTLN(""); |
|||
} |
|||
} |
|||
mScanActive = false; |
|||
WiFi.scanDelete(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::connectionEvent(WiFiStatus_t status) { |
|||
DPRINTLN(DBG_INFO, "connectionEvent"); |
|||
|
|||
switch(status) { |
|||
case CONNECTED: |
|||
if(mStaConn != CONNECTED) { |
|||
mStaConn = CONNECTED; |
|||
DBGPRINTLN(F("\n[WiFi] Connected")); |
|||
} |
|||
break; |
|||
|
|||
case GOT_IP: |
|||
mStaConn = GOT_IP; |
|||
if (mScanActive) { // maybe another scan has started
|
|||
WiFi.scanDelete(); |
|||
mScanActive = false; |
|||
} |
|||
welcome(WiFi.localIP().toString(), F(" (Station)")); |
|||
WiFi.softAPdisconnect(); |
|||
WiFi.mode(WIFI_STA); |
|||
DBGPRINTLN(F("[WiFi] AP disabled")); |
|||
delay(100); |
|||
mAppWifiCb(true); |
|||
break; |
|||
|
|||
case DISCONNECTED: |
|||
if(mStaConn != CONNECTING) { |
|||
mStaConn = DISCONNECTED; |
|||
mCnt = 5; // try to reconnect in 5 sec
|
|||
setupWifi(); // reconnect with AP / Station setup
|
|||
mAppWifiCb(false); |
|||
DPRINTLN(DBG_INFO, "[WiFi] Connection Lost"); |
|||
} |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
#if defined(ESP8266) |
|||
//-------------------------------------------------------------------------
|
|||
void ahoywifi::onConnect(const WiFiEventStationModeConnected& event) { |
|||
connectionEvent(CONNECTED); |
|||
} |
|||
|
|||
//-------------------------------------------------------------------------
|
|||
void ahoywifi::onGotIP(const WiFiEventStationModeGotIP& event) { |
|||
connectionEvent(GOT_IP); |
|||
} |
|||
|
|||
//-------------------------------------------------------------------------
|
|||
void ahoywifi::onDisconnect(const WiFiEventStationModeDisconnected& event) { |
|||
connectionEvent(DISCONNECTED); |
|||
} |
|||
|
|||
#else |
|||
//-------------------------------------------------------------------------
|
|||
void ahoywifi::onWiFiEvent(WiFiEvent_t event) { |
|||
DBGPRINT(F("Wifi event: ")); |
|||
DBGPRINTLN(String(event)); |
|||
|
|||
switch(event) { |
|||
case SYSTEM_EVENT_STA_CONNECTED: |
|||
connectionEvent(CONNECTED); |
|||
break; |
|||
|
|||
case SYSTEM_EVENT_STA_GOT_IP: |
|||
connectionEvent(GOT_IP); |
|||
break; |
|||
|
|||
case SYSTEM_EVENT_STA_DISCONNECTED: |
|||
connectionEvent(DISCONNECTED); |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
#endif |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::welcome(String ip, String mode) { |
|||
DBGPRINTLN(F("\n\n--------------------------------")); |
|||
DBGPRINTLN(F("Welcome to AHOY!")); |
|||
DBGPRINT(F("\npoint your browser to http://")); |
|||
DBGPRINT(ip); |
|||
DBGPRINTLN(mode); |
|||
DBGPRINTLN(F("to configure your device")); |
|||
DBGPRINTLN(F("--------------------------------\n")); |
|||
} |
@ -0,0 +1,77 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __AHOYWIFI_H__ |
|||
#define __AHOYWIFI_H__ |
|||
|
|||
#include "../utils/dbg.h" |
|||
#include <Arduino.h> |
|||
#include <WiFiUdp.h> |
|||
#include <DNSServer.h> |
|||
#include "ESPAsyncWebServer.h" |
|||
|
|||
#include "../config/settings.h" |
|||
|
|||
class app; |
|||
|
|||
class ahoywifi { |
|||
public: |
|||
typedef std::function<void(bool)> appWifiCb; |
|||
|
|||
ahoywifi(); |
|||
|
|||
|
|||
void setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb); |
|||
void tickWifiLoop(void); |
|||
bool getNtpTime(void); |
|||
void scanAvailNetworks(void); |
|||
void getAvailNetworks(JsonObject obj); |
|||
|
|||
private: |
|||
typedef enum WiFiStatus { |
|||
DISCONNECTED = 0, |
|||
CONNECTING, |
|||
CONNECTED, |
|||
IN_AP_MODE, |
|||
GOT_IP |
|||
} WiFiStatus_t; |
|||
|
|||
void setupWifi(bool startAP); |
|||
void setupAp(void); |
|||
void setupStation(void); |
|||
void sendNTPpacket(IPAddress& address); |
|||
void sortRSSI(int *sort, int n); |
|||
void getBSSIDs(void); |
|||
void connectionEvent(WiFiStatus_t status); |
|||
#if defined(ESP8266) |
|||
void onConnect(const WiFiEventStationModeConnected& event); |
|||
void onGotIP(const WiFiEventStationModeGotIP& event); |
|||
void onDisconnect(const WiFiEventStationModeDisconnected& event); |
|||
#else |
|||
void onWiFiEvent(WiFiEvent_t event); |
|||
#endif |
|||
void welcome(String ip, String mode); |
|||
|
|||
|
|||
settings_t *mConfig; |
|||
appWifiCb mAppWifiCb; |
|||
|
|||
DNSServer mDns; |
|||
IPAddress mApIp; |
|||
WiFiUDP mUdp; // for time server
|
|||
#if defined(ESP8266) |
|||
WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler; |
|||
#endif |
|||
|
|||
WiFiStatus_t mStaConn; |
|||
uint8_t mCnt; |
|||
uint32_t *mUtcTimestamp; |
|||
|
|||
uint8_t mScanCnt; |
|||
bool mScanActive; |
|||
std::list<uint8_t> mBSSIDList; |
|||
}; |
|||
|
|||
#endif /*__AHOYWIFI_H__*/ |
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,30 @@ |
|||
# EKD ESPNRF Case |
|||
<picture> |
|||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/10756851/221722400-eefc8790-6283-4c00-a82b-e2699cae72d6.jpg"> |
|||
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/10756851/221722400-eefc8790-6283-4c00-a82b-e2699cae72d6.jpg"> |
|||
<img alt="EKD ESPNRF Case" src="https://user-images.githubusercontent.com/10756851/221722400-eefc8790-6283-4c00-a82b-e2699cae72d6.jpg"> |
|||
</picture> |
|||
|
|||
### Print Details: |
|||
- Print with 0.2 mm Layers |
|||
- use 100% infill |
|||
- no supports needed |
|||
|
|||
### Things needed: |
|||
- 3D Printer |
|||
- Wemos D1 Mini (format style) |
|||
- NRF24L01+ Board |
|||
- ~ 15cm wire |
|||
- Soldering Iron + Solder |
|||
- Suction pump to free the NRF Board from the pins. |
|||
(Solder wick works too but i do not recommend =) |
|||
- If you want to go for a wall mounted device, add some screws. |
|||
|
|||
|
|||
Unsolder the Pins from the NRF Board and use short wires instead. I went this way to keep the design as flat as possible. |
|||
<picture> |
|||
<img alt="EKD ESPNRF Case" src="https://user-images.githubusercontent.com/10756851/221722732-1ae9162c-ef77-492e-babf-075045b81f69.png"> |
|||
</picture> |
|||
If you got questions or need help feel free to ask on discord. |
|||
or find me on github.com/subdancer |
|||
Cheers. |
@ -1,23 +0,0 @@ |
|||
// Place your settings in this file to overwrite default and user settings. |
|||
{ |
|||
// identify that settings is loaded |
|||
"workbench.colorCustomizations": { |
|||
"editorLineNumber.foreground": "#00ff00" |
|||
}, |
|||
|
|||
"editor.wordWrap": "off", |
|||
"files.eol" : "\n", |
|||
"files.trimTrailingWhitespace" : true, |
|||
|
|||
"diffEditor.ignoreTrimWhitespace": true, |
|||
"files.autoSave": "afterDelay", |
|||
|
|||
"editor.tabSize": 4, |
|||
"editor.insertSpaces": true, |
|||
// `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents. |
|||
// Set to false to keep the values you've explicitly set, above. |
|||
"editor.detectIndentation": false, |
|||
|
|||
// https://clang.llvm.org/docs/ClangFormatStyleOptions.html |
|||
"C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}", |
|||
} |
@ -1,7 +0,0 @@ |
|||
# Changelog |
|||
|
|||
- v0.5.17 |
|||
* Bug fix for 1 channel inverters (HM300, HM400) see #246 |
|||
* Bug fix for read back the active power limit from inverter #243 (before version 0.5.16 the reported limit was just a copy of the user set point, now it is the actual value which the inverter uses) |
|||
* Update the [user manual](https://github.com/grindylow/ahoy/blob/main/tools/esp8266/User_Manual.md); added section aobut the published data on mqtt; section about zero export control; added section about code implementation command queue |
|||
* Added tx-Id number to packet payload struct. (eg. can be 0x95 or 0xD1) --> less messages fails and faster handling of changing power limit |
@ -1,161 +0,0 @@ |
|||
/*
|
|||
CircularBuffer - An Arduino circular buffering library for arbitrary types. |
|||
|
|||
Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl |
|||
|
|||
This library is free software; you can redistribute it and/or |
|||
modify it under the terms of the GNU Lesser General Public |
|||
License as published by the Free Software Foundation; either |
|||
version 2.1 of the License, or (at your option) any later version. |
|||
|
|||
This library is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|||
Lesser General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU Lesser General Public |
|||
License along with this library; if not, write to the Free Software |
|||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
|||
*/ |
|||
|
|||
#ifndef CircularBuffer_h |
|||
#define CircularBuffer_h |
|||
|
|||
#if defined(ESP8266) || defined(ESP32) |
|||
#define DISABLE_IRQ noInterrupts() |
|||
#define RESTORE_IRQ interrupts() |
|||
#else |
|||
#define DISABLE_IRQ \ |
|||
uint8_t sreg = SREG; \ |
|||
cli(); |
|||
|
|||
#define RESTORE_IRQ \ |
|||
SREG = sreg; |
|||
#endif |
|||
|
|||
template <class BUFFERTYPE, uint8_t BUFFERSIZE> |
|||
class CircularBuffer { |
|||
|
|||
typedef BUFFERTYPE BufferType; |
|||
BufferType Buffer[BUFFERSIZE]; |
|||
|
|||
public: |
|||
CircularBuffer() : m_buff(Buffer) { |
|||
m_size = BUFFERSIZE; |
|||
clear(); |
|||
} |
|||
|
|||
/** Clear all entries in the circular buffer. */ |
|||
void clear(void) |
|||
{ |
|||
m_front = 0; |
|||
m_fill = 0; |
|||
} |
|||
|
|||
/** Test if the circular buffer is empty */ |
|||
inline bool empty(void) const |
|||
{ |
|||
return !m_fill; |
|||
} |
|||
|
|||
/** Return the number of records stored in the buffer */ |
|||
inline uint8_t available(void) const |
|||
{ |
|||
return m_fill; |
|||
} |
|||
|
|||
/** Test if the circular buffer is full */ |
|||
inline bool full(void) const |
|||
{ |
|||
return m_fill == m_size; |
|||
} |
|||
|
|||
inline uint8_t getFill(void) const { |
|||
return m_fill; |
|||
} |
|||
|
|||
/** Aquire record on front of the buffer, for writing.
|
|||
* After filling the record, it has to be pushed to actually |
|||
* add it to the buffer. |
|||
* @return Pointer to record, or NULL when buffer is full. |
|||
*/ |
|||
BUFFERTYPE* getFront(void) const |
|||
{ |
|||
DISABLE_IRQ; |
|||
BUFFERTYPE* f = NULL; |
|||
if (!full()) |
|||
f = get(m_front); |
|||
RESTORE_IRQ; |
|||
return f; |
|||
} |
|||
|
|||
/** Push record to front of the buffer
|
|||
* @param record Record to push. If record was aquired previously (using getFront) its |
|||
* data will not be copied as it is already present in the buffer. |
|||
* @return True, when record was pushed successfully. |
|||
*/ |
|||
bool pushFront(BUFFERTYPE* record) |
|||
{ |
|||
bool ok = false; |
|||
DISABLE_IRQ; |
|||
if (!full()) |
|||
{ |
|||
BUFFERTYPE* f = get(m_front); |
|||
if (f != record) |
|||
*f = *record; |
|||
m_front = (m_front+1) % m_size; |
|||
m_fill++; |
|||
ok = true; |
|||
} |
|||
RESTORE_IRQ; |
|||
return ok; |
|||
} |
|||
|
|||
/** Aquire record on back of the buffer, for reading.
|
|||
* After reading the record, it has to be pop'ed to actually |
|||
* remove it from the buffer. |
|||
* @return Pointer to record, or NULL when buffer is empty. |
|||
*/ |
|||
BUFFERTYPE* getBack(void) const |
|||
{ |
|||
BUFFERTYPE* b = NULL; |
|||
DISABLE_IRQ; |
|||
if (!empty()) |
|||
b = get(back()); |
|||
RESTORE_IRQ; |
|||
return b; |
|||
} |
|||
|
|||
/** Remove record from back of the buffer.
|
|||
* @return True, when record was pop'ed successfully. |
|||
*/ |
|||
bool popBack(void) |
|||
{ |
|||
bool ok = false; |
|||
DISABLE_IRQ; |
|||
if (!empty()) |
|||
{ |
|||
m_fill--; |
|||
ok = true; |
|||
} |
|||
RESTORE_IRQ; |
|||
return ok; |
|||
} |
|||
|
|||
protected: |
|||
inline BUFFERTYPE * get(const uint8_t idx) const |
|||
{ |
|||
return &(m_buff[idx]); |
|||
} |
|||
inline uint8_t back(void) const |
|||
{ |
|||
return (m_front - m_fill + m_size) % m_size; |
|||
} |
|||
|
|||
uint8_t m_size; // Total number of records that can be stored in the buffer.
|
|||
BUFFERTYPE* const m_buff; |
|||
volatile uint8_t m_front; // Index of front element (not pushed yet).
|
|||
volatile uint8_t m_fill; // Amount of records currently pushed.
|
|||
}; |
|||
|
|||
#endif // CircularBuffer_h
|
@ -1,260 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#if defined(ESP32) && defined(F) |
|||
#undef F |
|||
#define F(sl) (sl) |
|||
#endif |
|||
#include "ahoywifi.h" |
|||
|
|||
|
|||
// NTP CONFIG
|
|||
#define NTP_PACKET_SIZE 48 |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
ahoywifi::ahoywifi(app *main, sysConfig_t *sysCfg, config_t *config) { |
|||
mMain = main; |
|||
mSysCfg = sysCfg; |
|||
mConfig = config; |
|||
|
|||
mDns = new DNSServer(); |
|||
mUdp = new WiFiUDP(); |
|||
|
|||
mWifiStationTimeout = 10; |
|||
wifiWasEstablished = false; |
|||
mNextTryTs = 0; |
|||
mApLastTick = 0; |
|||
mApActive = false; |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::setup(uint32_t timeout, bool settingValid) { |
|||
mWifiStationTimeout = timeout; |
|||
#ifndef AP_ONLY |
|||
if(false == mApActive) |
|||
mApActive = setupStation(mWifiStationTimeout); |
|||
#endif |
|||
|
|||
if(!settingValid) { |
|||
DPRINTLN(DBG_WARN, F("your settings are not valid! check [IP]/setup")); |
|||
mApActive = true; |
|||
mApLastTick = millis(); |
|||
mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000)); |
|||
setupAp(WIFI_AP_SSID, WIFI_AP_PWD); |
|||
} |
|||
else { |
|||
DPRINTLN(DBG_INFO, F("\n\n----------------------------------------")); |
|||
DPRINTLN(DBG_INFO, F("Welcome to AHOY!")); |
|||
DPRINT(DBG_INFO, F("\npoint your browser to http://")); |
|||
if(mApActive) |
|||
DBGPRINTLN(F("192.168.1.1")); |
|||
else |
|||
DBGPRINTLN(WiFi.localIP().toString()); |
|||
DPRINTLN(DBG_INFO, F("to configure your device")); |
|||
DPRINTLN(DBG_INFO, F("----------------------------------------\n")); |
|||
} |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
bool ahoywifi::loop(void) { |
|||
if(mApActive) { |
|||
mDns->processNextRequest(); |
|||
#ifndef AP_ONLY |
|||
if(mMain->checkTicker(&mNextTryTs, (WIFI_AP_ACTIVE_TIME * 1000))) { |
|||
mApActive = setupStation(mWifiStationTimeout); |
|||
if(mApActive) { |
|||
if(strlen(WIFI_AP_PWD) < 8) |
|||
DPRINTLN(DBG_ERROR, F("password must be at least 8 characters long")); |
|||
mApLastTick = millis(); |
|||
mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000)); |
|||
setupAp(WIFI_AP_SSID, WIFI_AP_PWD); |
|||
} |
|||
} |
|||
else { |
|||
if(millis() - mApLastTick > 10000) { |
|||
uint8_t cnt = WiFi.softAPgetStationNum(); |
|||
if(cnt > 0) { |
|||
DPRINTLN(DBG_INFO, String(cnt) + F(" client connected, resetting AP timeout")); |
|||
mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000)); |
|||
} |
|||
mApLastTick = millis(); |
|||
DPRINTLN(DBG_INFO, F("AP will be closed in ") + String((mNextTryTs - mApLastTick) / 1000) + F(" seconds")); |
|||
} |
|||
} |
|||
#endif |
|||
} |
|||
|
|||
if((WiFi.status() != WL_CONNECTED) && wifiWasEstablished) { |
|||
if(!mApActive) { |
|||
DPRINTLN(DBG_INFO, "[WiFi]: Connection Lost"); |
|||
mApActive = setupStation(mWifiStationTimeout); |
|||
} |
|||
} |
|||
|
|||
return mApActive; |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::setupAp(const char *ssid, const char *pwd) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::setupAp")); |
|||
IPAddress apIp(192, 168, 1, 1); |
|||
|
|||
DPRINTLN(DBG_INFO, F("\n---------\nAP MODE\nSSID: ") |
|||
+ String(ssid) + F("\nPWD: ") |
|||
+ String(pwd) + F("\nActive for: ") |
|||
+ String(WIFI_AP_ACTIVE_TIME) + F(" seconds") |
|||
+ F("\n---------\n")); |
|||
DPRINTLN(DBG_DEBUG, String(mNextTryTs)); |
|||
|
|||
WiFi.mode(WIFI_AP); |
|||
WiFi.softAPConfig(apIp, apIp, IPAddress(255, 255, 255, 0)); |
|||
WiFi.softAP(ssid, pwd); |
|||
|
|||
mDns->start(53, "*", apIp); |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
bool ahoywifi::setupStation(uint32_t timeout) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::setupStation")); |
|||
int32_t cnt; |
|||
bool startAp = false; |
|||
|
|||
if(timeout >= 3) |
|||
cnt = (timeout - 3) / 2 * 10; |
|||
else { |
|||
timeout = 1; |
|||
cnt = 1; |
|||
} |
|||
|
|||
WiFi.mode(WIFI_STA); |
|||
WiFi.begin(mSysCfg->stationSsid, mSysCfg->stationPwd); |
|||
if(String(mSysCfg->deviceName) != "") |
|||
WiFi.hostname(mSysCfg->deviceName); |
|||
|
|||
delay(2000); |
|||
DPRINTLN(DBG_INFO, F("connect to network '") + String(mSysCfg->stationSsid) + F("' ...")); |
|||
while (WiFi.status() != WL_CONNECTED) { |
|||
delay(100); |
|||
if(cnt % 40 == 0) |
|||
Serial.println("."); |
|||
else |
|||
Serial.print("."); |
|||
|
|||
if(timeout > 0) { // limit == 0 -> no limit
|
|||
if(--cnt <= 0) { |
|||
if(WiFi.status() != WL_CONNECTED) { |
|||
startAp = true; |
|||
WiFi.disconnect(); |
|||
} |
|||
delay(100); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
Serial.println("."); |
|||
|
|||
if(false == startAp) |
|||
wifiWasEstablished = true; |
|||
|
|||
delay(1000); |
|||
return startAp; |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
bool ahoywifi::getApActive(void) { |
|||
return mApActive; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
time_t ahoywifi::getNtpTime(void) { |
|||
//DPRINTLN(DBG_VERBOSE, F("wifi::getNtpTime"));
|
|||
time_t date = 0; |
|||
IPAddress timeServer; |
|||
uint8_t buf[NTP_PACKET_SIZE]; |
|||
uint8_t retry = 0; |
|||
|
|||
WiFi.hostByName(mConfig->ntpAddr, timeServer); |
|||
mUdp->begin(mConfig->ntpPort); |
|||
|
|||
sendNTPpacket(timeServer); |
|||
|
|||
while(retry++ < 5) { |
|||
int wait = 150; |
|||
while(--wait) { |
|||
if(NTP_PACKET_SIZE <= mUdp->parsePacket()) { |
|||
uint64_t secsSince1900; |
|||
mUdp->read(buf, NTP_PACKET_SIZE); |
|||
secsSince1900 = (buf[40] << 24); |
|||
secsSince1900 |= (buf[41] << 16); |
|||
secsSince1900 |= (buf[42] << 8); |
|||
secsSince1900 |= (buf[43] ); |
|||
|
|||
date = secsSince1900 - 2208988800UL; // UTC time
|
|||
break; |
|||
} |
|||
else |
|||
delay(10); |
|||
} |
|||
} |
|||
|
|||
return date; |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::scanAvailNetworks(void) { |
|||
int n = WiFi.scanComplete(); |
|||
if(n == -2) |
|||
WiFi.scanNetworks(true); |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::getAvailNetworks(JsonObject obj) { |
|||
JsonArray nets = obj.createNestedArray("networks"); |
|||
|
|||
int n = WiFi.scanComplete(); |
|||
if(n > 0) { |
|||
int sort[n]; |
|||
for (int i = 0; i < n; i++) |
|||
sort[i] = i; |
|||
for (int i = 0; i < n; i++) |
|||
for (int j = i + 1; j < n; j++) |
|||
if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i])) |
|||
std::swap(sort[i], sort[j]); |
|||
for (int i = 0; i < n; ++i) { |
|||
nets[i]["ssid"] = WiFi.SSID(sort[i]); |
|||
nets[i]["rssi"] = WiFi.RSSI(sort[i]); |
|||
} |
|||
WiFi.scanDelete(); |
|||
} |
|||
} |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void ahoywifi::sendNTPpacket(IPAddress& address) { |
|||
//DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket"));
|
|||
uint8_t buf[NTP_PACKET_SIZE] = {0}; |
|||
|
|||
buf[0] = B11100011; // LI, Version, Mode
|
|||
buf[1] = 0; // Stratum
|
|||
buf[2] = 6; // Max Interval between messages in seconds
|
|||
buf[3] = 0xEC; // Clock Precision
|
|||
// bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset
|
|||
buf[12] = 49; // four-byte reference ID identifying
|
|||
buf[13] = 0x4E; |
|||
buf[14] = 49; |
|||
buf[15] = 52; |
|||
|
|||
mUdp->beginPacket(address, 123); // NTP request, port 123
|
|||
mUdp->write(buf, NTP_PACKET_SIZE); |
|||
mUdp->endPacket(); |
|||
} |
@ -1,53 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __AHOYWIFI_H__ |
|||
#define __AHOYWIFI_H__ |
|||
|
|||
#include "dbg.h" |
|||
|
|||
// NTP
|
|||
#include <WiFiUdp.h> |
|||
#include <TimeLib.h> |
|||
#include <DNSServer.h> |
|||
|
|||
#include "defines.h" |
|||
|
|||
#include "app.h" |
|||
|
|||
class app; |
|||
|
|||
class ahoywifi { |
|||
public: |
|||
ahoywifi(app *main, sysConfig_t *sysCfg, config_t *config); |
|||
~ahoywifi() {} |
|||
|
|||
void setup(uint32_t timeout, bool settingValid); |
|||
bool loop(void); |
|||
void setupAp(const char *ssid, const char *pwd); |
|||
bool setupStation(uint32_t timeout); |
|||
bool getApActive(void); |
|||
time_t getNtpTime(void); |
|||
void scanAvailNetworks(void); |
|||
void getAvailNetworks(JsonObject obj); |
|||
|
|||
private: |
|||
void sendNTPpacket(IPAddress& address); |
|||
|
|||
config_t *mConfig; |
|||
sysConfig_t *mSysCfg; |
|||
app *mMain; |
|||
|
|||
DNSServer *mDns; |
|||
WiFiUDP *mUdp; // for time server
|
|||
|
|||
uint32_t mWifiStationTimeout; |
|||
uint32_t mNextTryTs; |
|||
uint32_t mApLastTick; |
|||
bool mApActive; |
|||
bool wifiWasEstablished; |
|||
}; |
|||
|
|||
#endif /*__AHOYWIFI_H__*/ |
@ -1,907 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#if defined(ESP32) && defined(F) |
|||
#undef F |
|||
#define F(sl) (sl) |
|||
#endif |
|||
|
|||
#include "app.h" |
|||
|
|||
#include <ArduinoJson.h> |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
app::app() { |
|||
Serial.begin(115200); |
|||
DPRINTLN(DBG_VERBOSE, F("app::app")); |
|||
mEep = new eep(); |
|||
mWifi = new ahoywifi(this, &mSysConfig, &mConfig); |
|||
|
|||
resetSystem(); |
|||
loadDefaultConfig(); |
|||
|
|||
mSys = new HmSystemType(); |
|||
mSys->enableDebug(); |
|||
mShouldReboot = false; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::setup(uint32_t timeout) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::setup")); |
|||
|
|||
mWifiSettingsValid = checkEEpCrc(ADDR_START, ADDR_WIFI_CRC, ADDR_WIFI_CRC); |
|||
mSettingsValid = checkEEpCrc(ADDR_START_SETTINGS, ((ADDR_NEXT) - (ADDR_START_SETTINGS)), ADDR_SETTINGS_CRC); |
|||
loadEEpconfig(); |
|||
|
|||
mWifi->setup(timeout, mWifiSettingsValid); |
|||
|
|||
#ifndef AP_ONLY |
|||
setupMqtt(); |
|||
#endif |
|||
mSys->setup(mConfig.amplifierPower, mConfig.pinIrq, mConfig.pinCe, mConfig.pinCs); |
|||
|
|||
mWebInst = new web(this, &mSysConfig, &mConfig, &mStat, mVersion); |
|||
mWebInst->setup(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::loop(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::loop")); |
|||
|
|||
bool apActive = mWifi->loop(); |
|||
mWebInst->loop(); |
|||
|
|||
if (millis() - mPrevMillis >= 1000) { |
|||
mPrevMillis += 1000; |
|||
mUptimeSecs++; |
|||
if (0 != mUtcTimestamp) |
|||
mUtcTimestamp++; |
|||
} |
|||
|
|||
if (checkTicker(&mNtpRefreshTicker, mNtpRefreshInterval)) { |
|||
if (!apActive) |
|||
mUpdateNtp = true; |
|||
} |
|||
|
|||
if (mUpdateNtp) { |
|||
mUpdateNtp = false; |
|||
mUtcTimestamp = mWifi->getNtpTime(); |
|||
DPRINTLN(DBG_INFO, F("[NTP]: ") + getDateTimeStr(mUtcTimestamp) + F(" UTC")); |
|||
} |
|||
|
|||
if (mFlagSendDiscoveryConfig) { |
|||
mFlagSendDiscoveryConfig = false; |
|||
sendMqttDiscoveryConfig(); |
|||
} |
|||
|
|||
if (mShouldReboot) { |
|||
DPRINTLN(DBG_INFO, F("Rebooting...")); |
|||
ESP.restart(); |
|||
} |
|||
|
|||
mSys->Radio.loop(); |
|||
|
|||
yield(); |
|||
|
|||
if (checkTicker(&mRxTicker, 5)) { |
|||
bool rxRdy = mSys->Radio.switchRxCh(); |
|||
|
|||
if (!mSys->BufCtrl.empty()) { |
|||
uint8_t len; |
|||
packet_t *p = mSys->BufCtrl.getBack(); |
|||
|
|||
if (mSys->Radio.checkPaketCrc(p->packet, &len, p->rxCh)) { |
|||
// process buffer only on first occurrence
|
|||
if (mConfig.serialDebug) { |
|||
DPRINT(DBG_INFO, "RX " + String(len) + "B Ch" + String(p->rxCh) + " | "); |
|||
mSys->Radio.dumpBuf(NULL, p->packet, len); |
|||
} |
|||
|
|||
mStat.frmCnt++; |
|||
|
|||
if (0 != len) { |
|||
Inverter<> *iv = mSys->findInverter(&p->packet[1]); |
|||
if ((NULL != iv) && (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES))) { // response from get information command
|
|||
mPayload[iv->id].txId = p->packet[0]; |
|||
DPRINTLN(DBG_DEBUG, F("Response from info request received")); |
|||
uint8_t *pid = &p->packet[9]; |
|||
if (*pid == 0x00) { |
|||
DPRINT(DBG_DEBUG, F("fragment number zero received and ignored")); |
|||
} else { |
|||
DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); |
|||
if ((*pid & 0x7F) < 5) { |
|||
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len - 11); |
|||
mPayload[iv->id].len[(*pid & 0x7F) - 1] = len - 11; |
|||
} |
|||
|
|||
if ((*pid & ALL_FRAMES) == ALL_FRAMES) { |
|||
// Last packet
|
|||
if ((*pid & 0x7f) > mPayload[iv->id].maxPackId) { |
|||
mPayload[iv->id].maxPackId = (*pid & 0x7f); |
|||
if (*pid > 0x81) |
|||
mLastPacketId = *pid; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if ((NULL != iv) && (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES))) { // response from dev control command
|
|||
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); |
|||
|
|||
mPayload[iv->id].txId = p->packet[0]; |
|||
iv->devControlRequest = false; |
|||
|
|||
if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { |
|||
String msg = (p->packet[10] == 0x00 && p->packet[11] == 0x00) ? "" : "NOT "; |
|||
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has ") + msg + F("accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1])); |
|||
} |
|||
iv->devControlCmd = Init; |
|||
} |
|||
} |
|||
} |
|||
mSys->BufCtrl.popBack(); |
|||
} |
|||
yield(); |
|||
|
|||
if (rxRdy) { |
|||
processPayload(true); |
|||
} |
|||
} |
|||
|
|||
if (mMqttActive) |
|||
mMqtt.loop(); |
|||
|
|||
if (checkTicker(&mTicker, 1000)) { |
|||
if (mUtcTimestamp > 946684800 && mConfig.sunLat && mConfig.sunLon && (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 != (mLatestSunTimestamp + mCalculatedTimezoneOffset) / 86400) { // update on reboot or midnight
|
|||
if (!mLatestSunTimestamp) { // first call: calculate time zone from longitude to refresh at local midnight
|
|||
mCalculatedTimezoneOffset = (int8_t)((mConfig.sunLon >= 0 ? mConfig.sunLon + 7.5 : mConfig.sunLon - 7.5) / 15) * 3600; |
|||
} |
|||
calculateSunriseSunset(); |
|||
mLatestSunTimestamp = mUtcTimestamp; |
|||
} |
|||
|
|||
if ((++mMqttTicker >= mMqttInterval) && (mMqttInterval != 0xffff) && mMqttActive) { |
|||
mMqttTicker = 0; |
|||
sendMqtt(); |
|||
} |
|||
|
|||
if (mConfig.serialShowIv) { |
|||
if (++mSerialTicker >= mConfig.serialInterval) { |
|||
mSerialTicker = 0; |
|||
char topic[30], val[10]; |
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL != iv) { |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
if (iv->isAvailable(mUtcTimestamp, rec)) { |
|||
DPRINTLN(DBG_INFO, "Inverter: " + String(id)); |
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
if (0.0f != iv->getValue(i, rec)) { |
|||
snprintf(topic, 30, "%s/ch%d/%s", iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); |
|||
snprintf(val, 10, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec)); |
|||
DPRINTLN(DBG_INFO, String(topic) + ": " + String(val)); |
|||
} |
|||
yield(); |
|||
} |
|||
DPRINTLN(DBG_INFO, ""); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (++mSendTicker >= mConfig.sendInterval) { |
|||
mSendTicker = 0; |
|||
|
|||
if (mUtcTimestamp > 946684800 && (!mConfig.sunDisNightCom || !mLatestSunTimestamp || (mUtcTimestamp >= mSunrise && mUtcTimestamp <= mSunset))) { // Timestamp is set and (inverter communication only during the day if the option is activated and sunrise/sunset is set)
|
|||
if (mConfig.serialDebug) |
|||
DPRINTLN(DBG_DEBUG, F("Free heap: 0x") + String(ESP.getFreeHeap(), HEX)); |
|||
|
|||
if (!mSys->BufCtrl.empty()) { |
|||
if (mConfig.serialDebug) |
|||
DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->BufCtrl.getFill())); |
|||
} |
|||
|
|||
int8_t maxLoop = MAX_NUM_INVERTERS; |
|||
Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId); |
|||
do { |
|||
// if(NULL != iv)
|
|||
// mPayload[iv->id].requested = false;
|
|||
mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; |
|||
iv = mSys->getInverterByPos(mSendLastIvId); |
|||
} while ((NULL == iv) && ((maxLoop--) > 0)); |
|||
|
|||
if (NULL != iv) { |
|||
if (!mPayload[iv->id].complete) |
|||
processPayload(false); |
|||
|
|||
if (!mPayload[iv->id].complete) { |
|||
if (0 == mPayload[iv->id].maxPackId) |
|||
mStat.rxFailNoAnser++; |
|||
else |
|||
mStat.rxFail++; |
|||
|
|||
iv->setQueuedCmdFinished(); // command failed
|
|||
if (mConfig.serialDebug) |
|||
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); |
|||
if (mConfig.serialDebug) { |
|||
DPRINT(DBG_INFO, F("Inverter #") + String(iv->id) + " "); |
|||
DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload[iv->id].retransmits) + ")"); |
|||
} |
|||
} |
|||
|
|||
resetPayload(iv); |
|||
mPayload[iv->id].requested = true; |
|||
|
|||
yield(); |
|||
if (mConfig.serialDebug) { |
|||
DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status())); |
|||
DPRINTLN(DBG_INFO, F("Requesting Inverter SN ") + String(iv->serial.u64, HEX)); |
|||
} |
|||
|
|||
if (iv->devControlRequest) { |
|||
if (mConfig.serialDebug) |
|||
DPRINTLN(DBG_INFO, F("Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0])); |
|||
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit); |
|||
mPayload[iv->id].txCmd = iv->devControlCmd; |
|||
iv->clearCmdQueue(); |
|||
iv->enqueCommand<InfoCommand>(SystemConfigPara); |
|||
} else { |
|||
uint8_t cmd = iv->getQueuedCmd(); |
|||
mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex); |
|||
mPayload[iv->id].txCmd = cmd; |
|||
mRxTicker = 0; |
|||
} |
|||
} |
|||
} else if (mConfig.serialDebug) |
|||
DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!")); |
|||
yield(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::handleIntr(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::handleIntr")); |
|||
mSys->Radio.handleIntr(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
bool app::buildPayload(uint8_t id) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::buildPayload")); |
|||
uint16_t crc = 0xffff, crcRcv = 0x0000; |
|||
if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) |
|||
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; |
|||
|
|||
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { |
|||
if (mPayload[id].len[i] > 0) { |
|||
if (i == (mPayload[id].maxPackId - 1)) { |
|||
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); |
|||
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); |
|||
} else |
|||
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); |
|||
} |
|||
yield(); |
|||
} |
|||
|
|||
return (crc == crcRcv) ? true : false; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::processPayload(bool retransmit) { |
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
|
|||
if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { |
|||
// no processing needed if txId is not 0x95
|
|||
// DPRINTLN(DBG_INFO, F("processPayload - set complete, txId: ") + String(mPayload[iv->id].txId, HEX));
|
|||
mPayload[iv->id].complete = true; |
|||
} |
|||
|
|||
if (!mPayload[iv->id].complete) { |
|||
if (!buildPayload(iv->id)) { // payload not complete
|
|||
if ((mPayload[iv->id].requested) && (retransmit)) { |
|||
if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { |
|||
// This is required to prevent retransmissions without answer.
|
|||
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); |
|||
mPayload[iv->id].retransmits = mConfig.maxRetransPerPyld; |
|||
} else { |
|||
if (mPayload[iv->id].retransmits < mConfig.maxRetransPerPyld) { |
|||
mPayload[iv->id].retransmits++; |
|||
if (mPayload[iv->id].maxPackId != 0) { |
|||
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { |
|||
if (mPayload[iv->id].len[i] == 0) { |
|||
if (mConfig.serialDebug) |
|||
DPRINTLN(DBG_WARN, F("while retrieving data: Frame ") + String(i + 1) + F(" missing: Request Retransmit")); |
|||
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); |
|||
break; // only retransmit one frame per loop
|
|||
} |
|||
yield(); |
|||
} |
|||
} else { |
|||
if (mConfig.serialDebug) |
|||
DPRINTLN(DBG_WARN, F("while retrieving data: last frame missing: Request Retransmit")); |
|||
if (0x00 != mLastPacketId) |
|||
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, mLastPacketId, true); |
|||
else { |
|||
mPayload[iv->id].txCmd = iv->getQueuedCmd(); |
|||
mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex); |
|||
} |
|||
} |
|||
mSys->Radio.switchRxCh(100); |
|||
} |
|||
} |
|||
} |
|||
} else { // payload complete
|
|||
DPRINTLN(DBG_INFO, F("procPyld: cmd: ") + String(mPayload[iv->id].txCmd)); |
|||
DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); |
|||
DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); |
|||
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
|
|||
mPayload[iv->id].complete = true; |
|||
|
|||
uint8_t payload[128]; |
|||
uint8_t payloadLen = 0; |
|||
|
|||
memset(payload, 0, 128); |
|||
|
|||
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { |
|||
memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); |
|||
payloadLen += (mPayload[iv->id].len[i]); |
|||
yield(); |
|||
} |
|||
payloadLen -= 2; |
|||
|
|||
if (mConfig.serialDebug) { |
|||
DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); |
|||
mSys->Radio.dumpBuf(NULL, payload, payloadLen); |
|||
} |
|||
|
|||
if (NULL == rec) { |
|||
DPRINTLN(DBG_ERROR, F("record is NULL!")); |
|||
} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { |
|||
if (mPayload[iv->id].txId == (TX_REQ_INFO + 0x80)) |
|||
mStat.rxSuccess++; |
|||
|
|||
rec->ts = mPayload[iv->id].ts; |
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
iv->addValue(i, payload, rec); |
|||
yield(); |
|||
} |
|||
iv->doCalculations(); |
|||
|
|||
mMqttSendList.push(mPayload[iv->id].txCmd); |
|||
} else { |
|||
DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); |
|||
mStat.rxFail++; |
|||
} |
|||
|
|||
iv->setQueuedCmdFinished(); |
|||
} |
|||
} |
|||
|
|||
yield(); |
|||
|
|||
} |
|||
|
|||
// ist MQTT aktiviert und es wurden Daten vom einem oder mehreren WR aufbereitet
|
|||
// dann die den mMqttTicker auf mMqttIntervall -2 setzen, also
|
|||
// MQTT aussenden in 2 sek aktivieren
|
|||
if ((mMqttInterval != 0xffff) && (!mMqttSendList.empty())) { |
|||
mMqttTicker = mMqttInterval - 2; |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::cbMqtt(char *topic, byte *payload, unsigned int length) { |
|||
// callback handling on subscribed devcontrol topic
|
|||
DPRINTLN(DBG_INFO, F("app::cbMqtt")); |
|||
// subcribed topics are mTopic + "/devcontrol/#" where # is <inverter_id>/<subcmd in dec>
|
|||
// eg. mypvsolar/devcontrol/1/11 with payload "400" --> inverter 1 active power limit 400 Watt
|
|||
const char *token = strtok(topic, "/"); |
|||
while (token != NULL) { |
|||
if (strcmp(token, "devcontrol") == 0) { |
|||
token = strtok(NULL, "/"); |
|||
uint8_t iv_id = std::stoi(token); |
|||
|
|||
if (iv_id >= 0 && iv_id <= MAX_NUM_INVERTERS) { |
|||
Inverter<> *iv = this->mSys->getInverterByPos(iv_id); |
|||
if (NULL != iv) { |
|||
if (!iv->devControlRequest) { // still pending
|
|||
token = strtok(NULL, "/"); |
|||
|
|||
switch (std::stoi(token)) { |
|||
// Active Power Control
|
|||
case ActivePowerContr: |
|||
token = strtok(NULL, "/"); // get ControlMode aka "PowerPF.Desc" in DTU-Pro Code from topic string
|
|||
if (token == NULL) // default via mqtt ommit the LimitControlMode
|
|||
iv->powerLimit[1] = AbsolutNonPersistent; |
|||
else |
|||
iv->powerLimit[1] = std::stoi(token); |
|||
if (length <= 5) { // if (std::stoi((char*)payload) > 0) more error handling powerlimit needed?
|
|||
if (iv->powerLimit[1] >= AbsolutNonPersistent && iv->powerLimit[1] <= RelativPersistent) { |
|||
iv->devControlCmd = ActivePowerContr; |
|||
iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length)); // THX to @silversurfer
|
|||
/*if (iv->powerLimit[1] & 0x0001)
|
|||
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("%")); |
|||
else |
|||
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W"));*/ |
|||
|
|||
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + String(iv->powerLimit[1] & 0x0001) ? F("%") : F("W")); |
|||
} |
|||
iv->devControlRequest = true; |
|||
} else { |
|||
DPRINTLN(DBG_INFO, F("Invalid mqtt payload recevied: ") + String((char *)payload)); |
|||
} |
|||
break; |
|||
|
|||
// Turn On
|
|||
case TurnOn: |
|||
iv->devControlCmd = TurnOn; |
|||
DPRINTLN(DBG_INFO, F("Turn on inverter ") + String(iv->id)); |
|||
iv->devControlRequest = true; |
|||
break; |
|||
|
|||
// Turn Off
|
|||
case TurnOff: |
|||
iv->devControlCmd = TurnOff; |
|||
DPRINTLN(DBG_INFO, F("Turn off inverter ") + String(iv->id)); |
|||
iv->devControlRequest = true; |
|||
break; |
|||
|
|||
// Restart
|
|||
case Restart: |
|||
iv->devControlCmd = Restart; |
|||
DPRINTLN(DBG_INFO, F("Restart inverter ") + String(iv->id)); |
|||
iv->devControlRequest = true; |
|||
break; |
|||
|
|||
// Reactive Power Control
|
|||
case ReactivePowerContr: |
|||
iv->devControlCmd = ReactivePowerContr; |
|||
if (true) { // if (std::stoi((char*)payload) > 0) error handling powerlimit needed?
|
|||
iv->devControlCmd = ReactivePowerContr; |
|||
iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length)); |
|||
iv->powerLimit[1] = 0x0000; // if reactivepower limit is set via external interface --> set it temporay
|
|||
DPRINTLN(DBG_DEBUG, F("Reactivepower limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W")); |
|||
iv->devControlRequest = true; |
|||
} |
|||
break; |
|||
|
|||
// Set Power Factor
|
|||
case PFSet: |
|||
// iv->devControlCmd = PFSet;
|
|||
// uint16_t power_factor = std::stoi(strtok(NULL, "/"));
|
|||
DPRINTLN(DBG_INFO, F("Set Power Factor not implemented for inverter ") + String(iv->id)); |
|||
break; |
|||
|
|||
// CleanState lock & alarm
|
|||
case CleanState_LockAndAlarm: |
|||
iv->devControlCmd = CleanState_LockAndAlarm; |
|||
DPRINTLN(DBG_INFO, F("CleanState lock & alarm for inverter ") + String(iv->id)); |
|||
iv->devControlRequest = true; |
|||
break; |
|||
|
|||
default: |
|||
DPRINTLN(DBG_INFO, "Not implemented"); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
token = strtok(NULL, "/"); |
|||
} |
|||
DPRINTLN(DBG_INFO, F("app::cbMqtt finished")); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
bool app::getWifiApActive(void) { |
|||
return mWifi->getApActive(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::scanAvailNetworks(void) { |
|||
mWifi->scanAvailNetworks(); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::getAvailNetworks(JsonObject obj) { |
|||
mWifi->getAvailNetworks(obj); |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::sendMqttDiscoveryConfig(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::sendMqttDiscoveryConfig")); |
|||
|
|||
char stateTopic[64], discoveryTopic[64], buffer[512], name[32], uniq_id[32]; |
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL != iv) { |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
DynamicJsonDocument deviceDoc(128); |
|||
deviceDoc["name"] = iv->name; |
|||
deviceDoc["ids"] = String(iv->serial.u64, HEX); |
|||
deviceDoc["cu"] = F("http://") + String(WiFi.localIP().toString()); |
|||
deviceDoc["mf"] = "Hoymiles"; |
|||
deviceDoc["mdl"] = iv->name; |
|||
JsonObject deviceObj = deviceDoc.as<JsonObject>(); |
|||
DynamicJsonDocument doc(384); |
|||
|
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
if (rec->assign[i].ch == CH0) { |
|||
snprintf(name, 32, "%s %s", iv->name, iv->getFieldName(i, rec)); |
|||
} else { |
|||
snprintf(name, 32, "%s CH%d %s", iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); |
|||
} |
|||
snprintf(stateTopic, 64, "%s/%s/ch%d/%s", mConfig.mqtt.topic, iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); |
|||
snprintf(discoveryTopic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); |
|||
snprintf(uniq_id, 32, "ch%d_%s", rec->assign[i].ch, iv->getFieldName(i, rec)); |
|||
const char *devCls = getFieldDeviceClass(rec->assign[i].fieldId); |
|||
const char *stateCls = getFieldStateClass(rec->assign[i].fieldId); |
|||
|
|||
doc["name"] = name; |
|||
doc["stat_t"] = stateTopic; |
|||
doc["unit_of_meas"] = iv->getUnit(i, rec); |
|||
doc["uniq_id"] = String(iv->serial.u64, HEX) + "_" + uniq_id; |
|||
doc["dev"] = deviceObj; |
|||
doc["exp_aft"] = mMqttInterval + 5; // add 5 sec if connection is bad or ESP too slow
|
|||
if (devCls != NULL) |
|||
doc["dev_cla"] = devCls; |
|||
if (stateCls != NULL) |
|||
doc["stat_cla"] = stateCls; |
|||
|
|||
serializeJson(doc, buffer); |
|||
mMqtt.sendMsg2(discoveryTopic, buffer, true); |
|||
// DPRINTLN(DBG_INFO, F("mqtt sent"));
|
|||
doc.clear(); |
|||
} |
|||
|
|||
// TODO: remove this field, obsolete?
|
|||
mMqttConfigSendState[id] = true; |
|||
|
|||
yield(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::sendMqtt(void) { |
|||
mMqtt.isConnected(true); // really needed? See comment from HorstG-57 #176
|
|||
char topic[32 + MAX_NAME_LENGTH], val[32]; |
|||
float total[4]; |
|||
bool sendTotal = false; |
|||
memset(total, 0, sizeof(float) * 4); |
|||
snprintf(val, 32, "%ld", millis() / 1000); |
|||
|
|||
mMqtt.sendMsg("uptime", val); |
|||
|
|||
if(mMqttSendList.empty()) |
|||
return; |
|||
|
|||
while(!mMqttSendList.empty()) { |
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL == iv) |
|||
continue; // skip to next inverter
|
|||
|
|||
record_t<> *rec = iv->getRecordStruct(mMqttSendList.front()); |
|||
|
|||
if(mMqttSendList.front() == RealTimeRunData_Debug) { |
|||
// inverter status
|
|||
uint8_t status = MQTT_STATUS_AVAIL_PROD; |
|||
if (!iv->isAvailable(mUtcTimestamp, rec)) |
|||
status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; |
|||
if (!iv->isProducing(mUtcTimestamp, rec)) { |
|||
if (MQTT_STATUS_AVAIL_PROD == status) |
|||
status = MQTT_STATUS_AVAIL_NOT_PROD; |
|||
} |
|||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->name); |
|||
snprintf(val, 32, "%s%s%s%s", |
|||
(MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not " : "", |
|||
"available and ", |
|||
(MQTT_STATUS_NOT_AVAIL_NOT_PROD || MQTT_STATUS_AVAIL_NOT_PROD) ? "not " : "", |
|||
"producing" |
|||
); |
|||
mMqtt.sendMsg(topic, val); |
|||
|
|||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->name); |
|||
snprintf(val, 32, "%d", status); |
|||
mMqtt.sendMsg(topic, val); |
|||
|
|||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->name); |
|||
snprintf(val, 48, "%i", iv->getLastTs(rec) * 1000); |
|||
mMqtt.sendMsg(topic, val); |
|||
} |
|||
|
|||
// data
|
|||
for (uint8_t i = 0; i < rec->length; i++) { |
|||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); |
|||
snprintf(val, 10, "%.3f", iv->getValue(i, rec)); |
|||
mMqtt.sendMsg(topic, val); |
|||
|
|||
// calculate total values for RealTimeRunData_Debug
|
|||
if (mMqttSendList.front() == RealTimeRunData_Debug) { |
|||
if (CH0 == rec->assign[i].ch) { |
|||
switch (rec->assign[i].fieldId) { |
|||
case FLD_PAC: |
|||
total[0] += iv->getValue(i, rec); |
|||
break; |
|||
case FLD_YT: |
|||
total[1] += iv->getValue(i, rec); |
|||
break; |
|||
case FLD_YD: |
|||
total[2] += iv->getValue(i, rec); |
|||
break; |
|||
case FLD_PDC: |
|||
total[3] += iv->getValue(i, rec); |
|||
break; |
|||
} |
|||
} |
|||
sendTotal = true; |
|||
} |
|||
yield(); |
|||
} |
|||
} |
|||
|
|||
mMqttSendList.pop(); // remove from list once all inverters were processed
|
|||
} |
|||
|
|||
if (true == sendTotal) { |
|||
uint8_t fieldId; |
|||
for (uint8_t i = 0; i < 4; i++) { |
|||
switch (i) { |
|||
default: |
|||
case 0: |
|||
fieldId = FLD_PAC; |
|||
break; |
|||
case 1: |
|||
fieldId = FLD_YT; |
|||
break; |
|||
case 2: |
|||
fieldId = FLD_YD; |
|||
break; |
|||
case 3: |
|||
fieldId = FLD_PDC; |
|||
break; |
|||
} |
|||
snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); |
|||
snprintf(val, 10, "%.3f", total[i]); |
|||
mMqtt.sendMsg(topic, val); |
|||
} |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
const char *app::getFieldDeviceClass(uint8_t fieldId) { |
|||
uint8_t pos = 0; |
|||
for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { |
|||
if (deviceFieldAssignment[pos].fieldId == fieldId) |
|||
break; |
|||
} |
|||
return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : deviceClasses[deviceFieldAssignment[pos].deviceClsId]; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
const char *app::getFieldStateClass(uint8_t fieldId) { |
|||
uint8_t pos = 0; |
|||
for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { |
|||
if (deviceFieldAssignment[pos].fieldId == fieldId) |
|||
break; |
|||
} |
|||
return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId]; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::resetSystem(void) { |
|||
mUptimeSecs = 0; |
|||
mPrevMillis = 0; |
|||
mUpdateNtp = false; |
|||
mFlagSendDiscoveryConfig = false; |
|||
|
|||
mNtpRefreshTicker = 0; |
|||
mNtpRefreshInterval = NTP_REFRESH_INTERVAL; // [ms]
|
|||
|
|||
#ifdef AP_ONLY |
|||
mUtcTimestamp = 1; |
|||
#else |
|||
mUtcTimestamp = 0; |
|||
#endif |
|||
|
|||
mHeapStatCnt = 0; |
|||
|
|||
mSendTicker = 0xffff; |
|||
mMqttTicker = 0xffff; |
|||
mMqttInterval = MQTT_INTERVAL; |
|||
mSerialTicker = 0xffff; |
|||
mMqttActive = false; |
|||
|
|||
mTicker = 0; |
|||
mRxTicker = 0; |
|||
|
|||
mSendLastIvId = 0; |
|||
|
|||
mShowRebootRequest = false; |
|||
|
|||
memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t))); |
|||
memset(&mStat, 0, sizeof(statistics_t)); |
|||
mLastPacketId = 0x00; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::loadDefaultConfig(void) { |
|||
memset(&mSysConfig, 0, sizeof(sysConfig_t)); |
|||
memset(&mConfig, 0, sizeof(config_t)); |
|||
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); |
|||
|
|||
snprintf(mSysConfig.deviceName, DEVNAME_LEN, "%s", DEF_DEVICE_NAME); |
|||
|
|||
// wifi
|
|||
snprintf(mSysConfig.stationSsid, SSID_LEN, "%s", FB_WIFI_SSID); |
|||
snprintf(mSysConfig.stationPwd, PWD_LEN, "%s", FB_WIFI_PWD); |
|||
|
|||
// nrf24
|
|||
mConfig.sendInterval = SEND_INTERVAL; |
|||
mConfig.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; |
|||
mConfig.pinCs = DEF_CS_PIN; |
|||
mConfig.pinCe = DEF_CE_PIN; |
|||
mConfig.pinIrq = DEF_IRQ_PIN; |
|||
mConfig.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; |
|||
|
|||
// ntp
|
|||
snprintf(mConfig.ntpAddr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); |
|||
mConfig.ntpPort = DEF_NTP_PORT; |
|||
|
|||
// Latitude + Longitude
|
|||
mConfig.sunLat = 0.0; |
|||
mConfig.sunLon = 0.0; |
|||
mConfig.sunDisNightCom = false; |
|||
|
|||
// mqtt
|
|||
snprintf(mConfig.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); |
|||
mConfig.mqtt.port = DEF_MQTT_PORT; |
|||
snprintf(mConfig.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER); |
|||
snprintf(mConfig.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); |
|||
snprintf(mConfig.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); |
|||
|
|||
// serial
|
|||
mConfig.serialInterval = SERIAL_INTERVAL; |
|||
mConfig.serialShowIv = false; |
|||
mConfig.serialDebug = false; |
|||
|
|||
// Disclaimer
|
|||
mConfig.disclaimer = false; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::loadEEpconfig(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::loadEEpconfig")); |
|||
|
|||
if (mWifiSettingsValid) |
|||
mEep->read(ADDR_CFG_SYS, (uint8_t *)&mSysConfig, CFG_SYS_LEN); |
|||
if (mSettingsValid) { |
|||
mEep->read(ADDR_CFG, (uint8_t *)&mConfig, CFG_LEN); |
|||
|
|||
mSendTicker = mConfig.sendInterval; |
|||
mSerialTicker = 0; |
|||
|
|||
// inverter
|
|||
uint64_t invSerial; |
|||
char name[MAX_NAME_LENGTH + 1] = {0}; |
|||
uint16_t modPwr[4]; |
|||
Inverter<> *iv; |
|||
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial); |
|||
mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), name, MAX_NAME_LENGTH); |
|||
mEep->read(ADDR_INV_CH_PWR + (i * 2 * 4), modPwr, 4); |
|||
if (0ULL != invSerial) { |
|||
iv = mSys->addInverter(name, invSerial, modPwr); |
|||
if (NULL != iv) { // will run once on every dtu boot
|
|||
for (uint8_t j = 0; j < 4; j++) { |
|||
mEep->read(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); |
|||
} |
|||
} |
|||
|
|||
// TODO: the original mqttinterval value is not needed any more
|
|||
mMqttInterval += mConfig.sendInterval; |
|||
} |
|||
} |
|||
|
|||
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
iv = mSys->getInverterByPos(i, false); |
|||
if (NULL != iv) |
|||
resetPayload(iv); |
|||
} |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::saveValues(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::saveValues")); |
|||
|
|||
mEep->write(ADDR_CFG_SYS, (uint8_t *)&mSysConfig, CFG_SYS_LEN); |
|||
mEep->write(ADDR_CFG, (uint8_t *)&mConfig, CFG_LEN); |
|||
Inverter<> *iv; |
|||
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { |
|||
iv = mSys->getInverterByPos(i, false); |
|||
mEep->write(ADDR_INV_ADDR + (i * 8), iv->serial.u64); |
|||
mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), iv->name, MAX_NAME_LENGTH); |
|||
// max channel power / name
|
|||
for (uint8_t j = 0; j < 4; j++) { |
|||
mEep->write(ADDR_INV_CH_PWR + (i * 2 * 4) + (j * 2), iv->chMaxPwr[j]); |
|||
mEep->write(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); |
|||
} |
|||
} |
|||
|
|||
updateCrc(); |
|||
|
|||
// update sun
|
|||
mLatestSunTimestamp = 0; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::setupMqtt(void) { |
|||
if (mSettingsValid) { |
|||
if (mConfig.mqtt.broker[0] > 0) { |
|||
mMqttActive = true; |
|||
if (mMqttInterval < MIN_MQTT_INTERVAL) mMqttInterval = MIN_MQTT_INTERVAL; |
|||
} else { |
|||
mMqttInterval = 0xffff; |
|||
} |
|||
|
|||
mMqttTicker = 0; |
|||
mMqtt.setup(&mConfig.mqtt, mSysConfig.deviceName); |
|||
mMqtt.setCallback(std::bind(&app::cbMqtt, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); |
|||
|
|||
if (mMqttActive) { |
|||
mMqtt.sendMsg("version", mVersion); |
|||
if (mMqtt.isConnected()) { |
|||
mMqtt.sendMsg("device", mSysConfig.deviceName); |
|||
mMqtt.sendMsg("uptime", "0"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::resetPayload(Inverter<> *iv) { |
|||
DPRINTLN(DBG_INFO, "resetPayload: id: " + String(iv->id)); |
|||
memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES); |
|||
mPayload[iv->id].txCmd = 0; |
|||
mPayload[iv->id].retransmits = 0; |
|||
mPayload[iv->id].maxPackId = 0; |
|||
mPayload[iv->id].complete = false; |
|||
mPayload[iv->id].requested = false; |
|||
mPayload[iv->id].ts = mUtcTimestamp; |
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
void app::calculateSunriseSunset() { |
|||
// Source: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
|
|||
|
|||
// Julian day since 1.1.2000 12:00 + correction 69.12s
|
|||
double n_JulianDay = (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 - 10957.0 + 0.0008; |
|||
// Mean solar time
|
|||
double J = n_JulianDay - mConfig.sunLon / 360; |
|||
// Solar mean anomaly
|
|||
double M = fmod((357.5291 + 0.98560028 * J), 360); |
|||
// Equation of the center
|
|||
double C = 1.9148 * SIN(M) + 0.02 * SIN(2 * M) + 0.0003 * SIN(3 * M); |
|||
// Ecliptic longitude
|
|||
double lambda = fmod((M + C + 180 + 102.9372), 360); |
|||
// Solar transit
|
|||
double Jtransit = 2451545.0 + J + 0.0053 * SIN(M) - 0.0069 * SIN(2 * lambda); |
|||
// Declination of the sun
|
|||
double delta = ASIN(SIN(lambda) * SIN(23.44)); |
|||
// Hour angle
|
|||
double omega = ACOS(SIN(-0.83) - SIN(mConfig.sunLat) * SIN(delta) / COS(mConfig.sunLat) * COS(delta)); |
|||
// Calculate sunrise and sunset
|
|||
double Jrise = Jtransit - omega / 360; |
|||
double Jset = Jtransit + omega / 360; |
|||
// Julian sunrise/sunset to UTC unix timestamp (days incl. fraction to seconds + unix offset 1.1.2000 12:00)
|
|||
mSunrise = (Jrise - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line
|
|||
mSunset = (Jset - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line
|
|||
} |
@ -1,303 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __APP_H__ |
|||
#define __APP_H__ |
|||
|
|||
#include "dbg.h" |
|||
#include "Arduino.h" |
|||
|
|||
|
|||
#include <queue> |
|||
#include <RF24.h> |
|||
#include <RF24_config.h> |
|||
#include <ArduinoJson.h> |
|||
|
|||
#include "eep.h" |
|||
#include "defines.h" |
|||
#include "crc.h" |
|||
|
|||
#include "CircularBuffer.h" |
|||
#include "hmSystem.h" |
|||
#include "mqtt.h" |
|||
#include "ahoywifi.h" |
|||
#include "web.h" |
|||
|
|||
// convert degrees and radians for sun calculation
|
|||
#define SIN(x) (sin(radians(x))) |
|||
#define COS(x) (cos(radians(x))) |
|||
#define ASIN(x) (degrees(asin(x))) |
|||
#define ACOS(x) (degrees(acos(x))) |
|||
|
|||
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType; |
|||
|
|||
typedef struct { |
|||
uint8_t txCmd; |
|||
uint8_t txId; |
|||
uint8_t invId; |
|||
uint32_t ts; |
|||
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; |
|||
uint8_t len[MAX_PAYLOAD_ENTRIES]; |
|||
bool complete; |
|||
uint8_t maxPackId; |
|||
uint8_t retransmits; |
|||
bool requested; |
|||
} invPayload_t; |
|||
|
|||
class ahoywifi; |
|||
class web; |
|||
|
|||
class app { |
|||
public: |
|||
app(); |
|||
~app() {} |
|||
|
|||
void setup(uint32_t timeout); |
|||
void loop(void); |
|||
void handleIntr(void); |
|||
void cbMqtt(char* topic, byte* payload, unsigned int length); |
|||
void saveValues(void); |
|||
void resetPayload(Inverter<>* iv); |
|||
bool getWifiApActive(void); |
|||
void scanAvailNetworks(void); |
|||
void getAvailNetworks(JsonObject obj); |
|||
|
|||
uint8_t getIrqPin(void) { |
|||
return mConfig.pinIrq; |
|||
} |
|||
|
|||
uint64_t Serial2u64(const char *val) { |
|||
char tmp[3]; |
|||
uint64_t ret = 0ULL; |
|||
uint64_t u64; |
|||
memset(tmp, 0, 3); |
|||
for(uint8_t i = 0; i < 6; i++) { |
|||
tmp[0] = val[i*2]; |
|||
tmp[1] = val[i*2 + 1]; |
|||
if((tmp[0] == '\0') || (tmp[1] == '\0')) |
|||
break; |
|||
u64 = strtol(tmp, NULL, 16); |
|||
ret |= (u64 << ((5-i) << 3)); |
|||
} |
|||
return ret; |
|||
} |
|||
|
|||
String getDateTimeStr(time_t t) { |
|||
char str[20]; |
|||
if(0 == t) |
|||
sprintf(str, "n/a"); |
|||
else |
|||
sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t)); |
|||
return String(str); |
|||
} |
|||
|
|||
String getTimeStr(uint32_t offset = 0) { |
|||
char str[10]; |
|||
if(0 == mUtcTimestamp) |
|||
sprintf(str, "n/a"); |
|||
else |
|||
sprintf(str, "%02d:%02d:%02d ", hour(mUtcTimestamp + offset), minute(mUtcTimestamp + offset), second(mUtcTimestamp + offset)); |
|||
return String(str); |
|||
} |
|||
|
|||
inline uint32_t getUptime(void) { |
|||
return mUptimeSecs; |
|||
} |
|||
|
|||
inline uint32_t getTimestamp(void) { |
|||
return mUtcTimestamp; |
|||
} |
|||
|
|||
void setTimestamp(uint32_t newTime) { |
|||
DPRINTLN(DBG_DEBUG, F("setTimestamp: ") + String(newTime)); |
|||
if(0 == newTime) |
|||
mUpdateNtp = true; |
|||
else |
|||
{ |
|||
mUtcTimestamp = newTime; |
|||
} |
|||
} |
|||
|
|||
inline uint32_t getSunrise(void) { |
|||
return mSunrise; |
|||
} |
|||
inline uint32_t getSunset(void) { |
|||
return mSunset; |
|||
} |
|||
inline uint32_t getLatestSunTimestamp(void) { |
|||
return mLatestSunTimestamp; |
|||
} |
|||
|
|||
void eraseSettings(bool all = false) { |
|||
//DPRINTLN(DBG_VERBOSE, F("main.h:eraseSettings"));
|
|||
uint8_t buf[64]; |
|||
uint16_t addr = (all) ? ADDR_START : ADDR_START_SETTINGS; |
|||
uint16_t end; |
|||
|
|||
memset(buf, 0xff, 64); |
|||
do { |
|||
end = addr + 64; |
|||
if(end > (ADDR_SETTINGS_CRC + 2)) |
|||
end = (ADDR_SETTINGS_CRC + 2); |
|||
DPRINTLN(DBG_DEBUG, F("erase: 0x") + String(addr, HEX) + " - 0x" + String(end, HEX)); |
|||
mEep->write(addr, buf, (end-addr)); |
|||
addr = end; |
|||
} while(addr < (ADDR_SETTINGS_CRC + 2)); |
|||
mEep->commit(); |
|||
} |
|||
|
|||
inline bool checkTicker(uint32_t *ticker, uint32_t interval) { |
|||
uint32_t mil = millis(); |
|||
if(mil >= *ticker) { |
|||
*ticker = mil + interval; |
|||
return true; |
|||
} |
|||
else if(mil < (*ticker - interval)) { |
|||
*ticker = mil + interval; |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
inline bool mqttIsConnected(void) { return mMqtt.isConnected(); } |
|||
inline bool getSettingsValid(void) { return mSettingsValid; } |
|||
inline bool getRebootRequestState(void) { return mShowRebootRequest; } |
|||
inline uint32_t getMqttTxCnt(void) { return mMqtt.getTxCnt(); } |
|||
|
|||
HmSystemType *mSys; |
|||
bool mShouldReboot; |
|||
bool mFlagSendDiscoveryConfig; |
|||
|
|||
private: |
|||
void resetSystem(void); |
|||
void loadDefaultConfig(void); |
|||
void loadEEpconfig(void); |
|||
void setupMqtt(void); |
|||
|
|||
void sendMqttDiscoveryConfig(void); |
|||
void sendMqtt(void); |
|||
|
|||
bool buildPayload(uint8_t id); |
|||
void processPayload(bool retransmit); |
|||
|
|||
const char* getFieldDeviceClass(uint8_t fieldId); |
|||
const char* getFieldStateClass(uint8_t fieldId); |
|||
|
|||
inline uint16_t buildEEpCrc(uint32_t start, uint32_t length) { |
|||
DPRINTLN(DBG_VERBOSE, F("main.h:buildEEpCrc")); |
|||
uint8_t buf[32]; |
|||
uint16_t crc = 0xffff; |
|||
uint8_t len; |
|||
|
|||
while(length > 0) { |
|||
len = (length < 32) ? length : 32; |
|||
mEep->read(start, buf, len); |
|||
crc = ah::crc16(buf, len, crc); |
|||
start += len; |
|||
length -= len; |
|||
} |
|||
return crc; |
|||
} |
|||
|
|||
void updateCrc(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("app::updateCrc")); |
|||
uint16_t crc; |
|||
|
|||
crc = buildEEpCrc(ADDR_START, ADDR_WIFI_CRC); |
|||
DPRINTLN(DBG_DEBUG, F("new Wifi CRC: ") + String(crc, HEX)); |
|||
mEep->write(ADDR_WIFI_CRC, crc); |
|||
|
|||
crc = buildEEpCrc(ADDR_START_SETTINGS, ((ADDR_NEXT) - (ADDR_START_SETTINGS))); |
|||
DPRINTLN(DBG_DEBUG, F("new Settings CRC: ") + String(crc, HEX)); |
|||
mEep->write(ADDR_SETTINGS_CRC, crc); |
|||
|
|||
mEep->commit(); |
|||
} |
|||
|
|||
bool checkEEpCrc(uint32_t start, uint32_t length, uint32_t crcPos) { |
|||
DPRINTLN(DBG_VERBOSE, F("main.h:checkEEpCrc")); |
|||
DPRINTLN(DBG_DEBUG, F("start: ") + String(start) + F(", length: ") + String(length)); |
|||
uint16_t crcRd, crcCheck; |
|||
crcCheck = buildEEpCrc(start, length); |
|||
mEep->read(crcPos, &crcRd); |
|||
DPRINTLN(DBG_DEBUG, "CRC RD: " + String(crcRd, HEX) + " CRC CALC: " + String(crcCheck, HEX)); |
|||
return (crcCheck == crcRd); |
|||
} |
|||
|
|||
void stats(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("main.h:stats")); |
|||
#ifdef ESP8266 |
|||
uint32_t free; |
|||
uint16_t max; |
|||
uint8_t frag; |
|||
ESP.getHeapStats(&free, &max, &frag); |
|||
#elif defined(ESP32) |
|||
uint32_t free; |
|||
uint32_t max; |
|||
uint8_t frag; |
|||
free = ESP.getFreeHeap(); |
|||
max = ESP.getMaxAllocHeap(); |
|||
frag = 0; |
|||
#endif |
|||
DPRINT(DBG_VERBOSE, F("free: ") + String(free)); |
|||
DPRINT(DBG_VERBOSE, F(" - max: ") + String(max) + "%"); |
|||
DPRINTLN(DBG_VERBOSE, F(" - frag: ") + String(frag)); |
|||
} |
|||
|
|||
void calculateSunriseSunset(void); |
|||
|
|||
uint32_t mUptimeSecs; |
|||
uint32_t mPrevMillis; |
|||
uint8_t mHeapStatCnt; |
|||
uint32_t mNtpRefreshTicker; |
|||
uint32_t mNtpRefreshInterval; |
|||
|
|||
|
|||
bool mWifiSettingsValid; |
|||
bool mSettingsValid; |
|||
|
|||
eep *mEep; |
|||
uint32_t mUtcTimestamp; |
|||
bool mUpdateNtp; |
|||
|
|||
bool mShowRebootRequest; |
|||
|
|||
ahoywifi *mWifi; |
|||
web *mWebInst; |
|||
sysConfig_t mSysConfig; |
|||
config_t mConfig; |
|||
char mVersion[12]; |
|||
|
|||
uint16_t mSendTicker; |
|||
uint8_t mSendLastIvId; |
|||
|
|||
invPayload_t mPayload[MAX_NUM_INVERTERS]; |
|||
statistics_t mStat; |
|||
uint8_t mLastPacketId; |
|||
|
|||
// timer
|
|||
uint32_t mTicker; |
|||
uint32_t mRxTicker; |
|||
|
|||
// mqtt
|
|||
mqtt mMqtt; |
|||
uint16_t mMqttTicker; |
|||
uint16_t mMqttInterval; |
|||
bool mMqttActive; |
|||
bool mMqttConfigSendState[MAX_NUM_INVERTERS]; |
|||
std::queue<uint8_t> mMqttSendList; |
|||
|
|||
// serial
|
|||
uint16_t mSerialTicker; |
|||
|
|||
// sun
|
|||
int32_t mCalculatedTimezoneOffset; |
|||
uint32_t mSunrise; |
|||
uint32_t mSunset; |
|||
uint32_t mLatestSunTimestamp; |
|||
}; |
|||
|
|||
#endif /*__APP_H__*/ |
@ -1,199 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __DEFINES_H__ |
|||
#define __DEFINES_H__ |
|||
|
|||
#include "config.h" |
|||
|
|||
//-------------------------------------
|
|||
// VERSION
|
|||
//-------------------------------------
|
|||
#define VERSION_MAJOR 0 |
|||
#define VERSION_MINOR 5 |
|||
#define VERSION_PATCH 28 |
|||
|
|||
//-------------------------------------
|
|||
typedef struct { |
|||
uint8_t rxCh; |
|||
uint8_t packet[MAX_RF_PAYLOAD_SIZE]; |
|||
} packet_t; |
|||
|
|||
typedef enum { |
|||
InverterDevInform_Simple = 0, // 0x00
|
|||
InverterDevInform_All = 1, // 0x01
|
|||
GridOnProFilePara = 2, // 0x02
|
|||
HardWareConfig = 3, // 0x03
|
|||
SimpleCalibrationPara = 4, // 0x04
|
|||
SystemConfigPara = 5, // 0x05
|
|||
RealTimeRunData_Debug = 11, // 0x0b
|
|||
RealTimeRunData_Reality = 12, // 0x0c
|
|||
RealTimeRunData_A_Phase = 13, // 0x0d
|
|||
RealTimeRunData_B_Phase = 14, // 0x0e
|
|||
RealTimeRunData_C_Phase = 15, // 0x0f
|
|||
AlarmData = 17, // 0x11, Alarm data - all unsent alarms
|
|||
AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms
|
|||
RecordData = 19, // 0x13
|
|||
InternalData = 20, // 0x14
|
|||
GetLossRate = 21, // 0x15
|
|||
GetSelfCheckState = 30, // 0x1e
|
|||
InitDataState = 0xff |
|||
} InfoCmdType; |
|||
|
|||
typedef enum { |
|||
TurnOn = 0, // 0x00
|
|||
TurnOff = 1, // 0x01
|
|||
Restart = 2, // 0x02
|
|||
Lock = 3, // 0x03
|
|||
Unlock = 4, // 0x04
|
|||
ActivePowerContr = 11, // 0x0b
|
|||
ReactivePowerContr = 12, // 0x0c
|
|||
PFSet = 13, // 0x0d
|
|||
CleanState_LockAndAlarm = 20, // 0x14
|
|||
SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files
|
|||
Init = 0xff |
|||
} DevControlCmdType; |
|||
|
|||
typedef enum { |
|||
AbsolutNonPersistent = 0UL, // 0x0000
|
|||
RelativNonPersistent = 1UL, // 0x0001
|
|||
AbsolutPersistent = 256UL, // 0x0100
|
|||
RelativPersistent = 257UL // 0x0101
|
|||
} PowerLimitControlType; |
|||
|
|||
#define MIN_SERIAL_INTERVAL 5 |
|||
#define MIN_SEND_INTERVAL 15 |
|||
#define MIN_MQTT_INTERVAL 60 |
|||
|
|||
|
|||
#define MQTT_STATUS_NOT_AVAIL_NOT_PROD 0 |
|||
#define MQTT_STATUS_AVAIL_NOT_PROD 1 |
|||
#define MQTT_STATUS_AVAIL_PROD 2 |
|||
|
|||
//-------------------------------------
|
|||
// EEPROM
|
|||
//-------------------------------------
|
|||
#define SSID_LEN 32 |
|||
#define PWD_LEN 64 |
|||
#define DEVNAME_LEN 16 |
|||
#define CRC_LEN 2 // uint16_t
|
|||
#define DISCLAIMER 1 |
|||
|
|||
#define INV_ADDR_LEN MAX_NUM_INVERTERS * 8 // uint64_t
|
|||
#define INV_NAME_LEN MAX_NUM_INVERTERS * MAX_NAME_LENGTH // char[]
|
|||
#define INV_CH_CH_PWR_LEN MAX_NUM_INVERTERS * 2 * 4 // uint16_t (4 channels)
|
|||
#define INV_CH_CH_NAME_LEN MAX_NUM_INVERTERS * MAX_NAME_LENGTH * 4 // (4 channels)
|
|||
#define INV_INTERVAL_LEN 2 // uint16_t
|
|||
#define INV_MAX_RTRY_LEN 1 // uint8_t
|
|||
|
|||
#define CFG_SUN_LEN 9 // 2x float(4+4) + bool(1)
|
|||
|
|||
#define NTP_ADDR_LEN 32 // DNS Name
|
|||
|
|||
#define MQTT_ADDR_LEN 32 // DNS Name
|
|||
#define MQTT_USER_LEN 16 |
|||
#define MQTT_PWD_LEN 32 |
|||
#define MQTT_TOPIC_LEN 32 |
|||
#define MQTT_DISCOVERY_PREFIX "homeassistant" |
|||
#define MQTT_MAX_PACKET_SIZE 384 |
|||
#define MQTT_RECONNECT_DELAY 5000 |
|||
|
|||
#pragma pack(push) // push current alignment to stack
|
|||
#pragma pack(1) // set alignment to 1 byte boundary
|
|||
typedef struct { |
|||
char broker[MQTT_ADDR_LEN]; |
|||
uint16_t port; |
|||
char user[MQTT_USER_LEN]; |
|||
char pwd[MQTT_PWD_LEN]; |
|||
char topic[MQTT_TOPIC_LEN]; |
|||
} mqttConfig_t; |
|||
#pragma pack(pop) // restore original alignment from stack
|
|||
|
|||
|
|||
typedef struct { |
|||
char deviceName[DEVNAME_LEN]; |
|||
|
|||
// wifi
|
|||
char stationSsid[SSID_LEN]; |
|||
char stationPwd[PWD_LEN]; |
|||
} sysConfig_t; |
|||
|
|||
#pragma pack(push) // push current alignment to stack
|
|||
#pragma pack(1) // set alignment to 1 byte boundary
|
|||
typedef struct { |
|||
// nrf24
|
|||
uint16_t sendInterval; |
|||
uint8_t maxRetransPerPyld; |
|||
uint8_t pinCs; |
|||
uint8_t pinCe; |
|||
uint8_t pinIrq; |
|||
uint8_t amplifierPower; |
|||
|
|||
// Disclaimer
|
|||
bool disclaimer; |
|||
|
|||
// ntp
|
|||
char ntpAddr[NTP_ADDR_LEN]; |
|||
uint16_t ntpPort; |
|||
|
|||
// mqtt
|
|||
mqttConfig_t mqtt; |
|||
|
|||
// sun
|
|||
float sunLat; |
|||
float sunLon; |
|||
bool sunDisNightCom; // disable night communication
|
|||
|
|||
// serial
|
|||
uint16_t serialInterval; |
|||
bool serialShowIv; |
|||
bool serialDebug; |
|||
} config_t; |
|||
#pragma pack(pop) // restore original alignment from stack
|
|||
|
|||
typedef struct { |
|||
uint32_t rxFail; |
|||
uint32_t rxFailNoAnser; |
|||
uint32_t rxSuccess; |
|||
uint32_t frmCnt; |
|||
} statistics_t; |
|||
|
|||
|
|||
#define CFG_MQTT_LEN MQTT_ADDR_LEN + 2 + MQTT_USER_LEN + MQTT_PWD_LEN +MQTT_TOPIC_LEN |
|||
#define CFG_SYS_LEN DEVNAME_LEN + SSID_LEN + PWD_LEN + 1 |
|||
#define CFG_LEN 7 + NTP_ADDR_LEN + 2 + CFG_MQTT_LEN + CFG_SUN_LEN + 4 + DISCLAIMER |
|||
|
|||
#define ADDR_START 0 |
|||
#define ADDR_CFG_SYS ADDR_START |
|||
#define ADDR_WIFI_CRC ADDR_CFG_SYS + CFG_SYS_LEN |
|||
#define ADDR_START_SETTINGS ADDR_WIFI_CRC + CRC_LEN |
|||
|
|||
#define ADDR_CFG ADDR_START_SETTINGS |
|||
#define ADDR_CFG_INVERTER ADDR_CFG + CFG_LEN |
|||
|
|||
#define ADDR_INV_ADDR ADDR_CFG_INVERTER |
|||
#define ADDR_INV_NAME ADDR_INV_ADDR + INV_ADDR_LEN |
|||
#define ADDR_INV_CH_PWR ADDR_INV_NAME + INV_NAME_LEN |
|||
#define ADDR_INV_CH_NAME ADDR_INV_CH_PWR + INV_CH_CH_PWR_LEN |
|||
#define ADDR_INV_INTERVAL ADDR_INV_CH_NAME + INV_CH_CH_NAME_LEN |
|||
#define ADDR_INV_MAX_RTRY ADDR_INV_INTERVAL + INV_INTERVAL_LEN |
|||
|
|||
#define ADDR_NEXT ADDR_INV_MAX_RTRY + INV_INTERVAL_LEN |
|||
|
|||
|
|||
#define ADDR_SETTINGS_CRC ADDR_NEXT + 2 |
|||
|
|||
#if(ADDR_SETTINGS_CRC <= ADDR_NEXT) |
|||
#pragma error "address overlap! (ADDR_SETTINGS_CRC="+ ADDR_SETTINGS_CRC +", ADDR_NEXT="+ ADDR_NEXT +")" |
|||
#endif |
|||
|
|||
#if(ADDR_SETTINGS_CRC >= 4096 - CRC_LEN) |
|||
#pragma error "EEPROM size exceeded! (ADDR_SETTINGS_CRC="+ ADDR_SETTINGS_CRC +", CRC_LEN="+ CRC_LEN +")" |
|||
#pragma error "Configure less inverters? (MAX_NUM_INVERTERS=" + MAX_NUM_INVERTERS +")" |
|||
#endif |
|||
|
|||
|
|||
|
|||
#endif /*__DEFINES_H__*/ |
@ -1,161 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __EEP_H__ |
|||
#define __EEP_H__ |
|||
|
|||
#include "Arduino.h" |
|||
#include <EEPROM.h> |
|||
#ifdef ESP32 |
|||
#include <nvs_flash.h> |
|||
#endif |
|||
|
|||
class eep { |
|||
public: |
|||
eep() { |
|||
|
|||
#ifdef ESP32 |
|||
if(!EEPROM.begin(4096)) { |
|||
nvs_flash_init(); |
|||
EEPROM.begin(4096); |
|||
} |
|||
#else |
|||
EEPROM.begin(4096); |
|||
#endif |
|||
|
|||
} |
|||
~eep() { |
|||
EEPROM.end(); |
|||
} |
|||
|
|||
void read(uint32_t addr, char *str, uint8_t length) { |
|||
for(uint8_t i = 0; i < length; i ++) { |
|||
*(str++) = (char)EEPROM.read(addr++); |
|||
} |
|||
} |
|||
|
|||
void read(uint32_t addr, float *value) { |
|||
uint8_t *p = (uint8_t*)value; |
|||
for(uint8_t i = 0; i < 4; i ++) { |
|||
*(p++) = (uint8_t)EEPROM.read(addr++); |
|||
} |
|||
} |
|||
|
|||
void read(uint32_t addr, bool *value) { |
|||
uint8_t intVal = 0x00; |
|||
intVal = EEPROM.read(addr++); |
|||
*value = (intVal == 0x01); |
|||
} |
|||
|
|||
void read(uint32_t addr, uint8_t *value) { |
|||
*value = (EEPROM.read(addr++)); |
|||
} |
|||
|
|||
void read(uint32_t addr, uint8_t data[], uint16_t length) { |
|||
for(uint16_t i = 0; i < length; i ++) { |
|||
*(data++) = EEPROM.read(addr++); |
|||
} |
|||
} |
|||
|
|||
void read(uint32_t addr, uint16_t *value) { |
|||
*value = (EEPROM.read(addr++) << 8); |
|||
*value |= (EEPROM.read(addr++)); |
|||
} |
|||
|
|||
void read(uint32_t addr, uint16_t data[], uint16_t length) { |
|||
for(uint16_t i = 0; i < length; i ++) { |
|||
*(data) = (EEPROM.read(addr++) << 8); |
|||
*(data++) |= (EEPROM.read(addr++)); |
|||
} |
|||
} |
|||
|
|||
void read(uint32_t addr, uint32_t *value) { |
|||
*value = (EEPROM.read(addr++) << 24); |
|||
*value |= (EEPROM.read(addr++) << 16); |
|||
*value |= (EEPROM.read(addr++) << 8); |
|||
*value |= (EEPROM.read(addr++)); |
|||
} |
|||
|
|||
void read(uint32_t addr, uint64_t *value) { |
|||
read(addr, (uint32_t *)value); |
|||
*value <<= 32; |
|||
uint32_t tmp; |
|||
read(addr+4, &tmp); |
|||
*value |= tmp; |
|||
/**value = (EEPROM.read(addr++) << 56);
|
|||
*value |= (EEPROM.read(addr++) << 48); |
|||
*value |= (EEPROM.read(addr++) << 40); |
|||
*value |= (EEPROM.read(addr++) << 32); |
|||
*value |= (EEPROM.read(addr++) << 24); |
|||
*value |= (EEPROM.read(addr++) << 16); |
|||
*value |= (EEPROM.read(addr++) << 8); |
|||
*value |= (EEPROM.read(addr++));*/ |
|||
} |
|||
|
|||
void write(uint32_t addr, const char *str, uint8_t length) { |
|||
for(uint8_t i = 0; i < length; i ++) { |
|||
EEPROM.write(addr++, str[i]); |
|||
} |
|||
} |
|||
|
|||
void write(uint32_t addr, uint8_t data[], uint16_t length) { |
|||
for(uint16_t i = 0; i < length; i ++) { |
|||
EEPROM.write(addr++, data[i]); |
|||
} |
|||
} |
|||
|
|||
void write(uint32_t addr, float value) { |
|||
uint8_t *p = (uint8_t*)&value; |
|||
for(uint8_t i = 0; i < 4; i ++) { |
|||
EEPROM.write(addr++, p[i]); |
|||
} |
|||
} |
|||
|
|||
void write(uint32_t addr, bool value) { |
|||
uint8_t intVal = (value) ? 0x01 : 0x00; |
|||
EEPROM.write(addr++, intVal); |
|||
} |
|||
|
|||
void write(uint32_t addr, uint8_t value) { |
|||
EEPROM.write(addr++, value); |
|||
} |
|||
|
|||
void write(uint32_t addr, uint16_t value) { |
|||
EEPROM.write(addr++, (value >> 8) & 0xff); |
|||
EEPROM.write(addr++, (value ) & 0xff); |
|||
} |
|||
|
|||
|
|||
void write(uint32_t addr, uint16_t data[], uint16_t length) { |
|||
for(uint16_t i = 0; i < length; i ++) { |
|||
EEPROM.write(addr++, (data[i] >> 8) & 0xff); |
|||
EEPROM.write(addr++, (data[i] ) & 0xff); |
|||
} |
|||
} |
|||
|
|||
void write(uint32_t addr, uint32_t value) { |
|||
EEPROM.write(addr++, (value >> 24) & 0xff); |
|||
EEPROM.write(addr++, (value >> 16) & 0xff); |
|||
EEPROM.write(addr++, (value >> 8) & 0xff); |
|||
EEPROM.write(addr++, (value ) & 0xff); |
|||
} |
|||
|
|||
void write(uint32_t addr, uint64_t value) { |
|||
EEPROM.write(addr++, (value >> 56) & 0xff); |
|||
EEPROM.write(addr++, (value >> 48) & 0xff); |
|||
EEPROM.write(addr++, (value >> 40) & 0xff); |
|||
EEPROM.write(addr++, (value >> 32) & 0xff); |
|||
EEPROM.write(addr++, (value >> 24) & 0xff); |
|||
EEPROM.write(addr++, (value >> 16) & 0xff); |
|||
EEPROM.write(addr++, (value >> 8) & 0xff); |
|||
EEPROM.write(addr++, (value ) & 0xff); |
|||
} |
|||
|
|||
void commit(void) { |
|||
EEPROM.commit(); |
|||
} |
|||
}; |
|||
|
|||
#endif /*__EEP_H__*/ |
@ -1,376 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://github.com/lumpapu/ahoy
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __RADIO_H__ |
|||
#define __RADIO_H__ |
|||
|
|||
#include "dbg.h" |
|||
#include <RF24.h> |
|||
#include "crc.h" |
|||
#ifndef DISABLE_IRQ |
|||
#if defined(ESP8266) || defined(ESP32) |
|||
#define DISABLE_IRQ noInterrupts() |
|||
#define RESTORE_IRQ interrupts() |
|||
#else |
|||
#define DISABLE_IRQ \ |
|||
uint8_t sreg = SREG; \ |
|||
cli(); |
|||
|
|||
#define RESTORE_IRQ \ |
|||
SREG = sreg; |
|||
#endif |
|||
#endif |
|||
//#define CHANNEL_HOP // switch between channels or use static channel to send
|
|||
|
|||
#define DEFAULT_RECV_CHANNEL 3 |
|||
#define SPI_SPEED 1000000 |
|||
|
|||
#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) |
|||
|
|||
#define RF_CHANNELS 5 |
|||
#define RF_LOOP_CNT 300 |
|||
|
|||
#define TX_REQ_INFO 0x15 |
|||
#define TX_REQ_DEVCONTROL 0x51 |
|||
#define ALL_FRAMES 0x80 |
|||
#define SINGLE_FRAME 0x81 |
|||
|
|||
const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
// MACROS
|
|||
//-----------------------------------------------------------------------------
|
|||
#define CP_U32_LittleEndian(buf, v) ({ \ |
|||
uint8_t *b = buf; \ |
|||
b[0] = ((v >> 24) & 0xff); \ |
|||
b[1] = ((v >> 16) & 0xff); \ |
|||
b[2] = ((v >> 8) & 0xff); \ |
|||
b[3] = ((v ) & 0xff); \ |
|||
}) |
|||
|
|||
#define CP_U32_BigEndian(buf, v) ({ \ |
|||
uint8_t *b = buf; \ |
|||
b[3] = ((v >> 24) & 0xff); \ |
|||
b[2] = ((v >> 16) & 0xff); \ |
|||
b[1] = ((v >> 8) & 0xff); \ |
|||
b[0] = ((v ) & 0xff); \ |
|||
}) |
|||
|
|||
#define BIT_CNT(x) ((x)<<3) |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
// HM Radio class
|
|||
//-----------------------------------------------------------------------------
|
|||
template <class BUFFER, uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW> |
|||
class HmRadio { |
|||
public: |
|||
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { |
|||
DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); |
|||
DPRINT(DBG_VERBOSE, String(CE_PIN)); |
|||
DPRINT(DBG_VERBOSE, F(", CS_PIN: ")); |
|||
DPRINT(DBG_VERBOSE, String(CS_PIN)); |
|||
DPRINT(DBG_VERBOSE, F(", SPI_SPEED: ")); |
|||
DPRINTLN(DBG_VERBOSE, String(SPI_SPEED) + ")"); |
|||
|
|||
// Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz.
|
|||
// Channel List 2403, 2423, 2440, 2461, 2475MHz
|
|||
mRfChLst[0] = 03; |
|||
mRfChLst[1] = 23; |
|||
mRfChLst[2] = 40; |
|||
mRfChLst[3] = 61; |
|||
mRfChLst[4] = 75; |
|||
|
|||
mTxChIdx = 2; // Start TX with 40
|
|||
mRxChIdx = 0; // Start RX with 03
|
|||
mRxLoopCnt = RF_LOOP_CNT; |
|||
|
|||
mSendCnt = 0; |
|||
|
|||
mSerialDebug = false; |
|||
mIrqRcvd = false; |
|||
} |
|||
~HmRadio() {} |
|||
|
|||
void setup(BUFFER *ctrl, uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); |
|||
pinMode(irq, INPUT_PULLUP); |
|||
mBufCtrl = ctrl; |
|||
|
|||
|
|||
uint32_t dtuSn = 0x87654321; |
|||
uint32_t chipID = 0; // will be filled with last 3 bytes of MAC
|
|||
#ifdef ESP32 |
|||
uint64_t MAC = ESP.getEfuseMac(); |
|||
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); |
|||
#else |
|||
chipID = ESP.getChipId(); |
|||
#endif |
|||
if(chipID) { |
|||
dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
|
|||
for(int i = 0; i < 7; i++) { |
|||
dtuSn |= (chipID % 10) << (i * 4); |
|||
chipID /= 10; |
|||
} |
|||
} |
|||
// change the byte order of the DTU serial number and append the required 0x01 at the end
|
|||
DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; |
|||
|
|||
mNrf24.begin(ce, cs); |
|||
mNrf24.setRetries(0, 0); |
|||
|
|||
mNrf24.setChannel(DEFAULT_RECV_CHANNEL); |
|||
mNrf24.setDataRate(RF24_250KBPS); |
|||
mNrf24.setCRCLength(RF24_CRC_16); |
|||
mNrf24.setAutoAck(false); |
|||
mNrf24.setPayloadSize(MAX_RF_PAYLOAD_SIZE); |
|||
mNrf24.setAddressWidth(5); |
|||
mNrf24.openReadingPipe(1, DTU_RADIO_ID); |
|||
mNrf24.enableDynamicPayloads(); |
|||
|
|||
// enable only receiving interrupts
|
|||
mNrf24.maskIRQ(true, true, false); |
|||
|
|||
DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); |
|||
DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); |
|||
mNrf24.setPALevel(ampPwr & 0x03); |
|||
mNrf24.startListening(); |
|||
|
|||
DPRINTLN(DBG_INFO, F("Radio Config:")); |
|||
mNrf24.printPrettyDetails(); |
|||
|
|||
mTxCh = setDefaultChannels(); |
|||
|
|||
if(!mNrf24.isChipConnected()) { |
|||
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); |
|||
} |
|||
} |
|||
|
|||
void loop(void) { |
|||
DISABLE_IRQ; |
|||
if(mIrqRcvd) { |
|||
mIrqRcvd = false; |
|||
bool tx_ok, tx_fail, rx_ready; |
|||
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
|
|||
RESTORE_IRQ; |
|||
uint8_t pipe, len; |
|||
packet_t *p; |
|||
while(mNrf24.available(&pipe)) { |
|||
if(!mBufCtrl->full()) { |
|||
p = mBufCtrl->getFront(); |
|||
p->rxCh = mRfChLst[mRxChIdx]; |
|||
len = mNrf24.getPayloadSize(); |
|||
if(len > MAX_RF_PAYLOAD_SIZE) |
|||
len = MAX_RF_PAYLOAD_SIZE; |
|||
|
|||
mNrf24.read(p->packet, len); |
|||
mBufCtrl->pushFront(p); |
|||
yield(); |
|||
} |
|||
else |
|||
break; |
|||
} |
|||
mNrf24.flush_rx(); // drop the packet
|
|||
} |
|||
else |
|||
RESTORE_IRQ; |
|||
} |
|||
|
|||
void enableDebug() { |
|||
mSerialDebug = true; |
|||
} |
|||
|
|||
void handleIntr(void) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:handleIntr"));
|
|||
mIrqRcvd = true; |
|||
} |
|||
|
|||
uint8_t setDefaultChannels(void) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setDefaultChannels"));
|
|||
mTxChIdx = 2; // Start TX with 40
|
|||
mRxChIdx = 0; // Start RX with 03
|
|||
return mRfChLst[mTxChIdx]; |
|||
} |
|||
|
|||
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) { |
|||
DPRINTLN(DBG_INFO, F("sendControlPacket cmd: ") + String(cmd)); |
|||
sendCmdPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME, false); |
|||
uint8_t cnt = 0; |
|||
mTxBuf[10 + cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
|
|||
mTxBuf[10 + cnt++] = 0x00; |
|||
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
|
|||
mTxBuf[10 + cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
|
|||
mTxBuf[10 + cnt++] = ((data[0] * 10) ) & 0xff; // power limit
|
|||
mTxBuf[10 + cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
|
|||
mTxBuf[10 + cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
|
|||
} |
|||
|
|||
// crc control data
|
|||
uint16_t crc = ah::crc16(&mTxBuf[10], cnt); |
|||
mTxBuf[10 + cnt++] = (crc >> 8) & 0xff; |
|||
mTxBuf[10 + cnt++] = (crc ) & 0xff; |
|||
|
|||
// crc over all
|
|||
mTxBuf[10 + cnt] = ah::crc8(mTxBuf, 10 + cnt); |
|||
|
|||
sendPacket(invId, mTxBuf, 10 + cnt + 1, true); |
|||
} |
|||
|
|||
void sendTimePacket(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId) { |
|||
DPRINTLN(DBG_INFO, F("sendTimePacket")); |
|||
sendCmdPacket(invId, TX_REQ_INFO, ALL_FRAMES, false); |
|||
mTxBuf[10] = cmd; // cid
|
|||
mTxBuf[11] = 0x00; |
|||
CP_U32_LittleEndian(&mTxBuf[12], ts); |
|||
if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { |
|||
mTxBuf[18] = (alarmMesId >> 8) & 0xff; |
|||
mTxBuf[19] = (alarmMesId ) & 0xff; |
|||
} |
|||
uint16_t crc = ah::crc16(&mTxBuf[10], 14); |
|||
mTxBuf[24] = (crc >> 8) & 0xff; |
|||
mTxBuf[25] = (crc ) & 0xff; |
|||
mTxBuf[26] = ah::crc8(mTxBuf, 26); |
|||
|
|||
sendPacket(invId, mTxBuf, 27, true); |
|||
} |
|||
|
|||
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) { |
|||
DPRINTLN(DBG_VERBOSE, F("sendCmdPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX)); |
|||
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); |
|||
mTxBuf[0] = mid; // message id
|
|||
CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); |
|||
CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); |
|||
mTxBuf[9] = pid; |
|||
if(calcCrc) { |
|||
mTxBuf[10] = ah::crc8(mTxBuf, 10); |
|||
sendPacket(invId, mTxBuf, 11, false); |
|||
} |
|||
} |
|||
|
|||
bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) { |
|||
//DPRINTLN(DBG_INFO, F("hmRadio.h:checkPaketCrc"));
|
|||
*len = (buf[0] >> 2); |
|||
if(*len > (MAX_RF_PAYLOAD_SIZE - 2)) |
|||
*len = MAX_RF_PAYLOAD_SIZE - 2; |
|||
for(uint8_t i = 1; i < (*len + 1); i++) { |
|||
buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7); |
|||
} |
|||
|
|||
uint8_t crc = ah::crc8(buf, *len-1); |
|||
bool valid = (crc == buf[*len-1]); |
|||
|
|||
return valid; |
|||
} |
|||
|
|||
bool switchRxCh(uint16_t addLoop = 0) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:switchRxCh"));
|
|||
mRxLoopCnt += addLoop; |
|||
if(mRxLoopCnt != 0) { |
|||
mRxLoopCnt--; |
|||
DISABLE_IRQ; |
|||
mNrf24.stopListening(); |
|||
mNrf24.setChannel(getRxNxtChannel()); |
|||
mNrf24.startListening(); |
|||
RESTORE_IRQ; |
|||
} |
|||
return (0 == mRxLoopCnt); // receive finished
|
|||
} |
|||
|
|||
void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf"));
|
|||
if(NULL != info) |
|||
DBGPRINT(String(info)); |
|||
for(uint8_t i = 0; i < len; i++) { |
|||
DHEX(buf[i]); |
|||
DBGPRINT(" "); |
|||
} |
|||
DBGPRINTLN(""); |
|||
} |
|||
|
|||
bool isChipConnected(void) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
|
|||
return mNrf24.isChipConnected(); |
|||
} |
|||
|
|||
|
|||
|
|||
uint32_t mSendCnt; |
|||
|
|||
bool mSerialDebug; |
|||
|
|||
private: |
|||
void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len, bool clear=false) { |
|||
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket"));
|
|||
//DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt));
|
|||
//dumpBuf("SEN ", buf, len);
|
|||
if(mSerialDebug) { |
|||
DPRINT(DBG_INFO, "TX " + String(len) + "B Ch" + String(mRfChLst[mTxChIdx]) + " | "); |
|||
dumpBuf(NULL, buf, len); |
|||
} |
|||
|
|||
DISABLE_IRQ; |
|||
mNrf24.stopListening(); |
|||
|
|||
if(clear) |
|||
mRxLoopCnt = RF_LOOP_CNT; |
|||
|
|||
mNrf24.setChannel(mRfChLst[mTxChIdx]); |
|||
mTxCh = getTxNxtChannel(); // switch channel for next packet
|
|||
mNrf24.openWritingPipe(invId); // TODO: deprecated
|
|||
mNrf24.setCRCLength(RF24_CRC_16); |
|||
mNrf24.enableDynamicPayloads(); |
|||
mNrf24.setAutoAck(true); |
|||
mNrf24.setRetries(3, 15); // 3*250us and 15 loops -> 11.25ms
|
|||
mNrf24.write(buf, len); |
|||
|
|||
// Try to avoid zero payload acks (has no effect)
|
|||
mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated
|
|||
mRxChIdx = 0; |
|||
mNrf24.setChannel(mRfChLst[mRxChIdx]); |
|||
mNrf24.setAutoAck(false); |
|||
mNrf24.setRetries(0, 0); |
|||
mNrf24.disableDynamicPayloads(); |
|||
mNrf24.setCRCLength(RF24_CRC_DISABLED); |
|||
mNrf24.startListening(); |
|||
|
|||
RESTORE_IRQ; |
|||
mSendCnt++; |
|||
} |
|||
|
|||
uint8_t getTxNxtChannel(void) { |
|||
|
|||
if(++mTxChIdx >= RF_CHANNELS) |
|||
mTxChIdx = 0; |
|||
return mRfChLst[mTxChIdx]; |
|||
} |
|||
|
|||
uint8_t getRxNxtChannel(void) { |
|||
|
|||
if(++mRxChIdx >= RF_CHANNELS) |
|||
mRxChIdx = 0; |
|||
return mRfChLst[mRxChIdx]; |
|||
} |
|||
|
|||
uint64_t DTU_RADIO_ID; |
|||
|
|||
uint8_t mTxCh; |
|||
uint8_t mTxChIdx; |
|||
|
|||
uint8_t mRfChLst[RF_CHANNELS]; |
|||
|
|||
uint8_t mRxChIdx; |
|||
uint16_t mRxLoopCnt; |
|||
|
|||
RF24 mNrf24; |
|||
BUFFER *mBufCtrl; |
|||
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; |
|||
|
|||
DevControlCmdType DevControlCmd; |
|||
|
|||
volatile bool mIrqRcvd; |
|||
}; |
|||
|
|||
#endif /*__RADIO_H__*/ |
@ -1,112 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://github.com/lumpapu/ahoy
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __HM_SYSTEM_H__ |
|||
#define __HM_SYSTEM_H__ |
|||
|
|||
#include "hmInverter.h" |
|||
#include "hmRadio.h" |
|||
#include "CircularBuffer.h" |
|||
|
|||
typedef CircularBuffer<packet_t, PACKET_BUFFER_SIZE> BufferType; |
|||
typedef HmRadio<BufferType> RadioType; |
|||
|
|||
template <uint8_t MAX_INVERTER=3, class RADIO = RadioType, class BUFFER = BufferType, class INVERTERTYPE=Inverter<float>> |
|||
class HmSystem { |
|||
public: |
|||
typedef RADIO RadioType; |
|||
RadioType Radio; |
|||
typedef BUFFER BufferType; |
|||
BufferType BufCtrl; |
|||
//DevControlCmdType DevControlCmd;
|
|||
|
|||
HmSystem() { |
|||
mNumInv = 0; |
|||
} |
|||
~HmSystem() { |
|||
// TODO: cleanup
|
|||
} |
|||
|
|||
void setup() { |
|||
Radio.setup(&BufCtrl); |
|||
} |
|||
|
|||
void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin) { |
|||
Radio.setup(&BufCtrl, ampPwr, irqPin, cePin, csPin); |
|||
} |
|||
|
|||
INVERTERTYPE *addInverter(const char *name, uint64_t serial, uint16_t chMaxPwr[]) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); |
|||
if(MAX_INVERTER <= mNumInv) { |
|||
DPRINT(DBG_WARN, F("max number of inverters reached!")); |
|||
return NULL; |
|||
} |
|||
INVERTERTYPE *p = &mInverter[mNumInv]; |
|||
p->id = mNumInv; |
|||
p->serial.u64 = serial; |
|||
memcpy(p->chMaxPwr, chMaxPwr, (4*2)); |
|||
DPRINT(DBG_VERBOSE, "SERIAL: " + String(p->serial.b[5], HEX)); |
|||
DPRINTLN(DBG_VERBOSE, " " + String(p->serial.b[4], HEX)); |
|||
if(p->serial.b[5] == 0x11) { |
|||
switch(p->serial.b[4]) { |
|||
case 0x21: p->type = INV_TYPE_1CH; break; |
|||
case 0x41: p->type = INV_TYPE_2CH; break; |
|||
case 0x61: p->type = INV_TYPE_4CH; break; |
|||
default: |
|||
DPRINT(DBG_ERROR, F("unknown inverter type: 11")); |
|||
DPRINTLN(DBG_ERROR, String(p->serial.b[4], HEX)); |
|||
break; |
|||
} |
|||
} |
|||
else |
|||
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); |
|||
|
|||
p->init(); |
|||
uint8_t len = (uint8_t)strlen(name); |
|||
strncpy(p->name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len); |
|||
|
|||
mNumInv ++; |
|||
return p; |
|||
} |
|||
|
|||
INVERTERTYPE *findInverter(uint8_t buf[]) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); |
|||
INVERTERTYPE *p; |
|||
for(uint8_t i = 0; i < mNumInv; i++) { |
|||
p = &mInverter[i]; |
|||
if((p->serial.b[3] == buf[0]) |
|||
&& (p->serial.b[2] == buf[1]) |
|||
&& (p->serial.b[1] == buf[2]) |
|||
&& (p->serial.b[0] == buf[3])) |
|||
return p; |
|||
} |
|||
return NULL; |
|||
} |
|||
|
|||
INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); |
|||
if(pos >= MAX_INVERTER) |
|||
return NULL; |
|||
else if((mInverter[pos].initialized && mInverter[pos].serial.u64 != 0ULL) || false == check) |
|||
return &mInverter[pos]; |
|||
else |
|||
return NULL; |
|||
} |
|||
|
|||
uint8_t getNumInverters(void) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getNumInverters")); |
|||
return mNumInv; |
|||
} |
|||
|
|||
void enableDebug() { |
|||
Radio.enableDebug(); |
|||
} |
|||
|
|||
private: |
|||
INVERTERTYPE mInverter[MAX_INVERTER]; |
|||
uint8_t mNumInv; |
|||
}; |
|||
|
|||
#endif /*__HM_SYSTEM_H__*/ |
@ -1,154 +0,0 @@ |
|||
/** |
|||
* GENERIC FUNCTIONS |
|||
*/ |
|||
|
|||
function topnav() { |
|||
toggle("topnav"); |
|||
} |
|||
|
|||
function parseMenu(obj) { |
|||
var e = document.getElementById("topnav"); |
|||
e.innerHTML = ""; |
|||
for(var i = 0; i < obj["name"].length; i ++) { |
|||
if(obj["name"][i] == "-") |
|||
e.appendChild(span("", ["seperator"])); |
|||
else { |
|||
var l = link(obj["link"][i], obj["name"][i], obj["trgt"][i]); |
|||
if(obj["link"][i] == window.location.pathname) |
|||
l.classList.add("active"); |
|||
e.appendChild(l); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function parseVersion(obj) { |
|||
document.getElementById("version").appendChild( |
|||
link("https://github.com/lumapu/ahoy/commits/" + obj["build"], "Git SHA: " + obj["build"] + " :: " + obj["version"], "_blank") |
|||
); |
|||
} |
|||
|
|||
function setHide(id, hide) { |
|||
var elm = document.getElementById(id); |
|||
if(hide) { |
|||
if(!elm.classList.contains("hide")) |
|||
elm.classList.add("hide"); |
|||
} |
|||
else |
|||
elm.classList.remove('hide'); |
|||
} |
|||
|
|||
|
|||
function toggle(id) { |
|||
var e = document.getElementById(id); |
|||
if(!e.classList.contains("hide")) |
|||
e.classList.add("hide"); |
|||
else |
|||
e.classList.remove('hide'); |
|||
} |
|||
|
|||
function getAjax(url, ptr, method="GET", json=null) { |
|||
var xhr = new XMLHttpRequest(); |
|||
if(xhr != null) { |
|||
xhr.open(method, url, true); |
|||
xhr.onreadystatechange = p; |
|||
if("POST" == method) |
|||
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); |
|||
xhr.send(json); |
|||
} |
|||
function p() { |
|||
if(xhr.readyState == 4) { |
|||
if(null != xhr.responseText) { |
|||
if(null != ptr) |
|||
ptr(JSON.parse(xhr.responseText)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* CREATE DOM FUNCTIONS |
|||
*/ |
|||
|
|||
function des(val) { |
|||
e = document.createElement('p'); |
|||
e.classList.add("subdes"); |
|||
e.innerHTML = val; |
|||
return e; |
|||
} |
|||
|
|||
function lbl(htmlfor, val, cl=null, id=null) { |
|||
e = document.createElement('label'); |
|||
e.htmlFor = htmlfor; |
|||
e.innerHTML = val; |
|||
if(null != cl) e.classList.add(...cl); |
|||
if(null != id) e.id = id; |
|||
return e; |
|||
} |
|||
|
|||
function inp(name, val, max=32, cl=["text"], id=null, type=null) { |
|||
e = document.createElement('input'); |
|||
e.classList.add(...cl); |
|||
e.name = name; |
|||
e.value = val; |
|||
if(null != type) e.maxLength = max; |
|||
if(null != id) e.id = id; |
|||
if(null != type) e.type = type; |
|||
return e; |
|||
} |
|||
|
|||
function sel(name, opt, selId) { |
|||
e = document.createElement('select'); |
|||
e.name = name; |
|||
for(it of opt) { |
|||
o = document.createElement('option'); |
|||
o.value = it[0]; |
|||
o.innerHTML = it[1]; |
|||
if(it[0] == selId) |
|||
o.selected = true; |
|||
e.appendChild(o); |
|||
} |
|||
return e; |
|||
} |
|||
|
|||
function selDelAllOpt(sel) { |
|||
var i, l = sel.options.length - 1; |
|||
for(i = l; i >= 0; i--) { |
|||
sel.remove(i); |
|||
} |
|||
} |
|||
|
|||
function opt(val, html) { |
|||
o = document.createElement('option'); |
|||
o.value = val; |
|||
o.innerHTML = html; |
|||
e.appendChild(o); |
|||
return o; |
|||
} |
|||
|
|||
function div(cl) { |
|||
e = document.createElement('div'); |
|||
e.classList.add(...cl); |
|||
return e; |
|||
} |
|||
|
|||
function span(val, cl=null, id=null) { |
|||
e = document.createElement('span'); |
|||
e.innerHTML = val; |
|||
if(null != cl) e.classList.add(...cl); |
|||
if(null != id) e.id = id; |
|||
return e; |
|||
} |
|||
|
|||
function br() { |
|||
return document.createElement('br'); |
|||
} |
|||
|
|||
function link(dst, text, target=null) { |
|||
var a = document.createElement('a'); |
|||
var t = document.createTextNode(text); |
|||
a.href = dst; |
|||
if(null != target) |
|||
a.target = target; |
|||
a.appendChild(t); |
|||
return a; |
|||
} |
@ -1,85 +0,0 @@ |
|||
import re |
|||
import os |
|||
import gzip |
|||
import glob |
|||
|
|||
from pathlib import Path |
|||
|
|||
def convert2Header(inFile, compress): |
|||
fileType = inFile.split(".")[1] |
|||
define = inFile.split(".")[0].upper() |
|||
define2 = inFile.split(".")[1].upper() |
|||
inFileVarName = inFile.replace(".", "_") |
|||
print(inFile + ", compress: " + str(compress)) |
|||
|
|||
if os.getcwd()[-4:] != "html": |
|||
outName = "html/" + "h/" + inFileVarName + ".h" |
|||
inFile = "html/" + inFile |
|||
Path("html/h").mkdir(exist_ok=True) |
|||
else: |
|||
outName = "h/" + inFileVarName + ".h" |
|||
Path("h").mkdir(exist_ok=True) |
|||
|
|||
f = open(inFile, "r") |
|||
data = f.read() |
|||
f.close() |
|||
|
|||
if fileType == "html": |
|||
if False == compress: |
|||
data = data.replace('\n', '') |
|||
data = re.sub(r"\>\s+\<", '><', data) # whitespaces between xml tags |
|||
data = re.sub(r"(\r\n|\r|\n)(\s+|\s?)", '', data) # whitespaces inner javascript |
|||
length = len(data) # get unescaped length |
|||
if False == compress: |
|||
data = re.sub(r"\"", '\\\"', data) # escape quotation marks |
|||
elif fileType == "js": |
|||
#data = re.sub(r"(\r\n|\r|\n)(\s+|\s?)", '', data) # whitespaces inner javascript |
|||
#data = re.sub(r"\s?(\=|\!\=|\{|,)+\s?", r'\1', data) # whitespaces inner javascript |
|||
length = len(data) # get unescaped length |
|||
if False == compress: |
|||
data = re.sub(r"\"", '\\\"', data) # escape quotation marks |
|||
else: |
|||
data = data.replace('\n', '') |
|||
data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css |
|||
length = len(data) # get unescaped length # get unescaped length |
|||
|
|||
f = open(outName, "w") |
|||
f.write("#ifndef __{}_{}_H__\n".format(define, define2)) |
|||
f.write("#define __{}_{}_H__\n".format(define, define2)) |
|||
if compress: |
|||
zipped = gzip.compress(bytes(data, 'utf-8')) |
|||
zippedStr = "" |
|||
for i in range(len(zipped)): |
|||
zippedStr += "0x{:02x}".format(zipped[i]) #hex(zipped[i]) |
|||
if (i + 1) != len(zipped): |
|||
zippedStr += ", " |
|||
if (i + 1) % 16 == 0 and i != 0: |
|||
zippedStr += "\n" |
|||
f.write("#define {}_len {}\n".format(inFileVarName, len(zipped))) |
|||
f.write("const uint8_t {}[] PROGMEM = {{\n{}}};\n".format(inFileVarName, zippedStr)) |
|||
else: |
|||
f.write("const char {}[] PROGMEM = \"{}\";\n".format(inFileVarName, data)) |
|||
f.write("const uint32_t {}_len = {};\n".format(inFileVarName, length)) |
|||
f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) |
|||
f.close() |
|||
|
|||
# delete all files in the 'h' dir, but ignore 'favicon_ico_gz.h' |
|||
dir = 'h' |
|||
if os.getcwd()[-4:] != "html": |
|||
dir = "html/" + dir |
|||
|
|||
for f in os.listdir(dir): |
|||
if not f.startswith('favicon_ico_gz'): |
|||
os.remove(os.path.join(dir, f)) |
|||
|
|||
# grab all files with following extensions |
|||
if os.getcwd()[-4:] != "html": |
|||
os.chdir('./html') |
|||
types = ('*.html', '*.css', '*.js') # the tuple of file types |
|||
files_grabbed = [] |
|||
for files in types: |
|||
files_grabbed.extend(glob.glob(files)) |
|||
|
|||
# go throw the array |
|||
for val in files_grabbed: |
|||
convert2Header(val, True) |
@ -1,100 +0,0 @@ |
|||
#ifndef __FAVICON_ICO_GZ_H__ |
|||
#define __FAVICON_ICO_GZ_H__ |
|||
#define favicon_ico_gz_len 1533 |
|||
const uint8_t favicon_ico_gz[] PROGMEM = {0x1f, 0x8b, 0x08, 0x08, 0xf2, 0xc5, 0xd5, 0x62, 0x04, 0x00, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f, |
|||
0x6e, 0x2e, 0x69, 0x63, 0x6f, 0x00, 0xed, 0x5c, 0x49, 0x68, 0x13, 0x51, 0x18, 0xfe, 0x62, 0xa3, |
|||
0x51, 0x28, 0xd6, 0x83, 0x82, 0xa0, 0x98, 0xb8, 0x1c, 0xbc, 0x59, 0x11, 0x5c, 0x50, 0xac, 0x88, |
|||
0x8a, 0xb8, 0xdd, 0x3c, 0x89, 0xd0, 0x93, 0x7a, 0x53, 0x51, 0x9b, 0x80, 0x4b, 0x46, 0xad, 0xfb, |
|||
0xd2, 0xb4, 0x2e, 0xb8, 0xa3, 0xc6, 0xba, 0xe1, 0x02, 0xae, 0xad, 0x0a, 0x26, 0x3d, 0xe8, 0xc5, |
|||
0x83, 0x57, 0x31, 0x2d, 0xc1, 0x8b, 0xb7, 0x92, 0x63, 0x0e, 0xa1, 0xcf, 0xff, 0xcf, 0xbc, 0xc9, |
|||
0x32, 0xa4, 0x66, 0xcf, 0x4b, 0xf3, 0xfa, 0xc3, 0xc7, 0x97, 0xcc, 0xcc, 0xcb, 0xf7, 0xbe, 0x6f, |
|||
0x26, 0x6f, 0x26, 0xf3, 0x92, 0x00, 0x0e, 0x34, 0x61, 0xda, 0x34, 0x66, 0x0f, 0xf6, 0x38, 0x81, |
|||
0xa5, 0x00, 0x3c, 0x1e, 0xf3, 0xf9, 0x53, 0x5a, 0x7e, 0x8f, 0x96, 0xad, 0x59, 0x63, 0x3e, 0x5f, |
|||
0xb8, 0x16, 0xd8, 0x30, 0x03, 0x58, 0x48, 0xdb, 0xd0, 0x2a, 0x5a, 0x62, 0x2e, 0x1f, 0xad, 0x06, |
|||
0xc3, 0x5d, 0xc6, 0x60, 0x38, 0x20, 0x08, 0x5e, 0xf1, 0x08, 0x77, 0x09, 0x42, 0xf4, 0xe2, 0xd7, |
|||
0x67, 0x2f, 0x56, 0xf5, 0x7b, 0x21, 0x18, 0xfc, 0xb8, 0x63, 0x0b, 0x7e, 0x11, 0x84, 0x77, 0x0b, |
|||
0xee, 0x2e, 0x9a, 0x03, 0x6f, 0xab, 0x1b, 0x82, 0x31, 0x38, 0x10, 0x38, 0xce, 0xaf, 0x51, 0x72, |
|||
0x7b, 0xd2, 0xe6, 0xf6, 0xbf, 0xbf, 0xf6, 0xcc, 0x2e, 0xbd, 0x7d, 0xf2, 0x35, 0x76, 0x88, 0x20, |
|||
0xf6, 0x96, 0xde, 0x3e, 0xe0, 0xa5, 0x76, 0xad, 0xd4, 0xfe, 0x64, 0xc9, 0xed, 0xa9, 0xff, 0xd4, |
|||
0xd6, 0x4f, 0x68, 0x2f, 0xb9, 0x3d, 0xf5, 0x9f, 0x70, 0xb8, 0xaf, 0x03, 0x4b, 0x4b, 0x6e, 0x4f, |
|||
0xfd, 0xe7, 0x7d, 0x4b, 0x6d, 0x4f, 0x96, 0xd3, 0xde, 0x7a, 0x8d, 0x72, 0xda, 0x73, 0x7d, 0xdb, |
|||
0x8f, 0x29, 0x45, 0xb5, 0x7f, 0x84, 0x4e, 0xfb, 0x71, 0xda, 0xb1, 0x19, 0x9d, 0x05, 0xb5, 0xef, |
|||
0xc5, 0xa5, 0xd1, 0x8e, 0x75, 0xef, 0x56, 0x9c, 0xfa, 0x6f, 0xfb, 0x87, 0x58, 0x86, 0x3c, 0x75, |
|||
0x70, 0x13, 0x96, 0xe5, 0x6a, 0x1f, 0x19, 0xe8, 0x6a, 0x47, 0x81, 0xd5, 0xea, 0x41, 0x7b, 0x56, |
|||
0xfb, 0x50, 0xe0, 0x72, 0xae, 0xed, 0xbe, 0x1a, 0x98, 0xcc, 0xc8, 0xf9, 0x1a, 0x73, 0x70, 0x9a, |
|||
0xda, 0x8f, 0xda, 0xf6, 0xb9, 0x81, 0x49, 0xfd, 0x3e, 0xbc, 0xa5, 0xfd, 0xd7, 0xcf, 0xf9, 0x8f, |
|||
0xf6, 0x1a, 0xff, 0xeb, 0xe7, 0x04, 0x42, 0x13, 0xc3, 0x20, 0x84, 0x00, 0x57, 0x14, 0x68, 0x89, |
|||
0x01, 0xee, 0x38, 0xb0, 0xfa, 0x08, 0x70, 0x64, 0xb5, 0x39, 0xce, 0x78, 0x08, 0x6b, 0x0a, 0x1f, |
|||
0x67, 0x2c, 0x0c, 0xf3, 0x72, 0xf1, 0x18, 0xab, 0xe4, 0x3e, 0x4f, 0x23, 0x88, 0x37, 0xbc, 0xae, |
|||
0xcf, 0x07, 0xaf, 0xf9, 0x1e, 0x48, 0x83, 0x97, 0xf1, 0xba, 0x43, 0x5b, 0xf0, 0x86, 0x8f, 0x87, |
|||
0x4c, 0xd0, 0xfe, 0x5d, 0xc5, 0xeb, 0x28, 0x97, 0x61, 0x33, 0xdf, 0x34, 0x78, 0x39, 0x8f, 0x55, |
|||
0xaa, 0xf4, 0x23, 0x03, 0x81, 0x53, 0x56, 0x1f, 0x54, 0xe8, 0xb3, 0xe6, 0x50, 0x28, 0x70, 0xde, |
|||
0xea, 0x83, 0x0a, 0xfd, 0x64, 0x1f, 0xc2, 0x81, 0x93, 0xbc, 0x0d, 0xf1, 0x01, 0x15, 0xfa, 0x49, |
|||
0x0c, 0x74, 0x9d, 0xe3, 0xed, 0x94, 0xe9, 0x13, 0x78, 0x1f, 0x24, 0xf5, 0x7b, 0xb1, 0x54, 0x89, |
|||
0x3e, 0xef, 0x7f, 0x59, 0x7c, 0xce, 0x53, 0xa5, 0x2f, 0x7a, 0xe0, 0x92, 0x7d, 0x38, 0xad, 0x44, |
|||
0x9f, 0xf6, 0x3f, 0x9f, 0xb3, 0x65, 0x1f, 0x3a, 0x95, 0xe8, 0x9b, 0xbe, 0x8f, 0x59, 0x7d, 0x50, |
|||
0xa4, 0x9f, 0xea, 0x03, 0xe9, 0xaf, 0x57, 0xa4, 0x9f, 0xea, 0x83, 0x42, 0x7d, 0xbe, 0x7e, 0xd9, |
|||
0xcf, 0xeb, 0x3e, 0xf9, 0xd0, 0xae, 0x44, 0x9f, 0xf7, 0xbf, 0x2c, 0x3a, 0x87, 0x9f, 0x56, 0xa9, |
|||
0x6f, 0xf5, 0x41, 0xa5, 0xbe, 0xbc, 0x0e, 0x3d, 0xa3, 0x52, 0xdf, 0xea, 0x83, 0x4a, 0x7d, 0xae, |
|||
0x0f, 0x87, 0x30, 0x5f, 0xa5, 0x3e, 0x57, 0x0d, 0xf4, 0x8d, 0x7c, 0xfa, 0xa4, 0x67, 0x54, 0x49, |
|||
0xff, 0x86, 0x10, 0x74, 0x29, 0x9b, 0xbf, 0x1c, 0xa4, 0xd9, 0x5d, 0x51, 0xfd, 0x20, 0x6e, 0x16, |
|||
0xa8, 0x9d, 0xd5, 0x87, 0x8a, 0xe8, 0x17, 0xaf, 0x9d, 0xea, 0x03, 0xe9, 0xf6, 0x94, 0xa9, 0x7f, |
|||
0xab, 0x44, 0xed, 0xac, 0x3e, 0x94, 0xa4, 0xff, 0x1c, 0xcd, 0x65, 0x6a, 0xa7, 0xfa, 0x60, 0x6c, |
|||
0x47, 0x73, 0xb1, 0xfa, 0xd5, 0xa8, 0x42, 0xf4, 0x23, 0xe1, 0xc0, 0x15, 0x54, 0xa9, 0x16, 0xbb, |
|||
0x71, 0xe5, 0x7f, 0xfa, 0x91, 0x50, 0xf7, 0x6d, 0x21, 0x8c, 0x09, 0xa8, 0x5e, 0x39, 0x48, 0xf3, |
|||
0x6a, 0x86, 0x7e, 0x59, 0xda, 0xfd, 0x07, 0x31, 0x97, 0x81, 0xe2, 0x2a, 0xdd, 0x87, 0xb4, 0xfe, |
|||
0x9d, 0x62, 0xb5, 0xe9, 0xde, 0xcb, 0x1c, 0x3a, 0xef, 0x0c, 0xf5, 0x77, 0xe0, 0xcf, 0x17, 0x1f, |
|||
0xe6, 0xa1, 0xb8, 0xe2, 0x3e, 0x5c, 0x23, 0x94, 0xa4, 0xfd, 0xd1, 0x07, 0x0f, 0x5d, 0x03, 0x45, |
|||
0xad, 0x73, 0x3f, 0x3f, 0xe6, 0x65, 0x28, 0xae, 0x92, 0x7d, 0x70, 0x4e, 0x9e, 0xe6, 0xe9, 0x24, |
|||
0xac, 0x23, 0x2c, 0x20, 0x4c, 0x27, 0x34, 0x13, 0x78, 0xf9, 0x24, 0xc2, 0x44, 0x7e, 0x7c, 0x96, |
|||
0xb0, 0x91, 0xb0, 0x90, 0x30, 0xd3, 0x5c, 0xe7, 0x6c, 0x26, 0x4c, 0x25, 0xb4, 0x30, 0x02, 0x84, |
|||
0x9f, 0x84, 0x58, 0x1a, 0xee, 0xb8, 0xc7, 0xd9, 0x96, 0xf0, 0x38, 0xfd, 0x23, 0x1e, 0xa7, 0x10, |
|||
0x1e, 0x97, 0x10, 0x51, 0x0b, 0x2d, 0x23, 0xfe, 0x98, 0x3b, 0xd1, 0x16, 0x6f, 0x8b, 0xbb, 0x13, |
|||
0x0b, 0xe5, 0x3d, 0x0a, 0xa3, 0xf8, 0xfb, 0x14, 0xb9, 0x3e, 0xbb, 0x45, 0xac, 0xed, 0xe5, 0x67, |
|||
0x38, 0x2f, 0x8f, 0xa1, 0x79, 0xd1, 0x8b, 0x5d, 0xb6, 0xeb, 0x89, 0x61, 0xce, 0x35, 0x0f, 0x86, |
|||
0x6d, 0xf7, 0xc1, 0x76, 0xf1, 0x58, 0x9b, 0x17, 0x9b, 0xe1, 0xb5, 0x8d, 0x09, 0x91, 0xf4, 0xfb, |
|||
0x21, 0x8d, 0x02, 0xee, 0x93, 0x68, 0xe7, 0x3f, 0x6f, 0x06, 0x1a, 0xf8, 0xe7, 0x1a, 0x0a, 0x75, |
|||
0x9d, 0xd0, 0xcd, 0xff, 0xe0, 0x40, 0xf7, 0xa5, 0xbc, 0x19, 0x34, 0xb2, 0x7f, 0xf3, 0xda, 0xe0, |
|||
0xa2, 0x3d, 0x03, 0x9d, 0xfc, 0xe7, 0xca, 0x80, 0xef, 0x5b, 0xea, 0xe4, 0xdf, 0xcc, 0xa0, 0xeb, |
|||
0x82, 0x3d, 0x03, 0x9d, 0xfc, 0xe7, 0xca, 0x20, 0x12, 0xee, 0xee, 0xd4, 0xc9, 0x3f, 0x21, 0x35, |
|||
0x77, 0x91, 0xce, 0x20, 0xb0, 0x5b, 0x27, 0xff, 0xe9, 0xb9, 0x93, 0x74, 0xe9, 0xe6, 0xdf, 0xca, |
|||
0x40, 0x67, 0xff, 0xd6, 0xdc, 0x91, 0xd6, 0xfe, 0x93, 0xe8, 0xf6, 0xdb, 0xfc, 0xb7, 0xe7, 0xf5, |
|||
0xde, 0x48, 0xfe, 0x6d, 0xe3, 0xbf, 0x35, 0x7f, 0xa5, 0xb3, 0xff, 0x82, 0x32, 0x68, 0x70, 0xff, |
|||
0x32, 0x83, 0x33, 0xba, 0xf9, 0x17, 0x0f, 0x30, 0x2b, 0xeb, 0x79, 0x10, 0x97, 0xb5, 0xf2, 0x4f, |
|||
0xe3, 0x3f, 0xc1, 0x6f, 0xcb, 0xe0, 0xac, 0x56, 0xfe, 0xe5, 0xdc, 0xad, 0x3d, 0x03, 0xcd, 0xfc, |
|||
0x67, 0x65, 0xc0, 0xf3, 0x0a, 0xf4, 0xbc, 0x5b, 0x33, 0xff, 0x3c, 0x97, 0x74, 0xd4, 0x9e, 0x81, |
|||
0x56, 0xfe, 0x73, 0x67, 0xd0, 0xa3, 0x95, 0x7f, 0x13, 0x87, 0x6d, 0x19, 0xcc, 0xd6, 0xcc, 0x3f, |
|||
0x1f, 0xf3, 0x47, 0x32, 0xb7, 0xd5, 0xce, 0xbf, 0x3c, 0x0e, 0x34, 0xf7, 0x9f, 0xcc, 0x40, 0x73, |
|||
0xff, 0x3c, 0x26, 0x6e, 0xb3, 0xda, 0xc8, 0xef, 0x57, 0x5d, 0xd1, 0xca, 0xbf, 0x6d, 0xfc, 0x17, |
|||
0x34, 0x15, 0xd9, 0xe7, 0xc5, 0x55, 0x5d, 0xfd, 0x17, 0x98, 0x41, 0x43, 0xfb, 0xb7, 0x32, 0xa0, |
|||
0xef, 0xd8, 0x5d, 0xd3, 0xd5, 0x7f, 0x66, 0x06, 0xba, 0xfa, 0xcf, 0xcc, 0x40, 0x57, 0xff, 0x56, |
|||
0x06, 0xe4, 0xf9, 0xba, 0xae, 0xfe, 0x33, 0x33, 0xd0, 0xd5, 0x7f, 0x66, 0x06, 0xba, 0xfa, 0xcf, |
|||
0x18, 0x0f, 0x4e, 0xeb, 0xea, 0xdf, 0xaa, 0x71, 0xff, 0xe3, 0xfe, 0x1b, 0xc4, 0xff, 0x4e, 0x94, |
|||
0x59, 0x1d, 0x5b, 0xb1, 0x33, 0xaf, 0xf7, 0x7a, 0xf4, 0x1f, 0xc4, 0x13, 0xf1, 0x9c, 0x7e, 0x4e, |
|||
0x5a, 0x66, 0x19, 0x06, 0x26, 0x50, 0x06, 0xf7, 0xc7, 0x94, 0xff, 0x0a, 0x79, 0x2f, 0x2a, 0x83, |
|||
0xfa, 0xf1, 0xff, 0x54, 0x7c, 0x85, 0x13, 0x15, 0x2e, 0xce, 0x80, 0x7e, 0x83, 0xf2, 0xa0, 0xce, |
|||
0xfd, 0x57, 0xc5, 0xbb, 0x55, 0xdb, 0xb7, 0xa3, 0x89, 0x33, 0xa8, 0x53, 0xff, 0xcf, 0xaa, 0xe9, |
|||
0x3d, 0x33, 0x03, 0xf2, 0xfb, 0xb0, 0xce, 0xfc, 0xd7, 0xc4, 0xbb, 0x3d, 0x83, 0x3a, 0xf1, 0x5f, |
|||
0x53, 0xef, 0x59, 0xef, 0x85, 0xad, 0x08, 0x2a, 0xf6, 0xff, 0x5c, 0x85, 0x77, 0x7b, 0x06, 0x8a, |
|||
0xfc, 0x2b, 0xf5, 0x6e, 0xcf, 0xa0, 0xa6, 0xfe, 0x83, 0x58, 0x54, 0xc9, 0xf3, 0x7b, 0xb9, 0xc5, |
|||
0x19, 0xd0, 0xff, 0x9c, 0x2c, 0xaa, 0x95, 0xff, 0xb1, 0x50, 0xe3, 0xfe, 0xc7, 0xfd, 0xb7, 0x56, |
|||
0xc8, 0xff, 0x50, 0xb8, 0xeb, 0x3e, 0xc6, 0x58, 0xd1, 0x7f, 0xfd, 0xdc, 0x6f, 0xad, 0x8c, 0xff, |
|||
0x17, 0x3f, 0x7e, 0xdc, 0x9c, 0x88, 0x31, 0x56, 0xdb, 0xe9, 0x2f, 0x8c, 0xc8, 0x5f, 0x6f, 0x99, |
|||
0xfe, 0xc7, 0xa4, 0xf7, 0x62, 0x32, 0x68, 0x54, 0xef, 0xb6, 0x0c, 0x1e, 0x17, 0xe9, 0xff, 0x65, |
|||
0x23, 0x78, 0x2f, 0x24, 0x03, 0x95, 0xde, 0x69, 0x1e, 0xf8, 0x14, 0x03, 0x35, 0x28, 0xce, 0x60, |
|||
0x91, 0x1b, 0x4f, 0x6c, 0xfe, 0xd5, 0x79, 0xf7, 0xe1, 0x44, 0x6a, 0xce, 0xab, 0x03, 0xe7, 0x51, |
|||
0x83, 0xca, 0x91, 0x41, 0xe6, 0x77, 0xfc, 0xdf, 0xfd, 0xfe, 0xd0, 0xe3, 0x42, 0x0d, 0xea, 0x93, |
|||
0x17, 0xc7, 0xed, 0xf3, 0xbe, 0x74, 0x1c, 0x9c, 0x43, 0x0d, 0x6a, 0xc9, 0x12, 0x4c, 0x24, 0xdf, |
|||
0xaf, 0x6c, 0xfe, 0xdf, 0xab, 0xf4, 0xae, 0x28, 0x83, 0xd7, 0x84, 0xba, 0xf1, 0x6e, 0x81, 0xb7, |
|||
0x41, 0x0d, 0xca, 0xca, 0x00, 0xcb, 0x11, 0x75, 0xd1, 0xd7, 0x14, 0x0d, 0xc9, 0x21, 0xc9, 0x51, |
|||
0xc9, 0xfb, 0x24, 0x2f, 0x97, 0x3c, 0x5b, 0xf2, 0x54, 0xc9, 0x2e, 0xc9, 0x4d, 0x7d, 0x26, 0x3b, |
|||
0xe2, 0x26, 0xc3, 0x62, 0x9f, 0xe4, 0xe5, 0x92, 0x57, 0x49, 0x5e, 0x2d, 0xb9, 0x4d, 0xf2, 0x4a, |
|||
0xc3, 0xe4, 0x15, 0x21, 0xb9, 0x7d, 0x54, 0x72, 0xbb, 0xe4, 0x56, 0xc9, 0x33, 0x25, 0x37, 0x4b, |
|||
0x76, 0x49, 0x6e, 0x92, 0xec, 0xb0, 0xf4, 0xec, 0x1c, 0x93, 0x1c, 0x97, 0x9c, 0x90, 0x3c, 0x22, |
|||
0x59, 0x58, 0x7c, 0x46, 0xf2, 0x77, 0xc9, 0x7f, 0x25, 0x8b, 0x82, 0xd8, 0x41, 0x7f, 0x3b, 0x91, |
|||
0xec, 0x8f, 0x10, 0x21, 0x66, 0xfe, 0x67, 0x06, 0xe6, 0x16, 0x21, 0x62, 0xcc, 0x6e, 0x21, 0xe2, |
|||
0xcc, 0x6d, 0x42, 0x24, 0x98, 0xfd, 0x42, 0x8c, 0x30, 0x0b, 0x2a, 0xf6, 0xcf, 0x7c, 0x87, 0x72, |
|||
0x61, 0x4e, 0xe4, 0x67, 0x3f, 0xf3, 0x08, 0x7d, 0x28, 0x4f, 0x16, 0x1c, 0x26, 0x1b, 0x4d, 0xb4, |
|||
0x90, 0x56, 0x85, 0x5c, 0xb4, 0x11, 0x6d, 0x1a, 0x6d, 0x21, 0x55, 0x52, 0x8e, 0x31, 0xf8, 0x31, |
|||
0x2f, 0xe3, 0x75, 0xff, 0x00, 0xd3, 0x39, 0x74, 0x2c, 0x6e, 0x57, 0x00, 0x00}; |
|||
#endif /*__FAVICON_ICO_GZ_H__*/ |
@ -1,205 +0,0 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Index</title> |
|||
<link rel="stylesheet" type="text/css" href="style.css"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<script type="text/javascript" src="api.js"></script> |
|||
</head> |
|||
<body> |
|||
<div class="topnav"> |
|||
<a href="/" class="title">AhoyDTU</a> |
|||
<a href="javascript:void(0);" class="icon" onclick="topnav()"> |
|||
<span></span> |
|||
<span></span> |
|||
<span></span> |
|||
</a> |
|||
<div id="topnav" class="hide"></div> |
|||
</div> |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<script> |
|||
function promptFunction() { |
|||
var Text = prompt("This project was started from https://www.mikrocontroller.net/topic/525778 this discussion.\n\n" + |
|||
"The Hoymiles protocol was decrypted through the voluntary efforts of many participants. ahoy, among others, was developed based on this work.\n" + |
|||
"The software was developed to the best of our knowledge and belief. Nevertheless, no liability can be accepted for a malfunction or guarantee loss of the inverter.\n\n" + |
|||
"Ahoy is freely available. If you paid money for the software, you probably got ripped off.\n\nPlease type in 'YeS', you are accept our Disclaim. You should then save your config.", ""); |
|||
if (Text != "YeS") |
|||
promptFunction(); |
|||
else |
|||
return true; |
|||
|
|||
} |
|||
</script> |
|||
<p><span class="des">Uptime: </span><span id="uptime"></span></p> |
|||
<p><span class="des">ESP-Time: </span><span id="date"></span></p> |
|||
<div id="sun"> |
|||
<span class="des">Sunrise: </span><span id="sunrise"></span><br> |
|||
<span class="des">Sunset: </span><span id="sunset"></span> |
|||
</div> |
|||
<p><span class="des">WiFi RSSI: </span><span id="wifi_rssi"></span> dBm</p> |
|||
<p> |
|||
<span class="des">Statistics: </span> |
|||
<pre id="stat"></pre> |
|||
<pre id="iv"></pre> |
|||
<pre id="warn_info"></pre> |
|||
</p> |
|||
<p>Every <span id="refresh"></span> seconds the values are updated</p> |
|||
|
|||
<div id="note"> |
|||
Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a><br/> |
|||
<h3>Documentation</h3> |
|||
<a href="https://ahoydtu.de" target="_blank">ahoydtu.de</a> |
|||
|
|||
<h3>Support this project:</h3> |
|||
<ul> |
|||
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li> |
|||
<li>Contribute to <a href="https://github.com/lumapu/ahoy/blob/main/tools/esp8266/User_Manual.md" target="_blank">documentation</a></li> |
|||
<li>Test <a href="https://github.com/lumapu/ahoy/actions/workflows/compile_development.yml" target="_blank">development firmware</a></li> |
|||
<li>make a <a href="https://paypal.me/lupusch" target="_blank">donation</a></li> |
|||
</ul> |
|||
<p class="lic"> |
|||
This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div id="footer"> |
|||
<div class="left"> |
|||
<a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2022</a> |
|||
<ul> |
|||
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> |
|||
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="right"> |
|||
<span id="version"></span><br/><br/> |
|||
<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a> |
|||
</div> |
|||
</div> |
|||
<script type="text/javascript"> |
|||
var exeOnce = true; |
|||
|
|||
function apiCb(obj) { |
|||
var e = document.getElementById("apiResult"); |
|||
if(obj["success"]) { |
|||
e.innerHTML = " command excuted"; |
|||
getAjax("/api/index", parse); |
|||
} |
|||
else |
|||
e.innerHTML = " Error: " + obj["error"]; |
|||
} |
|||
|
|||
function setTime() { |
|||
var date = new Date(); |
|||
var obj = new Object(); |
|||
obj.cmd = "set_time"; |
|||
obj.ts = parseInt(date.getTime() / 1000); |
|||
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function parseSys(obj) { |
|||
// Disclaimer |
|||
//if(obj["disclaimer"] == false) sessionStorage.setItem("gDisclaimer", promptFunction()); |
|||
if(true == exeOnce) |
|||
parseVersion(obj); |
|||
document.getElementById("wifi_rssi").innerHTML = obj["wifi_rssi"]; |
|||
|
|||
var date = new Date(obj["ts_now"] * 1000); |
|||
var up = obj["ts_uptime"]; |
|||
var days = parseInt(up / 86400) % 365; |
|||
var hrs = parseInt(up / 3600) % 24; |
|||
var min = parseInt(up / 60) % 60; |
|||
var sec = up % 60; |
|||
var sunrise = new Date(obj["ts_sunrise"] * 1000); |
|||
var sunset = new Date(obj["ts_sunset"] * 1000); |
|||
document.getElementById("uptime").innerHTML = days + " Days, " |
|||
+ ("0"+hrs).substr(-2) + ":" |
|||
+ ("0"+min).substr(-2) + ":" |
|||
+ ("0"+sec).substr(-2); |
|||
var dSpan = document.getElementById("date"); |
|||
if(0 != obj["ts_now"]) |
|||
dSpan.innerHTML = date.toLocaleString('de-DE'); |
|||
else { |
|||
dSpan.innerHTML = ""; |
|||
var e = inp("set", "sync from browser", 0, ["btn"], "set", "button"); |
|||
dSpan.appendChild(span("NTP timeserver unreachable. ")); |
|||
dSpan.appendChild(e); |
|||
dSpan.appendChild(span("", ["span"], "apiResult")); |
|||
e.addEventListener("click", setTime); |
|||
} |
|||
|
|||
if(!obj["ts_sun_upd"]) { |
|||
var e = document.getElementById("sun"); |
|||
if(null != e) |
|||
e.parentNode.removeChild(e); |
|||
} |
|||
else { |
|||
document.getElementById("sunrise").innerHTML = sunrise.toLocaleString('de-DE'); |
|||
document.getElementById("sunset").innerHTML = sunset.toLocaleString('de-DE'); |
|||
} |
|||
} |
|||
|
|||
function parseStat(obj) { |
|||
document.getElementById("stat").innerHTML = "RX success: " + obj["rx_success"] |
|||
+ "\nRX fail: " + obj["rx_fail"] |
|||
+ "\nRX no answer: " + obj["rx_fail_answer"] |
|||
+ "\nFrames received: " + obj["frame_cnt"] |
|||
+ "\nTX cnt: " + obj["tx_cnt"]; |
|||
} |
|||
|
|||
function parseIv(obj) { |
|||
var html = ""; |
|||
for(var i of obj) { |
|||
html += "Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is "; |
|||
if(false == i["is_avail"]) |
|||
html += "not "; |
|||
html += "available and is "; |
|||
if(false == i["is_producing"]) |
|||
html += "not "; |
|||
html += "producing\n"; |
|||
|
|||
if(false == i["is_avail"]) { |
|||
if(i["ts_last_success"] > 0) { |
|||
var date = new Date(i["ts_last_success"] * 1000); |
|||
html += "-> last successful transmission: " + date.toLocaleString('de-DE') + "\n"; |
|||
} |
|||
} |
|||
|
|||
} |
|||
document.getElementById("iv").innerHTML = html; |
|||
} |
|||
|
|||
function parseWarnInfo(warn, info) { |
|||
var html = ""; |
|||
for(var w of warn) { |
|||
html += "WARN: " + w + "\n"; |
|||
} |
|||
for(var i of info) { |
|||
html += "INFO: " + i + "\n"; |
|||
} |
|||
document.getElementById("warn_info").innerHTML = html; |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
if(true == exeOnce) |
|||
parseMenu(obj["menu"]); |
|||
parseSys(obj["system"]); |
|||
parseStat(obj["statistics"]); |
|||
parseIv(obj["inverter"]); |
|||
parseWarnInfo(obj["warnings"], obj["infos"]); |
|||
document.getElementById("refresh").innerHTML = obj["refresh_interval"]; |
|||
if(true == exeOnce) { |
|||
window.setInterval("getAjax('/api/index', parse)", obj["refresh_interval"] * 1000); |
|||
exeOnce = false; |
|||
} |
|||
} |
|||
else |
|||
document.getElementById("refresh").innerHTML = "n/a"; |
|||
} |
|||
|
|||
getAjax("/api/index", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -1,451 +0,0 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Setup</title> |
|||
<link rel="stylesheet" type="text/css" href="style.css"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<script type="text/javascript" src="api.js"></script> |
|||
<script type="text/javascript"> |
|||
function load() { |
|||
for(it of document.getElementsByClassName("s_collapsible")) { |
|||
it.addEventListener("click", function() { |
|||
this.classList.toggle("active"); |
|||
var content = this.nextElementSibling; |
|||
content.style.display = (content.style.display === "block") ? "none" : "block"; |
|||
}); |
|||
} |
|||
} |
|||
</script> |
|||
</head> |
|||
<body onload="load()"> |
|||
<div class="topnav"> |
|||
<a href="/" class="title">AhoyDTU</a> |
|||
<a href="javascript:void(0);" class="icon" onclick="topnav()"> |
|||
<span></span> |
|||
<span></span> |
|||
<span></span> |
|||
</a> |
|||
<div id="topnav" class="hide"></div> |
|||
</div> |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a> |
|||
|
|||
<form method="post" action="/save"> |
|||
<fieldset> |
|||
<legend class="des">Device Host Name</legend> |
|||
<label for="device">Device Name</label> |
|||
<input type="text" name="device" class="text"/> |
|||
<input type="hidden" name="disclaimer" value="false" id="disclaimer"> |
|||
</fieldset> |
|||
|
|||
<button type="button" class="s_collapsible">WiFi</button> |
|||
<div class="s_content"> |
|||
<fieldset> |
|||
<legend class="des">WiFi</legend> |
|||
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p> |
|||
<label for="scanbtn">Search Networks</label> |
|||
<input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/><br/> |
|||
<label for="networks">Avail Networks</label> |
|||
<select name="networks" id="networks" onChange="selNet()"> |
|||
<option value="-1">not scanned</option> |
|||
</select> |
|||
<label for="ssid">SSID</label> |
|||
<input type="text" name="ssid" class="text"/> |
|||
<label for="pwd">Password</label> |
|||
<input type="password" class="text" name="pwd" value="{PWD}"/> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">Inverter</button> |
|||
<div class="s_content"> |
|||
<fieldset> |
|||
<legend class="des">Inverter</legend> |
|||
<div id="inverter"></div><br/> |
|||
<input type="button" id="btnAdd" class="btn" value="Add Inverter"/> |
|||
<p class="subdes">General</p> |
|||
<label for="invInterval">Interval [s]</label> |
|||
<input type="text" class="text" name="invInterval"/> |
|||
<label for="invRetry">Max retries per Payload</label> |
|||
<input type="text" class="text" name="invRetry"/> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">NTP Server</button> |
|||
<div class="s_content"> |
|||
<fieldset> |
|||
<legend class="des">NTP Server</legend> |
|||
<label for="ntpAddr">NTP Server / IP</label> |
|||
<input type="text" class="text" name="ntpAddr"/> |
|||
<label for="ntpPort">NTP Port</label> |
|||
<input type="text" class="text" name="ntpPort"/> |
|||
<label for="ntpBtn">set system time</label> |
|||
<input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/> |
|||
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/> |
|||
<span id="apiResultNtp"></span> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">Sunrise & Sunset</button> |
|||
<div class="s_content"> |
|||
<fieldset> |
|||
<legend class="des">Sunrise & Sunset</legend> |
|||
<label for="sunLat">Latitude (decimal)</label> |
|||
<input type="text" class="text" name="sunLat"/> |
|||
<label for="sunLon">Longitude (decimal)</label> |
|||
<input type="text" class="text" name="sunLon"/> |
|||
<br> |
|||
<label for="sunDisNightCom">disable night communication</label> |
|||
<input type="checkbox" class="cb" name="sunDisNightCom"/><br/> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">MQTT</button> |
|||
<div class="s_content"> |
|||
<fieldset> |
|||
<legend class="des">MQTT</legend> |
|||
<label for="mqttAddr">Broker / Server IP</label> |
|||
<input type="text" class="text" name="mqttAddr" maxlength="32" /> |
|||
<label for="mqttPort">Port</label> |
|||
<input type="text" class="text" name="mqttPort"/> |
|||
<label for="mqttUser">Username (optional)</label> |
|||
<input type="text" class="text" name="mqttUser"/> |
|||
<label for="mqttPwd">Password (optional)</label> |
|||
<input type="password" class="text" name="mqttPwd"/> |
|||
<label for="mqttTopic">Topic</label> |
|||
<input type="text" class="text" name="mqttTopic"/> |
|||
<label for="mqttBtn">Discovery Config (homeassistant)</label> |
|||
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/> |
|||
<span id="apiResultMqtt"></span> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<button type="button" class="s_collapsible">System Config</button> |
|||
<div class="s_content"> |
|||
<fieldset> |
|||
<legend class="des">System Config</legend> |
|||
<p class="des">Pinout (Wemos)</p> |
|||
<div id="pinout"></div> |
|||
|
|||
<p class="des">Radio (NRF24L01+)</p> |
|||
<div id="rf24"></div> |
|||
|
|||
<p class="des">Serial Console</p> |
|||
<label for="serEn">print inverter data</label> |
|||
<input type="checkbox" class="cb" name="serEn"/><br/> |
|||
<label for="serDbg">Serial Debug</label> |
|||
<input type="checkbox" class="cb" name="serDbg"/><br/> |
|||
<label for="serIntvl">Interval [s]</label> |
|||
<input type="text" class="text" name="serIntvl"/> |
|||
</fieldset> |
|||
</div> |
|||
|
|||
<label for="reboot">Reboot device after successful save</label> |
|||
<input type="checkbox" class="cb" name="reboot"/> |
|||
<input type="submit" value="save" class="btn right"/><br/> |
|||
<br/> |
|||
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a> (only saved values) |
|||
</form> |
|||
</div> |
|||
</div> |
|||
<div id="footer"> |
|||
<div class="left"> |
|||
<a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2022</a> |
|||
<ul> |
|||
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> |
|||
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="right"> |
|||
<span id="version"></span><br/><br/> |
|||
<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a> |
|||
</div> |
|||
</div> |
|||
<script type="text/javascript"> |
|||
var highestId = 0; |
|||
var maxInv = 0; |
|||
|
|||
const re = /11[2,4,6]1.*/; |
|||
|
|||
document.getElementById("btnAdd").addEventListener("click", function() { |
|||
if(highestId <= (maxInv-1)) |
|||
ivHtml(JSON.parse('{"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId + 1); |
|||
}); |
|||
|
|||
function apiCbWifi(obj) { |
|||
var e = document.getElementById("networks"); |
|||
selDelAllOpt(e); |
|||
if(obj["success"]) |
|||
e.appendChild(opt("-1", "scanning ...")) |
|||
else |
|||
e.appendChild(opt("-1", "Error: " + obj["error"])); |
|||
} |
|||
|
|||
function apiCbNtp(obj) { |
|||
var e = document.getElementById("apiResultNtp"); |
|||
if(obj["success"]) |
|||
e.innerHTML = "command excuted"; |
|||
else |
|||
e.innerHTML = "Error: " + obj["error"]; |
|||
} |
|||
|
|||
function apiCbMqtt(obj) { |
|||
var e = document.getElementById("apiResultMqtt"); |
|||
if(obj["success"]) |
|||
e.innerHTML = "command excuted"; |
|||
else |
|||
e.innerHTML = "Error: " + obj["error"]; |
|||
} |
|||
|
|||
function setTime() { |
|||
var date = new Date(); |
|||
var obj = new Object(); |
|||
obj.cmd = "set_time"; |
|||
obj.ts = parseInt(date.getTime() / 1000); |
|||
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function scan() { |
|||
var obj = new Object(); |
|||
obj.cmd = "scan_wifi"; |
|||
getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj)); |
|||
setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 7000); |
|||
} |
|||
|
|||
function syncTime() { |
|||
var obj = new Object(); |
|||
obj.cmd = "sync_ntp"; |
|||
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function sendDiscoveryConfig() { |
|||
var obj = new Object(); |
|||
obj.cmd = "discovery_cfg"; |
|||
getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj)); |
|||
} |
|||
|
|||
function delIv() { |
|||
var id = this.id.substring(0,4); |
|||
var e = document.getElementsByName(id + "Addr")[0]; |
|||
e.value = ""; |
|||
e.dispatchEvent(new Event("keyup")); |
|||
e.dispatchEvent(new Event("change")); |
|||
document.getElementsByName(id + "Name")[0].value = ""; |
|||
} |
|||
|
|||
function ivHtml(obj, id) { |
|||
highestId = id; |
|||
if(highestId == (maxInv - 1)) |
|||
setHide("btnAdd", true); |
|||
iv = document.getElementById("inverter"); |
|||
iv.appendChild(des("Inverter " + id)); |
|||
id = "inv" + id; |
|||
|
|||
|
|||
iv.appendChild(lbl(id + "Addr", "Serial Number (12 digits)*")); |
|||
var addr = inp(id + "Addr", obj["serial"], 12) |
|||
iv.appendChild(addr); |
|||
['keyup', 'change'].forEach(function(evt) { |
|||
|
|||
addr.addEventListener(evt, (e) => { |
|||
var serial = addr.value.substring(0,4); |
|||
var max = 0; |
|||
for(var i=0;i<4;i++) { |
|||
setHide(id+"ModPwr"+i, true); |
|||
setHide(id+"ModName"+i, true); |
|||
} |
|||
setHide("lbl"+id+"ModPwr", true); |
|||
setHide("lbl"+id+"ModName", true); |
|||
|
|||
if(serial === "1161") max = 4; |
|||
else if(serial === "1141") max = 2; |
|||
else if(serial === "1121") max = 1; |
|||
else max = 0; |
|||
|
|||
if(max != 0) { |
|||
for(var i=0;i<max;i++) { |
|||
setHide(id+"ModPwr"+i, false); |
|||
setHide(id+"ModName"+i, false); |
|||
} |
|||
setHide("lbl"+id+"ModPwr", false); |
|||
setHide("lbl"+id+"ModName", false); |
|||
} |
|||
}) |
|||
}); |
|||
|
|||
for(var i of [["Name", "name", "Name*", 32]]) { |
|||
iv.appendChild(lbl(id + i[0], i[2])); |
|||
iv.appendChild(inp(id + i[0], obj[i[1]], i[3])); |
|||
} |
|||
|
|||
for(var j of [["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4], ["ModName", "ch_name", "Module Name", 16]]) { |
|||
var cl = (re.test(obj["serial"])) ? null : ["hide"]; |
|||
iv.appendChild(lbl(null, j[2], cl, "lbl" + id + j[0])); |
|||
d = div([j[0]]); |
|||
i = 0; |
|||
cl = (re.test(obj["serial"])) ? ["text", "sh"] : ["text", "sh", "hide"]; |
|||
for(it of obj[j[1]]) { |
|||
d.appendChild(inp(id + j[0] + i, it, j[3], cl, id + j[0] + i)); |
|||
i++; |
|||
} |
|||
iv.appendChild(d); |
|||
} |
|||
iv.appendChild(br()); |
|||
iv.appendChild(lbl(id + "lbldel", "Delete")); |
|||
var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button"); |
|||
iv.appendChild(del); |
|||
del.addEventListener("click", delIv); |
|||
} |
|||
|
|||
function ivGlob(obj) { |
|||
for(var i of [["invInterval", "interval"], ["invRetry", "retries"]]) |
|||
document.getElementsByName(i[0])[0].value = obj[i[1]]; |
|||
} |
|||
|
|||
function parseSys(obj) { |
|||
for(var i of [["device", "device_name"], ["ssid", "ssid"]]) |
|||
document.getElementsByName(i[0])[0].value = obj[i[1]]; |
|||
parseVersion(obj); |
|||
} |
|||
|
|||
function parseIv(obj) { |
|||
for(var i = 0; i < obj.inverter.length; i++) |
|||
ivHtml(obj.inverter[i], i); |
|||
ivGlob(obj); |
|||
maxInv = obj["max_num_inverters"]; |
|||
} |
|||
|
|||
function parseMqtt(obj) { |
|||
for(var i of [["Addr", "broker"], ["Port", "port"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"]]) |
|||
document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]]; |
|||
} |
|||
|
|||
function parseNtp(obj) { |
|||
for(var i of [["ntpAddr", "addr"], ["ntpPort", "port"]]) |
|||
document.getElementsByName(i[0])[0].value = obj[i[1]]; |
|||
} |
|||
|
|||
function parseSun(obj) { |
|||
document.getElementsByName("sunLat")[0].value = obj["lat"]; |
|||
document.getElementsByName("sunLon")[0].value = obj["lon"]; |
|||
document.getElementsByName("sunDisNightCom")[0].checked = obj["disnightcom"]; |
|||
} |
|||
|
|||
function parsePinout(obj, type) { |
|||
var e = document.getElementById("pinout"); |
|||
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']]; |
|||
for(p of pins) { |
|||
e.appendChild(lbl(p[1], p[0].toUpperCase())); |
|||
if("ESP8266" == type) { |
|||
e.appendChild(sel(p[1], [ |
|||
[0, "D3 (GPIO0)"], |
|||
[1, "TX (GPIO1)"], |
|||
[2, "D4 (GPIO2)"], |
|||
[3, "RX (GPIO3)"], |
|||
[4, "D2 (GPIO4)"], |
|||
[5, "D1 (GPIO5)"], |
|||
[6, "GPIO6"], |
|||
[7, "GPIO7"], |
|||
[8, "GPIO8"], |
|||
[9, "GPIO9"], |
|||
[10, "GPIO10"], |
|||
[11, "GPIO11"], |
|||
[12, "D6 (GPIO12)"], |
|||
[13, "D7 (GPIO13)"], |
|||
[14, "D5 (GPIO14)"], |
|||
[15, "D8 (GPIO15)"], |
|||
[16, "D0 (GPIO16 - no IRQ!)"] |
|||
], obj[p[0]])); |
|||
} |
|||
else { |
|||
e.appendChild(sel(p[1], [ |
|||
[0, "GPIO0"], |
|||
[1, "TX (GPIO1)"], |
|||
[2, "GPIO2 (LED)"], |
|||
[3, "RX (GPIO3)"], |
|||
[4, "GPIO4"], |
|||
[5, "GPIO5"], |
|||
[12, "GPIO12"], |
|||
[13, "GPIO13"], |
|||
[14, "GPIO14"], |
|||
[15, "GPIO15"], |
|||
[16, "GPIO16"], |
|||
[17, "GPIO17"], |
|||
[18, "GPIO18"], |
|||
[19, "GPIO19"], |
|||
[21, "GPIO21"], |
|||
[22, "GPIO22"], |
|||
[23, "GPIO23"], |
|||
[25, "GPIO25"], |
|||
[26, "GPIO26"], |
|||
[27, "GPIO27"], |
|||
[32, "GPIO32"], |
|||
[33, "GPIO33"], |
|||
[34, "GPIO34"], |
|||
[35, "GPIO35"], |
|||
[36, "VP (GPIO36)"], |
|||
[39, "VN (GPIO39)"] |
|||
], obj[p[0]])); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function parseRadio(obj) { |
|||
var e = document.getElementById("rf24"); |
|||
e.appendChild(lbl("rf24Power", "Amplifier Power Level")); |
|||
e.appendChild(sel("rf24Power", [ |
|||
[0, "MIN"], |
|||
[1, "LOW"], |
|||
[2, "HIGH"], |
|||
[3, "MAX"] |
|||
], obj["power_level"])); |
|||
} |
|||
|
|||
function parseSerial(obj) { |
|||
for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"]]) |
|||
document.getElementsByName(i[0])[0].checked = obj[i[1]]; |
|||
document.getElementsByName("serIntvl")[0].value = obj["interval"]; |
|||
} |
|||
|
|||
function parse(root) { |
|||
if(null != root) { |
|||
parseMenu(root["menu"]); |
|||
parseSys(root["system"]); |
|||
parseIv(root["inverter"]); |
|||
parseMqtt(root["mqtt"]); |
|||
parseNtp(root["ntp"]); |
|||
parseSun(root["sun"]); |
|||
parsePinout(root["pinout"], root["system"]["esp_type"]); |
|||
parseRadio(root["radio"]); |
|||
parseSerial(root["serial"]); |
|||
} |
|||
} |
|||
|
|||
function listNetworks(root) { |
|||
var s = document.getElementById("networks"); |
|||
selDelAllOpt(s); |
|||
if(root["networks"].length > 0) { |
|||
s.appendChild(opt("-1", "please select network")); |
|||
for(i = 0; i < root["networks"].length; i++) { |
|||
s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)")); |
|||
} |
|||
} |
|||
else |
|||
s.appendChild(opt("-1", "no network found")); |
|||
} |
|||
|
|||
function selNet() { |
|||
var s = document.getElementById("networks"); |
|||
var e = document.getElementsByName("ssid")[0]; |
|||
if(-1 != s.value) |
|||
e.value = s.value; |
|||
} |
|||
|
|||
hiddenInput = document.getElementById("disclaimer") |
|||
hiddenInput.value = sessionStorage.getItem("gDisclaimer"); |
|||
|
|||
getAjax("/api/setup", parse); |
|||
|
|||
</script> |
|||
</body> |
|||
</html> |
@ -1,416 +0,0 @@ |
|||
html, body { |
|||
font-family: Arial; |
|||
margin: 0; |
|||
padding: 0; |
|||
height: 100%; |
|||
min-height: 100%; |
|||
} |
|||
|
|||
h2 { |
|||
padding-left: 10px; |
|||
} |
|||
|
|||
.topnav { |
|||
background-color: #333; |
|||
position: fixed; |
|||
top: 0; |
|||
width: 100%; |
|||
} |
|||
|
|||
.topnav a { |
|||
color: #fff; |
|||
padding: 14px 14px; |
|||
text-decoration: none; |
|||
font-size: 17px; |
|||
display: block; |
|||
height: 20px; |
|||
} |
|||
|
|||
#topnav a { |
|||
color: #fff; |
|||
} |
|||
|
|||
.topnav a.icon { |
|||
background: #333; |
|||
display: block; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
} |
|||
|
|||
.topnav a:hover { |
|||
background-color: #044e86 !important; |
|||
color: #000; |
|||
} |
|||
|
|||
.title { |
|||
background-color: #006ec0; |
|||
color: #fff !important; |
|||
padding-left: 80px !important |
|||
} |
|||
|
|||
.topnav .icon span { |
|||
display: block; |
|||
width: 30px; |
|||
height: 3px; |
|||
margin-bottom: 5px; |
|||
position: relative; |
|||
background: #fff; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
.topnav .active { |
|||
background-color: #555; |
|||
} |
|||
|
|||
span.seperator { |
|||
width: 100%; |
|||
height: 1px; |
|||
margin: 5px 0px 5px; |
|||
background-color: #494949; |
|||
display: block; |
|||
} |
|||
|
|||
#wrapper { |
|||
min-height: 100%; |
|||
} |
|||
|
|||
#content { |
|||
padding: 50px 20px 120px 20px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
#footer { |
|||
height: 121px; |
|||
margin-top: -121px; |
|||
background-color: #555; |
|||
width: 100%; |
|||
font-size: 13px; |
|||
} |
|||
|
|||
#footer .right { |
|||
color: #bbb; |
|||
margin: 23px 25px; |
|||
text-align: right; |
|||
} |
|||
|
|||
#footer .left { |
|||
color: #bbb; |
|||
margin: 23px 0px 0px 25px; |
|||
} |
|||
|
|||
#footer ul { |
|||
list-style-type: none; |
|||
margin: 20px auto; |
|||
padding: 0; |
|||
} |
|||
|
|||
#footer ul li, #footer a { |
|||
color: #bbb; |
|||
margin-bottom: 10px; |
|||
padding-left: 5px; |
|||
font-size: 13px; |
|||
} |
|||
|
|||
#footer a:hover { |
|||
color: #fff; |
|||
} |
|||
|
|||
.hide { |
|||
display: none; |
|||
} |
|||
|
|||
@media only screen and (min-width: 992px) { |
|||
.topnav { |
|||
width: 230px !important; |
|||
height: 100%; |
|||
} |
|||
|
|||
.topnav a.icon { |
|||
display: none !important; |
|||
} |
|||
|
|||
.topnav a { |
|||
padding: 14px 24px; |
|||
} |
|||
|
|||
.topnav .title { |
|||
padding-left: 24px !important; |
|||
} |
|||
|
|||
.topnav .hide { |
|||
display: block; |
|||
} |
|||
|
|||
#content { |
|||
padding: 15px 15px 120px 250px; |
|||
} |
|||
|
|||
#footer .left { |
|||
margin-left: 250px !important; |
|||
} |
|||
} |
|||
|
|||
/** old CSS below **/ |
|||
|
|||
p { |
|||
text-align: justify; |
|||
font-size: 13pt; |
|||
} |
|||
|
|||
p.lic, p.lic a { |
|||
font-size: 8pt; |
|||
color: #999; |
|||
} |
|||
|
|||
.des { |
|||
margin-top: 20px; |
|||
font-size: 13pt; |
|||
color: #006ec0; |
|||
} |
|||
|
|||
.s_active, .s_collapsible:hover { |
|||
background-color: #044e86; |
|||
color: #fff; |
|||
} |
|||
|
|||
.s_content { |
|||
display: none; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.s_collapsible { |
|||
background-color: #006ec0; |
|||
color: white; |
|||
cursor: pointer; |
|||
padding: 18px; |
|||
width: 100%; |
|||
border: none; |
|||
text-align: left; |
|||
outline: none; |
|||
font-size: 15px; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.subdes { |
|||
font-size: 12pt; |
|||
color: #006ec0; |
|||
margin-left: 7px; |
|||
} |
|||
|
|||
.subsubdes { |
|||
font-size:12pt; |
|||
color:#006ec0; |
|||
margin: 0 0 7px 12px; |
|||
} |
|||
|
|||
a:link, a:visited { |
|||
text-decoration: none; |
|||
font-size: 13pt; |
|||
color: #006ec0; |
|||
} |
|||
|
|||
a:hover, a:focus { |
|||
color: #f00; |
|||
} |
|||
|
|||
a.btn { |
|||
background-color: #006ec0; |
|||
color: #fff; |
|||
padding: 7px 15px 7px 15px; |
|||
display: inline-block; |
|||
} |
|||
|
|||
a.btn:hover { |
|||
background-color: #044e86 !important; |
|||
} |
|||
|
|||
input, select { |
|||
padding: 7px; |
|||
font-size: 13pt; |
|||
} |
|||
|
|||
input.text, select { |
|||
width: 70%; |
|||
box-sizing: border-box; |
|||
margin-bottom: 10px; |
|||
border: 1px solid #ccc; |
|||
} |
|||
|
|||
input.sh { |
|||
max-width: 150px !important; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
input.btnDel { |
|||
background-color: #c00 !important; |
|||
} |
|||
|
|||
input.btn { |
|||
background-color: #006ec0; |
|||
color: #fff; |
|||
border: 0px; |
|||
padding: 7px 20px 7px 20px; |
|||
margin-bottom: 10px; |
|||
text-transform: uppercase; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
input.btn:hover { |
|||
background-color: #044e86; |
|||
} |
|||
|
|||
input.cb { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
label { |
|||
width: 20%; |
|||
display: inline-block; |
|||
font-size: 12pt; |
|||
padding-right: 10px; |
|||
margin: 10px 0px 0px 15px; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
pre { |
|||
white-space: pre-wrap; |
|||
} |
|||
|
|||
fieldset { |
|||
margin-bottom: 15px; |
|||
} |
|||
|
|||
.left { |
|||
float: left; |
|||
} |
|||
|
|||
.right { |
|||
float: right; |
|||
} |
|||
|
|||
div.ch-iv { |
|||
width: 100%; |
|||
background-color: #32b004; |
|||
display: inline-block; |
|||
margin-bottom: 15px; |
|||
padding-bottom: 20px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
div.ch { |
|||
width: 220px; |
|||
min-height: 350px; |
|||
background-color: #006ec0; |
|||
display: inline-block; |
|||
margin: 0 20px 10px 0px; |
|||
overflow: auto; |
|||
padding-bottom: 20px; |
|||
} |
|||
|
|||
div.ch-all { |
|||
width: 100%; |
|||
background-color: #b06e04; |
|||
display: inline-block; |
|||
margin-bottom: 15px; |
|||
padding-bottom: 20px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head, div.ch-all .value, div.ch-all .info, div.ch-all .head { |
|||
color: #fff; |
|||
display: block; |
|||
width: 100%; |
|||
text-align: center; |
|||
} |
|||
|
|||
.subgrp { |
|||
float: left; |
|||
width: 220px; |
|||
} |
|||
|
|||
div.ch .unit, div.ch-iv .unit, div.ch-all .unit { |
|||
font-size: 19px; |
|||
margin-left: 10px; |
|||
} |
|||
|
|||
div.ch .value, div.ch-iv .value, div.ch-all .value { |
|||
margin-top: 20px; |
|||
font-size: 24px; |
|||
} |
|||
|
|||
div.ch .info, div.ch-iv .info, div.ch-all .info { |
|||
margin-top: 3px; |
|||
font-size: 10px; |
|||
} |
|||
|
|||
div.ch .head { |
|||
background-color: #003c80; |
|||
padding: 10px 0 10px 0; |
|||
} |
|||
|
|||
div.ch-all .head { |
|||
background-color: #8e5903; |
|||
padding: 10px 0 10px 0; |
|||
} |
|||
|
|||
div.ch-iv .head { |
|||
background-color: #1c6800; |
|||
padding: 10px 0 10px 0; |
|||
} |
|||
|
|||
div.iv { |
|||
max-width: 960px; |
|||
margin-bottom: 40px; |
|||
} |
|||
|
|||
div.ts { |
|||
font-size: 13px; |
|||
background-color: #ddd; |
|||
border-top: 7px solid #999; |
|||
padding: 7px; |
|||
} |
|||
|
|||
div.ModPwr, div.ModName { |
|||
width:70%; |
|||
display: inline-block; |
|||
} |
|||
|
|||
#note { |
|||
margin: 50px 10px 10px 10px; |
|||
padding-top: 10px; |
|||
width: 100%; |
|||
border-top: 1px solid #bbb; |
|||
} |
|||
|
|||
@media(max-width: 500px) { |
|||
div.ch .unit, div.ch-iv .unit { |
|||
font-size: 18px; |
|||
} |
|||
|
|||
div.ch { |
|||
width: 170px; |
|||
min-height: 100px |
|||
} |
|||
|
|||
.subgrp { |
|||
width: 180px; |
|||
} |
|||
} |
|||
|
|||
#serial { |
|||
width: 100%; |
|||
} |
|||
|
|||
#content .serial { |
|||
max-width: 1000px; |
|||
} |
|||
|
|||
.dot { |
|||
height: 15px; |
|||
width: 15px; |
|||
background-color: #f00; |
|||
border-radius: 50%; |
|||
display: inline-block; |
|||
margin-top: 15px; |
|||
} |
@ -1,54 +0,0 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>System</title> |
|||
<link rel="stylesheet" type="text/css" href="style.css"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<script type="text/javascript" src="api.js"></script> |
|||
</head> |
|||
<body> |
|||
<div class="topnav"> |
|||
<a href="/" class="title">AhoyDTU</a> |
|||
<a href="javascript:void(0);" class="icon" onclick="topnav()"> |
|||
<span></span> |
|||
<span></span> |
|||
<span></span> |
|||
</a> |
|||
<div id="topnav" class="hide"></div> |
|||
</div> |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<a href="/factory" class="btn">Factory Reset</a><br/> |
|||
<br/> |
|||
<a href="/reboot" class="btn">Reboot</a> |
|||
</div> |
|||
</div> |
|||
<div id="footer"> |
|||
<div class="left"> |
|||
<a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2022</a> |
|||
<ul> |
|||
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> |
|||
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="right"> |
|||
<span id="version"></span><br/><br/> |
|||
<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a> |
|||
</div> |
|||
</div> |
|||
<script type="text/javascript"> |
|||
function parseSys(obj) { |
|||
parseVersion(obj); |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
parseMenu(obj["menu"]); |
|||
parseSys(obj["system"]); |
|||
} |
|||
} |
|||
|
|||
getAjax("/api/index", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -1,60 +0,0 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Update</title> |
|||
<link rel="stylesheet" type="text/css" href="style.css"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<script type="text/javascript" src="api.js"></script> |
|||
</head> |
|||
<body> |
|||
<div class="topnav"> |
|||
<a href="/" class="title">AhoyDTU</a> |
|||
<a href="javascript:void(0);" class="icon" onclick="topnav()"> |
|||
<span></span> |
|||
<span></span> |
|||
<span></span> |
|||
</a> |
|||
<div id="topnav" class="hide"></div> |
|||
</div> |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<div> |
|||
Make sure that you have noted all your settings before starting an update. New versions may have changed their memory layout which can break your existing settings.<br/> |
|||
<br/> |
|||
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a> |
|||
</div> |
|||
<br/><br/> |
|||
<form method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> |
|||
<input type="file" name="update"><input type="submit" value="Update"> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
<div id="footer"> |
|||
<div class="left"> |
|||
<a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2022</a> |
|||
<ul> |
|||
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> |
|||
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="right"> |
|||
<span id="version"></span><br/><br/> |
|||
<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a> |
|||
</div> |
|||
</div> |
|||
<script type="text/javascript"> |
|||
function parseSys(obj) { |
|||
parseVersion(obj); |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
parseMenu(obj["menu"]); |
|||
parseSys(obj["system"]); |
|||
} |
|||
} |
|||
|
|||
getAjax("/api/index", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -1,148 +0,0 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<title>Live</title> |
|||
<link rel="stylesheet" type="text/css" href="style.css"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<meta name="apple-mobile-web-app-capable" content="yes"> |
|||
<script type="text/javascript" src="api.js"></script> |
|||
</head> |
|||
<body> |
|||
<div class="topnav"> |
|||
<a href="/" class="title">AhoyDTU</a> |
|||
<a href="javascript:void(0);" class="icon" onclick="topnav()"> |
|||
<span></span> |
|||
<span></span> |
|||
<span></span> |
|||
</a> |
|||
<div id="topnav" class="hide"></div> |
|||
</div> |
|||
<div id="wrapper"> |
|||
<div id="content"> |
|||
<div id="live"></div> |
|||
<p>Every <span id="refresh"></span> seconds the values are updated</p> |
|||
</div> |
|||
</div> |
|||
<div id="footer"> |
|||
<div class="left"> |
|||
<a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2022</a> |
|||
<ul> |
|||
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> |
|||
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="right"> |
|||
<span id="version"></span><br/><br/> |
|||
<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a> |
|||
</div> |
|||
</div> |
|||
<script type="text/javascript"> |
|||
var exeOnce = true; |
|||
|
|||
function parseSys(obj) { |
|||
if(true == exeOnce) |
|||
parseVersion(obj); |
|||
} |
|||
|
|||
function parseIv(obj, root) { |
|||
var ivHtml = []; |
|||
|
|||
var tDiv = div(["ch-all", "iv"]); |
|||
tDiv.appendChild(span("Total", ["head"])); |
|||
var total = new Array(root.ch0_fld_names.length).fill(0); |
|||
if(obj.length > 1) |
|||
ivHtml.push(tDiv); |
|||
|
|||
for(var iv of obj) { |
|||
main = div(["iv"]); |
|||
var ch0 = div(["ch-iv"]); |
|||
var limit = iv["power_limit_read"] + "%"; |
|||
if(limit == "65535%") |
|||
limit = "n/a"; |
|||
ch0.appendChild(span(iv["name"] + " Limit " + limit + " | last Alarm: " + iv["last_alarm"], ["head"])); |
|||
|
|||
for(var j = 0; j < root.ch0_fld_names.length; j++) { |
|||
var val = Math.round(iv["ch"][0][j] * 100) / 100; |
|||
if(val > 0) { |
|||
var sub = div(["subgrp"]); |
|||
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"])); |
|||
sub.appendChild(span(root["ch0_fld_names"][j], ["info"])); |
|||
ch0.appendChild(sub); |
|||
|
|||
switch(j) { |
|||
case 2: total[j] += val; break; // P_AC |
|||
case 6: total[j] += val; break; // YieldTotal |
|||
case 7: total[j] += val; break; // YieldDay |
|||
case 8: total[j] += val; break; // P_DC |
|||
case 10: total[j] += val; break; // Q_AC |
|||
} |
|||
} |
|||
} |
|||
main.appendChild(ch0); |
|||
|
|||
|
|||
for(var i = 1; i < (iv["channels"] + 1); i++) { |
|||
var ch = div(["ch"]); |
|||
ch.appendChild(span(("" == iv["ch_names"][i]) ? ("CHANNEL " + i) : iv["ch_names"][i], ["head"])); |
|||
|
|||
for(var j = 0; j < root.fld_names.length; j++) { |
|||
var val = Math.round(iv["ch"][i][j] * 100) / 100; |
|||
if(val > 0) { |
|||
ch.appendChild(span(val + " " + span(root["fld_units"][j], ["unit"]).innerHTML, ["value"])); |
|||
ch.appendChild(span(root["fld_names"][j], ["info"])); |
|||
} |
|||
} |
|||
main.appendChild(ch); |
|||
} |
|||
|
|||
var ts = div(["ts"]); |
|||
var ageInfo = "Last received data requested at: "; |
|||
if(iv["ts_last_success"] > 0) { |
|||
var date = new Date(iv["ts_last_success"] * 1000); |
|||
ageInfo += date.toLocaleString('de-DE'); |
|||
} |
|||
else |
|||
ageInfo += "nothing received"; |
|||
|
|||
ts.innerHTML = ageInfo; |
|||
|
|||
main.appendChild(ts); |
|||
ivHtml.push(main); |
|||
} |
|||
|
|||
// total |
|||
if(obj.length > 1) { |
|||
for(var j = 0; j < root.ch0_fld_names.length; j++) { |
|||
var val = total[j]; |
|||
if(val > 0) { |
|||
var sub = div(["subgrp"]); |
|||
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"])); |
|||
sub.appendChild(span(root["ch0_fld_names"][j], ["info"])); |
|||
tDiv.appendChild(sub); |
|||
} |
|||
} |
|||
} |
|||
|
|||
document.getElementById("live").replaceChildren(...ivHtml); |
|||
} |
|||
|
|||
function parse(obj) { |
|||
if(null != obj) { |
|||
if(true == exeOnce) |
|||
parseMenu(obj["menu"]); |
|||
parseSys(obj["system"]); |
|||
parseIv(obj["inverter"], obj); |
|||
document.getElementById("refresh").innerHTML = obj["refresh_interval"]; |
|||
if(true == exeOnce) { |
|||
window.setInterval("getAjax('/api/live', parse)", obj["refresh_interval"] * 1000); |
|||
exeOnce = false; |
|||
} |
|||
} |
|||
else |
|||
document.getElementById("refresh").innerHTML = "n/a"; |
|||
} |
|||
|
|||
getAjax("/api/live", parse); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -1,133 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __MQTT_H__ |
|||
#define __MQTT_H__ |
|||
|
|||
#ifdef ESP8266 |
|||
#include <ESP8266WiFi.h> |
|||
#elif defined(ESP32) |
|||
#include <WiFi.h> |
|||
#endif |
|||
|
|||
#if defined(ESP32) && defined(F) |
|||
#undef F |
|||
#define F(sl) (sl) |
|||
#endif |
|||
#include <PubSubClient.h> |
|||
#include "defines.h" |
|||
|
|||
class mqtt { |
|||
public: |
|||
mqtt() { |
|||
mClient = new PubSubClient(mEspClient); |
|||
mAddressSet = false; |
|||
|
|||
mLastReconnect = 0; |
|||
mTxCnt = 0; |
|||
|
|||
memset(mDevName, 0, DEVNAME_LEN); |
|||
} |
|||
|
|||
~mqtt() { } |
|||
|
|||
void setup(mqttConfig_t *cfg, const char *devname) { |
|||
DPRINTLN(DBG_VERBOSE, F("mqtt.h:setup")); |
|||
mAddressSet = true; |
|||
|
|||
mCfg = cfg; |
|||
snprintf(mDevName, DEVNAME_LEN, "%s", devname); |
|||
|
|||
mClient->setServer(mCfg->broker, mCfg->port); |
|||
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE); |
|||
} |
|||
|
|||
void setCallback(MQTT_CALLBACK_SIGNATURE){ |
|||
mClient->setCallback(callback); |
|||
} |
|||
|
|||
void sendMsg(const char *topic, const char *msg) { |
|||
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:sendMsg"));
|
|||
char top[64]; |
|||
snprintf(top, 64, "%s/%s", mCfg->topic, topic); |
|||
sendMsg2(top, msg, false); |
|||
mTxCnt++; |
|||
} |
|||
|
|||
void sendMsg2(const char *topic, const char *msg, boolean retained) { |
|||
if(mAddressSet) { |
|||
if(!mClient->connected()) |
|||
reconnect(); |
|||
if(mClient->connected()) |
|||
mClient->publish(topic, msg, retained); |
|||
} |
|||
} |
|||
|
|||
bool isConnected(bool doRecon = false) { |
|||
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:isConnected"));
|
|||
if(doRecon && !mClient->connected()) |
|||
reconnect(); |
|||
return mClient->connected(); |
|||
} |
|||
|
|||
void loop() { |
|||
//DPRINT(F("m"));
|
|||
if(!mClient->connected()) |
|||
reconnect(); |
|||
mClient->loop(); |
|||
} |
|||
|
|||
uint32_t getTxCnt(void) { |
|||
return mTxCnt; |
|||
} |
|||
|
|||
private: |
|||
void reconnect(void) { |
|||
DPRINTLN(DBG_DEBUG, F("mqtt.h:reconnect")); |
|||
DPRINTLN(DBG_DEBUG, F("MQTT mClient->_state ") + String(mClient->state()) ); |
|||
|
|||
#ifdef ESP8266 |
|||
DPRINTLN(DBG_DEBUG, F("WIFI mEspClient.status ") + String(mEspClient.status()) ); |
|||
#endif |
|||
|
|||
boolean resub = false; |
|||
if(!mClient->connected() && (millis() - mLastReconnect) > MQTT_RECONNECT_DELAY ) { |
|||
mLastReconnect = millis(); |
|||
if(strlen(mDevName) > 0) { |
|||
// der Server und der Port müssen neu gesetzt werden,
|
|||
// da ein MQTT_CONNECTION_LOST -3 die Werte zerstört hat.
|
|||
mClient->setServer(mCfg->broker, mCfg->port); |
|||
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE); |
|||
|
|||
char lwt[MQTT_TOPIC_LEN + 7 ]; // "/uptime" --> + 7 byte
|
|||
snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg->topic); |
|||
|
|||
if((strlen(mCfg->user) > 0) && (strlen(mCfg->pwd) > 0)) |
|||
resub = mClient->connect(mDevName, mCfg->user, mCfg->pwd, lwt, 0, false, "offline"); |
|||
else |
|||
resub = mClient->connect(mDevName, lwt, 0, false, "offline"); |
|||
// ein Subscribe ist nur nach einem connect notwendig
|
|||
if(resub) { |
|||
char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte
|
|||
// ToDo: "/devcontrol/#" is hardcoded
|
|||
snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg->topic); |
|||
DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic)); |
|||
mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#"
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
WiFiClient mEspClient; |
|||
PubSubClient *mClient; |
|||
|
|||
bool mAddressSet; |
|||
mqttConfig_t *mCfg; |
|||
char mDevName[DEVNAME_LEN]; |
|||
uint32_t mLastReconnect; |
|||
uint32_t mTxCnt; |
|||
}; |
|||
|
|||
#endif /*__MQTT_H_*/ |
@ -1,109 +0,0 @@ |
|||
; PlatformIO Project Configuration File |
|||
; |
|||
; Build options: build flags, source filter |
|||
; Upload options: custom upload port, speed and extra flags |
|||
; Library options: dependencies, extra library storages |
|||
; Advanced options: extra scripting |
|||
; |
|||
; Please visit documentation for the other options and examples |
|||
; https://docs.platformio.org/page/projectconf.html |
|||
|
|||
[platformio] |
|||
src_dir = . |
|||
|
|||
[env] |
|||
framework = arduino |
|||
|
|||
build_flags = |
|||
-include "config.h" |
|||
; ;;;;; Possible Debug options ;;;;;; |
|||
; https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level |
|||
;-DDEBUG_ESP_PORT=Serial |
|||
;-DDEBUG_ESP_CORE |
|||
;-DDEBUG_ESP_WIFI |
|||
;-DDEBUG_ESP_HTTP_CLIENT |
|||
;-DDEBUG_ESP_HTTP_SERVER |
|||
;-DDEBUG_ESP_OOM |
|||
|
|||
monitor_speed = 115200 |
|||
|
|||
extra_scripts = |
|||
pre:scripts/auto_firmware_version.py |
|||
pre:html/convert.py |
|||
|
|||
lib_deps = |
|||
https://github.com/yubox-node-org/ESPAsyncWebServer |
|||
nrf24/RF24 |
|||
paulstoffregen/Time |
|||
knolleary/PubSubClient |
|||
bblanchon/ArduinoJson |
|||
;esp8266/DNSServer |
|||
;esp8266/EEPROM |
|||
;esp8266/ESP8266WiFi |
|||
;esp8266/SPI |
|||
;esp8266/Ticker |
|||
|
|||
[env:esp8266-release] |
|||
platform = espressif8266 |
|||
board = esp12e |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -D RELEASE |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp8266-debug] |
|||
platform = espressif8266 |
|||
board = esp12e |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial |
|||
build_type = debug |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp8266-1m-release] |
|||
platform = espressif8266 |
|||
board = esp8285 |
|||
board_build.ldscript = eagle.flash.1m64.ld |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -D RELEASE |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp8266-1m-debug] |
|||
platform = espressif8266 |
|||
board = esp8285 |
|||
board_build.ldscript = eagle.flash.1m64.ld |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial |
|||
build_type = debug |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp32-wroom32-release] |
|||
platform = espressif32 |
|||
board = lolin_d32 |
|||
build_flags = -D RELEASE -std=gnu++14 |
|||
build_unflags = -std=gnu++11 |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
|||
|
|||
[env:esp32-wroom32-debug] |
|||
platform = espressif32 |
|||
board = lolin_d32 |
|||
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -std=gnu++14 |
|||
build_unflags = -std=gnu++11 |
|||
build_type = debug |
|||
monitor_filters = |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue