From 09168baa5383763fc36e7ddbfbf55dcb55785339 Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 26 Jun 2022 01:54:00 +0800 Subject: [PATCH] feat: add image uploader --- Firmware/ATC_Paper.bin | Bin 92584 -> 92584 bytes Firmware/src/epd_ble_service.c | 8 +- readme.md | 4 +- tools/data/images/car-sign.png | Bin 0 -> 4130 bytes tools/requriments.txt | 1 + tools/scripts/image2hex.py | 6 +- tools/utils.py | 7 +- web_tools/index.html | 354 ++++++++++++++++-- web_tools/js/dithering.js | 148 ++++++++ web_tools/js/utils.js | 17 + ...ricetag Flasher.html => uart_flasher.html} | 0 11 files changed, 494 insertions(+), 51 deletions(-) create mode 100644 tools/data/images/car-sign.png create mode 100644 web_tools/js/dithering.js create mode 100644 web_tools/js/utils.js rename web_tools/{Hanshow TLSR8359 E-Paper Pricetag Flasher.html => uart_flasher.html} (100%) diff --git a/Firmware/ATC_Paper.bin b/Firmware/ATC_Paper.bin index ec70fcc1a8891ca279ad8aca7fa736236a2d6b7b..a92763d8c0d784c2866e978e75f6b59dc1c19ee5 100644 GIT binary patch delta 177 zcmV;i08am?(*>y01+WmC0Wgyhn=5}XZJY2S3ey;$21rN%@{33q1LE$5=#-;d@tp!H z$sV9-1_Gi3kTjdoy~{Hp!Sz@p9~0;p-J%pqW70Mo5P0)3&7|DvHq9!n`yJVn`w6u fc$)wKoSOguW}5&2MwLT*M?0| delta 177 zcmV;i08am?(*>y01+WmC0dSKMn=5~CZJY2S3ey;$1|khASXS;f>6D{e@ty)I$sV9- z1_Gi3kT#p@hXSnV$x28P@{33s0~iY$P~nFGtl!B=NEQf`qEhkxmj;|Dp!S!4pVfyD z3mI4t2=|u@ozI8Jp!Sz_pA86m-d@3P-(Nfn`w6u fbejMGn416qVw(T}Mw= epd_buffer_size + 1) + if ((payload[1] << 8 | payload[2]) + payload_len - 3 >= epd_buffer_size + 1) { out_buffer[0] = 0x00; out_buffer[1] = 0x00; bls_att_pushNotifyData(EPD_BLE_CMD_OUT_DP_H, out_buffer, 2); return 0; } - memcpy(epd_buffer + byte_pos, payload + 1, payload_len - 1); - - byte_pos += payload_len - 1; + memcpy(epd_buffer + (payload[1] << 8 | payload[2]), payload + 3, payload_len - 3); out_buffer[0] = payload_len >> 8; out_buffer[1] = payload_len & 0xff; diff --git a/readme.md b/readme.md index 0afbe3e..a39a829 100644 --- a/readme.md +++ b/readme.md @@ -63,9 +63,11 @@ Firmware CRC32: 0xe62d501e - [x] 添加蓝牙上传图片后notify - [x] 添加场景且支持切换 - [x] 图片模式 -- [ ] web 支持图片切换 +- [x] web 支持图片切换 - [x] 添加新的时间场景 - [x] 支持设置年月日 +- [x] web 支持画图编辑,直接上传图片,抖动算法 +- [ ] 三色抖动算法、设备端三色显示支持,蓝牙传输支持 ### 计划新增 - [ ] 安卓端控制器 diff --git a/tools/data/images/car-sign.png b/tools/data/images/car-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..0a0b2c7ba0b9961df0889ff085548e7114edd64a GIT binary patch literal 4130 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 电子价签蓝牙控制器 + + + @@ -51,6 +70,8 @@ async function sendCommand(cmd) { if (epdCharacteristic) { await epdCharacteristic.writeValueWithResponse(cmd) + } else { + addLog('服务不可用。蓝牙链接上了吗?') } } @@ -63,10 +84,12 @@ async function rxTxSendCommand(cmd) { if (rxtxCharacteristic) { await rxtxCharacteristic.writeValueWithResponse(cmd); + } else { + addLog('服务不可用。蓝牙链接上了吗?') } } - async function upload_tiff() { + async function upload_tiff_image() { const startTime = new Date().getTime(); const buffer = await document.getElementById('tiff_file').files[0].arrayBuffer(); const arr = bytesToHex(buffer); @@ -80,7 +103,7 @@ const step = 480; let partIndex = 0; for (let i = 0; i < arr.length; i += step) { - addLog(`正在上传第${partIndex+1}块. 块大小: ${step + 2}byte`); + addLog(`正在上传第${partIndex+1}块. 块大小: ${step + 1}byte`); await sendCommand(hexToBytes("03" + arr.slice(i, i + step))); partIndex += 1; } @@ -89,6 +112,31 @@ addLog(`上传tiff完成,耗时${(new Date().getTime() - startTime)/1000}s`); } + async function upload_image() { + const canvas = document.getElementById('canvas'); + const value = bytesToHex(canvas2bytes(canvas)); + + const startTime = new Date().getTime(); + + addLog(`开始刷新屏幕内存, 大小 ${value.length/2/1024}KB`); + + await sendCommand(hexToBytes("0000")); + + await sendCommand(hexToBytes("020000")); + + const step = 480; + let partIndex = 0; + for (let i = 0; i < value.length; i += step) { + addLog(`正在发送第${partIndex+1}块. 块大小: ${step/2 + 3}byte. 起始位置: ${i/2}`); + await sendCommand(hexToBytes("03" + intToHex(i / 2, 2) + value.substring(i, i + step))); + partIndex += 1; + } + + await sendCommand(hexToBytes("01")) + + addLog(`刷新完成,耗时${(new Date().getTime() - startTime)/1000}s`); + } + async function upload_epd_buffer() { const startTime = new Date().getTime(); const value = document.getElementById('buffer').value.replace(/(?:\r\n|\r|\n|,|0x| )/g, ''); @@ -102,16 +150,13 @@ const step = 480; let partIndex = 0; for (let i = 0; i < value.length; i += step) { - addLog(`正在发送第${partIndex+1}块. 块大小: ${step + 2}byte`); - await sendCommand(hexToBytes("03" + value.substring(i, i + step))); - await delay(50); + addLog(`正在发送第${partIndex+1}块. 块大小: ${step/2 + 3}byte. 起始位置: ${i/2}`); + await sendCommand(hexToBytes("03" + intToHex(i / 2, 2) + value.substring(i, i + step))); partIndex += 1; } - await sendCommand(hexToBytes("03" + value.substring(value.length-step, value.length))); - await delay(50); + await delay(150); await sendCommand(hexToBytes("01")) - await delay(50); addLog(`刷新完成,耗时${(new Date().getTime() - startTime)/1000}s`); } @@ -176,10 +221,6 @@ setTimeout(async function () { await connect(); }, 300); } - function handleNotify(data) { - addLog("接受到字节: " + bytesToHex(data.buffer)); - } - async function connect() { if (epdCharacteristic == null) { addLog("正在链接: " + bleDevice.name); @@ -197,7 +238,7 @@ epdCharacteristic.addEventListener('characteristicvaluechanged', (event) => { console.log('epd ret', bytesToHex(event.target.value.buffer)) - const count = parseInt('0x'+ bytesToHex(event.target.value.buffer)) * 2; + const count = parseInt('0x'+ bytesToHex(event.target.value.buffer)); addLog(`> [来自屏幕]: 收到${count} byte数据`); }); @@ -220,23 +261,6 @@ dom.scrollTop = dom.scrollHeight; } - function hexToBytes(hex) { - for (var bytes = [], c = 0; c < hex.length; c += 2) - bytes.push(parseInt(hex.substr(c, 2), 16)); - return new Uint8Array(bytes); - } - - function bytesToHex(data) { - return new Uint8Array(data).reduce( - function (memo, i) { - return memo + ("0" + i.toString(16)).slice(-2); - }, ""); - } - - function intToHex(intIn, bytes=4) { - return intIn.toString(16).padStart(bytes * 2, '0'); - } - function getUnixTime() { const hourOffset = document.getElementById('hour-offset').value; const unixNow = Math.round(Date.now() / 1000)+(60*60*hourOffset) - new Date().getTimezoneOffset() * 60; @@ -244,24 +268,223 @@ const date = new Date((unixNow + new Date().getTimezoneOffset() * 60)*1000); const localeTimeString = date.toLocaleTimeString(); - return {unixNow, localeTimeString, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), week: date.getDay()} + return {unixNow, localeTimeString, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), week: date.getDay() || 7} + } + + async function update_image () { + const image_file = document.getElementById('image_file'); + if (image_file.files.length > 0) { + const file = image_file.files[0]; + + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext("2d"); + + const image = new Image(); + image.src = URL.createObjectURL(file); + image.onload = function(event) { + URL.revokeObjectURL(this.src); + ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); + convert_dithering() + } + } + } + + function get_position(canvas, x, y){ + let rect = canvas.getBoundingClientRect() + return { + x: x - rect.left * (canvas.width/rect.width), + y: y - rect.top * (canvas.height/rect.height) + } + } + + function clear_canvas() { + if(confirm('确认清除屏幕?')) { + const canvas = document.getElementById('canvas'); + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + } + + function convert_dithering() { + const canvas = document.getElementById('canvas'); + const ctx = canvas.getContext("2d"); + dithering(ctx, canvas.width, canvas.height, parseInt(document.getElementById('threshold').value), document.getElementById('dithering').value); } document.body.onload = () => { setInterval(() => { const { localeTimeString, year, month, day, week } = getUnixTime(); document.getElementById('time-setter').innerText = `设置时间为:${year}-${month}-${day} ${localeTimeString} 星期${week}`; - }, 1) - } + }, 1000); + const canvas = document.getElementById('canvas'); + + let is_allow_drawing = false; + let is_allow_move_editor = false; + const image_mode = document.getElementById('canvas-mode'); + const paint_size = document.getElementById('paint-size'); + const paint_color = document.getElementById('paint-color'); + const editor = document.getElementById('edit-font'); + const font = document.getElementById('font'); + document.getElementById('dithering').value = 'Atkinson'; + image_mode.value = 'paint'; + paint_color.value = 'black'; + font.value = '黑体'; + + editor.onmousemove = function (e) { + editor.style.fontSize = `${paint_size.value * 10}px`; + editor.style.color = paint_color.value; + editor.style.fontFamily = font.value; + + if (is_allow_move_editor) { + const {x, y} = get_position(canvas, e.clientX, e.clientY); + if (x < 0 || y < 0 || x > canvas.width || y > canvas.height) { + return; + } + + editor.style.left = `${e.clientX-50}px`; + editor.style.top = `${e.clientY-50}px`; + + } + } + + editor.onmousedown = function (e) { + is_allow_move_editor = true; + } + + editor.onmouseup = function (e) { + is_allow_move_editor = false; + } + + document.getElementById('update-text').onclick = function () { + if (!editor.value.length) { + alert('请先输入文字'); + return; + } + editor.style.display = 'none'; + const ctx = canvas.getContext("2d"); + ctx.beginPath(); + ctx.font = `${paint_size.value * 10}px ${font.value}`; + ctx.fillStyle = paint_color.value; + const {x, y} = get_position(canvas, parseInt(editor.style.left), parseInt(editor.style.top) + paint_size.value * 10); + + ctx.fillText(editor.value, x, y); + } + + image_mode.onchange = function (e) { + if (image_mode.value === 'font') { + document.getElementById('update-text').style.display = 'inline-block'; + document.getElementById('font').style.display = 'inline-block'; + + editor.style.display='block'; + editor.style.left = `${e.clientX}px`; + editor.style.top = `${e.clientY}px`; + return; + } + document.getElementById('update-text').style.display = 'none'; + document.getElementById('font').style.display = 'none'; + editor.style.display='none'; + } + + paint_size.onchange = function () { + if (image_mode.value === 'font') { + editor.style.fontSize = `${paint_size.value * 10}px`; + } + } + + paint_color.onchange = function () { + if (image_mode.value === 'font') { + editor.style.color = paint_color.value; + } + } + + font.onchange = function () { + if (image_mode.value === 'font') { + editor.style.fontFamily = font.value; + } + } + + canvas.onmousedown = function(e) { + let ele = get_position(canvas, e.clientX, e.clientY) + let { x, y } = ele + const ctx = canvas.getContext("2d"); + + switch (image_mode.value) { + case 'paint': + is_allow_drawing = true; + ctx.beginPath(); + ctx.moveTo(x, y); + break; + case 'font': + if (editor.style.display === 'none') { + editor.style.display='block'; + editor.style.left = `${e.clientX}px`; + editor.style.top = `${e.clientY}px`; + editor.style.fontSize = `${paint_size.value * 10}px`; + editor.style.color = paint_color.value; + editor.style.fontFamily = font.value; + } + + break + default: + break; + } + }; + + canvas.onmousemove = (e) => { + const ctx = canvas.getContext("2d"); + let ele = get_position(canvas, e.clientX, e.clientY) + let { x, y } = ele; + switch (image_mode.value) { + case 'paint': + if (is_allow_drawing) { + ctx.lineWidth = paint_size.value; + ctx.strokeStyle=paint_color.value; + ctx.lineTo(x, y); + ctx.stroke(); + } + break; + case 'font': + break; + + default: + break; + } + } + + canvas.onmouseup = function() { + switch (image_mode.value) { + case 'paint': + is_allow_drawing = false; + break; + + case 'font': + editor.focus(); + is_allow_move_editor = false; + break; + + default: + break; + } + } + + canvas.onmouseleave = function () { + if (image_mode.value === 'paint') { + is_allow_drawing = false; + } + } + }

电子价签蓝牙控制器

+ 串口升级串口升级 +


+

指令控制


@@ -273,20 +496,69 @@

- +

上传图片到屏幕

+ + 抖动算法: + + 阈值: + +
-
+ +
+
+ 模式: + + 画笔/文字大小: + + 画笔颜色: + + + + + +
+
+ + +
+ +
+

- 上传tiff到屏幕 -
- -
-
-
+

设置时间


偏移+小时 +
+
+

屏幕控制

+ + +
+
+
+
日志: diff --git a/web_tools/js/dithering.js b/web_tools/js/dithering.js new file mode 100644 index 0000000..34f453b --- /dev/null +++ b/web_tools/js/dithering.js @@ -0,0 +1,148 @@ +const bwrPalette = [ + [0, 0, 0, 0], + [255, 255, 255, 0], + [255, 0, 0, 0] +] + +const bwPalette = [ + [0, 0, 0, 0], + [255, 255, 255, 0], +] + +function get_near_color(color, palette) { + let minDistanceSquared = 255*255 + 255*255 + 255*255 + 1; + + let bestIndex = 0; + for (let i = 0; i < palette.length; i++) { + let rdiff = (color[0] & 0xff) - (palette[i][0] & 0xff); + let gdiff = (color[1] & 0xff) - (palette[i][1] & 0xff); + let bdiff = (color[2] & 0xff) - (palette[i][2] & 0xff); + let distanceSquared = rdiff*rdiff + gdiff*gdiff + bdiff*bdiff; + if (distanceSquared < minDistanceSquared) { + minDistanceSquared = distanceSquared; + bestIndex = i; + } + } + return bestIndex; + +} + +function updatePixel(imageData, index, color) { + imageData[index] = color[0]; + imageData[index+1] = color[1]; + imageData[index+2] = color[2]; + imageData[index+3] = color[3]; +} + +function dithering(ctx, width, height, threshold, type) { + const bayerThresholdMap = [ + [ 15, 135, 45, 165 ], + [ 195, 75, 225, 105 ], + [ 60, 180, 30, 150 ], + [ 240, 120, 210, 90 ] + ]; + + const lumR = []; + const lumG = []; + const lumB = []; + for (let i=0; i<256; i++) { + lumR[i] = i*0.299; + lumG[i] = i*0.587; + lumB[i] = i*0.114; + } + const imageData = ctx.getImageData(0, 0, width, height); + + const imageDataLength = imageData.data.length; + + // Greyscale luminance (sets r pixels to luminance of rgb) + for (let i = 0; i <= imageDataLength; i += 4) { + imageData.data[i] = Math.floor(lumR[imageData.data[i]] + lumG[imageData.data[i+1]] + lumB[imageData.data[i+2]]); + } + + const w = imageData.width; + let newPixel, err; + + for (let currentPixel = 0; currentPixel <= imageDataLength; currentPixel+=4) { + + if (type ==="none") { + // No dithering + imageData.data[currentPixel] = imageData.data[currentPixel] < threshold ? 0 : 255; + } else if (type ==="bayer") { + // 4x4 Bayer ordered dithering algorithm + var x = currentPixel/4 % w; + var y = Math.floor(currentPixel/4 / w); + var map = Math.floor( (imageData.data[currentPixel] + bayerThresholdMap[x%4][y%4]) / 2 ); + imageData.data[currentPixel] = (map < threshold) ? 0 : 255; + } else if (type ==="floydsteinberg") { + // Floyda€"Steinberg dithering algorithm + newPixel = imageData.data[currentPixel] < 129 ? 0 : 255; + err = Math.floor((imageData.data[currentPixel] - newPixel) / 16); + imageData.data[currentPixel] = newPixel; + + imageData.data[currentPixel + 4 ] += err*7; + imageData.data[currentPixel + 4*w - 4 ] += err*3; + imageData.data[currentPixel + 4*w ] += err*5; + imageData.data[currentPixel + 4*w + 4 ] += err*1; + } else { + // Bill Atkinson's dithering algorithm + newPixel = imageData.data[currentPixel] < threshold ? 0 : 255; + err = Math.floor((imageData.data[currentPixel] - newPixel) / 8); + imageData.data[currentPixel] = newPixel; + + imageData.data[currentPixel + 4 ] += err; + imageData.data[currentPixel + 8 ] += err; + imageData.data[currentPixel + 4*w - 4 ] += err; + imageData.data[currentPixel + 4*w ] += err; + imageData.data[currentPixel + 4*w + 4 ] += err; + imageData.data[currentPixel + 8*w ] += err; + } + + // Set g and b pixels equal to r + imageData.data[currentPixel + 1] = imageData.data[currentPixel + 2] = imageData.data[currentPixel]; + } + + ctx.putImageData(imageData, 0, 0); +} + +function canvas2bytes(canvas, rotate=1) { + const ctx = canvas.getContext("2d"); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + const arr = []; + let buffer = []; + + for (let x = canvas.width - 1; x >= 0; x--) { + for (let y = 0; y < canvas.height; y++) { + const index = (canvas.width * 4 * y) + x * 4; + buffer.push(imageData.data[index] > 0 ? 1 : 0); + if (buffer.length === 8) { + arr.push(parseInt(buffer.join(''), 2)); + buffer = []; + } + } + } + return arr; +} + +function scaleImageData(canvas, scale) { + const ctx = canvas.getContext("2d"); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const scaled = ctx.createImageData(imageData.width * scale, imageData.height * scale); + const subLine = ctx.createImageData(scale, 1).data + for (let row = 0; row < imageData.height; row++) { + for (let col = 0; col < imageData.width; col++) { + let sourcePixel = imageData.data.subarray( + (row * imageData.width + col) * 4, + (row * imageData.width + col) * 4 + 4 + ); + for (let x = 0; x < scale; x++) subLine.set(sourcePixel, x*4) + for (let y = 0; y < scale; y++) { + let destRow = row * scale + y; + let destCol = col * scale; + scaled.data.set(subLine, (destRow * scaled.width + destCol) * 4) + } + } + } + + return scaled; +} \ No newline at end of file diff --git a/web_tools/js/utils.js b/web_tools/js/utils.js new file mode 100644 index 0000000..7c827c8 --- /dev/null +++ b/web_tools/js/utils.js @@ -0,0 +1,17 @@ + +function hexToBytes(hex) { + for (var bytes = [], c = 0; c < hex.length; c += 2) + bytes.push(parseInt(hex.substr(c, 2), 16)); + return new Uint8Array(bytes); +} + +function bytesToHex(data) { + return new Uint8Array(data).reduce( + function (memo, i) { + return memo + ("0" + i.toString(16)).slice(-2); + }, ""); +} + +function intToHex(intIn, bytes=4) { + return intIn.toString(16).padStart(bytes * 2, '0'); +} diff --git a/web_tools/Hanshow TLSR8359 E-Paper Pricetag Flasher.html b/web_tools/uart_flasher.html similarity index 100% rename from web_tools/Hanshow TLSR8359 E-Paper Pricetag Flasher.html rename to web_tools/uart_flasher.html