2026-01-25

改裝 電子卡尺

使用卡尺量度物件長度很簡單,但對視力不好的人來說並不容易
在下有一位同樣從事科技工作的朋友,但視力非常差
雖然科技上有很多無障礙支援,但物理世界的層面仍然相對較少,畢竟他們都不是主要消費對象
因此在下打算將一款普通的電子卡尺改裝成能夠發聲,讓我的朋友都能夠聽到量度的數值

外觀

電子卡尺 的外觀

電路板

電子卡尺 中的 印刷電路板

電路板 提供 4支引腳,允許 量度數值 通過 電子卡尺螢幕 以外的方式顯示

改裝

在下使用不同顏色的跳線焊接到引腳,以便更容易理解各個引腳的功能:

顏色 引腳 功能
紅色 VCC 1.2V 至 1.5V 電源
黃色 CLK 序列時脈
綠色 DAT 序列資料
黑色 GND 接地

電子卡尺 的外殼也需要改裝,否則跳線將無法使用

在下在外殼上對應焊接跳線的位置鑽孔,讓跳線可以引導到外殼外部

在下將 0.1寸引腳 焊接到跳線,方便連接到 麵包板 測試

工作電壓

由於 電子卡尺 原本只需要用 1粒1.5V LR44電池 就能夠驅動,但從 開發板 引導出的 電壓 通常是 5V 或 3.3V
相對於使用 1.5V 的 電子卡尺 電壓過高,使用 5V 或 3.3V 很可能會損壞 電子卡尺 的 電子零件
因此,需要將電壓調節到 1.5V 才能安全連接到電子卡尺

要將電壓由 5V 或 3.3V 調節到 1.5V ,有不少方法,最簡單的就是使用 電位器(Potentiometer) ,將電壓調節到 1.5V
另一種方法是使用 電壓分配定則(Voltage Divider Rule) , 利用兩個符合比例的 電阻器(Resistor) ,將電壓調節到 1.5V

但在下使用更懶惰的方法,直接使用 穩壓器 (Voltage Regulator) ,不論是 5V 或 3.3V 都能夠調節到 1.5V
AMS1117-1.5 是其中一種 穩壓器 , 5V 或 3.3V 都能夠穩定地調節到 1.5V

位置 引腳 功能
GND 接地
OUT 1.5V 電源輸出
VCC 2.5V 至 15V 電源輸入

在下將 AMS1117-1.5 焊接 0.1寸引腳,可以方便在 麵包板 上測試,將來亦可以方便測試其他使用 1.5V 的裝置

訊號週期

CLKDAT

使用 電子卡尺 的 資料(Data)時脈(Clock) 讀取資料
每1個訊號週期 分開6組 , 每組4個位元 , 共有24個位元 ,並以 最高有效位(Most Significant Bit (MSB)) 來表達資料
資料 及 時脈 閒置時都是 高電壓邏輯, 當 時脈 為 低電壓邏輯 時,將 資料 取樣來讀取 位元資料

時脈 保持 低電壓邏輯 200微秒 取樣,完成取樣後 保持 高電壓邏輯 100微秒
每次組取樣完成後額外保持 高電壓邏輯 400微秒,每次週期完成後額外保持 高電壓邏輯 120毫秒 ,等待下次週期

訊號功能

位元位置
23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
S ? ? U D[0:19]
位元位置 功能
0 數值第0位元
1 數值第1位元
2 數值第2位元
3 數值第3位元
4 數值第4位元
5 數值第5位元
6 數值第6位元
7 數值第7位元
8 數值第8位元
9 數值第9位元
10 數值第10位元
11 數值第11位元
12 數值第12位元
13 數值第13位元
14 數值第14位元
15 數值第15位元
16 數值第16位元
17 數值第17位元
18 數值第18位元
19 數值第19位元
20 正負值位元, 0 為 正數; 1 為 負數
21 用途不明
22 用途不明
23 單位位元, 0 為 毫米(mm) ; 1 為 寸(in)
  • 第0至19位元 為 數值資料,即 電子卡尺 量度的資料
  • 第20位元 表示 正負值, 0 為正數 ; 1 為負數
  • 第23位元 表示 單位, 0 為毫米(mm) ; 1 為寸(in)
  • 第21及22位元 則 用途不明 , 通常是 0

先將 第0至19位元 採取 數值資料
如果 單位 為 毫米,將 數值資料 除以100,就是 量度數值
而當 單位 為 寸 ,則將 數值資料 除以2000 ,才是 量度數值
考慮到需要以 浮點數(float) 表示,分別乘以 0.01 及 0.0005 會更方便
最後設定 正負符號 就是 實際的量度數值

