mirror of
https://github.com/lumapu/ahoy.git
synced 2025-05-25 14:56:11 +02:00
moved payload related stuff to other file
fix line break for inverter list on index update release workflow action fix #405 renamed binary for ESP8285 (platformio.ini)
This commit is contained in:
parent
1d7b02571d
commit
9c68ad1467
9 changed files with 320 additions and 220 deletions
14
.github/workflows/compile_esp8266.yml
vendored
14
.github/workflows/compile_esp8266.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
- uses: benjlevesque/short-sha@v2.0
|
- uses: benjlevesque/short-sha@v2.1
|
||||||
id: short-sha
|
id: short-sha
|
||||||
with:
|
with:
|
||||||
length: 7
|
length: 7
|
||||||
|
@ -46,16 +46,17 @@ jobs:
|
||||||
pip install --upgrade platformio
|
pip install --upgrade platformio
|
||||||
|
|
||||||
- name: Convert HTML files
|
- name: Convert HTML files
|
||||||
working-directory: tools/esp8266/html
|
working-directory: src/web/html
|
||||||
run: python convert.py
|
run: python convert.py
|
||||||
- name: Run PlatformIO
|
- name: Run PlatformIO
|
||||||
run: pio run -d tools/esp8266 --environment esp8266-release --environment esp8266-1m-release --environment esp32-wroom32-release
|
run: pio run -d tools/esp8266 --environment esp8266-release --environment esp8266-1m-release --environment esp32-wroom32-release
|
||||||
|
|
||||||
- name: Rename Binary files
|
- name: Rename Binary files
|
||||||
id: rename-binary-files
|
id: rename-binary-files
|
||||||
working-directory: tools/esp8266/scripts
|
working-directory: src
|
||||||
run: python getVersion.py
|
run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT
|
||||||
- name: create-release
|
|
||||||
|
- name: Create Release
|
||||||
id: create-release
|
id: create-release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
with:
|
with:
|
||||||
|
@ -74,7 +75,8 @@ jobs:
|
||||||
VERSION: ${{ steps.rename-binary-files.outputs.name }}
|
VERSION: ${{ steps.rename-binary-files.outputs.name }}
|
||||||
- name: create-artifact
|
- name: create-artifact
|
||||||
run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip tools/esp8266/.pio/build/out/* tools/esp8266/User_Manual.md
|
run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip tools/esp8266/.pio/build/out/* tools/esp8266/User_Manual.md
|
||||||
- name: upload-release
|
|
||||||
|
- name: Upload Release
|
||||||
id: upload-release
|
id: upload-release
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -204,7 +204,7 @@ When everything is wired up and the firmware is flashed, it is time to connect t
|
||||||
If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.<br/>
|
If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.<br/>
|
||||||
<br/>
|
<br/>
|
||||||
If connected to your local Network, you just have to find out the used IP Address or try the default name [http://ahoy-dtu/](http://ahoy-dtu/). In most cases your Router will give you a hint.<br/>
|
If connected to your local Network, you just have to find out the used IP Address or try the default name [http://ahoy-dtu/](http://ahoy-dtu/). In most cases your Router will give you a hint.<br/>
|
||||||
If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is [http://192.168.1.1/](http://192.168.1.1/).<br/>
|
If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is [http://192.168.4.1/](http://192.168.4.1/).<br/>
|
||||||
Just open the IP-Address in your browser.<br/>
|
Just open the IP-Address in your browser.<br/>
|
||||||
<br/>
|
<br/>
|
||||||
The webinterface has the following abilities:
|
The webinterface has the following abilities:
|
||||||
|
@ -216,7 +216,7 @@ When everything is wired up and the firmware is flashed, it is time to connect t
|
||||||
|
|
||||||
##### HTTP based Pages
|
##### HTTP based Pages
|
||||||
|
|
||||||
To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. [http://ahoy-dtu/setup](http://ahoy-dtu/setup) or [http://192.168.1.1/setup](http://192.168.1.1/setup) ).<br/>
|
To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. [http://ahoy-dtu/setup](http://ahoy-dtu/setup) or [http://192.168.4.1/setup](http://192.168.4.1/setup) ).<br/>
|
||||||
|
|
||||||
| page | use | output | default availability |
|
| page | use | output | default availability |
|
||||||
| ---- | ------ | ------ | ------ |
|
| ---- | ------ | ------ | ------ |
|
||||||
|
|
208
src/app.cpp
208
src/app.cpp
|
@ -38,6 +38,9 @@ void app::setup(uint32_t timeout) {
|
||||||
mWifi->setup(timeout, mWifiSettingsValid);
|
mWifi->setup(timeout, mWifiSettingsValid);
|
||||||
|
|
||||||
mSys->setup(mConfig.amplifierPower, mConfig.pinIrq, mConfig.pinCe, mConfig.pinCs);
|
mSys->setup(mConfig.amplifierPower, mConfig.pinIrq, mConfig.pinCe, mConfig.pinCs);
|
||||||
|
mPayload.setup(mSys);
|
||||||
|
mPayload.enableSerialDebug(mConfig.serialDebug);
|
||||||
|
mPayload.addListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1));
|
||||||
#ifndef AP_ONLY
|
#ifndef AP_ONLY
|
||||||
setupMqtt();
|
setupMqtt();
|
||||||
#endif
|
#endif
|
||||||
|
@ -108,42 +111,7 @@ void app::loop(void) {
|
||||||
mStat.frmCnt++;
|
mStat.frmCnt++;
|
||||||
|
|
||||||
if (0 != len) {
|
if (0 != len) {
|
||||||
Inverter<> *iv = mSys->findInverter(&p->packet[1]);
|
mPayload.add(p, len);
|
||||||
if ((NULL != iv) && (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES))) { // response from get information command
|
|
||||||
mPayload[iv->id].txId = p->packet[0];
|
|
||||||
DPRINTLN(DBG_DEBUG, F("Response from info request received"));
|
|
||||||
uint8_t *pid = &p->packet[9];
|
|
||||||
if (*pid == 0x00) {
|
|
||||||
DPRINT(DBG_DEBUG, F("fragment number zero received and ignored"));
|
|
||||||
} else {
|
|
||||||
DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX));
|
|
||||||
if ((*pid & 0x7F) < 5) {
|
|
||||||
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len - 11);
|
|
||||||
mPayload[iv->id].len[(*pid & 0x7F) - 1] = len - 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((*pid & ALL_FRAMES) == ALL_FRAMES) {
|
|
||||||
// Last packet
|
|
||||||
if ((*pid & 0x7f) > mPayload[iv->id].maxPackId) {
|
|
||||||
mPayload[iv->id].maxPackId = (*pid & 0x7f);
|
|
||||||
if (*pid > 0x81)
|
|
||||||
mLastPacketId = *pid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((NULL != iv) && (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES))) { // response from dev control command
|
|
||||||
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received"));
|
|
||||||
|
|
||||||
mPayload[iv->id].txId = p->packet[0];
|
|
||||||
iv->devControlRequest = false;
|
|
||||||
|
|
||||||
if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) {
|
|
||||||
String msg = (p->packet[10] == 0x00 && p->packet[11] == 0x00) ? "" : "NOT ";
|
|
||||||
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has ") + msg + F("accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1]));
|
|
||||||
}
|
|
||||||
iv->devControlCmd = Init;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mSys->BufCtrl.popBack();
|
mSys->BufCtrl.popBack();
|
||||||
|
@ -151,7 +119,7 @@ void app::loop(void) {
|
||||||
yield();
|
yield();
|
||||||
|
|
||||||
if (rxRdy) {
|
if (rxRdy) {
|
||||||
processPayload(true);
|
mPayload.process(true, mConfig.maxRetransPerPyld, &mStat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,18 +180,16 @@ void app::loop(void) {
|
||||||
int8_t maxLoop = MAX_NUM_INVERTERS;
|
int8_t maxLoop = MAX_NUM_INVERTERS;
|
||||||
Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId);
|
Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId);
|
||||||
do {
|
do {
|
||||||
// if(NULL != iv)
|
|
||||||
// mPayload[iv->id].requested = false;
|
|
||||||
mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1;
|
mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1;
|
||||||
iv = mSys->getInverterByPos(mSendLastIvId);
|
iv = mSys->getInverterByPos(mSendLastIvId);
|
||||||
} while ((NULL == iv) && ((maxLoop--) > 0));
|
} while ((NULL == iv) && ((maxLoop--) > 0));
|
||||||
|
|
||||||
if (NULL != iv) {
|
if (NULL != iv) {
|
||||||
if (!mPayload[iv->id].complete)
|
if (!mPayload.isComplete(iv))
|
||||||
processPayload(false);
|
mPayload.process(false, mConfig.maxRetransPerPyld, &mStat);
|
||||||
|
|
||||||
if (!mPayload[iv->id].complete) {
|
if (!mPayload.isComplete(iv)) {
|
||||||
if (0 == mPayload[iv->id].maxPackId)
|
if (0 == mPayload.getMaxPacketId(iv))
|
||||||
mStat.rxFailNoAnser++;
|
mStat.rxFailNoAnser++;
|
||||||
else
|
else
|
||||||
mStat.rxFail++;
|
mStat.rxFail++;
|
||||||
|
@ -233,12 +199,12 @@ void app::loop(void) {
|
||||||
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
|
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
|
||||||
if (mConfig.serialDebug) {
|
if (mConfig.serialDebug) {
|
||||||
DPRINT(DBG_INFO, F("(#") + String(iv->id) + ") ");
|
DPRINT(DBG_INFO, F("(#") + String(iv->id) + ") ");
|
||||||
DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload[iv->id].retransmits) + ")");
|
DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload.getRetransmits(iv)) + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPayload(iv);
|
mPayload.reset(iv, mUtcTimestamp);
|
||||||
mPayload[iv->id].requested = true;
|
mPayload.request(iv);
|
||||||
|
|
||||||
yield();
|
yield();
|
||||||
if (mConfig.serialDebug) {
|
if (mConfig.serialDebug) {
|
||||||
|
@ -250,14 +216,14 @@ void app::loop(void) {
|
||||||
if (mConfig.serialDebug)
|
if (mConfig.serialDebug)
|
||||||
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0]));
|
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0]));
|
||||||
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit);
|
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit);
|
||||||
mPayload[iv->id].txCmd = iv->devControlCmd;
|
mPayload.setTxCmd(iv, iv->devControlCmd);
|
||||||
iv->clearCmdQueue();
|
iv->clearCmdQueue();
|
||||||
iv->enqueCommand<InfoCommand>(SystemConfigPara);
|
iv->enqueCommand<InfoCommand>(SystemConfigPara);
|
||||||
} else {
|
} else {
|
||||||
uint8_t cmd = iv->getQueuedCmd();
|
uint8_t cmd = iv->getQueuedCmd();
|
||||||
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket"));
|
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket"));
|
||||||
mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex);
|
mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload.getTs(iv), iv->alarmMesIndex);
|
||||||
mPayload[iv->id].txCmd = cmd;
|
mPayload.setTxCmd(iv, cmd);
|
||||||
mRxTicker = 0;
|
mRxTicker = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,134 +242,6 @@ void app::handleIntr(void) {
|
||||||
mSys->Radio.handleIntr();
|
mSys->Radio.handleIntr();
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
bool app::buildPayload(uint8_t id) {
|
|
||||||
DPRINTLN(DBG_VERBOSE, F("app::buildPayload"));
|
|
||||||
uint16_t crc = 0xffff, crcRcv = 0x0000;
|
|
||||||
if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES)
|
|
||||||
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) {
|
|
||||||
if (mPayload[id].len[i] > 0) {
|
|
||||||
if (i == (mPayload[id].maxPackId - 1)) {
|
|
||||||
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
|
|
||||||
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]);
|
|
||||||
} else
|
|
||||||
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
|
|
||||||
}
|
|
||||||
yield();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (crc == crcRcv) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
void app::processPayload(bool retransmit) {
|
|
||||||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
|
|
||||||
Inverter<> *iv = mSys->getInverterByPos(id);
|
|
||||||
if (NULL == iv)
|
|
||||||
continue; // skip to next inverter
|
|
||||||
|
|
||||||
if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) {
|
|
||||||
// no processing needed if txId is not 0x95
|
|
||||||
// DPRINTLN(DBG_INFO, F("processPayload - set complete, txId: ") + String(mPayload[iv->id].txId, HEX));
|
|
||||||
mPayload[iv->id].complete = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mPayload[iv->id].complete) {
|
|
||||||
if (!buildPayload(iv->id)) { // payload not complete
|
|
||||||
if ((mPayload[iv->id].requested) && (retransmit)) {
|
|
||||||
if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) {
|
|
||||||
// This is required to prevent retransmissions without answer.
|
|
||||||
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm..."));
|
|
||||||
mPayload[iv->id].retransmits = mConfig.maxRetransPerPyld;
|
|
||||||
} else {
|
|
||||||
if (mPayload[iv->id].retransmits < mConfig.maxRetransPerPyld) {
|
|
||||||
mPayload[iv->id].retransmits++;
|
|
||||||
if (mPayload[iv->id].maxPackId != 0) {
|
|
||||||
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) {
|
|
||||||
if (mPayload[iv->id].len[i] == 0) {
|
|
||||||
if (mConfig.serialDebug)
|
|
||||||
DPRINTLN(DBG_WARN, F("while retrieving data: Frame ") + String(i + 1) + F(" missing: Request Retransmit"));
|
|
||||||
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true);
|
|
||||||
break; // only retransmit one frame per loop
|
|
||||||
}
|
|
||||||
yield();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mConfig.serialDebug)
|
|
||||||
DPRINTLN(DBG_WARN, F("while retrieving data: last frame missing: Request Retransmit"));
|
|
||||||
if (0x00 != mLastPacketId)
|
|
||||||
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, mLastPacketId, true);
|
|
||||||
else {
|
|
||||||
mPayload[iv->id].txCmd = iv->getQueuedCmd();
|
|
||||||
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 (mConfig.serialDebug) {
|
|
||||||
DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): ");
|
|
||||||
mSys->Radio.dumpBuf(NULL, payload, payloadLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NULL == rec) {
|
|
||||||
DPRINTLN(DBG_ERROR, F("record is NULL!"));
|
|
||||||
} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) {
|
|
||||||
if (mPayload[iv->id].txId == (TX_REQ_INFO + 0x80))
|
|
||||||
mStat.rxSuccess++;
|
|
||||||
|
|
||||||
rec->ts = mPayload[iv->id].ts;
|
|
||||||
for (uint8_t i = 0; i < rec->length; i++) {
|
|
||||||
iv->addValue(i, payload, rec);
|
|
||||||
yield();
|
|
||||||
}
|
|
||||||
iv->doCalculations();
|
|
||||||
|
|
||||||
mMqttSendList.push(mPayload[iv->id].txCmd);
|
|
||||||
} else {
|
|
||||||
DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes"));
|
|
||||||
mStat.rxFail++;
|
|
||||||
}
|
|
||||||
|
|
||||||
iv->setQueuedCmdFinished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yield();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ist MQTT aktiviert und es wurden Daten vom einem oder mehreren WR aufbereitet
|
|
||||||
// dann die den mMqttTicker auf mMqttIntervall -2 setzen, also
|
|
||||||
// MQTT aussenden in 2 sek aktivieren
|
|
||||||
if ((mMqttInterval != 0xffff) && (!mMqttSendList.empty())) {
|
|
||||||
mMqttTicker = mMqttInterval - 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
bool app::getWifiApActive(void) {
|
bool app::getWifiApActive(void) {
|
||||||
return mWifi->getApActive();
|
return mWifi->getApActive();
|
||||||
|
@ -451,9 +289,7 @@ void app::resetSystem(void) {
|
||||||
|
|
||||||
mShowRebootRequest = false;
|
mShowRebootRequest = false;
|
||||||
|
|
||||||
memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t)));
|
|
||||||
memset(&mStat, 0, sizeof(statistics_t));
|
memset(&mStat, 0, sizeof(statistics_t));
|
||||||
mLastPacketId = 0x00;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
@ -545,7 +381,7 @@ void app::loadEEpconfig(void) {
|
||||||
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
|
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
|
||||||
iv = mSys->getInverterByPos(i, false);
|
iv = mSys->getInverterByPos(i, false);
|
||||||
if (NULL != iv)
|
if (NULL != iv)
|
||||||
resetPayload(iv);
|
mPayload.reset(iv, mUtcTimestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -627,15 +463,3 @@ void app::updateLed(void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
void app::resetPayload(Inverter<> *iv) {
|
|
||||||
DPRINTLN(DBG_INFO, "resetPayload: id: " + String(iv->id));
|
|
||||||
memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES);
|
|
||||||
mPayload[iv->id].txCmd = 0;
|
|
||||||
mPayload[iv->id].retransmits = 0;
|
|
||||||
mPayload[iv->id].maxPackId = 0;
|
|
||||||
mPayload[iv->id].complete = false;
|
|
||||||
mPayload[iv->id].requested = false;
|
|
||||||
mPayload[iv->id].ts = mUtcTimestamp;
|
|
||||||
}
|
|
||||||
|
|
23
src/app.h
23
src/app.h
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
#include "hm/CircularBuffer.h"
|
#include "hm/CircularBuffer.h"
|
||||||
#include "hm/hmSystem.h"
|
#include "hm/hmSystem.h"
|
||||||
|
#include "hm/payload.h"
|
||||||
#include "wifi/ahoywifi.h"
|
#include "wifi/ahoywifi.h"
|
||||||
#include "web/mqtt.h"
|
#include "web/mqtt.h"
|
||||||
#include "web/web.h"
|
#include "web/web.h"
|
||||||
|
@ -33,19 +34,7 @@
|
||||||
|
|
||||||
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
|
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
|
||||||
typedef mqtt<HmSystemType> MqttType;
|
typedef mqtt<HmSystemType> MqttType;
|
||||||
|
typedef payload<HmSystemType> PayloadType;
|
||||||
typedef struct {
|
|
||||||
uint8_t txCmd;
|
|
||||||
uint8_t txId;
|
|
||||||
uint8_t invId;
|
|
||||||
uint32_t ts;
|
|
||||||
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE];
|
|
||||||
uint8_t len[MAX_PAYLOAD_ENTRIES];
|
|
||||||
bool complete;
|
|
||||||
uint8_t maxPackId;
|
|
||||||
uint8_t retransmits;
|
|
||||||
bool requested;
|
|
||||||
} invPayload_t;
|
|
||||||
|
|
||||||
class ahoywifi;
|
class ahoywifi;
|
||||||
class web;
|
class web;
|
||||||
|
@ -65,6 +54,10 @@ class app {
|
||||||
void scanAvailNetworks(void);
|
void scanAvailNetworks(void);
|
||||||
void getAvailNetworks(JsonObject obj);
|
void getAvailNetworks(JsonObject obj);
|
||||||
|
|
||||||
|
void payloadEventListener(uint8_t cmd) {
|
||||||
|
mMqttSendList.push(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t getIrqPin(void) {
|
uint8_t getIrqPin(void) {
|
||||||
return mConfig.pinIrq;
|
return mConfig.pinIrq;
|
||||||
}
|
}
|
||||||
|
@ -183,7 +176,6 @@ class app {
|
||||||
void setupLed(void);
|
void setupLed(void);
|
||||||
void updateLed(void);
|
void updateLed(void);
|
||||||
|
|
||||||
bool buildPayload(uint8_t id);
|
|
||||||
void processPayload(bool retransmit);
|
void processPayload(bool retransmit);
|
||||||
|
|
||||||
inline uint16_t buildEEpCrc(uint32_t start, uint32_t length) {
|
inline uint16_t buildEEpCrc(uint32_t start, uint32_t length) {
|
||||||
|
@ -268,13 +260,12 @@ class app {
|
||||||
sysConfig_t mSysConfig;
|
sysConfig_t mSysConfig;
|
||||||
config_t mConfig;
|
config_t mConfig;
|
||||||
char mVersion[12];
|
char mVersion[12];
|
||||||
|
PayloadType mPayload;
|
||||||
|
|
||||||
uint16_t mSendTicker;
|
uint16_t mSendTicker;
|
||||||
uint8_t mSendLastIvId;
|
uint8_t mSendLastIvId;
|
||||||
|
|
||||||
invPayload_t mPayload[MAX_NUM_INVERTERS];
|
|
||||||
statistics_t mStat;
|
statistics_t mStat;
|
||||||
uint8_t mLastPacketId;
|
|
||||||
|
|
||||||
// timer
|
// timer
|
||||||
uint32_t mTicker;
|
uint32_t mTicker;
|
||||||
|
|
250
src/hm/payload.h
Normal file
250
src/hm/payload.h
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// 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 <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_*/
|
|
@ -64,7 +64,7 @@ monitor_filters =
|
||||||
time ; Add timestamp with milliseconds for each new line
|
time ; Add timestamp with milliseconds for each new line
|
||||||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
|
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
|
||||||
|
|
||||||
[env:esp8266-1m-release]
|
[env:esp8285-release]
|
||||||
platform = espressif8266
|
platform = espressif8266
|
||||||
board = esp8285
|
board = esp8285
|
||||||
board_build.ldscript = eagle.flash.1m64.ld
|
board_build.ldscript = eagle.flash.1m64.ld
|
||||||
|
@ -75,7 +75,7 @@ monitor_filters =
|
||||||
time ; Add timestamp with milliseconds for each new line
|
time ; Add timestamp with milliseconds for each new line
|
||||||
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
|
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
|
||||||
|
|
||||||
[env:esp8266-1m-debug]
|
[env:esp8285-debug]
|
||||||
platform = espressif8266
|
platform = espressif8266
|
||||||
board = esp8285
|
board = esp8285
|
||||||
board_build.ldscript = eagle.flash.1m64.ld
|
board_build.ldscript = eagle.flash.1m64.ld
|
||||||
|
|
33
src/utils/handler.h
Normal file
33
src/utils/handler.h
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// 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__*/
|
|
@ -54,7 +54,7 @@
|
||||||
<h3>Support this project:</h3>
|
<h3>Support this project:</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li>
|
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li>
|
||||||
<li>Contribute to <a href="https://github.com/lumapu/ahoy/blob/main/tools/esp8266/User_Manual.md" target="_blank">documentation</a></li>
|
<li>Contribute to <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">documentation</a></li>
|
||||||
<li>Test <a href="https://github.com/lumapu/ahoy/actions/workflows/compile_development.yml" target="_blank">development firmware</a></li>
|
<li>Test <a href="https://github.com/lumapu/ahoy/actions/workflows/compile_development.yml" target="_blank">development firmware</a></li>
|
||||||
<li>make a <a href="https://paypal.me/lupusch" target="_blank">donation</a></li>
|
<li>make a <a href="https://paypal.me/lupusch" target="_blank">donation</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -153,7 +153,7 @@
|
||||||
for(var i of obj) {
|
for(var i of obj) {
|
||||||
html += "Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is ";
|
html += "Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is ";
|
||||||
if(false == i["is_avail"])
|
if(false == i["is_avail"])
|
||||||
html += "not yet available";
|
html += "not yet available\n";
|
||||||
else {
|
else {
|
||||||
html += "available and is ";
|
html += "available and is ";
|
||||||
if(false == i["is_producing"])
|
if(false == i["is_producing"])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue