12 Commits

Author SHA1 Message Date
Nic Limper
4d08454fff Update README.md 2023-09-26 20:30:03 +02:00
Nic Limper
f131b5ce84 Update README.md 2023-09-26 20:29:23 +02:00
Nic Limper
33ba6a7aa7 updated web interface design 2023-09-26 10:27:57 +02:00
Nic Limper
75c6a6c0f9 previews now also show for tags connected to external AP; removed extra debug msg 2023-09-24 21:34:30 +02:00
Jonas Niesner
f7e2025487 M3 display led code fixes 2023-09-23 22:47:56 +02:00
Nic Limper
a91dd5c2a2 fix hangs on esp32-s2; refactor task runner; moved some json objects from the stack to the heap
temporary, there's some extra debug info on the terminal. Will be removed again in the near future.
2023-09-23 18:40:28 +02:00
Nic Limper
0ba287f734 mutex on file write; prevent other AP from stealing tag content; bigger heap for timeTask 2023-09-22 19:42:36 +02:00
Nic Limper
ad52c64b94 typo 2023-09-22 11:13:48 +02:00
Nic Limper
8c06bb04f3 typo 2023-09-22 11:13:15 +02:00
Nic Limper
07807afe08 add 4.2" mini stands design 2023-09-22 11:11:36 +02:00
onkelfunny
eb173e355f avoid loading *.raw files when preview is disabled (#136)
by @onkelfunny
2023-09-21 17:03:21 +02:00
onkelfunny
b792b71608 default configurable tag content (#134)
by @onkelfunny
2023-09-21 13:51:03 +02:00
30 changed files with 1207 additions and 533 deletions

View File

@@ -160,7 +160,7 @@ void watchdog_enable(int timeout) {
uint32_t sleepMsEntry = 0; uint32_t sleepMsEntry = 0;
uint32_t loops = 0; uint32_t loops = 0;
bool interruped = false; bool interrupted = false;
// uint8_t ledcfg[12] = {0b00100010,0x78,0b00100100,5,0x03,0b01000011,1,0xC2,0b1100001,10,10,0}; // uint8_t ledcfg[12] = {0b00100010,0x78,0b00100100,5,0x03,0b01000011,1,0xC2,0b1100001,10,10,0};
// uint8_t ledcfg[12] = {0b00010010,0x7D,0,0,0x03,0xE8,0,0,0,0,0,0}; // uint8_t ledcfg[12] = {0b00010010,0x7D,0,0,0x03,0xE8,0,0,0,0,0,0};
@@ -185,14 +185,14 @@ void resettimer() {
// tell the sleep function to net sleep again // tell the sleep function to net sleep again
sleepMsEntry = sleepMsEntry - 999999999; sleepMsEntry = sleepMsEntry - 999999999;
loops = 0; loops = 0;
interruped = true; interrupted = true;
} }
void flashled(uint8_t color, uint8_t brightnes) { void flashled(uint8_t color, uint8_t brightness) {
uint8_t colorred = (color >> 5) & 0b00000111; uint8_t colorred = (color >> 5) & 0b00000111;
uint8_t colorgreen = (color >> 2) & 0b00000111; uint8_t colorgreen = (color >> 2) & 0b00000111;
uint8_t colorblue = color & 0b00000011; uint8_t colorblue = color & 0b00000011;
for (uint16_t i = 0; i < brightnes; i++) { for (uint16_t i = 0; i < brightness; i++) {
digitalWrite(LED_RED, !(colorred >= 7)); digitalWrite(LED_RED, !(colorred >= 7));
digitalWrite(LED_GREEN, !(colorgreen >= 7)); digitalWrite(LED_GREEN, !(colorgreen >= 7));
digitalWrite(LED_BLUE, !(colorblue >= 3)); digitalWrite(LED_BLUE, !(colorblue >= 3));
@@ -248,11 +248,10 @@ void sleepwithinterrupts(uint32_t sleepinterval) {
void ledflashlogic(uint32_t ms) { void ledflashlogic(uint32_t ms) {
watchdog_enable(ms + 1000); watchdog_enable(ms + 1000);
uint8_t brightnes = ledcfg[0] >> 4 & 0b00001111; uint8_t brightness = ledcfg[0] >> 4 & 0b00001111;
uint8_t mode = ledcfg[0] & 0b00001111; uint8_t mode = ledcfg[0] & 0b00001111;
// lets not blink for short delays // lets not blink for short delays
if (ms < 2000) mode = 15; if (ms < 2000) mode = 15;
// if(mode == 0)sleepwithinterrupts(ms);
if (mode == 1) { if (mode == 1) {
uint8_t color = ledcfg[1]; uint8_t color = ledcfg[1];
uint32_t ledinerv = (ledcfg[2] << 24) + (ledcfg[3] << 16) + (ledcfg[4] << 8) + ledcfg[5]; uint32_t ledinerv = (ledcfg[2] << 24) + (ledcfg[3] << 16) + (ledcfg[4] << 8) + ledcfg[5];
@@ -264,11 +263,12 @@ void ledflashlogic(uint32_t ms) {
} }
if (sleepinterval > ms) sleepinterval = ms; if (sleepinterval > ms) sleepinterval = ms;
for (uint32_t i = 0; i < loops; i++) { for (uint32_t i = 0; i < loops; i++) {
flashled(color, brightnes); flashled(color, brightness);
sleepwithinterrupts(sleepinterval); sleepwithinterrupts(sleepinterval);
} }
} else if (mode == 0) { }
interruped = false; else if (mode == 0) {
interrupted = false;
uint8_t interloopdelayfactor = 100; uint8_t interloopdelayfactor = 100;
u_int8_t loopdelayfactor = 100; u_int8_t loopdelayfactor = 100;
uint8_t c1 = ledcfg[1]; uint8_t c1 = ledcfg[1];
@@ -290,36 +290,40 @@ void ledflashlogic(uint32_t ms) {
uint32_t looptimesum = fulllooptime1 + fulllooptime2 + fulllooptime3; uint32_t looptimesum = fulllooptime1 + fulllooptime2 + fulllooptime3;
int fittingrepeats = (int)ms / looptimesum; int fittingrepeats = (int)ms / looptimesum;
grouprepeats = fittingrepeats; //catch edge case
if (grouprepeats == 0) grouprepeats = 1; if (grouprepeats == 0) sleepwithinterrupts(ms);
for (int j = 0; j < grouprepeats; j++) { for (int j = 0; j < fittingrepeats; j++) {
if (!interruped) { if(j > grouprepeats){
brightness = 0;
ledcfg[0] = 0xff;
}
if (!interrupted) {
for (int i = 0; i < loopcnt1; i++) { for (int i = 0; i < loopcnt1; i++) {
flashled(c1, brightnes); flashled(c1, brightness);
sleepwithinterrupts(loop1delay * loopdelayfactor); sleepwithinterrupts(loop1delay * loopdelayfactor);
if (interruped) break; if (interrupted) break;
} }
sleepwithinterrupts(ildelay1 * interloopdelayfactor); sleepwithinterrupts(ildelay1 * interloopdelayfactor);
} }
if (!interruped) { if (!interrupted) {
for (int i = 0; i < loopcnt2; i++) { for (int i = 0; i < loopcnt2; i++) {
flashled(c2, brightnes); flashled(c2, brightness);
sleepwithinterrupts(loop2delay * loopdelayfactor); sleepwithinterrupts(loop2delay * loopdelayfactor);
if (interruped) break; if (interrupted) break;
} }
sleepwithinterrupts(ildelay2 * interloopdelayfactor); sleepwithinterrupts(ildelay2 * interloopdelayfactor);
} }
if (!interruped) { if (!interrupted) {
for (int i = 0; i < loopcnt3; i++) { for (int i = 0; i < loopcnt3; i++) {
flashled(c3, brightnes); flashled(c3, brightness);
sleepwithinterrupts(loop3delay * loopdelayfactor); sleepwithinterrupts(loop3delay * loopdelayfactor);
if (interruped) break; if (interrupted) break;
} }
sleepwithinterrupts(ildelay3 * interloopdelayfactor); sleepwithinterrupts(ildelay3 * interloopdelayfactor);
} }
if (interruped) break; if (interrupted) break;
} }
} else } else
sleepwithinterrupts(ms); sleepwithinterrupts(ms);

Binary file not shown.

View File

@@ -11,7 +11,7 @@ extern bool prepareDataAvail(String& filename, uint8_t dataType, uint8_t dataTyp
extern void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP); extern void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP);
extern void processXferComplete(struct espXferComplete* xfc, bool local); extern void processXferComplete(struct espXferComplete* xfc, bool local);
extern void processXferTimeout(struct espXferComplete* xfc, bool local); extern void processXferTimeout(struct espXferComplete* xfc, bool local);
extern void processDataReq(struct espAvailDataReq* adr, bool local); extern void processDataReq(struct espAvailDataReq* adr, bool local, IPAddress remoteIP = IPAddress(0, 0, 0, 0));
extern void processTagReturnData(struct espTagReturnData* trd, uint8_t len, bool local); extern void processTagReturnData(struct espTagReturnData* trd, uint8_t len, bool local);
extern bool sendTagCommand(const uint8_t* dst, uint8_t cmd, bool local, const uint8_t* payload = nullptr); extern bool sendTagCommand(const uint8_t* dst, uint8_t cmd, bool local, const uint8_t* payload = nullptr);

View File

@@ -33,6 +33,7 @@ class DynStorage {
bool isInited; bool isInited;
}; };
extern SemaphoreHandle_t fsMutex;
extern DynStorage Storage; extern DynStorage Storage;
extern fs::FS *contentFS; extern fs::FS *contentFS;
extern void copyFile(File in, File out); extern void copyFile(File in, File out);

View File

@@ -21,7 +21,7 @@
class tagRecord { class tagRecord {
public: public:
tagRecord() : mac{0}, alias(""), lastseen(0), nextupdate(0), contentMode(0), pending(false), md5{0}, md5pending{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0), isExternal(false), pendingIdle(0), hasCustomLUT(false), rotate(0), lut(0), tagSoftwareVersion(0), currentChannel(0), dataType(0), filename(""), data(nullptr), len(0) {} tagRecord() : mac{0}, alias(""), lastseen(0), nextupdate(0), contentMode(0), pending(false), md5{0}, md5pending{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0), isExternal(false), apIp(IPAddress(0, 0, 0, 0)), pendingIdle(0), hasCustomLUT(false), rotate(0), lut(0), tagSoftwareVersion(0), currentChannel(0), dataType(0), filename(""), data(nullptr), len(0) {}
uint8_t mac[8]; uint8_t mac[8];
String alias; String alias;
@@ -42,6 +42,7 @@ class tagRecord {
uint8_t capabilities; uint8_t capabilities;
uint32_t lastfullupdate; uint32_t lastfullupdate;
bool isExternal; bool isExternal;
IPAddress apIp;
uint16_t pendingIdle; uint16_t pendingIdle;
bool hasCustomLUT; bool hasCustomLUT;
uint8_t rotate; uint8_t rotate;
@@ -85,7 +86,6 @@ struct varStruct {
bool changed; bool changed;
}; };
// extern SemaphoreHandle_t tagDBOwner;
extern Config config; extern Config config;
extern std::vector<tagRecord*> tagDB; extern std::vector<tagRecord*> tagDB;
extern std::unordered_map<int, HwType> hwtype; extern std::unordered_map<int, HwType> hwtype;

View File

@@ -123,5 +123,26 @@ static bool isSleeping(int sleeptime1, int sleeptime2) {
} }
} }
class Timer {
public:
Timer(unsigned long interval) : interval_(interval), previousMillis_(0) {}
void setInterval(unsigned long interval) {
interval_ = interval;
}
bool doRun() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis_ >= interval_) {
previousMillis_ = currentMillis;
return true;
}
return false;
}
private:
unsigned long interval_;
unsigned long previousMillis_;
};
} // namespace util } // namespace util

View File

@@ -6,7 +6,6 @@
void init_web(); void init_web();
void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void doJsonUpload(AsyncWebServerRequest *request); void doJsonUpload(AsyncWebServerRequest *request);
extern void networkProcess(void *parameter);
void wsLog(String text); void wsLog(String text);
void wsErr(String text); void wsErr(String text);
void wsSendTaginfo(const uint8_t *mac, uint8_t syncMode); void wsSendTaginfo(const uint8_t *mac, uint8_t syncMode);

View File

@@ -148,9 +148,24 @@ void drawNew(const uint8_t mac[8], const bool buttonPressed, tagRecord *&taginfo
memset(&wifimac[6], 0, 2); memset(&wifimac[6], 0, 2);
const bool isAp = memcmp(mac, wifimac, 8) == 0; const bool isAp = memcmp(mac, wifimac, 8) == 0;
if ((taginfo->wakeupReason == WAKEUP_REASON_FIRSTBOOT || taginfo->wakeupReason == WAKEUP_REASON_WDT_RESET) && taginfo->contentMode == 0 && isAp) { if ((taginfo->wakeupReason == WAKEUP_REASON_FIRSTBOOT || taginfo->wakeupReason == WAKEUP_REASON_WDT_RESET) && taginfo->contentMode == 0) {
taginfo->contentMode = 21; if (isAp) {
taginfo->nextupdate = 0; taginfo->contentMode = 21;
taginfo->nextupdate = 0;
} else if (contentFS->exists("/tag_defaults.json")) {
DynamicJsonDocument doc(1000);
fs::File tagDefaults = contentFS->open("/tag_defaults.json", "r");
DeserializationError err = deserializeJson(doc, tagDefaults);
if (!err) {
if (doc.containsKey("contentMode")) {
taginfo->contentMode = doc["contentMode"];
}
if (doc.containsKey("modecfgjson")) {
taginfo->modeConfigJson = doc["modecfgjson"].as<String>();
}
}
tagDefaults.close();
}
} }
char hexmac[17]; char hexmac[17];
@@ -393,7 +408,7 @@ void drawNew(const uint8_t mac[8], const bool buttonPressed, tagRecord *&taginfo
if (!util::isEmptyOrNull(configFilename)) { if (!util::isEmptyOrNull(configFilename)) {
String configUrl = cfgobj["url"].as<String>(); String configUrl = cfgobj["url"].as<String>();
if (!util::isEmptyOrNull(configUrl)) { if (!util::isEmptyOrNull(configUrl)) {
StaticJsonDocument<1000> json; DynamicJsonDocument json(1000);
Serial.println("Get json url + file"); Serial.println("Get json url + file");
if (util::httpGetJson(configUrl, json, 1000)) { if (util::httpGetJson(configUrl, json, 1000)) {
if (getJsonTemplateFileExtractVariables(filename, configFilename, json, taginfo, imageParams)) { if (getJsonTemplateFileExtractVariables(filename, configFilename, json, taginfo, imageParams)) {
@@ -676,7 +691,7 @@ void drawWeather(String &filename, JsonObject &cfgobj, const tagRecord *taginfo,
units += "&temperature_unit=fahrenheit&windspeed_unit=mph"; units += "&temperature_unit=fahrenheit&windspeed_unit=mph";
} }
StaticJsonDocument<1000> doc; DynamicJsonDocument doc(1000);
const bool success = util::httpGetJson("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&windspeed_unit=ms&timezone=" + tz + units, doc, 5000); const bool success = util::httpGetJson("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_weather=true&windspeed_unit=ms&timezone=" + tz + units, doc, 5000);
if (!success) { if (!success) {
return; return;
@@ -842,11 +857,15 @@ int getImgURL(String &filename, String URL, time_t fetched, imgParam &imageParam
http.setTimeout(5000); // timeout in ms http.setTimeout(5000); // timeout in ms
const int httpCode = http.GET(); const int httpCode = http.GET();
if (httpCode == 200) { if (httpCode == 200) {
xSemaphoreTake(fsMutex, portMAX_DELAY);
File f = contentFS->open("/temp/temp.jpg", "w"); File f = contentFS->open("/temp/temp.jpg", "w");
if (f) { if (f) {
http.writeToStream(&f); http.writeToStream(&f);
f.close(); f.close();
xSemaphoreGive(fsMutex);
jpg2buffer("/temp/temp.jpg", filename, imageParams); jpg2buffer("/temp/temp.jpg", filename, imageParams);
} else {
xSemaphoreGive(fsMutex);
} }
} else { } else {
if (httpCode != 304) { if (httpCode != 304) {
@@ -1120,7 +1139,7 @@ void drawAPinfo(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
} }
TFT_eSprite spr = TFT_eSprite(&tft); TFT_eSprite spr = TFT_eSprite(&tft);
StaticJsonDocument<2048> loc; DynamicJsonDocument loc(2048);
getTemplate(loc, 21, taginfo->hwType); getTemplate(loc, 21, taginfo->hwType);
initSprite(spr, imageParams.width, imageParams.height, imageParams); initSprite(spr, imageParams.width, imageParams.height, imageParams);
@@ -1429,7 +1448,7 @@ void getLocation(JsonObject &cfgobj) {
filter["results"][0]["latitude"] = true; filter["results"][0]["latitude"] = true;
filter["results"][0]["longitude"] = true; filter["results"][0]["longitude"] = true;
filter["results"][0]["timezone"] = true; filter["results"][0]["timezone"] = true;
StaticJsonDocument<1000> doc; DynamicJsonDocument doc(1000);
if (util::httpGetJson("https://geocoding-api.open-meteo.com/v1/search?name=" + urlEncode(cfgobj["location"]) + "&count=1", doc, 5000, &filter)) { if (util::httpGetJson("https://geocoding-api.open-meteo.com/v1/search?name=" + urlEncode(cfgobj["location"]) + "&count=1", doc, 5000, &filter)) {
cfgobj["#lat"] = doc["results"][0]["latitude"].as<String>(); cfgobj["#lat"] = doc["results"][0]["latitude"].as<String>();
cfgobj["#lon"] = doc["results"][0]["longitude"].as<String>(); cfgobj["#lon"] = doc["results"][0]["longitude"].as<String>();
@@ -1493,7 +1512,7 @@ void prepareConfigFile(const uint8_t *dst, const JsonObject &config) {
void getTemplate(JsonDocument &json, const uint8_t id, const uint8_t hwtype) { void getTemplate(JsonDocument &json, const uint8_t id, const uint8_t hwtype) {
StaticJsonDocument<80> filter; StaticJsonDocument<80> filter;
StaticJsonDocument<2048> doc; DynamicJsonDocument doc(2048);
const String idstr = String(id); const String idstr = String(id);
constexpr const char *templateKey = "template"; constexpr const char *templateKey = "template";

View File

@@ -125,6 +125,7 @@ bool downloadAndWriteBinary(String &filename, const char *url) {
int binaryResponseCode = binaryHttp.GET(); int binaryResponseCode = binaryHttp.GET();
Serial.println(binaryResponseCode); Serial.println(binaryResponseCode);
if (binaryResponseCode == HTTP_CODE_OK) { if (binaryResponseCode == HTTP_CODE_OK) {
xSemaphoreTake(fsMutex, portMAX_DELAY);
File file = contentFS->open(filename, "wb"); File file = contentFS->open(filename, "wb");
if (file) { if (file) {
wsSerial("downloading " + String(filename)); wsSerial("downloading " + String(filename));
@@ -138,6 +139,7 @@ bool downloadAndWriteBinary(String &filename, const char *url) {
vTaskDelay(1 / portTICK_PERIOD_MS); vTaskDelay(1 / portTICK_PERIOD_MS);
} }
file.close(); file.close();
xSemaphoreGive(fsMutex);
binaryHttp.end(); binaryHttp.end();
file = contentFS->open(filename, "r"); file = contentFS->open(filename, "r");
@@ -150,6 +152,7 @@ bool downloadAndWriteBinary(String &filename, const char *url) {
file.close(); file.close();
} }
} else { } else {
xSemaphoreGive(fsMutex);
wsSerial("file open error " + String(filename)); wsSerial("file open error " + String(filename));
} }
} else { } else {

View File

@@ -175,7 +175,7 @@ bool flasher::getInfoBlockType() {
} }
bool flasher::findTagByMD5() { bool flasher::findTagByMD5() {
StaticJsonDocument<3000> doc; DynamicJsonDocument doc(3000);
DynamicJsonDocument APconfig(600); DynamicJsonDocument APconfig(600);
fs::File readfile = contentFS->open("/tag_md5_db.json", "r"); fs::File readfile = contentFS->open("/tag_md5_db.json", "r");
DeserializationError err = deserializeJson(doc, readfile); DeserializationError err = deserializeJson(doc, readfile);
@@ -205,7 +205,7 @@ bool flasher::findTagByMD5() {
} }
bool flasher::findTagByType(uint8_t type) { bool flasher::findTagByType(uint8_t type) {
StaticJsonDocument<3000> doc; DynamicJsonDocument doc(3000);
DynamicJsonDocument APconfig(600); DynamicJsonDocument APconfig(600);
fs::File readfile = contentFS->open("/tag_md5_db.json", "r"); fs::File readfile = contentFS->open("/tag_md5_db.json", "r");
DeserializationError err = deserializeJson(doc, readfile); DeserializationError err = deserializeJson(doc, readfile);
@@ -271,11 +271,13 @@ bool flasher::backupFlash() {
getFirmwareMD5(); getFirmwareMD5();
if (!zbs->select_flash(0)) return false; if (!zbs->select_flash(0)) return false;
md5char[16] = 0x00; md5char[16] = 0x00;
xSemaphoreTake(fsMutex, portMAX_DELAY);
fs::File backup = contentFS->open("/" + (String)md5char + "_backup.bin", "w", true); fs::File backup = contentFS->open("/" + (String)md5char + "_backup.bin", "w", true);
for (uint32_t c = 0; c < 65535; c++) { for (uint32_t c = 0; c < 65535; c++) {
backup.write(zbs->read_flash(c)); backup.write(zbs->read_flash(c));
} }
backup.close(); backup.close();
xSemaphoreGive(fsMutex);
return true; return true;
} }

View File

@@ -10,6 +10,7 @@
#include "storage.h" #include "storage.h"
#include "system.h" #include "system.h"
#include "tag_db.h" #include "tag_db.h"
#include "wifimanager.h"
#ifdef HAS_USB #ifdef HAS_USB
#include "usbflasher.h" #include "usbflasher.h"
@@ -21,6 +22,13 @@
#include "util.h" #include "util.h"
#include "web.h" #include "web.h"
util::Timer intervalSysinfo(3000);
util::Timer intervalVars(10000);
util::Timer intervalSaveDB(300000);
util::Timer intervalContentRunner(1000);
SET_LOOP_TASK_STACK_SIZE(16 * 1024);
void pinTest(); void pinTest();
void delayedStart(void* parameter) { void delayedStart(void* parameter) {
@@ -33,32 +41,6 @@ void delayedStart(void* parameter) {
vTaskDelete(NULL); vTaskDelete(NULL);
} }
void timeTask(void* parameter) {
wsSendSysteminfo();
util::printHeap();
while (1) {
unsigned long startMillis = millis();
time_t now;
time(&now);
if (now % 5 == 0 || apInfo.state != AP_STATE_ONLINE || config.runStatus != RUNSTATUS_RUN) {
wsSendSysteminfo();
}
if (now % 10 == 9 && config.runStatus != RUNSTATUS_STOP) {
checkVars();
}
if (now % 300 == 7 && config.runStatus != RUNSTATUS_STOP) {
saveDB("/current/tagDB.json");
}
if (apInfo.state == AP_STATE_ONLINE) {
contentRunner();
}
if (millis() - startMillis < 1000) {
vTaskDelay((1000 - millis() + startMillis) / portTICK_PERIOD_MS);
}
}
}
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
Serial.print(">\n"); Serial.print(">\n");
@@ -146,9 +128,7 @@ void setup() {
#endif #endif
loadDB("/current/tagDB.json"); loadDB("/current/tagDB.json");
cleanupCurrent(); cleanupCurrent();
// tagDBOwner = xSemaphoreCreateMutex();
xTaskCreate(APTask, "AP Process", 6000, NULL, 2, NULL); xTaskCreate(APTask, "AP Process", 6000, NULL, 2, NULL);
xTaskCreate(networkProcess, "Wifi", 6000, NULL, configMAX_PRIORITIES - 10, NULL);
vTaskDelay(10 / portTICK_PERIOD_MS); vTaskDelay(10 / portTICK_PERIOD_MS);
config.runStatus = RUNSTATUS_INIT; config.runStatus = RUNSTATUS_INIT;
@@ -158,26 +138,39 @@ void setup() {
config.runStatus = RUNSTATUS_PAUSE; config.runStatus = RUNSTATUS_PAUSE;
} }
xTaskCreate(timeTask, "timed tasks", 12000, NULL, 2, NULL);
xTaskCreate(initTime, "init time", 5000, NULL, 2, NULL); xTaskCreate(initTime, "init time", 5000, NULL, 2, NULL);
xTaskCreate(delayedStart, "delaystart", 2000, NULL, 2, NULL); xTaskCreate(delayedStart, "delaystart", 2000, NULL, 2, NULL);
wsSendSysteminfo();
util::printHeap();
} }
void loop() { void loop() {
vTaskDelay(10000 / portTICK_PERIOD_MS); ws.cleanupClients();
// performDeviceFlash(); wm.poll();
while (1) {
// pinTest(); if (intervalSysinfo.doRun()) {
while (1) { wsSendSysteminfo();
}
if (intervalVars.doRun() && config.runStatus != RUNSTATUS_STOP) {
checkVars();
}
if (intervalSaveDB.doRun() && config.runStatus != RUNSTATUS_STOP) {
saveDB("/current/tagDB.json");
}
if (intervalContentRunner.doRun() && apInfo.state == AP_STATE_ONLINE) {
contentRunner();
}
#ifdef YELLOW_IPS_AP #ifdef YELLOW_IPS_AP
extern void yellow_ap_display_loop(void); extern void yellow_ap_display_loop(void);
yellow_ap_display_loop(); yellow_ap_display_loop();
#else
vTaskDelay(10000 / portTICK_PERIOD_MS);
// pinTest();
#endif #endif
}
#ifdef OPENEPAPERLINK_PCB #ifdef OPENEPAPERLINK_PCB
time_t tagConnectTimer = 0;
if (millis() - tagConnectTimer > 1000) {
tagConnectTimer = millis();
if (extTagConnected()) { if (extTagConnected()) {
flashCountDown(3); flashCountDown(3);
@@ -190,7 +183,8 @@ void loop() {
pinMode(FLASHER_EXT_TEST, INPUT); pinMode(FLASHER_EXT_TEST, INPUT);
vTaskDelay(1000 / portTICK_PERIOD_MS); vTaskDelay(1000 / portTICK_PERIOD_MS);
} }
#endif
vTaskDelay(100 / portTICK_PERIOD_MS);
} }
} #endif
vTaskDelay(100 / portTICK_PERIOD_MS);
}

View File

@@ -217,6 +217,7 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
} }
#endif #endif
xSemaphoreTake(fsMutex, portMAX_DELAY);
fs::File f_out = contentFS->open(fileout, "w"); fs::File f_out = contentFS->open(fileout, "w");
switch (imageParams.bpp) { switch (imageParams.bpp) {
@@ -232,6 +233,8 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
if (!buffer) { if (!buffer) {
Serial.println("Failed to allocate buffer"); Serial.println("Failed to allocate buffer");
util::printLargestFreeBlock(); util::printLargestFreeBlock();
f_out.close();
xSemaphoreGive(fsMutex);
return; return;
} }
spr2color(spr, imageParams, buffer, buffer_size, false); spr2color(spr, imageParams, buffer, buffer_size, false);
@@ -251,5 +254,6 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
} }
f_out.close(); f_out.close();
xSemaphoreGive(fsMutex);
Serial.println("finished writing buffer " + String(millis() - t) + "ms"); Serial.println("finished writing buffer " + String(millis() - t) + "ms");
} }

View File

@@ -110,6 +110,7 @@ void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, const uint8
} else { } else {
sendDataAvail(&pending); sendDataAvail(&pending);
} }
wsSendTaginfo(dst, SYNC_TAGSTATUS); wsSendTaginfo(dst, SYNC_TAGSTATUS);
} }
@@ -240,18 +241,22 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) {
http.begin(imageUrl); http.begin(imageUrl);
int httpCode = http.GET(); int httpCode = http.GET();
if (httpCode == 200) { if (httpCode == 200) {
xSemaphoreTake(fsMutex, portMAX_DELAY);
File file = contentFS->open(filename, "w"); File file = contentFS->open(filename, "w");
http.writeToStream(&file); http.writeToStream(&file);
file.close(); file.close();
xSemaphoreGive(fsMutex);
} else if (httpCode == 404) { } else if (httpCode == 404) {
imageUrl = "http://" + remoteIP.toString() + "/current/" + String(hexmac) + ".raw"; imageUrl = "http://" + remoteIP.toString() + "/current/" + String(hexmac) + ".raw";
http.end(); http.end();
http.begin(imageUrl); http.begin(imageUrl);
httpCode = http.GET(); httpCode = http.GET();
if (httpCode == 200) { if (httpCode == 200) {
xSemaphoreTake(fsMutex, portMAX_DELAY);
File file = contentFS->open(filename, "w"); File file = contentFS->open(filename, "w");
http.writeToStream(&file); http.writeToStream(&file);
file.close(); file.close();
xSemaphoreGive(fsMutex);
} }
} }
http.end(); http.end();
@@ -430,7 +435,7 @@ void processXferTimeout(struct espXferComplete* xfc, bool local) {
if (local) udpsync.netProcessXferTimeout(xfc); if (local) udpsync.netProcessXferTimeout(xfc);
} }
void processDataReq(struct espAvailDataReq* eadr, bool local) { void processDataReq(struct espAvailDataReq* eadr, bool local, IPAddress remoteIP) {
if (config.runStatus == RUNSTATUS_STOP) return; if (config.runStatus == RUNSTATUS_STOP) return;
char buffer[64]; char buffer[64];
@@ -450,13 +455,15 @@ void processDataReq(struct espAvailDataReq* eadr, bool local) {
if (!local) { if (!local) {
if (taginfo->isExternal == false) { if (taginfo->isExternal == false) {
wsLog("moved AP from local to external " + String(hexmac)); wsLog("moved AP from local to external " + String(hexmac));
taginfo->isExternal = true;
} }
taginfo->isExternal = true; taginfo->apIp = remoteIP;
} else { } else {
if (taginfo->isExternal == true) { if (taginfo->isExternal == true) {
wsLog("moved AP from external to local " + String(hexmac)); wsLog("moved AP from external to local " + String(hexmac));
taginfo->isExternal = false;
} }
taginfo->isExternal = false; taginfo->apIp = IPAddress(0, 0, 0, 0);
} }
if (taginfo->pendingIdle == 0) { if (taginfo->pendingIdle == 0) {
@@ -508,9 +515,11 @@ void processDataReq(struct espAvailDataReq* eadr, bool local) {
// sprintf(buffer, "<REMOTE ADR %02X%02X%02X%02X%02X%02X%02X%02X\n\0", eadr->src[7], eadr->src[6], eadr->src[5], eadr->src[4], eadr->src[3], eadr->src[2], eadr->src[1], eadr->src[0]); // sprintf(buffer, "<REMOTE ADR %02X%02X%02X%02X%02X%02X%02X%02X\n\0", eadr->src[7], eadr->src[6], eadr->src[5], eadr->src[4], eadr->src[3], eadr->src[2], eadr->src[1], eadr->src[0]);
} }
wsSendTaginfo(eadr->src, SYNC_TAGSTATUS);
if (local) { if (local) {
wsSendTaginfo(eadr->src, SYNC_TAGSTATUS);
udpsync.netProcessDataReq(eadr); udpsync.netProcessDataReq(eadr);
} else {
wsSendTaginfo(eadr->src, SYNC_NOSYNC);
} }
} }
@@ -653,7 +662,7 @@ void updateTaginfoitem(struct TagInfo* taginfoitem, IPAddress remoteIP) {
char hexmac[17]; char hexmac[17];
mac2hex(taginfo->mac, hexmac); mac2hex(taginfo->mac, hexmac);
if (taginfo->contentMode != 12 && taginfoitem->contentMode != 12) { if (taginfo->contentMode != 12 && taginfoitem->contentMode != 12 && taginfoitem->contentMode != 0) {
wsLog("Remote AP at " + remoteIP.toString() + " takes control over tag " + String(hexmac)); wsLog("Remote AP at " + remoteIP.toString() + " takes control over tag " + String(hexmac));
taginfo->contentMode = 12; taginfo->contentMode = 12;
} }
@@ -712,11 +721,15 @@ bool checkMirror(struct tagRecord* taginfo, struct pendingData* pending) {
} else { } else {
char dst_path[64]; char dst_path[64];
sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X%02X%02X.pending\0", taginfo2->mac[7], taginfo2->mac[6], taginfo2->mac[5], taginfo2->mac[4], taginfo2->mac[3], taginfo2->mac[2], taginfo2->mac[1], taginfo2->mac[0]); sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X%02X%02X.pending\0", taginfo2->mac[7], taginfo2->mac[6], taginfo2->mac[5], taginfo2->mac[4], taginfo2->mac[3], taginfo2->mac[2], taginfo2->mac[1], taginfo2->mac[0]);
xSemaphoreTake(fsMutex, portMAX_DELAY);
File file = contentFS->open(dst_path, "w"); File file = contentFS->open(dst_path, "w");
if (file) { if (file) {
file.write(taginfo2->data, taginfo2->len); file.write(taginfo2->data, taginfo2->len);
file.close(); file.close();
xSemaphoreGive(fsMutex);
udpsync.netSendDataAvail(&pending2); udpsync.netSendDataAvail(&pending2);
} else {
xSemaphoreGive(fsMutex);
} }
} }

View File

@@ -94,6 +94,7 @@ void handleLittleFSUpload(AsyncWebServerRequest* request, String filename, size_
} else { } else {
path = request->getParam("path", true)->value(); path = request->getParam("path", true)->value();
Serial.println("update " + path); Serial.println("update " + path);
xSemaphoreTake(fsMutex, portMAX_DELAY);
request->_tempFile = contentFS->open(path, "w", true); request->_tempFile = contentFS->open(path, "w", true);
} }
} }
@@ -105,6 +106,7 @@ void handleLittleFSUpload(AsyncWebServerRequest* request, String filename, size_
} }
if (final) { if (final) {
request->_tempFile.close(); request->_tempFile.close();
xSemaphoreGive(fsMutex);
if (error) { if (error) {
request->send(507, "text/plain", "Error. Disk full?"); request->send(507, "text/plain", "Error. Disk full?");
} else { } else {
@@ -155,7 +157,6 @@ void updateFirmware(const char* url, const char* expectedMd5, const size_t size)
config.runStatus = RUNSTATUS_STOP; config.runStatus = RUNSTATUS_STOP;
vTaskDelay(3000 / portTICK_PERIOD_MS); vTaskDelay(3000 / portTICK_PERIOD_MS);
// xSemaphoreTake(tagDBOwner, portMAX_DELAY);
saveDB("/current/tagDB.json"); saveDB("/current/tagDB.json");
// destroyDB(); // destroyDB();
@@ -213,7 +214,6 @@ void updateFirmware(const char* url, const char* expectedMd5, const size_t size)
httpClient.end(); httpClient.end();
// loadDB("/current/tagDB.json"); // loadDB("/current/tagDB.json");
config.runStatus = RUNSTATUS_RUN; config.runStatus = RUNSTATUS_RUN;
// xSemaphoreGive(tagDBOwner);
} }
void handleRollback(AsyncWebServerRequest* request) { void handleRollback(AsyncWebServerRequest* request) {
@@ -303,7 +303,7 @@ void handleUpdateActions(AsyncWebServerRequest* request) {
request->send(200, "No update actions needed"); request->send(200, "No update actions needed");
return; return;
} }
StaticJsonDocument<1000> doc; DynamicJsonDocument doc(1000);
DeserializationError error = deserializeJson(doc, file); DeserializationError error = deserializeJson(doc, file);
const JsonArray deleteFiles = doc["deletefile"].as<JsonArray>(); const JsonArray deleteFiles = doc["deletefile"].as<JsonArray>();
for (const auto& filePath : deleteFiles) { for (const auto& filePath : deleteFiles) {

View File

@@ -10,6 +10,8 @@
DynStorage::DynStorage() : isInited(0) {} DynStorage::DynStorage() : isInited(0) {}
SemaphoreHandle_t fsMutex;
static void initLittleFS() { static void initLittleFS() {
LittleFS.begin(); LittleFS.begin();
contentFS = &LittleFS; contentFS = &LittleFS;
@@ -86,12 +88,15 @@ void copyBetweenFS(FS& sourceFS, const char* source_path, FS& targetFS) {
copyBetweenFS(sourceFS, file.path(), targetFS); copyBetweenFS(sourceFS, file.path(), targetFS);
} else { } else {
xSemaphoreTake(fsMutex, portMAX_DELAY);
File target = contentFS->open(file.path(), "w"); File target = contentFS->open(file.path(), "w");
if (target) { if (target) {
copyFile(file, target); copyFile(file, target);
target.close(); target.close();
file.close(); file.close();
xSemaphoreGive(fsMutex);
} else { } else {
xSemaphoreGive(fsMutex);
Serial.print("Couldn't create high target file"); Serial.print("Couldn't create high target file");
Serial.println(file.path()); Serial.println(file.path());
return; return;
@@ -100,10 +105,14 @@ void copyBetweenFS(FS& sourceFS, const char* source_path, FS& targetFS) {
file = root.openNextFile(); file = root.openNextFile();
} }
} else { } else {
xSemaphoreTake(fsMutex, portMAX_DELAY);
File target = contentFS->open(root.path(), "w"); File target = contentFS->open(root.path(), "w");
if (target) { if (target) {
copyFile(root, target); copyFile(root, target);
target.close();
xSemaphoreGive(fsMutex);
} else { } else {
xSemaphoreGive(fsMutex);
Serial.print("Couldn't create target file "); Serial.print("Couldn't create target file ");
Serial.println(root.path()); Serial.println(root.path());
return; return;
@@ -120,6 +129,7 @@ void copyIfNeeded(const char* path) {
#endif #endif
void DynStorage::begin() { void DynStorage::begin() {
fsMutex = xSemaphoreCreateMutex();
initLittleFS(); initLittleFS();
#ifdef HAS_SDCARD #ifdef HAS_SDCARD

View File

@@ -19,7 +19,6 @@ std::unordered_map<int, HwType> hwdata = {
{2, {400, 300, 0, 2}}}; {2, {400, 300, 0, 2}}};
Config config; Config config;
// SemaphoreHandle_t tagDBOwner;
tagRecord* tagRecord::findByMAC(const uint8_t mac[8]) { tagRecord* tagRecord::findByMAC(const uint8_t mac[8]) {
for (tagRecord* tag : tagDB) { for (tagRecord* tag : tagDB) {
@@ -116,6 +115,7 @@ void fillNode(JsonObject& tag, const tagRecord* taginfo) {
tag["capabilities"] = taginfo->capabilities; tag["capabilities"] = taginfo->capabilities;
tag["modecfgjson"] = taginfo->modeConfigJson; tag["modecfgjson"] = taginfo->modeConfigJson;
tag["isexternal"] = taginfo->isExternal; tag["isexternal"] = taginfo->isExternal;
tag["apip"] = taginfo->apIp.toString();
tag["rotate"] = taginfo->rotate; tag["rotate"] = taginfo->rotate;
tag["lut"] = taginfo->lut; tag["lut"] = taginfo->lut;
tag["ch"] = taginfo->currentChannel; tag["ch"] = taginfo->currentChannel;
@@ -128,9 +128,11 @@ void saveDB(const String& filename) {
const long t = millis(); const long t = millis();
Storage.begin(); Storage.begin();
xSemaphoreTake(fsMutex, portMAX_DELAY);
fs::File file = contentFS->open(filename, "w"); fs::File file = contentFS->open(filename, "w");
if (!file) { if (!file) {
Serial.println("saveDB: Failed to open file"); Serial.println("saveDB: Failed to open file");
xSemaphoreGive(fsMutex);
return; return;
} }
@@ -149,6 +151,7 @@ void saveDB(const String& filename) {
file.write(']'); file.write(']');
file.close(); file.close();
xSemaphoreGive(fsMutex);
Serial.println("DB saved " + String(millis() - t) + "ms"); Serial.println("DB saved " + String(millis() - t) + "ms");
} }
@@ -168,7 +171,7 @@ void loadDB(const String& filename) {
bool parsing = true; bool parsing = true;
if (readfile.find("[")) { if (readfile.find("[")) {
StaticJsonDocument<1000> doc; DynamicJsonDocument doc(1000);
while (parsing) { while (parsing) {
DeserializationError err = deserializeJson(doc, readfile); DeserializationError err = deserializeJson(doc, readfile);
if (!err) { if (!err) {
@@ -207,6 +210,7 @@ void loadDB(const String& filename) {
taginfo->capabilities = tag["capabilities"]; taginfo->capabilities = tag["capabilities"];
taginfo->modeConfigJson = tag["modecfgjson"].as<String>(); taginfo->modeConfigJson = tag["modecfgjson"].as<String>();
taginfo->isExternal = tag["isexternal"].as<bool>(); taginfo->isExternal = tag["isexternal"].as<bool>();
taginfo->apIp.fromString(tag["apip"].as<String>());
taginfo->rotate = tag["rotate"] | 0; taginfo->rotate = tag["rotate"] | 0;
taginfo->lut = tag["lut"] | 0; taginfo->lut = tag["lut"] | 0;
taginfo->currentChannel = tag["ch"] | 0; taginfo->currentChannel = tag["ch"] | 0;
@@ -313,6 +317,7 @@ void initAPconfig() {
} }
void saveAPconfig() { void saveAPconfig() {
xSemaphoreTake(fsMutex, portMAX_DELAY);
fs::File configFile = contentFS->open("/current/apconfig.json", "w"); fs::File configFile = contentFS->open("/current/apconfig.json", "w");
DynamicJsonDocument APconfig(500); DynamicJsonDocument APconfig(500);
APconfig["channel"] = config.channel; APconfig["channel"] = config.channel;
@@ -328,6 +333,7 @@ void saveAPconfig() {
APconfig["sleeptime2"] = config.sleepTime2; APconfig["sleeptime2"] = config.sleepTime2;
serializeJsonPretty(APconfig, configFile); serializeJsonPretty(APconfig, configFile);
configFile.close(); configFile.close();
xSemaphoreGive(fsMutex);
} }
HwType getHwType(const uint8_t id) { HwType getHwType(const uint8_t id) {

View File

@@ -50,7 +50,7 @@ void UDPcomm::processPacket(AsyncUDPPacket packet) {
espAvailDataReq adr; espAvailDataReq adr;
memset(&adr, 0, sizeof(espAvailDataReq)); memset(&adr, 0, sizeof(espAvailDataReq));
memcpy(&adr, &packet.data()[1], std::min(packet.length() - 1, sizeof(espAvailDataReq))); memcpy(&adr, &packet.data()[1], std::min(packet.length() - 1, sizeof(espAvailDataReq)));
processDataReq(&adr, false); processDataReq(&adr, false, senderIP);
break; break;
} }
case PKT_XFER_COMPLETE: { case PKT_XFER_COMPLETE: {

View File

@@ -35,15 +35,6 @@ WifiManager wm;
SemaphoreHandle_t wsMutex; SemaphoreHandle_t wsMutex;
uint32_t lastssidscan = 0; uint32_t lastssidscan = 0;
void networkProcess(void *parameter) {
wsMutex = xSemaphoreCreateMutex();
while (true) {
ws.cleanupClients();
wm.poll();
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
void wsLog(String text) { void wsLog(String text) {
StaticJsonDocument<250> doc; StaticJsonDocument<250> doc;
doc["logMsg"] = text; doc["logMsg"] = text;
@@ -77,20 +68,22 @@ void wsSendSysteminfo() {
time_t now; time_t now;
time(&now); time(&now);
static int freeSpaceLastRun = 0; static int freeSpaceLastRun = 0;
static size_t tagDBsize = 0;
static size_t freeSpace = Storage.freeSpace(); static size_t freeSpace = Storage.freeSpace();
sys["currtime"] = now; sys["currtime"] = now;
sys["heap"] = ESP.getFreeHeap(); sys["heap"] = ESP.getFreeHeap();
sys["recordcount"] = tagDB.size(); sys["recordcount"] = tagDBsize;
sys["dbsize"] = dbSize(); sys["dbsize"] = dbSize();
if (millis() - freeSpaceLastRun > 30000) { if (millis() - freeSpaceLastRun > 30000 || freeSpaceLastRun == 0) {
freeSpace = Storage.freeSpace(); freeSpace = Storage.freeSpace();
tagDBsize = tagDB.size();
freeSpaceLastRun = millis(); freeSpaceLastRun = millis();
} }
sys["littlefsfree"] = freeSpace; sys["littlefsfree"] = freeSpace;
sys["apstate"] = apInfo.state; sys["apstate"] = apInfo.state;
sys["runstate"] = config.runStatus; sys["runstate"] = config.runStatus;
#if !defined(CONFIG_IDF_TARGET_ESP32) #if !defined(CONFIG_IDF_TARGET_ESP32)
sys["temp"] = temperatureRead(); // sys["temp"] = temperatureRead();
#endif #endif
sys["rssi"] = WiFi.RSSI(); sys["rssi"] = WiFi.RSSI();
sys["wifistatus"] = WiFi.status(); sys["wifistatus"] = WiFi.status();
@@ -188,6 +181,7 @@ uint8_t wsClientCount() {
} }
void init_web() { void init_web() {
wsMutex = xSemaphoreCreateMutex();
Storage.begin(); Storage.begin();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
@@ -322,6 +316,7 @@ void init_web() {
} }
if (strcmp(cmdValue, "ledflash") == 0) { if (strcmp(cmdValue, "ledflash") == 0) {
struct ledFlash flashData = {0}; struct ledFlash flashData = {0};
flashData.mode = 1;
flashData.flashDuration = 8; flashData.flashDuration = 8;
flashData.color1 = 0x3C; // green flashData.color1 = 0x3C; // green
flashData.color2 = 0xE4; // red flashData.color2 = 0xE4; // red
@@ -338,6 +333,7 @@ void init_web() {
} }
if (strcmp(cmdValue, "ledflash_long") == 0) { if (strcmp(cmdValue, "ledflash_long") == 0) {
struct ledFlash flashData = {0}; struct ledFlash flashData = {0};
flashData.mode = 1;
flashData.flashDuration = 15; flashData.flashDuration = 15;
flashData.color1 = 0xE4; // red flashData.color1 = 0xE4; // red
flashData.flashCount1 = 5; flashData.flashCount1 = 5;
@@ -347,6 +343,12 @@ void init_web() {
const uint8_t *payload = reinterpret_cast<const uint8_t *>(&flashData); const uint8_t *payload = reinterpret_cast<const uint8_t *>(&flashData);
sendTagCommand(mac, CMD_DO_LEDFLASH, !taginfo->isExternal, payload); sendTagCommand(mac, CMD_DO_LEDFLASH, !taginfo->isExternal, payload);
} }
if (strcmp(cmdValue, "ledflash_stop") == 0) {
struct ledFlash flashData = {0};
flashData.mode = 0;
const uint8_t *payload = reinterpret_cast<const uint8_t *>(&flashData);
sendTagCommand(mac, CMD_DO_LEDFLASH, !taginfo->isExternal, payload);
}
request->send(200, "text/plain", "Ok, done"); request->send(200, "text/plain", "Ok, done");
} else { } else {
request->send(400, "text/plain", "Error: mac not found"); request->send(400, "text/plain", "Error: mac not found");
@@ -360,6 +362,7 @@ void init_web() {
server.on("/led_flash", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/led_flash", HTTP_GET, [](AsyncWebServerRequest *request) {
// color picker: https://roger-random.github.io/RGB332_color_wheel_three.js/ // color picker: https://roger-random.github.io/RGB332_color_wheel_three.js/
// http GET to /led_flash?mac=000000000000&pattern=3/0x1C,4,5/0xE0,3,1/0x4F,5,10/5 // http GET to /led_flash?mac=000000000000&pattern=3/0x1C,4,5/0xE0,3,1/0x4F,5,10/5
// http://192.168.178.198/led_flash?mac=00007E1F250CB29C&pattern=1/0x1C,1,15/0xE0,1,15/0x4F,1,15/1
// (flashDuration/color1,flashCount1,delay1/color2,flashCount2,delay2/color3,flashCount3,delay3/repeats) // (flashDuration/color1,flashCount1,delay1/color2,flashCount2,delay2/color3,flashCount3,delay3/repeats)
if (request->hasParam("mac")) { if (request->hasParam("mac")) {
String dst = request->getParam("mac")->value(); String dst = request->getParam("mac")->value();
@@ -597,6 +600,9 @@ void init_web() {
request->send(404); request->send(404);
}); });
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "content-type");
server.begin(); server.begin();
} }
@@ -611,6 +617,7 @@ void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index
} else { } else {
filename = "unknown.jpg"; filename = "unknown.jpg";
} }
xSemaphoreTake(fsMutex, portMAX_DELAY);
request->_tempFile = contentFS->open("/" + filename, "w"); request->_tempFile = contentFS->open("/" + filename, "w");
} }
if (len) { if (len) {
@@ -618,6 +625,7 @@ void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index
} }
if (final) { if (final) {
request->_tempFile.close(); request->_tempFile.close();
xSemaphoreGive(fsMutex);
if (request->hasParam("mac", true)) { if (request->hasParam("mac", true)) {
String dst = request->getParam("mac", true)->value(); String dst = request->getParam("mac", true)->value();
uint8_t mac[8]; uint8_t mac[8];
@@ -656,13 +664,16 @@ void doJsonUpload(AsyncWebServerRequest *request) {
String dst = request->getParam("mac", true)->value(); String dst = request->getParam("mac", true)->value();
uint8_t mac[8]; uint8_t mac[8];
if (hex2mac(dst, mac)) { if (hex2mac(dst, mac)) {
xSemaphoreTake(fsMutex, portMAX_DELAY);
File file = LittleFS.open("/current/" + dst + ".json", "w"); File file = LittleFS.open("/current/" + dst + ".json", "w");
if (!file) { if (!file) {
request->send(400, "text/plain", "Failed to create file"); request->send(400, "text/plain", "Failed to create file");
xSemaphoreGive(fsMutex);
return; return;
} }
file.print(request->getParam("json", true)->value()); file.print(request->getParam("json", true)->value());
file.close(); file.close();
xSemaphoreGive(fsMutex);
tagRecord *taginfo = tagRecord::findByMAC(mac); tagRecord *taginfo = tagRecord::findByMAC(mac);
if (taginfo != nullptr) { if (taginfo != nullptr) {
uint32_t ttl = 0; uint32_t ttl = 0;

View File

@@ -215,7 +215,6 @@ void WifiManager::pollSerial() {
} }
} }
// temporary write some more debug info
void WifiManager::WiFiEvent(WiFiEvent_t event) { void WifiManager::WiFiEvent(WiFiEvent_t event) {
Serial.printf("[WiFi-event %d] ", event); Serial.printf("[WiFi-event %d] ", event);
String eventname=""; String eventname="";
@@ -259,8 +258,8 @@ void WifiManager::WiFiEvent(WiFiEvent_t event) {
default: default:
break; break;
} }
terminalLog(eventname); if (eventname) terminalLog(eventname);
logLine("WiFi event [" + String(event) + "]: " + eventname); // logLine("WiFi event [" + String(event) + "]: " + eventname);
} }
// *** Improv // *** Improv

View File

@@ -8,15 +8,419 @@
<title>Open EPaper Link Access Point</title> <title>Open EPaper Link Access Point</title>
<link rel="stylesheet" href="main.css" type="text/css" /> <link rel="stylesheet" href="main.css" type="text/css" />
<link rel="icon" type="image/vnd.icon" href="favicon.ico"> <link rel="icon" type="image/vnd.icon" href="favicon.ico">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
</head> </head>
<body> <body>
<header> <header>
<div class="logo">Open EPaper Link Access Point</div> <div class="logo">Open EPaper Link Access Point</div>
<nav>
<div>
<!-- tabs -->
<div class="tab-container">
<div class="tablinks material-symbols-outlined" data-target="hometab" title="Dashboard">home</div>
<div class="tablinks material-symbols-outlined" data-target="tagtab" title="Tags">sell</div>
<div class="tablinks material-symbols-outlined" data-target="aptab" title="Access Points">cell_tower
</div>
<!--<div class="tablinks material-symbols-outlined" data-target="templatetab" title="Templates">browse
</div>-->
<div class="tablinks material-symbols-outlined" data-target="configtab" title="Settings">settings
</div>
<div class="tablinks material-symbols-outlined" data-target="logtab" title="Logging">text_snippet
</div>
</div>
<!-- /tabs -->
<div><span id="runstate"></div>
<div><span id="apstatecolor">&#11044;</span> <span id="apstate">loading</span></div>
<div><a href="/edit" target="littlefs" class="filebutton material-symbols-outlined">folder_open</a>
</div>
</div>
</nav>
</header> </header>
<div id="configbox"> <form>
<div class="closebtn">&#10006;</div> <div class="container">
<div class="window">
<div id="hometab" class="tabcontent">
<table>
<tr onclick="setFilterAndShow('')">
<td class="material-symbols-outlined" style="color:#239f26" id="dashboardStatusIcon">
check_circle
</td>
<td id="dashboardStatus" style="color:#239f26" colspan="2">
initialising...
</td>
</tr>
<tr onclick="setFilterAndShow('')">
<td class="material-symbols-outlined" style="color:#77239e">
sell
</td>
<td>
tags
</td>
<td id="dashboardTagCount" style="color:#77239e">
0
</td>
</tr>
<tr onclick="setFilterAndShow('pending')">
<td class="material-symbols-outlined" style="color:#235f9e">
hourglass_empty
</td>
<td>
pending data
</td>
<td id="dashboardPending" style="color:#235f9e">
0
</td>
</tr>
<tr onclick="setFilterAndShow('lowbatt')">
<td class="material-symbols-outlined" style="color:#9e9223">
battery_low
</td>
<td>
low battery
</td>
<td id="dashboardLowBatt" style="color:#9e9223">
0
</td>
</tr>
<tr onclick="setFilterAndShow('inactive')">
<td class="material-symbols-outlined" style="color:#9e2323">
signal_disconnected
</td>
<td>
timeout
</td>
<td id="dashboardTimeout" style="color:#9e2323">
0
</td>
</tr>
<!--
<tr onclick="$(`[data-target='aptab']`).click()">
<td class="material-symbols-outlined" style="color:#239f26">
cell_tower
</td>
<td>
access points
</td>
<td id="dashboardApCount" style="color:#239f26">
</td>
</tr>
-->
</table>
</div>
<div id="tagtab" class="tabcontent">
<div class="tagheader">
<h3>Currently active tags</h3>
<div id="activefilter"></div><button class="material-symbols-outlined"
id="toggleFilters">filter_alt</button>
</div>
<div id="filterOptions">
<div>
<div>group by</div>
<div>
<input type="radio" name="group" value="" id="rnone" checked><label
for="rnone">None</label>
</div>
<div>
<input type="radio" name="group" value="model" id="rtagtype"><label for="rtagtype">Tag
model</label>
</div>
<div>
<input type="radio" name="group" value="contentmode" id="rcontent"><label
for="rcontent">Content</label>
</div>
<div>
<input type="radio" name="group" value="data-channel" id="rchannel"><label
for="rchannel">Channel</label>
</div>
</div>
<div>
<div>sort by</div>
<div>
<input type="radio" name="sort" value="alias" id="ralias" checked><label
for="ralias">Alias</label>
</div>
<div>
<input type="radio" name="sort" value="mac" id="rmac"><label for="rmac">Mac</label>
</div>
<div>
<input type="radio" name="sort" value="data-lastseen" id="rlastseen"><label
for="rlastseen">Last seen</label>
</div>
<div>
<input type="radio" name="sort" value="data-nextupdate" id="rnext"><label
for="rnext">Next update</label>
</div>
</div>
<div>
<div>filter</div>
<div>
<input type="checkbox" name="filter" value="local" id="rlocal"><label
for="rlocal">local</label>
</div>
<div>
<input type="checkbox" name="filter" value="remote" id="rremote"><label
for="rremote">remote</label>
</div>
<div>
<input type="checkbox" name="filter" value="inactive" id="rinactive"><label
for="rinactive">timed out</label>
</div>
<div>
<input type="checkbox" name="filter" value="pending" id="rpending"><label
for="rpending">pending</label>
</div>
<div>
<input type="checkbox" name="filter" value="lowbatt" id="rlowbatt"><label
for="rlowbatt">low battery</label>
</div>
</div>
</div>
<div id="taglist" class="taglist">
<div class="tagcard" id="tagtemplate">
<div class="currimg"><canvas class="tagimg"></div>
<div class="mac"></div>
<div class="alias"></div>
<div class="model"></div>
<div class="received"></div>
<div class="contentmode"></div>
<div class="lastseen"></div>
<div class="nextcheckin"></div>
<div class="nextupdate"></div>
<div class="corner">
<div class="pendingicon" title="A new message is waiting for the tag to pick up">
&circlearrowright;</div>
<div class="warningicon" title="This tag has not been seen for a long time">&#9888;
</div>
</div>
</div>
</div>
</div>
<div id="logtab" class="tabcontent">
<div class="tabheader">
<div><img id="clearlog" src="data:image/gif;base64,R0lGODlhEAAQAPMAANXV1e3t7d/f39HR0dvb2/Hx8dTU1OLi4urq6mZmZpmZmf///wAAAAAAAAAAAAAAACH5BAEAAAwALAAAAAAQABAAAARBkMlJq71Yrp3ZXkr4WWCYnOZSgQVyEMYwJCq1nHhe20qgCAoA7QLyAYU7njE4JPV+zOSkCEUSFbmTVPPpbjvgTAQAOw==
"></div>
<div><input type="checkbox" id="showdebug" value="1"><label for="showdebug">Show all websocket
traffic</label></div>
</div>
<ul id="messages" class="messages">
</ul>
</div>
<div id="aptab" class="tabcontent">
<h3>Active access points</h3>
<div id="aplist">
<div id="apcard" class="apcard">
<p class="apip">194.109.6.66</p>
<p class="apalias">AP kitchen</p>
<div>
<span class="material-symbols-outlined">
sell
</span>
<span class="aptagcount">13</span>
<span class="material-symbols-outlined space">
cell_tower
</span>
<span class="apchannel">25</span>
</div>
<p class="apswversion">
fetching software version...
</p>
</div>
</div>
</div>
<div id="templatetab" class="tabcontent">
Work in progress...
</div>
<div id="configtab" class="tabcontent">
<h3>Access Point config</h3>
<p>
<label for="apcfgalias">Alias</label>
<input id="apcfgalias" type="text">
</p>
<p>
<label for="apcfgchid">Channel</label>
<select id="apcfgchid">
<option value="0" selected>auto</option>
<option value="11">11</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
<option value="26">26</option>
<option value="27">27</option>
</select>
</p>
<p>
<label for="apcfgledbrightness">LED brightness</label>
<select id="apcfgledbrightness">
<option value="-1">off</option>
<option value="20">10%</option>
<option value="64">25%</option>
<option value="128" selected>50%</option>
<option value="192">75%</option>
<option value="255">100%</option>
</select>
</p>
<p>
<label for="apcfglanguage">Content language</label>
<select id="apcfglanguage">
<option value="0" selected>EN English</option>
<option value="1">NL Nederlands</option>
<option value="2">DE Deutsch</option>
</select>
</p>
<p title="Depending on the content, a tag can sleep for
longer periods when no updates are expected
(like a date display). This setting specifies
the maximum sleep time.">
<label for="apclatency">Maximum sleep</label>
<select id="apclatency">
<option value="0" selected>shortest (40 sec)</option>
<option value="5">5 minutes</option>
<option value="10">10 minute</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
</select>
</p>
<p title="If connected to the website, don't sleep extra.
Latency will be around 40 seconds.">
<label for="apcpreventsleep">Shorten latency during config</label>
<select id="apcpreventsleep">
<option value="0">no</option>
<option value="1" selected>yes</option>
</select>
</p>
<p
title="Stops updates at night, and put the tags to sleep. During the configured night time, this overrides the maximum sleep time.">
<label for="apcnight1">No updates between</label>
<select id="apcnight1"></select>
<span style="align-self:center;">and</span>
<select id="apcnight2"></select>
</p>
<p title="Turn off preview images on the webpage if you want to manage many tags,
to save file system space">
<label for="apcpreview">Preview images</label>
<select id="apcpreview">
<option value="1" selected>yes</option>
<option value="0">no</option>
</select>
</p>
<p title="Wifi transmit power">
<label for="apcwifipower">Wifi power</label>
<select id="apcwifipower">
<option value="78">19.5 dBm</option>
<option value="76">19.0 dBm</option>
<option value="74">18.5 dBm</option>
<option value="68">17.0 dBm</option>
<option value="60">15.0 dBm</option>
<option value="52">13.0 dBm</option>
<option value="44">11.0 dBm</option>
<option value="34" selected>8.5 dBm</option>
<option value="28">7.0 dBm</option>
<option value="20">5.0 dBm</option>
<option value="8">2.0 dBm</option>
</select>
</p>
<p title="Your local time zone">
<label for="apctimezone">Local time zone</label>
<select id="apctimezone">
<optgroup label="Europe">
<option value="CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" selected>Central European
Time</option>
<option value="EET-2EEST-3,M3.5.0/03:00:00,M10.5.0/04:00:00">Athens, Greece</option>
<option value="GMT+0IST-1,M3.5.0/01:00:00,M10.5.0/02:00:00">Dublin, Ireland</option>
<option value="EET-2EEST-3,M3.5.0/03:00:00,M10.5.0/04:00:00">Helsinki, Finland</option>
<option value="WET-0WEST-1,M3.5.0/01:00:00,M10.5.0/02:00:00">Lisbon, Portugal</option>
<option value="GMT+0BST-1,M3.5.0/01:00:00,M10.5.0/02:00:00">London, Great Britain
</option>
<option value="EET-2EEST,M3.5.0/3,M10.5.0/4">Kyiv, Ukraine</option>
</optgroup>
<optgroup label="USA / Canada">
<option value="HAW10">Hawaii Time</option>
<option value="AKST9AKDT">Alaska Time</option>
<option value="PST8PDT">Pacific Time</option>
<option value="MST7MDT">Mountain Time</option>
<option value="MST7">Arizona, no DST</option>
<option value="CST6CDT">Central Time</option>
<option value="EST5EDT">Eastern Time</option>
</optgroup>
<optgroup label="Australia / New Zealand">
<option value="EST-10EDT-11,M10.5.0/02:00:00,M3.5.0/03:00:00">Melbourne, Sydney</option>
<option value="WST-8">Perth</option>
<option value="EST-10">Brisbane</option>
<option value="CST-9:30CDT-10:30,M10.5.0/02:00:00,M3.5.0/03:00:00">Adelaide</option>
<option value="CST-9:30">Darwin</option>
<option value="EST-10EDT-11,M10.1.0/02:00:00,M3.5.0/03:00:00">Hobart</option>
<option value="NZST-12NZDT-13,M9.4.0/02:00:00,M4.1.0/03:00:00">New Zealand</option>
</optgroup>
<optgroup label="Asia">
<option value="JST-9">Tokyo</option>
<option value="WIB-7">Jakarta</option>
<option value="GMT+2">Jerusalem</option>
<option value="SGT-8">Singapore</option>
<option value="ULAT-8ULAST,M3.5.0/2,M9.5.0/2">Ulaanbaatar, Mongolia</option>
</optgroup>
<optgroup label="Central and South America">
<option value="BRST+3BRDT+2,M10.3.0,M2.3.0">Brazil, Sao Paulo</option>
<option value="UTC+3">Argentina</option>
<option value="CST+6">Central America</option>
</optgroup>
</select>
</p>
<p>
<input type="button" value="Save" id="apcfgsave"><span id="apcfgmsg"></span>
</p>
<h3>Manage</h3>
<p>
<button id="rebootbutton">Reboot AP</button> Saves the tagDB and instantly reboots the Access
Point
</p>
<p>
<a href="/backup_db" id="downloadDBbutton">Download tagDB</a>
</p>
<p>
<button id="updatebutton">Update</button> Manage firmware of the ESP32
</p>
<p>
<a href="/setup" target="setup" class="wifibutton">WiFi config</a> Opens a new window with WiFi
config options
</p>
<p>
<br>
<a href="https://github.com/jjwbruijn/OpenEPaperLink" target="_new">Github
OpenEPaperLink</a><br>
<a href="https://github.com/jjwbruijn/OpenEPaperLink/wiki" target="_new">OpenEPaperLink
Wiki</a><br>
</p>
</div>
</div>
</div>
</form>
<footer class="logbox">
<p>
<span>&nbsp;</span>
<span id="sysinfo"></span>
</p>
</footer>
<!-- -->
<dialog id="configbox">
<div class="closebtn2">&#10006;</div>
<h3 id="cfgmac">00000000</h3> <h3 id="cfgmac">00000000</h3>
<p> <p>
<label for="cfgalias">Alias</label> <label for="cfgalias">Alias</label>
@@ -50,183 +454,33 @@
<button id="cfgscan">scan</button> <button id="cfgscan">scan</button>
<button id="cfgdeepsleep">deep sleep</button> <button id="cfgdeepsleep">deep sleep</button>
<button id="cfgreset">reset settings</button> <button id="cfgreset">reset settings</button>
<button id="cfgdelete" title="remove"><img src="data:image/gif;base64,R0lGODlhEAAQAPMAANXV1e3t7d/f39HR0dvb2/Hx8dTU1OLi4urq6mZmZpmZmf///wAAAAAAAAAAAAAAACH5BAEAAAwALAAAAAAQABAAAARBkMlJq71Yrp3ZXkr4WWCYnOZSgQVyEMYwJCq1nHhe20qgCAoA7QLyAYU7njE4JPV+zOSkCEUSFbmTVPPpbjvgTAQAOw== "></button> <button id="cfgdelete" title="remove"><img
src="data:image/gif;base64,R0lGODlhEAAQAPMAANXV1e3t7d/f39HR0dvb2/Hx8dTU1OLi4urq6mZmZpmZmf///wAAAAAAAAAAAAAAACH5BAEAAAwALAAAAAAQABAAAARBkMlJq71Yrp3ZXkr4WWCYnOZSgQVyEMYwJCq1nHhe20qgCAoA7QLyAYU7njE4JPV+zOSkCEUSFbmTVPPpbjvgTAQAOw== "></button>
</p> </p>
</div> </div>
<p id="savebar"> <p id="savebar">
<span><input type="button" value="Save" id="cfgsave"></span> <span><input type="button" value="Save" id="cfgsave"></span>
<span id="cfgmore" title="advanced options">&#x25BC;</span> <span id="cfgmore" title="advanced options">&#x25BC;</span>
</p> </p>
</div> </dialog>
<div id="apconfigbox">
<div class="closebtn">&#10006;</div>
<h3>Access Point config</h3>
<p>
<label for="apcfgalias">Alias</label>
<input id="apcfgalias" type="text">
</p>
<p>
<label for="apcfgchid">Channel</label>
<select id="apcfgchid">
<option value="0" selected>auto</option>
<option value="11">11</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
<option value="26">26</option>
<option value="27">27</option>
</select>
</p>
<p>
<label for="apcfgledbrightness">LED brightness</label>
<select id="apcfgledbrightness">
<option value="-1">off</option>
<option value="20">10%</option>
<option value="64">25%</option>
<option value="128" selected>50%</option>
<option value="192">75%</option>
<option value="255">100%</option>
</select>
</p>
<p>
<label for="apcfglanguage">Content language</label>
<select id="apcfglanguage">
<option value="0" selected>EN English</option>
<option value="1">NL Nederlands</option>
<option value="2">DE Deutsch</option>
</select>
</p>
<p title="Depending on the content, a tag can sleep for
longer periods when no updates are expected
(like a date display). This setting specifies
the maximum sleep time.">
<label for="apclatency">Maximum sleep</label>
<select id="apclatency">
<option value="0" selected>shortest (40 sec)</option>
<option value="5">5 minutes</option>
<option value="10">10 minute</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
</select>
</p>
<p title="If connected to the website, don't sleep extra.
Latency will be around 40 seconds.">
<label for="apcpreventsleep">Shorten latency during config</label>
<select id="apcpreventsleep">
<option value="0">no</option>
<option value="1" selected>yes</option>
</select>
</p>
<p title="Stops updates at night, and put the tags to sleep. During the configured night time, this overrides the maximum sleep time.">
<label for="apcnight1">No updates between</label>
<select id="apcnight1"></select>
<span style="align-self:center;">and</span>
<select id="apcnight2"></select>
</p>
<p title="Turn off preview images on the webpage if you want to manage many tags,
to save file system space">
<label for="apcpreview">Preview images</label>
<select id="apcpreview">
<option value="1" selected>yes</option>
<option value="0">no</option>
</select>
</p>
<p title="Wifi transmit power">
<label for="apcwifipower">Wifi power</label>
<select id="apcwifipower">
<option value="78">19.5 dBm</option>
<option value="76">19.0 dBm</option>
<option value="74">18.5 dBm</option>
<option value="68">17.0 dBm</option>
<option value="60">15.0 dBm</option>
<option value="52">13.0 dBm</option>
<option value="44">11.0 dBm</option>
<option value="34" selected>8.5 dBm</option>
<option value="28">7.0 dBm</option>
<option value="20">5.0 dBm</option>
<option value="8">2.0 dBm</option>
</select>
</p>
<p title="Your local time zone">
<label for="apctimezone">Local time zone</label>
<select id="apctimezone">
<optgroup label="Europe">
<option value="CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" selected>Central European Time</option>
<option value="EET-2EEST-3,M3.5.0/03:00:00,M10.5.0/04:00:00">Athens, Greece</option>
<option value="GMT+0IST-1,M3.5.0/01:00:00,M10.5.0/02:00:00">Dublin, Ireland</option>
<option value="EET-2EEST-3,M3.5.0/03:00:00,M10.5.0/04:00:00">Helsinki, Finland</option>
<option value="WET-0WEST-1,M3.5.0/01:00:00,M10.5.0/02:00:00">Lisbon, Portugal</option>
<option value="GMT+0BST-1,M3.5.0/01:00:00,M10.5.0/02:00:00">London, Great Britain</option>
<option value="EET-2EEST,M3.5.0/3,M10.5.0/4">Kyiv, Ukraine</option>
</optgroup>
<optgroup label="USA / Canada">
<option value="HAW10">Hawaii Time</option>
<option value="AKST9AKDT">Alaska Time</option>
<option value="PST8PDT">Pacific Time</option>
<option value="MST7MDT">Mountain Time</option>
<option value="MST7">Arizona, no DST</option>
<option value="CST6CDT">Central Time</option>
<option value="EST5EDT">Eastern Time</option>
</optgroup>
<optgroup label="Australia / New Zealand">
<option value="EST-10EDT-11,M10.5.0/02:00:00,M3.5.0/03:00:00">Melbourne, Sydney</option>
<option value="WST-8">Perth</option>
<option value="EST-10">Brisbane</option>
<option value="CST-9:30CDT-10:30,M10.5.0/02:00:00,M3.5.0/03:00:00">Adelaide</option>
<option value="CST-9:30">Darwin</option>
<option value="EST-10EDT-11,M10.1.0/02:00:00,M3.5.0/03:00:00">Hobart</option>
<option value="NZST-12NZDT-13,M9.4.0/02:00:00,M4.1.0/03:00:00">New Zealand</option>
</optgroup>
<optgroup label="Asia">
<option value="JST-9">Tokyo</option>
<option value="WIB-7">Jakarta</option>
<option value="GMT+2">Jerusalem</option>
<option value="SGT-8">Singapore</option>
<option value="ULAT-8ULAST,M3.5.0/2,M9.5.0/2">Ulaanbaatar, Mongolia</option>
</optgroup>
<optgroup label="Central and South America">
<option value="BRST+3BRDT+2,M10.3.0,M2.3.0">Brazil, Sao Paulo</option>
<option value="UTC+3">Argentina</option>
<option value="CST+6">Central America</option>
</optgroup>
</select>
</p>
<p>
<input type="button" value="Save" id="apcfgsave">
</p>
<p>
Active access points:<br>
<table id="aptable">
<tr>
<th>ip</th>
<th>alias</th>
<th>tags</th>
<th>ch</th>
<th>AP ver</th>
</tr>
</table>
</p>
<p>
<span id="rebootbutton">reboot AP</span>
<a href="/backup_db" id="downloadDBbutton">download tagDB</a>
<span id="updatebutton">update</span>
<a href="/setup" target="setup" class="filebutton">WiFi config</a>
</p>
<p>
<a href="https://github.com/jjwbruijn/OpenEPaperLink" target="_new">Github OpenEPaperLink</a>
</p>
</div>
<div id="apupdatebox"> <div id="apupdatebox">
<div class="closebtn">&#10006;</div> <div class="closebtn">&#10006;</div>
<h3>Update dashboard</h3> <h3>Update dashboard</h3>
<div id="easyupdate"></div> <div id="easyupdate"></div>
<div id="advanceddiv"> <div id="advanceddiv">
<!--<div>
repo: <input type="text" name="repo"> <button id="switchRepo">Switch</button><br>
environment: <span id="environment">environment</span>
</div>-->
<div id="releasetable"></div> <div id="releasetable"></div>
<div> <div>
<div id="rollbackOption" style="display:none"><button id="rollbackBtn">Rollback to previous firmware</button></div> <div id="rollbackOption" style="display:none"><button id="rollbackBtn">Rollback to previous
<span id="c6Option"><div id="updateC6Option"><button id="updateC6Btn">Update ESP32-C6</button> <input type="checkbox" value="1" checked id="c6download"> download latest version</div></span> firmware</button></div>
<span id="c6Option">
<div id="updateC6Option"><button id="updateC6Btn">Update ESP32-C6</button> <input type="checkbox"
value="1" checked id="c6download"> download latest version</div>
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -235,84 +489,8 @@ to save file system space">
style="display: none; position: absolute; background: white; border: 1px solid gray; padding: 0; list-style: none;"> style="display: none; position: absolute; background: white; border: 1px solid gray; padding: 0; list-style: none;">
</ul> </ul>
<form>
<div class="container">
<div class="window">
<div class="actionbox">
<div>
<div>Currently active tags: <button class="filebutton" id="toggleFilters">arrange</button></div>
<div><span id="temp"></div>
<div><span id="runstate"></div>
<div><span id="apstatecolor">&#11044;</span> <span id="apstate">loading</span></div>
<div><span id="apconfigbutton">AP config</span></div>
<div><a href="/edit" target="littlefs" class="filebutton">File System</a></div>
</div>
<div id="filterOptions">
<div>
<div>group by</div>
<div><input type="radio" name="group" value="" id="rnone" checked><label for="rnone">None</label></div>
<div><input type="radio" name="group" value="model" id="rtagtype"><label for="rtagtype">Tag model</label></div>
<div><input type="radio" name="group" value="contentmode" id="rcontent"><label for="rcontent">Content</label></div>
<div><input type="radio" name="group" value="data-channel" id="rchannel"><label for="rchannel">Channel</label></div>
</div>
<div>
<div>sort by</div>
<div><input type="radio" name="sort" value="alias" id="ralias" checked><label for="ralias">Alias</label></div>
<div><input type="radio" name="sort" value="mac" id="rmac"><label for="rmac">Mac</label></div>
<div><input type="radio" name="sort" value="data-lastseen" id="rlastseen"><label for="rlastseen">Last seen</label></div>
<div><input type="radio" name="sort" value="data-nextupdate" id="rnext"><label for="rnext">Next update</label></div>
</div>
<div>
<div>filter</div>
<div><input type="checkbox" name="filter" value="local" id="rlocal"><label for="rlocal">only local</label></div>
<div><input type="checkbox" name="filter" value="remote" id="rremote"><label for="rremote">only remote</label></div>
<div><input type="checkbox" name="filter" value="inactive" id="rinactive"><label for="rinactive">only inactive</label></div>
<div><input type="checkbox" name="filter" value="pending" id="rpending"><label for="rpending">only pending</label>
</div>
</div>
</div>
</div>
<div id="taglist" class="taglist">
<div class="tagcard" id="tagtemplate">
<div class="currimg"><canvas class="tagimg"></div>
<div class="mac"></div>
<div class="alias"></div>
<div class="model"></div>
<div class="received"></div>
<div class="contentmode"></div>
<div class="lastseen"></div>
<div class="nextcheckin"></div>
<div class="nextupdate"></div>
<div class="corner">
<div class="pendingicon" title="A new message is waiting for the tag to pick up">&circlearrowright;</div>
<div class="warningicon" title="This tag has not been seen for a long time">&#9888;</div>
</div>
</div>
</div>
<div class="logbox">
<p>
<span>logging</span>
<span><img id="clearlog" src="data:image/gif;base64,R0lGODlhEAAQAPMAANXV1e3t7d/f39HR0dvb2/Hx8dTU1OLi4urq6mZmZpmZmf///wAAAAAAAAAAAAAAACH5BAEAAAwALAAAAAAQABAAAARBkMlJq71Yrp3ZXkr4WWCYnOZSgQVyEMYwJCq1nHhe20qgCAoA7QLyAYU7njE4JPV+zOSkCEUSFbmTVPPpbjvgTAQAOw==
"></span>
<span id="sysinfo"></span>
</p>
<ul id="messages" class="messages">
</ul>
</div>
</div>
</div>
</form>
<script src="main.js"></script> <script src="main.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,7 @@
* { * {
margin:0; margin: 0;
padding:0; padding: 0;
border:0; border: 0;
list-style-type: none; list-style-type: none;
outline: none; outline: none;
font-weight: 400; font-weight: 400;
@@ -12,7 +12,8 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
html, body { html,
body {
height: 100%; height: 100%;
} }
@@ -24,60 +25,263 @@ body {
} }
header { header {
height: 50px;
background-color: #646260; background-color: #646260;
z-index: 999;
position: sticky;
top: 0px;
width: 100%;
} }
label { nav>div:first-child>div:first-child {
width: 120px;
display: inline-block;
vertical-align: top;
padding: 3px 0px;
}
.logo {
margin: 0 auto;
height: 50px;
text-indent: 50px;
overflow:hidden;
font-size: 2.5em;
color: white;
}
.window {
margin: 0 auto;
max-width: 94%;
}
.actionbox {
margin: 5px;
}
.actionbox>div:first-child>div:first-child {
flex-grow: 2; flex-grow: 2;
} }
.actionbox>div { nav>div {
display:flex; display: flex;
gap: 20px; gap: 20px;
padding: 10px; padding: 10px;
background-color: white; background-color: white;
} }
.actionbox label { nav label {
padding: 0px 5px; padding: 0px 5px;
vertical-align: text-bottom; vertical-align: text-bottom;
width: auto; width: auto;
cursor: pointer; cursor: pointer;
} }
#rebootbutton, #updatebutton, #downloadDBbutton, #apconfigbutton, .filebutton { footer {
padding: 2px 5px; padding: 5px;
background-color: #cccccc; position: fixed;
bottom: 0;
width: 100%;
background-color: white;
#sysinfo {
float: right;
}
}
.logo {
margin: 0 auto;
height: 50px;
text-indent: 50px;
overflow: hidden;
font-size: 2.5em;
color: white;
}
h3 {
padding-bottom: 10px;
font-size: 1.5em;
}
/* tabs */
.tab-container {
display: flex;
gap: 1rem;
}
.tablinks {
background-color: #f2f2f2;
padding: 5px 10px;
border: none;
cursor: pointer;
transition: background-color 0.3s;
transition: all 0.2s ease-in-out;
&:hover {
background-color: #ddd;
}
}
.tab-container .active {
background-color: #ccc;
margin-top: 10px;
margin-bottom: -10px;
padding-bottom: 0px;
}
.tabcontent {
display: none;
border-top: 1px solid #ccc;
}
label {
width: 200px;
display: inline-block;
vertical-align: top;
padding: 4px 20px;
}
.container {
padding-bottom: 20px;
;
}
#hometab {
font-size: 24px;
& table {
margin: 20px;
background: #fff;
padding: 5px 20px;
border-spacing: 0px;
}
& td {
padding: 10px 12px;
}
& td:nth-child(3) {
text-align: right;
font-size: 32px;
}
& tr:hover {
background-color: #ccc;
cursor: pointer;
}
& .material-symbols-outlined {
font-size: 48px;
}
}
.tagheader {
width: 100%;
padding: 10px 10px;
display: flex;
gap: 2em;
justify-content: space-between;
& #toggleFilters {
padding: 0px 10px;
}
& h3 {
padding-bottom: 0px;
}
& #activefilter {
flex-grow: 2;
align-self: center;
}
}
.tabheader {
background-color: white;
padding: 5px 10px;
display: flex;
gap: 2em;
}
#filterOptions {
background-color: white;
max-height: 0;
padding: 0px 10px;
overflow: hidden;
transition: all 0.3s ease-in-out;
display: flex;
gap: 1rem;
&.active {
padding: 10px 10px;
max-height: 100px;
margin-bottom: 10px;
}
& label {
width: inherit;
display: inline-block;
vertical-align: top;
padding: 0px 5px;
}
}
#aptab,
#configtab {
padding: 10px;
& p {
padding: 3px;
}
}
#aplist {
display: flex;
gap: 1em;
flex-flow: wrap;
}
.apcard {
background-color: #fff;
border: 1px solid #ccc;
padding: 10px;
width: 300px;
& .apalias {
font-size: 1.5em;
}
& .space {
padding-left: 20px;
}
& div {
display: flex;
font-size: 2.0em;
align-items: center;
gap: 0.2em;
}
}
#apcard {
display: none;
}
#apcfgsave {
margin: 20px 205px;
}
.filebutton {
width: inherit;
padding: 5px 10px;
margin-bottom: 0px;
margin-top: -1px;
background-color: #ccc;
text-decoration: none; text-decoration: none;
color: black; color: black;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
text-align: center;
&:hover {
background-color: #aaa;
}
}
#rebootbutton,
#updatebutton,
#downloadDBbutton,
.wifibutton {
padding: 4px 5px;
background-color: #ccc;
text-decoration: none;
color: black;
cursor: pointer;
white-space: nowrap;
width: 120px;
margin: 2px 5px 2px 20px;
display: inline-block;
text-align: center;
&:hover {
background-color: #aaa;
}
} }
.columns div { .columns div {
@@ -100,15 +304,17 @@ input {
border-radius: 0px; border-radius: 0px;
} }
input[type=button], button { input[type=button],
button {
border: 0px; border: 0px;
padding: 4px 10px; padding: 4px 10px;
cursor:pointer; cursor: pointer;
background-color: #ccc;
} }
input[type=button]:hover, input[type=button]:hover,
button:hover { button:hover {
background-color:#aaaaaa; background-color: #aaaaaa;
} }
select { select {
@@ -117,7 +323,8 @@ select {
border: solid 1px #cccccc; border: solid 1px #cccccc;
} }
#configbox, #apconfigbox, #apupdatebox { #apconfigbox,
#apupdatebox {
display: none; display: none;
position: fixed; position: fixed;
top: 65px; top: 65px;
@@ -131,19 +338,35 @@ select {
max-height: calc(100vh - 75px); max-height: calc(100vh - 75px);
} }
#configbox p, #apconfigbox p, #apupdatebox p { #configbox {
margin: auto;
min-width: 380px;
padding: 15px;
background-color: #f0e6d3;
z-index: 999;
box-shadow: 7px 10px 52px -19px rgba(0, 0, 0, 0.63);
overflow: auto;
max-height: calc(100vh - 75px);
}
#configbox p,
#apconfigbox p,
#apupdatebox p {
padding: 5px; padding: 5px;
display: flex; display: flex;
gap: 5px; gap: 5px;
align-items: flex-start; align-items: flex-start;
} }
#configbox h3, #apconfigbox h3, #apupdatebox h3 { #configbox h3,
#apconfigbox h3,
#apupdatebox h3 {
font-size: 1.5em; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
#configbox input, #apconfigbox input { #configbox input,
#apconfigbox input {
border: solid 1px #cccccc; border: solid 1px #cccccc;
} }
@@ -191,31 +414,6 @@ select {
background-color: #e6f0d3; background-color: #e6f0d3;
} }
#aptable {
width: 100%;
border-spacing: 0;
}
#aptable th {
text-align: left;
background-color: #00000020;
padding: 0px 3px;
}
#aptable th, #aptable td {
border-right: 1px solid #000010;
padding: 0px 3px;
}
#aptable td:nth-child(1), #aptable th:nth-child(1) {
border-left: 1px solid #000010;
}
#aptable td:nth-child(3),
#aptable td:nth-child(4) {
text-align: right;
}
#apupdatebox { #apupdatebox {
background-color: #f0d0c8; background-color: #f0d0c8;
width: 700px; width: 700px;
@@ -228,7 +426,8 @@ select {
padding: 2px 10px; padding: 2px 10px;
} }
.closebtn { .closebtn,
.closebtn2 {
border: 1px solid black; border: 1px solid black;
float: right; float: right;
width: 19px; width: 19px;
@@ -239,22 +438,21 @@ select {
cursor: pointer; cursor: pointer;
} }
.logbox { #logtab {
margin: 5px; & img {
} vertical-align: bottom;
cursor: pointer;
}
.logbox p { & label {
background-color: #ffffff; width: inherit;
padding: 5px 10px; vertical-align: top;
} padding: 4px 10px;
}
.logbox img { & input {
vertical-align: bottom; vertical-align: text-bottom;
cursor:pointer; }
}
.logbox #sysinfo {
float: right;
} }
.blink-red { .blink-red {
@@ -265,10 +463,11 @@ select {
.taglist { .taglist {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0px 10px 30px 10px;
} }
#tagtemplate { #tagtemplate {
display:none; display: none;
} }
.tagcard { .tagcard {
@@ -280,12 +479,12 @@ select {
background-color: #ffffff; background-color: #ffffff;
border: 1px solid #cccccc; border: 1px solid #cccccc;
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease;
}
.tagcard:hover { &:hover {
cursor:pointer; cursor: pointer;
box-shadow: 7px 10px 52px -19px rgba(0, 0, 0, 0.63); box-shadow: 7px 10px 52px -19px rgba(0, 0, 0, 0.63);
filter: brightness(1.02); filter: brightness(1.02);
}
} }
.tagflash { .tagflash {
@@ -305,30 +504,19 @@ select {
margin: 0px 5px; margin: 0px 5px;
} }
#filterOptions {
max-height: 0;
padding: 0 10px;
overflow: hidden;
transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out;
}
#filterOptions.active {
max-height: 100px;
padding: 10px 10px;
}
.currimg { .currimg {
float: right; float: right;
} }
.currimg img, .currimg canvas { .currimg img,
.currimg canvas {
max-width: 50px; max-width: 50px;
border: 1px solid #c0c0c0; border: 1px solid #c0c0c0;
} }
.mac { .mac {
font-size: 0.9em; font-size: 0.9em;
cursor:pointer; cursor: pointer;
} }
.alias { .alias {
@@ -344,6 +532,7 @@ select {
font-size: .85em; font-size: .85em;
padding-bottom: 5px; padding-bottom: 5px;
} }
.received div { .received div {
display: inline-block; display: inline-block;
} }
@@ -354,16 +543,19 @@ select {
padding-bottom: 5px; padding-bottom: 5px;
} }
.lastseen, .nextcheckin, .nextupdate { .lastseen,
.nextcheckin,
.nextupdate {
font-size: 0.9em; font-size: 0.9em;
} }
.lastseen span, .lastseen span,
.nextcheckin span, .nextcheckin span,
.nextupdate span { .nextupdate span {
width:105px; width: 105px;
display:inline-block; display: inline-block;
} }
.corner { .corner {
position: absolute; position: absolute;
right: 0px; right: 0px;
@@ -383,7 +575,7 @@ select {
} }
.warningicon { .warningicon {
display:none; display: none;
font-size: 1.3em; font-size: 1.3em;
background-color: yellow; background-color: yellow;
color: black; color: black;
@@ -412,12 +604,24 @@ ul.messages li.new {
color: red; color: red;
} }
.mono {
font-family: monospace;
word-break: break-all;
background-color: #666;
color: #ccc;
padding: 2px;
}
.quote {
color: white;
}
#paintbutton { #paintbutton {
padding: 1px 3px; padding: 1px 3px;
border: 1px solid black; border: 1px solid black;
font-size: 1.3em; font-size: 1.3em;
vertical-align: top; vertical-align: top;
margin-left:12px; margin-left: 12px;
cursor: pointer; cursor: pointer;
} }
@@ -472,6 +676,7 @@ ul.messages li.new {
background-color: #dddddd; background-color: #dddddd;
width: 40px; width: 40px;
} }
#buttonbar button { #buttonbar button {
font-size: 1.2em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
@@ -513,7 +718,7 @@ ul.messages li.new {
/* updatescreens */ /* updatescreens */
#easyupdate{ #easyupdate {
margin-top: 10px; margin-top: 10px;
} }
@@ -578,7 +783,7 @@ ul.messages li.new {
.console { .console {
width: 100%; width: 100%;
background-color: black; background-color: black;
font-family: 'lucida console','ui-monospace'; font-family: 'lucida console', 'ui-monospace';
color: white; color: white;
padding: 5px 10px; padding: 5px 10px;
margin: 20px 0px; margin: 20px 0px;
@@ -587,6 +792,7 @@ ul.messages li.new {
overflow-y: scroll; overflow-y: scroll;
white-space: break-spaces; white-space: break-spaces;
} }
.console div { .console div {
word-break: break-all; word-break: break-all;
} }
@@ -594,36 +800,62 @@ ul.messages li.new {
/* media */ /* media */
@media(max-width: 460px) { @media(max-width: 460px) {
.messages li div, ul.messages li div.date, ul.messages li div.message {
display:block; .messages li div,
position:relative; ul.messages li div.date,
ul.messages li div.message {
display: block;
position: relative;
padding: 0; padding: 0;
left: auto; left: auto;
} }
.messages li div.message, li.pending {
.messages li div.message,
li.pending {
margin-bottom: 8px; margin-bottom: 8px;
} }
ul.messages { ul.messages {
padding-bottom: 4px; padding-bottom: 4px;
} }
} }
@keyframes new { @keyframes new {
0% { background-color: rgba(255, 255, 204, 1); } 0% {
50% { background-color: rgba(255, 255, 204, .5); } background-color: rgba(255, 255, 204, 1);
100% { background-color: rgba(255, 255, 204, 0); } }
50% {
background-color: rgba(255, 255, 204, .5);
}
100% {
background-color: rgba(255, 255, 204, 0);
}
} }
@keyframes tagflash { @keyframes tagflash {
0% { opacity: 1; } 0% {
50% { opacity: 0; } opacity: 1;
100% { opacity: 1; } }
50% {
opacity: 0;
}
100% {
opacity: 1;
}
} }
@keyframes pending { @keyframes pending {
0% { } 0% {}
50% { background-color: #d4d4f5;}
100% { } 50% {
background-color: #d4d4f5;
}
100% {}
} }
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
@@ -638,7 +870,7 @@ ul.messages li.new {
min-height: 200px; min-height: 200px;
} }
.logbox #sysinfo { footer #sysinfo {
float: none; float: none;
display: block; display: block;
} }
@@ -682,4 +914,4 @@ ul.messages li.new {
padding: 1px 1px; padding: 1px 1px;
} }
} }

View File

@@ -11,6 +11,8 @@ const WAKEUP_REASON_NETWORK_SCAN = 0xFD;
const WAKEUP_REASON_WDT_RESET = 0xFE; const WAKEUP_REASON_WDT_RESET = 0xFE;
let tagTypes = {}; let tagTypes = {};
let apConfig = {};
let tagDB = {};
const apstate = [ const apstate = [
{ state: "offline", color: "red" }, { state: "offline", color: "red" },
@@ -38,7 +40,22 @@ let socket;
let finishedInitialLoading = false; let finishedInitialLoading = false;
let getTagtypeBusy = false; let getTagtypeBusy = false;
const loadConfig = new Event("loadConfig");
window.addEventListener("loadConfig", function () {
fetch("/get_ap_config")
.then(response => response.json())
.then(data => {
apConfig = data;
if (data.alias) {
$(".logo").innerHTML = data.alias;
this.document.title = data.alias;
}
});
});
window.addEventListener("load", function () { window.addEventListener("load", function () {
initTabs();
fetch('/content_cards.json') fetch('/content_cards.json')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
@@ -51,19 +68,38 @@ window.addEventListener("load", function () {
console.error('Error:', error); console.error('Error:', error);
alert("I can't load /www/content_cards.json.\r\nHave you upload it to the data partition?"); alert("I can't load /www/content_cards.json.\r\nHave you upload it to the data partition?");
}); });
fetch("/get_ap_config")
.then(response => response.json()) window.dispatchEvent(loadConfig);
.then(data => {
if (data.alias) {
$(".logo").innerHTML = data.alias;
this.document.title = data.alias;
}
});
dropUpload(); dropUpload();
populateTimes($('#apcnight1')); populateTimes($('#apcnight1'));
populateTimes($('#apcnight2')); populateTimes($('#apcnight2'));
}); });
/* tabs */
let activeTab = '';
function initTabs() {
const tabLinks = document.querySelectorAll(".tablinks");
const tabContents = document.querySelectorAll(".tabcontent");
tabLinks.forEach(tabLink => {
tabLink.addEventListener("click", function () {
const targetId = this.getAttribute("data-target");
const loadTabEvent = new CustomEvent('loadTab', { detail: targetId });
document.dispatchEvent(loadTabEvent);
tabContents.forEach(tabContent => {
tabContent.style.display = "none";
});
tabLinks.forEach(link => {
link.classList.remove("active");
});
document.getElementById(targetId).style.display = "block";
this.classList.add("active");
});
});
tabLinks[0].click();
};
function loadTags(pos) { function loadTags(pos) {
fetch("/get_db?pos=" + pos) fetch("/get_db?pos=" + pos)
.then(response => response.json()) .then(response => response.json())
@@ -83,7 +119,10 @@ function connect() {
}); });
socket.addEventListener("message", (event) => { socket.addEventListener("message", (event) => {
// console.log(event.data) if ($('#showdebug').checked) {
showMessage(event.data);
console.log(event.data);
}
const msg = JSON.parse(event.data); const msg = JSON.parse(event.data);
if (msg.logMsg) { if (msg.logMsg) {
showMessage(msg.logMsg, false); showMessage(msg.logMsg, false);
@@ -104,17 +143,12 @@ function connect() {
$("#apstatecolor").style.color = apstate[msg.sys.apstate].color; $("#apstatecolor").style.color = apstate[msg.sys.apstate].color;
$("#apstate").innerHTML = apstate[msg.sys.apstate].state; $("#apstate").innerHTML = apstate[msg.sys.apstate].state;
$("#runstate").innerHTML = runstate[msg.sys.runstate].state; $("#runstate").innerHTML = runstate[msg.sys.runstate].state;
if (msg.sys.temp) $("#temp").innerHTML = msg.sys.temp.toFixed(1) + '°C'; $('#dashboardStatus').innerHTML = apstate[msg.sys.apstate].state;
} }
servertimediff = (Date.now() / 1000) - msg.sys.currtime; servertimediff = (Date.now() / 1000) - msg.sys.currtime;
} }
if (msg.apitem) { if (msg.apitem) {
let row = $("#aptable").insertRow(); populateAPCard(msg.apitem);
row.insertCell(0).innerHTML = "<a href=\"http://" + msg.apitem.ip + "\" target=\"_new\">" + msg.apitem.ip + "</a>";
row.insertCell(1).innerHTML = msg.apitem.alias;
row.insertCell(2).innerHTML = msg.apitem.count;
row.insertCell(3).innerHTML = msg.apitem.channel;
row.insertCell(4).innerHTML = msg.apitem.version;
} }
if (msg.console) { if (msg.console) {
if (otamodule && typeof (otamodule.print) === "function") { if (otamodule && typeof (otamodule.print) === "function") {
@@ -146,6 +180,7 @@ function convertSize(bytes) {
function processTags(tagArray) { function processTags(tagArray) {
for (const element of tagArray) { for (const element of tagArray) {
const tagmac = element.mac; const tagmac = element.mac;
tagDB[tagmac] = element;
let div = $('#tag' + tagmac); let div = $('#tag' + tagmac);
if (div == null) { if (div == null) {
@@ -207,13 +242,18 @@ function processTags(tagArray) {
$('#tag' + tagmac + ' .received').style.opacity = "0"; $('#tag' + tagmac + ' .received').style.opacity = "0";
} }
if (element.contentMode == 20) { if (!apConfig.preview || element.contentMode == 20) {
$('#tag' + tagmac + ' .tagimg').style.display = 'none'; $('#tag' + tagmac + ' .tagimg').style.display = 'none'
} else if (div.dataset.hash != element.hash && div.dataset.hwtype > -1 && (!element.isexternal || element.contentMode != 12)) { } else if (div.dataset.hash != element.hash && div.dataset.hwtype > -1) {
loadImage(tagmac, '/current/' + tagmac + '.raw?' + element.hash); let cachetag = element.hash;
if (element.hash == '00000000000000000000000000000000') cachetag = Math.random();
if (element.isexternal && element.contentMode == 12) {
loadImage(tagmac, 'http://' + tagDB[tagmac].apip + '/current/' + tagmac + '.raw?' + cachetag);
} else {
loadImage(tagmac, '/current/' + tagmac + '.raw?' + cachetag);
}
div.dataset.hash = element.hash; div.dataset.hash = element.hash;
} }
if (element.isexternal && element.contentMode == 12) $('#tag' + tagmac + ' .tagimg').style.display = 'none';
if (element.nextupdate > 1672531200 && element.nextupdate != 3216153600) { if (element.nextupdate > 1672531200 && element.nextupdate != 3216153600) {
const date = new Date(element.nextupdate * 1000); const date = new Date(element.nextupdate * 1000);
@@ -284,9 +324,16 @@ function processTags(tagArray) {
function updatecards() { function updatecards() {
if (servertimediff > 1000000000) servertimediff = 0; if (servertimediff > 1000000000) servertimediff = 0;
let tagcount = 0;
let pendingcount = 0;
let timeoutcount = 0;
let lowbattcount = 0;
$('#taglist').querySelectorAll('[data-mac]').forEach(item => { $('#taglist').querySelectorAll('[data-mac]').forEach(item => {
let tagmac = item.dataset.mac; let tagmac = item.dataset.mac;
tagcount++;
if (tagDB[tagmac].pending) pendingcount++;
if (tagDB[tagmac].batteryMv < 2400 && tagDB[tagmac].batteryMv != 0 && tagDB[tagmac].batteryMv != 1337) lowbattcount++;
if (item.dataset.lastseen && item.dataset.lastseen > (Date.now() / 1000) - servertimediff - 30 * 24 * 3600 * 60) { if (item.dataset.lastseen && item.dataset.lastseen > (Date.now() / 1000) - servertimediff - 30 * 24 * 3600 * 60) {
let idletime = (Date.now() / 1000) - servertimediff - item.dataset.lastseen; let idletime = (Date.now() / 1000) - servertimediff - item.dataset.lastseen;
$('#tag' + tagmac + ' .lastseen').innerHTML = "<span>last seen</span>" + displayTime(Math.floor(idletime)) + " ago"; $('#tag' + tagmac + ' .lastseen').innerHTML = "<span>last seen</span>" + displayTime(Math.floor(idletime)) + " ago";
@@ -294,6 +341,7 @@ function updatecards() {
$('#tag' + tagmac + ' .warningicon').style.display = 'inline-block'; $('#tag' + tagmac + ' .warningicon').style.display = 'inline-block';
$('#tag' + tagmac).classList.remove("tagpending") $('#tag' + tagmac).classList.remove("tagpending")
$('#tag' + tagmac).style.background = '#e0e0a0'; $('#tag' + tagmac).style.background = '#e0e0a0';
timeoutcount++;
} }
if (idletime > 24 * 3600) { if (idletime > 24 * 3600) {
$('#tag' + tagmac).style.opacity = '.5'; $('#tag' + tagmac).style.opacity = '.5';
@@ -314,6 +362,11 @@ function updatecards() {
$('#tag' + tagmac + ' .nextcheckin').innerHTML = ""; $('#tag' + tagmac + ' .nextcheckin').innerHTML = "";
} }
}) })
$('#dashboardTagCount').innerHTML = tagcount;
$('#dashboardPending').innerHTML = pendingcount;
$('#dashboardLowBatt').innerHTML = lowbattcount;
$('#dashboardTimeout').innerHTML = timeoutcount;
} }
$('#clearlog').onclick = function () { $('#clearlog').onclick = function () {
@@ -327,6 +380,13 @@ document.querySelectorAll('.closebtn').forEach(button => {
}); });
}); });
document.querySelectorAll('.closebtn2').forEach(button => {
button.addEventListener('click', (event) => {
event.target.parentNode.close();
$('#advancedoptions').style.height = '0px';
});
});
//clicking on a tag: load config dialog for tag //clicking on a tag: load config dialog for tag
$('#taglist').addEventListener("click", (event) => { $('#taglist').addEventListener("click", (event) => {
let currentElement = event.target; let currentElement = event.target;
@@ -365,7 +425,7 @@ function loadContentCard(mac) {
$('#cfgrotate').value = tagdata.rotate; $('#cfgrotate').value = tagdata.rotate;
$('#cfglut').value = tagdata.lut; $('#cfglut').value = tagdata.lut;
$('#cfgmore').innerHTML = '&#x25BC;'; $('#cfgmore').innerHTML = '&#x25BC;';
$('#configbox').style.display = 'block'; $('#configbox').showModal();
}) })
} }
@@ -427,7 +487,7 @@ $('#cfgsave').onclick = function () {
.catch(error => showMessage('Error: ' + error)); .catch(error => showMessage('Error: ' + error));
$('#advancedoptions').style.height = '0px'; $('#advancedoptions').style.height = '0px';
$('#configbox').style.display = 'none'; $('#configbox').close();
} }
function sendCmd(mac, cmd) { function sendCmd(mac, cmd) {
@@ -446,7 +506,7 @@ function sendCmd(mac, cmd) {
}) })
.catch(error => showMessage('Error: ' + error)); .catch(error => showMessage('Error: ' + error));
$('#advancedoptions').style.height = '0px'; $('#advancedoptions').style.height = '0px';
$('#configbox').style.display = 'none'; $('#configbox').close();
} }
$('#cfgdelete').onclick = function () { $('#cfgdelete').onclick = function () {
@@ -477,7 +537,8 @@ $('#cfgreset').onclick = function () {
sendCmd($('#cfgmac').dataset.mac, "reset"); sendCmd($('#cfgmac').dataset.mac, "reset");
} }
$('#rebootbutton').onclick = function () { $('#rebootbutton').onclick = function (event) {
event.preventDefault();
showMessage("rebooting AP....", true); showMessage("rebooting AP....", true);
fetch("/reboot", { fetch("/reboot", {
method: "POST" method: "POST"
@@ -485,29 +546,37 @@ $('#rebootbutton').onclick = function () {
socket.close(); socket.close();
} }
$('#apconfigbutton').onclick = function () { $('#configbox').addEventListener('click', (event) => {
let table = document.getElementById("aptable"); if (event.target.nodeName === 'DIALOG') {
const rowCount = table.rows.length; $('#configbox').close();
for (let i = rowCount - 1; i > 0; i--) {
table.deleteRow(i);
} }
fetch("/get_ap_config") });
.then(response => response.json())
.then(data => { document.addEventListener("loadTab", function (event) {
$('#apcfgalias').value = data.alias; activeTab = event.detail;
$('#apcfgchid').value = data.channel; switch (event.detail) {
$("#apcfgledbrightness").value = data.led; case 'configtab':
$("#apcfglanguage").value = data.language; case 'aptab':
$("#apclatency").value = data.maxsleep; fetch("/get_ap_config")
$("#apcpreventsleep").value = data.stopsleep; .then(response => response.json())
$("#apcpreview").value = data.preview; .then(data => {
$("#apcwifipower").value = data.wifipower; apConfig = data;
$("#apctimezone").value = data.timezone; $('#apcfgalias').value = data.alias;
$("#apcnight1").value = data.sleeptime1; $('#apcfgchid').value = data.channel;
$("#apcnight2").value = data.sleeptime2; $("#apcfgledbrightness").value = data.led;
}) $("#apcfglanguage").value = data.language;
$('#apconfigbox').style.display = 'block' $("#apclatency").value = data.maxsleep;
} $("#apcpreventsleep").value = data.stopsleep;
$("#apcpreview").value = data.preview;
$("#apcwifipower").value = data.wifipower;
$("#apctimezone").value = data.timezone;
$("#apcnight1").value = data.sleeptime1;
$("#apcnight2").value = data.sleeptime2;
})
$('#apcfgmsg').innerHTML = '';
break;
}
});
$('#apcfgsave').onclick = function () { $('#apcfgsave').onclick = function () {
let formData = new FormData(); let formData = new FormData();
@@ -522,19 +591,22 @@ $('#apcfgsave').onclick = function () {
formData.append('timezone', $('#apctimezone').value); formData.append('timezone', $('#apctimezone').value);
formData.append('sleeptime1', $('#apcnight1').value); formData.append('sleeptime1', $('#apcnight1').value);
formData.append('sleeptime2', $('#apcnight2').value); formData.append('sleeptime2', $('#apcnight2').value);
fetch("/save_apcfg", { fetch("/save_apcfg", {
method: "POST", method: "POST",
body: formData body: formData
}) })
.then(response => response.text()) .then(response => response.text())
.then(data => showMessage(data)) .then(data => {
showMessage(data);
window.dispatchEvent(loadConfig);
$('#apcfgmsg').innerHTML = 'OK, Saved';
})
.catch(error => showMessage('Error: ' + error)); .catch(error => showMessage('Error: ' + error));
$(".logo").innerHTML = $('#apcfgalias').value;
$('#apconfigbox').style.display = 'none';
} }
$('#updatebutton').onclick = function () { $('#updatebutton').onclick = function (event) {
$('#apconfigbox').style.display = 'none'; event.preventDefault();
$('#apupdatebox').style.display = 'block'; $('#apupdatebox').style.display = 'block';
loadOTA(); loadOTA();
} }
@@ -698,7 +770,9 @@ function showMessage(message, iserr) {
const messages = $('#messages'); const messages = $('#messages');
const date = new Date(); const date = new Date();
const time = date.toLocaleTimeString('nl-NL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const time = date.toLocaleTimeString('nl-NL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
if (iserr) { if (message.startsWith('{')) {
messages.insertAdjacentHTML("afterbegin", '<li class="new">' + htmlEncode(time) + ' <span class="mono">' + message.replace(/"([^"]+)"/g, '"<span class="quote">$1</span>"') + '</span></li>');
} else if (iserr) {
messages.insertAdjacentHTML("afterbegin", '<li class="new error">' + htmlEncode(time + ' ' + message) + '</li>'); messages.insertAdjacentHTML("afterbegin", '<li class="new error">' + htmlEncode(time + ' ' + message) + '</li>');
} else { } else {
messages.insertAdjacentHTML("afterbegin", '<li class="new">' + htmlEncode(time + ' ' + message) + '</li>'); messages.insertAdjacentHTML("afterbegin", '<li class="new">' + htmlEncode(time + ' ' + message) + '</li>');
@@ -865,12 +939,18 @@ function GroupSortFilter() {
} }
let show = true; let show = true;
let batteryMv = tagDB[item.dataset.mac].batteryMv;
if ($('input[name="filter"][value="remote"]').checked && item.dataset.isexternal == "false") show = false; if ($('input[name="filter"][value="remote"]').checked && item.dataset.isexternal == "false") show = false;
if ($('input[name="filter"][value="local"]').checked && item.dataset.isexternal == "true") show = false; if ($('input[name="filter"][value="local"]').checked && item.dataset.isexternal == "true") show = false;
if ($('input[name="filter"][value="inactive"]').checked && item.querySelector('.warningicon').style.display != 'inline-block') show = false; if ($('input[name="filter"][value="inactive"]').checked && item.querySelector('.warningicon').style.display != 'inline-block') show = false;
if ($('input[name="filter"][value="pending"]').checked && !item.classList.contains("tagpending")) show = false; if ($('input[name="filter"][value="pending"]').checked && !item.classList.contains("tagpending")) show = false;
if ($('input[name="filter"][value="lowbatt"]').checked && (batteryMv >= 2400 || batteryMv == 0 || batteryMv == 1337)) show = false;
if (!show) item.style.display = 'none'; else item.style.display = 'block'; if (!show) item.style.display = 'none'; else item.style.display = 'block';
item.style.order = order++; item.style.order = order++;
const checkedValues = Array.from(document.querySelectorAll('input[name="filter"]:checked'))
.map(checkbox => checkbox.value)
.join(', ');
$('#activefilter').innerHTML = (checkedValues ? 'filtered by ' + checkedValues : '');
}); });
headItems = Array.from($('#taglist').getElementsByClassName('taggroup')); headItems = Array.from($('#taglist').getElementsByClassName('taggroup'));
@@ -879,7 +959,7 @@ function GroupSortFilter() {
}) })
} }
$('#toggleFilters').addEventListener('click', () => { $('#toggleFilters').addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault();
const filterOptions = $('#filterOptions'); const filterOptions = $('#filterOptions');
filterOptions.classList.toggle('active'); filterOptions.classList.toggle('active');
@@ -890,6 +970,13 @@ $('#toggleFilters').addEventListener('click', () => {
} }
}); });
$('#activefilter').addEventListener('click', (event) => {
event.preventDefault();
const filterOptions = $('#filterOptions');
filterOptions.classList.add('active');
filterOptions.style.maxHeight = filterOptions.scrollHeight + 20 + 'px';
});
async function getTagtype(hwtype) { async function getTagtype(hwtype) {
if (tagTypes[hwtype]) { if (tagTypes[hwtype]) {
return tagTypes[hwtype]; return tagTypes[hwtype];
@@ -1094,7 +1181,8 @@ $('#taglist').addEventListener('contextmenu', (e) => {
if (tagTypes[hwtype].options?.includes("led")) { if (tagTypes[hwtype].options?.includes("led")) {
contextMenuOptions.push( contextMenuOptions.push(
{ id: 'ledflash', label: 'Flash the LED' }, { id: 'ledflash', label: 'Flash the LED' },
{ id: 'ledflash_long', label: 'Flash the LED (long)' } { id: 'ledflash_long', label: 'Flash the LED (long)' },
{ id: 'ledflash_stop', label: 'Stop flashing' }
); );
} }
contextMenuOptions.push( contextMenuOptions.push(
@@ -1133,4 +1221,83 @@ function populateTimes(element) {
option.text = i.toString().padStart(2, "0") + ":00"; option.text = i.toString().padStart(2, "0") + ":00";
element.appendChild(option); element.appendChild(option);
} }
}
function populateAPCard(msg) {
let apip = msg.ip;
let apid = apip.replace(/\./g, "-");
if (!$('#ap' + apid)) {
div = $('#apcard').cloneNode(true);
div.setAttribute('id', 'ap' + apid);
$('#aplist').appendChild(div);
}
let alias = msg.alias;
if (!alias) alias = apip;
$('#ap' + apid + ' .apip').innerHTML = "<a href=\"http://" + apip + "\" target=\"_new\">" + apip + "</a>";
$('#ap' + apid + ' .apalias').innerHTML = alias;
$('#ap' + apid + ' .aptagcount').innerHTML = msg.count;
$('#ap' + apid + ' .apchannel').innerHTML = msg.channel;
const elements = document.querySelectorAll('.apchannel');
Array.from(elements).forEach(element => {
if (element.textContent === msg.channel && element.id !== 'ap' + apid) {
$('#ap' + apid + ' .apchannel').style.color = 'red';
$('#ap' + apid + ' .apchannel').innerHTML += ' conflict';
}
});
// $('#ap' + apid + ' .apversion').innerHTML = msg.version;
if (activeTab == 'aptab') {
populateAPInfo(apip);
}
}
function populateAPInfo(apip) {
let apid = apip.replace(/\./g, "-");
fetch('http://' + apip + '/sysinfo')
.then(response => {
if (response.status != 200) {
$('#ap' + apid + ' .apswversion').innerHTML = "Error fetching sysinfo: " + response.status;
return {};
} else {
return response.json();
}
})
.then(data => {
if (data.env) {
let version = '';
version += `env: ${data.env}<br>`;
version += `build date: ${formatEpoch(data.buildtime)}<br>`;
version += `esp32 version: ${data.buildversion}<br>`;
version += `psram size: ${data.psramsize}<br>`;
version += `flash size: ${data.flashsize}<br>`;
$('#ap' + apid + ' .apswversion').innerHTML = version;
// if (data.env == 'ESP32_S3_16_8_YELLOW_AP') $("#c6Option").style.display = 'block';
}
})
.catch(error => {
$('#ap' + apid + ' .apswversion').innerHTML = "Error fetching sysinfo: " + error;
});
}
function formatEpoch(epochTime) {
const date = new Date(epochTime * 1000); // Convert seconds to milliseconds
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
function setFilterAndShow(filter) {
$('input[name="filter"][value="remote"]').checked = false;
$('input[name="filter"][value="local"]').checked = false;
$('input[name="filter"][value="inactive"]').checked = (filter == 'inactive');
$('input[name="filter"][value="pending"]').checked = (filter == 'pending');
$('input[name="filter"][value="lowbatt"]').checked = (filter == 'lowbatt');
GroupSortFilter();
$(`[data-target='tagtab']`).click();
} }

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

View File

@@ -6,4 +6,11 @@ There are holder and stands for every tag. 1.54", 2.9" and 4.2".
![image](https://github.com/slimline33/OpenEPaperLink/assets/3323812/836875fd-7a5f-4a14-8cbe-e83381784879) ![image](https://github.com/slimline33/OpenEPaperLink/assets/3323812/836875fd-7a5f-4a14-8cbe-e83381784879)
![image](../../docs/assets/2-9_Solum_shelf_holder_2cm.jpg) ![image](2-9_Solum_shelf_holder_2cm.jpg)
** 4.2 mini stand v2 by Nic.stl
![image](4.2_mini_stand_v2.jpeg)
4.2" mini stand by Nic. Note that these are designed with tight tolerances, to be printed on a resin printer. If you print it using your desktop 3D-printer, it might not fit.

View File

@@ -34,6 +34,7 @@ Not all connections are required by all tags! If you want to solder fewer wires,
## Flashing the flasher ## Flashing the flasher
Clone the [Tag_Flasher repo](https://github.com/jjwbruijn/OpenEPaperLink/tree/master/Tag_Flasher/ESP32_Flasher) and open into PlatformIO. Choose the correct COM-port and hit 'Upload'. Clone the [Tag_Flasher repo](https://github.com/jjwbruijn/OpenEPaperLink/tree/master/Tag_Flasher/ESP32_Flasher) and open into PlatformIO. Choose the correct COM-port and hit 'Upload'.
Also, the precompiled binaries are part of any [release](https://github.com/jjwbruijn/OpenEPaperLink/releases), and to make it even easier, you can use the web flasher on https://install.openepaperlink.nl to even flash it without installing any extra software.
## OEPL-Flasher.py ## OEPL-Flasher.py

View File

@@ -174,13 +174,16 @@ struct ledFlash {
uint8_t flashDuration : 4; uint8_t flashDuration : 4;
uint8_t color1; uint8_t color1;
uint8_t flashCount1 : 4; uint8_t flashCount1 : 4;
uint8_t delay1 : 4; uint8_t flashSpeed1 : 4;
uint8_t delay1;
uint8_t color2; uint8_t color2;
uint8_t flashCount2 : 4; uint8_t flashCount2 : 4;
uint8_t delay2 : 4; uint8_t flashSpeed2 : 4;
uint8_t delay2;
uint8_t color3; uint8_t color3;
uint8_t flashCount3 : 4; uint8_t flashCount3 : 4;
uint8_t delay3 : 4; uint8_t flashSpeed3 : 4;
uint8_t delay3;
uint8_t repeats; uint8_t repeats;
uint8_t spare; uint8_t spare;
} __packed; } __packed;