訊號例子

例子 1

例如 000000000010011100001111
第0至19位元 數值資料 為 00000010011100001111(bin) ,即是 9999(dec)
第20位元 正負值 為 0 ,即是 正數
第23位元 單位 為 0,即是 毫米
因此,實際量度數值 為 9999 * 0.01 = 99.99 mm

例子 2

例如 100000000000100110100101
第0至19位元 數值資料 為 00000000100110100101(bin) ,即是 2469(dec)
第20位元 正負值 為 0 ,即是 正數
第23位元 單位為 1 ,即是 寸
因此,實際量度數值 為 2469 * 0.0005 = 1.2345 in

例子 3

例如 100100000000011010010100
第0至19位元 數值資料 為 00000000011010010100(bin) ,即是 1684(dec)
第20位元 正負值 為 1 ,即是 負數
第23位元 單位 為 1 ,即是 寸
因此,實際量度數值 為 -1684 * 0.0005 = -0.8420 in

使用微控制器

在下打算將 電子卡尺 改裝成能夠將 量度數值 在網頁上顯示,並將 量度數值 傳送到 宿主裝置

最初在下使用 ESP32C3 Super Mini 建立 網頁伺服器BLE HID 達到效果
但在下發現 ESP32C3 Super Mini 的儲存空間不足夠同時執行 2種操作,因此改為使用 ESP32C6 Super Mini

ESP32C6 Super Mini 的正面

ESP32C6 晶片

標示 BAT 的 LED
如果 ESP32C6 Super Mini 連接到 鋰離子電池鋰離子聚合物電池,用來顯示差電狀態

標示 15 的 LED
與 IO15 連接,高電壓邏輯 時 亮著

WS2812B RGB LED
與 IO8 連接,能夠使用 NeoPixel 等函式庫來驅動

ESP32C6 Super Mini 的背面提供能焊接 鋰離子電池 的 正負極 焊墊

引腳

USB方向(正面)
左排引腳 右排引腳
IO16/TX 1 20 5V
IO17/RX 2 19 GND
IO0 3 18 3V3
IO1 4 17 IO20
IO2 5 16 IO19
IO3 6 15 IO18
IO4 7 14 IO15/LED
IO5 8 13 IO14
IO6 9 12 IO9/BOOT
IO7 10 11 IO8/RGB
編號 引腳 方向 功能
1 TX 輸出 第16數碼引腳, UART 的 TX引腳
2 RX 輸入 第17數碼引腳, UART 的 RX引腳
3 IO0 輸入輸出 第0數碼引腳,具備 12位元 ADC
4 IO1 輸入輸出 第1數碼引腳,具備 12位元 ADC
5 IO2 輸入輸出 第2數碼引腳,具備 12位元 ADC
6 IO3 輸入輸出 第3數碼引腳,具備 12位元 ADC
7 IO4 輸入輸出 第4數碼引腳,具備 12位元 ADC
8 IO5 輸入輸出 第5數碼引腳,具備 12位元 ADC
9 IO6 輸入輸出 第6數碼引腳,具備 12位元 ADC
10 IO7 輸入輸出 第7數碼引腳
11 IO8 輸入輸出 第8數碼引腳
與 WS2812B RGB LED 連接
12 IO9 輸入輸出 第9數碼引腳
與 BOOT 連接,起動時 低電壓邏輯 載入到起動模式
13 IO14 輸入輸出 第14數碼引腳
14 IO15 輸入輸出 第15數碼引腳
與 IO15 LED 連接,高電壓邏輯 亮著
15 IO18 輸入輸出 第18數碼引腳
16 IO9 輸入輸出 第19數碼引腳
17 IO20 輸入輸出 第20數碼引腳
18 3V3 3.3V 輸出
19 GND 接地
20 5V 使用 USB供電 時,5V 輸出
不使用 USB供電 時, 5V 輸入以啟動裝置
IO12 輸入輸出 第12數碼引腳
IO13 輸入輸出 第13數碼引腳
IO21 輸入輸出 第21數碼引腳
IO22 輸入輸出 第22數碼引腳
IO23 輸入輸出 第23數碼引腳

訊號電壓

由於 電子卡尺 的 工作電壓 只有 1.5V , 因此其 訊號電壓 最高也只有 1.5V
但大多數 微控制器 的 高電壓邏輯 是 5V 或 3.3V ,因此無法將 1.5V 視為 高電壓邏輯
所以在下使用 邏輯轉換器(Logic Shifter) 將 1.5V 高電壓邏輯 提升到 5V 或 3.3V ,讓對應的 微控制器 能偵測到 高電壓邏輯

