diff --git a/docs/images/0.jpg b/docs/images/0.jpg index 1f13239..f949de8 100644 Binary files a/docs/images/0.jpg and b/docs/images/0.jpg differ diff --git a/docs/index.html b/docs/index.html index 24846e3..2e37cf5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,269 +1,111 @@ - 电子墨水屏蓝牙控制器 - + 4.2 寸电子墨水屏蓝牙控制器 -

4.2 寸电子墨水屏蓝牙控制器(nRF51)

+
+

4.2 寸电子墨水屏蓝牙控制器(nRF51)

+
+ 连接 +
+ 驱动: + + 引脚: + + +
+ + +
-
- 一些小提示 - -

- 致谢:屏幕驱动代码来自微雪 E-Paper Shield,本网页代码最初基于 atc1441/ATC_TLSR_Paper 项目的网页控制端代码修改而来。 -

-
- -
- 蓝牙连接 - - -
+
+ 传输 +
+
+ + + +
+
+ + + +
+
+
+
+ + 抖动算法: + + 阈值: + +
+ + +
+
-
- 驱动配置 - 屏幕驱动: - - 引脚配置: - - -
- -
- 调试助手 - - - -
- -
- 蓝牙传图 -
- - -
- -
提示:此文本框需填写取模后的图片数据(十六进制)
-
- -
- 日志 - -
-
+
+ 日志 + +
+
+
+ 提示 + +

+ 致谢:屏幕驱动代码来自微雪 E-Paper Shield,本网页代码最初基于 atc1441/ATC_TLSR_Paper 项目的网页控制端代码修改而来。 +

