蓝牙墨水屏上位机学习(3)-ELink墨水屏电子纸社区-FPGA CPLD-ChipDebug

蓝牙墨水屏上位机学习(3)

imgData=ctx.createImageData(100,100);
 
imgData.data[4]=0;
imgData.data[5]=255;
imgData.data[6]=0;
imgData.data[7]=255;

 

main.js中sendimg()函数学习,对应发送图片按钮

图片[1]-蓝牙墨水屏上位机学习(3)-ELink墨水屏电子纸社区-FPGA CPLD-ChipDebug

函数代码如下:

async function sendimg() {
  const canvasSize = document.getElementById('canvasSize').value;
  const ditherMode = document.getElementById('ditherMode').value;
  const epdDriverSelect = document.getElementById('epddriver');
  const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex];
 
  if (selectedOption.getAttribute('data-size') !== canvasSize) {
    addLog(`画布尺寸和驱动不匹配,请重新选择。`);
    return;
  }
  if (selectedOption.getAttribute('data-color') !== ditherMode) {
    addLog(`颜色模式和驱动不匹配,请重新选择。`);
    return;
  }
 
  startTime = new Date().getTime();
  const status = document.getElementById("status");
  status.parentElement.style.display = "block";
 
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const processedData = processImageData(imageData);
 
  updateButtonStatus(true);
 
  if (ditherMode === 'fourColor') {
    await writeImage(processedData, 'color');
  } else if (ditherMode === 'threeColor') {
    const halfLength = Math.floor(processedData.length / 2);
    await writeImage(processedData.slice(0, halfLength), 'bw');
    await writeImage(processedData.slice(halfLength), 'red');
  } else if (ditherMode === 'blackWhiteColor') {
    await writeImage(processedData, 'bw');
  } else {
    addLog("当前固件不支持此颜色模式。");
    updateButtonStatus();
    return;
  }
 
  await write(EpdCmd.REFRESH);
  updateButtonStatus();
 
  const sendTime = (new Date().getTime() - startTime) / 1000.0;
  addLog(`发送完成!耗时: ${sendTime}s`);
  setStatus(`发送完成!耗时: ${sendTime}s`);
  addLog("屏幕刷新完成前请不要操作。");
  setTimeout(() => {
    status.parentElement.style.display = "none";
  }, 5000);
}

 

电子纸图像发送函数 sendimg() 详解

这个 sendimg() 函数实现了通过蓝牙向电子纸显示设备发送图像数据的核心功能。它处理图像预处理、数据分块传输和屏幕刷新命令,是电子纸显示系统的关键部分。下面我将详细解析其功能和实现逻辑。

函数整体功能

sendimg() 函数的主要工作流程:

  1. 验证用户选择的画布尺寸、颜色模式与电子纸驱动是否匹配
  2. 从 Canvas 获取图像数据并进行预处理(如抖动处理)
  3. 根据不同的颜色模式(黑白 / 三色 / 四色)组织和发送数据
  4. 发送屏幕刷新命令触发电子纸更新显示
  5. 记录发送时间并显示操作结果

代码逻辑详解

1. 参数验证与初始化
const canvasSize = document.getElementById('canvasSize').value;
const ditherMode = document.getElementById('ditherMode').value;
const epdDriverSelect = document.getElementById('epddriver');
const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex];
 
// 验证画布尺寸与驱动是否匹配
if (selectedOption.getAttribute('data-size') !== canvasSize) {
  addLog(`画布尺寸和驱动不匹配,请重新选择。`);
  return;
}
// 验证颜色模式与驱动是否匹配
if (selectedOption.getAttribute('data-color') !== ditherMode) {
  addLog(`颜色模式和驱动不匹配,请重新选择。`);
  return;
}
 
// 记录开始时间并显示状态
startTime = new Date().getTime();
const status = document.getElementById("status");
status.parentElement.style.display = "block";

 

补充javascript知识:

HTML DOM Document 对象

HTML DOM 节点
在 HTML DOM (Document Object Model) 中 , 每一个元素都是 节点:

文档是一个文档。
所有的HTML元素都是元素节点。
所有 HTML 属性都是属性节点。
文本插入到 HTML 元素是文本节点。are text nodes。
注释是注释节点。
Document 对象
当浏览器载入 HTML 文档, 它就会成为 document 对象。

document 对象是HTML文档的根节点与所有其他节点(元素节点,文本节点,属性节点, 注释节点)。

Document 对象使我们可以从脚本中对 HTML 页面中的所有元素进行访问。

提示:Document 对象是 Window 对象的一部分,可通过 window.document 属性对其进行访问。

Document 对象属性和方法
HTML文档中可以使用以上属性和方法:

属性 / 方法       描述
document.activeElement    返回当前获取焦点元素
document.addEventListener()    向文档添加句柄
document.adoptNode(node)    从另外一个文档返回 adapded 节点到当前文档。
document.anchors    返回对文档中所有 Anchor 对象的引用。
document.applets    返回对文档中所有 Applet 对象的引用。
document.baseURI    返回文档的绝对基础 URI
document.body    返回文档的body元素
document.close()    关闭用 document.open() 方法打开的输出流,并显示选定的数据。
document.cookie    设置或返回与当前文档有关的所有 cookie。
document.createAttribute()    创建一个属性节点
document.createComment()    createComment() 方法可创建注释节点。
document.createDocumentFragment()    创建空的 DocumentFragment 对象,并返回此对象。
document.createElement()    创建元素节点。
document.createTextNode()    创建文本节点。
document.doctype    返回与文档相关的文档类型声明 (DTD)。
document.documentElement    返回文档的根节点
document.documentMode    返回用于通过浏览器渲染文档的模式
document.documentURI    设置或返回文档的位置
document.domain    返回当前文档的域名。
document.domConfig    返回normalizeDocument()被调用时所使用的配置
document.embeds    返回文档中所有嵌入的内容(embed)集合
document.forms    返回对文档中所有 Form 对象引用。
document. getElementsByClassName()    返回文档中所有指定类名的元素集合,作为 NodeList 对象。
document.getElementById()    返回对拥有指定 id 的第一个对象的引用。
document.getElementsByName()    返回带有指定名称的对象集合。
document.getElementsByTagName()    返回带有指定标签名的对象集合。
document.images    返回对文档中所有 Image 对象引用。
document.implementation    返回处理该文档的 DOMImplementation 对象。
document.importNode()    把一个节点从另一个文档复制到该文档以便应用。
document.inputEncoding    返回用于文档的编码方式(在解析时)。
document.lastModified    返回文档被最后修改的日期和时间。
document.links    返回对文档中所有 Area 和 Link 对象引用。
document.normalize()    删除空文本节点,并连接相邻节点
document.normalizeDocument()    删除空文本节点,并连接相邻节点的
document.open()    打开一个流,以收集来自任何 document.write() 或 document.writeln() 方法的输出。
document.querySelector()    返回文档中匹配指定的CSS选择器的第一元素
document.querySelectorAll()    document.querySelectorAll() 是 HTML5中引入的新方法,返回文档中匹配的CSS选择器的所有元素节点列表
document.readyState    返回文档状态 (载入中……)
document.referrer    返回载入当前文档的文档的 URL。
document.removeEventListener()    移除文档中的事件句柄(由 addEventListener() 方法添加)
document.renameNode()    重命名元素或者属性节点。
document.scripts    返回页面中所有脚本的集合。
document.strictErrorChecking    设置或返回是否强制进行错误检查。
document.title    返回当前文档的标题。
document.URL    返回文档完整的URL
document.write()    向文档写 HTML 表达式 或 JavaScript 代码。
document.writeln()    等同于 write() 方法,不同的是在每个表达式之后写一个换行符。


其中:document.getElementById()   返回对拥有指定 id 的第一个对象的引用。

sendimg()函数的第一行代码:

const canvasSize = document.getElementById(‘canvasSize’).value;

1. document : document 是浏览器提供的一个全局对象,代表当前加载的 HTML 文档。通过这个对象,可以访问和操作文档中的所有元素。
2. getElementById(‘canvasSize’) :这是 document 对象的一个方法,用于通过元素的 id 属性来查找文档中的元素。在这里,它会查找 id 为 canvasSize 的元素。如果找到了该元素,则返回对应的 DOM 元素对象;如果未找到,则返回 null 。
3. .value :这是访问 DOM 元素的 value 属性,通常用于获取表单元素(如 <input> 、 <select> 、 <textarea> 等)的当前值。
4. const canvasSize :使用 const 关键字声明一个常量 canvasSize ,并将 id 为 canvasSize 的元素的 value 属性值赋给它。 const 声明的常量一旦赋值就不能再重新赋值。
### 示例场景
假设 HTML 文档中有如下代码:

“`
<input type=”number” id=”canvasSize” value=”800″>
“`
当执行 const canvasSize = document.getElementById(‘canvasSize’).value; 时, canvasSize 的值将是字符串 “800” 。

在index.html中找到如下 代码:

<select id="canvasSize" onchange="updateCanvasSize()">
						<option value="1.54_152_152">1.54 (152x152)</option>
						<option value="1.54_200_200">1.54 (200x200)</option>
						<option value="2.13_212_104">2.13 (212x104)</option>
						<option value="2.13_250_122">2.13 (250x122)</option>
						<option value="2.66_296_152">2.66 (296x152)</option>
						<option value="2.9_296_128">2.9 (296x128)</option>
						<option value="2.9_384_168">2.9 (384x168)</option>
						<option value="3.5_384_184">3.5 (384x184)</option>
						<option value="3.7_416_240">3.7 (416x240)</option>
						<option value="3.97_800_480">3.97 (800x480)</option>
						<option value="4.2_400_300" selected>4.2 (400x300)</option>
						<option value="5.79_792_272">5.79 (792x272)</option>
						<option value="7.5_800_480">7.5 (800x480)</option>
						<option value="10.2_960_640">10.2 (960x640)</option>
						<option value="10.85_1360_480">10.85 (1360x480)</option>
						<option value="11.6_960_640">11.6 (960x640)</option>
						<option value="4E_600_400">4E (600x400)</option>
						<option value="7.3E6">7.3E6 (480x800)</option>
					</select>

 

html语法中<select>标签用法如下 :

如:

<!DOCTYPE html>
<html>
<head> 
<meta charset="utf-8"> 
<title>菜鸟教程(runoob.com)</title> 
</head>
<body>
 
<select>
  <option value="volvo">Volvo</option>
  <option value="saab">Saab</option>
  <option value="opel">Opel</option>
  <option value="audi">Audi</option>
</select>
  
</body>
</html>

 

运行结果是

图片[2]-蓝牙墨水屏上位机学习(3)-ELink墨水屏电子纸社区-FPGA CPLD-ChipDebug

这是一个下拉选择框。

当选项改变时执行的函数是:updateCanvasSize()

