diff --git a/ESP32_AP-Flasher/data/fonts/bahnschrift80.vlw b/ESP32_AP-Flasher/data/fonts/bahnschrift80.vlw deleted file mode 100644 index 84d11fff..00000000 Binary files a/ESP32_AP-Flasher/data/fonts/bahnschrift80.vlw and /dev/null differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib30.vlw b/ESP32_AP-Flasher/data/fonts/calibrib30.vlw index f2da8ab3..6c813489 100644 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib30.vlw and b/ESP32_AP-Flasher/data/fonts/calibrib30.vlw differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib35.vlw b/ESP32_AP-Flasher/data/fonts/calibrib35.vlw deleted file mode 100644 index f6a1d70d..00000000 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib35.vlw and /dev/null differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib40.vlw b/ESP32_AP-Flasher/data/fonts/calibrib40.vlw deleted file mode 100644 index 4ed81027..00000000 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib40.vlw and /dev/null differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib50.vlw b/ESP32_AP-Flasher/data/fonts/calibrib50.vlw index 6c0b0dd1..b2b327fa 100644 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib50.vlw and b/ESP32_AP-Flasher/data/fonts/calibrib50.vlw differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib62.vlw b/ESP32_AP-Flasher/data/fonts/calibrib62.vlw index 97ea7ad3..fb235b10 100644 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib62.vlw and b/ESP32_AP-Flasher/data/fonts/calibrib62.vlw differ diff --git a/ESP32_AP-Flasher/data/www/index.html b/ESP32_AP-Flasher/data/www/index.html index 9fb22b36..ea9c7520 100644 --- a/ESP32_AP-Flasher/data/www/index.html +++ b/ESP32_AP-Flasher/data/www/index.html @@ -38,6 +38,7 @@ +

@@ -80,7 +81,7 @@