在下使用 YF08E ,一種 八通道邏輯轉換器 ,支援 1.2V 至 3.6V 訊號電壓 提升至 1.65V 至 5.5V

TXS0108E 將 YF08E 模組化,連接了電容器,方便於使用

編號 引腳 方向 功能
1 VA 輸入參考訊號電壓,範圍為 1.2V 至 3.6V
2 A1 輸入 第1輸入訊號電壓
3 A2 輸入 第2輸入訊號電壓
4 A3 輸入 第3輸入訊號電壓
5 A4 輸入 第4輸入訊號電壓
6 A5 輸入 第5輸入訊號電壓
7 A6 輸入 第6輸入訊號電壓
8 A7 輸入 第7輸入訊號電壓
9 A8 輸入 第8輸入訊號電壓
10 OE 高電壓邏輯時啟動
11 GND 接地
12 B8 輸出 第8輸出訊號電壓
13 B7 輸出 第7輸出訊號電壓
14 B6 輸出 第6輸出訊號電壓
15 B5 輸出 第5輸出訊號電壓
16 B4 輸出 第4輸出訊號電壓
17 B3 輸出 第3輸出訊號電壓
18 B2 輸出 第2輸出訊號電壓
19 B1 輸出 第1輸出訊號電壓
20 VB 輸出參考訊號電壓,範圍為 1.65V 至 5.5V

使用 CircuitPython

最初在下使用 Arduino IDE 編寫程式,但無法啟用 ESP32C6 Super Mini的 BLE HID 功能
由於 CircuitPython 擁有 BLE HID 函式庫,因此在下嘗試使用 CircuitPython

然而 ESP32系列 不會像 Raspberry Pi Pico系列一般,在 起動模式 時被宿主當作 USB儲存裝置,而是需要使用 ESPTool 來寫入 CircuitPython
可以到 https://github.com/espressif/esptool/releases/latest 下載最新版本的 ESPTool
另外,如果在 Arduino IDE 已經安裝了 ESP32 相關的開發工具, ESPTool 也會包含在內

先將 ESP32C6 Super Mini 以 起動模式 啟動

直接使用 ESPTool

如果一直使用 Arduino IDE 開發 ESP32 相關的程式,最簡單的方式就是直接使用 ESPTool,在 Terminal 中輸入:

"path-of-esptool" --chip esp32c6 --port "path-of-esp32c6-port" erase-flash

清除 ESP32C6 Super Mini 的資料

"path-of-esptool" --chip esp32c6 --port "path-of-esp32c6-port" --baud 115200 write-flash -z 0x0 "path-of-circuitpython-for-esp32c6.bin"

寫入 CircuitPython 到 ESP32C6 Super Mini

留意,較舊的 ESPTool 的 清除指令寫入指令 會使用 erase_flashwrite_flash ;而較新的版本則會使用 erase-flashwrite-flash

使用 Adafruit WebSerial ESPTool

如果沒有使用 Arduino IDE 或 ESPTool ,可以前往 https://adafruit.github.io/Adafruit_WebSerial_ESPTool/
由於 Adafruit WebSerial ESPTool 需要使用 WebSerial API ,因此需要使用支援 WebSerial API 的網頁瀏覽器
目前只有 基於 Chromium 的網頁瀏覽器 支持 WebSerial API

如果網頁瀏覽器支援 WebSerial API ,當按下 Adafruit WebSerial ESPTool 頁面右上角的 Connect按鈕
將會出現網頁要求與序列埠連線的提示,可以選取 ESP32C6 Super Mini 的 序列埠 (Serial Port)(不同作業系統的描述名稱會有所不同)

當 Adafruit WebSerial ESPTool 成功連接到 ESP32C6 Super Mini 後,介面將顯示相應的操作功能

如果需要將 韌體 寫入 ESP32C6 Super Mini,請先清除 ESP32C6 Super Mini 的所有資料
按下 Erase按鈕 後,網頁將顯示清除所有資料的提示信息,按下確認後便會開始清除資料

資料清除後, Adafruit WebSerial ESPTool 的 終端模擬器 將顯示清除完成的提示

前往 https://circuitpython.org/board/makergo_esp32c6_supermini/ 下載支援 ESP32C6 Super Mini 的韌體
該頁面提供了最新的 穩定版本開發版本 的 韌體 下載選項,選擇適合的版本來使用

回到 Adafruit WebSerial ESPTool 後,在 Choose a file... 選擇需要寫入到 ESP32C6 Super Mini 的韌體
後按下 Program按鈕 ,便會開始將韌體寫入到 ESP32C6 Super Mini