function updateCanvasSize() {
  const selectedSizeName = document.getElementById('canvasSize').value;
  const selectedSize = canvasSizes.find(size => size.name === selectedSizeName);
 
  canvas.width = selectedSize.width;
  canvas.height = selectedSize.height;
 
  updateImage(false);
}

 

调用 updateImage 函数

    • updateImage() 函数的作用是将用户选择的图像加载到画布上,并根据画布的尺寸进行缩放。
  1. 传入参数 false
    • updateImage 的参数 clear 控制是否在加载新图像前清除画布。
    • false 表示不清除画布,保留原有内容(如已绘制的文本或线条)。
  2. 执行时机
    • 当用户调整画布尺寸后,画布会被重置为空白状态。
    • 调用 updateImage(false) 会重新加载并绘制图像,同时保留其他元素(如文本、线条)。

为什么需要这行代码?

当你调整画布大小时,浏览器会自动清空画布内容。因此,在 updateCanvasSize() 中:

  1. 首先设置新的画布尺寸(canvas.width 和 canvas.height)。
  2. 然后调用 updateImage(false) 重新绘制图像,确保:
    • 图像适应新的画布尺寸。
    • 之前添加的文本和线条(通过 redrawTextElements() 和 redrawLineSegments())也会重新绘制。

示例场景

假设用户:

  1. 上传了一张图片到画布。
  2. 在图片上添加了一些文字注释。
  3. 调整了画布尺寸。

如果没有 updateImage(false)

  • 画布会变成空白(尺寸改变导致内容清空)。
  • 用户添加的文字和图片都会丢失。

有了 updateImage(false)

  • 图片会按新尺寸重新绘制。
  • 文字注释会保留在原来的位置(假设 redrawTextElements() 实现了这一点)。

参数 false 的重要性

传入 false 至关重要,因为:

  • 如果传入 true,会调用 clearCanvas(),导致所有内容被清除,包括用户添加的文本和线条。
  • false 确保只重新加载图像,而不清除其他元素。

总结

updateImage(false) 的核心目的是:

  • 在画布尺寸改变后,重新绘制图像以适应新尺寸。
  • 保留用户在画布上添加的其他元素(如文本、线条)。
  • 通过 convertDithering() 重新应用抖动效果(如果图像需要这种处理)。

updateImage(false)函数代码如下:

function updateImage(clear = false) {
  const image_file = document.getElementById('image_file');
  if (image_file.files.length == 0) return;
 
  if (clear) clearCanvas();
 
  const file = image_file.files[0];
  let 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);
 
    // Redraw text and lines
    redrawTextElements();
    redrawLineSegments();
 
    convertDithering()
  }
}

 

这个函数的作用是把用户选择的图像加载到 Canvas 上,并且可以选择是否清除画布原有内容。下面是对代码的详细解析:

函数定义与参数