diff --git a/ESP32_AP-Flasher/data/www/main.css b/ESP32_AP-Flasher/data/www/main.css index be547bdc..c0459d9f 100644 --- a/ESP32_AP-Flasher/data/www/main.css +++ b/ESP32_AP-Flasher/data/www/main.css @@ -92,14 +92,17 @@ input { border-radius: 0px; } -input[type=button] { +input[type=button], button { border: 0px; padding: 4px 10px; cursor:pointer; } -input[type=button]:hover { + +input[type=button]:hover, +button:hover { background-color:#aaaaaa; } + select { padding: 4px; border-radius: 0px; @@ -128,7 +131,7 @@ select { #configbox input, #apconfigbox input { border: solid 1px #666666; - padding: 4px; + /*padding: 4px;*/ } #configbox label, #apconfigbox label { @@ -326,6 +329,81 @@ ul.messages li.new { color: red; } +#paintbutton { + padding: 1px 3px; + border: 1px solid black; + font-size: 1.3em; + vertical-align: top; + margin-left:12px; + cursor: pointer; +} + +#paintbutton:hover { + background-color: #aaaaaa; +} + +/* painter */ + +#canvasdiv { + padding: 5px; +} + +#canvasdiv canvas { + border: 1px solid black; +} + +#buttonbar { + padding: 5px; + display: flex; + gap: 5px; +} + +#buttonbar button, +#layersdiv button { + padding: 1px 2px; + border: 1px solid #cccccc; + background-color: #dddddd; + width: 40px; +} +#buttonbar button { + font-size: 1.2em; + font-weight: bold; +} + +#buttonbar .active { + background-color: #ffffff; + cursor: pointer; +} + +#buttonbar button:hover, +#layersdiv button:hover { + background-color: #cccccc; + cursor: pointer; +} + +#layersdiv { + padding: 0px 5px; +} + +#layersdiv>div { + display: flex; + gap: 5px; + margin-bottom: 5px; +} + +#layersdiv input, +#layersdiv select { + padding: 2px; +} + +#font-select { + width: 150px; +} + +#savebar button { + border: solid 1px #666666; +} + @media(max-width: 460px) { .messages li div, ul.messages li div.date, ul.messages li div.message { diff --git a/ESP32_AP-Flasher/data/www/main.js b/ESP32_AP-Flasher/data/www/main.js index 3b7dbd6f..9049b2ab 100644 --- a/ESP32_AP-Flasher/data/www/main.js +++ b/ESP32_AP-Flasher/data/www/main.js @@ -31,6 +31,7 @@ contentModeOptions[12] = []; const imageQueue = []; let isProcessing = false; let servertimediff = 0; +let paintLoaded = false, paintShow = false; let socket; connect(); @@ -343,6 +344,40 @@ $('#apcfgsave').onclick = function () { $('#apconfigbox').style.display = 'none'; } +$('#paintbutton').onclick = function () { + if (paintShow) { + paintShow = false; + $('#cfgsave').parentNode.style.display = 'block'; + contentselected(); + } else { + paintShow = true; + $('#cfgsave').parentNode.style.display = 'none'; + $('#customoptions').innerHTML = "

"; + const mac = $('#cfgmac').dataset.mac + const hwtype = $('#tag' + mac).dataset.hwtype; + var [width, height] = displaySizeLookup[hwtype] || [0, 0]; + if (height > width) [width, height] = [height, width]; + if (paintLoaded) { + startPainter(mac, width, height); + } else { + loadScript('painter.js', function () { + startPainter(mac, width, height); + }); + } + } +} + +function loadScript(url, callback) { + var script = document.createElement('script'); + script.src = url; + script.onload = function () { + if (callback) { + callback(); + } + }; + document.head.appendChild(script); +} + function contentselected() { let contentMode = $('#cfgcontent').value; let extraoptions = contentModeOptions[contentMode]; @@ -352,6 +387,7 @@ function contentselected() { obj = JSON.parse($('#cfgcontent').dataset.json); } if (contentMode) { + $('#paintbutton').style.display = (contentMode == 0 ? 'inline-block' : 'none'); extraoptions.forEach(element => { var label = document.createElement("label"); label.innerHTML = element; @@ -366,6 +402,8 @@ function contentselected() { $('#customoptions').appendChild(p); }); } + paintShow = false; + $('#cfgsave').parentNode.style.display = 'block'; } function showMessage(message,iserr) { diff --git a/ESP32_AP-Flasher/data/www/painter.js b/ESP32_AP-Flasher/data/www/painter.js new file mode 100644 index 00000000..1244dff2 --- /dev/null +++ b/ESP32_AP-Flasher/data/www/painter.js @@ -0,0 +1,360 @@ +function startPainter(mac, width, height) { + let isDrawing = false; + let lastX = 0; + let lastY = 0; + let color = 'black'; + let linewidth = 3; + let cursor = 'auto'; + let isAddingText = false; + let layerDiv, intervalId, showCursor, input, textX, textY, font, sizeSelect, isDragging; + + var fonts = ['Roboto', 'Open Sans', 'Lato', 'Montserrat', 'PT Sans', 'Barlow Condensed', 'Headland One', 'Sofia Sans Extra Condensed', 'Mynerve', 'Lilita One', 'Passion One', 'Big Shoulders Display']; + + loadGoogleFonts(fonts); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.imageSmoothingEnabled = false; + + $("#canvasdiv").appendChild(canvas); + canvas.style.imageRendering = 'pixelated'; + + canvas.addEventListener('mousedown', startDrawing); + canvas.addEventListener('mouseup', stopDrawing); + canvas.addEventListener('mousemove', draw); + + canvas.addEventListener('touchstart', startDrawing, { passive: true }); + canvas.addEventListener('touchend', stopDrawing); + canvas.addEventListener('touchmove', draw, { passive: true }); + + const bgCanvas = document.createElement('canvas'); + bgCanvas.width = canvas.width; + bgCanvas.height = canvas.height; + const bgCtx = bgCanvas.getContext('2d'); + + bgCtx.fillStyle = '#ffffff'; + bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height); + + const txtButton = document.createElement('button'); + txtButton.innerHTML = 'tT'; + txtButton.style.fontStyle = 'italic'; + txtButton.addEventListener('click', addText); + + const blackButton = document.createElement('button'); + blackButton.innerHTML = '﹏🖌'; + blackButton.style.color = 'black'; + blackButton.addEventListener('click', () => { + color = 'black'; + linewidth = 3; + cursor = 'url("data:image/svg+xml;utf8,") 2 2, auto'; + blackButton.classList.add('active'); + redButton.classList.remove('active'); + whiteButton.classList.remove('active'); + }); + blackButton.classList.add('active'); + + const redButton = document.createElement('button'); + redButton.innerHTML = '﹏🖌'; + redButton.style.color = 'red'; + redButton.addEventListener('click', () => { + color = 'red'; + linewidth = 3; + cursor = 'url("data:image/svg+xml;utf8,") 2 2, auto'; + blackButton.classList.remove('active'); + redButton.classList.add('active'); + whiteButton.classList.remove('active'); + }); + + const whiteButton = document.createElement('button'); + whiteButton.innerHTML = '⬤'; + whiteButton.style.color = 'white'; + whiteButton.addEventListener('click', () => { + color = 'white'; + linewidth = 20; + cursor = 'url("data:image/svg+xml;utf8,") 10 10, auto'; + blackButton.classList.remove('active'); + redButton.classList.remove('active'); + whiteButton.classList.add('active'); + }); + + const clearButton = document.createElement('button'); + clearButton.innerHTML = '🖵'; + clearButton.addEventListener('click', () => { + if (isAddingText) handleFinish(false); + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }); + + const uploadButton = document.createElement('button'); + uploadButton.innerHTML = 'Upload'; + uploadButton.addEventListener('click', () => { + if (isAddingText) handleFinish(true); + const dataURL = canvas.toDataURL('image/jpeg'); + const binaryImage = dataURLToBlob(dataURL); + const formData = new FormData(); + formData.append('mac', mac); + formData.append('dither', '0'); + formData.append('file', binaryImage, 'image.jpg'); + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/imgupload'); + xhr.send(formData); + $('#configbox').style.display = 'none'; + }); + + $("#buttonbar").appendChild(blackButton); + $("#buttonbar").appendChild(redButton); + $("#buttonbar").appendChild(whiteButton); + $("#buttonbar").appendChild(txtButton); + $("#buttonbar").appendChild(clearButton); + $("#savebar").appendChild(uploadButton); + + canvas.addEventListener('mouseenter', function () { + if (!isAddingText) { + canvas.style.cursor = cursor; + } else { + canvas.style.cursor = 'move'; + } + }); + + canvas.addEventListener('mouseleave', function () { + canvas.style.cursor = 'auto'; + }); + + function startDrawing(e) { + if (isAddingText) return; + isDrawing = true; + var rect = canvas.getBoundingClientRect(); + lastX = e.pageX - rect.left - window.pageXOffset; + lastY = e.pageY - rect.top - window.pageYOffset; + } + + function stopDrawing() { + if (isAddingText) return; + isDrawing = false; + } + + function draw(e) { + if (isAddingText) return; + if (!isDrawing) return; + var rect = canvas.getBoundingClientRect(); + ctx.beginPath(); + ctx.moveTo(lastX, lastY); + ctx.lineTo(e.pageX - rect.left - window.pageXOffset, e.pageY - rect.top - window.pageYOffset); + ctx.strokeStyle = color; + ctx.lineWidth = linewidth; + ctx.lineCap = "round"; + ctx.stroke(); + lastX = e.pageX - rect.left - window.pageXOffset; + lastY = e.pageY - rect.top - window.pageYOffset; + } + + function addText() { + if (isAddingText) { + handleFinish(true); + return; + } + txtButton.classList.add('active'); + bgCtx.drawImage(canvas, 0, 0); + + const defaultX = 5; + const defaultY = 40; + isDragging = false; + let startX, startY; + showCursor = true; + + textX = defaultX; + textY = defaultY; + font = '24px ' + fonts[0]; + + input = document.createElement('textarea'); + input.type = 'text'; + input.placeholder = 'Type text here'; + input.style.opacity = '0'; + input.style.position = 'absolute'; + input.style.left = '-200px' + + input.addEventListener('input', () => { + drawText(input.value, textX, textY); + }); + input.addEventListener('keyup', () => { + input.selectionStart = input.selectionEnd = input.value.length; + }); + + input.addEventListener('blur', function () { + input.focus(); + }); + + intervalId = setInterval(function () { + showCursor = !showCursor; + drawText(input.value, textX, textY); + }, 300); + + canvas.addEventListener('mouseup', handleMouseUp); + canvas.addEventListener('mousedown', handleMouseDown); + canvas.addEventListener('mousemove', handleMouseMove); + + canvas.addEventListener('touchstart', handleTouchStart, { passive: true }); + 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]; + + const fontSelect = document.createElement('select'); + fontSelect.id = 'font-select'; + for (var i = 0; i < fonts.length; i++) { + const option = document.createElement('option'); + option.value = fonts[i]; + option.text = fonts[i]; + option.style.fontFamily = fonts[i]; + fontSelect.appendChild(option); + } + + sizeSelect = document.createElement('select'); + sizeSelect.id = 'size-select'; + for (var i = 0; i < sizes.length; i++) { + const option = document.createElement('option'); + option.value = sizes[i]; + option.text = sizes[i] + ' px'; + sizeSelect.appendChild(option); + } + + function updateFont() { + var selectedFont = fontSelect.value; + var selectedSize = sizeSelect.value; + fontSelect.style.fontFamily = selectedFont; + font = selectedSize + 'px ' + selectedFont; + drawText(input.value, textX, textY); + } + + fontSelect.value = fonts[0]; + sizeSelect.value = '24'; + fontSelect.addEventListener('change', updateFont); + sizeSelect.addEventListener('change', updateFont); + + const finishButton = document.createElement('button'); + finishButton.innerHTML = '✔'; + finishButton.addEventListener('click', clickHandleFinish); + + layerDiv = document.createElement('div'); + + layerDiv.appendChild(input); + layerDiv.appendChild(fontSelect); + layerDiv.appendChild(sizeSelect); + layerDiv.appendChild(finishButton); + $("#layersdiv").appendChild(layerDiv); + input.focus(); + + isAddingText = true; + //cursor = 'move'; + blackButton.innerHTML = 'aA' + redButton.innerHTML = 'aA' + if (color=='white') { + whiteButton.classList.remove('active'); + blackButton.classList.add('active'); + color='black'; + } + } + + function handleFinish(apply) { + canvas.removeEventListener('mousedown', handleMouseDown); + canvas.removeEventListener('mouseup', handleMouseUp); + canvas.removeEventListener('mousemove', handleMouseMove); + + canvas.removeEventListener('touchstart', handleTouchStart); + canvas.removeEventListener('touchend', handleTouchEnd); + canvas.removeEventListener('touchmove', handleTouchMove); + isAddingText = false; + cursor = 'auto'; + layerDiv.remove(); + clearInterval(intervalId); + showCursor = false; + if (apply) drawText(input.value, textX, textY); + txtButton.classList.remove('active'); + blackButton.innerHTML = '﹏🖌' + redButton.innerHTML = '﹏🖌' + } + + function drawText(text, x, y) { + ctx.drawImage(bgCanvas, 0, 0); + ctx.save(); + ctx.translate(x, y); + ctx.font = font; + ctx.fillStyle = color; + const lines = text.split('\n'); + lines.forEach((line, index) => { + ctx.fillText(line + (showCursor && index === lines.length - 1 ? '|' : ''), 0, index * (sizeSelect.value * 1.1)); + }); + ctx.restore(); + } + + function handleMouseDown(e) { + isDragging = true; + startX = textX; + startY = textY; + ({ clientX: lastMouseX, clientY: lastMouseY } = e); + } + + function handleMouseMove(e) { + if (isDragging) { + const { clientX, clientY } = e; + textX = startX + clientX - lastMouseX; + textY = startY + clientY - lastMouseY; + drawText(input.value, textX, textY); + } + } + + function handleTouchStart(e) { + isDragging = true; + startX = textX; + startY = textY; + ({ clientX: lastTouchX, clientY: lastTouchY } = e.touches[0]); + } + + function handleTouchMove(e) { + if (isDragging) { + const { clientX, clientY } = e.touches[0]; + textX = startX + clientX - lastTouchX; + textY = startY + clientY - lastTouchY; + drawText(input.value, textX, textY); + } + } + + function handleMouseUp(e) { + isDragging = false; + } + + function handleTouchEnd(e) { + isDragging = false; + } + + function clickHandleFinish() { + handleFinish(true); + } + +} + +function loadGoogleFonts(fonts) { + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://fonts.googleapis.com/css?family=' + fonts.join('|'); + document.head.appendChild(link); +} + +function dataURLToBlob(dataURL) { + const byteString = atob(dataURL.split(',')[1]); + const mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0]; + const arrayBuffer = new ArrayBuffer(byteString.length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let i = 0; i < byteString.length; i++) { + uint8Array[i] = byteString.charCodeAt(i); + } + return new Blob([arrayBuffer], { type: mimeString }); +} + +paintLoaded = true; diff --git a/ESP32_AP-Flasher/data/www/upload-test.html b/ESP32_AP-Flasher/data/www/upload-test.html index dc460b2f..a21e671a 100644 --- a/ESP32_AP-Flasher/data/www/upload-test.html +++ b/ESP32_AP-Flasher/data/www/upload-test.html @@ -10,6 +10,8 @@


+
+


diff --git a/ESP32_AP-Flasher/include/language.h b/ESP32_AP-Flasher/include/language.h index c09587e6..a26b1dba 100644 --- a/ESP32_AP-Flasher/include/language.h +++ b/ESP32_AP-Flasher/include/language.h @@ -4,12 +4,12 @@ static int defaultLanguage = 0; -static String languageList[] = {"EN - English", "NL - Dutch", "DE - Deutsch"}; +static String languageList[] = {"EN - English", "NL - Nederlands", "DE - Deutsch"}; /*EN English language section*/ static String languageEnDaysShort[] = {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; -static String languageEnDays[] = {"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"}; -static String languageEnMonth[] = {"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"}; +static String languageEnDays[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; +static String languageEnMonth[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; /*END English language section END*/ /*NL Dutch language section*/ @@ -20,8 +20,8 @@ static String languageNlMonth[] = {"januari", "februari", "maart", "april", "mei /*DE German language section*/ static String languageDeDaysShort[] = {"SO", "MO", "DI", "MI", "DO", "FR", "SA"}; -static String languageDeDays[] = {"sonntag", "montag", "dienstag", "mittwoch", "donnerstag", "freitag", "samstag"}; -static String languageDeMonth[] = {"januar", "februar", "maerz", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "dezember"}; +static String languageDeDays[] = {"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}; +static String languageDeMonth[] = {"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"}; /*END German language section END*/ static String* languageDaysShort[] = {languageEnDaysShort, languageNlDaysShort, languageDeDaysShort}; diff --git a/ESP32_AP-Flasher/platformio.ini b/ESP32_AP-Flasher/platformio.ini index 25808b37..e7d172f8 100644 --- a/ESP32_AP-Flasher/platformio.ini +++ b/ESP32_AP-Flasher/platformio.ini @@ -134,7 +134,7 @@ board_upload.flash_size = 16MB [env:Simple_AP] board = esp32dev -board_build.partitions = no_ota.csv +board_build.partitions = default.csv build_flags = -DCORE_DEBUG_LEVEL=0 diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index a41e6b1a..db5c83b6 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -107,11 +107,12 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { case Image: if (cfgobj["filename"].as() && cfgobj["filename"].as() != "null" && !cfgobj["#fetched"].as()) { - if (cfgobj["dither"] && cfgobj["dither"].as() == false) imageParams.dither = false; + if (cfgobj["dither"] && cfgobj["dither"] == "0") imageParams.dither = false; jpg2buffer(cfgobj["filename"].as(), filename, imageParams); if (imageParams.hasRed) imageParams.dataType = DATATYPE_IMG_RAW_2BPP; if (prepareDataAvail(&filename, imageParams.dataType, mac, cfgobj["timetolive"].as())) { cfgobj["#fetched"] = true; + if (cfgobj["delete"].as()) LittleFS.remove("/"+cfgobj["filename"].as()); } else { wsErr("Error accessing " + filename); } @@ -814,6 +815,8 @@ bool getCalFeed(String &filename, String URL, String title, tagRecord *&taginfo, time(&now); struct tm timeinfo; localtime_r(&now, &timeinfo); + static char dateString[40]; + strftime(dateString, sizeof(dateString), " - %d.%m.%Y", &timeinfo); HTTPClient http; http.begin(URL); @@ -848,6 +851,7 @@ bool getCalFeed(String &filename, String URL, String title, tagRecord *&taginfo, u8f.setBackgroundColor(PAL_WHITE); u8f.setCursor(5, 16); u8f.print(title); + u8f.print(dateString); int n = doc.size(); if (n > 7) n = 7; @@ -883,6 +887,7 @@ bool getCalFeed(String &filename, String URL, String title, tagRecord *&taginfo, u8f.setBackgroundColor(PAL_WHITE); u8f.setCursor(5, 16); u8f.print(title); + u8f.print(dateString); int n = doc.size(); if (n > 8) n = 8; diff --git a/ESP32_AP-Flasher/src/main.cpp b/ESP32_AP-Flasher/src/main.cpp index 495ee4b8..d0138fff 100644 --- a/ESP32_AP-Flasher/src/main.cpp +++ b/ESP32_AP-Flasher/src/main.cpp @@ -133,4 +133,4 @@ void loop() { while (1) { vTaskDelay(10000 / portTICK_PERIOD_MS); } -} \ No newline at end of file +} diff --git a/ESP32_AP-Flasher/src/makeimage.cpp b/ESP32_AP-Flasher/src/makeimage.cpp index 743729b0..5578e84a 100644 --- a/ESP32_AP-Flasher/src/makeimage.cpp +++ b/ESP32_AP-Flasher/src/makeimage.cpp @@ -62,10 +62,31 @@ struct Error { float b; }; +// Gamma brightness lookup table +// gamma = 1.50 steps = 256 range = 0-255 +const uint8_t gamma_lut[256] PROGMEM = { + 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, + 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, + 11, 12, 12, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20, + 21, 21, 22, 23, 23, 24, 25, 26, 26, 27, 28, 28, 29, 30, 31, 31, + 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 44, + 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 65, 65, 66, 67, 68, 69, 70, 71, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 112, 113, 114, 115, 116, 117, 119, 120, 121, 122, 123, 124, 126, + 127, 128, 129, 130, 132, 133, 134, 135, 136, 138, 139, 140, 141, 142, 144, 145, + 146, 147, 149, 150, 151, 152, 154, 155, 156, 158, 159, 160, 161, 163, 164, 165, + 167, 168, 169, 171, 172, 173, 174, 176, 177, 178, 180, 181, 182, 184, 185, 187, + 188, 189, 191, 192, 193, 195, 196, 197, 199, 200, 202, 203, 204, 206, 207, 209, + 210, 211, 213, 214, 216, 217, 218, 220, 221, 223, 224, 226, 227, 228, 230, 231, + 233, 234, 236, 237, 239, 240, 242, 243, 245, 246, 248, 249, 251, 252, 254, 255, +}; + uint32_t colorDistance(const Color &c1, const Color &c2, const Error &e1) { - float r_diff = c1.r + e1.r - c2.r; - float g_diff = c1.g + e1.g - c2.g; - float b_diff = c1.b + e1.b - c2.b; + float r_diff = gamma_lut[c1.r] + e1.r - gamma_lut[c2.r]; + float g_diff = gamma_lut[c1.g] + e1.g - gamma_lut[c2.g]; + float b_diff = gamma_lut[c1.b] + e1.b - gamma_lut[c2.b]; return round(r_diff * r_diff + g_diff * g_diff + b_diff * b_diff); } diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index baf4f40a..d18aa0f4 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -87,7 +87,8 @@ void prepareIdleReq(uint8_t* dst, uint16_t nextCheckin) { bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t nextCheckin) { if (nextCheckin > MIN_RESPONSE_TIME) nextCheckin = MIN_RESPONSE_TIME; - + if (wsClientCount()) nextCheckin=0; + uint8_t src[8]; *((uint64_t*)src) = swap64(*((uint64_t*)dst)); uint8_t mac[6]; @@ -131,6 +132,9 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t if (memcmp(md5bytes, taginfo->md5pending, 16) == 0) { wsLog("new image is the same as current or already pending image. not updating tag."); wsSendTaginfo(mac); + if (LittleFS.exists(*filename)) { + LittleFS.remove(*filename); + } return true; } diff --git a/ESP32_AP-Flasher/src/web.cpp b/ESP32_AP-Flasher/src/web.cpp index 427092b5..45320980 100644 --- a/ESP32_AP-Flasher/src/web.cpp +++ b/ESP32_AP-Flasher/src/web.cpp @@ -365,12 +365,16 @@ void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index request->_tempFile.close(); if (request->hasParam("mac", true)) { String dst = request->getParam("mac", true)->value(); + bool dither = true; + if (request->hasParam("dither", true)) { + if (request->getParam("dither", true)->value() == "0") dither = false; + } uint8_t mac[6]; if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) == 6) { tagRecord *taginfo = nullptr; taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { - taginfo->modeConfigJson = "{\"filename\":\"" + dst + ".jpg\",\"timetolive\":\"0\"}"; + taginfo->modeConfigJson = "{\"filename\":\"" + dst + ".jpg\",\"timetolive\":\"0\",\"dither\":\"" + String(dither) + "\",\"delete\":\"1\"}"; taginfo->contentMode = 0; taginfo->nextupdate = 0; wsSendTaginfo(mac); diff --git a/Hardware/OpenEPaperLink Mini AP/Building.md b/Hardware/OpenEPaperLink Mini AP/Building.md new file mode 100644 index 00000000..5f968b21 --- /dev/null +++ b/Hardware/OpenEPaperLink Mini AP/Building.md @@ -0,0 +1,72 @@ +# OpenEPaperLink Mini AP # + + + +This is a very minimalistic accesspoint; in the most basic version it is exactly a tiny ESP32, a 'segmented-display' tag, and a little flex PCB. It can be used as an accesspoint for OpenEPaperLink. The small size makes it also very useful as an extra accesspoint, using OpenEPaperLink's Multi-AP feature + +## Parts ## + + +* Wemos S2 Mini (or clone) +* Solum Segmented pricetag +* OpenEPaperLink Mini AP Flex PCB + +While it can't hurt to add the following optional parts, they're *not* needed +* 3D-printed case +* Optional: 1206-sized SMD capacitor +* Optional: WS2812B LED + +While the Wemos S3 mini *should* be compatible with an updated pindefinitions, this has not been tested (yet) + +## Assembly ## + + + +1. A good place to start is to add the optional parts first; the optional extra capacitor and WS2812B LED +The flex PCB (depending on the board house) can be very thin indeed, don't let your soldering iron dwell on the pads any longer than necessary. + + + + + +2. Add a little bit of solder on each of the debug pads on the tag. This makes it a little easier to solder the flex PCB later on + + + + + +3. Place the Wemos S2 mini on the flex PCB, on a somewhat heat-resistant surface. Make sure to align the hole properly, and apply some pressure on the S2 module to make sure there is minimal distance between the flex PCB and the module. Fill a few holes with solder, taking care to not heat it any longer than required for the solder to properly flow. Not all pads need to be soldered; the top 3 rows of pins can be left unsoldered; this makes it easier to put the module into the case later on. + + + + + +4. Turn the PCB/Module around, and make sure all the pads are soldered on the other side. Again, as can be seen in the picture, the top 3 rows can be left unsoldered + + + + +5. Align the tag-pads with the debug header on the Segmented tag, and solder it on. + +6. You're basically done! This combination isn't really sturdy though, its best mounted in a small 3D-printed enclosure. + + + + + +7. Start by inserting the Wemos module into the lower part of the case. It should snap in place; depending on the tolerances of your printer, you might need to make small adjustments with a sharp knife. + + + +8. Optionally, you can add some small screws, but the fit should be tight enough for the Wemos module not to move around (too) much. + + + +9. Flip the tag over, and insert it into the top half of the case. Again, the fit should be pretty tight. After inserting the tag, add some cyanoacrylate glue to the edges to fix it in place. + +10. After you've given the glue enough time to cure, glue the two halves of the case together. Use some clamps to make sure all the edges are nice and clean + +## Programming ## + +* Use PlatformIO to program the ESP32, using the 'Mini AP' build option. +* Use the 'Upload filesystem image' option to prepare the ESP32's filesystem. Crucially, the tag_db_md5.json and AP_FW_Pack.bin need to be on the ESP32 to allow it to update the tag to a new version diff --git a/Hardware/OpenEPaperLink Mini AP/Getting Started.md b/Hardware/OpenEPaperLink Mini AP/Getting Started.md new file mode 100644 index 00000000..d9027c97 --- /dev/null +++ b/Hardware/OpenEPaperLink Mini AP/Getting Started.md @@ -0,0 +1,75 @@ +# Getting started with the OpenEPaperLink and the Mini-AP # + +So you've made or bought yourself an AP and a few tags! Cool! Here's how to get started with OpenEPaperLink. We'll explain the hardware features of the Mini-AP, if you built one yourself, you'll probably be familiar enough with that hardware. This guide expects a fully flashed AP, both the ESP32 and the AP-tag inside. Also, this only works with tags running OpenEPaperLink firmware. If this is not the case, this guide will be a less-than-awesome place to start setting up OpenEPaperLink + +## Hardware ## + + +That is all! + +Now I can hear you think: 'Geewhizz mister, you're lazy!', and you're not wrong... Okay fine I'll explain a little further. + +This thing: +- is USB-C powered (5V, about 120mA or 0.6 watt) +- Powered by an ESP32-S2 (with PSRAM) +- Uses an upcycled Solum pricetag as display and radio +- Is actually multi-CPU! Neato! +- Has a WS2812B RGB Led (for the haters: don't worry, you can disable it easily) +- Has two hardware switches; one to enter usb-download mode, (GPIO 0) and one to reset the ESP32. You can reach them with a paperclip +- Uses an E-Paper display for telling you some Accesspoint status information. Who doesn't like E-Paper! + +The MiniAP uses an ESP32 microcontroller and pricetag to work as an accesspoint for OpenEPaperLink displays that support 802.15.4 (Zigbee-esque) packets. + +## Software ## + +This thing runs software from this repo: +- ESP32_AP-Flasher firmware on the ESP32 +- zbs243_AP_FW on the segmented tag-AP + +The AP can be accessed with a nice web-interface that lets you select data for the tags to display. The ESP32 connects to your network through WiFi. + +## AP Setup ## +Alright, let's get this show on the road. We'll start with the AP first, and then connect some tags to it in the next part. +* Connect your AP to a power source. This can be a computer, of course, but you can also use any 5V wall-wart you have laying around. The power consumption isn't incredibly high. +* The tag will start, and the RGB led should blink blue-green-blue. This indicates it is waiting for a wifi connection +* You'll need to set up the WiFi connection for the AP. The ESP32 inside uses the awesome WiFiManager project to set up an wifi-accesspoint that you can use to easily configure wifi +* Use your computer, phone, tablet, whatever, and connect to the 'OpenEPaperLink Setup' WiFi Network. + + + +* Usually, a browser will pop-up, and presents you a 'captive portal'. If it doesn't, simpy remain connected to the OpenEPaperLink wifi network and point your browser to http://192.168.4.1 + + + +* Click or tap on 'Configure Wifi' - this will start a scan for WiFi networks +* You should get a list of WiFi networks the AP can receive. Click on your WiFi network, enter your password, and hit 'save'. + + + +* After the AP has been successfully connected to the network, the LED on the back should be green, and will fade in and out. +* The 802.15.4/ZigBee-ish radio will now start. You can see some stuff appear on the ePaper screen +* After the 'Boot'-message, the screen will show you the AP's IP address + + + +* Due to the limited characters, the screen will show the IP address in two parts. Remember this IP address, you're going to need it... +* ... here. Connect your device back to the same network as you've just connected the AP to, and point your browser to the IP address. Forgot it already? It happens. Simply restart your AP, and it'll show the IP address again. +* You should see an awesome web interface! If you want to kill the RGB Led, now is the first convenient moment to do it. Hit up 'AP config' and check out the config there. All kinds of cool features! +* If you're aware of a zigbee network at home, you can select a different channel in the AP config. As most channels have some kind of overlap with WiFi, we've only made some channels available with little or no overlap. Now would also be a good time to select a channel if you change it, because if you change it with tags already associated, it may take them up to an hour-ish to reconnect to the new channel. + +## Adding Tags ## +* Take a (programmed) tag, and insert the batteries. This may sound easier than it is, in reality. See, these tags are cheap. They don't reset automatically when the voltage drops; if you replace batteries, you'll sometimes need to reset them (by shortening out the contacts), and you want to insert the batteries in a swift motion, so that they make contact and stay connected. You can use the battery cover to pop the batteries in, as shown in the video below. + +https://github.com/jjwbruijn/OpenEPaperLink/assets/2544995/e4c693f8-b018-4b83-94df-399dc285618a + + +* If you want to make sure you'll reset the tag properly, the easiest way to drain the internal capacitors is to shorten the them using a battery. A battery inserted in reverse will shorten the contacts. On tags with multiple batteries: don't keep batteries in the bay with one battery shorting out the contacts; they're wired in parallel. +* Now that you've successfully powered on your tag, it's time to see if it's showing up on the AP-webinterface. A few seconds after the 'Waiting for data...' screen is shown on a tag, it should show up on the accesspoint. +* Select the tag in the webinterface, choose some content, and the tag should update the next time it checks in! + +## Signal strength ## +The AP has a range of up to 25 meters line of sight, but since there is some overlap with WiFi channels, the coexistence of WiFi can reduce the system's range. Also, don't expect these signals to penetrate concrete (rebar) floors or walls very well. + +## Further reading ## +* [Troubleshooting tags](https://github.com/jjwbruijn/OpenEPaperLink/blob/master/Tags-specs/troubleshooting.md) +* [How to build a Mini AP](https://github.com/jjwbruijn/OpenEPaperLink/tree/master/Hardware/OpenEPaperLink%20Mini%20AP/README.md) diff --git a/Hardware/OpenEPaperLink Mini AP/README.md b/Hardware/OpenEPaperLink Mini AP/README.md index 5f968b21..26d140ad 100644 --- a/Hardware/OpenEPaperLink Mini AP/README.md +++ b/Hardware/OpenEPaperLink Mini AP/README.md @@ -1,72 +1,15 @@ -# OpenEPaperLink Mini AP # +# OpenEPaperLink Mini-AP # +![image](https://github.com/jjwbruijn/OpenEPaperLink/assets/2544995/d5e4c583-11e7-4d83-ba36-a225ad200cd5) - +A small OpenEPaperLink AP, using an ESP32-S2 and a segmented tag -This is a very minimalistic accesspoint; in the most basic version it is exactly a tiny ESP32, a 'segmented-display' tag, and a little flex PCB. It can be used as an accesspoint for OpenEPaperLink. The small size makes it also very useful as an extra accesspoint, using OpenEPaperLink's Multi-AP feature +* [How to build one](https://github.com/jjwbruijn/OpenEPaperLink/blob/master/Hardware/OpenEPaperLink%20Mini%20AP/Building.md) -## Parts ## - +* [How to use one](https://github.com/jjwbruijn/OpenEPaperLink/blob/master/Hardware/OpenEPaperLink%20Mini%20AP/Getting%20Started.md) -* Wemos S2 Mini (or clone) -* Solum Segmented pricetag -* OpenEPaperLink Mini AP Flex PCB - -While it can't hurt to add the following optional parts, they're *not* needed -* 3D-printed case -* Optional: 1206-sized SMD capacitor -* Optional: WS2812B LED - -While the Wemos S3 mini *should* be compatible with an updated pindefinitions, this has not been tested (yet) - -## Assembly ## - - - -1. A good place to start is to add the optional parts first; the optional extra capacitor and WS2812B LED -The flex PCB (depending on the board house) can be very thin indeed, don't let your soldering iron dwell on the pads any longer than necessary. - - - - - -2. Add a little bit of solder on each of the debug pads on the tag. This makes it a little easier to solder the flex PCB later on - - - - - -3. Place the Wemos S2 mini on the flex PCB, on a somewhat heat-resistant surface. Make sure to align the hole properly, and apply some pressure on the S2 module to make sure there is minimal distance between the flex PCB and the module. Fill a few holes with solder, taking care to not heat it any longer than required for the solder to properly flow. Not all pads need to be soldered; the top 3 rows of pins can be left unsoldered; this makes it easier to put the module into the case later on. - - - - - -4. Turn the PCB/Module around, and make sure all the pads are soldered on the other side. Again, as can be seen in the picture, the top 3 rows can be left unsoldered - - - - -5. Align the tag-pads with the debug header on the Segmented tag, and solder it on. - -6. You're basically done! This combination isn't really sturdy though, its best mounted in a small 3D-printed enclosure. - - - - - -7. Start by inserting the Wemos module into the lower part of the case. It should snap in place; depending on the tolerances of your printer, you might need to make small adjustments with a sharp knife. - - - -8. Optionally, you can add some small screws, but the fit should be tight enough for the Wemos module not to move around (too) much. - - - -9. Flip the tag over, and insert it into the top half of the case. Again, the fit should be pretty tight. After inserting the tag, add some cyanoacrylate glue to the edges to fix it in place. - -10. After you've given the glue enough time to cure, glue the two halves of the case together. Use some clamps to make sure all the edges are nice and clean - -## Programming ## - -* Use PlatformIO to program the ESP32, using the 'Mini AP' build option. -* Use the 'Upload filesystem image' option to prepare the ESP32's filesystem. Crucially, the tag_db_md5.json and AP_FW_Pack.bin need to be on the ESP32 to allow it to update the tag to a new version + + +* [Case](https://github.com/jjwbruijn/OpenEPaperLink/tree/master/Hardware/OpenEPaperLink%20Mini%20AP/Case) + +* [PCB](https://github.com/jjwbruijn/OpenEPaperLink/tree/master/Hardware/OpenEPaperLink%20Mini%20AP/PCB) + diff --git a/README.md b/README.md index c1275935..438a70fa 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ On the 2.9" tags, both the UC8151 and SSD1619 display variants are supported - High transfer speeds - It can do about 5kbyte/s in favorable RF conditions. This allows for lower power - RF-friendly - We don't need to acknowledge EVERY packet, and we don't need to transfer data we already have -The entire setup requires a few tags, and an ESP32. A (preferably, but not necessarily) broken tag is used as an 802.15.4 radio for the ESP32. You'll need a ZBS_Flasher in order to flash both the AP with its firmware, and the tags. Using the 'mac' option on ZBS_Flasher makes sure a tag flashed with a custom firmware has a valid mac address; it used the stock mac address assigned to the tag if it hasn't been flashed before. If you want to set it yourself, you can edit the mac address in the infopage. The AP expects a tag with a mac that starts with 00:00, followed by 6 bytes. The MAC-address also needs to be set on the AP-tag. +The entire setup requires a few tags, and an ESP32. A tag is used as an 802.15.4 radio for the ESP32. You'll need a ZBS_Flasher in order to flash the tags. Using the 'mac' option on ZBS_Flasher makes sure a tag flashed with a custom firmware has a valid mac address; it used the stock mac address assigned to the tag if it hasn't been flashed before. If you want to set it yourself, you can edit the mac address in the infopage. The AP expects a tag with a mac that starts with 00:00, followed by 6 bytes. -Once flashed, you can hook the AP tag up to the ESP32 by connecting the tags serial lines to some free pins. Make sure you set the pins in settings.h, so that the ESP32 can communicate with it. This can be validated by checking the ESP32 debug output; you should see 'sync burst' displayed every 30 seconds +You can hook the AP tag up to the ESP32 with mod wires or a flex pcb. The esp will flash the AP firmware to the Tag automatically. In some cases, a power off/on cycle is required. Please check the serial console output for status information. -You can access the ESP32 with any web browser after connecting it to your WiFi Network. The file browser is located at /edit. For sending data to tags, you'll need to upload the information in 'data' to the ESP32's filesystem. After uploading, you can access the status screen at /index.html. If everything is working, you should be able to see tags synchronising to the network. After uploading a suitable .bmp file to the filesystem, this file can be sent to the tag by entering it's 6-byte mac address and filename. +You can access the ESP32 with any web browser after connecting it to your WiFi Network. The file browser is located at /edit. For sending data to tags, you'll need to upload the information in 'data' to the ESP32's filesystem or over HTTP. After uploading, you can access the status screen at /index.html. If everything is working, you should be able to see tags synchronising to the network. After uploading a suitable .jpg file to the filesystem, this file can be sent to the tag by entering it's 6-byte mac address and filename. ## The protocol explained - The tag checks in with the AP every 40+ seconds. Actual check-in interval is highly dependent on RF conditions @@ -51,12 +51,11 @@ You can access the ESP32 with any web browser after connecting it to your WiFi N ### AP: - Important! The AP needs to be able to tell a tag to try again later if it's already doing comms with another tag. The AP can't handle concurrent checkins/download due to memory constraints! - More reliable serial comms (sometimes bytes are dropped) -- Include source mac with blockrequest struct ### ESP32: - Do more with status info as sent by the tags ## Known issues: -- Some tags work better as AP's than others. Your range may suck. The boards on these tags are tiny and fragile. For instance, a dab of hot-glue on a board is enough to warp it pretty severely, and will damage the components that are soldered on there. Reportedly, segmented-display solum tags work well. +- Some tags work better as AP's than others. Your range may suck. The boards on these tags are tiny and fragile. For instance, a dab of hot-glue on a board is enough to warp it pretty severely, and will damage the components that are soldered on there. Reportedly, segmented-display Solum tags work well. ## Hints and excuses: - I'm sorry if reading this spaghetti code makes you lose your mind. 'Of all the things I've lost, I miss my mind the most' I know it is pretty unreadable. I could blame SDCC for a lot of things, but it's mostly me. @@ -71,3 +70,9 @@ You can access the ESP32 with any web browser after connecting it to your WiFi N - atc1441 Hats off to these legends! + +## Automated Builds +- After a PR gets merged to the main branch, the ESP32 code will automatically be compiled. This can take up to 20 minutes. +- Information about the latest builds can be found below +builds + diff --git a/zbs243_AP_FW/packagebinaries.py b/zbs243_AP_FW/packagebinaries.py new file mode 100644 index 00000000..ba1fd18f --- /dev/null +++ b/zbs243_AP_FW/packagebinaries.py @@ -0,0 +1,57 @@ +import os +import json + +types = { + 0x00: "AP_FW_1.54.bin", + 0x01: "AP_FW_2.9.bin", + 0xF0: "AP_FW_Segmented_UK.bin", + 0xFF: "AP_FW_Nodisplay.bin" +} + +binpath = "../binaries/" +tocmaxsize = 512 + +toc = [] +output = bytearray(tocmaxsize) + +# Read version from main.c +with open("main.c") as file: + lines = file.readlines() + version_line = next(line for line in lines if "version" in line and "uint16_t" in line) + _, version = version_line.split("= 0x") + version = int(version.strip().split(";")[0], 16) + +binaries = [file for file in os.listdir(binpath) if file.startswith("AP_FW") and not file.endswith("Pack")] +for file in binaries: + file = file.strip() + type_id = -1 + for typeid, typefile in types.items(): + if typefile == file: + type_id = typeid + break + if type_id == -1: + raise ValueError("We don't recognize filetype <{}>, sorry...".format(file)) + with open(os.path.join(binpath, file), "rb") as binary_file: + binary = binary_file.read() + length = len(binary) + offset = len(output) + subarr = { + 'type': type_id, + 'version': version, + 'name': file, + 'offset': offset, + 'length': length + } + toc.append(subarr) + output.extend(binary) + +jtoc = json.dumps(toc) +tocsize = len(jtoc) +if tocsize > tocmaxsize: + raise ValueError("TOC is too big! Adjust size and try again.") +output[:len(jtoc)] = jtoc.encode() +with open(os.path.join(binpath, "AP_FW_Pack.bin"), "wb") as output_file: + output_file.write(bytes(output)) + +print(toc) +print("All done.")