使用 Thonny

Run > Configure Interpreter ... > Interpreter 選擇 CircuitPython (Generic) 後,按 (esptool)

  • Target Port 選擇 ESP32C6 Super Mini 的 連接埠
  • CircuitPython family 選擇 ESP32C6
  • varient 選擇 Maker Go ESP32C6 Supermini
  • version 建議選擇 最新穩定版

然後按 Install按鈕

Thonny 會從 circuitpython.org 下載對應的 CircuitPython 韌體,並安裝到 ESP32C6 Super Mini 上

線路原型

由於 ESP32C6 Super Mini 的參考訊號電壓 是 3.3V 不是 5V
因此連接到 邏輯轉換器 的 輸出參考訊號電壓 必須使用 3.3V ,否則可能會損壞

實際線路

基本上與線路原型相同

編寫程式

由於 ESP32系列 不會當作 USB儲存裝置 ,因此使用支援 Python REPL 的工具,最簡單同樣使用 Thonny 即可

過往編寫程式時,通常作為主機控制從機,因此只需了解從機接收訊號的要求後,在主機編寫符合從機要求的訊號,便能控制從機運作
然而這次的操作則是相反,電子卡尺 作為主機向從機發送訊號,當從機接收到訊號後,需要解析這些訊號並轉換成資料,讓使用者能夠閱讀

雖然電子卡尺的訊號週期已大致解析,但在現實中無法做到完全精準的操作
例如,在每次 訊號週期 開始前,時脈 保持 高電壓邏輯 120毫秒,似乎可以使用 delay(120) 來延遲每次訊號週期
然而,實際上 時脈 會有些微的時間差距,而 delay(120) 也不會完全精準地延遲 120毫秒
因此需要改變操作邏輯

當 訊號週期 的狀態為 低電壓邏輯 時,需要持續檢查
一旦 時脈 轉為 高電壓邏輯 便開始計時,當 時脈 再次降為 低電壓邏輯 時,計算 高電壓邏輯 的持續時間
如果持續時間超過 120毫秒,則開始 訊號週期;否則再次檢查
由於 訊號週期 中的 高電壓邏輯 及 低電壓邏輯 都不會超過 120毫秒,因此不需要計算持續時間
只需要確保在 時脈 由 高電壓邏輯 轉為 低電壓邏輯 時向 資料 取樣
完成 24次取樣 便可結束 訊號週期

測試程式
# digital_caliper_reader.py
from digitalio import DigitalInOut, Direction, Pull
from analogio import AnalogIn
from time import monotonic
class DigitalCaliperReader:
	RETRY = 256
	BYTE_LENGTH = 8
	DATA_LENGTH = BYTE_LENGTH * 3
	def __init__(self, clk, dat, led = None, normal = False):
		self.__value = 0
		self.__clk = DigitalInOut(clk)
		self.__dat = DigitalInOut(dat)
		self.__clk.direction = Direction.INPUT
		self.__dat.direction = Direction.INPUT
		#self.__clk = AnalogIn(clk)
		#self.__dat = AnalogIn(dat)
		self.__normal = normal
		if led == None:
			self.__led = None
		else:
			self.__led = DigitalInOut(led)
			self.__led.direction = Direction.OUTPUT
			self.__led.value = self.__normal
	def update(self):
		self.__value = 0
		if self.__led != None:
			self.__led.value = not self.__normal
		for retry in range(DigitalCaliperReader.RETRY):
			while not self.__clk.value: pass
			previous_millis = monotonic()
			while self.__clk.value: pass
			if (monotonic() - previous_millis) > 0.1:
				for i in range(DigitalCaliperReader.DATA_LENGTH):
					while self.__clk.value: pass
					while not self.__clk.value: pass
					if self.__dat.value: self.__value |= (1 << i)
				break
		if self.__led != None:
			self.__led.value = self.__normal
	def get_raw_value(self):
		return self.__value
	def get_value(self):
		return (self.get_raw_value() & 0x0FFFFF)
	def is_negative(self):
		return (self.get_raw_value() & 0x100000) > 0
	def is_inch(self):
		return (self.get_raw_value() & 0x800000) > 0
	def get_c_value(self):
		return self.get_value() * (0.0005 if self.is_inch() else 0.01) * (-1 if self.is_negative() else 1)
	def get_formatted_string(self):
		return ("{:.4f} in" if self.is_inch() else "{:.2f} mm").format(self.get_c_value())
	def get_debug_string(self):
		template = "{{:0{}b}}".format(DigitalCaliperReader.DATA_LENGTH)
		string = template.format(self.__value)
		return ','.join(string[i : i + DigitalCaliperReader.BYTE_LENGTH] for i in range(0, DigitalCaliperReader.DATA_LENGTH, DigitalCaliperReader.BYTE_LENGTH))