function updateImage(clear = false) { 
  • clear:这是一个布尔类型的可选参数,默认值为false。它的作用是控制在加载新图像之前是否要清除画布。

获取并验证图像文件

const image_file = document.getElementById('image_file');
  if (image_file.files.length == 0) return;

 

image_file:通过 ID 获取页面上的文件输入元素(即<input type="file">)。

  • files.length:用于检查用户是否选择了文件。要是没有选择文件,函数会直接返回,终止执行。

清除画布(可选操作)

  if (clear) clearCanvas(); 
  • clear参数为true时,会调用clearCanvas()函数清除画布上的所有内容。

创建并加载图像对象

const file = image_file.files[0];
  let image = new Image();
  image.src = URL.createObjectURL(file);

 

file:获取用户选择的第一个文件。

  • new Image():创建一个 HTMLImageElement 对象,用于加载图像。
  • URL.createObjectURL(file):生成一个临时的 URL,指向用户选择的文件,以便将其赋值给Image对象的src属性。

图像加载完成后的回调函数

image.onload = function (event) {
    URL.revokeObjectURL(this.src);

 

URL.revokeObjectURL(this.src):释放之前创建的临时 URL,防止内存泄漏。因为此时图像已经加载完成,不再需要这个临时 URL 了。


onload 是 JavaScript 中一个常用的事件处理属性,用于在资源加载完成后执行特定代码。它是处理异步加载操作的关键机制,广泛应用于图片、脚本、iframe 等资源的加载场景。以下是关于 onload 的详细解释:

1. 基本概念

onload 是 DOM 元素的一个属性,用于绑定一个回调函数。当元素所引用的资源(如图片、脚本)成功加载完成时,浏览器会触发这个回调函数。

语法

element.onload = function() {   // 资源加载完成后执行的代码 }; 

2. 常见应用场景

(1)图片加载完成后处理

最常见的场景是在图片加载完成后执行操作(如调整尺寸、显示动画):

const img = new Image();
img.src = "https://example.com/image.jpg";
 
img.onload = function() {
  console.log("图片加载成功!");
  console.log("图片尺寸:", this.width, "x", this.height);
  
  // 将图片添加到页面
  document.body.appendChild(img);
};
 
img.onerror = function() {
  console.error("图片加载失败!");
};

 

(2)脚本加载完成后执行代码

动态加载外部脚本时,可以用 onload 确保脚本加载完成后再调用其中的函数:

const script = document.createElement("script");
script.src = "https://example.com/library.js";
 
script.onload = function() {
  // 脚本加载完成后调用其中的函数
  initLibrary();
};
 
document.head.appendChild(script);

 

(3)iframe 加载完成后操作

const iframe = document.createElement("iframe");
iframe.src = "https://example.com";
 
iframe.onload = function() {
  // 获取 iframe 内容
  const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
  console.log("iframe 加载完成!");
};
 
document.body.appendChild(iframe);

 

3. 核心特点

(1)异步执行

onload 是异步的。设置 src 后,浏览器会开始加载资源,但不会等待加载完成,而是继续执行后续代码。只有当资源加载完成后,才会触发回调函数。

示例

const img = new Image();
img.onload = function() {
  console.log("图片加载完成(异步)"); // 后输出
};
 
img.src = "https://example.com/image.jpg";
console.log("设置图片 src 后继续执行(同步)"); // 先输出

 

(2)仅在成功加载时触发

如果资源加载失败(如网络错误、文件不存在),onload 不会被触发,而是触发 onerror 事件。因此,建议同时监听这两个事件。

(3)this 指向

在 onload 回调中,this 指向触发事件的 DOM 元素本身(如 Image 对象、script 标签等)。

4. 与其他加载事件的对比

事件 触发时机 适用场景
onload 资源完全加载完成(包括所有依赖) 图片、脚本、iframe 等
onerror 资源加载失败 错误处理
onreadystatechange 资源状态变化(如 loadingcomplete 兼容性处理(旧浏览器)
DOMContentLoaded HTML 文档解析完成(不等待资源) 尽早显示页面内容
window.onload 整个页面(包括资源)加载完成 需要所有资源的场景

5. 注意事项

  1. 释放对象 URL(如你最初的代码片段):
    当使用 URL.createObjectURL(blob) 创建临时 URL 后,应在图片加载完成后调用 URL.revokeObjectURL() 释放内存:
    const blob = new Blob([data], { type: "image/jpeg" });
    const url = URL.createObjectURL(blob);
     
    img.onload = function() {
      URL.revokeObjectURL(url); // 释放内存
    };
     
    img.src = url;

     

    兼容性

  2. onload 是原生 JavaScript 属性,所有浏览器都支持。但对于复杂场景(如监听多个资源),可考虑使用 Promise 或 Promise.all() 简化代码。
  3. 缓存问题
    如果图片已被缓存,onload 可能会立即触发(甚至在设置 src 之前)。为确保一致性,建议先绑定 onload,再设置 src

在 onload 事件的回调函数中,event 是一个事件对象(Event Object),它包含了与事件相关的详细信息。当浏览器触发 onload 事件时,会自动创建这个对象并作为参数传递给回调函数。


1. event 参数的作用

event 对象提供了关于事件的上下文信息,例如:

  • 事件类型(如 "load"
  • 触发事件的元素(通过 event.target
  • 时间戳(事件发生的时间)
  • 其他与特定事件相关的属性(如鼠标位置、键盘按键等,在 onload 中较少用到)

2. 在 onload 事件中的常见属性

在 onload 回调中,最常用的 event 属性是:

(1)event.target

指向触发事件的 DOM 元素,等价于回调函数中的 this

const img = new Image();
img.onload = function(event) {
  console.log(event.target === this); // true
  console.log("加载完成的图片:", event.target.src);
};
img.src = "https://example.com/image.jpg";

 

(2)event.type

返回事件的类型(字符串),对于 onload 事件,值为 "load"

img.onload = function(event) {
  console.log("事件类型:", event.type); // 输出 "load"
};

 

3. 为什么需要 event 参数?

虽然在简单场景中可以直接使用 this 访问元素,但 event 参数提供了更丰富的信息,尤其在以下场景中很有用:

(1)通用事件处理函数

当一个函数被多个事件共用时,通过 event.target 可以区分具体是哪个元素触发了事件。

function handleLoad(event) {
  console.log(`${event.target.tagName} 加载完成:`, event.target.src);
}
 
const img1 = new Image();
const img2 = new Image();
 
img1.onload = handleLoad;
img2.onload = handleLoad;
 
img1.src = "img1.jpg";
img2.src = "img2.jpg";

 

(2)阻止默认行为

在某些事件(如 clicksubmit)中,可以使用 event.preventDefault() 阻止默认行为。但 onload 事件没有默认行为需要阻止,因此这个方法在 onload 中无效。

4. 与 this 的区别

  • this:指向调用回调函数的对象(即 DOM 元素本身)。
  • event.target:指向触发事件的元素。在大多数情况下,两者是相同的,但在事件冒泡时可能不同。
    const img = new Image();
    img.onload = function(event) {
      console.log("this:", this);         // Image 对象
      console.log("event.target:", event.target); // 同样是 Image 对象
    };

     

    5. 示例代码

下面是一个完整示例,展示 event 参数在 onload 中的用法:

const img = new Image();
 
// 设置图片源前先绑定事件
img.onload = function(event) {
  // 释放对象 URL(如果使用了 createObjectURL)
  if (this.src.startsWith("blob:")) {
    URL.revokeObjectURL(this.src);
  }
 
  // 使用 event 对象获取信息
  console.log("事件类型:", event.type); // "load"
  console.log("加载的图片:", event.target.src);
  console.log("图片尺寸:", this.width, "x", this.height);
 
  // 将图片添加到页面
  document.body.appendChild(this);
};
 
img.onerror = function(event) {
  console.error("图片加载失败:", event.type);
  console.error("错误信息:", event.message); // 部分浏览器支持
};
 
// 创建一个临时 Blob URL 作为示例
const blob = new Blob([""], { type: "image/svg+xml" });
img.src = URL.createObjectURL(blob);

 

在 Canvas 上绘制图像

ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);

 

ctx:Canvas 的 2D 上下文对象,通过canvas.getContext('2d')获取。

  • drawImage 参数
    • 第一个参数image:要绘制的图像对象。
    • 第二到第五个参数0, 0, image.width, image.height:表示从原始图像的左上角 (0, 0) 开始,绘制整个图像。
    • 最后四个参数0, 0, canvas.width, canvas.height:表示将图像绘制在 Canvas 的左上角,并根据 Canvas 的尺寸对图像进行缩放。

重绘其他元素

redrawTextElements();
    redrawLineSegments();

 

  • redrawTextElements():重新绘制之前添加的文本元素,确保它们不会因为图像更新而丢失。
  • redrawLineSegments():重新绘制之前添加的线段,保证线段也不会因图像更新而消失。

应用抖动效果

convertDithering()
  }
}

 

  • convertDithering():调用函数对 Canvas 上的图像应用抖动算法,将图像转换为特定的视觉风格,比如模拟黑白印刷效果。

function convertDithering() {
  const contrast = parseFloat(document.getElementById('contrast').value);
  const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const imageData = new ImageData(
    new Uint8ClampedArray(currentImageData.data),
    currentImageData.width,
    currentImageData.height
  );
 
  adjustContrast(imageData, contrast);
 
  const mode = document.getElementById('ditherMode').value;
  const processedData = processImageData(ditherImage(imageData));
  const finalImageData = decodeProcessedData(processedData, canvas.width, canvas.height, mode);
  ctx.putImageData(finalImageData, 0, 0);
}

 

代码功能概述

这段代码定义了一个名为 convertDithering 的函数,用于对 Canvas 中的图像进行抖动处理。抖动是一种在有限颜色空间中模拟更多颜色的技术,常用于黑白图像、低色深显示设备或艺术效果。

函数的主要流程包括:

  1. 获取用户设置的对比度值
  2. 复制当前画布图像数据
  3. 调整图像对比度
  4. 应用抖动算法处理图像
  5. 处理并解码图像数据
  6. 将处理后的图像绘制回画布

详细代码解释

function convertDithering() {
  // 1. 获取用户设置的对比度值(从页面输入元素获取)
  const contrast = parseFloat(document.getElementById('contrast').value);
  
  // 2. 获取当前画布上的图像数据,并创建副本
  //    - ctx 是 Canvas 2D 上下文
  //    - 复制数据避免直接修改原始图像
  const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const imageData = new ImageData(
    new Uint8ClampedArray(currentImageData.data),
    currentImageData.width,
    currentImageData.height
  );
 
  // 3. 调整图像对比度
  //    - adjustContrast 是自定义函数,增强或减弱图像对比度
  adjustContrast(imageData, contrast);
 
  // 4. 获取用户选择的抖动模式(如 Floyd-Steinberg、Bayer 等)
  const mode = document.getElementById('ditherMode').value;
  
  // 5. 应用抖动算法处理图像数据
  //    - ditherImage 是自定义函数,实现具体的抖动算法
  //    - processImageData 可能对抖动后的数据进行后处理(如压缩、编码)
  const processedData = processImageData(ditherImage(imageData));
  
  // 6. 解码处理后的数据并转换为 ImageData 对象
  //    - 根据选择的抖动模式,将处理后的数据还原为画布可绘制的格式
  const finalImageData = decodeProcessedData(processedData, canvas.width, canvas.height, mode);
  
  // 7. 将最终处理好的图像数据绘制到画布上
  ctx.putImageData(finalImageData, 0, 0);
}

 

关键步骤解析

1. 对比度调整
const contrast = parseFloat(document.getElementById('contrast').value);

 

从页面的 contrast 输入框获取用户设置的对比度值
  • parseFloat 确保将输入转换为数字
2. 图像数据复制
const imageData = new ImageData(
  new Uint8ClampedArray(currentImageData.data),
  currentImageData.width,
  currentImageData.height
);

 

使用 Uint8ClampedArray 创建原始图像数据的深拷贝
  • 避免直接修改原始数据,保留原图作为参考
3. 抖动处理流程
adjustContrast(imageData, contrast);
const processedData = processImageData(ditherImage(imageData));

 

adjustContrast:增强或减弱图像对比度,使后续抖动效果更明显
  • ditherImage:核心抖动算法实现,将图像转换为黑白或有限色彩
  • processImageData:可能进行数据优化(如压缩、降噪)
4. 模式选择与解码
const mode = document.getElementById('ditherMode').value;
const finalImageData = decodeProcessedData(processedData, canvas.width, canvas.height, mode);

 

支持多种抖动模式(如 Floyd-Steinberg、Jarvis、Atkinson 等)
  • decodeProcessedData 根据不同模式解码处理后的数据

可能的辅助函数实现

以下是代码中调用的几个关键函数的可能实现示例:

// 调整图像对比度
function adjustContrast(imageData, contrast) {
  const data = imageData.data;
  const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
  
  for (let i = 0; i < data.length; i += 4) {
    // 对每个像素的 RGB 值应用对比度公式
    data[i] = factor * (data[i] - 128) + 128;     // Red
    data[i + 1] = factor * (data[i + 1] - 128) + 128; // Green
    data[i + 2] = factor * (data[i + 2] - 128) + 128; // Blue
    // 保持透明度不变
  }
}
 
// 应用 Floyd-Steinberg 抖动算法
function ditherImage(imageData) {
  const data = imageData.data;
  const width = imageData.width;
  const height = imageData.height;
  
  // 遍历每个像素(跳过边缘)
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const index = (y * width + x) * 4;
      
      // 计算当前像素亮度(简化为灰度值)
      const gray = 0.299 * data[index] + 0.587 * data[index + 1] + 0.114 * data[index + 2];
      
      // 阈值处理(0 或 255)
      const newColor = gray < 128 ? 0 : 255;
      const error = gray - newColor;
      
      // 应用新颜色
      data[index] = data[index + 1] = data[index + 2] = newColor;
      
      // 误差扩散到相邻像素(Floyd-Steinberg 算法)
      if (x + 1 < width) {
        data[index + 4] += error * 7/16;       // 右
      }
      if (x - 1 >= 0 && y + 1 < height) {
        data[index + width * 4 - 4] += error * 3/16; // 左下
      }
      if (y + 1 < height) {
        data[index + width * 4] += error * 5/16;     // 下
      }
      if (x + 1 < width && y + 1 < height) {
        data[index + width * 4 + 4] += error * 1/16; // 右下
      }
    }
  }
  
  return imageData;
}

 

抖动算法原理

抖动是一种通过在相邻像素间分布误差来模拟更多颜色的技术。最常见的 Floyd-Steinberg 算法工作流程:

  1. 将彩色图像转换为灰度图
  2. 对每个像素应用阈值(通常为 128),转换为黑白
  3. 计算原始灰度值与转换后值之间的误差
  4. 将误差按特定比例分配给周围的像素(扩散)
  5. 处理下一个像素时,使用已包含误差的像素值

关键技术点

  1. 异步加载
    • 图像加载是异步进行的,所以代码需要在onload回调函数中处理图像绘制,确保图像完全加载后再进行操作。
  2. 内存管理
    • 使用URL.revokeObjectURL()释放临时 URL,避免占用过多内存。
  3. 图像缩放
    • 通过drawImage的参数设置,实现图像自动适应 Canvas 尺寸的效果。
  4. 状态保留
    • 通过调用redrawTextElements()redrawLineSegments(),保证 Canvas 上的其他元素不会因图像更新而丢失。

潜在问题与优化建议

  1. 错误处理
    • 代码没有处理图像加载失败的情况。可以添加image.onerror回调来处理这种异常情况。
  2. 依赖检查
    • 函数依赖外部变量ctxcanvas,需要确保在调用该函数之前这些变量已经正确初始化。
  3. 防抖处理
    • 如果这个函数会被频繁调用(例如在调整 Canvas 大小时),可以考虑添加防抖措施,避免性能问题。
  4. 参数验证
    • 可以添加对clear参数类型的验证,确保传入的是布尔值。

总结

这个函数实现了图像加载、缩放和抖动效果应用的完整流程,同时能够保留 Canvas 上的其他元素。通过合理使用异步回调和 Canvas API,确保了图像能够正确加载并处理。


sendimg()函数的第二行代码:

const ditherMode = document.getElementById(‘ditherMode’).value//抖动模式

1. document : document 是浏览器提供的全局对象,代表当前加载的 HTML 文档。借助该对象,你能访问和操作文档里的所有元素。
2. getElementById(‘ditherMode’) :这是 document 对象的一个方法,作用是通过元素的 id 属性在文档里查找对应的元素。若找到了 id 为 ditherMode 的元素,就会返回对应的 DOM 元素对象;若未找到,则返回 null 。
3. .value :这是在访问 DOM 元素的 value 属性,通常用于获取表单元素(如 <input> 、 <select> 、 <textarea> 等)的当前值。
4. const ditherMode :使用 const 关键字声明一个常量 ditherMode ,并把 id 为 ditherMode 的元素的 value 属性值赋给它。 const 声明的常量一旦赋值,就不能再重新赋值。
### 示例场景
假设 HTML 文档中有如下代码:

“`
<select id=”ditherMode”>
<option value=”none”>无抖动</option>
<option value=”floyd-steinberg” selected>弗洛伊德 – 斯坦伯格抖动</option>
<option value=”ordered”>有序抖动</option>
</select>
“`
当执行 const ditherMode = document.getElementById(‘ditherMode’).value; 时, ditherMode 的值会是字符串 “floyd-steinberg” ,因为该选项当前处于选中状态。

在index.html中找到如下 代码:

<select id="ditherMode" onchange="updateImage(false)">
						<option value="blackWhiteColor">双色(黑白)</option>
						<option value="threeColor">三色(黑白红)</option>
						<option value="fourColor">四色(黑白红黄)</option>
						<option value="sixColor">六色(黑白红黄蓝绿)</option>
					</select>

 

sendimg()函数的第三行代码:

const epdDriverSelect = document.getElementById(‘epddriver’);

1. const :这是 ES6 引入的关键字,用于声明一个常量。常量一旦被赋值,在其作用域内就不能再被重新赋值。
2. epdDriverSelect :这是声明的常量名称,推测该常量可能用于表示与电子纸驱动(EPD Driver)相关的选择器。
3. document : document 是浏览器提供的全局对象,代表当前加载的 HTML 文档。借助这个对象,你可以访问和操作文档中的所有元素。
4. getElementById(‘epddriver’) :这是 document 对象的一个方法,作用是通过元素的 id 属性在文档中查找对应的元素。如果找到了 id 为 epddriver 的元素,就会返回对应的 DOM 元素对象;如果没找到,则返回 null 。
### 示例场景
假设 HTML 文档中有如下代码:

“`
<select id=”epddriver”>
<option value=”driver1″>驱动 1</option>
<option value=”driver2″>驱动 2</option>
</select>
“`
当执行 const epdDriverSelect = document.getElementById(‘epddriver’); 时, epdDriverSelect 就会指向这个 <select> 元素,后续可以通过这个常量对该元素进行操作,比如获取或修改其值、添加事件监听器等。



1. 什么是 HTML DOM?
HTML DOM 是浏览器将 HTML 文档解析为一个树形结构的编程接口。它将文档中的每个部分(如元素、属性、文本等)表示为节点(Node),这些节点共同组成了 DOM 树。通过 DOM,开发者可以使用 JavaScript 动态地操作网页内容。

DOM 树的结构
DOM 树由多个节点组成,常见的节点类型包括:

文档节点(Document):整个文档的根节点。
元素节点(Element):HTML 标签(如 <div>、<p> 等)。
属性节点(Attribute):HTML 元素的属性(如 class、id 等)。
文本节点(Text):元素中的文本内容。
注释节点(Comment):HTML 文档中的注释。



在Index.html中找到如下 代码:

<select id="epddriver" onchange="updateDitcherOptions()">
						<option value="01" data-color="blackWhiteColor" data-size="4.2_400_300">UC8176/UC8276(黑白)</option>
						<option value="03" data-color="threeColor" data-size="4.2_400_300">UC8176/UC8276(三色)</option>
						<option value="04" data-color="blackWhiteColor" data-size="4.2_400_300">SSD1619/SSD1683(黑白)</option>
						<option value="02" data-color="threeColor" data-size="4.2_400_300">SSD1619/SSD1683(三色)</option>
						<option value="05" data-color="fourColor" data-size="4.2_400_300">JD79668(四色)</option>
					</select>

 

代码第4行

const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex];

