mirror of
https://github.com/tsl0922/EPD-nRF5.git
synced 2025-12-06 15:42:48 +08:00
update EPD client
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 472 KiB |
344
docs/index.html
344
docs/index.html
File diff suppressed because one or more lines are too long
249
docs/js/dithering.js
Normal file
249
docs/js/dithering.js
Normal file
@@ -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);
|
||||
}
|
||||
252
docs/js/main.js
Normal file
252
docs/js/main.js
Normal file
@@ -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 + '<br>';
|
||||
console.log(time + logTXT);
|
||||
while ((document.getElementById("log").innerHTML.match(/<br>/g) || []).length > 10) {
|
||||
var logs_br_position = document.getElementById("log").innerHTML.search("<br>");
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user