# code.py
import board, time
from digital_caliper_reader import DigitalCaliperReader
digital_caliper_reader = DigitalCaliperReader(board.IO19, board.IO20)
previous_string = ""
while True:
	digital_caliper_reader.update()
	current_string = digital_caliper_reader.get_formatted_string()
	if previous_string != current_string:
		previous_string = current_string
		print("\r{:10}".format(previous_string), end = "")
	time.sleep(0.001)

在下製作類別來方便 讀取 電子卡尺 的訊息,並因應不同狀態製作不同讀取方法,讓應用或修改時更加方便

測試效果,將 電子卡尺 的 量度數值 在 Thonny 的指令列上顯示

安裝函式庫

CircuitPython 官方網站提供大量函式庫方便開發者開發韌體,否則開發者便需要先開發驅動程式才能開發韌體
可以到 https://circuitpython.org/libraries 根據所安裝 CircuitPython 的版本,下載對應的函式庫
在下建議下載 mpy版本 的函式庫,雖然 mpy版本 不能修改,但相比 py版本 佔據空間會較少
由於開發板的儲存空間不多,不可能同時安裝所有函式庫,根據開發需要安裝對應的函式庫
例如在下這個專案要使用 CircuitPython裝置 建立網頁伺服器便需要安裝 adafruit_httpserver
再使用 BLE HID 便需要安裝 adafruit_ble, adafruit_bus_device, adafruit_hid
將需要使用的函式庫安裝到 CircuitPython裝置 的 lib目錄 便可以使用

先在 Thonny 載入到 CircuitPython裝置 的 lib目錄
然後從 宿主裝置 上載需要使用的函式庫到 CircuitPython裝置 的 lib目錄

在網頁顯示量度數值

既然能夠將 量度數值 在 Thonny 的指令列上顯示,就可以將 量度數值 以其他方式顯示

# code.py
from board import IO19, IO20
from time import sleep
from wifi import radio
from socketpool import SocketPool
from adafruit_httpserver.methods import GET
from adafruit_httpserver.server import Server
from adafruit_httpserver.response import Response
from digital_caliper_reader import DigitalCaliperReader
digital_caliper_reader = DigitalCaliperReader(IO19, IO20)
radio.tx_power = 8.5
radio.start_ap(ssid = "ESP32C6 Digital Caliper", password = "12345678")
socket_pool = SocketPool(radio)
server = Server(socket_pool)
@server.route("/", GET)
def server_root(request: Request):
	html = """<!DOCTYPE html>
<html lang="en">
	<head>
		<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<title>ESP32C6 Digital Caliper Reader</title>
		<style>
#container {
	font-size: 200px;
	font-family: "Courier New";
}
		</style>
		<script>
// <!--
const MODE_REQUEST_ANIMATION_FRAME = 0;
const MODE_SET_TIMEOUT = 1;
const MODE_SET_INTERVAL = 2;
const UPDATE_MODE = MODE_SET_TIMEOUT;
const INTERVAL = 1000;
var value = "";
function sendRequest() {
	window.fetch("/formatted-string").then(function(response) {
		return response.text();
	}).then(function(data) {
		if (value != data) {
			value = data;
			document.getElementById("other").focus();
			let container = document.getElementById("container");
			container.textContent = value;
			container.focus();
		}
	}).then(function(error) {
		if (error != undefined) {
			console.log(error);
		}
	});
	if (UPDATE_MODE == MODE_REQUEST_ANIMATION_FRAME) {
		window.requestAnimationFrame(function() {
			sendRequest();
		});
	} else if (UPDATE_MODE == MODE_SET_TIMEOUT) {
		window.setTimeout(function() {
			sendRequest();
		}, INTERVAL);
	}
}
window.addEventListener("load", function(loadEvent) {
	if (UPDATE_MODE == MODE_REQUEST_ANIMATION_FRAME) {
		window.requestAnimationFrame(function() {
			sendRequest();
		});
	} else if (UPDATE_MODE == MODE_SET_TIMEOUT) {
		window.setTimeout(function() {
			sendRequest();
		}, INTERVAL);
	} else if (UPDATE_MODE == MODE_SET_INTERVAL) {
		window.setInterval(function() {
			sendRequest();
		}, INTERVAL);
	}
});
// -->
		</script>
	</head>
	<body>
		<span id="other" tabindex="0"></span><div id="container" tabindex="0"></div>
	</body>
</html>"""
	return Response(request, html, content_type = "text/html")