代码解析:获取选中的电子纸驱动选项

这行代码从 HTML 的下拉选择框(<select>)中获取用户当前选中的电子纸驱动选项,是前端开发中处理表单选择的典型操作。下面我将详细解析其功能和实现逻辑。

代码功能详解

1. 获取 select 元素引用
const epdDriverSelect = document.getElementById('epddriver'); 
  • 作用:获取 ID 为 epddriver 的 HTML 元素引用
  • 典型 HTML 结构
    <select id="epddriver">
      <option value="epd213b" data-size="128x296" data-color="blackWhiteColor">2.13" 黑白</option>
      <option value="epd213c" data-size="128x296" data-color="threeColor">2.13" 三色</option>
      <option value="epd42" data-size="400x300" data-color="blackWhiteColor">4.2" 黑白</option>
    </select>

     

2. 获取选中的选项索引
epdDriverSelect.selectedIndex 
  • selectedIndex 属性:返回当前选中选项的索引值(从 0 开始)
  • 示例:如果用户选择了 “2.13” 三色 “,则 selectedIndex 为 1
3. 获取选中的选项元素
const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex]; 
  • options 属性:是一个类数组对象,包含所有 <option> 子元素
  • 最终结果selectedOption 指向用户当前选中的 <option> 元素

关键属性与应用场景

1. 常用的 <option> 属性
  • value:选项的值(如 "epd213b"
  • text:显示给用户的文本(如 "2.13" 黑白"
  • 自定义数据属性
    • data-size:兼容的画布尺寸(如 "128x296"
    • data-color:支持的颜色模式(如 "blackWhiteColor"
2. 在电子纸系统中的应用

获取选中的驱动选项后,通常会读取其自定义数据属性进行验证:

// 验证画布尺寸与驱动是否匹配
if (selectedOption.getAttribute('data-size') !== canvasSize) {
  addLog(`画布尺寸和驱动不匹配,请重新选择。`);
  return;
}
 
// 验证颜色模式与驱动是否匹配
if (selectedOption.getAttribute('data-color') !== ditherMode) {
  addLog(`颜色模式和驱动不匹配,请重新选择。`);
  return;
}

 

代码示例与交互逻辑

下面是一个完整的示例,展示如何监听驱动选择变化并动态更新 UI:

// 获取select元素
const driverSelect = document.getElementById('epddriver');
 
// 监听选择变化事件
driverSelect.addEventListener('change', function() {
  const selectedOption = this.options[this.selectedIndex];
  
  // 获取驱动信息
  const driverValue = selectedOption.value;
  const driverName = selectedOption.text;
  const supportedSize = selectedOption.getAttribute('data-size');
  const supportedColor = selectedOption.getAttribute('data-color');
  
  // 更新UI显示
  document.getElementById('driverInfo').textContent = `
    已选择: ${driverName}
    支持尺寸: ${supportedSize}
    支持颜色: ${supportedColor}
  `;
  
  // 自动调整画布尺寸(可选)
  resizeCanvas(supportedSize);
});
 
// 初始化显示当前选择
function initDriverInfo() {
  const event = new Event('change');
  driverSelect.dispatchEvent(event);
}
 
// 页面加载后初始化
window.addEventListener('DOMContentLoaded', initDriverInfo);

 

2. 图像数据处理

sendimg()函数中的代码:

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const processedData = processImageData(imageData);

HTML canvas getImageData() 方法

  • ctx 是 Canvas 2D 绘图上下文,通过 canvas.getContext('2d') 获取
  • getImageData() 方法从画布中提取像素数据
  • 参数 (0, 0, canvas.width, canvas.height) 表示提取整个画布区域
  • 返回的 imageData 对象包含:
    • width 和 height 属性(画布尺寸)
    • data 属性:一个 Uint8ClampedArray 类型的数组
      • 包含每个像素的 RGBA 值(红、绿、蓝、透明度)
      • 每个值范围为 0-255
      • 数组长度为 width * height * 4
      • 例如:前 4 个值代表左上角第一个像素的 RGBA,接下来 4 个是第二个像素,依此类推
  • processImageData() 是一个自定义函数,用于处理图像数据
  • 常见处理场景包括:
    • 颜色模式转换:电子纸通常只支持黑白或有限灰度
    • 抖动算法:通过图案模拟更多灰度级别(类似老电视的黑白效果)
    • 降噪、对比度增强等预处理
  • 函数可能返回修改后的 ImageData 对象,或者转换后的二进制数据
  • 电子纸设备通常需要特定格式的图像数据(如黑白位图)
// 禁用按钮防止重复操作 updateButtonStatus(true); 
  • updateButtonStatus() 是一个控制 UI 状态的函数
  • 参数 true 表示禁用相关按钮
  • 目的是防止用户在图像处理过程中重复点击按钮
  • 可能的实现方式:javascript
    function updateButtonStatus(disabled) {
      const button = document.getElementById('processButton');
      button.disabled = disabled;
      button.textContent = disabled ? '处理中...' : '处理图像';
    }
  • 通常配合异步操作使用,图像处理完成后会调用 updateButtonStatus(false) 重新启用按钮

实例

下面的代码通过 getImageData() 复制画布上指定矩形的像素数据,然后通过 putImageData() 将图像数据放回画布:

var c=document.getElementById(“myCanvas”);
var ctx=c.getContext(“2d”);
ctx.fillStyle=”red”;
ctx.fillRect(10,10,50,50);

function copy()
{
var imgData=ctx.getImageData(10,10,50,50);
ctx.putImageData(imgData,10,70);
}

定义和用法
getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。

注意:ImageData 对象不是图像,它规定了画布上一个部分(矩形),并保存了该矩形内每个像素的信息。

对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:

R – 红色(0-255)
G – 绿色(0-255)
B – 蓝色(0-255)
A – alpha 通道(0-255; 0 是透明的,255 是完全可见的)

color/alpha 信息以数组形式存在,并存储于 ImageData 对象的 data 属性中。

提示:在操作完成数组中的 color/alpha 信息之后,您可以使用 putImageData() 方法将图像数据拷贝回画布上。

实例:

以下代码可获得被返回的 ImageData 对象中第一个像素的 color/alpha 信息:

red=imgData.data[0];
green=imgData.data[1];
blue=imgData.data[2];
alpha=imgData.data[3];

提示:您也可以使用 getImageData() 方法来反转画布上某个图像的每个像素的颜色。

使用该公式遍历所有的像素,并改变其颜色值:

red=255-old_red;
green=255-old_green;
blue=255-old_blue;

JavaScript 语法:    context.getImageData(x,y,width,height);
参数值
参数    描述
x    开始复制的左上角位置的 x 坐标(以像素计)。
y    开始复制的左上角位置的 y 坐标(以像素计)。
width    要复制的矩形区域的宽度。
height    要复制的矩形区域的高度

实例
使用 getImageData() 来反转画布上的图像的每个像素的颜色:

JavaScript:

var c=document.getElementById(“myCanvas”);
var ctx=c.getContext(“2d”);//2d渲染
var img=document.getElementById(“scream”);
ctx.drawImage(img,0,0);
var imgData=ctx.getImageData(0,0,c.width,c.height);
// invert colors
for (var i=0;i<imgData.data.length;i+=4)
{
imgData.data[i]=255-imgData.data[i];
imgData.data[i+1]=255-imgData.data[i+1];
imgData.data[i+2]=255-imgData.data[i+2];
imgData.data[i+3]=255;
}
ctx.putImageData(imgData,0,0);


HTML canvas ImageData data 属性

实例

创建 100*100 像素的 ImageData 对象,其中每个像素均被设置为红色:

var c=document.getElementById(“myCanvas”);
var ctx=c.getContext(“2d”);
var imgData=ctx.createImageData(100,100);
for (var i=0;i<imgData.data.length;i+=4)
{
imgData.data[i+0]=255;
imgData.data[i+1]=0;
imgData.data[i+2]=0;
imgData.data[i+3]=255;
}
ctx.putImageData(imgData,10,10);

定义和用法

data 属性返回一个对象,该对象包含指定的 ImageData 对象的图像数据。

对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:

R – 红色(0-255)
G – 绿色(0-255)
B – 蓝色(0-255)
A – alpha 通道(0-255; 0 是透明的,255 是完全可见的)

color/alpha 信息以数组形式存在,并存储于 ImageData 对象的 data 属性中。

实例:

把 ImageData 对象中的第一个像素变为红色的语法:

imgData=ctx.createImageData(100,100);
 
imgData.data[0]=255;
imgData.data[1]=0;
imgData.data[2]=0;
imgData.data[3]=255;

 

把 ImageData 对象中的第二个像素变为绿色的语法:

imgData=ctx.createImageData(100,100);  imgData.data[4]=0; imgData.data[5]=255; imgData.data[6]=0; imgData.data[7]=255;

processImageData函数

function processImageData(imageData) {
  const width = imageData.width;
  const height = imageData.height;
  const data = imageData.data;
  const mode = document.getElementById('ditherMode').value;
 
  let processedData;
 
  if (mode === 'sixColor') {
    processedData = new Uint8Array(width * height);
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const index = (y * width + x) * 4;
        const r = data[index];
        const g = data[index + 1];
        const b = data[index + 2];
 
        const closest = findClosestColor(r, g, b);
        const newIndex = (x * height) + (height - 1 - y);
        processedData[newIndex] = closest.value;
      }
    }
  } else if (mode === 'fourColor') {
    processedData = new Uint8Array(Math.ceil((width * height) / 4));
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const index = (y * width + x) * 4;
        const r = data[index];
        const g = data[index + 1];
        const b = data[index + 2];
        const closest = findClosestColor(r, g, b); // 使用 fourColorPalette
        const colorValue = closest.value; // 0x00 (黑), 0x01 (白), 0x02 (红), 0x03 (黄)
        const newIndex = (y * width + x) / 4 | 0;
        const shift = 6 - ((x % 4) * 2);
        processedData[newIndex] |= (colorValue << shift);
      }
    }
  } else if (mode === 'blackWhiteColor') {
    const byteWidth = Math.ceil(width / 8);
    processedData = new Uint8Array(byteWidth * height);
    const threshold = 140;
 
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const index = (y * width + x) * 4;
        const r = data[index];
        const g = data[index + 1];
        const b = data[index + 2];
        const grayscale = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
        const bit = grayscale >= threshold ? 1 : 0;
        const byteIndex = y * byteWidth + Math.floor(x / 8);
        const bitIndex = 7 - (x % 8);
        processedData[byteIndex] |= (bit << bitIndex);
      }
    }
  } else if (mode === 'threeColor') {
    const byteWidth = Math.ceil(width / 8);
    const blackWhiteThreshold = 140;
    const redThreshold = 160;
 
    const blackWhiteData = new Uint8Array(height * byteWidth);
    const redWhiteData = new Uint8Array(height * byteWidth);
 
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const index = (y * width + x) * 4;
        const r = data[index];
        const g = data[index + 1];
        const b = data[index + 2];
        const grayscale = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
 
        const blackWhiteBit = grayscale >= blackWhiteThreshold ? 1 : 0;
        const blackWhiteByteIndex = y * byteWidth + Math.floor(x / 8);
        const blackWhiteBitIndex = 7 - (x % 8);
        if (blackWhiteBit) {
          blackWhiteData[blackWhiteByteIndex] |= (0x01 << blackWhiteBitIndex);
        } else {
          blackWhiteData[blackWhiteByteIndex] &= ~(0x01 << blackWhiteBitIndex);
        }
 
        const redWhiteBit = (r > redThreshold && r > g && r > b) ? 0 : 1;
        const redWhiteByteIndex = y * byteWidth + Math.floor(x / 8);
        const redWhiteBitIndex = 7 - (x % 8);
        if (redWhiteBit) {
          redWhiteData[redWhiteByteIndex] |= (0x01 << redWhiteBitIndex);
        } else {
          redWhiteData[redWhiteByteIndex] &= ~(0x01 << redWhiteBitIndex);
        }
      }
    }
 
    processedData = new Uint8Array(blackWhiteData.length + redWhiteData.length);
    processedData.set(blackWhiteData, 0);
    processedData.set(redWhiteData, blackWhiteData.length);
  }
 
  return processedData;
}

 