+
+
+ + \ No newline at end of file diff --git a/docs/js/dithering.js b/docs/js/dithering.js new file mode 100644 index 0000000..06b5b12 --- /dev/null +++ b/docs/js/dithering.js @@ -0,0 +1,249 @@ +const bwrPalette = [ + [0, 0, 0, 255], + [255, 255, 255, 255], + [255, 0, 0, 255] +] + +const bwPalette = [ + [0, 0, 0, 255], + [255, 255, 255, 255], +] + +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, type='bw') { + const ctx = canvas.getContext("2d"); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + const arr = []; + let buffer = []; + + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + const index = (canvas.width * y + x) * 4; + if (type !== 'bwr') { + buffer.push(imageData.data[index] > 0 && imageData.data[index+1] > 0 && imageData.data[index+2] > 0 ? 1 : 0); + } else { + buffer.push(imageData.data[index] > 0 && imageData.data[index+1] === 0 && imageData.data[index+2] === 0 ? 1 : 0); + } + + if (buffer.length === 8) { + arr.push(parseInt(buffer.join(''), 2)); + buffer = []; + } + } + } + return arr; +} + +function bytes2canvas(bytes, canvas) { + const ctx = canvas.getContext("2d"); + const imageData = ctx.createImageData(canvas.width, canvas.height); + + let buffer = []; + for (let byte of bytes) { + const binaryString = byte.toString(2).padStart(8, '0'); + buffer.push(...binaryString.split('').map(bit => parseInt(bit, 10))); + } + let len = buffer.length; + + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + const index = (canvas.width * y + x) * 4; + const bit = buffer.shift(); + const value = bit ? 255 : 0; + imageData.data[index] = value; // R + imageData.data[index + 1] = value; // G + imageData.data[index + 2] = value; // B + imageData.data[index + 3] = 255; // A + } + } + + if (buffer.length * 2 == len) { + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + const index = (canvas.width * y + x) * 4; + const bit = buffer.shift(); + if (bit) { + imageData.data[index] = 255; // R + imageData.data[index + 1] = 0; // G + imageData.data[index + 2] = 0; // B + imageData.data[index + 3] = 255; // A + } + } + } + } + + ctx.putImageData(imageData, 0, 0); +} + +function getColorDistance(rgba1, rgba2) { + const [r1, b1, g1] = rgba1; + const [r2, b2, g2] = rgba2; + + const rm = (r1 + r2 ) / 2; + + const r = r1 - r2; + const g = g1 - g2; + const b = b1 - b2; + + return Math.sqrt((2 + rm / 256) * r * r + 4 * g * g + (2 + (255 - rm) / 256) * b * b); +} + +function getNearColor(pixel, palette) { + let minDistance = 255 * 255 * 3 + 1; + let paletteIndex = 0; + + for (let i = 0; i < palette.length; i++) { + const targetColor = palette[i]; + const distance = getColorDistance(pixel, targetColor); + if (distance < minDistance) { + minDistance = distance; + paletteIndex = i; + } + } + + return palette[paletteIndex]; +} + + +function getNearColorV2(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 palette[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 getColorErr(color1, color2, rate) { + const res = []; + for (let i = 0; i < 3; i++) { + res.push(Math.floor((color1[i] - color2[i]) / rate)); + } + return res; +} + +function updatePixelErr(imageData, index, err, rate) { + imageData[index] += err[0] * rate; + imageData[index+1] += err[1] * rate; + imageData[index+2] += err[2] * rate; +} + +function ditheringCanvasByPalette(canvas, palette, type) { + palette = palette || bwrPalette; + + const ctx = canvas.getContext('2d'); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const w = imageData.width; + + for (let currentPixel = 0; currentPixel <= imageData.data.length; currentPixel+=4) { + const newColor = getNearColorV2(imageData.data.slice(currentPixel, currentPixel+4), palette); + + if (type === "bwr_floydsteinberg") { + const err = getColorErr(imageData.data.slice(currentPixel, currentPixel+4), newColor, 16); + + updatePixel(imageData.data, currentPixel, newColor); + updatePixelErr(imageData.data, currentPixel +4, err, 7); + updatePixelErr(imageData.data, currentPixel + 4*w - 4, err, 3); + updatePixelErr(imageData.data, currentPixel + 4*w, err, 5); + updatePixelErr(imageData.data, currentPixel + 4*w + 4, err, 1); + } else { + const err = getColorErr(imageData.data.slice(currentPixel, currentPixel+4), newColor, 8); + + updatePixel(imageData.data, currentPixel, newColor); + updatePixelErr(imageData.data, currentPixel +4, err, 1); + updatePixelErr(imageData.data, currentPixel +8, err, 1); + updatePixelErr(imageData.data, currentPixel +4 * w - 4, err, 1); + updatePixelErr(imageData.data, currentPixel +4 * w, err, 1); + updatePixelErr(imageData.data, currentPixel +4 * w + 4, err, 1); + updatePixelErr(imageData.data, currentPixel +8 * w, err, 1); + } + } + ctx.putImageData(imageData, 0, 0); +} \ No newline at end of file diff --git a/docs/js/main.js b/docs/js/main.js new file mode 100644 index 0000000..c31f020 --- /dev/null +++ b/docs/js/main.js @@ -0,0 +1,252 @@ +let bleDevice; +let gattServer; +let Theservice; +let writeCharacteristic; +let reconnectTrys = 0; + +let imgArray = ""; +let imgArrayLen = 0; +let chunkSize = 38; +let uploadPart = 0; +let totalPart = 0; + +function resetVariables() { + gattServer = null; + Theservice = null; + writeCharacteristic = null; + document.getElementById("log").value = ''; + imgArray = ""; + imgArrayLen = 0; + uploadPart = 0; +} + +function handleError(error) { + console.log(error); + resetVariables(); + if (bleDevice == null) + return; + if (reconnectTrys <= 5) { + reconnectTrys++; + connect(); + } + else { + addLog("Was not able to connect, aborting"); + reconnectTrys = 0; + } +} + +async function sendCommand(cmd) { + if (writeCharacteristic) { + await writeCharacteristic.writeValue(cmd); + } +} + +async function sendcmd(cmdTXT) { + let cmd = hexToBytes(cmdTXT); + addLog('Send CMD: ' + cmdTXT); + await sendCommand(cmd); +} + +function setDriver() { + let driver = document.getElementById("epddriver").value; + let pins = document.getElementById("epdpins").value; + sendcmd("00" + pins).then(() => { + sendcmd("01" + driver); + }); +} + +function clearscreen() { + if(confirm('确认清除屏幕内容?')) { + sendcmd("01").then(() => { + sendcmd("02").then(() => { + sendcmd("06"); + }) + }).catch(handleError); + } +} + +function sendimg(cmdIMG) { + startTime = new Date().getTime(); + imgArray = cmdIMG.replace(/(?:\r\n|\r|\n|,|0x| )/g, ''); + imgArrayLen = imgArray.length; + uploadPart = 0; + totalPart = Math.round(imgArrayLen / chunkSize); + console.log('Sending image ' + imgArrayLen); + sendcmd("01").then(() => { + sendCommand(hexToBytes("0313")).then(() => { + sendIMGpart(); + }); + }).catch(handleError); +} + +function sendIMGpart() { + if (imgArray.length > 0) { + let currentPart = imgArray.substring(0, chunkSize); + let currentTime = (new Date().getTime() - startTime) / 1000.0; + imgArray = imgArray.substring(chunkSize); + setStatus('正在发送块: ' + (uploadPart++) + "/" + totalPart + ", 用时: " + currentTime + "s"); + addLog('Sending Part: ' + currentPart); + sendCommand(hexToBytes("04" + currentPart)).then(() => { + sendIMGpart(); + }) + } else { + sendCommand(hexToBytes("05")).then(() => { + let sendTime = (new Date().getTime() - startTime) / 1000.0; + addLog("Done! Time used: " + sendTime + "s"); + setStatus("发送完成!耗时: " + sendTime + "s"); + sendcmd("06"); + }) + } +} + +function updateButtonStatus() { + let connected = gattServer != null && gattServer.connected; + let status = connected ? null : 'disabled'; + document.getElementById("sendcmdbutton").disabled = status; + document.getElementById("clearscreenbutton").disabled = status; + document.getElementById("sendimgbutton").disabled = status; + document.getElementById("setDriverbutton").disabled = status; +} + +function disconnect() { + resetVariables(); + addLog('Disconnected.'); + document.getElementById("connectbutton").innerHTML = '连接'; + updateButtonStatus(); +} + +function preConnect() { + if (gattServer != null && gattServer.connected) { + if (bleDevice != null && bleDevice.gatt.connected) + bleDevice.gatt.disconnect(); + } + else { + connectTrys = 0; + navigator.bluetooth.requestDevice({ optionalServices: ['62750001-d828-918d-fb46-b6c11c675aec'], acceptAllDevices: true }).then(device => { + device.addEventListener('gattserverdisconnected', disconnect); + bleDevice = device; + connect(); + }).catch(handleError); + } +} + +function reConnect() { + connectTrys = 0; + if (bleDevice != null && bleDevice.gatt.connected) + bleDevice.gatt.disconnect(); + resetVariables(); + addLog("Reconnect"); + setTimeout(function () { connect(); }, 300); +} + +function connect() { + if (writeCharacteristic == null) { + addLog("Connecting to: " + bleDevice.name); + bleDevice.gatt.connect().then(server => { + addLog('> Found GATT Server'); + gattServer = server; + return gattServer.getPrimaryService('62750001-d828-918d-fb46-b6c11c675aec'); + }).then(service => { + addLog('> Found Service'); + Theservice = service; + return Theservice.getCharacteristic('62750002-d828-918d-fb46-b6c11c675aec'); + }).then(characteristic => { + addLog('> Found Characteristic'); + document.getElementById("connectbutton").innerHTML = '断开'; + updateButtonStatus(); + writeCharacteristic = characteristic; + return; + }).catch(handleError); + } +} + +function setStatus(statusText) { + document.getElementById("status").innerHTML = statusText; +} + +function addLog(logTXT) { + var today = new Date(); + var time = ("0" + today.getHours()).slice(-2) + ":" + ("0" + today.getMinutes()).slice(-2) + ":" + ("0" + today.getSeconds()).slice(-2) + " : "; + document.getElementById("log").innerHTML += time + logTXT + '
'; + console.log(time + logTXT); + while ((document.getElementById("log").innerHTML.match(/
/g) || []).length > 10) { + var logs_br_position = document.getElementById("log").innerHTML.search("
"); + document.getElementById("log").innerHTML = document.getElementById("log").innerHTML.substring(logs_br_position + 4); + } +} + +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) { + var stringOut = ""; + stringOut = ("0000" + intIn.toString(16)).substr(-4) + return stringOut.substring(2, 4) + stringOut.substring(0, 2); +} + +function updateImageData(canvas) { + document.getElementById('cmdIMAGE').value = bytesToHex(canvas2bytes(canvas, 'bw')); + if (document.getElementById("epddriver").value == '03' && + document.getElementById('dithering').value.startsWith('bwr')) { + document.getElementById('cmdIMAGE').value += bytesToHex(canvas2bytes(canvas, 'bwr')); + } +} + +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 clear_canvas() { + if(confirm('确认清除画布内容?')) { + const canvas = document.getElementById('canvas'); + const ctx = canvas.getContext("2d"); + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + document.getElementById('cmdIMAGE').value = ''; + } +} + +function convert_dithering() { + const canvas = document.getElementById('canvas'); + const ctx = canvas.getContext("2d"); + const mode = document.getElementById('dithering').value; + if (mode.startsWith('bwr')) { + ditheringCanvasByPalette(canvas, bwrPalette, mode); + } else { + dithering(ctx, canvas.width, canvas.height, parseInt(document.getElementById('threshold').value), mode); + } + updateImageData(canvas); +} + +document.body.onload = () => { + updateButtonStatus(); + + const canvas = document.getElementById('canvas'); + bytes2canvas(hexToBytes(document.getElementById('cmdIMAGE').value), canvas); + + document.getElementById('dithering').value = 'none'; +} \ No newline at end of file