@server.route("/formatted-string", GET)
def server_formatted_string(request: Request):
	digital_caliper_reader.update()
	return Response(request, digital_caliper_reader.get_formatted_string(), content_type = "text/plain")
@server.route("/debug", GET)
def server_debug(request: Request):
	digital_caliper_reader.update()
	return Response(request, digital_caliper_reader.get_debug_string(), content_type = "text/plain")
server.start(str(radio.ipv4_address_ap), port = 80)
while True:
	server.poll()
	sleep(0.001)

測試在網頁上顯示 電子卡尺 量度數值 的效果
閣下應該會發現 讀取 的 量度數值 不太穩定,即使沒有移動 電子卡尺 的 刻度計,量度數值 仍會變化
甚至移動 刻度計 時,顯示的 量度數值 還會超越 量度範圍

在下使用 Javascriptwindow.requestAnimationFrame 不斷讀取 電子卡尺 的 量度數值
window.requestAnimationFrame 理論上會盡量以 60FPS 的速度執行 回呼功能 (Callback Function)
並根據網頁瀏覽器的渲染速度、硬件資源自動調節每次更新的速度

不過讀取 電子卡尺 的 量度數值 速度其實不需要達到 60FPS 甚至 1毫秒 的更新速度
因此在下都保留使用 window.setTimeoutwindow.setInterval 的選項 及 調整更新速度的設定
不竟不是遊戲需要實時更新,其實更新間距是1秒至3秒 其實都是合理的更新速度

使用 BLE HID 傳送 量度數值
# code.py
from digitalio import DigitalInOut, Direction, Pull
from board import IO19, IO20, IO18, IO15
from time import sleep
from wifi import radio
from socketpool import SocketPool
from adafruit_httpserver.methods import GET
from adafruit_httpserver.server import Server
from adafruit_httpserver.response import Response
from adafruit_ble import BLERadio
from adafruit_ble.services.standard.hid import HIDService
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from _bleio import adapter
from digital_caliper_reader import DigitalCaliperReader
BTN = DigitalInOut(IO18)
BTN.direction = Direction.INPUT
BTN.pull = Pull.UP
BTN_value = BTN.value
LED = DigitalInOut(IO15)
LED.direction = Direction.OUTPUT
LED.value = False
digital_caliper_reader = DigitalCaliperReader(IO19, IO20)
radio.tx_power = 8.5
radio.start_ap(ssid = "ESP32C6 Digital Caliper", password = "12345678")
socket_pool = SocketPool(radio)
server = Server(socket_pool)
@server.route("/", GET)
def server_root(request: Request):
	html = """<!DOCTYPE html>
<html lang="en">
	<head>
		<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<title>ESP32C6 Digital Caliper Reader</title>
		<style>
#container {
	font-size: 200px;
	font-family: "Courier New";
}
		</style>
		<script>
// <!--
const MODE_REQUEST_ANIMATION_FRAME = 0;
const MODE_SET_TIMEOUT = 1;
const MODE_SET_INTERVAL = 2;
const UPDATE_MODE = MODE_SET_TIMEOUT;
const INTERVAL = 1000;
var value = "";
function sendRequest() {
	window.fetch("/formatted-string").then(function(response) {
		return response.text();
	}).then(function(data) {
		if (value != data) {
			value = data;
			document.getElementById("other").focus();
			let container = document.getElementById("container");
			container.textContent = value;
			container.focus();
		}
	}).then(function(error) {
		if (error != undefined) {
			console.log(error);
		}
	});
	if (UPDATE_MODE == MODE_REQUEST_ANIMATION_FRAME) {
		window.requestAnimationFrame(function() {
			sendRequest();
		});
	} else if (UPDATE_MODE == MODE_SET_TIMEOUT) {
		window.setTimeout(function() {
			sendRequest();
		}, INTERVAL);
	}
}
window.addEventListener("load", function(loadEvent) {
	if (UPDATE_MODE == MODE_REQUEST_ANIMATION_FRAME) {
		window.requestAnimationFrame(function() {
			sendRequest();
		});
	} else if (UPDATE_MODE == MODE_SET_TIMEOUT) {
		window.setTimeout(function() {
			sendRequest();
		}, INTERVAL);
	} else if (UPDATE_MODE == MODE_SET_INTERVAL) {
		window.setInterval(function() {
			sendRequest();
		}, INTERVAL);
	}
});
// -->
		</script>
	</head>
	<body>
		<span id="other" tabindex="0"></span><div id="container" tabindex="0"></div>
	</body>
</html>"""
	return Response(request, html, content_type = "text/html")