函数概述

processImageData 函数负责将 Canvas 的原始图像数据(RGBA 格式)转换为适合电子纸显示的特定格式。它支持四种不同的色彩模式:六色、四色、黑白和三色模式,并根据选择的模式生成对应的图像数据。

参数与变量

  • imageData:包含原始图像像素信息的对象,具有 widthheightdata 属性
  • width/height:图像的宽高
  • data:像素数据数组(RGBA 格式,每个像素占 4 个字节)
  • mode:通过下拉菜单选择的色彩模式(sixColorfourColorblackWhiteColorthreeColor

色彩模式处理详解

1. 六色模式 (sixColor)
processedData = new Uint8Array(width * height);
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    const index = (y * width + x) * 4;
    const r = data[index];
    const g = data[index + 1];
    const b = data[index + 2];
 
    const closest = findClosestColor(r, g, b);
    const newIndex = (x * height) + (height - 1 - y);
    processedData[newIndex] = closest.value;
  }
}

 

创建存储结果的数组
    • Uint8Array 是一个 8 位无符号整数数组(每个元素占 1 字节,范围 0-255)
    • width * height 计算出图像总像素数,每个像素在六色模式下用 1 字节表示
    • 这行代码为处理后的图像数据分配内存空间
    • Uint8Array、Uint16Array和Uint32Array是JavaScript中的TypedArray类型,用于处理二进制数据。它们分别表示8位、16位和32位的无符号整数数组。

      当需要处理二进制数据时,可以使用TypedArray来提高性能和效率。以下是使用不同类型的TypedArray的一些常见场景:

      Uint8Array:
      概念:Uint8Array是一个8位无符号整数数组,每个元素占用1个字节。
      优势:适用于处理字节数据,如图像、音频、视频等。
      应用场景:图像处理、音频处理、视频处理、网络通信等。
      腾讯云相关产品:腾讯云对象存储(COS)提供了对二进制数据的存储和处理能力,可用于存储和处理Uint8Array类型的数据。详情请参考:腾讯云对象存储(COS)
      Uint16Array:
      概念:Uint16Array是一个16位无符号整数数组,每个元素占用2个字节。
      优势:适用于处理较大范围的整数数据,如图像像素数据、传感器数据等。
      应用场景:图像处理、传感器数据处理、网络通信等。
      腾讯云相关产品:腾讯云物联网平台(IoT Hub)提供了设备数据采集和处理的能力,可用于处理Uint16Array类型的数据。详情请参考:腾讯云物联网平台(IoT Hub)
      Uint32Array:
      概念:Uint32Array是一个32位无符号整数数组,每个元素占用4个字节。
      优势:适用于处理较大范围的整数数据,如计数器、哈希算法等。
      应用场景:计数器、哈希算法、网络通信等。
      腾讯云相关产品:腾讯云云函数(SCF)提供了无服务器计算能力,可用于处理Uint32Array类型的数据。详情请参考:腾讯云云函数(SCF)
      总结:Uint8Array、Uint16Array和Uint32Array是JavaScript中的TypedArray类型,用于处理二进制数据。它们分别适用于不同的场景,如图像处理、音视频处理、传感器数据处理、计数器、哈希算法等。腾讯云提供了相关产品和服务,可用于存储、处理和分析这些类型的数据。

    const index = (y * width + x) * 4; 
  1. 计算原始数据中的像素索引
    • y * width + x 计算当前像素在一维数组中的位置(假设每像素 1 个值)
    • 乘以 4 是因为原始数据是 RGBA 格式,每个像素占 4 个字节
    • 例如:左上角第一个像素的索引是 0,第二个像素是 4,依此类推
      const r = data[index];
          const g = data[index + 1];
          const b = data[index + 2];

      提取 RGB 值

    • data[index] 获取红色分量(0-255)
    • data[index + 1] 获取绿色分量
    • data[index + 2] 获取蓝色分量
    • 透明度分量 data[index + 3] 在此模式下被忽略
    const closest = findClosestColor(r, g, b); 
  1. 找到最接近的目标颜色
    • findClosestColor 是一个自定义函数,用于将 RGB 值映射到六色调色板中的一种
    • 例如:输入 RGB (255, 0, 0) 可能返回红色对应的调色板索引
    • 返回值 closest 是一个对象,包含颜色索引或值
    const newIndex = (x * height) + (height - 1 - y); 
  1. 计算处理后数据的存储位置
    • 这是一个关键步骤,涉及坐标系统的转换
    • x * height 计算列方向的偏移量(注意这里用高度而非宽度)
    • height - 1 - y 反转行方向(将 Y 轴翻转,原顶部变为底部)
    • 整体效果:将图像从正常坐标系转换为适合特定显示设备的坐标系
    • 例如:100×200 的图像,坐标 (50, 100) 会映射到新位置:50×200 + (200-1-100) = 10099
    processedData[newIndex] = closest.value; 
  1. 存储处理后的颜色值
    • 将找到的最接近颜色的值存入新数组的计算位置
    • 例如:如果 closest.value 是 3,表示对应六色调色板中的第 3 种颜

关键技术点解析

  1. 数据格式转换
    • 从 RGBA(每像素 4 字节)转换为单通道(每像素 1 字节)
    • 每个字节表示六色调色板中的一种颜色
  2. 坐标系统转换
    • newIndex = (x * height) + (height - 1 - y) 的设计目的:
      • 通常用于旋转或镜像图像,适应特定硬件的显示需求
      • 可能是将水平图像数据转换为垂直排列,供某些电子纸设备使用
      • 例如:对于宽度大于高度的图像,可能需要转置后才能正确显示
  3. 颜色映射
    • findClosestColor 函数的实现通常涉及:
      • 定义一个包含六种颜色的调色板(如:黑、白、红、绿、蓝、黄)
      • 计算输入 RGB 与调色板中每种颜色的欧氏距离
      • 返回距离最小的颜色索引
2. 四色模式 (fourColor)

javascript

processedData = new Uint8Array(Math.ceil((width * height) / 4));
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    const index = (y * width + x) * 4;
    const r = data[index];
    const g = data[index + 1];
    const b = data[index + 2];
    const closest = findClosestColor(r, g, b);
    const colorValue = closest.value; // 0x00 (黑), 0x01 (白), 0x02 (红), 0x03 (黄)
    const newIndex = (y * width + x) / 4 | 0;
    const shift = 6 - ((x % 4) * 2);
    processedData[newIndex] |= (colorValue << shift);
  }
}

 

特点:四色调色板(黑、白、红、黄),每 2 位表示一个像素,节省空间

  • 处理流程
    1. 遍历每个像素,提取 RGB 值
    2. 找到最接近的四色值
    3. 每 4 个像素压缩到一个字节中(每个像素 2 位)
    4. 使用位运算将颜色值存入正确位置
3. 黑白模式 (blackWhiteColor)

javascript

const byteWidth = Math.ceil(width / 8);
processedData = new Uint8Array(byteWidth * height);
const threshold = 140;
 
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    const index = (y * width + x) * 4;
    const r = data[index];
    const g = data[index + 1];
    const b = data[index + 2];
    const grayscale = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
    const bit = grayscale >= threshold ? 1 : 0;
    const byteIndex = y * byteWidth + Math.floor(x / 8);
    const bitIndex = 7 - (x % 8);
    processedData[byteIndex] |= (bit << bitIndex);
  }
}

 

特点:纯黑白显示,每个像素用 1 位表示,最节省空间

  • 处理流程
    1. 将 RGB 值转换为灰度值(使用标准公式)
    2. 应用阈值(140)判断该像素是黑还是白
    3. 每 8 个像素压缩到一个字节中
    4. 注意位顺序:从最高位到最低位存储从左到右的像素
4. 三色模式 (threeColor)

javascript

const byteWidth = Math.ceil(width / 8);
const blackWhiteThreshold = 140;
const redThreshold = 160;
 
