diff --git a/Firmware/ATC_Paper.bin b/Firmware/ATC_Paper.bin
index ec70fcc..a92763d 100644
Binary files a/Firmware/ATC_Paper.bin and b/Firmware/ATC_Paper.bin differ
diff --git a/Firmware/src/epd_ble_service.c b/Firmware/src/epd_ble_service.c
index 27eabb0..fbd19ad 100644
--- a/Firmware/src/epd_ble_service.c
+++ b/Firmware/src/epd_ble_service.c
@@ -15,7 +15,7 @@ extern uint8_t *epd_temp;
return 0; \
}
-extern unsigned char epd_buffer[epd_buffer_size];
+extern uint8_t epd_buffer[epd_buffer_size];
unsigned int byte_pos = 0;
int epd_ble_handle_write(void *p)
@@ -47,16 +47,14 @@ int epd_ble_handle_write(void *p)
return 0;
// Write data to image buffer.
case 0x03:
- if (byte_pos + payload_len - 1 >= 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 0000000..0a0b2c7
Binary files /dev/null and b/tools/data/images/car-sign.png differ
diff --git a/tools/requriments.txt b/tools/requriments.txt
index e69de29..4fcc503 100644
--- a/tools/requriments.txt
+++ b/tools/requriments.txt
@@ -0,0 +1 @@
+Pillow==9.1.0
\ No newline at end of file
diff --git a/tools/scripts/image2hex.py b/tools/scripts/image2hex.py
index 7f49765..219b9ff 100644
--- a/tools/scripts/image2hex.py
+++ b/tools/scripts/image2hex.py
@@ -1,8 +1,10 @@
-from PIL import Image
+from PIL.Image import Dither # noqa
from tools.utils import image2hex, load_test_image
if __name__ == '__main__':
# print(image2hex(load_test_image('test-01.bmp')))
- print(image2hex(load_test_image('test-02.bmp')))
+
+ print(image2hex(load_test_image('mao.bmp'), dither=Dither.FLOYDSTEINBERG))
+ print(image2hex(load_test_image('car-sign.png'), dither=Dither.NONE))
diff --git a/tools/utils.py b/tools/utils.py
index eefa655..8065e6c 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -1,6 +1,7 @@
import os
from PIL import Image
+from PIL.Image import Dither # noqa
def hex2bytes(hex_string):
@@ -29,11 +30,13 @@ def hex2image(hex_string, width, height):
image.show('test')
-def image2hex(image, width=296, height=128):
+def image2hex(image, width=296, height=128, dither=Dither.NONE):
if isinstance(image, str):
image = Image.open(image)
- return bytes2hex(image.resize((width, height)).rotate(90, expand=True).resize((height, width)).convert('1').tobytes())
+ image = image.resize((width, height)).rotate(90, expand=True)
+ # image.show()
+ return bytes2hex(image.resize((height, width)).convert('1', dither=dither).tobytes())
def load_test_image(name):
diff --git a/web_tools/index.html b/web_tools/index.html
index f8afb16..97dfa86 100644
--- a/web_tools/index.html
+++ b/web_tools/index.html
@@ -4,6 +4,25 @@
电子价签蓝牙控制器
+
+
+
@@ -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