/* xdrv_13_display.ino - Display support for Tasmota Copyright (C) 2021 Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_DISPLAY #define XDRV_13 13 #include Renderer *renderer; enum ColorType { COLOR_BW, COLOR_COLOR }; #ifndef DISP_BATCH_FILE #define DISP_BATCH_FILE "/display.bat" #endif #ifdef USE_UFILESYS extern FS *ufsp; extern FS *ffsp; #endif #ifdef USE_TOUCH_BUTTONS extern VButton *buttons[MAX_TOUCH_BUTTONS]; #endif // drawing color is WHITE // on epaper the whole display buffer is transfered inverted this results in white paper uint16_t fg_color = 1; uint16_t bg_color = 0; uint8_t color_type = COLOR_BW; uint8_t auto_draw = 1; int16_t disp_xpos = 0; int16_t disp_ypos = 0; #ifdef USE_MULTI_DISPLAY #ifndef MAX_MULTI_DISPLAYS #define MAX_MULTI_DISPLAYS 3 #endif struct MULTI_DISP { Renderer *display; uint16_t fg_color; uint16_t bg_color; int16_t disp_xpos; int16_t disp_ypos; uint8_t color_type; uint8_t auto_draw; uint8_t model; uint8_t used; } displays[MAX_MULTI_DISPLAYS]; uint8_t cur_display; Renderer *Init_uDisplay(const char *desc); void Set_display(uint8_t index) { displays[index].display = renderer; displays[index].fg_color = fg_color; displays[index].bg_color = bg_color; displays[index].color_type = color_type; displays[index].auto_draw = auto_draw; displays[index].disp_xpos = disp_xpos; displays[index].disp_ypos = disp_ypos; displays[index].model = Settings->display_model; displays[index].used = 1; cur_display = index; } void Get_display(uint8_t index) { renderer = displays[index].display; fg_color = displays[index].fg_color; bg_color = displays[index].bg_color; color_type = displays[index].color_type; auto_draw = displays[index].auto_draw; disp_xpos = displays[index].disp_xpos; disp_ypos = displays[index].disp_ypos; if (renderer) renderer->setDrawMode(auto_draw >> 1); //Settings->display_model = displays[index].model; cur_display = index; } #endif // USE_MULTI_DISPLAY #ifndef TXT_MAX_SFAC #define TXT_MAX_SFAC 4 #endif // TXT_MAX_SFAC const uint8_t DISPLAY_MAX_DRIVERS = 32; // Max number of display drivers/models supported by xdsp_interface.ino const uint8_t DISPLAY_MAX_COLS = 64; // Max number of columns allowed with command DisplayCols const uint8_t DISPLAY_MAX_ROWS = 64; // Max number of lines allowed with command DisplayRows const uint8_t DISPLAY_LOG_ROWS = 32; // Number of lines in display log buffer #define D_PRFX_DISPLAY "Display" #define D_CMND_DISP_ADDRESS "Address" #define D_CMND_DISP_COLS "Cols" #define D_CMND_DISP_DIMMER "Dimmer" #define D_CMND_DISP_MODE "Mode" #define D_CMND_DISP_MODEL "Model" #define D_CMND_DISP_TYPE "Type" #define D_CMND_DISP_REFRESH "Refresh" #define D_CMND_DISP_ROWS "Rows" #define D_CMND_DISP_SIZE "Size" #define D_CMND_DISP_FONT "Font" #define D_CMND_DISP_ROTATE "Rotate" #define D_CMND_DISP_INVERT "Invert" #define D_CMND_DISP_WIDTH "Width" #define D_CMND_DISP_HEIGHT "Height" #define D_CMND_DISP_BLINKRATE "Blinkrate" #define D_CMND_DISP_BATCH "Batch" #define D_CMND_DISP_TEXT "Text" #define D_CMND_DISP_CLEAR "Clear" #define D_CMND_DISP_NUMBER "Number" #define D_CMND_DISP_FLOAT "Float" #define D_CMND_DISP_NUMBERNC "NumberNC" // NC - "No Clear" #define D_CMND_DISP_FLOATNC "FloatNC" // NC - "No Clear" #define D_CMND_DISP_RAW "Raw" #define D_CMND_DISP_LEVEL "Level" #define D_CMND_DISP_SEVENSEG_TEXT "SevensegText" #define D_CMND_DISP_SEVENSEG_TEXTNC "SevensegTextNC" // NC - "No Clear" #define D_CMND_DISP_SCROLLDELAY "ScrollDelay" #define D_CMND_DISP_CLOCK "Clock" #define D_CMND_DISP_TEXTNC "TextNC" // NC - "No Clear" #define D_CMND_DISP_SCROLLTEXT "ScrollText" #define D_CMND_DISP_REINIT "reinit" enum XdspFunctions { FUNC_DISPLAY_INIT_DRIVER, FUNC_DISPLAY_INIT, FUNC_DISPLAY_EVERY_50_MSECOND, FUNC_DISPLAY_EVERY_SECOND, FUNC_DISPLAY_MODEL, FUNC_DISPLAY_MODE, FUNC_DISPLAY_POWER, FUNC_DISPLAY_CLEAR, FUNC_DISPLAY_DRAW_FRAME, FUNC_DISPLAY_DRAW_HLINE, FUNC_DISPLAY_DRAW_VLINE, FUNC_DISPLAY_DRAW_LINE, FUNC_DISPLAY_DRAW_CIRCLE, FUNC_DISPLAY_FILL_CIRCLE, FUNC_DISPLAY_DRAW_RECTANGLE, FUNC_DISPLAY_FILL_RECTANGLE, FUNC_DISPLAY_TEXT_SIZE, FUNC_DISPLAY_FONT_SIZE, FUNC_DISPLAY_ROTATION, FUNC_DISPLAY_DRAW_STRING, FUNC_DISPLAY_DIM, FUNC_DISPLAY_BLINKRATE, #ifdef USE_UFILESYS FUNC_DISPLAY_BATCH, #endif FUNC_DISPLAY_NUMBER, FUNC_DISPLAY_FLOAT, FUNC_DISPLAY_NUMBERNC, FUNC_DISPLAY_FLOATNC, FUNC_DISPLAY_RAW, FUNC_DISPLAY_LEVEL, FUNC_DISPLAY_SEVENSEG_TEXT, FUNC_DISPLAY_SEVENSEG_TEXTNC, FUNC_DISPLAY_SCROLLDELAY, FUNC_DISPLAY_CLOCK, FUNC_DISPLAY_SCROLLTEXT }; enum DisplayInitModes { DISPLAY_INIT_MODE, DISPLAY_INIT_PARTIAL, DISPLAY_INIT_FULL }; const char kDisplayCommands[] PROGMEM = D_PRFX_DISPLAY "|" // Prefix "|" D_CMND_DISP_MODEL "|" D_CMND_DISP_TYPE "|" D_CMND_DISP_WIDTH "|" D_CMND_DISP_HEIGHT "|" D_CMND_DISP_MODE "|" D_CMND_DISP_INVERT "|" D_CMND_DISP_REFRESH "|" D_CMND_DISP_DIMMER "|" D_CMND_DISP_COLS "|" D_CMND_DISP_ROWS "|" D_CMND_DISP_SIZE "|" D_CMND_DISP_FONT "|" D_CMND_DISP_ROTATE "|" D_CMND_DISP_TEXT "|" D_CMND_DISP_ADDRESS "|" D_CMND_DISP_BLINKRATE "|" #ifdef USE_UFILESYS D_CMND_DISP_BATCH "|" #endif D_CMND_DISP_CLEAR "|" D_CMND_DISP_NUMBER "|" D_CMND_DISP_FLOAT "|" D_CMND_DISP_NUMBERNC "|" D_CMND_DISP_FLOATNC "|" D_CMND_DISP_RAW "|" D_CMND_DISP_LEVEL "|" D_CMND_DISP_SEVENSEG_TEXT "|" D_CMND_DISP_SEVENSEG_TEXTNC "|" D_CMND_DISP_SCROLLDELAY "|" D_CMND_DISP_CLOCK "|" D_CMND_DISP_TEXTNC "|" D_CMND_DISP_SCROLLTEXT "|" D_CMND_DISP_REINIT ; void (* const DisplayCommand[])(void) PROGMEM = { &CmndDisplay, &CmndDisplayModel, &CmndDisplayType, &CmndDisplayWidth, &CmndDisplayHeight, &CmndDisplayMode, &CmndDisplayInvert, &CmndDisplayRefresh, &CmndDisplayDimmer, &CmndDisplayColumns, &CmndDisplayRows, &CmndDisplaySize, &CmndDisplayFont, &CmndDisplayRotate, &CmndDisplayText, &CmndDisplayAddress, &CmndDisplayBlinkrate, #ifdef USE_UFILESYS &CmndDisplayBatch, #endif &CmndDisplayClear, &CmndDisplayNumber, &CmndDisplayFloat, &CmndDisplayNumberNC, &CmndDisplayFloatNC, &CmndDisplayRaw, &CmndDisplayLevel, &CmndDisplaySevensegText, &CmndDisplaySevensegTextNC, &CmndDisplayScrollDelay, &CmndDisplayClock, &CmndDisplayTextNC, &CmndDisplayScrollText,&DisplayReInitDriver }; #ifdef USE_GRAPH typedef union { uint8_t data; struct { uint8_t overlay : 1; uint8_t draw : 1; uint8_t nu3 : 1; uint8_t nu4 : 1; uint8_t nu5 : 1; uint8_t nu6 : 1; uint8_t nu7 : 1; uint8_t nu8 : 1; }; } GFLAGS; struct GRAPH { uint16_t xp; uint16_t yp; uint16_t xs; uint16_t ys; float ymin; float ymax; float range; uint32_t x_time; // time per x slice in milliseconds uint32_t last_ms; uint32_t last_ms_redrawn; int16_t decimation; // decimation or graph duration in minutes uint16_t dcnt; uint32_t summ; uint16_t xcnt; uint8_t *values; uint8_t xticks; uint8_t yticks; uint8_t last_val; uint8_t color_index; uint16_t bg_color; uint16_t fg_color; GFLAGS flags; }; struct GRAPH *graph[NUM_GRAPHS]; #endif // USE_GRAPH char *dsp_str; uint16_t dsp_x; uint16_t dsp_y; uint16_t dsp_x2; uint16_t dsp_y2; uint16_t dsp_rad; uint16_t dsp_color; int16_t dsp_len; bool disp_apply_display_dimmer_request = false; int8_t disp_power = -1; uint8_t disp_device = 0; uint8_t disp_refresh = 1; uint8_t disp_autodraw = 1; uint8_t dsp_init; uint8_t dsp_font; uint8_t dsp_flag; uint8_t dsp_on; #define PREDEF_INDEXCOLORS 19 uint16_t index_colors[MAX_INDEXCOLORS - PREDEF_INDEXCOLORS]; #ifdef USE_DISPLAY_MODES1TO5 char **disp_log_buffer; char **disp_screen_buffer; char disp_temp[2]; // C or F char disp_pres[5]; // hPa or mmHg char disp_topic[TOPSZ]; uint8_t disp_log_buffer_cols = 0; uint8_t disp_log_buffer_idx = 0; uint8_t disp_log_buffer_ptr = 0; uint8_t disp_screen_buffer_cols = 0; uint8_t disp_screen_buffer_rows = 0; bool disp_subscribed = false; #endif // USE_DISPLAY_MODES1TO5 /*********************************************************************************************/ void DisplayClear(void) { if (renderer) { renderer->fillScreen(bg_color); } else { XdspCall(FUNC_DISPLAY_CLEAR); } } void DisplayInit(uint8_t mode) { if (renderer) { renderer->DisplayInit(mode, Settings->display_size, Settings->display_rotate, Settings->display_font); } else { dsp_init = mode; XdspCall(FUNC_DISPLAY_INIT); } DisplayClear(); } void DisplayDrawStringAt(uint16_t x, uint16_t y, char *str, uint16_t color, uint8_t flag) { if (renderer) { renderer->DrawStringAt(x, y, str, color, flag); } else { dsp_x = x; dsp_y = y; dsp_str = str; dsp_color = color; dsp_flag = flag; XdspCall(FUNC_DISPLAY_DRAW_STRING); } } void DisplayOnOff(uint8_t on) { if (disp_device) { ExecuteCommandPower(disp_device, on, SRC_DISPLAY); } } /*-------------------------------------------------------------------------------------------*/ // get asci float number uint8_t fatoiv(char *cp,float *res) { uint8_t index=0; *res=CharToFloat(cp); while (*cp) { if ((*cp>='0' && *cp<='9') || (*cp=='-') || (*cp=='.')) { cp++; index++; } else { break; } } return index; } // get asci number until delimiter and return asci number lenght and value uint8_t atoiv(char *cp, int16_t *res) { uint8_t index = 0; *res = atoi(cp); while (*cp) { if ((*cp>='0' && *cp<='9') || (*cp=='-')) { cp++; index++; } else { break; } } return index; } // get asci number until delimiter and return asci number lenght and value uint8_t atoiV(char *cp, uint16_t *res) { uint8_t index = 0; *res = atoi(cp); while (*cp) { if (*cp>='0' && *cp<='9') { cp++; index++; } else { break; } } return index; } // right align string void alignright(char *string) { uint16_t slen=strlen(string); uint16_t len=slen; while (len) { // count spaces to the right if (string[len-1]!=' ') { break; } len--; } uint16_t diff=slen-len; if (diff>0) { // move string memmove(&string[diff],string,len); memset(string,' ',diff); } } char *get_string(char *buff,uint8_t len,char *cp) { uint8_t index=0; while (*cp!=':') { buff[index]=*cp++; index++; if (index>=len) break; } buff[index]=0; cp++; return cp; } #define ESCAPE_CHAR '~' // decode text escapes, 1 hexbyte assumed uint32_t decode_te(char *line) { uint32_t skip = 0; char sbuf[3],*cp; while (*line) { if (*line==ESCAPE_CHAR) { cp=line+1; if (*cp!=0 && *cp==ESCAPE_CHAR) { // escape escape, discard one memmove(cp,cp+1,strlen(cp)); skip++; } else { // escape HH if (strlen(cp)<2) { // illegal lenght, ignore return skip; } // take 2 hex chars sbuf[0]=*(cp); sbuf[1]=*(cp+1); sbuf[2]=0; *line=strtol(sbuf,0,16); // must shift string 2 bytes shift zero also memmove(cp,cp+2,strlen(cp)-1); skip += 2; } } line++; } return skip; } /*-------------------------------------------------------------------------------------------*/ // Getter and Setter for DisplayDimer // Original encoding is range 0..15 // New encoding is range 0..100 using negative numbers, i.e. 0..-100 uint8_t GetDisplayDimmer(void) { if (Settings->display_dimmer_protected > 0) { return changeUIntScale(Settings->display_dimmer_protected, 0, 15, 0, 100); } else { if (Settings->display_dimmer_protected < -100) { Settings->display_dimmer_protected = -100; } return - Settings->display_dimmer_protected; } } // retro-compatible call to get range 0..15 uint8_t GetDisplayDimmer16(void) { return changeUIntScale(GetDisplayDimmer(), 0, 100, 0, 15); } // In: 0..100 void SetDisplayDimmer(uint8_t dimmer) { if (dimmer > 100) { dimmer = 100; } Settings->display_dimmer_protected = - dimmer; } /*-------------------------------------------------------------------------------------------*/ #define DISPLAY_BUFFER_COLS 128 // Max number of characters in linebuf uint16_t GetColorFromIndex(uint32_t index) { if (index >= MAX_INDEXCOLORS) index = 0; if (index < PREDEF_INDEXCOLORS) { return renderer->GetColorFromIndex(index); } else { return index_colors[index - PREDEF_INDEXCOLORS]; } } void DisplayText(void) { uint8_t lpos; uint8_t escape = 0; uint8_t var; int16_t lin = 0; int16_t col = 0; int16_t fill = 0; int16_t temp; int16_t temp1; float ftemp; char linebuf[DISPLAY_BUFFER_COLS]; char *dp = linebuf; char *cp = XdrvMailbox.data; memset(linebuf, ' ', sizeof(linebuf)); linebuf[sizeof(linebuf)-1] = 0; *dp = 0; while (*cp) { if (!escape) { // check for escape if (*cp == '[') { escape = 1; cp++; // if string in buffer print it dp -= decode_te(linebuf); if ((uint32_t)dp - (uint32_t)linebuf) { if (!fill) { *dp = 0; } #ifdef USE_DISPLAY_MODES1TO5 if (!Settings->display_mode) { #endif // USE_DISPLAY_MODES1TO5 if (col > 0 && lin > 0) { // use col and lin DisplayDrawStringAt(col, lin, linebuf, fg_color, 1); } else { // use disp_xpos, disp_ypos DisplayDrawStringAt(disp_xpos, disp_ypos, linebuf, fg_color, 0); } memset(linebuf, ' ', sizeof(linebuf)); linebuf[sizeof(linebuf)-1] = 0; dp = linebuf; #ifdef USE_DISPLAY_MODES1TO5 } #endif // USE_DISPLAY_MODES1TO5 } } else { // copy chars if (dp < (linebuf + DISPLAY_BUFFER_COLS)) { *dp++ = *cp++; } else { break; } } } else { // check escapes if (*cp == ']') { escape = 0; cp++; } else { // analyze escapes switch (*cp++) { case 'z': // clear display DisplayClear(); disp_xpos = 0; disp_ypos = 0; col = 0; lin = 0; break; case 'i': // init display with partial update DisplayInit(DISPLAY_INIT_PARTIAL); break; case 'I': // init display with full refresh DisplayInit(DISPLAY_INIT_FULL); break; case 'o': DisplayOnOff(0); break; case 'O': DisplayOnOff(1); break; case 'x': // set disp_xpos var = atoiv(cp, &disp_xpos); cp += var; break; case 'y': // set disp_ypos var = atoiv(cp, &disp_ypos); cp += var; break; case 'l': // text line lxx var = atoiv(cp, &lin); cp += var; //display.setCursor(display.getCursorX(),(lin-1)*font_y*txtsize); break; case 'c': // text column cxx var = atoiv(cp, &col); cp += var; //display.setCursor((col-1)*font_x*txtsize,display.getCursorY()); break; case 'C': // text color cxx if (*cp=='i') { // color index 0-18 cp++; var = atoiv(cp, &temp); if (renderer) ftemp = GetColorFromIndex(temp); } else { // float because it must handle unsigned 16 bit var = fatoiv(cp,&ftemp); } fg_color=ftemp; cp += var; if (renderer) renderer->setTextColor(fg_color,bg_color); break; case 'B': // bg color Bxx if (*cp=='i') { // color index 0-18 cp++; var = atoiv(cp, &temp); if (renderer) ftemp = GetColorFromIndex(temp); } else { var = fatoiv(cp,&ftemp); } bg_color=ftemp; cp += var; if (renderer) renderer->setTextColor(fg_color,bg_color); break; case 'p': // pad field with spaces fxx var = atoiv(cp, &fill); cp += var; break; #ifdef USE_UFILESYS case 'P': { char *ep = strchr(cp,':'); if (ep) { *ep = 0; ep++; int16_t scale = 0; int16_t xs = 0; int16_t ys = 0; if (isdigit(*ep)) { var = atoiv(ep, &scale); ep += var; if (*ep == ':') { ep++; var = atoiv(ep, &xs); ep += var; ep++; var = atoiv(ep, &ys); ep += var; } } Draw_RGB_Bitmap(cp, disp_xpos, disp_ypos, scale, false, xs, ys); cp = ep; } } break; #ifdef USE_MULTI_DISPLAY case 'S': { int16_t rot = -1, srot, model; var = atoiv(cp, &temp); cp += var; if (temp < 1 || temp > MAX_MULTI_DISPLAYS) { temp = 1; } temp--; if (*cp == 'r') { cp++; var = atoiv(cp, &rot); cp += var; } if (*cp == ':') { cp++; if (displays[temp].used) { Set_display(cur_display); Get_display(temp); } } else { char *ep=strchr(cp,':'); if (ep) { *ep=0; ep++; File fp; if (ffsp) { AddLog(LOG_LEVEL_INFO, PSTR("DSP: File: %s"),cp); fp = ffsp->open(cp, "r"); if (fp > 0) { uint32_t size = fp.size(); char *fdesc = (char *)calloc(size + 4, 1); if (fdesc) { model = Settings->display_model; fp.read((uint8_t*)fdesc, size); fp.close(); if (renderer) { // save ptr Set_display(temp); renderer = nullptr; } else { Renderer *svptr = renderer; Get_display(temp); renderer = svptr; if (rot >= 0) { srot = Settings->display_rotate; Settings->display_rotate = rot; } } renderer = Init_uDisplay(fdesc); if (rot >= 0) { Settings->display_rotate = srot; } Set_display(temp); AddLog(LOG_LEVEL_INFO, PSTR("DSP: File descriptor loaded")); free(fdesc); Settings->display_model = model; } } } } cp = ep; } } break; #endif // USE_MULTI_DISPLAY #endif // USE_UFILESYS case 'h': // hor line to var = atoiv(cp, &temp); cp += var; if (temp < 0) { if (renderer) renderer->writeFastHLine(disp_xpos + temp, disp_ypos, -temp, fg_color); //else DisplayDrawHLine(disp_xpos + temp, disp_ypos, -temp, fg_color); } else { if (renderer) renderer->writeFastHLine(disp_xpos, disp_ypos, temp, fg_color); //else DisplayDrawHLine(disp_xpos, disp_ypos, temp, fg_color); } disp_xpos += temp; break; case 'v': // vert line to var = atoiv(cp, &temp); cp += var; if (temp < 0) { if (renderer) renderer->writeFastVLine(disp_xpos, disp_ypos + temp, -temp, fg_color); //else DisplayDrawVLine(disp_xpos, disp_ypos + temp, -temp, fg_color); } else { if (renderer) renderer->writeFastVLine(disp_xpos, disp_ypos, temp, fg_color); //else DisplayDrawVLine(disp_xpos, disp_ypos, temp, fg_color); } disp_ypos += temp; break; case 'L': // any line to var = atoiv(cp, &temp); cp += var; cp++; var = atoiv(cp, &temp1); cp += var; if (renderer) renderer->writeLine(disp_xpos, disp_ypos, temp, temp1, fg_color); //else DisplayDrawLine(disp_xpos, disp_ypos, temp, temp1, fg_color); disp_xpos += temp; disp_ypos += temp1; break; case 'k': // circle var = atoiv(cp, &temp); cp += var; if (renderer) renderer->drawCircle(disp_xpos, disp_ypos, temp, fg_color); //else DisplayDrawCircle(disp_xpos, disp_ypos, temp, fg_color); break; case 'K': // filled circle var = atoiv(cp, &temp); cp += var; if (renderer) renderer->fillCircle(disp_xpos, disp_ypos, temp, fg_color); //else DisplayDrawFilledCircle(disp_xpos, disp_ypos, temp, fg_color); break; case 'm': // epaper draw mode currently only for 4,7 inch displays var = atoiv(cp, &temp); cp += var; if (renderer) renderer->ep_update_mode(temp); break; case 'r': // rectangle var = atoiv(cp, &temp); cp += var; cp++; var = atoiv(cp, &temp1); cp += var; if (renderer) renderer->drawRect(disp_xpos, disp_ypos, temp, temp1, fg_color); //else DisplayDrawRectangle(disp_xpos, disp_ypos, temp, temp1, fg_color); break; case 'R': // filled rectangle var = atoiv(cp, &temp); cp += var; cp++; var = atoiv(cp, &temp1); cp += var; if (renderer) renderer->fillRect(disp_xpos, disp_ypos, temp, temp1, fg_color); //else DisplayDrawFilledRectangle(disp_xpos, disp_ypos, temp, temp1, fg_color); break; case 'u': // rounded rectangle { int16_t rad, xp, yp, width, height; if (*cp == 'p') { // update epaper display cp++; var = atoiv(cp, &xp); cp += var; cp++; var = atoiv(cp, &yp); cp += var; cp++; var = atoiv(cp, &width); cp += var; cp++; var = atoiv(cp, &height); cp += var; cp++; var = atoiv(cp, &temp); cp += var; if (renderer) renderer->ep_update_area(xp, yp, width, height, temp); break; } var = atoiv(cp, &temp); cp += var; cp++; var = atoiv(cp, &temp1); cp += var; cp++; var = atoiv(cp, &rad); cp += var; if (renderer) renderer->drawRoundRect(disp_xpos, disp_ypos, temp, temp1, rad, fg_color); //else DisplayDrawFilledRectangle(disp_xpos, disp_ypos, temp, temp1, fg_color); } break; case 'U': // rounded rectangle { int16_t rad; var = atoiv(cp, &temp); cp += var; cp++; var = atoiv(cp, &temp1); cp += var; cp++; var = atoiv(cp, &rad); cp += var; if (renderer) renderer->fillRoundRect(disp_xpos, disp_ypos, temp, temp1, rad, fg_color); //else DisplayDrawFilledRectangle(disp_xpos, disp_ypos, temp, temp1, fg_color); } break; case 't': if (*cp=='S') { cp++; if (dp < (linebuf + DISPLAY_BUFFER_COLS) -8) { snprintf_P(dp, 9, PSTR("%02d" D_HOUR_MINUTE_SEPARATOR "%02d" D_MINUTE_SECOND_SEPARATOR "%02d"), RtcTime.hour, RtcTime.minute, RtcTime.second); dp += 8; } } else { if (dp < (linebuf + DISPLAY_BUFFER_COLS) -5) { snprintf_P(dp, 6, PSTR("%02d" D_HOUR_MINUTE_SEPARATOR "%02d"), RtcTime.hour, RtcTime.minute); dp += 5; } } break; case 'T': { uint8_t param1 = RtcTime.day_of_month; uint8_t param2 = RtcTime.month; if (*cp=='U') { cp++; param1 = RtcTime.month; param2 = RtcTime.day_of_month; } if (dp < (linebuf + DISPLAY_BUFFER_COLS) -8) { snprintf_P(dp, 9, PSTR("%02d" D_MONTH_DAY_SEPARATOR "%02d" D_YEAR_MONTH_SEPARATOR "%02d"), param1, param2, RtcTime.year%2000); dp += 8; } break; } case 'd': if (*cp == 'c') { cp++; // define index colo var = atoiv(cp, &temp); cp += var; cp++; var = fatoiv(cp, &ftemp); cp += var; if (temp >= MAX_INDEXCOLORS) temp = PREDEF_INDEXCOLORS; if (temp < PREDEF_INDEXCOLORS) temp = PREDEF_INDEXCOLORS; index_colors[temp - PREDEF_INDEXCOLORS] = ftemp; break; } #ifdef USE_DT_VARS if (*cp == 'v') { cp++; { int16_t num, gxp, gyp, textbcol, textfcol, font, textsize, txlen, dp, time; var=atoiv(cp,&num); cp+=var; cp++; var=atoiv(cp,&gxp); cp+=var; cp++; var=atoiv(cp,&gyp); cp+=var; cp++; var=atoiv(cp,&textbcol); cp+=var; cp++; var=atoiv(cp,&textfcol); cp+=var; cp++; var=atoiv(cp,&font); cp+=var; cp++; var=atoiv(cp,&textsize); cp+=var; cp++; var=atoiv(cp,&txlen); cp+=var; cp++; var=atoiv(cp,&dp); cp+=var; cp++; var=atoiv(cp,&time); cp+=var; cp++; // text itself char bbuff[32]; cp = get_string(bbuff, sizeof(bbuff), cp); char unit[4]; cp = get_string(unit, sizeof(unit), cp); decode_te(unit); define_dt_var(num, gxp, gyp, textbcol, textfcol, font, textsize, txlen, time, dp, bbuff, unit); } } #endif // USE_DT_VARS // force draw grafics buffer if (renderer) renderer->Updateframe(); //else DisplayDrawFrame(); break; case 'D': // set auto draw mode auto_draw=*cp&3; if (renderer) renderer->setDrawMode(auto_draw>>1); cp += 1; break; case 's': // size sx var = atoiv(cp, &temp); if (temp > TXT_MAX_SFAC) temp = TXT_MAX_SFAC; if (renderer) renderer->setTextSize(temp); //else DisplaySetSize(*cp&3); cp+=var; break; case 'f': // font sx { uint8_t font = *cp&7; if (renderer) renderer->setTextFont(font); //else DisplaySetFont(font); if (font) { // for backward compatibility set size to 1 on non GFX fonts if (renderer) renderer->setTextSize(1); //else DisplaySetSize(1); } cp += 1; } break; #ifdef USE_UFILESYS #ifdef USE_RAMFONT extern FS *ffsp; case 'F': { char *ep = strchr(cp,':'); if (ep) { static uint8_t *ram_font; char fname[32]; *ep = 0; ep++; if (*cp == '-' && *(cp + 1) == 0) { if (ram_font) { free (ram_font); ram_font = 0; if (renderer) renderer->SetRamfont(0); } cp = ep; } else { if (*cp != '/') { fname[0] = '/'; fname[1] = 0; } else { fname[0] = 0; } strlcat(fname, cp, sizeof(fname)); if (!strstr(cp, ".fnt")) { strlcat(fname, ".fnt", sizeof(fname)); } if (ffsp) { File fp; fp = ffsp->open(fname, "r"); if (fp > 0) { uint32_t size = fp.size(); if (ram_font) free (ram_font); ram_font = (uint8_t*)special_malloc(size + 4); fp.read((uint8_t*)ram_font, size); fp.close(); if (renderer) renderer->SetRamfont(ram_font); //Serial.printf("Font loaded: %s\n",fname ); } } } cp = ep; } } break; #endif // USE_RAMFONT #endif // USE_UFILESYS case 'a': // rotation angle if (renderer) renderer->setRotation(*cp&3); //else DisplaySetRotation(*cp&3); cp+=1; break; #ifdef USE_GRAPH case 'G': // define graph if (*cp=='d') { cp++; var=atoiv(cp,&temp); cp+=var; cp++; var=atoiv(cp,&temp1); cp+=var; RedrawGraph(temp,temp1); break; } #if (defined(USE_SCRIPT_FATFS) && defined(USE_SCRIPT)) || defined(USE_UFILESYS) if (*cp=='s') { cp++; var=atoiv(cp,&temp); cp+=var; cp++; // path char bbuff[128]; cp=get_string(bbuff,sizeof(bbuff),cp); Save_graph(temp,bbuff); break; } if (*cp=='r') { cp++; var=atoiv(cp,&temp); cp+=var; cp++; // path char bbuff[128]; cp=get_string(bbuff,sizeof(bbuff),cp); Restore_graph(temp,bbuff); break; } #endif // USE_SCRIPT_FATFS { int16_t num,gxp,gyp,gxs,gys,dec,icol; float ymin,ymax; var=atoiv(cp,&num); cp+=var; cp++; var=atoiv(cp,&gxp); cp+=var; cp++; var=atoiv(cp,&gyp); cp+=var; cp++; var=atoiv(cp,&gxs); cp+=var; cp++; var=atoiv(cp,&gys); cp+=var; cp++; var=atoiv(cp,&dec); cp+=var; cp++; var=fatoiv(cp,&ymin); cp+=var; cp++; var=fatoiv(cp,&ymax); cp+=var; if (color_type==COLOR_COLOR) { // color graph requires channel color cp++; var=atoiv(cp,&icol); cp+=var; } else { icol=0; } DefineGraph(num,gxp,gyp,gxs,gys,dec,ymin,ymax,icol); } break; case 'g': { float temp; int16_t num; var=atoiv(cp,&num); cp+=var; cp++; var=fatoiv(cp,&temp); cp+=var; AddValue(num,temp); } break; #endif // USE_GRAPH #ifdef USE_AWATCH case 'w': var = atoiv(cp, &temp); cp += var; DrawAClock(temp); break; #endif // USE_AWATCH #ifdef USE_TOUCH_BUTTONS case 'b': { int16_t num, gxp, gyp, gxs, gys, outline, fill, textcolor, textsize; uint8_t dflg = 1, sbt = 0; if (*cp == 'e' || *cp == 'd' || *cp == 'D') { // enable disable delete uint8_t dis = 0; if (*cp == 'd') dis = 1; uint8_t del = 0; if (*cp == 'D') { del = 1; } cp++; var = atoiv(cp, &num); num = num % MAX_TOUCH_BUTTONS; cp += var; if (buttons[num]) { if (del) { if (renderer) renderer->fillRect(buttons[num]->spars.xp, buttons[num]->spars.yp, buttons[num]->spars.xs, buttons[num]->spars.ys, bg_color); delete buttons[num]; buttons[num] = 0; } else { buttons[num]->vpower.disable = dis; if (!dis) { if (buttons[num]->vpower.is_virtual) buttons[num]->xdrawButton(buttons[num]->vpower.on_off); else buttons[num]->xdrawButton(bitRead(TasmotaGlobal.power, num)); } } } break; } if (*cp == '-') { cp++; dflg = 0; } if (*cp == 's') { cp++; sbt = 1; } var = atoiv(cp,&num); cp += var; uint8_t bflags = num >> 8; num = num % MAX_TOUCH_BUTTONS; if (*cp == 's') { cp++; var=atoiv(cp,&gxp); if (buttons[num]) { // set slider or button if (buttons[num]->vpower.slider) { buttons[num]->UpdateSlider(-gxp, -gxp); } else { buttons[num]->vpower.on_off = gxp; buttons[num]->xdrawButton(buttons[num]->vpower.on_off); } } break; } cp++; var=atoiv(cp,&gxp); cp+=var; cp++; var=atoiv(cp,&gyp); cp+=var; cp++; var=atoiv(cp,&gxs); cp+=var; cp++; var=atoiv(cp,&gys); cp+=var; cp++; var=atoiv(cp,&outline); cp+=var; cp++; var=atoiv(cp,&fill); cp+=var; cp++; var=atoiv(cp,&textcolor); cp+=var; cp++; var=atoiv(cp,&textsize); cp+=var; cp++; // text itself char bbuff[32]; if (!sbt) { // text itself cp = get_string(bbuff, sizeof(bbuff), cp); } if (buttons[num]) { delete buttons[num]; } if (renderer) { buttons[num] = new VButton(); if (buttons[num]) { if (!sbt) { buttons[num]->vpower.slider = 0; buttons[num]->xinitButtonUL(renderer, gxp, gyp, gxs, gys, GetColorFromIndex(outline),\ GetColorFromIndex(fill), GetColorFromIndex(textcolor), bbuff, textsize); if (!bflags) { // power button if (dflg) buttons[num]->xdrawButton(bitRead(TasmotaGlobal.power, num)); buttons[num]->vpower.is_virtual = 0; } else { // virtual button buttons[num]->vpower.is_virtual = 1; if (bflags==2) { // push buttons[num]->vpower.is_pushbutton = 1; } else { // toggle buttons[num]->vpower.is_pushbutton = 0; } if (dflg) buttons[num]->xdrawButton(buttons[num]->vpower.on_off); buttons[num]->vpower.disable = !dflg; } } else { // slider buttons[num]->vpower.slider = 1; buttons[num]->SliderInit(renderer, gxp, gyp, gxs, gys, outline, GetColorFromIndex(fill),\ GetColorFromIndex(textcolor), GetColorFromIndex(textsize)); } } } } break; #endif // USE_TOUCH_BUTTONS default: // unknown escape Response_P(PSTR("Unknown Escape")); goto exit; break; } } } } exit: // now draw buffer dp -= decode_te(linebuf); if ((uint32_t)dp - (uint32_t)linebuf) { if (!fill) { *dp = 0; } else { linebuf[abs(int(fill))] = 0; } if (fill<0) { // right align alignright(linebuf); } #ifdef USE_DISPLAY_MODES1TO5 if (Settings->display_mode) { DisplayLogBufferAdd(linebuf); } else #endif // USE_DISPLAY_MODES1TO5 if (col > 0 && lin > 0) { // use col and lin DisplayDrawStringAt(col, lin, linebuf, fg_color, 1); } else { // use disp_xpos, disp_ypos DisplayDrawStringAt(disp_xpos, disp_ypos, linebuf, fg_color, 0); } } // draw buffer if (auto_draw&1) { if (renderer) renderer->Updateframe(); //else DisplayDrawFrame(); } } #ifdef USE_UFILESYS void Display_Text_From_File(const char *file) { File fp; if (!ufsp) return; fp = ufsp->open(file, FS_FILE_READ); if (fp > 0) { char *savptr = XdrvMailbox.data; char linebuff[128]; while (fp.available()) { uint16_t index = 0; while (fp.available()) { uint8_t buf[1]; fp.read(buf,1); if (buf[0]=='\n' || buf[0]=='\r') { break; } else { linebuff[index] = buf[0]; index++; if (index >= sizeof(linebuff) - 1) { break; } } } linebuff[index] = 0; char *cp = linebuff; while (*cp==' ') cp++; if (*cp == ';') continue; //AddLog(LOG_LEVEL_INFO, PSTR("displaytext %s"), cp); // execute display text here XdrvMailbox.data = cp; XdrvMailbox.data_len = 0; DisplayText(); } XdrvMailbox.data = savptr; fp.close(); } } #endif // USE_UFILESYS #ifdef USE_DT_VARS #ifndef MAX_DT_VARS #define MAX_DT_VARS 8 #endif // MAX_DT_VARS #define MAX_DVTSIZE 24 typedef struct { uint16_t xp; uint16_t yp; uint8_t txtbcol; uint8_t txtfcol; int8_t txtsiz; int8_t txtlen; int8_t dp; int8_t font; int8_t time; int8_t timer; char unit[6]; char *jstrbuf; char rstr[32]; } DT_VARS; DT_VARS *dt_vars[MAX_DT_VARS]; void define_dt_var(uint32_t num, uint32_t xp, uint32_t yp, uint32_t txtbcol, uint32_t txtfcol, int32_t font, int32_t txtsiz, int32_t txtlen, int32_t time, int32_t dp, char *jstr, char *unit) { if (num >= MAX_DT_VARS) return; if (dt_vars[num]) { if (dt_vars[num]->jstrbuf) free(dt_vars[num]->jstrbuf); free(dt_vars[num]); } //dt [dv0:100:100:0:3:2:1:10:2:WLAN#ID:uV:] DT_VARS *dtp = (DT_VARS*)malloc(sizeof(DT_VARS)); if (!dtp) return; dt_vars[num] = dtp; dtp->xp = xp; dtp->yp = yp; dtp->txtbcol = txtbcol; dtp->txtfcol = txtfcol; dtp->font = font; dtp->txtsiz = txtsiz; dtp->time = time; if (txtlen > MAX_DVTSIZE) {txtlen = MAX_DVTSIZE;} dtp->txtlen = txtlen; dtp->dp = dp; uint8_t jlen = strlen(jstr); dtp->jstrbuf = (char*)calloc(jlen + 2,1); if (!dtp->jstrbuf) { free (dtp); return; } dtp->rstr[0] = 0; strcpy(dtp->unit, unit); strcpy(dtp->jstrbuf, jstr); if (!time) time = 1; dtp->timer = time; } void draw_dt_vars(void) { if (!renderer) return; for (uint32_t cnt = 0; cnt < MAX_DT_VARS; cnt++) { DT_VARS *dtp = dt_vars[cnt]; if (dtp) { if (dtp->jstrbuf) { // draw dtp->timer--; if (!dtp->timer) { dtp->timer = dtp->time; char vstr[MAX_DVTSIZE + 7]; memset(vstr, ' ', sizeof(vstr)); strcpy(vstr, dtp->rstr); strcat(vstr, " "); strcat(vstr, dtp->unit); uint16_t slen = strlen(vstr); vstr[slen] = ' '; if (!dtp->txtlen) { vstr[slen] = 0; } else { vstr[abs(int(dtp->txtlen))] = 0; } if (dtp->txtlen < 0) { // right align alignright(vstr); } if (dtp->txtsiz > 0) { renderer->setDrawMode(0); } else { renderer->setDrawMode(2); } renderer->setTextColor(GetColorFromIndex(dtp->txtfcol),GetColorFromIndex(dtp->txtbcol)); renderer->setTextFont(dtp->font); renderer->setTextSize(abs(dtp->txtsiz)); if (dtp->jstrbuf[0]=='[') { uint16_t s_disp_xpos = disp_xpos; uint16_t s_disp_ypos = disp_ypos; uint16_t s_bg_color = bg_color; uint16_t s_fg_color = fg_color; disp_xpos = dtp->xp; disp_ypos = dtp->yp; bg_color = GetColorFromIndex(dtp->txtbcol); fg_color = GetColorFromIndex(dtp->txtfcol); char *savmbd = XdrvMailbox.data; XdrvMailbox.data = dtp->jstrbuf; DisplayText(); XdrvMailbox.data = savmbd; disp_xpos = s_disp_xpos; disp_ypos = s_disp_ypos; bg_color = s_bg_color; fg_color = s_fg_color; } else { renderer->DrawStringAt(dtp->xp, dtp->yp, vstr, GetColorFromIndex(dtp->txtfcol), 0); } // restore display vars renderer->setTextColor(fg_color, bg_color); renderer->setDrawMode(auto_draw>>1); } } } } } #define DTV_JSON_SIZE 1024 void DisplayDTVarsTeleperiod(void) { ResponseClear(); MqttShowState(); uint32_t jlen = ResponseLength(); if (jlen < DTV_JSON_SIZE) { char *json = (char*)malloc(jlen + 2); if (json) { strlcpy(json, ResponseData(), jlen + 1); get_dt_vars(json); free(json); } } } void get_dt_mqtt(void) { GetNextSensor(); get_dt_vars(ResponseData()); } void get_dt_vars(char *json) { if (strlen(json)) { JsonParser parser(json); JsonParserObject obj = parser.getRootObject(); for (uint32_t cnt = 0; cnt < MAX_DT_VARS; cnt++) { if (dt_vars[cnt]) { if (dt_vars[cnt]->jstrbuf && dt_vars[cnt]->jstrbuf[0]!='[') { char sbuf[32]; uint32_t res = JsonParsePath(&obj, dt_vars[cnt]->jstrbuf, '#', NULL, sbuf, sizeof(sbuf)); if (res) { if (dt_vars[cnt]->dp < 0) { // use string strcpy(dt_vars[cnt]->rstr, sbuf); } else { // convert back and forth dtostrfd(CharToFloat(sbuf), dt_vars[cnt]->dp, dt_vars[cnt]->rstr); } } } } } } } void free_dt_vars(void) { for (uint32_t cnt = 0; cnt < MAX_DT_VARS; cnt++) { if (dt_vars[cnt]) { if (dt_vars[cnt]->jstrbuf) free(dt_vars[cnt]->jstrbuf); free(dt_vars[cnt]); dt_vars[cnt] = 0; } } } #endif // USE_DT_VARS /*********************************************************************************************/ #ifdef USE_DISPLAY_MODES1TO5 void DisplayClearScreenBuffer(void) { if (disp_screen_buffer_cols) { for (uint32_t i = 0; i < disp_screen_buffer_rows; i++) { memset(disp_screen_buffer[i], 0, disp_screen_buffer_cols); } } } void DisplayFreeScreenBuffer(void) { if (disp_screen_buffer != nullptr) { for (uint32_t i = 0; i < disp_screen_buffer_rows; i++) { if (disp_screen_buffer[i] != nullptr) { free(disp_screen_buffer[i]); } } free(disp_screen_buffer); disp_screen_buffer_cols = 0; disp_screen_buffer_rows = 0; } } void DisplayAllocScreenBuffer(void) { if (!disp_screen_buffer_cols) { disp_screen_buffer_rows = Settings->display_rows; disp_screen_buffer = (char**)calloc(sizeof(*disp_screen_buffer) * disp_screen_buffer_rows, 1); if (disp_screen_buffer != nullptr) { for (uint32_t i = 0; i < disp_screen_buffer_rows; i++) { disp_screen_buffer[i] = (char*)calloc(sizeof(*disp_screen_buffer[i]) * (Settings->display_cols[0] +1), 1); if (disp_screen_buffer[i] == nullptr) { DisplayFreeScreenBuffer(); break; } } } if (disp_screen_buffer != nullptr) { disp_screen_buffer_cols = Settings->display_cols[0] +1; DisplayClearScreenBuffer(); } } } void DisplayReAllocScreenBuffer(void) { DisplayFreeScreenBuffer(); DisplayAllocScreenBuffer(); } void DisplayFillScreen(uint32_t line) { uint32_t len = disp_screen_buffer_cols - strlen(disp_screen_buffer[line]); if (len) { memset(disp_screen_buffer[line] + strlen(disp_screen_buffer[line]), 0x20, len); disp_screen_buffer[line][disp_screen_buffer_cols -1] = 0; } } /*-------------------------------------------------------------------------------------------*/ void DisplayFreeLogBuffer(void) { if (disp_log_buffer != nullptr) { for (uint32_t i = 0; i < DISPLAY_LOG_ROWS; i++) { if (disp_log_buffer[i] != nullptr) { free(disp_log_buffer[i]); } } free(disp_log_buffer); disp_log_buffer_cols = 0; } } void DisplayAllocLogBuffer(void) { if (!disp_log_buffer_cols) { disp_log_buffer = (char**)calloc(sizeof(*disp_log_buffer) * DISPLAY_LOG_ROWS, 1); if (disp_log_buffer != nullptr) { for (uint32_t i = 0; i < DISPLAY_LOG_ROWS; i++) { disp_log_buffer[i] = (char*)calloc(sizeof(*disp_log_buffer[i]) * (Settings->display_cols[0] +1), 1); if (disp_log_buffer[i] == nullptr) { DisplayFreeLogBuffer(); break; } } } if (disp_log_buffer != nullptr) { disp_log_buffer_cols = Settings->display_cols[0] +1; DisplayClearScreenBuffer(); DisplayClear(); } } } void DisplayReAllocLogBuffer(void) { DisplayFreeLogBuffer(); DisplayAllocLogBuffer(); } void DisplayLogBufferAdd(char* txt) { if (disp_log_buffer_cols) { strlcpy(disp_log_buffer[disp_log_buffer_idx++], txt, disp_log_buffer_cols); // This preserves the % sign where printf won't if (DISPLAY_LOG_ROWS == disp_log_buffer_idx) { disp_log_buffer_idx = 0; } } } char* DisplayLogBuffer(char temp_code) { char* result = nullptr; if (disp_log_buffer_cols) { if (disp_log_buffer_idx != disp_log_buffer_ptr) { uint32_t log_buffer_ptr = disp_log_buffer_ptr; result = disp_log_buffer[disp_log_buffer_ptr++]; if (DISPLAY_LOG_ROWS == disp_log_buffer_ptr) { disp_log_buffer_ptr = 0; } char *pch = strchr(result, '~'); // = 0x7E (~) Replace degrees character (276 octal) if (pch != nullptr) { result[pch - result] = temp_code; } AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("DSP: %02d %s"), log_buffer_ptr, result); } } return result; } void DisplayLogBufferInit(void) { if (Settings->display_mode) { disp_log_buffer_idx = 0; disp_log_buffer_ptr = 0; disp_refresh = Settings->display_refresh; snprintf_P(disp_temp, sizeof(disp_temp), PSTR("%c"), TempUnit()); snprintf_P(disp_pres, sizeof(disp_pres), PressureUnit().c_str()); DisplayReAllocLogBuffer(); char buffer[40]; snprintf_P(buffer, sizeof(buffer), PSTR(D_VERSION " %s%s"), TasmotaGlobal.version, TasmotaGlobal.image_name); DisplayLogBufferAdd(buffer); snprintf_P(buffer, sizeof(buffer), PSTR("Display mode %d"), Settings->display_mode); DisplayLogBufferAdd(buffer); snprintf_P(buffer, sizeof(buffer), PSTR(D_CMND_HOSTNAME " %s"), NetworkHostname()); DisplayLogBufferAdd(buffer); snprintf_P(buffer, sizeof(buffer), PSTR(D_JSON_MAC " %s"), NetworkMacAddress().c_str()); DisplayLogBufferAdd(buffer); ext_snprintf_P(buffer, sizeof(buffer), PSTR("IP %_I"), (uint32_t)NetworkAddress()); DisplayLogBufferAdd(buffer); if (!TasmotaGlobal.global_state.wifi_down) { snprintf_P(buffer, sizeof(buffer), PSTR(D_JSON_SSID " %s"), SettingsText(SET_STASSID1 + Settings->sta_active)); DisplayLogBufferAdd(buffer); snprintf_P(buffer, sizeof(buffer), PSTR(D_JSON_RSSI " %d%%"), WifiGetRssiAsQuality(WiFi.RSSI())); DisplayLogBufferAdd(buffer); } } } /*********************************************************************************************\ * Sensors \*********************************************************************************************/ enum SensorQuantity { JSON_TEMPERATURE, JSON_DEWPOINT, JSON_HEATINDEX, JSON_PRESSURE, JSON_PRESSUREATSEALEVEL, JSON_POWERFACTOR, JSON_COUNTER, JSON_ANALOG_INPUT, JSON_UV_LEVEL, JSON_HUMIDITY, JSON_LIGHT, JSON_NOISE, JSON_AIRQUALITY, JSON_ILLUMINANCE, JSON_GAS, JSON_YESTERDAY, JSON_TOTAL, JSON_TODAY, JSON_PERIOD, JSON_CURRENT, JSON_VOLTAGE, JSON_POWERUSAGE, JSON_CO2, JSON_FREQUENCY }; const char kSensorQuantity[] PROGMEM = D_JSON_TEMPERATURE "|" D_JSON_DEWPOINT "|" D_JSON_HEATINDEX "|" // degrees D_JSON_PRESSURE "|" D_JSON_PRESSUREATSEALEVEL "|" // hPa D_JSON_POWERFACTOR "|" D_JSON_COUNTER "|" D_JSON_ANALOG_INPUT "|" D_JSON_UV_LEVEL "|" // No unit D_JSON_HUMIDITY "|" D_JSON_LIGHT "|" D_JSON_NOISE "|" D_JSON_AIRQUALITY "|" // percentage D_JSON_ILLUMINANCE "|" // lx D_JSON_GAS "|" // kOhm D_JSON_YESTERDAY "|" D_JSON_TOTAL "|" D_JSON_TODAY "|" // kWh D_JSON_PERIOD "|" // Wh D_JSON_CURRENT "|" // Ampere D_JSON_VOLTAGE "|" // Volt D_JSON_POWERUSAGE "|" // Watt D_JSON_CO2 "|" // ppm D_JSON_FREQUENCY; // Hz const char kSensorUnit[] PROGMEM = "|||" // degrees Celsius or Fahrenheit "||" // pressure hPa or mmHg "||||" // No unit "%|%|%|%|" // percentage D_UNIT_LUX "|" // lx D_UNIT_KILOOHM "|" // kOhm D_UNIT_KILOWATTHOUR "|" D_UNIT_KILOWATTHOUR "|" D_UNIT_KILOWATTHOUR "|" // kWh D_UNIT_WATTHOUR "|" // Wh D_UNIT_AMPERE "|" // A D_UNIT_VOLT "|" // V D_UNIT_WATT "|" // W D_UNIT_PARTS_PER_MILLION "|" // ppm D_UNIT_HERTZ; // Hz void DisplayJsonValue(const char* topic, const char* device, const char* mkey, const char* value) { SHOW_FREE_MEM(PSTR("DisplayJsonValue")); char temp[TOPSZ]; int quantity_code = GetCommandCode(temp, sizeof(temp), mkey, kSensorQuantity); if ((-1 == quantity_code) || !strcmp_P(mkey, S_RSLT_POWER)) { // Ok: Power, Not ok: POWER return; // Display value not supported } char svalue[Settings->display_cols[1] +1]; // Max sized unit string if (quantity_code <= JSON_HEATINDEX) { // Temperature snprintf_P(svalue, sizeof(svalue), PSTR("%s~%s"), value, disp_temp); // Used by DisplayLogBuffer replace degrees character (276 octal) } else if (quantity_code <= JSON_PRESSUREATSEALEVEL) { // Pressure snprintf_P(svalue, sizeof(svalue), PSTR("%s%s"), value, disp_pres); // hPa or mmHg } else { snprintf_P(svalue, sizeof(svalue), PSTR("%s%s"), value, GetTextIndexed(temp, sizeof(temp), quantity_code, kSensorUnit)); } char buffer[Settings->display_cols[0] +1]; // Max sized buffer string uint32_t size = strlen(topic); if ((Settings->display_rows > 4) && size) { // Skip header if less than five rows if (strcmp(topic, disp_topic)) { // Show topic header only once strcpy(disp_topic, topic); char buffer2[Settings->display_cols[0] +1]; // Max sized buffer string memset(buffer2, '-', sizeof(buffer2)); // Set to - buffer2[sizeof(buffer2) -1] = '\0'; snprintf_P(buffer, sizeof(buffer), PSTR("- %s %s"), topic, buffer2); // - pow1 ------------- DisplayLogBufferAdd(buffer); } size = 0; // Remove topic from source } memset(buffer, ' ', sizeof(buffer)); // Temporarily use for spaces buffer[sizeof(buffer) -1] = '\0'; char source[Settings->display_cols[0] - Settings->display_cols[1]]; // Max sized source string snprintf_P(source, sizeof(source), PSTR("%s%s%s%s"), (size)?topic:"", (size)?"/":"", mkey, buffer); // pow1/Voltage or Voltage if topic is empty (local sensor or header) snprintf_P(buffer, sizeof(buffer), PSTR("%s %s"), source, svalue); // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "topic [%s], device [%s], mkey [%s], source [%s], value [%s], quantity_code %d, log_buffer [%s]"), // topic, device, mkey, source, value, quantity_code, buffer); DisplayLogBufferAdd(buffer); } void DisplayAnalyzeJson(char *topic, const char *json) { // //tele/pow2/STATE {"Time":"2017-09-20T11:53:03", "Uptime":10, "Vcc":3.123, "POWER":"ON", "Wifi":{"AP":2, "SSId":"indebuurt2", "RSSI":68, "APMac":"00:22:6B:FE:8E:20"}} // //tele/pow2/ENERGY {"Time":"2017-09-20T11:53:03", "Total":6.522, "Yesterday":0.150, "Today":0.073, "Period":0.5, "Power":12.1, "Factor":0.56, "Voltage":210.1, "Current":0.102} // tele/pow1/SENSOR = {"Time":"2018-01-02T17:13:17","ENERGY":{"Total":13.091,"Yesterday":0.060,"Today":0.046,"Period":0.2,"Power":9.8,"Factor":0.49,"Voltage":206.8,"Current":0.096}} // tele/dual/STATE {"Time":"2017-09-20T11:53:03","Uptime":25,"Vcc":3.178,"POWER1":"OFF","POWER2":"OFF","Wifi":{"AP":2,"SSId":"indebuurt2","RSSI":100,"APMac":"00:22:6B:FE:8E:20"}} // tele/sc/SENSOR {"Time":"2017-09-20T11:53:09","Temperature":24.0,"Humidity":16.0,"Light":30,"Noise":20,"AirQuality":100,"TempUnit":"C"} // tele/rf1/SENSOR {"Time":"2017-09-20T11:53:23","BH1750":{"Illuminance":57}} // tele/wemos5/SENSOR {"Time":"2017-09-20T11:53:53","SHT1X":{"Temperature":20.1,"Humidity":58.9},"HTU21":{"Temperature":20.7,"Humidity":58.5},"BMP280":{"Temperature":21.6,"Pressure":1020.3},"TempUnit":"C"} // tele/th1/SENSOR {"Time":"2017-09-20T11:54:48","DS18B20":{"Temperature":49.7},"TempUnit":"C"} String jsonStr = json; // Move from stack to heap to fix watchdogs (20180626) JsonParser parser((char*)jsonStr.c_str()); JsonParserObject root = parser.getRootObject(); if (root) { // did JSON parsing went ok? const char *unit = root.getStr(PSTR(D_JSON_TEMPERATURE_UNIT), nullptr); // nullptr if not found if (unit) { snprintf_P(disp_temp, sizeof(disp_temp), PSTR("%s"), unit); // C or F } unit = root.getStr(PSTR(D_JSON_PRESSURE_UNIT), nullptr); // nullptr if not found if (unit) { snprintf_P(disp_pres, sizeof(disp_pres), PSTR("%s"), unit); // hPa or mmHg } for (auto key1 : root) { JsonParserToken value1 = key1.getValue(); if (value1.isObject()) { JsonParserObject Object2 = value1.getObject(); for (auto key2 : Object2) { JsonParserToken value2 = key2.getValue(); if (value2.isObject()) { JsonParserObject Object3 = value2.getObject(); for (auto key3 : Object3) { const char* value3 = key3.getValue().getStr(nullptr); if (value3 != nullptr) { // "DHT11":{"Temperature":null,"Humidity":null} - ignore null as it will raise exception 28 DisplayJsonValue(topic, key1.getStr(), key3.getStr(), value3); // Sensor 56% } } } else { const char* value = value2.getStr(nullptr); if (value != nullptr) { DisplayJsonValue(topic, key1.getStr(), key2.getStr(), value); // Sensor 56% } } } } else { const char* value = value1.getStr(nullptr); if (value != nullptr) { DisplayJsonValue(topic, key1.getStr(), key1.getStr(), value); // Topic 56% } } } } } void DisplayMqttSubscribe(void) { /* Subscribe to tele messages only * Supports the following FullTopic formats * - %prefix%/%topic% * - home/%prefix%/%topic% * - home/level2/%prefix%/%topic% etc. */ char stopic[TOPSZ]; strlcpy(stopic, SettingsText(SET_MQTT_FULLTOPIC), sizeof(stopic)); char *tp = strtok(stopic, "/"); char ntopic[TOPSZ]; ntopic[0] = '\0'; while (tp != nullptr) { if (!strcmp_P(tp, MQTT_TOKEN_PREFIX)) { break; } strncat_P(ntopic, PSTR("+/"), sizeof(ntopic) - strlen(ntopic) -1); // Add single-level wildcards tp = strtok(nullptr, "/"); } strncat(ntopic, SettingsText(SET_MQTTPREFIX3), sizeof(ntopic) - strlen(ntopic) -1); // Subscribe to tele messages strncat_P(ntopic, PSTR("/#"), sizeof(ntopic) - strlen(ntopic) -1); // Add multi-level wildcard if (Settings->display_model && (Settings->display_mode &0x04)) { disp_subscribed = true; MqttSubscribe(ntopic); } else { if (disp_subscribed) { disp_subscribed = false; MqttUnsubscribe(ntopic); } } } bool DisplayMqttData(void) { if (disp_subscribed) { char stopic[TOPSZ]; snprintf_P(stopic, sizeof(stopic) , PSTR("%s/"), SettingsText(SET_MQTTPREFIX3)); // tele/ char *tp = strstr(XdrvMailbox.topic, stopic); if (tp) { // tele/tasmota/SENSOR if (Settings->display_mode &0x04) { tp = tp + strlen(stopic); // tasmota/SENSOR char *topic = strtok(tp, "/"); // tasmota DisplayAnalyzeJson(topic, XdrvMailbox.data); } return true; } } return false; } void DisplayLocalSensor(void) { if ((Settings->display_mode &0x02) && (0 == TasmotaGlobal.tele_period)) { char no_topic[1] = { 0 }; // DisplayAnalyzeJson(TasmotaGlobal.mqtt_topic, ResponseData()); // Add local topic DisplayAnalyzeJson(no_topic, ResponseData()); // Discard any topic } } #endif // USE_DISPLAY_MODES1TO5 /*********************************************************************************************\ * Public \*********************************************************************************************/ void DisplayInitDriver(void) { XdspCall(FUNC_DISPLAY_INIT_DRIVER); // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "Display model %d"), Settings->display_model); if (Settings->display_model) { // ApplyDisplayDimmer(); // Not allowed here. Way too early in init sequence. Global power state has not been set at this point in time #ifdef USE_MULTI_DISPLAY Set_display(0); #endif // USE_MULTI_DISPLAY if (renderer) { renderer->setTextFont(Settings->display_font); renderer->setTextSize(Settings->display_size); // force opaque mode renderer->setDrawMode(0); for (uint32_t cnt = 0; cnt < (MAX_INDEXCOLORS - PREDEF_INDEXCOLORS); cnt++) { index_colors[cnt] = 0; } } #ifdef USE_DT_VARS free_dt_vars(); #endif #ifdef USE_UFILESYS Display_Text_From_File(DISP_BATCH_FILE); #endif #ifdef USE_GRAPH for (uint8_t count = 0; count < NUM_GRAPHS; count++) { graph[count] = 0; } #endif UpdateDevicesPresent(1); if (!PinUsed(GPIO_BACKLIGHT)) { if ((LT_PWM1 == TasmotaGlobal.light_type) && // Single PWM light channel ((4 == Settings->display_model) || // ILI9341 legacy (17 == Settings->display_model)) // Universal ) { UpdateDevicesPresent(-1); // Assume PWM channel is used for backlight } } disp_device = TasmotaGlobal.devices_present; #ifndef USE_DISPLAY_MODES1TO5 Settings->display_mode = 0; #else DisplayLogBufferInit(); #endif // USE_DISPLAY_MODES1TO5 } } void DisplaySetPower(void) { if (!disp_device) { return; } // Not initialized yet disp_power = bitRead(XdrvMailbox.index, disp_device -1); //AddLog(LOG_LEVEL_DEBUG, PSTR("DSP: Power %d"), disp_power); if (Settings->display_model) { if (!renderer) { XdspCall(FUNC_DISPLAY_POWER); } else { renderer->DisplayOnff(disp_power); #ifdef USE_BERRY // still call Berry virtual display in case it is not managed entirely by renderer Xdsp18(FUNC_DISPLAY_POWER); #endif // USE_BERRY } } } /*********************************************************************************************\ * Commands \*********************************************************************************************/ void CmndDisplay(void) { Response_P(PSTR("{\"" D_PRFX_DISPLAY "\":{\"" D_CMND_DISP_MODEL "\":%d,\"" D_CMND_DISP_TYPE "\":%d,\"" D_CMND_DISP_WIDTH "\":%d,\"" D_CMND_DISP_HEIGHT "\":%d,\"" D_CMND_DISP_MODE "\":%d,\"" D_CMND_DISP_DIMMER "\":%d,\"" D_CMND_DISP_SIZE "\":%d,\"" D_CMND_DISP_FONT "\":%d,\"" D_CMND_DISP_ROTATE "\":%d,\"" D_CMND_DISP_INVERT "\":%d,\"" D_CMND_DISP_REFRESH "\":%d,\"" D_CMND_DISP_COLS "\":[%d,%d],\"" D_CMND_DISP_ROWS "\":%d}}"), Settings->display_model, Settings->display_options.type, Settings->display_width, Settings->display_height, Settings->display_mode, GetDisplayDimmer(), Settings->display_size, Settings->display_font, Settings->display_rotate, Settings->display_options.invert, Settings->display_refresh, Settings->display_cols[0], Settings->display_cols[1], Settings->display_rows); } void CmndDisplayModel(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload < DISPLAY_MAX_DRIVERS)) { uint32_t last_display_model = Settings->display_model; Settings->display_model = XdrvMailbox.payload; if (XdspCall(FUNC_DISPLAY_MODEL)) { TasmotaGlobal.restart_flag = 2; // Restart to re-init interface and add/Remove MQTT subscribe } else { Settings->display_model = last_display_model; } } ResponseCmndNumber(Settings->display_model); } void CmndDisplayType(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 7)) { Settings->display_options.type = XdrvMailbox.payload; TasmotaGlobal.restart_flag = 2; } ResponseCmndNumber(Settings->display_options.type); } void CmndDisplayWidth(void) { if (XdrvMailbox.payload > 0) { if (XdrvMailbox.payload != Settings->display_width) { Settings->display_width = XdrvMailbox.payload; TasmotaGlobal.restart_flag = 2; // Restart to re-init width } } ResponseCmndNumber(Settings->display_width); } void CmndDisplayHeight(void) { if (XdrvMailbox.payload > 0) { if (XdrvMailbox.payload != Settings->display_height) { Settings->display_height = XdrvMailbox.payload; TasmotaGlobal.restart_flag = 2; // Restart to re-init height } } ResponseCmndNumber(Settings->display_height); } void CmndDisplayMode(void) { #ifdef USE_DISPLAY_MODES1TO5 /* Matrix / 7-segment LCD / Oled TFT * 1 = Text up and time Time * 2 = Date Local sensors Local sensors * 3 = Day Local sensors and time Local sensors and time * 4 = Mqtt left and time Mqtt (incl local) sensors Mqtt (incl local) sensors * 5 = Mqtt up and time Mqtt (incl local) sensors and time Mqtt (incl local) sensors and time */ if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 5)) { uint32_t last_display_mode = Settings->display_mode; Settings->display_mode = XdrvMailbox.payload; if (last_display_mode != Settings->display_mode) { // Switch to different mode if ((!last_display_mode && Settings->display_mode) || // Switch to mode 1, 2, 3 or 4 (last_display_mode && !Settings->display_mode)) { // Switch to mode 0 DisplayInit(DISPLAY_INIT_MODE); } if (1 == Settings->display_mode) { // Switch to mode 1 DisplayClear(); } else if (Settings->display_mode > 1) { // Switch to mode 2, 3 or 4 DisplayLogBufferInit(); } DisplayMqttSubscribe(); } } #endif // USE_DISPLAY_MODES1TO5 ResponseCmndNumber(Settings->display_mode); } // Apply the current display dimmer void ApplyDisplayDimmer(void) { disp_apply_display_dimmer_request = true; if ((disp_power < 0) || !disp_device) { return; } // Not initialized yet disp_apply_display_dimmer_request = false; uint8_t dimmer8 = changeUIntScale(GetDisplayDimmer(), 0, 100, 0, 255); uint16_t dimmer10_gamma = ledGamma10(dimmer8); if (dimmer8 && !(disp_power)) { ExecuteCommandPower(disp_device, POWER_ON, SRC_DISPLAY); } else if (!dimmer8 && disp_power) { ExecuteCommandPower(disp_device, POWER_OFF, SRC_DISPLAY); } if (renderer) { renderer->dim10(dimmer8, dimmer10_gamma); // provide 8 bits and gamma corrected dimmer in 8 bits #ifdef USE_BERRY // still call Berry virtual display in case it is not managed entirely by renderer Xdsp18(FUNC_DISPLAY_DIM); #endif // USE_BERRY } else { XdspCall(FUNC_DISPLAY_DIM); } } void CmndDisplayDimmer(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { uint8_t dimmer = XdrvMailbox.payload; SetDisplayDimmer(dimmer); ApplyDisplayDimmer(); } ResponseCmndNumber(GetDisplayDimmer()); } void CmndDisplaySize(void) { if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= TXT_MAX_SFAC)) { Settings->display_size = XdrvMailbox.payload; DisplayClear(); if (renderer) renderer->setTextSize(Settings->display_size); //else DisplaySetSize(Settings->display_size); } ResponseCmndNumber(Settings->display_size); } void CmndDisplayFont(void) { if ((XdrvMailbox.payload >=0) && (XdrvMailbox.payload <= 4)) { Settings->display_font = XdrvMailbox.payload; DisplayClear(); if (renderer) renderer->setTextFont(Settings->display_font); //else DisplaySetFont(Settings->display_font); } ResponseCmndNumber(Settings->display_font); } void CmndDisplayRotate(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload < 4)) { if ((Settings->display_rotate) != XdrvMailbox.payload) { /* // Needs font info regarding height and width if ((Settings->display_rotate &1) != (XdrvMailbox.payload &1)) { uint8_t temp_rows = Settings->display_rows; Settings->display_rows = Settings->display_cols[0]; Settings->display_cols[0] = temp_rows; #ifdef USE_DISPLAY_MODES1TO5 DisplayReAllocScreenBuffer(); #endif // USE_DISPLAY_MODES1TO5 } */ Settings->display_rotate = XdrvMailbox.payload; DisplayInit(DISPLAY_INIT_MODE); #ifdef USE_DISPLAY_MODES1TO5 DisplayLogBufferInit(); #endif // USE_DISPLAY_MODES1TO5 } } ResponseCmndNumber(Settings->display_rotate); } void CmndDisplayInvert(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { Settings->display_options.invert = XdrvMailbox.payload; if (renderer) renderer->invertDisplay(Settings->display_options.invert); } ResponseCmndNumber(Settings->display_options.invert); } void CmndDisplayRefresh(void) { if ((XdrvMailbox.payload >= 1) && (XdrvMailbox.payload <= 7)) { Settings->display_refresh = XdrvMailbox.payload; } ResponseCmndNumber(Settings->display_refresh); } void CmndDisplayColumns(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 2)) { if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= DISPLAY_MAX_COLS)) { Settings->display_cols[XdrvMailbox.index -1] = XdrvMailbox.payload; #ifdef USE_DISPLAY_MODES1TO5 if (1 == XdrvMailbox.index) { DisplayLogBufferInit(); DisplayReAllocScreenBuffer(); } #endif // USE_DISPLAY_MODES1TO5 } ResponseCmndIdxNumber(Settings->display_cols[XdrvMailbox.index -1]); } } void CmndDisplayRows(void) { if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= DISPLAY_MAX_ROWS)) { Settings->display_rows = XdrvMailbox.payload; #ifdef USE_DISPLAY_MODES1TO5 DisplayLogBufferInit(); DisplayReAllocScreenBuffer(); #endif // USE_DISPLAY_MODES1TO5 } ResponseCmndNumber(Settings->display_rows); } void CmndDisplayAddress(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 8)) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 255)) { Settings->display_address[XdrvMailbox.index -1] = XdrvMailbox.payload; } ResponseCmndIdxNumber(Settings->display_address[XdrvMailbox.index -1]); } } void CmndDisplayBlinkrate(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 3)) { if (!renderer) { XdspCall(FUNC_DISPLAY_BLINKRATE); } } ResponseCmndNumber(XdrvMailbox.payload); } #ifdef USE_UFILESYS void CmndDisplayBatch(void) { if (XdrvMailbox.data_len > 0) { if (!Settings->display_mode) { Display_Text_From_File(XdrvMailbox.data); } ResponseCmndChar(XdrvMailbox.data); } } #endif void CmndDisplayText(void) { if (disp_device && XdrvMailbox.data_len > 0) { if(Settings->display_model == 15 || Settings->display_model == 20) { XdspCall(FUNC_DISPLAY_SEVENSEG_TEXT); } else { DisplayText(); } ResponseCmndChar(XdrvMailbox.data); } } /*********************************************************************************************\ * Currently 7-segement specific - should have been handled by (extended) DisplayText command \*********************************************************************************************/ void CmndDisplayClear(void) { DisplayClear(); ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayNumber(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_NUMBER); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayFloat(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_FLOAT); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayNumberNC(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_NUMBERNC); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayFloatNC(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_FLOATNC); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayRaw(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_RAW); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayLevel(void) { bool result = false; if (!renderer) { result = XdspCall(FUNC_DISPLAY_LEVEL); } if(result) ResponseCmndNumber(XdrvMailbox.payload); } void CmndDisplaySevensegText(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_SEVENSEG_TEXT); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayTextNC(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_SEVENSEG_TEXTNC); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplaySevensegTextNC(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_SEVENSEG_TEXTNC); } ResponseCmndChar(XdrvMailbox.data); } void CmndDisplayScrollDelay(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_SCROLLDELAY); } ResponseCmndNumber(XdrvMailbox.payload); } void CmndDisplayClock(void) { if (!renderer) { XdspCall(FUNC_DISPLAY_CLOCK); } ResponseCmndNumber(XdrvMailbox.payload); } void CmndDisplayScrollText(void) { bool result = false; if (!renderer) { result = XdspCall(FUNC_DISPLAY_SCROLLTEXT); } if(result) ResponseCmndChar(XdrvMailbox.data); } void DisplayReInitDriver(void) { XdspCall(FUNC_DISPLAY_INIT_DRIVER); #ifdef USE_MULTI_DISPLAY Set_display(0); #endif // USE_MULTI_DISPLAY ResponseCmndDone(); } /*********************************************************************************************\ * Optional drivers \*********************************************************************************************/ #ifdef USE_TOUCH_BUTTONS // very limited path size, so, add .jpg void draw_picture(char *path, uint32_t xp, uint32_t yp, uint32_t xs, uint32_t ys, uint32_t ocol, bool inverted) { char ppath[16]; strcpy(ppath, path); uint8_t plen = strlen(path) -1; if (ppath[plen]=='1') { // index mode if (inverted) { ppath[plen] = '2'; } inverted = false; } if (ocol == 9) { strcat(ppath, ".rgb"); } else { strcat(ppath, ".jpg"); } Draw_RGB_Bitmap(ppath, xp, yp, 0, inverted, 0, 0); } #endif // USE_TOUCH_BUTTONS #ifdef ESP32 #ifdef JPEG_PICTS #include "img_converters.h" #include "esp_jpg_decode.h" bool jpg2rgb888(const uint8_t *src, size_t src_len, uint8_t * out, jpg_scale_t scale); bool jpg2rgb565(const uint8_t *src, size_t src_len, uint8_t * out, jpg_scale_t scale); char get_jpeg_size(unsigned char* data, unsigned int data_size, unsigned short *width, unsigned short *height); #endif // JPEG_PICTS #endif // ESP32 //#define SLOW_RGB16 #ifdef USE_UFILESYS extern FS *ufsp; #define XBUFF_LEN 128 void Draw_RGB_Bitmap(char *file, uint16_t xp, uint16_t yp, uint8_t scale, bool inverted, uint16_t xs, uint16_t ys ) { if (!renderer) return; File fp; char *ending = 0; for (uint32_t cnt = strlen(file) - 1; cnt >= 0; cnt--) { if (file[cnt] == '.') { ending = &file[cnt + 1]; break; } } if (!ending) return; char estr[8]; memset(estr, 0, sizeof(estr)); for (uint32_t cnt = 0; cnt < strlen(ending); cnt++) { estr[cnt] = tolower(ending[cnt]); } if (!strcmp(estr,"rgb")) { // special rgb format fp = ufsp->open(file, FS_FILE_READ); if (!fp) return; uint16_t xsize; fp.read((uint8_t*)&xsize, 2); uint16_t ysize; fp.read((uint8_t*)&ysize, 2); uint16_t xoffs; uint16_t yoffs; if (xs > 0) { // center in area xoffs = (xs - xsize) / 2; yoffs = (ys - ysize) / 2; xp += xoffs; yp += yoffs; } #ifndef SLOW_RGB16 renderer->setAddrWindow(xp, yp, xp + xsize, yp + ysize); uint16_t *rgb = (uint16_t *)special_malloc(xsize * 2); if (rgb) { //uint16_t rgb[xsize]; for (int16_t j = 0; j < ysize; j++) { fp.read((uint8_t*)rgb, xsize * 2); renderer->pushColors(rgb, xsize, true); OsWatchLoop(); } free(rgb); } renderer->setAddrWindow(0, 0, 0, 0); #else for (int16_t j = 0; j < ysize; j++) { for (int16_t i = 0; i < xsize; i++ ) { uint16_t rgb; uint8_t res = fp.read((uint8_t*)&rgb, 2); if (!res) break; renderer->writePixel(xp + i, yp, rgb); } delay(0); OsWatchLoop(); yp++; } #endif fp.close(); } else if (!strcmp(estr,"jpg") || !strcmp(estr,"jpeg")) { // jpeg files on ESP32 with more memory #ifdef ESP32 #ifdef JPEG_PICTS fp = ufsp->open(file, FS_FILE_READ); if (!fp) { // try url Draw_JPG_from_URL(file, xp, yp, scale); return; } uint32_t size = fp.size(); uint8_t *mem = (uint8_t *)special_malloc(size + 4); if (mem) { uint8_t res = fp.read(mem, size); if (res) { uint16_t xsize; uint16_t ysize; uint16_t xoffs; uint16_t yoffs; if (mem[0] == 0xff && mem[1] == 0xd8) { get_jpeg_size(mem, size, &xsize, &ysize); if (xs > 0) { // center in area xoffs = (xs - xsize) / 2; yoffs = (ys - ysize) / 2; xp += xoffs; yp += yoffs; } //Serial.printf(" x,y,fs %d - %d - %d\n",xsize, ysize, size ); if (xsize && ysize) { uint8_t *out_buf = (uint8_t *)special_malloc((xsize * ysize * 3) + 4); if (out_buf) { uint16_t *pixb = (uint16_t *)special_malloc((xsize * 2) + 4); if (pixb) { uint8_t *ob = out_buf; if (jpg2rgb888(mem, size, out_buf, (jpg_scale_t)JPG_SCALE_NONE)) { renderer->setAddrWindow(xp, yp, xp + xsize, yp + ysize); for (int32_t j = 0; j < ysize; j++) { if (inverted == false) { rgb888_to_565(ob, pixb, xsize); } else { rgb888_to_565i(ob, pixb, xsize); } ob += xsize * 3; renderer->pushColors(pixb, xsize, true); OsWatchLoop(); } renderer->setAddrWindow(0, 0, 0, 0); } free(out_buf); free(pixb); } else { free(out_buf); } } } if (scale) { if (renderer) renderer->drawRect(xp, yp, xsize, ysize, GetColorFromIndex(scale)); } } free(mem); } fp.close(); } #endif // JPEG_PICTS #endif // ESP32 } } #ifdef ESP32 #ifdef JPEG_PICTS #define JPG_DEFSIZE 150000 void Draw_jpeg(uint8_t *mem, uint16_t jpgsize, uint16_t xp, uint16_t yp, uint8_t scale) { if (mem[0] == 0xff && mem[1] == 0xd8) { uint16_t xsize; uint16_t ysize; get_jpeg_size(mem, jpgsize, &xsize, &ysize); //AddLog(LOG_LEVEL_INFO, PSTR("Pict size %d - %d - %d"), xsize, ysize, jpgsize); scale &= 3; uint8_t fac = 1 << scale; xsize /= fac; ysize /= fac; renderer->setAddrWindow(xp, yp, xp + xsize, yp + ysize); uint8_t *rgbmem = (uint8_t *)special_malloc(xsize * ysize * 2); if (rgbmem) { //jpg2rgb565(mem, jpgsize, rgbmem, JPG_SCALE_NONE); jpg2rgb565(mem, jpgsize, rgbmem, (jpg_scale_t)scale); renderer->pushColors((uint16_t*)rgbmem, xsize * ysize, true); free(rgbmem); } renderer->setAddrWindow(0, 0, 0, 0); } } void Draw_JPG_from_URL(char *url, uint16_t xp, uint16_t yp, uint8_t scale) { uint8_t *mem = 0; WiFiClient http_client; HTTPClient http; int32_t httpCode = 0; String weburl = "http://" + UrlEncode(url); http.begin(http_client, weburl); httpCode = http.GET(); //AddLog(LOG_LEVEL_INFO, PSTR("HTTP RESULT %d %s"), httpCode , weburl.c_str()); uint32_t jpgsize = 0; if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { mem = (uint8_t *)special_malloc(JPG_DEFSIZE); if (!mem) return; uint8_t *jpgp = mem; WiFiClient *stream = http.getStreamPtr(); int32_t len = http.getSize(); if (len < 0) len = 99999999; while (http.connected() && (len > 0)) { size_t size = stream->available(); if (size) { if (size > JPG_DEFSIZE) { size = JPG_DEFSIZE; } uint32_t read = stream->readBytes(jpgp, size); len -= read; jpgp += read; jpgsize += read; //AddLog(LOG_LEVEL_INFO,PSTR("HTTP read %d - %d"), read, jpgsize); } delayMicroseconds(1); } } else { AddLog(LOG_LEVEL_INFO, PSTR("HTTP ERROR %s"), http.getString().c_str()); } http.end(); http_client.stop(); if (jpgsize) { Draw_jpeg(mem, jpgsize, xp, yp, scale); } if (mem) free(mem); } #endif // JPEG_PICTS #endif // ESP32 #endif // USE_UFILESYS /*********************************************************************************************\ * AWatch \*********************************************************************************************/ #ifdef USE_AWATCH #define MINUTE_REDUCT 4 #ifndef pi #define pi 3.14159265359 #endif // draw analog watch, just for fun void DrawAClock(uint16_t rad) { if (!renderer) return; float frad=rad; uint16_t hred=frad/3.0; renderer->fillCircle(disp_xpos, disp_ypos, rad, bg_color); renderer->drawCircle(disp_xpos, disp_ypos, rad, fg_color); renderer->fillCircle(disp_xpos, disp_ypos, 4, fg_color); for (uint8_t count=0; count<60; count+=5) { float p1=((float)count*(pi/30)-(pi/2)); uint8_t len; if ((count%15)==0) { len=4; } else { len=2; } renderer->writeLine(disp_xpos+((float)(rad-len)*cosf(p1)), disp_ypos+((float)(rad-len)*sinf(p1)), disp_xpos+(frad*cosf(p1)), disp_ypos+(frad*sinf(p1)), fg_color); } // hour float hour=((float)RtcTime.hour*60.0+(float)RtcTime.minute)/60.0; float temp=(hour*(pi/6.0)-(pi/2.0)); renderer->writeLine(disp_xpos, disp_ypos,disp_xpos+(frad-hred)*cosf(temp),disp_ypos+(frad-hred)*sinf(temp), fg_color); // minute temp=((float)RtcTime.minute*(pi/30.0)-(pi/2.0)); renderer->writeLine(disp_xpos, disp_ypos,disp_xpos+(frad-MINUTE_REDUCT)*cosf(temp),disp_ypos+(frad-MINUTE_REDUCT)*sinf(temp), fg_color); } #endif // USE_AWATCH /*********************************************************************************************\ * Graphics \*********************************************************************************************/ #ifdef USE_GRAPH #define TICKLEN 4 void ClrGraph(uint16_t num) { struct GRAPH *gp=graph[num]; uint16_t xticks=gp->xticks; uint16_t yticks=gp->yticks; uint16_t count; // clr inside, but only 1.graph if overlapped if (gp->flags.overlay) return; renderer->fillRect(gp->xp+1,gp->yp+1,gp->xs-2,gp->ys-2,gp->bg_color); if (xticks) { float cxp=gp->xp,xd=(float)gp->xs/(float)xticks; for (count=0; countwriteFastVLine(cxp,gp->yp+gp->ys-TICKLEN,TICKLEN,gp->fg_color); cxp+=xd; } } if (yticks) { if (gp->ymin<0 && gp->ymax>0) { // draw zero seperator float cxp=0; float czp=gp->yp+(gp->ymax/gp->range); while (cxpxs) { renderer->writeFastHLine(gp->xp+cxp,czp,2,gp->fg_color); cxp+=6.0; } // align ticks to zero line float cyp=0,yd=gp->ys/yticks; for (count=0; countgp->yp) { renderer->writeFastHLine(gp->xp,czp-cyp,TICKLEN,gp->fg_color); renderer->writeFastHLine(gp->xp+gp->xs-TICKLEN,czp-cyp,TICKLEN,gp->fg_color); } if ((czp+cyp)<(gp->yp+gp->ys)) { renderer->writeFastHLine(gp->xp,czp+cyp,TICKLEN,fg_color); renderer->writeFastHLine(gp->xp+gp->xs-TICKLEN,czp+cyp,TICKLEN,gp->fg_color); } cyp+=yd; } } else { float cyp=gp->yp,yd=gp->ys/yticks; for (count=0; countwriteFastHLine(gp->xp,cyp,TICKLEN,gp->fg_color); renderer->writeFastHLine(gp->xp+gp->xs-TICKLEN,cyp,TICKLEN,gp->fg_color); cyp+=yd; } } } } // define a graph void DefineGraph(uint16_t num,uint16_t xp,uint16_t yp,int16_t xs,uint16_t ys,int16_t dec,float ymin, float ymax,uint8_t icol) { if (!renderer) return; uint8_t rflg=0; if (xs<0) { rflg=1; xs=abs(xs); } struct GRAPH *gp; uint16_t count; uint16_t index=num%NUM_GRAPHS; if (!graph[index]) { gp=(struct GRAPH*)calloc(sizeof(struct GRAPH),1); if (!gp) return; graph[index]=gp; } else { gp=graph[index]; if (rflg) { RedrawGraph(index,1); return; } } gp->bg_color=bg_color; gp->fg_color=fg_color; // 6 bits per axis gp->xticks=(num>>4)&0x3f; gp->yticks=(num>>10)&0x3f; gp->xp=xp; gp->yp=yp; gp->xs=xs; gp->ys=ys; if (!dec) dec=1; gp->decimation=dec; if (dec>0) { // is minutes per sweep prepare timing parameters in ms gp->x_time=((float)dec*60000.0)/(float)xs; gp->last_ms=millis()+gp->x_time; } gp->ymin=ymin; gp->ymax=ymax; gp->range=(ymax-ymin)/ys; gp->xcnt=0; gp->dcnt=0; gp->summ=0; if (gp->values) free(gp->values); gp->values=(uint8_t*) calloc(1,xs+2); if (!gp->values) { free(gp); graph[index]=0; return; } // start from zero gp->values[0]=0; gp->last_ms_redrawn=millis(); if (!icol) icol=1; gp->color_index=icol; gp->flags.overlay=0; gp->flags.draw=1; // check if previous graph has same coordinates if (index>0) { for (uint8_t count=0; countxp==gp1->xp) && (gp->yp==gp1->yp)) { gp->flags.overlay=1; break; } } } } // draw rectangle renderer->drawRect(xp,yp,xs,ys,gp->fg_color); // clr inside ClrGraph(index); } // check if to advance GRAPH void DisplayCheckGraph() { int16_t count; struct GRAPH *gp; for (count=0;countdecimation>0) { // if time over add value while (millis()>gp->last_ms) { gp->last_ms+=gp->x_time; uint8_t val; if (gp->dcnt) { val=gp->summ/gp->dcnt; gp->dcnt=0; gp->summ=0; gp->last_val=val; } else { val=gp->last_val; } AddGraph(count,val); } } } } } #if (defined(USE_SCRIPT_FATFS) && defined(USE_SCRIPT)) || defined(USE_UFILESYS) #ifdef ESP32 #include #endif void Save_graph(uint8_t num, char *path) { if (!renderer) return; uint16_t index=num%NUM_GRAPHS; struct GRAPH *gp=graph[index]; if (!gp) return; File fp; ufsp->remove(path); fp=ufsp->open(path,FS_FILE_WRITE); if (!fp) return; char str[32]; sprintf_P(str,PSTR("%d\t%d\t%d\t"),gp->xcnt,gp->xs,gp->ys); fp.print(str); dtostrfd(gp->ymin,2,str); fp.print(str); fp.print("\t"); dtostrfd(gp->ymax,2,str); fp.print(str); fp.print("\t"); for (uint32_t count=0;countxs;count++) { dtostrfd(gp->values[count],0,str); fp.print(str); fp.print("\t"); } fp.print("\n"); fp.close(); } void Restore_graph(uint8_t num, char *path) { if (!renderer) return; uint16_t index=num%NUM_GRAPHS; struct GRAPH *gp=graph[index]; if (!gp) return; File fp; fp=ufsp->open(path,FS_FILE_READ); if (!fp) return; char vbuff[32]; char *cp=vbuff; uint8_t buf[2]; uint8_t findex=0; for (uint32_t count=0;count<=gp->xs+4;count++) { cp=vbuff; findex=0; while (fp.available()) { fp.read(buf,1); if (buf[0]=='\t' || buf[0]==',' || buf[0]=='\n' || buf[0]=='\r') { break; } else { *cp++=buf[0]; findex++; if (findex>=sizeof(vbuff)-1) break; } } *cp=0; if (count<=4) { if (count==0) gp->xcnt=atoi(vbuff); } else { uint8_t yval = atoi(vbuff); if (yval >= gp->ys) { yval = gp->ys - 1; } gp->values[count-5] = yval; } } fp.close(); RedrawGraph(num,1); } #endif // USE_SCRIPT_FATFS void RedrawGraph(uint8_t num, uint8_t flags) { uint16_t index=num%NUM_GRAPHS; struct GRAPH *gp=graph[index]; if (!gp) return; if (!flags) { gp->flags.draw=0; return; } if (!renderer) return; gp->flags.draw=1; uint16_t linecol=gp->fg_color; if (color_type==COLOR_COLOR) { linecol = GetColorFromIndex(gp->color_index); } if (!gp->flags.overlay) { // draw rectangle renderer->drawRect(gp->xp,gp->yp,gp->xs,gp->ys,gp->fg_color); // clr inside ClrGraph(index); } for (uint16_t count=0;countxs-1;count++) { renderer->writeLine(gp->xp+count,gp->yp+gp->ys-gp->values[count]-1,gp->xp+count+1,gp->yp+gp->ys-gp->values[count+1]-1,linecol); } } // add next value to graph void AddGraph(uint8_t num,uint8_t val) { struct GRAPH *gp=graph[num]; if (!renderer) return; uint16_t linecol=gp->fg_color; if (color_type==COLOR_COLOR) { linecol = GetColorFromIndex(gp->color_index); } gp->xcnt++; if (gp->xcnt>gp->xs) { gp->xcnt=gp->xs; int16_t count; // shift values for (count=0;countxs-1;count++) { gp->values[count]=gp->values[count+1]; } gp->values[gp->xcnt-1]=val; if (!gp->flags.draw) return; // only redraw every second or longer if (millis()-gp->last_ms_redrawn>1000) { gp->last_ms_redrawn=millis(); // clr area and redraw graph if (!gp->flags.overlay) { // draw rectangle renderer->drawRect(gp->xp,gp->yp,gp->xs,gp->ys,gp->fg_color); // clr inner and draw ticks ClrGraph(num); } for (count=0;countxs-1;count++) { renderer->writeLine(gp->xp+count,gp->yp+gp->ys-gp->values[count]-1,gp->xp+count+1,gp->yp+gp->ys-gp->values[count+1]-1,linecol); } } } else { // add value and draw a single line gp->values[gp->xcnt]=val; if (!gp->flags.draw) return; renderer->writeLine(gp->xp+gp->xcnt-1,gp->yp+gp->ys-gp->values[gp->xcnt-1]-1,gp->xp+gp->xcnt,gp->yp+gp->ys-gp->values[gp->xcnt]-1,linecol); } } // add next value void AddValue(uint8_t num,float fval) { // not yet defined ??? num=num%NUM_GRAPHS; struct GRAPH *gp=graph[num]; if (!gp) return; if (fval>gp->ymax) fval=gp->ymax; if (fvalymin) fval=gp->ymin; int16_t val; val=(fval-gp->ymin)/gp->range; if (val>gp->ys-1) val=gp->ys-1; if (val<0) val=0; // summ values gp->summ+=val; gp->dcnt++; // decimation option if (gp->decimation<0) { if (gp->dcnt>=-gp->decimation) { // calc average val=gp->summ/gp->dcnt; gp->dcnt=0; gp->summ=0; // add to graph AddGraph(num,val); } } } #endif // USE_GRAPH /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv13(uint32_t function) { bool result = false; if (XdspPresent()) { switch (function) { case FUNC_PRE_INIT: DisplayInitDriver(); break; case FUNC_INIT: if (disp_apply_display_dimmer_request) { ApplyDisplayDimmer(); // Allowed here. } break; case FUNC_EVERY_50_MSECOND: if (Settings->display_model) { XdspCall(FUNC_DISPLAY_EVERY_50_MSECOND); } break; case FUNC_SET_POWER: DisplaySetPower(); break; case FUNC_EVERY_SECOND: #ifdef USE_GRAPH DisplayCheckGraph(); #endif // USE_GRAPH #ifdef USE_DT_VARS get_dt_mqtt(); draw_dt_vars(); #endif // USE_DT_VARS #ifdef USE_DISPLAY_MODES1TO5 if (Settings->display_model && Settings->display_mode) { uint32_t wait = 0; if (!Settings->flag5.display_no_splash) { // SetOption135 - (Display & LVGL) force disabling default 5 second splash screen wait = 6; } if (TasmotaGlobal.uptime > wait) { // Allow time to display splash screen XdspCall(FUNC_DISPLAY_EVERY_SECOND); } } #endif // USE_DISPLAY_MODES1TO5 break; case FUNC_AFTER_TELEPERIOD: #ifdef USE_DT_VARS DisplayDTVarsTeleperiod(); #endif // USE_DT_VARS break; #ifdef USE_DISPLAY_MODES1TO5 case FUNC_MQTT_SUBSCRIBE: DisplayMqttSubscribe(); break; case FUNC_MQTT_DATA: result = DisplayMqttData(); break; case FUNC_SHOW_SENSOR: DisplayLocalSensor(); break; #endif // USE_DISPLAY_MODES1TO5 case FUNC_COMMAND: result = DecodeCommand(kDisplayCommands, DisplayCommand); break; case FUNC_ACTIVE: result = true; break; } } return result; } #endif // USE_DISPLAY