const blackWhiteData = new Uint8Array(height * byteWidth);
const redWhiteData = new Uint8Array(height * byteWidth);
 
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    // 黑白层处理
    const index = (y * width + x) * 4;
    const r = data[index];
    const g = data[index + 1];
    const b = data[index + 2];
    const grayscale = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
 
    const blackWhiteBit = grayscale >= blackWhiteThreshold ? 1 : 0;
    const blackWhiteByteIndex = y * byteWidth + Math.floor(x / 8);
    const blackWhiteBitIndex = 7 - (x % 8);
    // 设置黑白位
    
    // 红色层处理
    const redWhiteBit = (r > redThreshold && r > g && r > b) ? 0 : 1;
    const redWhiteByteIndex = y * byteWidth + Math.floor(x / 8);
    const redWhiteBitIndex = 7 - (x % 8);
    // 设置红色位
  }
}
 
processedData = new Uint8Array(blackWhiteData.length + redWhiteData.length);
processedData.set(blackWhiteData, 0);
processedData.set(redWhiteData, blackWhiteData.length);

 

特点:使用两层数据表示三种颜色(黑、白、红)

  • 处理流程
    1. 创建两个独立的位图:黑白层和红色层
    2. 黑白层处理与黑白模式类似
    3. 红色层判断:如果 R 值足够高且明显大于 G 和 B,则认为是红色
    4. 最终数据由黑白层和红色层拼接而成

数据压缩技术

这个函数使用了几种常见的图像数据压缩技术:

  1. 位压缩:在黑白和三色模式中,每个像素只用 1 位表示,8 个像素压缩到一个字节
  2. 多位像素:四色模式中,每个像素用 2 位表示,4 个像素压缩到一个字节
  3. 分层存储:三色模式使用两层数据分别表示黑白和红色信息
  4.  

双通道数据合并解析

这三行代码实现了将两个独立通道的数据合并为一个连续的字节数组,是三色模式图像处理的最后一步。让我们详细分析这个过程。

代码功能概述

这段代码的作用是:

  1. 创建一个新的字节数组,大小为两个通道数据长度之和
  2. 将黑白通道数据复制到数组的前半部分
  3. 将红色通道数据复制到数组的后半部分

这种设计允许将两种颜色信息(黑白和红色)分别存储在不同的通道中,同时保持数据的连续性。

详细解析

1. 创建合并后的数组
processedData = new Uint8Array(blackWhiteData.length + redWhiteData.length); 
  • blackWhiteData:存储黑白通道信息的字节数组
  • redWhiteData:存储红色通道信息的字节数组
  • 新数组的长度是两个通道长度之和,确保有足够空间容纳所有数据
2. 复制黑白通道数据
processedData.set(blackWhiteData, 0); 
  • set() 方法将 blackWhiteData 中的所有元素复制到 processedData 中
  • 从索引 0 开始写入,即新数组的前半部分存储黑白通道数据
3. 复制红色通道数据
processedData.set(redWhiteData, blackWhiteData.length); 
  • 从索引 blackWhiteData.length 开始写入红色通道数据
  • 确保红色通道数据紧跟在黑白通道数据之后

数据结构示意图

假设黑白和红色通道各有 100 字节数据,合并后的数组结构如下:

processedData: [黑白通道数据(100字节), 红色通道数据(100字节)] 索引范围:         0 ~ 99              100 ~ 199 

双通道设计的意义

这种双通道设计在实际应用中有以下优势:

  1. 颜色表达能力增强:通过组合两个二值通道,可以表达三种颜色:
    • 黑白通道为 0,红色通道为 0 → 黑色
    • 黑白通道为 1,红色通道为 1 → 白色
    • 黑白通道为 0,红色通道为 1 → 红色
  2. 设备兼容性:许多电子墨水屏(如电子纸显示器)支持这种双色通道显示模式
    • 黑白通道控制主显示层
    • 红色通道控制叠加的彩色层
  3. 数据处理灵活性:两个通道可以独立处理和更新
    • 例如,只更新黑白通道而保持红色通道不变

后续处理逻辑推测

在实际应用中,合并后的数据可能会这样使用:

// 解码处理后的数据(推测实现)
function decodeProcessedData(processedData, width, height, mode) {
  if (mode === 'threeColor') {
    const byteWidth = Math.ceil(width / 8);
    const dataLength = byteWidth * height;
    
    // 分离两个通道
    const blackWhiteData = processedData.slice(0, dataLength);
    const redWhiteData = processedData.slice(dataLength);
    
    // 创建新的图像数据
    const imageData = new ImageData(width, height);
    const data = imageData.data;
    
    // 解码每个像素
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const byteIndex = y * byteWidth + Math.floor(x / 8);
        const bitIndex = 7 - (x % 8);
        
        // 获取黑白通道位值
        const bwBit = (blackWhiteData[byteIndex] >> bitIndex) & 1;
        
        // 获取红色通道位值
        const rBit = (redWhiteData[byteIndex] >> bitIndex) & 1;
        
        // 确定最终颜色
        let r, g, b;
        if (bwBit === 0 && rBit === 0) {
          [r, g, b] = [0, 0, 0];    // 黑色
        } else if (bwBit === 1 && rBit === 1) {
          [r, g, b] = [255, 255, 255]; // 白色
        } else if (bwBit === 0 && rBit === 1) {
          [r, g, b] = [255, 0, 0];    // 红色
        }
        
        // 设置像素值
        const index = (y * width + x) * 4;
        data[index] = r;
        data[index + 1] = g;
        data[index + 2] = b;
        data[index + 3] = 255; // 不透明
      }
    }
    
    return imageData;
  }
  
  // 其他模式的处理...
}

 

if (blackWhiteBit) { blackWhiteData[blackWhiteByteIndex] |= (0x01 << blackWhiteBitIndex); } else { blackWhiteData[blackWhiteByteIndex] &= ~(0x01 << blackWhiteBitIndex); } 

这段代码在处理黑白层数据时,通过位操作将每个像素的黑白值(blackWhiteBit)写入对应的字节位置。具体分析如下:

代码功能解释

if (blackWhiteBit) {
  // 如果当前像素为白色(blackWhiteBit = 1)
  blackWhiteData[blackWhiteByteIndex] |= (0x01 << blackWhiteBitIndex);
} else {
  // 如果当前像素为黑色(blackWhiteBit = 0)
  blackWhiteData[blackWhiteByteIndex] &= ~(0x01 << blackWhiteBitIndex);
}

 

关键步骤

  1. 确定写入位置
    • blackWhiteByteIndex:当前像素所在的字节索引(每行byteWidth个字节,每个字节存储 8 个像素)。
    • blackWhiteBitIndex:当前像素在字节中的位索引(范围 0-7,从右到左排列,即bit7bit0)。
  2. 位操作逻辑
    • 置位(白色)
      blackWhiteData[blackWhiteByteIndex] |= (0x01 << blackWhiteBitIndex); 

      blackWhiteData[blackWhiteByteIndex]的第blackWhiteBitIndex位设置为 1(通过按位或操作)。

    • 清位(黑色)
      blackWhiteData[blackWhiteByteIndex] &= ~(0x01 << blackWhiteBitIndex); 

      blackWhiteData[blackWhiteByteIndex]的第blackWhiteBitIndex位设置为 0(通过按位与取反操作)。

等价优化写法

这段代码可以简化为一行,避免条件判断:

// 优化后:根据blackWhiteBit的值直接设置或清除对应位
blackWhiteData[blackWhiteByteIndex] = 
  (blackWhiteData[blackWhiteByteIndex] & ~(0x01 << blackWhiteBitIndex)) | 
  (blackWhiteBit << blackWhiteBitIndex);

 

优化逻辑

  1. 清除目标位
    blackWhiteData[blackWhiteByteIndex] & ~(0x01 << blackWhiteBitIndex) 

    将目标位先置为 0(其他位保持不变)。

  2. 写入新值
    | (blackWhiteBit << blackWhiteBitIndex) 

    如果blackWhiteBit为 1,则将目标位重新置为 1;否则保持 0。

为什么要这样写?

  1. 位操作的必要性
    • 黑白模式下,每个像素仅需 1 位存储(0 = 黑,1 = 白)。
    • 一个字节有 8 位,因此需要通过位操作将 8 个像素压缩到 1 个字节中。
  2. 位操作的精确性
    • 按位或(|)和按位与(&)操作可以精确修改指定位置的位,而不影响其他位。
    • 例如,若要将字节的第 3 位(从 0 开始)置为 1:javascript
      byte |= (0x01 << 3); // 等价于 byte |= 0b00001000; 
    • 若要将第 3 位清 0:
      byte &= ~(0x01 << 3); // 等价于 byte &= 0b11110111; 

注意事项

  1. 函数依赖未定义的findClosestColor函数,该函数应根据不同模式提供对应的调色板
  2. 六色模式中的坐标转换(x * height) + (height - 1 - y)可能用于特定显示需求,需注意
  3. 阈值(如 140、160)可根据实际显示效果调整
  4. 处理后的数据格式需与目标电子纸设备的驱动程序兼容
3. 根据颜色模式发送数据
if (ditherMode === 'fourColor') {
  await writeImage(processedData, 'color');
} else if (ditherMode === 'threeColor') {
  // 三色模式需要分两次发送:黑白数据和红色数据
  const halfLength = Math.floor(processedData.length / 2);
  await writeImage(processedData.slice(0, halfLength), 'bw');
  await writeImage(processedData.slice(halfLength), 'red');
} else if (ditherMode === 'blackWhiteColor') {
  await writeImage(processedData, 'bw');
} else {
  addLog("当前固件不支持此颜色模式。");
  updateButtonStatus();
  return;
}

 

多色模式处理
    • 三色电子纸通常需要分别发送黑白层和红色层数据
    • 四色模式则一次性发送所有颜色数据
    • 不同模式调用 writeImage() 函数时会传递不同的参数(如 ‘bw’ 表示黑白数据)

async function writeImage(data, step = 'bw') {
  const chunkSize = document.getElementById('mtusize').value - 2;
  const interleavedCount = document.getElementById('interleavedcount').value;
  const count = Math.round(data.length / chunkSize);
  let chunkIdx = 0;
  let noReplyCount = interleavedCount;
 
  for (let i = 0; i < data.length; i += chunkSize) {
    let currentTime = (new Date().getTime() - startTime) / 1000.0;
    setStatus(`${step == 'bw' ? '黑白' : '颜色'}块: ${chunkIdx + 1}/${count + 1}, 总用时: ${currentTime}s`);
    const payload = [
      (step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0),
      ...data.slice(i, i + chunkSize),
    ];
    if (noReplyCount > 0) {
      await write(EpdCmd.WRITE_IMG, payload, false);
      noReplyCount--;
    } else {
      await write(EpdCmd.WRITE_IMG, payload, true);
      noReplyCount = interleavedCount;
    }
    chunkIdx++;
  }
}

 

这段代码定义了一个异步函数 writeImage,用于将图像数据分块发送到电子纸显示屏(EPD)。函数通过控制数据块的发送节奏,优化了与硬件通信的效率。

函数核心逻辑概述

函数接收图像数据 data 和处理步骤 step(默认为 'bw',即黑白层),并根据配置参数将数据分块发送给 EPD 设备。主要功能如下:

