mirror of https://github.com/lumapu/ahoy.git
73 changed files with 6267 additions and 3228 deletions
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 | | |
|||
|
@ -1,36 +1,33 @@ |
|||
# Changelog v0.5.66 |
|||
Changelog v0.6.0 |
|||
|
|||
**Note:** Version `0.5.42` to `0.5.65` were development versions. Last release version was `0.5.41` |
|||
Detailed change log (development changes): https://github.com/lumapu/ahoy/blob/945a671d27d10d0f7c175ebbf2fbb2806f9cd79a/src/CHANGES.md |
|||
## General |
|||
* improved night time calculation time to 1 minute after last communication pause #515 |
|||
* refactored code for better readability |
|||
* improved Hoymiles commuinication (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 (repsonsive 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) |
|||
|
|||
|
|||
* updated REST API and MQTT (both of them use the same functionality) |
|||
* improved stability |
|||
* Regular expressions for input fields which are used for MQTT to be compliant to MQTT |
|||
* WiFi optimization (AP Mode and STA in parallel, reconnect if local STA is unavailable) |
|||
* improved display of `/system` |
|||
* fix Update button protection (prevent double click #527) |
|||
* optimized scheduler #515 |
|||
* fix of duplicates in API `/api/record/live` (#526) |
|||
* added update information to `index.html` (check for update with github.com) |
|||
* fix web logout (auto logout) |
|||
* switched MQTT library |
|||
* removed MQTT `available_text` (can be deducted from `available`) |
|||
* enhanced MQTT documentation in `User_Manual.md` |
|||
* changed MQTT topic `status` to nummeric value, check documentation in `User_Manual.md` |
|||
* added immediate (each minute) report of inverter status MQTT #522 |
|||
* increased MQTT user, pwd and topic length to 64 characters + `\0`. (The string end `\0` reduces the available size by one) #516 |
|||
* added disable night communication flag to MQTT #505 |
|||
* added MQTT <TOPIC>/status to show status over all inverters |
|||
* added MQTT RX counter to index.html |
|||
* added protection mask to select which pages should be protected |
|||
* added monochrome display that show values also if nothing changed and in offline mode #498 |
|||
* added icons to index.html, added WiFi-strength symbol on each page |
|||
* refactored communication offset (adjustable in minutes now) |
|||
* factory reset formats entire little fs |
|||
* renamed sunrise / sunset on index.html to start / stop communication |
|||
* fixed static IP save |
|||
* fix NTP with static IP |
|||
* all values are displayed on /live even if they are 0 |
|||
* added NRF24 info to Systeminfo |
|||
* reordered enqueue commands after boot up to prevent same payload length for successive commands |
|||
## 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 |
|||
|
@ -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
|
@ -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,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__*/ |
@ -1,251 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __PAYLOAD_H__ |
|||
#define __PAYLOAD_H__ |
|||
|
|||
#include "../utils/dbg.h" |
|||
#include "../utils/crc.h" |
|||
#include "../utils/handler.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; |
|||
uint8_t retransmits; |
|||
bool requested; |
|||
} invPayload_t; |
|||
|
|||
|
|||
typedef std::function<void(uint8_t)> payloadListenerType; |
|||
|
|||
|
|||
template<class HMSYSTEM> |
|||
class Payload : public Handler<payloadListenerType> { |
|||
public: |
|||
Payload() : Handler() {} |
|||
|
|||
void setup(HMSYSTEM *sys) { |
|||
mSys = sys; |
|||
memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t))); |
|||
mLastPacketId = 0x00; |
|||
mSerialDebug = false; |
|||
} |
|||
|
|||
void enableSerialDebug(bool enable) { |
|||
mSerialDebug = enable; |
|||
} |
|||
|
|||
bool isComplete(Inverter<> *iv) { |
|||
return mPayload[iv->id].complete; |
|||
} |
|||
|
|||
uint8_t getMaxPacketId(Inverter<> *iv) { |
|||
return mPayload[iv->id].maxPackId; |
|||
} |
|||
|
|||
uint8_t getRetransmits(Inverter<> *iv) { |
|||
return mPayload[iv->id].retransmits; |
|||
} |
|||
|
|||
uint32_t getTs(Inverter<> *iv) { |
|||
return mPayload[iv->id].ts; |
|||
} |
|||
|
|||
void request(Inverter<> *iv) { |
|||
mPayload[iv->id].requested = true; |
|||
} |
|||
|
|||
void setTxCmd(Inverter<> *iv, uint8_t cmd) { |
|||
mPayload[iv->id].txCmd = cmd; |
|||
} |
|||
|
|||
void notify(uint8_t val) { |
|||
for(typename std::list<payloadListenerType>::iterator it = mList.begin(); it != mList.end(); ++it) { |
|||
(*it)(val); |
|||
} |
|||
} |
|||
|
|||
void add(packet_t *p, uint8_t 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; |
|||
} |
|||
} |
|||
|
|||
bool build(uint8_t id) { |
|||
DPRINTLN(DBG_VERBOSE, F("build")); |
|||
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 process(bool retransmit, uint8_t maxRetransmits, statistics_t *stat) { |
|||
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 (!build(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 = maxRetransmits; |
|||
} else { |
|||
if (mPayload[iv->id].retransmits < maxRetransmits) { |
|||
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) { |
|||
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 { |
|||
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(); |
|||
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket")); |
|||
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 (mSerialDebug) { |
|||
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)) |
|||
stat->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); |
|||
} else { |
|||
DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); |
|||
stat->rxFail++; |
|||
} |
|||
|
|||
iv->setQueuedCmdFinished(); |
|||
} |
|||
} |
|||
|
|||
yield(); |
|||
|
|||
} |
|||
} |
|||
|
|||
void reset(Inverter<> *iv, uint32_t utcTs) { |
|||
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 = utcTs; |
|||
} |
|||
|
|||
private: |
|||
HMSYSTEM *mSys; |
|||
invPayload_t mPayload[MAX_NUM_INVERTERS]; |
|||
uint8_t mLastPacketId; |
|||
bool mSerialDebug; |
|||
}; |
|||
|
|||
#endif /*__PAYLOAD_H_*/ |
@ -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__*/ |
@ -1,307 +0,0 @@ |
|||
#ifndef __MONOCHROME_DISPLAY__ |
|||
#define __MONOCHROME_DISPLAY__ |
|||
|
|||
#if defined(ENA_NOKIA) || defined(ENA_SSD1306) |
|||
#ifdef ENA_NOKIA |
|||
#include <U8g2lib.h> |
|||
#define DISP_PROGMEM U8X8_PROGMEM |
|||
#else // ENA_SSD1306
|
|||
/* esp8266 : SCL = 5, SDA = 4 */ |
|||
/* ewsp32 : SCL = 22, SDA = 21 */ |
|||
#include <Wire.h> |
|||
#include <SSD1306Wire.h> |
|||
#define DISP_PROGMEM PROGMEM |
|||
#endif |
|||
|
|||
#include <Timezone.h> |
|||
|
|||
#include "../../utils/helper.h" |
|||
#include "../../hm/hmSystem.h" |
|||
|
|||
static uint8_t bmp_arrow[] DISP_PROGMEM = { |
|||
B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111, |
|||
B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111, |
|||
B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000}; |
|||
|
|||
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 Tim
|
|||
|
|||
template<class HMSYSTEM> |
|||
class MonochromeDisplay { |
|||
public: |
|||
#if defined(ENA_NOKIA) |
|||
MonochromeDisplay() : mDisplay(U8G2_R0, 5, 4, 16), mCE(CEST, CET) { |
|||
mNewPayload = false; |
|||
mExtra = 0; |
|||
} |
|||
#else // ENA_SSD1306
|
|||
MonochromeDisplay() : mDisplay(0x3c, SDA, SCL), mCE(CEST, CET) { |
|||
mNewPayload = false; |
|||
mExtra = 0; |
|||
mRx = 0; |
|||
mUp = 1; |
|||
} |
|||
#endif |
|||
|
|||
void setup(HMSYSTEM *sys, uint32_t *utcTs) { |
|||
mSys = sys; |
|||
mUtcTs = utcTs; |
|||
memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS ); |
|||
memset( mTotal, 0, sizeof(float)*MAX_NUM_INVERTERS ); |
|||
mLastHour = 25; |
|||
#if defined(ENA_NOKIA) |
|||
mDisplay.begin(); |
|||
ShowInfoText("booting..."); |
|||
#else |
|||
mDisplay.init(); |
|||
mDisplay.flipScreenVertically(); |
|||
mDisplay.setContrast(63); |
|||
mDisplay.setBrightness(63); |
|||
|
|||
mDisplay.clear(); |
|||
mDisplay.setFont(ArialMT_Plain_24); |
|||
mDisplay.setTextAlignment(TEXT_ALIGN_CENTER_BOTH); |
|||
|
|||
mDisplay.drawString(64,22,"Starting..."); |
|||
mDisplay.display(); |
|||
mDisplay.setTextAlignment(TEXT_ALIGN_LEFT); |
|||
#endif |
|||
} |
|||
|
|||
void loop(void) { |
|||
|
|||
} |
|||
|
|||
void payloadEventListener(uint8_t cmd) { |
|||
mNewPayload = true; |
|||
} |
|||
|
|||
void tickerSecond() { |
|||
static int cnt=1; |
|||
if(mNewPayload || !(cnt % 10)) { |
|||
cnt=1; |
|||
mNewPayload = false; |
|||
DataScreen(); |
|||
} |
|||
else |
|||
cnt++; |
|||
} |
|||
|
|||
private: |
|||
#if defined(ENA_NOKIA) |
|||
void ShowInfoText(const char *txt) { |
|||
/* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */ |
|||
mDisplay.clear(); |
|||
mDisplay.firstPage(); |
|||
do { |
|||
const char *e; |
|||
const char *p = txt; |
|||
int y=10; |
|||
mDisplay.setFont(u8g2_font_5x8_tr); |
|||
while(1) { |
|||
for(e=p+1; (*e && (*e != '\n')); e++); |
|||
size_t len=e-p; |
|||
mDisplay.setCursor(2,y); |
|||
String res=((String)p).substring(0,len); |
|||
mDisplay.print(res); |
|||
if ( !*e ) |
|||
break; |
|||
p=e+1; |
|||
y+=12; |
|||
} |
|||
mDisplay.sendBuffer(); |
|||
} while( mDisplay.nextPage() ); |
|||
} |
|||
#endif |
|||
|
|||
void DataScreen(void) { |
|||
String timeStr = ah::getDateTimeStr(mCE.toLocal(*mUtcTs)).substring(2, 22); |
|||
int hr = timeStr.substring(9,2).toInt(); |
|||
IPAddress ip = WiFi.localIP(); |
|||
float totalYield = 0.0, totalYieldToday = 0.0, totalActual = 0.0; |
|||
char fmtText[32]; |
|||
int ucnt=0, num_inv=0; |
|||
unsigned int pow_i[ MAX_NUM_INVERTERS ]; |
|||
|
|||
memset( pow_i, 0, sizeof(unsigned int)* MAX_NUM_INVERTERS ); |
|||
if ( hr < mLastHour ) // next day ? reset today-values
|
|||
memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS ); |
|||
mLastHour = hr; |
|||
|
|||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { |
|||
Inverter<> *iv = mSys->getInverterByPos(id); |
|||
if (NULL != iv) { |
|||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|||
uint8_t pos; |
|||
uint8_t list[] = {FLD_PAC, FLD_YT, FLD_YD}; |
|||
|
|||
for (uint8_t fld = 0; fld < 3; fld++) { |
|||
pos = iv->getPosByChFld(CH0, list[fld],rec); |
|||
int isprod = iv->isProducing(*mUtcTs,rec); |
|||
|
|||
if(fld == 1) |
|||
{ |
|||
if ( isprod ) |
|||
mTotal[num_inv] = iv->getValue(pos,rec); |
|||
totalYield += mTotal[num_inv]; |
|||
} |
|||
if(fld == 2) |
|||
{ |
|||
if ( isprod ) |
|||
mToday[num_inv] = iv->getValue(pos,rec); |
|||
totalYieldToday += mToday[num_inv]; |
|||
} |
|||
if((fld == 0) && isprod ) |
|||
{ |
|||
pow_i[num_inv] = iv->getValue(pos,rec); |
|||
totalActual += iv->getValue(pos,rec); |
|||
ucnt++; |
|||
} |
|||
} |
|||
num_inv++; |
|||
} |
|||
} |
|||
/* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */ |
|||
mDisplay.clear(); |
|||
#if defined(ENA_NOKIA) |
|||
mDisplay.firstPage(); |
|||
do { |
|||
if(ucnt) { |
|||
mDisplay.drawXBMP(10,1,8,17,bmp_arrow); |
|||
mDisplay.setFont(u8g2_font_logisoso16_tr); |
|||
mDisplay.setCursor(25,17); |
|||
sprintf(fmtText,"%3.0f",totalActual); |
|||
mDisplay.print(String(fmtText)+F(" W")); |
|||
} |
|||
else |
|||
{ |
|||
mDisplay.setFont(u8g2_font_logisoso16_tr ); |
|||
mDisplay.setCursor(10,17); |
|||
mDisplay.print(String(F("offline"))); |
|||
} |
|||
mDisplay.drawHLine(2,20,78); |
|||
mDisplay.setFont(u8g2_font_5x8_tr); |
|||
mDisplay.setCursor(5,29); |
|||
if (( num_inv < 2 ) || !(mExtra%2)) |
|||
{ |
|||
sprintf(fmtText,"%4.0f",totalYieldToday); |
|||
mDisplay.print(F("today ")+String(fmtText)+F(" Wh")); |
|||
mDisplay.setCursor(5,37); |
|||
sprintf(fmtText,"%.1f",totalYield); |
|||
mDisplay.print(F("total ")+String(fmtText)+F(" kWh")); |
|||
} |
|||
else |
|||
{ |
|||
int id1=(mExtra/2)%(num_inv-1); |
|||
if( pow_i[id1] ) |
|||
mDisplay.print(F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W")); |
|||
else |
|||
mDisplay.print(F("#")+String(id1+1)+F(" -----")); |
|||
mDisplay.setCursor(5,37); |
|||
if( pow_i[id1+1] ) |
|||
mDisplay.print(F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W")); |
|||
else |
|||
mDisplay.print(F("#")+String(id1+2)+F(" -----")); |
|||
} |
|||
if ( !(mExtra%10) && ip ) { |
|||
mDisplay.setCursor(5,47); |
|||
mDisplay.print(ip.toString()); |
|||
} |
|||
else { |
|||
mDisplay.setCursor(0,47); |
|||
mDisplay.print(timeStr); |
|||
} |
|||
|
|||
mDisplay.sendBuffer(); |
|||
} while( mDisplay.nextPage() ); |
|||
mExtra++; |
|||
#else // ENA_SSD1306
|
|||
if(mUp) { |
|||
mRx += 2; |
|||
if(mRx >= 20) |
|||
mUp = 0; |
|||
} else { |
|||
mRx -= 2; |
|||
if(mRx <= 0) |
|||
mUp = 1; |
|||
} |
|||
int ex = 2*( mExtra % 5 ); |
|||
|
|||
if(ucnt) { |
|||
mDisplay.setBrightness(63); |
|||
mDisplay.drawXbm(10+ex,5,8,17,bmp_arrow); |
|||
mDisplay.setFont(ArialMT_Plain_24); |
|||
sprintf(fmtText,"%3.0f",totalActual); |
|||
mDisplay.drawString(25+ex,0,String(fmtText)+F(" W")); |
|||
} |
|||
else |
|||
{ |
|||
mDisplay.setBrightness(1); |
|||
mDisplay.setFont(ArialMT_Plain_24); |
|||
mDisplay.drawString(25+ex,0,String(F("offline"))); |
|||
} |
|||
mDisplay.setFont(ArialMT_Plain_16); |
|||
|
|||
if (( num_inv < 2 ) || !(mExtra%2)) |
|||
{ |
|||
sprintf(fmtText,"%4.0f",totalYieldToday); |
|||
mDisplay.drawString(5,22,F("today ")+String(fmtText)+F(" Wh")); |
|||
sprintf(fmtText,"%.1f",totalYield); |
|||
mDisplay.drawString(5,35,F("total ")+String(fmtText)+F(" kWh")); |
|||
} |
|||
else |
|||
{ |
|||
int id1=(mExtra/2)%(num_inv-1); |
|||
if( pow_i[id1] ) |
|||
mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W")); |
|||
else |
|||
mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" -----")); |
|||
if( pow_i[id1+1] ) |
|||
mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W")); |
|||
else |
|||
mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" -----")); |
|||
} |
|||
mDisplay.drawLine(2,23,123,23); |
|||
|
|||
if ( (!(mExtra%10) && ip )|| (timeStr.length()<16)) |
|||
{ |
|||
mDisplay.drawString(5,49,ip.toString()); |
|||
} |
|||
else |
|||
{ |
|||
int w=mDisplay.getStringWidth(timeStr.c_str(),timeStr.length(),0); |
|||
if ( w>127 ) |
|||
{ |
|||
String tt=timeStr.substring(9,17); |
|||
w=mDisplay.getStringWidth(tt.c_str(),tt.length(),0); |
|||
mDisplay.drawString(127-w-mRx,49,tt); |
|||
} |
|||
else |
|||
mDisplay.drawString(0,49,timeStr); |
|||
} |
|||
|
|||
mDisplay.display(); |
|||
mExtra++; |
|||
#endif |
|||
} |
|||
|
|||
// private member variables
|
|||
#if defined(ENA_NOKIA) |
|||
U8G2_PCD8544_84X48_1_4W_HW_SPI mDisplay; |
|||
#else // ENA_SSD1306
|
|||
SSD1306Wire mDisplay; |
|||
int mRx; |
|||
char mUp; |
|||
#endif |
|||
int mExtra; |
|||
bool mNewPayload; |
|||
float mTotal[ MAX_NUM_INVERTERS ]; |
|||
float mToday[ MAX_NUM_INVERTERS ]; |
|||
uint32_t *mUtcTs; |
|||
int mLastHour; |
|||
HMSYSTEM *mSys; |
|||
Timezone mCE; |
|||
}; |
|||
#endif |
|||
|
|||
#endif /*__MONOCHROME_DISPLAY__*/ |
@ -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__*/ |
@ -1,27 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __AHOY_TIMER_H__ |
|||
#define __AHOY_TIMER_H__ |
|||
|
|||
#include <Arduino.h> |
|||
|
|||
namespace ah { |
|||
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; |
|||
} |
|||
} |
|||
|
|||
#endif /*__AHOY_TIMER_H__*/ |
@ -1,33 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Lukas Pusch, lukas@lpusch.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __HANDLER_H__ |
|||
#define __HANDLER_H__ |
|||
|
|||
#include <memory> |
|||
#include <functional> |
|||
#include <list> |
|||
|
|||
template<class TYPE> |
|||
class Handler { |
|||
public: |
|||
Handler() {} |
|||
|
|||
void addListener(TYPE f) { |
|||
mList.push_back(f); |
|||
} |
|||
|
|||
/*virtual void notify(void) {
|
|||
for(typename std::list<TYPE>::iterator it = mList.begin(); it != mList.end(); ++it) { |
|||
(*it)(); |
|||
} |
|||
}*/ |
|||
|
|||
protected: |
|||
std::list<TYPE> mList; |
|||
}; |
|||
|
|||
#endif /*__HANDLER_H__*/ |
@ -1,110 +0,0 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://ahoydtu.de
|
|||
// Lukas Pusch, lukas@lpusch.de
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
#ifndef __LIST_H__ |
|||
#define __LIST_H__ |
|||
|
|||
template<class T, class... Args> |
|||
struct node_s { |
|||
typedef T dT; |
|||
node_s *pre; |
|||
node_s *nxt; |
|||
uint8_t id; |
|||
dT d; |
|||
node_s() : pre(NULL), nxt(NULL), d() {} |
|||
node_s(Args... args) : id(0), pre(NULL), nxt(NULL), d(args...) {} |
|||
}; |
|||
|
|||
template<int MAX_NUM, class T, class... Args> |
|||
class llist { |
|||
typedef node_s<T, Args...> elmType; |
|||
typedef T dataType; |
|||
public: |
|||
llist() : root(mPool) { |
|||
root = NULL; |
|||
elmType *p = mPool; |
|||
for(uint32_t i = 0; i < MAX_NUM; i++) { |
|||
p->id = i; |
|||
p++; |
|||
} |
|||
mFill = mMax = 0; |
|||
} |
|||
|
|||
elmType *add(Args... args) { |
|||
elmType *p = root, *t; |
|||
if(NULL == (t = getFreeNode())) |
|||
return NULL; |
|||
if(++mFill > mMax) |
|||
mMax = mFill; |
|||
|
|||
if(NULL == root) { |
|||
p = root = t; |
|||
p->pre = p; |
|||
p->nxt = p; |
|||
} |
|||
else { |
|||
p = root->pre; |
|||
t->pre = p; |
|||
p->nxt->pre = t; |
|||
t->nxt = p->nxt; |
|||
p->nxt = t; |
|||
} |
|||
t->d = dataType(args...); |
|||
return p; |
|||
} |
|||
|
|||
elmType *getFront() { |
|||
return root; |
|||
} |
|||
|
|||
elmType *get(elmType *p) { |
|||
p = p->nxt; |
|||
return (p == root) ? NULL : p; |
|||
} |
|||
|
|||
elmType *rem(elmType *p) { |
|||
if(NULL == p) |
|||
return NULL; |
|||
elmType *t = p->nxt; |
|||
p->nxt->pre = p->pre; |
|||
p->pre->nxt = p->nxt; |
|||
if((root == p) && (p->nxt == p)) |
|||
root = NULL; |
|||
else |
|||
root = p->nxt; |
|||
p->nxt = NULL; |
|||
p->pre = NULL; |
|||
p = NULL; |
|||
mFill--; |
|||
return (NULL == root) ? NULL : ((t == root) ? NULL : t); |
|||
} |
|||
|
|||
uint16_t getFill(void) { |
|||
return mFill; |
|||
} |
|||
|
|||
uint16_t getMaxFill(void) { |
|||
return mMax; |
|||
} |
|||
|
|||
protected: |
|||
elmType *root; |
|||
|
|||
private: |
|||
elmType *getFreeNode(void) { |
|||
elmType *n = mPool; |
|||
for(uint32_t i = 0; i < MAX_NUM; i++) { |
|||
if(NULL == n->nxt) |
|||
return n; |
|||
n++; |
|||
} |
|||
return NULL; |
|||
} |
|||
|
|||
elmType mPool[MAX_NUM]; |
|||
uint16_t mFill, mMax; |
|||
}; |
|||
|
|||
#endif /*__LIST_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,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,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,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> |
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. |
@ -0,0 +1,15 @@ |
|||
############################ |
|||
# build executable binary |
|||
############################ |
|||
|
|||
FROM python:slim-bullseye |
|||
|
|||
COPY . /hoymiles |
|||
WORKDIR /hoymiles |
|||
|
|||
RUN python3 -m pip install pyrf24 influxdb_client && \ |
|||
python3 -m pip list #watch for RF24 module - if its there its installed |
|||
|
|||
RUN pip install crcmod pyyaml paho-mqtt SunTimes |
|||
|
|||
CMD python3 -um hoymiles --log-transactions --verbose --config /etc/ahoy.yml |
Loading…
Reference in new issue