35 Commits
2.82 ... master

Author SHA1 Message Date
atc1441
eb0064363f Better HD150 5,83" Content design 2026-03-19 09:30:14 +01:00
atc1441
c1324c5089 Better HD150 5.83" Design 2026-03-19 08:54:52 +01:00
Skip Hansen
df3c615eef Update README.md 2026-03-14 08:49:11 -07:00
Nick Waterton
d5e19d20fa Fix buffer size truncation for non-8-aligned image dimensions (#561)
Integer division (w*h)/8 truncates when w*h is not a multiple of 8,
allocating one byte too few. spr2color then writes past the end of
the buffer, corrupting the heap. Use (w*h+7)/8 to round up correctly.

Triggered by any tag whose width*height is not divisible by 8.
2026-03-13 11:35:17 -07:00
alienkenny
e62a1b07bf Missed erase function (#549)
* Update swd.h

* Update swd.cpp

* Update usbflasher.cpp
2026-03-06 10:44:23 -08:00
Skip Hansen
4d06ef546d Point to Wiki for EFR32 tag flashing info from Tag Flasher readme.md. 2026-03-05 06:54:33 -08:00
Ruud
7096f7e756 Created new AP setup using the Lilygo T-Display-S3 board (#423)
* Created new AP setup using the Lilygo T-Display-S3 board

* Updated UseGhz setting
2026-03-04 12:57:48 -08:00
atc1441
1abedff388 Added 2.9 and 2.13 High Res 2026-02-27 14:16:53 +01:00
Steven Cooreman
8d2546a2aa Merge pull request #553 from stevew817/add_m3_42_bwry_definition
Add definition for M3 4.2 BWRY
2026-02-27 01:24:42 +01:00
atc1441
c5a8058d62 Merge branch 'master' of https://github.com/OpenEPaperLink/OpenEPaperLink 2026-02-26 14:21:39 +01:00
atc1441
ef59b87ae0 Update 3C.json 2026-02-26 14:21:38 +01:00
Skip Hansen
d526c43fd8 Added 91.json for the EL016F6W4A M3 1.6" BWRY variant. 2026-02-23 07:13:06 -08:00
Steven
86abc112dd Add definition for M3 4.2 BWRY
https://github.com/OpenEPaperLink/Shared_OEPL_Definitions/pull/5
https://github.com/OpenEPaperLink/Tag_FW_EFR32xG22/pull/16
2026-02-13 20:36:24 +01:00
Skip Hansen
af50f96671 Added 4F for 2.6" BWRY M3. 2026-02-13 07:45:17 -08:00
atc1441
46c8b73fa0 Added V2 ATC_BLE_OEPL Advertising Handling 2026-01-18 14:53:44 +01:00
Steven Cooreman
c65c8b749e Merge pull request #518 from stevew817/master
Add boilerplate definitions for more now-known Solum BWRY tags
2025-12-14 20:06:22 +01:00
atc1441
f61c2e3d04 Update 4E.json 2025-12-11 16:21:05 +01:00
atc1441
0dc406b865 Added M3 2.6" BW 2025-12-11 12:37:56 +01:00
Steven
25b185da28 Use correct base templates for BWRY variants 2025-12-07 19:36:58 +01:00
atc1441
496d4d382f Update contentmanager.cpp 2025-12-03 21:34:48 +01:00
atc1441
d09e89c9dd Updated the WebFiles as well 2025-11-16 22:04:44 +01:00
atc1441
ce319bb499 Merge pull request #526 from urmuzov/master
Fixed the pin order for the display on the ESP32_S3_16_8_4inch_AP
2025-11-16 21:51:47 +01:00
atc1441
4dbcc753fd Merge pull request #505 from scanalyzer/patch-3
Update main.js - Support cased jpeg file extensions.
2025-11-16 21:50:40 +01:00
atc1441
6951cd79b7 Merge pull request #504 from scanalyzer/patch-2
Update painter.js - Added more font sizes to support large emojis on T-Panel and larger displays.
2025-11-16 21:49:50 +01:00
Alexander Urmuzov
eeb18f204d Fixed the pin order for the display on the ESP32_S3_16_8_4inch_AP 2025-11-16 21:47:48 +01:00
Skip Hansen
bfff2ef0b9 Added support for ts_option to provide finer control of time stamps.
1. Fixed time stamp overlap on weather forecast content on  2.9" tags.
2. Fixed time stamp overlap on AP info screen on 160x80 TFT displays.
3. Changed black on white to white on black on TFT displays.
2025-11-06 15:38:21 -05:00
atc1441
ef5ddac368 Added BWRY Preview 2025-10-30 22:59:37 +01:00
Steven
0e8e7b5b75 Add 11.6" BWRY and fix previous BWRY definitions 2025-10-28 23:18:15 +01:00
Steven
e8a92c4fcb Add definitions for more now-known Solum BWRY tags
https://github.com/OpenEPaperLink/Tag_FW_EFR32xG22/issues/11
2025-10-22 01:09:26 +02:00
BenV
299b8f300e Feat: dayahead fixes and improvements (#516) 2025-10-20 19:05:38 +02:00
Nic Limper
0b0802ad02 Update README.md 2025-10-08 14:40:36 +02:00
scanalyzer
f8ce3a51d2 Update main.js
Adding support for cased versions of jpg and jpeg.
2025-09-14 18:55:53 -07:00
scanalyzer
0e63e064fc Update painter.js
Added more font sizes to support things like a single emoji on T-Panel and larger displays.
2025-09-14 18:41:51 -07:00
atc1441
8d6c763aba Merge pull request #496 from 4rianton/Add-the-ability-to-update-time-without-NTP
Ability to update time without requiring Internet Access
2025-09-13 11:04:12 +02:00
4rianton
ab48cbe747 Ability to update time without requiring Internet Access 2025-09-06 14:59:49 +02:00
44 changed files with 1254 additions and 124 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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
#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 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
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;
}

View File

@@ -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

View File

@@ -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");

View File

@@ -3,6 +3,7 @@
#include <Arduino.h>
#include <HardwareSerial.h>
#include <system.h>
#include <WiFi.h>
#include "commstructs.h"
#include "contentmanager.h"

View File

@@ -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();

View File

@@ -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;

View File

@@ -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();

View File

@@ -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"
}
]
},

View File

@@ -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");

View File

@@ -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';

View File

@@ -4,6 +4,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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 ],

View File

@@ -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",

View 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
}

View 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
}

View File

@@ -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" ],

View 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
}

View 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
}

View 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
}

View 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
}

View 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
View 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
}

View File

@@ -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" ]
}
}
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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" ],

View File

@@ -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",

View 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
}

View 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
}

View 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
View 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
}

View File

@@ -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}
]
}
}

View File

@@ -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}
]
}
}