参数与配置

  • data:待发送的图像数据(通常是前面 processImageData 处理后的结果)。
  • step:处理步骤,'bw' 表示黑白层,其他值表示颜色层。
  • 配置参数(从 DOM 获取):
    • mtusize:最大传输单元大小,用于确定每个数据块的最大长度。
    • interleavedcount:连续发送的无响应数据块数量,用于优化传输效率。

数据分块与发送流程

  1. 计算分块信息
    • chunkSize:每个数据块的大小(mtusize - 2)。
    • count:数据块总数(Math.round(data.length / chunkSize))。
  2. 循环发送数据块
    • 状态更新:显示当前发送进度(如 “黑白块: 10/100”)和总用时。
    • 构建数据块
      • 每个数据块的第一个字节为控制字节,格式为:
        • 0x0F(黑白层)或 0x00(颜色层),与 0x00(首块)或 0xF0(非首块)组合。
      • 后续字节为实际图像数据(data.slice(i, i + chunkSize))。
    • 发送控制
      • 无响应模式:连续发送 interleavedcount 个数据块,无需等待设备响应。
      • 有响应模式:每发送 interleavedcount 个数据块后,发送一个需要设备确认的块,重置计数器。

关键技术点

  1. 交错发送(Interleaved Transmission)
    • 通过 interleavedcount 参数控制连续发送的无响应数据块数量,减少等待确认的时间,提高吞吐量。
    • 适用于支持批量数据传输的设备,平衡了传输效率和可靠性。
  2. 控制字节设计
    • 0x0F/0x00 区分黑白层和颜色层,用于多图层显示的 EPD 设备。
    • 0x00/0xF0 区分数据块是否为第一个块,可能用于设备初始化或数据同步。
  3. 异步操作
    • 使用 await 等待 write 函数完成,确保数据按顺序发送,避免缓冲区溢出。

函数调用示例

javascript

// 发送黑白层数据
await writeImage(processedBlackWhiteData, 'bw');
 
// 发送颜色层数据(如三色模式中的红色层)
await writeImage(processedRedData, 'color');

 

再解析一次

函数功能概述

writeImage 是一个异步函数,用于将处理后的图像数据分块发送到电子纸显示屏(EPD)。它通过控制数据块的发送节奏,优化了与硬件的通信效率,同时提供进度反馈。

参数与核心逻辑

参数说明
  • data:待发送的图像数据(通常是 processImageData 处理后的结果)
  • step:可选参数,默认为 'bw'(黑白层),也可为 'color'(颜色层)
核心逻辑步骤
  1. 配置参数获取
    const chunkSize = document.getElementById('mtusize').value - 2;
    const interleavedCount = document.getElementById('interleavedcount').value;

     

    chunkSize:每个数据块的大小(减去 2 字节用于协议开销)

    • interleavedCount:连续发送的无响应数据块数量
  2. 分块计算
    const count = Math.round(data.length / chunkSize); 
    • 计算数据块总数(向上取整)
  3. 主循环(分块发送)
    for (let i = 0; i < data.length; i += chunkSize) {
      // 更新状态显示
      let currentTime = (new Date().getTime() - startTime) / 1000.0;
      setStatus(`${step == 'bw' ? '黑白' : '颜色'}块: ${chunkIdx + 1}/${count + 1}, 总用时: ${currentTime}s`);
      
      // 构建数据块
      const payload = [
        (step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0),
        ...data.slice(i, i + chunkSize),
      ];
      
      // 交错发送控制
      if (noReplyCount > 0) {
        await write(EpdCmd.WRITE_IMG, payload, false);
        noReplyCount--;
      } else {
        await write(EpdCmd.WRITE_IMG, payload, true);
        noReplyCount = interleavedCount;
      }
      chunkIdx++;
    }

     

    关键技术点详解

1. 数据块结构
const payload = [
  (step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0), // 控制字节
  ...data.slice(i, i + chunkSize),                          // 图像数据
];

 

控制字节
    • 高 4 位:0xF(黑白层)或 0x0(颜色层)
    • 低 4 位:0x0(首块)或 0xF(非首块)
    • 示例:
      • 黑白层首块:0x0F | 0x00 = 0x0F
      • 颜色层非首块:0x00 | 0xF0 = 0xF0
2. 交错发送机制
if (noReplyCount > 0) {
  await write(EpdCmd.WRITE_IMG, payload, false); // 无响应模式
  noReplyCount--;
} else {
  await write(EpdCmd.WRITE_IMG, payload, true);  // 有响应模式
  noReplyCount = interleavedCount;
}

 

无响应模式:连续发送 interleavedCount 个数据块,无需等待设备确认
  • 有响应模式:每发送 interleavedCount 次后,发送一次需要确认的数据包
  • 目的:减少等待时间,提高吞吐量(适合支持批量传输的设备)
3. 进度反馈
setStatus(`${step == 'bw' ? '黑白' : '颜色'}块: ${chunkIdx + 1}/${count + 1}, 总用时: ${currentTime}s`);

 

实时更新界面显示当前处理进度
  • 计算自 startTime 以来的总用时(秒)

潜在问题与优化建议

1. 输入验证
  • 问题:若 mtusize < 2,chunkSize 为负数
  • 优化:
    let chunkSize = parseInt(document.getElementById('mtusize').value) - 2;
    chunkSize = Math.max(1, chunkSize); // 确保最小值为 1

     

    2. 错误处理

  • 问题:缺少通信失败的重试机制
  • 优化:添加重试逻辑(示例)
    async function safeWrite(command, payload, needReply) {
      const MAX_RETRIES = 3;
      for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
        try {
          await write(command, payload, needReply);
          return;
        } catch (error) {
          console.warn(`写入失败 (尝试 ${attempt + 1}/${MAX_RETRIES}):`, error);
          if (attempt === MAX_RETRIES - 1) throw error;
          await new Promise(resolve => setTimeout(resolve, 100)); // 等待 100ms
        }
      }
    }

     

3. 性能优化
  • 问题:data.slice() 会创建大量临时数组
  • 优化:使用 Uint8Array.subarray() 避免内存复制javascript
    const payload = new Uint8Array(chunkSize + 1);
    payload[0] = (step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0);
    payload.set(data.subarray(i, i + chunkSize), 1);

     


const payload = [
(step == ‘bw’ ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0),
…data.slice(i, i + chunkSize),
];

这段代码用于构建发送给电子纸显示屏(EPD)的单个数据块(payload),包含了控制信息和实际图像数据。它是硬件通信协议的核心部分,确保设备能正确解析并显示图像。

payload 结构详解

payload 是一个数组,由1 字节控制信息 + 实际图像数据组成,具体结构如下:

位置 内容 长度 作用
第 1 个元素 控制字节(Control Byte) 1 字节 标识数据类型(黑白 / 颜色)、是否为首块,用于设备解析
后续元素 图像数据块 chunkSize字节 从原始数据中截取的部分图像数据(由data.slice(i, i + chunkSize)获取)

核心:控制字节的生成逻辑