@server.route("/formatted-string", GET)
def server_formatted_string(request: Request):
	digital_caliper_reader.update()
	return Response(request, digital_caliper_reader.get_formatted_string(), content_type = "text/plain")
@server.route("/debug", GET)
def server_debug(request: Request):
	digital_caliper_reader.update()
	return Response(request, digital_caliper_reader.get_debug_string(), content_type = "text/plain")
adapter.erase_bonding()
ble_radio = BLERadio()
ble_radio.name = "ESP32C6 BLE HID"
hid = HIDService()
keyboard = Keyboard(hid.devices)
keyboard_layout = KeyboardLayoutUS(keyboard)
advertisement = ProvideServicesAdvertisement(hid)
advertisement.appearance = 0x03C1
ble_radio.start_advertising(advertisement)
server.start(str(radio.ipv4_address_ap), port = 80)
while True:
	if ble_radio.connected:
		if not BTN_value and BTN.value:
			pass
		elif not BTN_value and not BTN.value:
			BTN_value = True
			digital_caliper_reader.update()
			string = "{}{}".format(digital_caliper_reader.get_formatted_string(), "\n")
			keyboard_layout.write(string)
			print("<press> - ".format(string))
		elif BTN_value and not BTN.value:
			pass
		elif BTN_value and BTN.value:
			BTN_value = False
			print("<release>")
		if not LED.value:
			LED.value = True
	else:
		if LED.value:
			LED.value = False
	server.poll()
	sleep(0.001)

補充資料

最後測試將 ESP32C6 Super Mini 設定成 BLE HID 鍵盤,令 量度數值 可以經 藍牙HID 直接傳送到 宿主裝置
如果需要量度多個物件時,使用者可以連接量度,不需要放下物件或卡尺

在下使用測試電路板,再將整個電路板黏到電子卡尺背後
並加入電池,讓改裝後的 電子卡尺 能夠獨立運作

在下為了按下按鈕更加方便,不將按鈕焊接到測試電路板的正面,而是焊接到測試電路板的側旁

其實市面上是有將刻度突顯讓視障人士能觸摸刻度來讀取量度數值的卡尺
亦有具備 藍牙HID 功能的 電子卡尺 ,能將 量度數值 傳送已配對的宿主裝置
但單純將刻度突顯,沒有額外功能的卡尺,已經需要 超過100港元,而具備 藍牙HID 的 電子卡尺 通常都超過 400港元
雖然這個價錢普遍社會人士都能夠負擔,但在下還是嘗試自行改裝
除了能夠節省成本,亦能自訂功能,可以更貼合特定需要

不過要自行改裝還要留意部分電子卡尺的電路板沒有輸出引腳,亦即是無法自行改裝
但這些電子卡尺並無法標明有否提供輸出引腳,但在下發現當量度精度只有 0.1毫米 都沒有輸出引腳
而精度為 0.01毫米 的 電子卡尺 才提供輸出引腳,因此在下認為如果想自行改裝電子卡尺,需要選擇精度為 0.01毫米 的電子卡尺

另外操作上,藍牙HID 功能仍然有暫時不明問題未能解決
當宿主裝置與 ESP32C6 的 藍牙HID 配對後,是能夠正常使用,但取消配對、中斷後則無法再次連接
必須重新啟動 ESP32C6 才能再次運作,不過由於 ESP32C6 提供重新啟動按鈕,不需要中斷電源
因此,暫時使用替補方法處理,將來再仔細研究問題的成因再完整地解決問題

