mirror of
https://github.com/OpenEPaperLink/OpenEPaperLink.git
synced 2026-03-21 05:06:39 +01:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb0064363f | ||
|
|
c1324c5089 | ||
|
|
df3c615eef | ||
|
|
d5e19d20fa | ||
|
|
e62a1b07bf | ||
|
|
4d06ef546d | ||
|
|
7096f7e756 | ||
|
|
1abedff388 | ||
|
|
8d2546a2aa | ||
|
|
c5a8058d62 | ||
|
|
ef59b87ae0 | ||
|
|
d526c43fd8 | ||
|
|
86abc112dd | ||
|
|
af50f96671 | ||
|
|
46c8b73fa0 | ||
|
|
c65c8b749e | ||
|
|
f61c2e3d04 | ||
|
|
0dc406b865 | ||
|
|
25b185da28 | ||
|
|
496d4d382f | ||
|
|
d09e89c9dd | ||
|
|
ce319bb499 | ||
|
|
4dbcc753fd | ||
|
|
6951cd79b7 | ||
|
|
eeb18f204d | ||
|
|
bfff2ef0b9 | ||
|
|
ef5ddac368 | ||
|
|
0e8e7b5b75 | ||
|
|
e8a92c4fcb | ||
|
|
299b8f300e | ||
|
|
0b0802ad02 | ||
|
|
f8ce3a51d2 | ||
|
|
0e63e064fc | ||
|
|
8d6c763aba | ||
|
|
ab48cbe747 |
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -196,6 +196,24 @@ jobs:
|
||||
cp ESP32_S3_16_8_LILYGO_AP/firmware.bin espbinaries/ESP32_S3_16_8_LILYGO_AP.bin
|
||||
cp ESP32_S3_16_8_LILYGO_AP/merged-firmware.bin espbinaries/ESP32_S3_16_8_LILYGO_AP_full.bin
|
||||
|
||||
- name: Build firmware for ESP32_S3_16_8_LILYGO_T3
|
||||
run: |
|
||||
cd ESP32_AP-Flasher
|
||||
export PLATFORMIO_BUILD_FLAGS="-D BUILD_VERSION=${{ github.ref_name }} -D SHA=$GITHUB_SHA"
|
||||
pio run --environment ESP32_S3_16_8_LILYGO_T3
|
||||
pio run --target buildfs --environment ESP32_S3_16_8_LILYGO_T3
|
||||
mkdir /home/runner/work/OpenEPaperLink/OpenEPaperLink/ESP32_S3_16_8_LILYGO_T3
|
||||
cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin /home/runner/work/OpenEPaperLink/OpenEPaperLink/ESP32_S3_16_8_LILYGO_T3/boot_app0.bin
|
||||
cp .pio/build/ESP32_S3_16_8_LILYGO_T3/firmware.bin /home/runner/work/OpenEPaperLink/OpenEPaperLink/ESP32_S3_16_8_LILYGO_T3/firmware.bin
|
||||
cp .pio/build/ESP32_S3_16_8_LILYGO_T3/bootloader.bin /home/runner/work/OpenEPaperLink/OpenEPaperLink/ESP32_S3_16_8_LILYGO_T3/bootloader.bin
|
||||
cp .pio/build/ESP32_S3_16_8_LILYGO_T3/partitions.bin /home/runner/work/OpenEPaperLink/OpenEPaperLink/ESP32_S3_16_8_LILYGO_T3/partitions.bin
|
||||
cp .pio/build/ESP32_S3_16_8_LILYGO_T3/littlefs.bin /home/runner/work/OpenEPaperLink/OpenEPaperLink/ESP32_S3_16_8_LILYGO_T3/littlefs.bin
|
||||
cd /home/runner/work/OpenEPaperLink/OpenEPaperLink/ESP32_S3_16_8_LILYGO_T3
|
||||
esptool.py --chip esp32-s3 merge_bin -o merged-firmware.bin --flash_mode dio --flash_freq 80m --flash_size 16MB 0x0000 bootloader.bin 0x8000 partitions.bin 0xe000 boot_app0.bin 0x10000 firmware.bin 0x00910000 littlefs.bin
|
||||
cd /home/runner/work/OpenEPaperLink/OpenEPaperLink
|
||||
cp ESP32_S3_16_8_LILYGO_T3/firmware.bin espbinaries/ESP32_S3_16_8_LILYGO_T3.bin
|
||||
cp ESP32_S3_16_8_LILYGO_T3/merged-firmware.bin espbinaries/ESP32_S3_16_8_LILYGO_T3_full.bin
|
||||
|
||||
- name: Build firmware for OpenEPaperLink_Nano_TLSR
|
||||
run: |
|
||||
cd ESP32_AP-Flasher
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -104,22 +104,22 @@ extern Arduino_RGB_Display *gfx;
|
||||
#define LCD_VSYNC 17
|
||||
#define LCD_HSYNC 16
|
||||
#define LCD_PCLK 21
|
||||
#define LCD_R0 11
|
||||
#define LCD_R1 12
|
||||
#define LCD_R2 13
|
||||
#define LCD_R3 14
|
||||
#define LCD_R4 0
|
||||
#define LCD_R0 0
|
||||
#define LCD_R1 11
|
||||
#define LCD_R2 12
|
||||
#define LCD_R3 13
|
||||
#define LCD_R4 14
|
||||
#define LCD_G0 8
|
||||
#define LCD_G1 20
|
||||
#define LCD_G2 3
|
||||
#define LCD_G3 46
|
||||
#define LCD_G4 9
|
||||
#define LCD_G5 10
|
||||
#define LCD_B0 4
|
||||
#define LCD_B1 5
|
||||
#define LCD_B2 6
|
||||
#define LCD_B3 7
|
||||
#define LCD_B4 15
|
||||
#define LCD_B0 15
|
||||
#define LCD_B1 4
|
||||
#define LCD_B2 5
|
||||
#define LCD_B3 6
|
||||
#define LCD_B4 7
|
||||
#define LCD_BL 38
|
||||
#define LCD_DE 18
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ struct imgParam {
|
||||
|
||||
uint8_t zlib;
|
||||
uint8_t g5;
|
||||
uint8_t ts_option;
|
||||
};
|
||||
|
||||
void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams);
|
||||
|
||||
@@ -56,6 +56,7 @@ class nrfswd : protected swd {
|
||||
|
||||
uint8_t nrf_read_bank(uint32_t address, uint32_t buffer[], int size);
|
||||
uint8_t nrf_write_bank(uint32_t address, uint32_t buffer[], int size);
|
||||
uint8_t nrf_erase_all();
|
||||
uint8_t erase_all_flash();
|
||||
uint8_t erase_uicr();
|
||||
uint8_t erase_page(uint32_t page);
|
||||
|
||||
@@ -287,6 +287,95 @@ board_upload.maximum_ram_size = 327680
|
||||
board_upload.flash_size = 16MB
|
||||
; ----------------------------------------------------------------------------------------
|
||||
; !!! this configuration expects an ESP32-S3 16MB Flash 8MB RAM
|
||||
; !!! Dedicated pinout for using the Lilygo T-Display-S3 board.
|
||||
; ----------------------------------------------------------------------------------------
|
||||
[env:ESP32_S3_16_8_LILYGO_T3]
|
||||
board = esp32-s3-devkitc-1
|
||||
board_build.partitions = large_spiffs_16MB.csv
|
||||
monitor_dtr = 0
|
||||
monitor_rts = 0
|
||||
build_unflags =
|
||||
-std=gnu++11
|
||||
-D CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y
|
||||
-D ILI9341_DRIVER
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags =
|
||||
-std=gnu++17
|
||||
${env.build_flags}
|
||||
-D HAS_TFT
|
||||
-D CORE_DEBUG_LEVEL=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D CONFIG_ESP32S3_SPIRAM_SUPPORT=1
|
||||
-D CONFIG_SPIRAM_USE_MALLOC=1
|
||||
-D POWER_NO_SOFT_POWER
|
||||
-D BOARD_HAS_PSRAM
|
||||
-D CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
|
||||
-D HAS_BLE_WRITER
|
||||
-D FLASHER_AP_SS=-1
|
||||
-D FLASHER_AP_CLK=-1
|
||||
-D FLASHER_AP_MOSI=-1
|
||||
-D FLASHER_AP_MISO=-1
|
||||
-D FLASHER_AP_RESET=44 ;47 ;purple RST
|
||||
-D FLASHER_AP_POWER={-1}
|
||||
-D FLASHER_AP_TEST=-1
|
||||
-D FLASHER_AP_TXD=17 ;white 2
|
||||
-D FLASHER_AP_RXD=18 ;orange 3
|
||||
-D FLASHER_DEBUG_TXD=12 ;15 ;yellow TX
|
||||
-D FLASHER_DEBUG_RXD=13 ;7 ;blue RX
|
||||
-D FLASHER_DEBUG_PROG=21 ;green 9
|
||||
-D FLASHER_LED=-1
|
||||
-D HAS_RGB_LED
|
||||
-D FLASHER_RGB_LED=48
|
||||
-D ST7789_DRIVER
|
||||
;-D ST7735_DRIVER
|
||||
;-D ST7735_GREENTAB160x80
|
||||
-D TFT_INVERSION_ON
|
||||
-D TFT_PARALLEL_8_BIT
|
||||
-D TFT_WIDTH=170 ;80
|
||||
-D TFT_HEIGHT=320 ;160
|
||||
;-D TFT_MISO=-1
|
||||
;-D TFT_MOSI=13
|
||||
;-D TFT_SCLK=12
|
||||
-D TFT_WR=8
|
||||
-D TFT_RD=9
|
||||
-D TFT_CS=6 ;10
|
||||
-D TFT_DC=7 ;11
|
||||
-D TFT_RST=5 ;1
|
||||
-D TFT_D0=39
|
||||
-D TFT_D1=40
|
||||
-D TFT_D2=41
|
||||
-D TFT_D3=42
|
||||
-D TFT_D4=45
|
||||
-D TFT_D5=46
|
||||
-D TFT_D6=47
|
||||
-D TFT_D7=48
|
||||
-D TFT_BL=38
|
||||
-D TFT_RGB_ORDER=TFT_BGR
|
||||
-D USE_HSPI_PORT
|
||||
-D LOAD_FONT2
|
||||
-D LOAD_FONT4
|
||||
-D LOAD_GLCD
|
||||
;-D LOAD_FONT6
|
||||
;-D LOAD_FONT7
|
||||
;-D LOAD_FONT8
|
||||
;-D LOAD_GFXFF
|
||||
;-D SMOOTH_FONT
|
||||
-D MD5_ENABLED=1
|
||||
-D SERIAL_FLASHER_INTERFACE_UART=1
|
||||
-D SERIAL_FLASHER_BOOT_HOLD_TIME_MS=200
|
||||
-D SERIAL_FLASHER_RESET_HOLD_TIME_MS=200
|
||||
-D C6_OTA_FLASHING
|
||||
-D HAS_SUBGHZ ; previously disabled but now enabled. Disabling causes the Channel chooser to disfunction.
|
||||
build_src_filter =
|
||||
+<*>-<usbflasher.cpp>-<swd.cpp>-<webflasher.cpp>
|
||||
board_build.flash_mode=qio
|
||||
board_build.arduino.memory_type = qio_opi
|
||||
board_build.psram_type=qspi_opi
|
||||
board_upload.maximum_size = 16777216
|
||||
board_upload.maximum_ram_size = 327680
|
||||
board_upload.flash_size = 16MB
|
||||
; ----------------------------------------------------------------------------------------
|
||||
; !!! this configuration expects an ESP32-S3 16MB Flash 8MB RAM
|
||||
; ----------------------------------------------------------------------------------------
|
||||
[env:ESP32_S3_C6_BIG_AP]
|
||||
board = esp32-s3-devkitc-1
|
||||
|
||||
@@ -64,7 +64,7 @@ uint8_t gicToOEPLtype(uint8_t gicType) {
|
||||
}
|
||||
}
|
||||
|
||||
struct BleAdvDataStruct {
|
||||
struct BleAdvDataStructV1 {
|
||||
uint16_t manu_id; // 0x1337 for us
|
||||
uint8_t version;
|
||||
uint16_t hw_type;
|
||||
@@ -73,6 +73,16 @@ struct BleAdvDataStruct {
|
||||
uint16_t battery_mv;
|
||||
uint8_t counter;
|
||||
} __packed;
|
||||
struct BleAdvDataStructV2 {
|
||||
uint16_t manu_id; // 0x1337 for us
|
||||
uint8_t version;
|
||||
uint16_t hw_type;
|
||||
uint16_t fw_version;
|
||||
uint16_t capabilities;
|
||||
uint16_t battery_mv;
|
||||
int8_t temperature;
|
||||
uint8_t counter;
|
||||
} __packed;
|
||||
|
||||
bool BLE_filter_add_device(BLEAdvertisedDevice advertisedDevice) {
|
||||
Serial.print("BLE Advertised Device found: ");
|
||||
@@ -91,16 +101,16 @@ bool BLE_filter_add_device(BLEAdvertisedDevice advertisedDevice) {
|
||||
uint8_t manuData[100];
|
||||
if (manuDatalen > sizeof(manuData))
|
||||
return false; // Manu data too big, could never happen but better make sure here
|
||||
Serial.printf(" Address type: %02X Manu data: ", advertisedDevice.getAddressType());
|
||||
for (int i = 0; i < advertisedDevice.getManufacturerData().length(); i++)
|
||||
Serial.printf("%02X", manuData[i]);
|
||||
Serial.printf("\r\n");
|
||||
#if ESP_ARDUINO_VERSION_MAJOR == 2
|
||||
memcpy(&manuData, (uint8_t*)advertisedDevice.getManufacturerData().data(), manuDatalen);
|
||||
#else
|
||||
// [Nic] suggested fix for arduino 3.x by copilot, but I cannot test it
|
||||
memcpy(&manuData, (uint8_t*)advertisedDevice.getManufacturerData().c_str(), manuDatalen);
|
||||
#endif
|
||||
Serial.printf(" Address type: %02X Manu data: ", advertisedDevice.getAddressType());
|
||||
for (int i = 0; i < advertisedDevice.getManufacturerData().length(); i++)
|
||||
Serial.printf("%02X", manuData[i]);
|
||||
Serial.printf("\r\n");
|
||||
if (manuDatalen == 7 && manuData[0] == 0x53 && manuData[1] == 0x50) { // Lets check for a Gicisky E-Paper display
|
||||
|
||||
struct espAvailDataReq theAdvData;
|
||||
@@ -125,23 +135,63 @@ bool BLE_filter_add_device(BLEAdvertisedDevice advertisedDevice) {
|
||||
|
||||
processDataReq(&theAdvData, true);
|
||||
return true;
|
||||
} else if (manuDatalen >= sizeof(BleAdvDataStruct) && manuData[0] == 0x37 && manuData[1] == 0x13) { // Lets check for a Gicisky E-Paper display
|
||||
} else if (manuDatalen >= 3 && manuData[0] == 0x37 && manuData[1] == 0x13) { // Lets check for a Gicisky E-Paper display
|
||||
Serial.printf("ATC BLE OEPL Detected\r\n");
|
||||
struct espAvailDataReq theAdvData;
|
||||
struct BleAdvDataStruct inAdvData;
|
||||
|
||||
memset((uint8_t*)&theAdvData, 0x00, sizeof(espAvailDataReq));
|
||||
memcpy(&inAdvData, manuData, sizeof(BleAdvDataStruct));
|
||||
/*Serial.printf("manu_id %04X\r\n", inAdvData.manu_id);
|
||||
Serial.printf("version %04X\r\n", inAdvData.version);
|
||||
Serial.printf("hw_type %04X\r\n", inAdvData.hw_type);
|
||||
Serial.printf("fw_version %04X\r\n", inAdvData.fw_version);
|
||||
Serial.printf("capabilities %04X\r\n", inAdvData.capabilities);
|
||||
Serial.printf("battery_mv %u\r\n", inAdvData.battery_mv);
|
||||
Serial.printf("counter %u\r\n", inAdvData.counter);*/
|
||||
if (inAdvData.version != 1) {
|
||||
printf("Version currently not supported!\r\n");
|
||||
return false;
|
||||
uint8_t versionAdvData = manuData[2];
|
||||
|
||||
switch (versionAdvData) {
|
||||
case 1: {
|
||||
if (manuDatalen >= sizeof(BleAdvDataStructV1)) {
|
||||
struct BleAdvDataStructV1 inAdvData;
|
||||
memcpy(&inAdvData, manuData, sizeof(BleAdvDataStructV1));
|
||||
printf("Version 1 ATC_BLE_OEPL Received\r\n");
|
||||
/*Serial.printf("manu_id %04X\r\n", inAdvData.manu_id);
|
||||
Serial.printf("version %02X\r\n", inAdvData.version);
|
||||
Serial.printf("hw_type %04X\r\n", inAdvData.hw_type);
|
||||
Serial.printf("fw_version %04X\r\n", inAdvData.fw_version);
|
||||
Serial.printf("capabilities %04X\r\n", inAdvData.capabilities);
|
||||
Serial.printf("battery_mv %u\r\n", inAdvData.battery_mv);
|
||||
Serial.printf("counter %u\r\n", inAdvData.counter);*/
|
||||
theAdvData.adr.batteryMv = inAdvData.battery_mv;
|
||||
theAdvData.adr.lastPacketRSSI = advertisedDevice.getRSSI();
|
||||
theAdvData.adr.hwType = inAdvData.hw_type & 0xff;
|
||||
theAdvData.adr.tagSoftwareVersion = inAdvData.fw_version;
|
||||
theAdvData.adr.capabilities = inAdvData.capabilities & 0xff;
|
||||
} else {
|
||||
printf("Version 1 data length incorrect!\r\n");
|
||||
return false;
|
||||
}
|
||||
} break;
|
||||
case 2: {
|
||||
if (manuDatalen >= sizeof(BleAdvDataStructV2)) {
|
||||
struct BleAdvDataStructV2 inAdvData;
|
||||
memcpy(&inAdvData, manuData, sizeof(BleAdvDataStructV2));
|
||||
printf("Version 2 ATC_BLE_OEPL Received\r\n");
|
||||
/*Serial.printf("manu_id %04X\r\n", inAdvData.manu_id);
|
||||
Serial.printf("version %02X\r\n", inAdvData.version);
|
||||
Serial.printf("hw_type %04X\r\n", inAdvData.hw_type);
|
||||
Serial.printf("fw_version %04X\r\n", inAdvData.fw_version);
|
||||
Serial.printf("capabilities %04X\r\n", inAdvData.capabilities);
|
||||
Serial.printf("battery_mv %u\r\n", inAdvData.battery_mv);
|
||||
Serial.printf("temperature %i\r\n", inAdvData.temperature);
|
||||
Serial.printf("counter %u\r\n", inAdvData.counter);*/
|
||||
theAdvData.adr.batteryMv = inAdvData.battery_mv;
|
||||
theAdvData.adr.temperature = inAdvData.temperature;
|
||||
theAdvData.adr.lastPacketRSSI = advertisedDevice.getRSSI();
|
||||
theAdvData.adr.hwType = inAdvData.hw_type & 0xff;
|
||||
theAdvData.adr.tagSoftwareVersion = inAdvData.fw_version;
|
||||
theAdvData.adr.capabilities = inAdvData.capabilities & 0xff;
|
||||
} else {
|
||||
printf("Version 2 data length incorrect!\r\n");
|
||||
return false;
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
printf("Version %02X currently not supported!\r\n", versionAdvData);
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
uint8_t macReversed[6];
|
||||
memcpy(&macReversed, (uint8_t*)advertisedDevice.getAddress().getNative(), 6);
|
||||
@@ -153,11 +203,6 @@ bool BLE_filter_add_device(BLEAdvertisedDevice advertisedDevice) {
|
||||
theAdvData.src[5] = macReversed[0];
|
||||
theAdvData.src[6] = manuData[0]; // We use this do find out what type of display we got for compression^^
|
||||
theAdvData.src[7] = manuData[1];
|
||||
theAdvData.adr.batteryMv = inAdvData.battery_mv;
|
||||
theAdvData.adr.lastPacketRSSI = advertisedDevice.getRSSI();
|
||||
theAdvData.adr.hwType = inAdvData.hw_type & 0xff;
|
||||
theAdvData.adr.tagSoftwareVersion = inAdvData.fw_version;
|
||||
theAdvData.adr.capabilities = inAdvData.capabilities & 0xff;
|
||||
processDataReq(&theAdvData, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -259,6 +259,27 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
} else if (interval < 180)
|
||||
interval = 60 * 60;
|
||||
|
||||
imageParams.ts_option = config.showtimestamp;
|
||||
if(imageParams.ts_option) {
|
||||
JsonDocument loc;
|
||||
getTemplate(loc, taginfo->contentMode, taginfo->hwType);
|
||||
|
||||
if(loc["ts_option"].is<int>()) {
|
||||
// Overide ts_option if present in template
|
||||
imageParams.ts_option = loc["ts_option"];
|
||||
}
|
||||
else {
|
||||
const JsonArray jsonArray = loc.as<JsonArray>();
|
||||
for (const JsonVariant &elem : jsonArray) {
|
||||
if(elem["ts_option"].is<int>()) {
|
||||
// Overide ts_option if present in template
|
||||
imageParams.ts_option = elem["ts_option"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (taginfo->contentMode) {
|
||||
case 0: // Not configured
|
||||
case 22: // Static image
|
||||
@@ -560,10 +581,26 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
case 27: // Day Ahead:
|
||||
|
||||
if (getDayAheadFeed(filename, cfgobj, taginfo, imageParams)) {
|
||||
taginfo->nextupdate = now + (3600 - now % 3600);
|
||||
// Get user-configured update frequency in minutes, default to 15 minutes (matching 15-min data intervals)
|
||||
int updateFreqMinutes = 15;
|
||||
if (cfgobj["updatefreq"].is<String>()) {
|
||||
String freqStr = cfgobj["updatefreq"].as<String>();
|
||||
freqStr.trim();
|
||||
if (freqStr.length() > 0) {
|
||||
updateFreqMinutes = freqStr.toInt();
|
||||
if (updateFreqMinutes < 1) {
|
||||
wsErr("Invalid update frequency, defaulting to 15 minutes");
|
||||
updateFreqMinutes = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
int updateInterval = updateFreqMinutes * 60; // Convert to seconds
|
||||
|
||||
// Schedule next update (align to interval boundary for consistent timing)
|
||||
taginfo->nextupdate = now + (updateInterval - now % updateInterval);
|
||||
updateTagImage(filename, mac, 0, taginfo, imageParams);
|
||||
} else {
|
||||
taginfo->nextupdate = now + 300;
|
||||
taginfo->nextupdate = now + 300; // Retry in 5 minutes on failure
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
@@ -1645,9 +1682,277 @@ YAxisScale calculateYAxisScale(double priceMin, double priceMax, int divisions)
|
||||
return {minY, maxY, roundedStepSize};
|
||||
}
|
||||
|
||||
bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgParam &imageParams) {
|
||||
// Helper function: calculate data range and align to interval boundaries
|
||||
// Returns: DataRange struct
|
||||
struct DataRange {
|
||||
int startIndex;
|
||||
int endIndex;
|
||||
int numAveragedSamples;
|
||||
int availableSamples;
|
||||
};
|
||||
|
||||
DataRange calculateDataRangeAndAlignment(const JsonDocument& doc, int originalDataSize, time_t now,
|
||||
int avgFactor, int targetIntervalMinutes, int barAreaWidth) {
|
||||
DataRange result;
|
||||
|
||||
// Step 1: Find data range - start at now-1h, trim far future if needed
|
||||
time_t targetStart = now - 3600; // 1 hour ago
|
||||
|
||||
// Find first data point at/before now-1h
|
||||
result.startIndex = 0;
|
||||
for (int i = originalDataSize - 1; i >= 0; i--) {
|
||||
time_t dataTime = doc[i]["time"];
|
||||
if (dataTime <= targetStart) {
|
||||
result.startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate maximum bars that can fit on display
|
||||
int maxBars = barAreaWidth; // 1px per bar minimum
|
||||
|
||||
// Calculate how many samples we'll have after averaging
|
||||
result.availableSamples = originalDataSize - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
|
||||
// Step 2: Trim from far future if needed (prioritize now and near future)
|
||||
result.endIndex = originalDataSize;
|
||||
if (result.numAveragedSamples > maxBars) {
|
||||
// Too many samples - limit the range
|
||||
int maxSamplesNeeded = maxBars * avgFactor;
|
||||
result.endIndex = result.startIndex + maxSamplesNeeded;
|
||||
if (result.endIndex > originalDataSize) result.endIndex = originalDataSize;
|
||||
result.availableSamples = result.endIndex - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
}
|
||||
|
||||
// Step 3: Align to interval boundary if averaging
|
||||
if (avgFactor > 1) {
|
||||
// Find first data point that aligns with target interval
|
||||
for (int i = result.startIndex; i < result.endIndex; i++) {
|
||||
time_t dataTime = doc[i]["time"];
|
||||
struct tm dt;
|
||||
localtime_r(&dataTime, &dt);
|
||||
|
||||
// Check if this timestamp aligns with target interval
|
||||
if (targetIntervalMinutes == 30) {
|
||||
if (dt.tm_min == 0 || dt.tm_min == 30) {
|
||||
result.startIndex = i;
|
||||
break;
|
||||
}
|
||||
} else if (targetIntervalMinutes == 60) {
|
||||
if (dt.tm_min == 0) {
|
||||
result.startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate after alignment
|
||||
result.availableSamples = result.endIndex - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function: Perform historic data backfill to optimize bar width
|
||||
// Returns: BackfillResult struct
|
||||
struct BackfillResult {
|
||||
int startIndex;
|
||||
int numAveragedSamples;
|
||||
int availableSamples;
|
||||
int addedHistoricCount;
|
||||
double barwidthBefore;
|
||||
double barwidthAfter;
|
||||
};
|
||||
|
||||
BackfillResult performHistoricBackfill(int startIndex, int endIndex, int numAveragedSamples,
|
||||
int avgFactor, int barAreaWidth, double targetBarwidth) {
|
||||
BackfillResult result;
|
||||
result.startIndex = startIndex;
|
||||
result.numAveragedSamples = numAveragedSamples;
|
||||
result.availableSamples = endIndex - startIndex;
|
||||
result.addedHistoricCount = 0;
|
||||
|
||||
int futureRawCount = endIndex - startIndex; // Raw samples (before averaging)
|
||||
int historicRawCount = startIndex; // Raw samples available before startIndex
|
||||
|
||||
// Calculate barwidth BEFORE backfill
|
||||
result.barwidthBefore = (double)barAreaWidth / numAveragedSamples;
|
||||
|
||||
if (historicRawCount > 0) {
|
||||
// Calculate current bar width
|
||||
double pixelsPerBar = (double)barAreaWidth / numAveragedSamples;
|
||||
int currentSpacing = (pixelsPerBar >= 3.0) ? 1 : 0;
|
||||
double currentBarWidth = pixelsPerBar - currentSpacing;
|
||||
|
||||
// Scenario 2: Bars > 5px target - add data to reduce toward target
|
||||
if (currentBarWidth > targetBarwidth) {
|
||||
// Target: 5px bars with 1px spacing = 6px total per bar
|
||||
int targetBars = barAreaWidth / (targetBarwidth + 1.0);
|
||||
int barsToAdd = targetBars - numAveragedSamples;
|
||||
|
||||
if (barsToAdd > 0) {
|
||||
int samplesToAdd = barsToAdd * avgFactor;
|
||||
result.addedHistoricCount = std::min(samplesToAdd, historicRawCount);
|
||||
result.startIndex -= result.addedHistoricCount;
|
||||
|
||||
// Bounds safety: ensure startIndex never goes negative
|
||||
if (result.startIndex < 0) {
|
||||
result.addedHistoricCount += result.startIndex; // Reduce by overflow amount
|
||||
result.startIndex = 0;
|
||||
}
|
||||
|
||||
result.availableSamples = endIndex - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
}
|
||||
}
|
||||
// Scenario 1: Bars <= 5px - fill empty space with historic data
|
||||
else {
|
||||
double currentPixelsPerBar = pixelsPerBar; // Use pixelsPerBar (not barwidth)
|
||||
int currentPixelsInt = (int)currentPixelsPerBar; // e.g., 3 from 3.05px
|
||||
|
||||
// Keep adding bars as long as pixelsPerBar doesn't drop below next integer
|
||||
while (historicRawCount > 0) {
|
||||
// Try adding avgFactor more raw samples (= 1 more bar after averaging)
|
||||
int testAveragedBars = result.numAveragedSamples + 1;
|
||||
double testPixelsPerBar = (double)barAreaWidth / testAveragedBars;
|
||||
int testPixelsInt = (int)testPixelsPerBar;
|
||||
|
||||
// Check if pixelsPerBar stays at or above current integer value
|
||||
// This ensures bars don't shrink (e.g., stay at 3px, don't drop to 2px)
|
||||
if (testPixelsInt >= currentPixelsInt) {
|
||||
// Pixels per bar still >= current integer - add this bar
|
||||
result.numAveragedSamples = testAveragedBars;
|
||||
historicRawCount -= avgFactor; // Consume raw samples
|
||||
result.addedHistoricCount += avgFactor;
|
||||
result.startIndex -= avgFactor; // Move start backwards into historic data
|
||||
|
||||
// Bounds safety: ensure startIndex never goes negative
|
||||
if (result.startIndex < 0) {
|
||||
result.addedHistoricCount += result.startIndex; // Reduce by overflow amount
|
||||
result.numAveragedSamples = (endIndex - 0) / avgFactor; // Recalculate with startIndex=0
|
||||
result.startIndex = 0;
|
||||
break; // Stop backfill
|
||||
}
|
||||
} else {
|
||||
// Next bar would drop below current integer, done
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update availableSamples after backfill
|
||||
if (result.addedHistoricCount > 0) {
|
||||
result.availableSamples = endIndex - result.startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate barwidth after backfill
|
||||
result.barwidthAfter = (double)barAreaWidth / result.numAveragedSamples;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Data structure for averaged price points
|
||||
struct AveragedDataPoint {
|
||||
time_t time;
|
||||
double price;
|
||||
int hour;
|
||||
int minute;
|
||||
};
|
||||
|
||||
// Helper function: Find cheapest consecutive blocks for appliance scheduling
|
||||
// Returns: CheapBlock struct
|
||||
struct CheapBlock {
|
||||
int start;
|
||||
int end;
|
||||
};
|
||||
|
||||
struct CheapBlocks {
|
||||
CheapBlock block1;
|
||||
CheapBlock block2;
|
||||
};
|
||||
|
||||
CheapBlocks findCheapestConsecutiveBlocks(const AveragedDataPoint* avgData, int numSamples,
|
||||
int blockSamples, time_t now) {
|
||||
CheapBlocks result;
|
||||
result.block1.start = -1;
|
||||
result.block1.end = -1;
|
||||
result.block2.start = -1;
|
||||
result.block2.end = -1;
|
||||
|
||||
if (blockSamples <= 0 || blockSamples > numSamples) {
|
||||
return result; // Invalid or disabled
|
||||
}
|
||||
|
||||
// Find all candidate blocks (simple: sum all prices in each block)
|
||||
struct BlockCandidate {
|
||||
int start;
|
||||
int end;
|
||||
double sum;
|
||||
};
|
||||
std::vector<BlockCandidate> candidates;
|
||||
|
||||
// Scan through visible data to find all valid consecutive blocks
|
||||
for (int i = 0; i <= numSamples - blockSamples; i++) {
|
||||
const time_t block_start_time = avgData[i].time;
|
||||
|
||||
// Only consider future blocks (skip past data)
|
||||
if (block_start_time < now) continue;
|
||||
|
||||
// Calculate sum for this block
|
||||
double blockSum = 0;
|
||||
for (int j = 0; j < blockSamples; j++) {
|
||||
blockSum += avgData[i + j].price;
|
||||
}
|
||||
|
||||
BlockCandidate candidate;
|
||||
candidate.start = i;
|
||||
candidate.end = i + blockSamples - 1;
|
||||
candidate.sum = blockSum;
|
||||
candidates.push_back(candidate);
|
||||
}
|
||||
|
||||
// Sort candidates by sum (cheapest first)
|
||||
std::sort(candidates.begin(), candidates.end(),
|
||||
[](const BlockCandidate& a, const BlockCandidate& b) { return a.sum < b.sum; });
|
||||
|
||||
// Pick up to 2 non-overlapping cheap blocks
|
||||
if (candidates.size() > 0) {
|
||||
result.block1.start = candidates[0].start;
|
||||
result.block1.end = candidates[0].end;
|
||||
|
||||
// Find second block that doesn't overlap with first
|
||||
for (size_t i = 1; i < candidates.size(); i++) {
|
||||
bool overlaps = (candidates[i].start <= result.block1.end && candidates[i].end >= result.block1.start);
|
||||
if (!overlaps) {
|
||||
result.block2.start = candidates[i].start;
|
||||
result.block2.end = candidates[i].end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool getDayAheadFeed(String& filename, JsonObject& cfgobj, tagRecord*& taginfo, imgParam& imageParams) {
|
||||
wsLog("get dayahead prices");
|
||||
|
||||
// Magic number constants for bar width calculations
|
||||
const double TARGET_BARWIDTH = 5.0; // Optimal bar width in pixels (good visibility)
|
||||
const double MIN_BARWIDTH = 2.0; // Minimum bar width (below this, bars hard to see)
|
||||
const int MIN_LABEL_SPACING = 30; // Minimum pixels between label centers
|
||||
const int DASH_LENGTH = 3; // Dashed line segment length
|
||||
const int GAP_LENGTH = 2; // Gap between dashed line segments
|
||||
const int STEM_WIDTH = 3; // Current time arrow stem width
|
||||
const int ARROW_WIDTH = 8; // Current time arrow head width
|
||||
const int ARROW_HEIGHT = 6; // Current time arrow head height
|
||||
const int STEM_HEIGHT = 10; // Current time arrow stem height
|
||||
const int GAP_AFTER_ARROW = 2; // Gap between arrow tip and vertical line
|
||||
|
||||
JsonDocument loc;
|
||||
getTemplate(loc, 27, taginfo->hwType);
|
||||
|
||||
@@ -1657,11 +1962,6 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm timeinfo;
|
||||
localtime_r(&now, &timeinfo);
|
||||
|
||||
char dateString[40];
|
||||
strftime(dateString, sizeof(dateString), languageDateFormat[0].c_str(), &timeinfo);
|
||||
|
||||
HTTPClient http;
|
||||
http.begin(URL);
|
||||
@@ -1684,20 +1984,80 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
|
||||
initSprite(spr, imageParams.width, imageParams.height, imageParams);
|
||||
|
||||
int n = doc.size();
|
||||
if (n == 0) {
|
||||
int originalDataSize = doc.size();
|
||||
if (originalDataSize == 0) {
|
||||
wsErr("No data in dayahead feed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect native data interval (15-min, hourly, etc.)
|
||||
int nativeIntervalMinutes = 15; // Default to 15 minutes (most common in Europe)
|
||||
if (originalDataSize >= 2) {
|
||||
time_t time0 = doc[0]["time"];
|
||||
time_t time1 = doc[1]["time"];
|
||||
nativeIntervalMinutes = (time1 - time0) / 60;
|
||||
}
|
||||
|
||||
// Get user's preferred display interval (0 = native, 30 = 30 minutes, 60 = 1 hour)
|
||||
int targetIntervalMinutes = cfgobj["interval"] ? cfgobj["interval"].as<int>() : 0;
|
||||
if (targetIntervalMinutes == 0) {
|
||||
targetIntervalMinutes = nativeIntervalMinutes; // Use native interval
|
||||
}
|
||||
|
||||
// Calculate averaging factor (how many native samples per target interval)
|
||||
int avgFactor = targetIntervalMinutes / nativeIntervalMinutes;
|
||||
if (avgFactor < 1) avgFactor = 1; // Safety: no upsampling
|
||||
|
||||
// Get bar area width for calculations
|
||||
int barAreaWidth = loc["bars"][1].as<int>();
|
||||
|
||||
// Store RAW current price BEFORE averaging (for accurate "now" display)
|
||||
// We'll calculate this after we have units/tariff variables below
|
||||
double rawCurrentPrice = std::numeric_limits<double>::quiet_NaN();
|
||||
int rawCurrentIndex = -1;
|
||||
int rawCurrentHour = 0; // Hour from the native interval containing "now"
|
||||
int rawCurrentMinute = 0; // Minute from the native interval containing "now"
|
||||
|
||||
// Calculate data range and align to interval boundaries (Steps 1-3)
|
||||
DataRange range = calculateDataRangeAndAlignment(doc, originalDataSize, now, avgFactor,
|
||||
targetIntervalMinutes, barAreaWidth);
|
||||
int startIndex = range.startIndex;
|
||||
int endIndex = range.endIndex;
|
||||
int numAveragedSamples = range.numAveragedSamples;
|
||||
int availableSamples = range.availableSamples;
|
||||
|
||||
// Safety check
|
||||
if (numAveragedSamples == 0) {
|
||||
wsErr("Not enough data for selected interval");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform historic data backfill to optimize bar width (Step 4)
|
||||
BackfillResult backfill = performHistoricBackfill(startIndex, endIndex, numAveragedSamples,
|
||||
avgFactor, barAreaWidth, TARGET_BARWIDTH);
|
||||
startIndex = backfill.startIndex;
|
||||
numAveragedSamples = backfill.numAveragedSamples;
|
||||
availableSamples = backfill.availableSamples;
|
||||
|
||||
// Debug logging (always visible in web console)
|
||||
int futureRawCount = endIndex - (startIndex + backfill.addedHistoricCount);
|
||||
int historicRawCount = startIndex + backfill.addedHistoricCount;
|
||||
wsLog("Day-ahead bar sizing: future_raw=" + String(futureRawCount) +
|
||||
" (incl now), historic_raw=" + String(historicRawCount) +
|
||||
", barwidth_before=" + String(backfill.barwidthBefore, 1) +
|
||||
"px, added_historic=" + String(backfill.addedHistoricCount) +
|
||||
", barwidth_after=" + String(backfill.barwidthAfter, 1) +
|
||||
"px (target=" + String(TARGET_BARWIDTH, 1) + "px)");
|
||||
|
||||
int units = cfgobj["units"].as<int>();
|
||||
if (units == 0) units = 1;
|
||||
double tarifkwh;
|
||||
double tariftax = cfgobj["tarifftax"].as<double>();
|
||||
double minPrice = std::numeric_limits<double>::max();
|
||||
double maxPrice = std::numeric_limits<double>::lowest();
|
||||
double prices[n];
|
||||
|
||||
// Create averaged data array
|
||||
AveragedDataPoint avgData[numAveragedSamples];
|
||||
|
||||
// Parse tariff array if provided
|
||||
JsonDocument doc2;
|
||||
JsonArray tariffArray;
|
||||
std::string tariffString = cfgobj["tariffkwh"].as<std::string>();
|
||||
@@ -1708,19 +2068,84 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
Serial.println("Error in tariffkwh array");
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
const JsonObject &obj = doc[i];
|
||||
|
||||
if (tariffArray.size() == 24) {
|
||||
// Average the data according to selected interval
|
||||
for (int i = 0; i < numAveragedSamples; i++) {
|
||||
double priceSum = 0;
|
||||
time_t blockTime = doc[startIndex + i * avgFactor]["time"];
|
||||
|
||||
// Average prices across the interval
|
||||
for (int j = 0; j < avgFactor; j++) {
|
||||
int idx = startIndex + i * avgFactor + j;
|
||||
if (idx >= endIndex) break; // Safety check - respect endIndex
|
||||
|
||||
const JsonObject& obj = doc[idx];
|
||||
const time_t item_time = obj["time"];
|
||||
struct tm item_timeinfo;
|
||||
localtime_r(&item_time, &item_timeinfo);
|
||||
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
if (tariffArray.size() == 24) {
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
}
|
||||
|
||||
double price = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
priceSum += price;
|
||||
}
|
||||
prices[i] = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
|
||||
// Store averaged data
|
||||
avgData[i].price = priceSum / avgFactor;
|
||||
avgData[i].time = blockTime;
|
||||
struct tm blockTimeInfo;
|
||||
localtime_r(&blockTime, &blockTimeInfo);
|
||||
avgData[i].hour = blockTimeInfo.tm_hour;
|
||||
avgData[i].minute = blockTimeInfo.tm_min;
|
||||
}
|
||||
|
||||
// Now work with averaged data (n becomes numAveragedSamples)
|
||||
int n = numAveragedSamples;
|
||||
|
||||
// Calculate RAW current price (find closest PAST/CURRENT data point, never future)
|
||||
if (std::isnan(rawCurrentPrice)) {
|
||||
time_t closestTimeDiff = std::numeric_limits<time_t>::max();
|
||||
|
||||
for (int i = 0; i < originalDataSize; i++) {
|
||||
time_t dataTime = doc[i]["time"];
|
||||
|
||||
// Skip future timestamps - only consider past and current
|
||||
if (dataTime > now) continue;
|
||||
|
||||
time_t diff = now - dataTime;
|
||||
|
||||
if (diff < closestTimeDiff) {
|
||||
closestTimeDiff = diff;
|
||||
rawCurrentIndex = i;
|
||||
|
||||
// Calculate raw price with tariff (same logic as averaging loop)
|
||||
struct tm item_timeinfo;
|
||||
localtime_r(&dataTime, &item_timeinfo);
|
||||
|
||||
if (tariffArray.size() == 24) {
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
}
|
||||
|
||||
rawCurrentPrice = (doc[i]["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
rawCurrentHour = item_timeinfo.tm_hour; // Store the native interval's hour
|
||||
rawCurrentMinute = item_timeinfo.tm_min; // Store the native interval's minute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate min/max and create sorted price array for percentiles
|
||||
double minPrice = std::numeric_limits<double>::max();
|
||||
double maxPrice = std::numeric_limits<double>::lowest();
|
||||
double prices[n];
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
prices[i] = avgData[i].price;
|
||||
minPrice = std::min(minPrice, prices[i]);
|
||||
maxPrice = std::max(maxPrice, prices[i]);
|
||||
}
|
||||
@@ -1742,7 +2167,25 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t barwidth = loc["bars"][1].as<int>() / n;
|
||||
// Step 4: Calculate optimal bar width and spacing
|
||||
// Goal: Fill entire width with bars, using spacing when room allows
|
||||
int availableWidth = loc["bars"][1].as<int>();
|
||||
int pixelsPerBar = availableWidth / n; // How many pixels each bar+spacing can use
|
||||
int barwidth;
|
||||
int barSpacing;
|
||||
|
||||
if (pixelsPerBar >= 3) {
|
||||
// Room for spacing - use 1px gap between bars
|
||||
barSpacing = 1;
|
||||
barwidth = pixelsPerBar - barSpacing; // Remaining pixels for bar itself
|
||||
} else {
|
||||
// Tight fit - no room for spacing
|
||||
barSpacing = 0;
|
||||
barwidth = pixelsPerBar; // Use all available pixels for bar
|
||||
}
|
||||
|
||||
// Result: n × (barwidth + barSpacing) ≈ availableWidth (within rounding)
|
||||
|
||||
uint16_t barheight = loc["bars"][2].as<int>() / (maxPrice - minPrice);
|
||||
uint16_t arrowY = 0;
|
||||
if (loc["bars"].size() >= 5) arrowY = loc["bars"][4].as<int>();
|
||||
@@ -1751,43 +2194,148 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
bool showcurrent = true;
|
||||
if (cfgobj["showcurr"] && cfgobj["showcurr"] == "0") showcurrent = false;
|
||||
|
||||
// Calculate label interval based on display width and number of bars
|
||||
// Goal: Space labels far enough apart to avoid overlap
|
||||
// Note: availableWidth already declared above, reuse it here
|
||||
int maxLabels = availableWidth / MIN_LABEL_SPACING;
|
||||
int labelInterval = (n + maxLabels - 1) / maxLabels; // Round up division
|
||||
|
||||
// Ensure labelInterval is at least 1
|
||||
if (labelInterval < 1) labelInterval = 1;
|
||||
|
||||
// For better aesthetics, round to nearest multiple based on target interval
|
||||
int labelsPerHour = 60 / targetIntervalMinutes;
|
||||
if (labelsPerHour > 0) {
|
||||
// Try to show labels at nice intervals (every hour, every 2 hours, etc.)
|
||||
int hoursPerLabel = (labelInterval + labelsPerHour - 1) / labelsPerHour; // Round up
|
||||
if (hoursPerLabel < 1) hoursPerLabel = 1;
|
||||
labelInterval = hoursPerLabel * labelsPerHour;
|
||||
|
||||
// But don't exceed our max labels constraint
|
||||
if (n / labelInterval > maxLabels) {
|
||||
// Fall back to spacing-based calculation
|
||||
labelInterval = (n + maxLabels - 1) / maxLabels;
|
||||
}
|
||||
}
|
||||
|
||||
// Find cheapest consecutive block(s) for appliance scheduling (Step 5)
|
||||
double cheapBlockHoursRaw = cfgobj["cheapblock"] ? cfgobj["cheapblock"].as<double>() : 3.0;
|
||||
int blockSamples = (int)(cheapBlockHoursRaw * labelsPerHour); // Convert hours to sample count
|
||||
|
||||
CheapBlocks cheapBlocks = findCheapestConsecutiveBlocks(avgData, n, blockSamples, now);
|
||||
int cheapBlockStart1 = cheapBlocks.block1.start;
|
||||
int cheapBlockEnd1 = cheapBlocks.block1.end;
|
||||
int cheapBlockStart2 = cheapBlocks.block2.start;
|
||||
int cheapBlockEnd2 = cheapBlocks.block2.end;
|
||||
|
||||
// Store pointer position for drawing after all bars
|
||||
int pointerX = -1; // -1 means no pointer to draw
|
||||
int currentHour = 0; // Hour of the current interval
|
||||
int currentMinute = 0; // Minute of the current interval
|
||||
int currentBarHeight = 0; // Height of the current bar (for narrow bar line skipping)
|
||||
|
||||
// Draw light grey background for cheapest blocks (BEFORE labels/ticks so they draw on top)
|
||||
int baselineY = spr.height() - barBottom;
|
||||
int xAxisHeight = 20; // Height of X-axis area (ticks + labels)
|
||||
|
||||
if (cheapBlockStart1 >= 0 && cheapBlockEnd1 >= 0) {
|
||||
int rectX = barX + cheapBlockStart1 * (barwidth + barSpacing);
|
||||
int rectWidth = (cheapBlockEnd1 - cheapBlockStart1 + 1) * (barwidth + barSpacing);
|
||||
spr.fillRect(rectX, baselineY + 1, rectWidth, xAxisHeight - 1, TFT_LIGHTGREY);
|
||||
}
|
||||
if (cheapBlockStart2 >= 0 && cheapBlockEnd2 >= 0) {
|
||||
int rectX = barX + cheapBlockStart2 * (barwidth + barSpacing);
|
||||
int rectWidth = (cheapBlockEnd2 - cheapBlockStart2 + 1) * (barwidth + barSpacing);
|
||||
spr.fillRect(rectX, baselineY + 1, rectWidth, xAxisHeight - 1, TFT_LIGHTGREY);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const JsonObject &obj = doc[i];
|
||||
const time_t item_time = obj["time"];
|
||||
struct tm item_timeinfo;
|
||||
localtime_r(&item_time, &item_timeinfo);
|
||||
// Get data from avgData array (already trimmed to visible range)
|
||||
const time_t item_time = avgData[i].time;
|
||||
const int item_hour = avgData[i].hour;
|
||||
const int item_minute = avgData[i].minute;
|
||||
const double price = avgData[i].price;
|
||||
|
||||
if (tariffArray.size() == 24) {
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
}
|
||||
|
||||
const double price = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
|
||||
uint16_t barcolor = getPercentileColor(prices, n, price, imageParams.hwdata);
|
||||
uint16_t barcolor = getPercentileColor(prices, numAveragedSamples, price, imageParams.hwdata);
|
||||
uint16_t thisbarh = mapDouble(price, minPrice, maxPrice, 0, loc["bars"][2].as<int>());
|
||||
spr.fillRect(barX + i * barwidth, spr.height() - barBottom - thisbarh, barwidth - 1, thisbarh, barcolor);
|
||||
if (i % 2 == 0 && loc["time"][0]) {
|
||||
drawString(spr, String(item_timeinfo.tm_hour), barX + i * barwidth + barwidth / 3 + 1, spr.height() - barBottom + 3, loc["time"][0], TC_DATUM, TFT_BLACK);
|
||||
spr.fillRect(barX + i * (barwidth + barSpacing), spr.height() - barBottom - thisbarh, barwidth, thisbarh, barcolor);
|
||||
|
||||
// Draw tickmarks and labels at appropriate intervals
|
||||
int labelX = barX + i * (barwidth + barSpacing) + barwidth / 2;
|
||||
int tickY = spr.height() - barBottom;
|
||||
|
||||
// Check if this bar represents the start of an hour (minute = 0)
|
||||
bool isHourStart = (item_minute == 0);
|
||||
// Check if this bar represents a half-hour (minute = 30)
|
||||
bool isHalfHour = (item_minute == 30);
|
||||
|
||||
// Skip tick marks on displays with limited vertical space (height < 150px)
|
||||
// to prevent labels from being pushed off the bottom edge
|
||||
bool skipTicks = (spr.height() < 150);
|
||||
|
||||
// Every 2 hours: Black tick (4px) + label
|
||||
if (i % labelInterval == 0 && loc["time"][0]) {
|
||||
if (!skipTicks) {
|
||||
spr.drawLine(labelX, tickY, labelX, tickY + 4, TFT_BLACK);
|
||||
}
|
||||
drawString(spr, String(item_hour), labelX, tickY + (skipTicks ? 3 : 6), loc["time"][0], TC_DATUM, TFT_BLACK);
|
||||
}
|
||||
// Every hour (not already labeled): Dark grey tick (3px)
|
||||
else if (isHourStart && !skipTicks) {
|
||||
spr.drawLine(labelX, tickY, labelX, tickY + 3, getColor("darkgray"));
|
||||
}
|
||||
// Every half hour: Very short dark grey tick (1px)
|
||||
else if (isHalfHour && !skipTicks) {
|
||||
spr.drawPixel(labelX, tickY + 1, getColor("darkgray"));
|
||||
}
|
||||
|
||||
if (now - item_time < 3600 && std::isnan(pricenow) && showcurrent) {
|
||||
spr.fillRect(barX + i * barwidth + (barwidth > 6 ? 3 : 1), 5 + arrowY, (barwidth > 6 ? barwidth - 6 : barwidth - 2), 10, imageParams.highlightColor);
|
||||
spr.fillTriangle(barX + i * barwidth, 15 + arrowY,
|
||||
barX + i * barwidth + barwidth - 1, 15 + arrowY,
|
||||
barX + i * barwidth + (barwidth - 1) / 2, 15 + barwidth + arrowY, imageParams.highlightColor);
|
||||
spr.drawLine(barX + i * barwidth + (barwidth - 1) / 2, 20 + barwidth + arrowY, barX + i * barwidth + (barwidth - 1) / 2, spr.height(), TFT_BLACK);
|
||||
pricenow = price;
|
||||
// Store pointer position for drawing after all bars
|
||||
// Use RAW current price (stored before averaging) for accurate display
|
||||
// Find the bar that contains "now" by checking if now falls within this bar's time interval
|
||||
// Important: Use targetIntervalMinutes (the actual bar interval after averaging/display)
|
||||
if (std::isnan(pricenow) && showcurrent) {
|
||||
time_t barEndTime = item_time + (targetIntervalMinutes * 60); // End of this bar's time range
|
||||
if (now >= item_time && now < barEndTime) {
|
||||
pointerX = barX + i * (barwidth + barSpacing) + barwidth / 2; // Center of the bar
|
||||
pricenow = rawCurrentPrice; // Use raw price, not averaged
|
||||
currentHour = rawCurrentHour; // Use native interval's hour (not averaged block)
|
||||
currentMinute = rawCurrentMinute; // Use native interval's minute (not averaged block)
|
||||
currentBarHeight = thisbarh; // Store bar height for line drawing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw current hour pointer AFTER all bars (so it won't be chopped off)
|
||||
// Use constant narrow stem with wider arrow head for a proper arrow shape
|
||||
if (pointerX >= 0 && showcurrent) {
|
||||
// Arrow stem (narrow rectangle)
|
||||
spr.fillRect(pointerX - STEM_WIDTH / 2, 5 + arrowY, STEM_WIDTH, STEM_HEIGHT, imageParams.highlightColor);
|
||||
// Arrow head (wider triangle)
|
||||
spr.fillTriangle(pointerX - ARROW_WIDTH / 2, 15 + arrowY,
|
||||
pointerX + ARROW_WIDTH / 2, 15 + arrowY,
|
||||
pointerX, 15 + arrowY + ARROW_HEIGHT, imageParams.highlightColor);
|
||||
|
||||
// Vertical line: dashed from arrow to bar, solid from baseline to bottom
|
||||
int lineStartY = 21 + arrowY + GAP_AFTER_ARROW;
|
||||
int baselineY = spr.height() - barBottom;
|
||||
int barTopY = baselineY - currentBarHeight;
|
||||
|
||||
// Draw DASHED line from arrow to top of bar (with gap) - for visual clarity
|
||||
for (int y = lineStartY; y < barTopY - 2; y += DASH_LENGTH + GAP_LENGTH) {
|
||||
int segmentEnd = min(y + DASH_LENGTH, barTopY - 2);
|
||||
spr.drawLine(pointerX, y, pointerX, segmentEnd, TFT_BLACK);
|
||||
}
|
||||
|
||||
// Draw solid line from baseline to bottom (for time indication in X-axis area)
|
||||
spr.drawLine(pointerX, baselineY, pointerX, spr.height(), TFT_BLACK);
|
||||
}
|
||||
|
||||
if (showcurrent) {
|
||||
if (barwidth < 5) {
|
||||
drawString(spr, String(timeinfo.tm_hour) + ":00", spr.width() / 2, 5, "calibrib16.vlw", TC_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(currentHour) + ":" + (currentMinute < 10 ? "0" : "") + String(currentMinute), spr.width() / 2, 5, "calibrib16.vlw", TC_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(pricenow) + "/kWh", spr.width() / 2, 25, loc["head"][0], TC_DATUM, TFT_BLACK, 30);
|
||||
} else {
|
||||
drawString(spr, String(timeinfo.tm_hour) + ":00", barX, 5, loc["head"][0], TL_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(currentHour) + ":" + (currentMinute < 10 ? "0" : "") + String(currentMinute), barX, 5, loc["head"][0], TL_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(pricenow) + "/kWh", spr.width() - barX, 5, loc["head"][0], TR_DATUM, TFT_BLACK, 30);
|
||||
}
|
||||
}
|
||||
@@ -2587,7 +3135,7 @@ uint32_t convert_tm_to_seconds(struct tm *t) {
|
||||
|
||||
void prepareTIME_RAW(const uint8_t *dst, time_t now) {
|
||||
uint8_t *data;
|
||||
size_t len = 1 + 4 + 4;
|
||||
size_t len = 1 + 4 + 4 + 1;
|
||||
struct tm timeinfo;
|
||||
localtime_r(&now, &timeinfo);
|
||||
uint32_t local_time = convert_tm_to_seconds(&timeinfo) + 20;// Adding 20 seconds for the average of upload time
|
||||
@@ -2603,6 +3151,7 @@ void prepareTIME_RAW(const uint8_t *dst, time_t now) {
|
||||
data[7] = ((uint8_t*)&unix_time)[1];
|
||||
data[8] = ((uint8_t*)&unix_time)[2];
|
||||
data[9] = ((uint8_t*)&unix_time)[3];
|
||||
data[10] = 1; // Design, 1 = Full DateTime 2 = Just Time Segment Style
|
||||
prepareDataAvail(data, len + 1, DATATYPE_TIME_RAW_DATA, dst);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -365,23 +365,59 @@ uint8_t *g5Compress(uint16_t width, uint16_t height, uint8_t *buffer, uint16_t b
|
||||
}
|
||||
#endif
|
||||
|
||||
void doTimestamp(TFT_eSprite *spr) {
|
||||
// The "ts_option" is a bitmapped variable with a default value of 1
|
||||
// which is black on white, long format @ bottom right.
|
||||
//
|
||||
// b2, b1, b0:
|
||||
// 0 - no timestamp
|
||||
// 1 - bottom right
|
||||
// 2 - top right
|
||||
// 3 - bottom left
|
||||
// 4 - top left
|
||||
// 5 -> 7 reserved
|
||||
// b3:
|
||||
// 0 - long format (year-month-day hr:min)
|
||||
// 1 - short format (month-day hr:min)
|
||||
// b4:
|
||||
// 0 - black on white
|
||||
// 1 - white on black
|
||||
// b5 -> b7: reserved
|
||||
//
|
||||
void doTimestamp(TFT_eSprite *spr, uint8_t ts_option) {
|
||||
time_t now = time(nullptr);
|
||||
struct tm *timeinfo = localtime(&now);
|
||||
char buffer[20];
|
||||
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M", timeinfo);
|
||||
strftime(buffer, sizeof(buffer),
|
||||
(ts_option & 0x8) ? "%m-%d %H:%M" : "%Y-%m-%d %H:%M",timeinfo);
|
||||
int ts_chars = strlen(buffer);
|
||||
|
||||
// spr->drawRect(spr->width() - 16 * 6 - 4, spr->height() - 10 - 2, 16 * 6 + 3, 11, TFT_BLACK);
|
||||
spr->drawRect(spr->width() - 16 * 6 - 3, spr->height() - 10 - 1, 16 * 6 + 1, 9, TFT_WHITE);
|
||||
spr->setTextColor(TFT_BLACK, TFT_WHITE);
|
||||
spr->setCursor(spr->width() - 16 * 6 - 2, spr->height() - 10, 1);
|
||||
uint16_t char_color;
|
||||
uint16_t bg_color;
|
||||
|
||||
if(ts_option & 0x10) {
|
||||
char_color = TFT_WHITE;
|
||||
bg_color= TFT_BLACK;
|
||||
}
|
||||
else {
|
||||
char_color = TFT_BLACK;
|
||||
bg_color = TFT_WHITE;
|
||||
}
|
||||
|
||||
ts_option = (ts_option & 0x3) - 1;
|
||||
|
||||
int32_t ts_x = (ts_option & 2) ? 1 : spr->width() - ts_chars * 6 - 2;
|
||||
int32_t ts_y = (ts_option & 1) ? 1 : spr->height() - 10;
|
||||
|
||||
spr->drawRect(ts_x - 1, ts_y - 1, ts_chars * 6 + 1, 9, bg_color);
|
||||
spr->setTextColor(char_color, bg_color);
|
||||
spr->setCursor(ts_x,ts_y);
|
||||
spr->print(buffer);
|
||||
}
|
||||
|
||||
void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
|
||||
long t = millis();
|
||||
|
||||
if (config.showtimestamp) doTimestamp(&spr);
|
||||
if (imageParams.ts_option) doTimestamp(&spr,imageParams.ts_option);
|
||||
#ifdef HAS_TFT
|
||||
extern uint8_t YellowSense;
|
||||
if (fileout == "direct") {
|
||||
@@ -425,7 +461,7 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
|
||||
case 1:
|
||||
case 2: {
|
||||
long bufw = spr.width(), bufh = spr.height();
|
||||
size_t buffer_size = (bufw * bufh) / 8;
|
||||
size_t buffer_size = ((bufw * bufh) + 7) / 8; // round up: not all dimensions are multiples of 8
|
||||
#ifdef BOARD_HAS_PSRAM
|
||||
uint8_t *buffer = (uint8_t *)ps_malloc(buffer_size);
|
||||
#else
|
||||
@@ -549,7 +585,7 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
|
||||
case 3:
|
||||
case 4: {
|
||||
long bufw = spr.width(), bufh = spr.height();
|
||||
size_t buffer_size = (bufw * bufh) / 8 * imageParams.bpp;
|
||||
size_t buffer_size = ((bufw * bufh) + 7) / 8 * imageParams.bpp;
|
||||
uint8_t *buffer = (uint8_t *)ps_malloc(buffer_size);
|
||||
if (!buffer) {
|
||||
Serial.println("Failed to allocate buffer");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <Arduino.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <system.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "commstructs.h"
|
||||
#include "contentmanager.h"
|
||||
|
||||
@@ -264,7 +264,19 @@ void nrfswd::write_register(uint32_t address, uint32_t value) {
|
||||
bool state3 = DP_Read(DP_RDBUFF, temp);
|
||||
// if (showDebug) Serial.printf("%i%i%i Write Register: 0x%08x : 0x%08x\r\n", state1, state2, state3, address, value);
|
||||
}
|
||||
|
||||
uint8_t nrfswd::nrf_erase_all() {
|
||||
nrf_port_selection(1);
|
||||
nrf_write_port(1, AP_NRF_ERASEALL, 1);
|
||||
long timeout = millis();
|
||||
while (nrf_read_port(1, AP_NRF_ERASEALLSTATUS)) {
|
||||
if (millis() - timeout > 1000) return 1;
|
||||
}
|
||||
nrf_write_port(1, AP_NRF_ERASEALL, 0);
|
||||
nrf_port_selection(0);
|
||||
nrf_soft_reset();
|
||||
init();
|
||||
return 0;
|
||||
}
|
||||
uint8_t nrfswd::erase_all_flash() {
|
||||
write_register(0x4001e504, 2);
|
||||
long timeout = millis();
|
||||
|
||||
@@ -306,6 +306,7 @@ typedef enum {
|
||||
|
||||
CMD_ERASE_FLASH = 26,
|
||||
CMD_ERASE_INFOPAGE = 27,
|
||||
CMD_ERASE_ALL = 28,
|
||||
CMD_SAVE_MAC_FROM_FW = 40,
|
||||
CMD_PASS_THROUGH = 50,
|
||||
|
||||
@@ -420,6 +421,16 @@ void processFlasherCommand(struct flasherCommand* cmd, uint8_t transportType) {
|
||||
}
|
||||
sendFlasherAnswer(CMD_ERASE_INFOPAGE, NULL, 0, transportType);
|
||||
break;
|
||||
case CMD_ERASE_ALL:
|
||||
if (selectedController == CONTROLLER_NRF82511) {
|
||||
if (nrfflasherp == nullptr) return;
|
||||
nrfflasherp->nrf_erase_all();
|
||||
} else if (selectedController == CONTROLLER_CC) {
|
||||
if (ccflasherp == nullptr) return;
|
||||
ccflasherp->erase_chip();
|
||||
}
|
||||
sendFlasherAnswer(CMD_ERASE_ALL, NULL, 0, transportType);
|
||||
break;
|
||||
case CMD_SELECT_PORT:
|
||||
wsSerial("> select port");
|
||||
selectedFlasherPort = cmd->data[0];
|
||||
@@ -453,7 +464,7 @@ void processFlasherCommand(struct flasherCommand* cmd, uint8_t transportType) {
|
||||
break;
|
||||
}
|
||||
nrfflasherp->init();
|
||||
temp_buff[0] = (nrfflasherp->isConnected && !nrfflasherp->isLocked);
|
||||
temp_buff[0] = nrfflasherp->isConnected ? (nrfflasherp->isLocked ? 2 : 1) : 0;
|
||||
sendFlasherAnswer(CMD_SELECT_NRF82511, temp_buff, 1, transportType);
|
||||
currentFlasherOffset = 0;
|
||||
selectedController = CONTROLLER_NRF82511;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "tag_db.h"
|
||||
#include "udp.h"
|
||||
#include "wifimanager.h"
|
||||
#include <sys/time.h>
|
||||
|
||||
#ifdef HAS_EXT_FLASHER
|
||||
#include "webflasher.h"
|
||||
@@ -648,6 +649,28 @@ void init_web() {
|
||||
request->send(200, "text/plain", "Ok, saved");
|
||||
});
|
||||
|
||||
// Allow external time sync (e.g., from Home Assistant) without Internet
|
||||
// Usage: POST /set_time with form field 'epoch' (UNIX time seconds)
|
||||
server.on("/set_time", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
if (request->hasParam("epoch", true)) {
|
||||
time_t epoch = static_cast<time_t>(request->getParam("epoch", true)->value().toInt());
|
||||
if (epoch > 1600000000) { // basic sanity check (~2020-09-13)
|
||||
struct timeval tv;
|
||||
tv.tv_sec = epoch;
|
||||
tv.tv_usec = 0;
|
||||
settimeofday(&tv, nullptr);
|
||||
logLine("Time set via /set_time");
|
||||
wsSendSysteminfo();
|
||||
request->send(200, "text/plain", "ok");
|
||||
return;
|
||||
} else {
|
||||
request->send(400, "text/plain", "invalid epoch");
|
||||
return;
|
||||
}
|
||||
}
|
||||
request->send(400, "text/plain", "missing 'epoch'");
|
||||
});
|
||||
|
||||
server.on("/set_var", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
if (request->hasParam("key", true) && request->hasParam("val", true)) {
|
||||
std::string key = request->getParam("key", true)->value().c_str();
|
||||
|
||||
@@ -451,6 +451,29 @@
|
||||
"0": "No",
|
||||
"1": "-Yes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "interval",
|
||||
"name": "Display interval",
|
||||
"desc": "Data averaging interval for better readability on smaller displays",
|
||||
"type": "select",
|
||||
"options": {
|
||||
"0": "-Native (15-min or hourly)",
|
||||
"30": "30 minutes",
|
||||
"60": "1 hour"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "cheapblock",
|
||||
"name": "Cheap block hours",
|
||||
"desc": "Number of hours for cheapest consecutive block (e.g., 3 for dishwasher, 2.5 for 2h30m). Set to 0 to disable. Shows up to 2 non-overlapping blocks.",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "updatefreq",
|
||||
"name": "Update frequency",
|
||||
"desc": "Tag refresh interval in minutes (e.g., 15, 30, 60). Default is 15 minutes to match 15-minute data intervals. Higher values save battery.",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1084,7 +1084,7 @@ function contentselected() {
|
||||
fetch('edit?list=%2F&recursive=1')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let files = data.filter(item => item.type === "file" && item.name.endsWith(".jpg"));
|
||||
let files = data.filter(item => item.type === "file" && (item.name.toLowerCase().endsWith(".jpg") || item.name.toLowerCase().endsWith(".jpeg")));
|
||||
if (element.type == 'binfile') files = data.filter(item => item.type === "file" && item.name.endsWith(".bin"));
|
||||
if (element.type == 'jsonfile') files = data.filter(item => item.type === "file" && item.name.endsWith(".json"));
|
||||
const optionElement = document.createElement("option");
|
||||
|
||||
@@ -215,7 +215,7 @@ function startPainter(mac, width, height, tagtype) {
|
||||
canvas.addEventListener('touchend', handleTouchEnd);
|
||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: true });
|
||||
|
||||
var sizes = [10,11,12,13,14,16,18,20,24,28,32,36,40,48,56,64,72,84];
|
||||
var sizes = [10,11,12,13,14,16,18,20,24,28,32,36,40,48,56,64,72,84,96,108,120,144,168,192,256,320,384,480,512];
|
||||
|
||||
const fontSelect = document.createElement('select');
|
||||
fontSelect.id = 'font-select';
|
||||
|
||||
@@ -5,6 +5,13 @@ This is an alternative firmware and protocol for the multiple Electronic Shelf L
|
||||
The software in this project consists of two parts: Accesspoint-firmware and Tag firmware.
|
||||
Additionally, there are various hardware designs for accesspoints and flasher-interfaces to program the tags, preferably using programming jigs
|
||||
|
||||
>[!Note]
|
||||
>Please refer to the [Wiki](https://github.com/jjwbruijn/OpenEPaperLink/wiki) for the latest information.
|
||||
>
|
||||
>Much of this README is now obsolete, but it has been retained for historical reference.
|
||||
>
|
||||
>For example the use of tags as RF coprocessors is no longer supported.
|
||||
|
||||
## Aims
|
||||
- Low power (currently around 9µA with a minimum of 40 second latency)
|
||||
- Even lower power when there's no AP around
|
||||
|
||||
@@ -120,6 +120,9 @@ python3 OEPL-Flasher.py -e -c -p COM31 read blaat.bin --flash
|
||||
See this [page](https://github.com/OpenEPaperLink/OpenEPaperLink/wiki/Chroma-Series-SubGhz-Tags#flashing-cc1110-based-chroma-tags)
|
||||
on the Wiki for additional information.
|
||||
|
||||
## EFR32-based
|
||||
|
||||
This flasher does **NOT** support tags based on the EFR32. See the [wiki](https://github.com/OpenEPaperLink/OpenEPaperLink/wiki/Flashing-SiLabs-based-M3-Newton-Displays) for information on how to flash those.
|
||||
## Credits
|
||||
|
||||
Much code was reused from ATC1441's various flashers
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Google Calendar
|
||||
|
||||
To use the 'clandar' content type, you need a helper script, using Google Apps Script. To use Google Apps Script to get all events for the next day and return them via JSON in a web app, you can follow these steps:
|
||||
To use the 'calendar' content type, you need a helper script, using Google Apps Script. To use Google Apps Script to get all events for the next day and return them via JSON in a web app, you can follow these steps:
|
||||
|
||||
* Create a new Google Apps Script project by going to [https://script.google.com](https://script.google.com) and clicking on "New project".
|
||||
* Paste the content of the calendar.js file into the editor
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 4,
|
||||
"version": 5,
|
||||
"name": "M2 2.9\"",
|
||||
"width": 296,
|
||||
"height": 128,
|
||||
@@ -43,7 +43,8 @@
|
||||
"day": [ 30, 18, "fonts/twcondensed20", 41, 108 ],
|
||||
"icon": [ 30, 55, 30 ],
|
||||
"wind": [ 18, 26 ],
|
||||
"line": [ 20, 128 ]
|
||||
"line": [ 20, 128 ],
|
||||
"ts_option": 10
|
||||
},
|
||||
"9": {
|
||||
"title": [ 2, 0, "bahnschrift20.vlw", 25 ],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "M3 1.6\" BWRY",
|
||||
"width": 168,
|
||||
"height": 168,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 2,
|
||||
"zlib_compression": "27",
|
||||
|
||||
25
resources/tagtypes/2B.json
Normal file
25
resources/tagtypes/2B.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "M3 2.9\" BWRY",
|
||||
"width": 384,
|
||||
"height": 168,
|
||||
"rotatebuffer": 3,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 16, 17, 18, 19, 20, 21, 27 ],
|
||||
"usetemplate": 51
|
||||
}
|
||||
25
resources/tagtypes/2C.json
Normal file
25
resources/tagtypes/2C.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "M3 4.3\" BWRY",
|
||||
"width": 522,
|
||||
"height": 152,
|
||||
"rotatebuffer": 3,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 16, 17, 18, 19, 20, 21, 27 ],
|
||||
"usetemplate": 47
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"name": "M3 4.2\" BWY",
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
@@ -15,6 +15,7 @@
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"highlight_color": 3,
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "button", "led" ],
|
||||
|
||||
25
resources/tagtypes/4A.json
Normal file
25
resources/tagtypes/4A.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "M3 1.6\" 200px BWRY",
|
||||
"width": 200,
|
||||
"height": 200,
|
||||
"rotatebuffer": 0,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 16, 17, 18, 19, 20, 21, 27 ],
|
||||
"usetemplate": 48
|
||||
}
|
||||
25
resources/tagtypes/4B.json
Normal file
25
resources/tagtypes/4B.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "M3 2.2\" BWRY",
|
||||
"width": 296,
|
||||
"height": 160,
|
||||
"rotatebuffer": 3,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 16, 17, 18, 19, 20, 21, 27 ],
|
||||
"usetemplate": 49
|
||||
}
|
||||
25
resources/tagtypes/4C.json
Normal file
25
resources/tagtypes/4C.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "M3 7.5\" BWRY",
|
||||
"width": 800,
|
||||
"height": 480,
|
||||
"rotatebuffer": 0,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "led" ],
|
||||
"contentids": [ 22, 23, 1, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20 ],
|
||||
"usetemplate": 54
|
||||
}
|
||||
25
resources/tagtypes/4D.json
Normal file
25
resources/tagtypes/4D.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 3,
|
||||
"name": "M3 11.6\" BWRY",
|
||||
"width": 960,
|
||||
"height": 640,
|
||||
"rotatebuffer": 0,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "led" ],
|
||||
"contentids": [ 22, 23, 1, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20 ],
|
||||
"usetemplate": 55
|
||||
}
|
||||
18
resources/tagtypes/4E.json
Normal file
18
resources/tagtypes/4E.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 2,
|
||||
"name": "M3 2.6\" BW",
|
||||
"width": 360,
|
||||
"height": 184,
|
||||
"rotatebuffer": 3,
|
||||
"bpp": 1,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ]
|
||||
},
|
||||
"highlight_color": 5,
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "button", "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 26, 27 ],
|
||||
"usetemplate": 1
|
||||
}
|
||||
25
resources/tagtypes/4F.json
Executable file
25
resources/tagtypes/4F.json
Executable file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "M3 2.6\" BWRY",
|
||||
"width": 360,
|
||||
"height": 184,
|
||||
"rotatebuffer": 3,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "button", "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 26, 27 ],
|
||||
"usetemplate": 1
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 4,
|
||||
"name": "HD150 5.83\" BWR",
|
||||
"width": 648,
|
||||
"height": 480,
|
||||
@@ -21,21 +21,44 @@
|
||||
"month": [ 300, 310, "Signika-SB.ttf", 110 ],
|
||||
"day": [ 300, 75, "Signika-SB.ttf", 250 ]
|
||||
},
|
||||
"10": {
|
||||
"title": [ 300, 10, "fonts/bahnschrift30" ],
|
||||
"pos": [ 300, 50 ]
|
||||
"4": {
|
||||
"location": [ 41, 25, "fonts/calibrib80" ],
|
||||
"wind": [ 177, 225, "fonts/calibrib30" ],
|
||||
"temp": [ 46, 300, "fonts/calibrib100" ],
|
||||
"icon": [ 527, 100, 150, 2 ],
|
||||
"dir": [ 122, 75, 180 ],
|
||||
"umbrella": [ 582, 313, 102 ]
|
||||
},
|
||||
"8": {
|
||||
"location": [ 10, 0, "fonts/calibrib80" ],
|
||||
"column": [ 7, 92 ],
|
||||
"day": [ 43, 138, "fonts/calibrib30", 188, 356 ],
|
||||
"rain": [ 49, 425 ],
|
||||
"icon": [ 46, 225, 60 ],
|
||||
"wind": [ 17, 150 ],
|
||||
"line": [ 96, 463 ]
|
||||
},
|
||||
"9": {
|
||||
"title": [ 6, 0, "Signika-SB.ttf", 32 ],
|
||||
"items": 6,
|
||||
"line": [ 9, 40, "calibrib16.vlw" ],
|
||||
"desc": [ 2, 8, "REFSAN12.vlw", 1.2 ]
|
||||
"line": [ 9, 50, "calibrib16.vlw" ],
|
||||
"desc": [ 2, 10, "REFSAN12.vlw", 1.2 ]
|
||||
},
|
||||
"10": {
|
||||
"title": [ 324, 13, "fonts/bahnschrift20" ],
|
||||
"pos": [ 324, 50 ]
|
||||
},
|
||||
"11": {
|
||||
"rotate": 0,
|
||||
"mode": 1,
|
||||
"days": 7,
|
||||
"gridparam": [ 7, 17, 30, "calibrib16.vlw", "tahoma9.vlw", 14 ]
|
||||
"gridparam": [ 3, 21, 38, "calibrib16.vlw", "tahoma9.vlw", 14 ]
|
||||
},
|
||||
"27": {
|
||||
"bars": [ 18, 620, 413, 20, 22 ],
|
||||
"time": [ "calibrib16.vlw" ],
|
||||
"yaxis": [ "calibrib16.vlw", 1, 12 ],
|
||||
"head": [ "calibrib30.vlw" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 7,5\"",
|
||||
"width": 800,
|
||||
"height": 480,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 2,00\"",
|
||||
"width": 152,
|
||||
"height": 200,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 3,5\"",
|
||||
"width": 384,
|
||||
"height": 184,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 2,9\"",
|
||||
"width": 296,
|
||||
"height": 128,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"zlib_compression": "27",
|
||||
"options": [ "button", "led" ],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "HS BWRY 2,60\"",
|
||||
"width": 296,
|
||||
"height": 152,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
18
resources/tagtypes/70.json
Normal file
18
resources/tagtypes/70.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "HS 2.9\" HighRes",
|
||||
"width": 384,
|
||||
"height": 168,
|
||||
"rotatebuffer": 1,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "button", "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 26, 27 ],
|
||||
"usetemplate": 51
|
||||
}
|
||||
18
resources/tagtypes/71.json
Normal file
18
resources/tagtypes/71.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "HS 2.13\" BWR High Res",
|
||||
"width": 296,
|
||||
"height": 144,
|
||||
"rotatebuffer": 1,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ]
|
||||
},
|
||||
"zlib_compression": "27",
|
||||
"shortlut": 2,
|
||||
"options": [ "button" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 27 ],
|
||||
"usetemplate": 1
|
||||
}
|
||||
25
resources/tagtypes/90.json
Normal file
25
resources/tagtypes/90.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 3,
|
||||
"name": "M3 4.2\" BWRY",
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
"rotatebuffer": 0,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
"options": [ "button", "led" ],
|
||||
"contentids": [ 22, 23, 1, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20, 27 ],
|
||||
"usetemplate": 2
|
||||
}
|
||||
24
resources/tagtypes/91.json
Executable file
24
resources/tagtypes/91.json
Executable file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "M3 1.6\" 200px BWRY",
|
||||
"width": 200,
|
||||
"height": 200,
|
||||
"rotatebuffer": 3,
|
||||
"bpp": 2,
|
||||
"colortable": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"options": [ "button", "led" ],
|
||||
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 10, 14, 17, 18, 19, 20, 21 ],
|
||||
"usetemplate": 48
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "TFT 320x172",
|
||||
"width": 320,
|
||||
"height": 172,
|
||||
@@ -23,7 +23,8 @@
|
||||
{ "text": [ 10, 95, "Channel:", "bahnschrift20", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 120, 95, "{ap_ch}", "bahnschrift20", 0, 0, 0, "1" ] },
|
||||
{ "text": [ 10, 120, "Tag count:", "bahnschrift20", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 120, 120, "{ap_tagcount}", "bahnschrift20", 0, 0, 0, "1" ] }
|
||||
{ "text": [ 120, 120, "{ap_tagcount}", "bahnschrift20", 0, 0, 0, "1" ] },
|
||||
{ "ts_option": 17}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "TFT 160x80",
|
||||
"width": 160,
|
||||
"height": 80,
|
||||
@@ -22,7 +22,8 @@
|
||||
{ "text": [ 1, 45, "Ch:", "REFSAN12.vlw", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 45, 45, "{ap_ch}", "REFSAN12.vlw", 0, 0, 0, "1" ] },
|
||||
{ "text": [ 1, 62, "Tags:", "REFSAN12.vlw", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 45, 62, "{ap_tagcount}", "REFSAN12.vlw", 0, 0, 0, "1" ] }
|
||||
{ "text": [ 45, 62, "{ap_tagcount}", "REFSAN12.vlw", 0, 0, 0, "1" ] },
|
||||
{ "ts_option": 25}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user