控制字节是通过按位或(| 组合两个条件判断结果生成的,具体拆解如下:

// 控制字节 = 图层标识位 + 首块标识位 (step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0) 
1. 图层标识位(高 4 位)
  • 当 step == 'bw'(黑白层)时,值为 0x0F(二进制 00001111
  • 当 step 为其他值(颜色层)时,值为 0x00(二进制 00000000
  • 作用:告诉设备当前发送的是黑白图层数据还是颜色图层数据(电子纸可能需要分层显示)。
2. 首块标识位(低 4 位)
  • 当 i == 0(第一个数据块)时,值为 0x00(二进制 00000000
  • 当 i != 0(非首块)时,值为 0xF0(二进制 11110000
  • 作用:标识当前数据块是否为整个图像的第一个块,帮助设备初始化接收缓冲区或同步数据起始点。
组合结果示例
场景 图层标识位 首块标识位 控制字节(十六进制) 二进制表示
黑白层的第一个块 0x0F 0x00 0x0F 00001111
黑白层的非第一个块 0x0F 0xF0 0xFF 11111111
颜色层的第一个块 0x00 0x00 0x00 00000000
颜色层的非第一个块 0x00 0xF0 0xF0 11110000

实际数据部分

...data.slice(i, i + chunkSize) 
  • 这部分通过数组展开运算符(...)将截取的图像数据添加到 payload 中。
  • data.slice(i, i + chunkSize) 表示从原始数据 data 的索引 i 开始,截取长度为 chunkSize 的子数据(最后一个块可能不足 chunkSize)。
  • 例如:若 data 是长度为 1000 的数组,chunkSize 为 200,则第一个 payload 的数据部分是 data[0] 到 data[199],第二个是 data[200] 到 data[399],以此类推。

为什么这样设计?

  1. 硬件协议适配:电子纸显示屏通常有固定的通信协议,要求每个数据块必须包含控制信息(如数据类型、块序号),否则设备无法正确解析。
  2. 分层显示需求:部分电子纸支持黑白 + 颜色双层显示,控制字节的图层标识位用于区分两层数据,确保设备按正确层次显示。
  3. 数据同步:首块标识位让设备知道 “这是图像的开始”,可以初始化内部状态(如清空缓冲区、重置计数器),避免数据错乱。

潜在问题与优化

  1. 类型转换风险:若 data 是 Uint8Array 等二进制数组,slice 方法返回的是新数组,... 展开可能导致性能损耗。优化方案:
    // 直接创建Uint8Array,避免数组展开
    const payload = new Uint8Array(1 + chunkSize);
    payload[0] = (step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0);
    payload.set(data.subarray(i, i + chunkSize), 1); // 使用subarray更高效

     

    控制字节可读性:可通过常量定义提升代码可读性:

    const LAYER_BW = 0x0F;
    const LAYER_COLOR = 0x00;
    const FIRST_CHUNK = 0x00;
    const NON_FIRST_CHUNK = 0xF0;
     
    const layerFlag = step === 'bw' ? LAYER_BW : LAYER_COLOR;
    const chunkFlag = i === 0 ? FIRST_CHUNK : NON_FIRST_CHUNK;
    const controlByte = layerFlag | chunkFlag;
    const payload = [controlByte, ...data.slice(i, i + chunkSize)];

     


async function write(cmd, data, withResponse = true) {
  if (!epdCharacteristic) {
    addLog("服务不可用,请检查蓝牙连接");
    return false;
  }
  let payload = [cmd];
  if (data) {
    if (typeof data == 'string') data = hex2bytes(data);
    if (data instanceof Uint8Array) data = Array.from(data);
    payload.push(...data)
  }
  addLog(bytes2hex(payload), '⇑');
  try {
    if (withResponse)
      await epdCharacteristic.writeValueWithResponse(Uint8Array.from(payload));
    else
      await epdCharacteristic.writeValueWithoutResponse(Uint8Array.from(payload));
  } catch (e) {
    console.error(e);
    if (e.message) addLog("write: " + e.message);
    return false;
  }
  return true;
}

 

蓝牙通信核心函数解析:write

这个异步函数 write 负责通过蓝牙低功耗(BLE)协议向电子纸设备发送命令和数据。它支持两种发送模式:带响应确认的可靠传输和无响应的快速传输。

函数参数与核心功能

参数说明
  • cmd:命令字节(如 EpdCmd.WRITE_IMG),标识操作类型
  • data:可选参数,要发送的具体数据(支持字符串、Uint8Array 或普通数组)
  • withResponse:可选布尔值,默认为 true,表示是否等待设备响应
核心逻辑步骤
  1. 连接状态检查
    if (!epdCharacteristic) {
      addLog("服务不可用,请检查蓝牙连接");
      return false;
    }

     

    确保蓝牙特征(epdCharacteristic)已正确获取,否则输出错误日志并返回失败

  2. 构建数据帧
    let payload = [cmd];
    if (data) {
      if (typeof data == 'string') data = hex2bytes(data);
      if (data instanceof Uint8Array) data = Array.from(data);
      payload.push(...data);
    }

     

    数据帧格式:[命令字节, ...数据内容]

    • 自动处理不同类型的输入数据:
      • 字符串:通过 hex2bytes 转换为字节数组
      • Uint8Array:转换为普通数组
      • 普通数组:直接拼接
  3. 日志记录
    addLog(bytes2hex(payload), '⇑'); 
    • 使用 bytes2hex 将数据转换为十六进制字符串并记录发送日志(带向上箭头标记)
  4. 发送数据
    if (withResponse)
      await epdCharacteristic.writeValueWithResponse(Uint8Array.from(payload));
    else
      await epdCharacteristic.writeValueWithoutResponse(Uint8Array.from(payload));

     

    • 根据 withResponse 参数选择不同的发送方式:
      • 带响应:等待设备确认(可靠传输,速度较慢)
      • 无响应:直接发送不等待确认(快速传输,可能丢包)
  5. 错误处理
    catch (e) {
      console.error(e);
      if (e.message) addLog("write: " + e.message);
      return false;
    }

     

    • 捕获并记录异常,返回失败状态

两种发送模式的差异

带响应模式(withResponse = true
  • 使用 writeValueWithResponse 方法
  • 蓝牙协议会等待设备返回确认信号(ATT Command Response)
  • 确保数据成功到达设备,但增加了传输延迟
  • 适用于关键命令(如初始化、数据同步)
无响应模式(withResponse = false
  • 使用 writeValueWithoutResponse 方法
  • 数据发送后立即返回,不等待设备确认
  • 传输速度快,但无法保证数据是否成功接收
  • 适用于批量数据传输(如图像数据),可通过交错发送优化效率

与之前代码的关联

在 writeImage 函数中,这种发送模式被策略性地使用:

if (noReplyCount > 0) {
  await write(EpdCmd.WRITE_IMG, payload, false); // 无响应模式,快速发送
  noReplyCount--;
} else {
  await write(EpdCmd.WRITE_IMG, payload, true);  // 带响应模式,确保同步
  noReplyCount = interleavedCount;
}

 

通过交替使用两种模式,既保证了传输效率,又确保了数据同步的可靠性

潜在优化建议

1. 类型转换优化

当前代码在 Uint8Array 和普通数组之间多次转换,可简化为:

// 直接操作Uint8Array,避免多次转换
const payload = new Uint8Array(1 + (data?.length || 0));
payload[0] = cmd;
if (data) {
  if (typeof data === 'string') data = hex2bytes(data);
  payload.set(data, 1); // 直接设置到Uint8Array中
}

 

2. 错误重试机制

添加自动重试逻辑,提高通信可靠性:

const MAX_RETRIES = 3;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
  try {
    // 发送代码...
    return true; // 成功发送
  } catch (e) {
    if (attempt === MAX_RETRIES - 1) {
      addLog(`发送失败(已重试${MAX_RETRIES}次): ${e.message}`);
      return false;
    }
    await new Promise(resolve => setTimeout(resolve, 100)); // 等待100ms后重试
  }
}

 

3. 并发控制

对于无响应模式,可限制最大并发数,避免设备缓冲区溢出:

// 全局计数器
let activeWrites = 0;
const MAX_CONCURRENT_WRITES = 5;
 
async function writeWithThrottle(...) {
  // 等待并发数降低
  while (activeWrites >= MAX_CONCURRENT_WRITES) {
    await new Promise(resolve => setTimeout(resolve, 50));
  }
  activeWrites++;
  
  try {
    // 发送代码...
  } finally {
    activeWrites--;
  }
}

 

if (data instanceof Uint8Array) data = Array.from(data);

这句代码的作用是Uint8Array类型的二进制数据转换为普通数组(Array),目的是统一数据处理格式,确保后续的数组操作(如push、展开运算符...)能兼容处理不同类型的输入数据。

详细解析

1. data instanceof Uint8Array
  • 判断数据类型instanceof 用于检测 data 是否为 Uint8Array 实例。
    • Uint8Array 是 JavaScript 中的一种类型化数组(TypedArray),专门用于存储 8 位无符号整数(取值范围 0-255),常用于处理二进制数据(如图像像素、蓝牙传输数据)。
    • 与普通数组(Array)相比,Uint8Array 有固定长度,且元素必须是数字,不支持普通数组的所有方法(虽然大部分通用,但为了兼容性通常会转换)。
2. Array.from(data)
  • 转换为普通数组Array.from() 方法将 Uint8Array 转换为普通的 JavaScript 数组(Array)。
    • 示例:若 data 是 Uint8Array([0x0F, 0x10, 0x20]),转换后变为 [0x0F, 0x10, 0x20](普通数组)。

为什么需要这个转换?

在代码中,后续操作需要对 data 进行数组拼接(payload.push(...data)),而 Uint8Array 虽然可以被展开(...),但在部分环境或早期 JavaScript 版本中可能存在兼容性问题。此外,普通数组的 API 更灵活(如 pushslice 等操作的行为更统一)。

  • 兼容性保障:确保 data 无论原始类型是 Uint8Array 还是普通数组,最终都以普通数组形式参与后续处理,避免因类型差异导致的意外错误。
  • 操作统一性:普通数组的 pushconcat 等方法对所有元素类型(这里是数字)的处理更一致,减少类型相关的调试成本。

示例对比

假设 data 是 Uint8Array

// 转换前:data 是 Uint8Array
console.log(data instanceof Uint8Array); // true
console.log(data.constructor.name); // "Uint8Array"
 
// 转换后:data 是普通数组
data = Array.from(data);
console.log(data instanceof Array); // true
console.log(data.constructor.name); // "Array"

 

潜在优化

如果确认运行环境支持 Uint8Array 的展开操作(现代浏览器和 Node.js 均支持),此转换并非必需。但保留转换可提高代码的兼容性,尤其对于需要适配多种设备或旧环境的场景。

若追求性能(避免额外的数组转换开销),可直接操作 Uint8Array

// 替代方案:直接将 Uint8Array 与命令合并为新的 Uint8Array
if (data instanceof Uint8Array) {
  const newPayload = new Uint8Array(1 + data.length);
  newPayload[0] = cmd;
  newPayload.set(data, 1); // 将 data 复制到新数组的第1位开始
  payload = newPayload;
}

 

4. 发送刷新命令与状态更新
// 发送刷新命令,触发电子纸物理刷新
await write(EpdCmd.REFRESH);
updateButtonStatus();
 
// 计算发送耗时并显示结果
const sendTime = (new Date().getTime() - startTime) / 1000.0;
addLog(`发送完成!耗时: ${sendTime}s`);
setStatus(`发送完成!耗时: ${sendTime}s`);
addLog("屏幕刷新完成前请不要操作。");
 
// 5秒后隐藏状态显示
setTimeout(() => {
  status.parentElement.style.display = "none";
}, 5000);

 

刷新命令EpdCmd.REFRESH 是一个特殊指令,通知电子纸控制器根据接收到的图像数据更新物理显示。
  • 时间记录:精确计算数据发送耗时,帮助用户评估系统性能。

关键函数解析

1. writeImage(data, type)

这个函数负责将图像数据分块发送到电子纸设备:

async function writeImage(data, type) {
  const chunkSize = 20; // 每包20字节(受蓝牙MTU限制)
  const cmd = type === 'bw' ? EpdCmd.WRITE_BW : 
              type === 'red' ? EpdCmd.WRITE_RED : EpdCmd.WRITE_COLOR;
  
  // 发送开始命令
  await write(cmd);
  
  // 分块发送数据
  for (let i = 0; i < data.length; i += chunkSize) {
    const chunk = data.slice(i, i + chunkSize);
    await write(chunk);
    
    // 更新进度显示
    const percent = ((i / data.length) * 100).toFixed(1);
    setStatus(`发送中: ${percent}%`);
  }
  
  // 发送结束命令
  await write(EpdCmd.END_DATA);
}

 

数据分块:将大图像数据拆分为小数据包(通常≤20 字节),适应蓝牙低功耗的传输限制

  • 命令类型:根据数据类型(黑白 / 红色 / 彩色)发送不同的命令前缀
  • 进度反馈:实时更新 UI 显示发送进度,提升用户体验
2. write(data)

这个底层函数负责通过蓝牙发送数据:

async function write(data) {
  if (!epdCharacteristic) {
    throw new Error('未连接到电子纸特征');
  }
  
  // 将数据转换为ArrayBuffer
  const buffer = data instanceof ArrayBuffer ? data : 
                 typeof data === 'number' ? new Uint8Array([data]).buffer : 
                 new TextEncoder().encode(data.toString()).buffer;
  
  // 写入数据到蓝牙特征
  return await epdCharacteristic.writeValueWithResponse(buffer);
}

 

数据格式转换:支持多种数据类型(数字、字符串、ArrayBuffer),统一转换为蓝牙可传输的格式

  • 带响应的写入:使用 writeValueWithResponse() 确保数据可靠传输

电子纸显示原理补充

电子纸(如 E Ink)与传统显示屏的区别:

  1. 双稳态特性:刷新后无需持续供电即可保持显示内容
  2. 低刷新频率:刷新过程较慢(通常需要 1-2 秒),但功耗极低
  3. 特殊驱动要求
    • 需要特定的波形驱动信号才能正确显示
    • 不同颜色模式(黑白、三色、四色)需要不同的数据格式和处理方式

优化建议

  1. 增加数据校验机制
    • 在发送大块数据后,添加 CRC 校验确保数据完整性
    • 实现错误重传机制,提高可靠性
  2. 优化刷新流程
    • 实现部分刷新功能,仅更新屏幕变化区域
    • 根据显示内容智能选择刷新模式(如快速刷新或高质量刷新)
  3. 增强用户反馈
    • 添加视觉指示器显示电子纸刷新状态
    • 提供刷新完成的声音提示或震动反馈
  4. 性能优化
    • 实现数据压缩算法,减少传输时间
    • 优化蓝牙 MTU(最大传输单元),提高单次传输数据量
请登录后发表评论

    没有回复内容