修改版本
# code.py
from digitalio import DigitalInOut, Direction, Pull
from board import IO0, IO1, IO2, IO15
from time import sleep
from wifi import radio
from socketpool import SocketPool
from adafruit_httpserver.methods import GET
from adafruit_httpserver.server import Server
from adafruit_httpserver.response import Response
from adafruit_ble import BLERadio
from adafruit_ble.services.standard.hid import HIDService
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from _bleio import adapter
from digital_caliper_reader import DigitalCaliperReader
BTN = DigitalInOut(IO2)
BTN.direction = Direction.INPUT
BTN.pull = Pull.UP
BTN_value = BTN.value
LED = DigitalInOut(IO15)
LED.direction = Direction.OUTPUT
LED.value = False
digital_caliper_reader = DigitalCaliperReader(IO1, IO0)
radio.tx_power = 8.5
radio.start_ap(ssid = "ESP32C6 Digital Caliper", password = "12345678")
socket_pool = SocketPool(radio)
server = Server(socket_pool)
@server.route("/", GET)
def server_root(request: Request):
	html = """<!DOCTYPE html>
<html lang="en">
	<head>
		<!--meta name="viewport" content="width=device-width, initial-scale=1.0"/-->
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<title>ESP32C6 Digital Caliper Reader</title>
		<style>
#container {
	font-size: 144px;
	font-family: "Courier New";
}
		</style>
		<script>
// <!--
const POLLING_INTERVAL = 500;
const UPDATE_INTERVAL = 1000;
let previous_string = "";
let last_fetched = "";
let stable_since = 0;
async function sendRequest() {
	let response = await window.fetch("/formatted-string");
	let current_string = await response.text();
	const NOW = Date.now();
	if (current_string != last_fetched) {
		last_fetched = current_string;
		stable_since = NOW;
	} else if (NOW - stable_since > UPDATE_INTERVAL) {
		if (previous_string != current_string) {
			previous_string = current_string;
			document.getElementById("other").focus();
			const container = document.getElementById("container");
			container.textContent = current_string;
			container.focus();
		}
	}
}
window.addEventListener("load", function(loadEvent) {
	window.setInterval(sendRequest, POLLING_INTERVAL);
});
// -->
		</script>
	</head>
	<body>
		<span id="other" tabindex="0"></span><div id="container" tabindex="0">0.00 mm</div>
	</body>
</html>"""
	return Response(request, html, content_type = "text/html")
@server.route("/formatted-string", GET)
def server_formatted_string(request: Request):
	digital_caliper_reader.update()
	return Response(request, digital_caliper_reader.get_formatted_string(), content_type = "text/plain")
@server.route("/debug", GET)
def server_debug(request: Request):
	digital_caliper_reader.update()
	return Response(request, digital_caliper_reader.get_debug_string(), content_type = "text/plain")
adapter.erase_bonding()
ble_radio = BLERadio()
ble_radio.name = "ESP32C6 BLE HID"
hid = HIDService()
keyboard = Keyboard(hid.devices)
keyboard_layout = KeyboardLayoutUS(keyboard)
advertisement = ProvideServicesAdvertisement(hid)
advertisement.appearance = 0x03C1
ble_radio.start_advertising(advertisement)
server.start(str(radio.ipv4_address_ap), port = 80)
while True:
	if ble_radio.connected:
		if not BTN_value and BTN.value:
			pass
		elif not BTN_value and not BTN.value:
			BTN_value = True
			digital_caliper_reader.update()
			string = "{}{}".format(digital_caliper_reader.get_formatted_string(), "\n")
			keyboard_layout.write(string)
			print("<press> - ".format(string))
		elif BTN_value and not BTN.value:
			pass
		elif BTN_value and BTN.value:
			BTN_value = False
			print("<release>")
		if not LED.value:
			LED.value = True
	else:
		if LED.value:
			LED.value = False
	server.poll()
	sleep(0.001)

在下將改裝後的電子卡尺給朋友測試,他建議不需要將量度資料實時更新,因為中途的變化並不重要,
等待量度資料固定後等待超過1秒,才更新顯示的資料,畫面便不需要顯示未固定的資料,而且亦能夠避免顯示錯誤內容。

總結

訊號電壓 是這個專案的第一難題
由於 電子卡尺 使用 1.5V供電,因此 訊號電壓 最高只有1.5V ,只有使用 3.3V供電 的 ESP32C6 Super Mini 的一半
這不足以讓 數碼輸入 識別為 高電壓邏輯

在下最初打算採用折衷方法,使用 類比輸入 來模擬 數碼輸入,測量的 訊號電壓 大約在 29000 至 30000 之間
理論上,當 類比輸入 超過 28000 時,可以視為 高電壓邏輯

然而,結果完全錯誤,原因在於 ESP32C6 的 類比輸入 需要約 100微秒 來擷取訊號,而 電子卡尺 的 訊號 只有約 20微秒
由於 類比輸入 的速度比 電子卡尺 的 訊號 慢,因此無法正確讀取資料,因此在下改用 邏輯轉換器 讀取 數碼輸入

訊號週期 是這個專案的第二難題
截至發佈這篇文章,在下仍未找到一份完整關於讀取訊號的技術文件,因此只能估計及從網上尋找資料
網上有不少相似的專案,都是將類似的 電子卡尺 改裝,不過各個專案的說明內容都各有些微的差別
因此仍然無法確定正確的讀取方法

參考資料

文章內容經由 AI 協助訂正

沒有留言 :

張貼留言