electron.vite + better-sqlite3 + serialport 完整使用教程
electron.vite + better-sqlite3 + serialport 完整使用教程
- 主要使用依赖/框架说明
- 1.electron.vite项目安装搭建
- 创建项目
- 效果
- 基础项目配置
- 自定义顶部栏
- 建立子窗
- 2.better-sqlite3 使用
- 安装
- 封装
- 第一步观察环境创建数据库
- 第二步创建版本表/获取版本/更新数据库
- 第三步创建任务表
- 增删改查事件注册
- 第四步数据库使用方法封装
- 第五步使用
- 3.serialport使用
- 模拟串口以及调试工具
- 安装
- 封装
- 使用
主要使用依赖/框架说明
框架/插件 | 说明 | 官方地址 |
---|---|---|
electron.vite | 基于vite的脚手架用于开发桌面端 | https://cn.electron-vite.org/ |
better-sqlite3 | 本地化数据库 | https://www.npmjs.com/package/better-sqlite3 |
serialport | 设备串口依赖 | https://serialport.io/docs/ |
1.electron.vite项目安装搭建
创建项目
npm create @quick-start/electron@latest
创建项目可能会失败可以尝试使用cnpm 淘宝镜像市场安装
cnpm create @quick-start/electron@latest
✔ Project name: … <electron-app>
✔ Select a framework: › vue
✔ Add TypeScript? … No / Yes
✔ Add Electron updater plugin? … No / Yes
✔ Enable Electron download mirror proxy? … No / Yes
Scaffolding project in ./<electron-app>...
Done.
npm install
npm run dev
效果
这几项目启动完毕
基础项目配置
修改 electron-builder 配置文件
appId: com.electron.app
productName: 项目名称
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
icon: build/icon.ico
# 配置文件示例,包含输入验证和异常处理逻辑
target:
-
target: "nsis" # 目标名称,必须为字符串
arch: ["x64"] # 架构列表,必须为非空列表
executableName: text33
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
oneClick: false # 设置为 false 以提供安装类型选择界面,允许用户选择是否创建桌面图标,允许用户选择安装路径
perMachine: true # 设置为 true 将使安装程序默认为所有用户安装应用,这需要管理员权限
allowToChangeInstallationDirectory: true # 如果设置为 true,安装程序将允许用户更改安装目录
allowElevation: true # 一般情况下,此字段不会被直接使用,权限提升主要依赖于 perMachine 的设定。当perMachine为true,安装程序会请求管理员权限
deleteAppDataOnUninstall: true # 如果设置为 true,卸载程序将删除AppData中的所有程序数据
createStartMenuShortcut: true # 如果设置为 true,安装程序将在开始菜单中创建程序快捷方式
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
自定义顶部栏
\src\main\index.ts 修改
效果需要重新运行一下
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
show: false, // 窗口是否可见
frame: false, // 隐藏标题栏
autoHideMenuBar: true,
// titleBarStyle: 'hidden',
// titleBarOverlay: {
// color: "#2f3241",
// symbolColor: "#74b1be",
// height: 80,
// },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
创建titlebar.ts文件用于控制头部
获取植入window体系中
\src\main\titlebar.ts
文件内容
import { app, ipcMain, BrowserWindow } from 'electron'
app.whenReady().then(() => {
ipcMain.on('minimize', (event) => {
const window = BrowserWindow.fromWebContents(event.sender)
// 最小化窗口
// @ts-ignore (define in dts)
window.minimize()
})
ipcMain.on('maximize', (event) => {
const window = BrowserWindow.fromWebContents(event.sender)
// @ts-ignore (define in dts)
if (window.isMaximized()) {
// @ts-ignore (define in dts)
window.unmaximize()
} else {
// @ts-ignore (define in dts)
window.maximize()
}
})
ipcMain.on('close', (event) => {
const window = BrowserWindow.fromWebContents(event.sender)
// @ts-ignore (define in dts)
window.close()
// 检查如果所有窗口都关闭了,退出应用
// if (BrowserWindow.getAllWindows().length === 0) {
// app.quit();
// }
})
})
在index.ts 引入
import "./titlebar";
/src/preload/index.ts 中定义
import { ipcRenderer, contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {
minimize: (): void => ipcRenderer.send('minimize'),
maximize: (): void => ipcRenderer.send('maximize'),
close: (): void => ipcRenderer.send('close')
}
在页面中使用
function minimize() {
console.log('minimize', window.api)
// 设置窗口最小化
window.api.minimize()
}
function maximize() {
window.api.maximize()
}
function close() {
window.api.close()
}
建立子窗
\src\main 建立一个文件夹
大致情况如下
otherWindows.ts文件内容
import { BrowserWindow, shell } from 'electron'
import { is } from '@electron-toolkit/utils'
import { join } from 'path'
import icon from '../../../resources/icon.png?asset'
let newWindow //在顶部定义一个变量
export const otherWindows = (): void => {
if (newWindow) {
// 是否是最小化
if (newWindow.isMinimized()) {
newWindow.restore()
}
newWindow.focus() // 存在 则聚焦
return
}
// 不存在 则创建
newWindow = new BrowserWindow({
width: 312,
height: 422,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/about.js'),
sandbox: false
}
})
newWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
newWindow.on('ready-to-show', () => {
newWindow.show()
})
// 关闭清理
newWindow.on('closed', () => {
newWindow = null
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
console.log(
"process.env['ELECTRON_RENDERER_URL']",
process.env['ELECTRON_RENDERER_URL'] + '/about.html'
)
newWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/about.html')
} else {
newWindow.loadFile(join(__dirname, '../renderer/about.html'))
}
}
about.js 文件内容
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}
renderer文件夹整理
核心代码
// 唤起新窗口
const openWindow = (): void => {
window.electron.ipcRenderer.send('create-new-window')
}
效果展示
关键信息后续会用到
ipcMain.handle()
ipcMain.on()
都属于事件注册
on对应send,不返回
handle对应invoke,返回
2.better-sqlite3 使用
安装
npm i better-sqlite3 -S
数据库可视化 SQLiteStudio https://github.com/pawelsalawa/sqlitestudio/releases
better-sqlite3 https://www.npmjs.com/package/better-sqlite3
封装
修改 electron-builder.config,用于减少包的体积,过滤掉未使用的代码
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
- '!**/better-sqlite3/{deps/**/*,src/**/*}'
在/src/main中新建一个splie3.ts文件
import Database from 'better-sqlite3' // 用于操作 SQLite 数据库的库
import { app, ipcMain } from 'electron' // 用于 Electron 应用的全局功能
import path from 'path' // 用于处理和操作文件路径的模块
import fs from 'fs'
let db // 声明一个变量用来存储数据库实例
第一步观察环境创建数据库
// 判断当前环境是否是开发环境
const databasePath = path.join(app.getPath('userData'), 'database')
// 确保数据库文件夹存在,如果不存在则创建它
if (!fs.existsSync(databasePath)) {
fs.mkdirSync(databasePath, { recursive: true })
}
// 初始化数据库并创建或打开指定路径的 SQLite 数据库文件
db = new Database(path.join(databasePath, 'uploadfile.db'), {
verbose: console.log
})
// 设置数据库的日志模式为 WAL(写时日志)模式,提高性能
db.pragma('journal_mode = WAL')
第二步创建版本表/获取版本/更新数据库
// 创建版本表
function createVersionTable(): void {
const createVersionTableQuery = `
CREATE TABLE IF NOT EXISTS version (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version INTEGER NOT NULL
);
`
db.prepare(createVersionTableQuery).run()
// 检查是否有版本记录,若没有,则插入默认版本 1
const currentVersion = getCurrentDatabaseVersion()
if (!currentVersion) {
const insertVersionQuery = `INSERT INTO version (version) VALUES (?);`
const stmt = db.prepare(insertVersionQuery)
stmt.run(1) // 默认插入版本 1
}
}
// 获取当前数据库版本
function getCurrentDatabaseVersion(): void | number {
const selectVersionQuery = `SELECT version FROM version ORDER BY id DESC LIMIT 1;`
const stmt = db.prepare(selectVersionQuery)
const result = stmt.get()
return result ? result.version : null // 默认返回旧版本(1)
}
// 检查是否需要更新数据库
function updateDatabase(currentVersion): void {
console.log(`Updating database from version ${currentVersion} to ${DB_VERSION}`)
if (currentVersion === 1) {
// 执行 1 -> 2 的更新操作
updateToVersion2()
}
// 更新数据库版本记录
const updateVersionQuery = `
INSERT INTO version (version) VALUES (?);
`
const stmt = db.prepare(updateVersionQuery)
stmt.run(DB_VERSION)
console.log(`Database updated to version ${DB_VERSION}`)
}
第三步创建任务表
// 创建任务列表表
function createTable(): void {
const createTableQuery = `
CREATE TABLE IF NOT EXISTS todo_list (
name TEXT,
created_at INTEGER,
id INTEGER PRIMARY KEY AUTOINCREMENT
);
`
// 执行创建表的 SQL 语句
db.prepare(createTableQuery).run()
}
增删改查事件注册
查
// 在 Electron 的主进程中注册一个 IPC 事件处理器
ipcMain.handle('db_query', async (_, query, params = []) => {
const stmt = db.prepare(query) // 准备 SQL 查询
return stmt.all(...params) // 执行查询并返回结果
})
// 异步获取任务数据
ipcMain.on('db_tasks_sync_get_by_user_role', (event, name) => {
let result = getTasksByUserRole<{
name: string
todo_id: number
created_at: number
}>(name)
result = result.map((task) => {
const newItem = { ...task }
newItem.name = newItem.name ? JSON.parse(newItem.name) : null
newItem.todo_id = newItem.todo_id ? Number(newItem.todo_id) : NaN
newItem.created_at = newItem.created_at ? Number(newItem.created_at) : NaN
return newItem
})
event.returnValue = result
})
// 根据 name 查询所有任务数据
export function getTasksByUserRole(name): void {
const selectQuery = `SELECT * FROM todo_list WHERE name = ?;`
const stmt = db.prepare(selectQuery)
return stmt.all(name) // 执行查询并返回结果
}
// 异步获取任务数据
ipcMain.on('db_tasks_sync_get', (event) => {
let result = getTasks<{
name: string
todo_id: number
created_at: number
}>()
result = result.map((task) => {
const newItem = { ...task }
newItem.name = newItem.name ? JSON.parse(newItem.name) : null
newItem.todo_id = newItem.todo_id ? Number(newItem.todo_id) : NaN
newItem.created_at = newItem.created_at ? Number(newItem.created_at) : NaN
return newItem
})
event.returnValue = result
})
// 示例:查询任务数据
export function getTasks<T>(): T[] {
const selectQuery = `SELECT * FROM todo_list;`
const stmt = db.prepare(selectQuery)
return stmt.all()
}
增
// 插入单条数据
ipcMain.handle('db_task_sync_insert', (task) => {
try {
return insertTask(task)
} catch (error) {
console.error(error)
return null
}
})
// 插入任务数据
export function insertTask(task): number {
const insertQuery = `
INSERT INTO todo_list (
name, todo_id, created_at
) VALUES (?, ?, ?)
`
const params = [task.name, task.todo_id, task.created_at]
// 执行插入任务数据的 SQL 语句
const stmt = db.prepare(insertQuery)
const result = stmt.run(...params)
// 返回插入的任务ID
return result.lastInsertRowid
}
// 批量插入数据
ipcMain.handle('db_tasks_insert', (_, tasks) => {
return insertTasks(tasks)
})
// 批量插入任务数据
export function insertTasks(tasks): void {
const insertQuery = `
INSERT INTO todo_list (
name, todo_id, created_at
) VALUES (?, ?, ?)
`
const stmt = db.prepare(insertQuery) // 准备 SQL 插入语句
// 批量插入任务数据
const transaction = db.transaction((tasks) => {
for (const task of tasks) {
const params = [task.name, task.todo_id, task.created_at]
stmt.run(...params) // 执行每一条任务的插入
}
})
transaction(tasks) // 开启事务并执行批量插入
}
改
// 异步更新单条数据
ipcMain.handle('db_task_update', (_, task) => {
return updateTask(task)
})
// 更新单个任务数据(根据 todo_id 或 id)
export function updateTask(task): void {
let updateQuery
let params: {
name: string
todo_id: number
created_at: number
id: number
}[]
// 判断更新的是 id 还是 todo_id
if (task.id) {
updateQuery = `
UPDATE todo_list SET
name = ?, todo_id = ?, created_at = ?
WHERE id = ?
`
params = [task.name, task.todo_id, task.created_at, task.id]
} else if (task.todo_id) {
updateQuery = `
UPDATE todo_list SET
name = ?, created_at = ?
WHERE todo_id = ?
`
params = [task.name, task.created_at, task.todo_id]
} else {
throw new Error('Task must have either id or todo_id')
}
const stmt = db.prepare(updateQuery) // 准备 SQL 更新语句
stmt.run(...params) // 执行更新操作
}
// 异步更新多条数据
ipcMain.handle('db_tasks_update', (_, tasks) => {
return updateTasks(tasks)
})
// 批量更新任务数据(根据 todo_id 或 id)
export function updateTasks(tasks): void {
const updateQuery = `
UPDATE todo_list SET
name = ?, created_at = ?
WHERE todo_id = ? OR id = ?
`
const stmt = db.prepare(updateQuery) // 准备 SQL 更新语句
const transaction = db.transaction((tasks) => {
tasks.forEach((task) => {
if (task.id || task.todo_id) {
const params = [task.name, task.created_at, task.todo_id, task.id]
stmt.run(...params) // 执行批量更新操作
} else {
console.warn('Task must have either id or todo_id')
}
})
})
transaction(tasks) // 执行批量更新事务
}
// 同步更新多条数据
ipcMain.on('db_tasks_sync_update', (event, tasks) => {
try {
updateTasks(tasks)
event.returnValue = true
} catch (error) {
console.error(error)
event.returnValue = false
}
})
// 同步更新单条数据
ipcMain.on('db_task_sync_update', (event, task) => {
try {
updateTask(task)
event.returnValue = true
} catch (error) {
event.returnValue = false
console.error(error)
}
})
删
// 同步删除单条数据
ipcMain.on('db_task_sync_delete', (event, task) => {
try {
deleteTaskByIdOrTaskId(task)
event.returnValue = true
} catch (error) {
console.error(error)
event.returnValue = false
}
})
// 异步删除单条数据
ipcMain.handle('db_task_delete', (task) => {
try {
deleteTaskByIdOrTaskId(task)
return true
} catch (error) {
console.error(error)
return false
}
})
// 根据传入对象的 id 或 todo_id 删除任务数据
export function deleteTaskByIdOrTaskId(task): void {
let deleteQuery
let identifier
// 检查传入对象中是否有 id 或 todo_id
if (task.id) {
// 如果传入对象中有 id,则按 id 删除
deleteQuery = `DELETE FROM todo_list WHERE id = ?`
identifier = task.id
} else if (task.todo_id) {
// 如果传入对象中有 todo_id,则按 todo_id 删除
deleteQuery = `DELETE FROM todo_list WHERE todo_id = ?`
identifier = task.todo_id
} else {
throw new Error('Object must have either id or todo_id')
}
const stmt = db.prepare(deleteQuery) // 准备 SQL 删除语句
stmt.run(identifier) // 执行删除操作
}
// 删除多条数据
ipcMain.on('db_tasks_sync_delete', (event, tasks) => {
try {
deleteTasksByIdOrTaskId(tasks)
event.returnValue = true
} catch (error) {
console.error(error)
event.returnValue = false
}
})
// 批量根据 todo_id 或 id 删除任务数据
export function deleteTasksByIdOrTaskId(tasks): void {
// 生成批量删除的 SQL 语句
const deleteQuery = `DELETE FROM todo_list WHERE id = ? OR todo_id = ?`
const stmt = db.prepare(deleteQuery) // 准备 SQL 删除语句
const transaction = db.transaction((tasks) => {
tasks.forEach((task) => {
if (task.id || task.todo_id) {
// 如果任务对象有 id 或 todo_id,则执行删除操作
stmt.run(task.id, task.todo_id) // 执行删除操作,按照 id 或 todo_id 删除
} else {
console.warn('Task must have either id or todo_id')
}
})
})
transaction(tasks) // 执行事务,批量删除任务
}
增删改查事件可以进行结构封装
下面是win事件注册
// 数据库事件注册
import { initDatabase } from './sqlite3'
app.whenReady().then(() => {
...
// 初始化数据库
initDatabase()
})
第四步数据库使用方法封装
/src/preload 新建一个sqlite3.ts
结合泛型来使用,可以自行修改传入参数
import { ipcRenderer } from 'electron'
export const betterSqlite = {
// 获取数据库中的所有待办事项
getTodoList: <T>(): Promise<T[]> => ipcRenderer.invoke('db_query', 'SELECT * FROM todo_list;'),
// 根据 user_id_role 获取任务列表
getTasksByUserRole: <T>(userIdRole: string): Promise<T> =>
ipcRenderer.invoke('db_tasks_sync_get_by_user_role', userIdRole),
// 插入单个任务
insertTask: <T>(task: T): Promise<void> => ipcRenderer.invoke('db_task_sync_insert', task),
// 插入多个任务
insertTasks: <T>(tasks: T[]): Promise<void> => ipcRenderer.invoke('db_tasks_insert', tasks),
// 更新单个任务
updateTask: <T>(task: T): Promise<void> => ipcRenderer.invoke('db_task_update', task),
// 更新多个任务
updateTasks: <T>(tasks: T[]): Promise<void> => ipcRenderer.invoke('db_tasks_update', tasks),
// 删除单个任务
deleteTask: <T>(task: T): Promise<void> => ipcRenderer.invoke('db_task_delete', task),
// 删除多个任务
deleteTasks: <T>(tasks: T[]): Promise<void> => ipcRenderer.invoke('db_tasks_sync_delete', tasks),
// 查询任务数据,根据 ID 或 todo_id
getTaskById: <T>(todoId: string | number): Promise<T> =>
ipcRenderer.invoke('db_query', 'SELECT * FROM todo_list WHERE todo_id = ?', [todoId]),
// 异步更新数据库中的任务状态
updateTaskStatus: (taskId: string | number, status: string | number): Promise<void> =>
ipcRenderer.invoke('db_task_sync_update', {
todo_id: taskId,
status: status
}),
// 获取数据库版本
getDatabaseVersion: (): Promise<string> =>
ipcRenderer.invoke('db_query', 'SELECT version FROM version ORDER BY id DESC LIMIT 1;')
}
第五步使用
//举个例子
window.betterSqlite.getTodoList()
3.serialport使用
模拟串口以及调试工具
串口调试工具 https://www.redisant.cn/mse
虚拟串口 https://help.electronic.us/support/solutions/articles/44002275310-installation
安装
官网地址https://serialport.io/docs/
$ npm install serialport
时间原因不做逐步解析后续完善
import { ipcMain, BrowserWindow } from 'electron'
import { SerialPort } from 'serialport'
let port: SerialPort
let mainWindow: BrowserWindow | null = null
// 初始化串口通信
export function initSerial(win: BrowserWindow) {
mainWindow = win
// 获取可用串口列表
ipcMain.handle('serial:list', async () => {
try {
const ports = await SerialPort.list()
return ports
} catch (error) {
console.error('获取串口列表失败:', error)
throw error
}
})
// 打开串口
ipcMain.handle('serial:open', async (_, options) => {
try {
if (port) {
await new Promise((resolve) => port.close(resolve))
}
port = new SerialPort({
path: options.path,
baudRate: options.baudRate || 9600,
...options
})
// 使用原始数据模式,不使用ReadlineParser
port.on('data', (data) => {
if (mainWindow) {
mainWindow.webContents.send('serial:data', data)
}
})
// 错误处理
port.on('error', (error) => {
console.error('串口错误:', error)
if (mainWindow) {
mainWindow.webContents.send('serial:error', error.message)
}
})
return true
} catch (error) {
console.error('打开串口失败:', error)
throw error
}
})
// 关闭串口
ipcMain.handle('serial:close', () => {
return new Promise((resolve) => {
if (!port) {
resolve(true)
return
}
port.close((error) => {
if (error) {
console.error('关闭串口失败:', error)
resolve(false)
} else {
// @ts-ignore
port = null
resolve(true)
}
})
})
})
// 写入数据
ipcMain.handle('serial:write', async (_, data) => {
return new Promise((resolve, reject) => {
if (!port) {
reject(new Error('串口未打开'))
return
}
port.write(data, (error) => {
if (error) {
console.error('写入数据失败:', error)
reject(error)
} else {
resolve(true)
}
})
})
})
// 获取串口状态
ipcMain.handle('serial:status', () => {
return {
isOpen: port !== null,
port: port ? port.path : null,
baudRate: port ? port.baudRate : null
}
})
// 读取线圈状态
ipcMain.handle('serial:readCoils', async (_, { slaveId, startAddress, quantity }) => {
return new Promise((resolve, reject) => {
if (!port) {
reject(new Error('串口未打开'))
return
}
// 构建Modbus请求报文
const request = Buffer.alloc(8)
request[0] = slaveId // 从站地址
request[1] = 0x01 // 功能码:读取线圈状态
request.writeUInt16BE(startAddress, 2) // 起始地址
request.writeUInt16BE(quantity, 4) // 读取数量
// 计算CRC16
const crc = calculateCRC16(request.slice(0, 6))
request.writeUInt16LE(crc, 6)
// 设置响应超时处理
const responseTimeout = setTimeout(() => {
removeListeners()
reject(new Error('读取超时'))
}, 1000)
// 处理响应数据
const handleResponse = (data) => {
// 检查响应长度
if (data.length < 5) return
// 验证从站地址和功能码
if (data[0] !== slaveId || data[1] !== 0x01) return
// 验证CRC
const responseCrc = data.readUInt16LE(data.length - 2)
const calculatedCrc = calculateCRC16(data.slice(0, data.length - 2))
if (responseCrc !== calculatedCrc) {
removeListeners()
reject(new Error('CRC校验错误'))
return
}
// 解析数据
const byteCount = data[2]
const coils = []
for (let i = 0; i < byteCount; i++) {
const byte = data[3 + i]
for (let bit = 0; bit < 8; bit++) {
if (coils.length < quantity) {
coils.push(((byte >> bit) & 1) as never)
}
}
}
removeListeners()
clearTimeout(responseTimeout)
resolve(coils)
}
// 清理监听器
const removeListeners = () => {
port.removeListener('data', handleResponse)
}
// 添加响应监听
port.on('data', handleResponse)
// 发送请求
port.write(request, (error) => {
if (error) {
removeListeners()
clearTimeout(responseTimeout)
reject(error)
}
})
})
})
// 写入单个线圈
ipcMain.handle('serial:writeSingleCoil', async (_, { slaveId, address, value }) => {
return new Promise((resolve, reject) => {
if (!port) {
reject(new Error('串口未打开'))
return
}
// 构建Modbus请求报文
const request = Buffer.alloc(8)
request[0] = slaveId // 从站地址
request[1] = 0x05 // 功能码:写入单个线圈
request.writeUInt16BE(address, 2) // 线圈地址
request.writeUInt16BE(value ? 0xFF00 : 0x0000, 4) // 线圈值
// 计算CRC16
const crc = calculateCRC16(request.slice(0, 6))
request.writeUInt16LE(crc, 6)
// 设置响应超时处理
const responseTimeout = setTimeout(() => {
removeListeners()
reject(new Error('写入超时'))
}, 1000)
// 处理响应数据
const handleResponse = (data) => {
// 检查响应长度
if (data.length !== 8) return
// 验证从站地址和功能码
if (data[0] !== slaveId || data[1] !== 0x05) return
// 验证CRC
const responseCrc = data.readUInt16LE(data.length - 2)
const calculatedCrc = calculateCRC16(data.slice(0, data.length - 2))
if (responseCrc !== calculatedCrc) {
removeListeners()
reject(new Error('CRC校验错误'))
return
}
removeListeners()
clearTimeout(responseTimeout)
resolve(true)
}
// 清理监听器
const removeListeners = () => {
port.removeListener('data', handleResponse)
}
// 添加响应监听
port.on('data', handleResponse)
// 发送请求
port.write(request, (error) => {
if (error) {
removeListeners()
clearTimeout(responseTimeout)
reject(error)
}
})
})
})
// 读取离散输入寄存器
ipcMain.handle('serial:readDiscreteInputs', async (_, { slaveId, startAddress, quantity }) => {
return new Promise((resolve, reject) => {
if (!port) {
reject(new Error('串口未打开'))
return
}
// 构建Modbus请求报文
const request = Buffer.alloc(8)
request[0] = slaveId // 从站地址
request[1] = 0x02 // 功能码:读取离散输入
request.writeUInt16BE(startAddress, 2) // 起始地址
request.writeUInt16BE(quantity, 4) // 读取数量
// 计算CRC16
const crc = calculateCRC16(request.slice(0, 6))
request.writeUInt16LE(crc, 6)
// 设置响应超时处理
const responseTimeout = setTimeout(() => {
removeListeners()
reject(new Error('读取超时'))
}, 1000)
// 处理响应数据
const handleResponse = (data) => {
// 检查响应长度
if (data.length < 5) return
// 验证从站地址和功能码
if (data[0] !== slaveId || data[1] !== 0x02) return
// 验证CRC
const responseCrc = data.readUInt16LE(data.length - 2)
const calculatedCrc = calculateCRC16(data.slice(0, data.length - 2))
if (responseCrc !== calculatedCrc) {
removeListeners()
reject(new Error('CRC校验错误'))
return
}
// 解析数据
const byteCount = data[2]
const inputs = []
for (let i = 0; i < byteCount; i++) {
const byte = data[3 + i]
for (let bit = 0; bit < 8; bit++) {
if (inputs.length < quantity) {
inputs.push(((byte >> bit) & 1) as never)
}
}
}
removeListeners()
clearTimeout(responseTimeout)
resolve(inputs)
}
// 清理监听器
const removeListeners = () => {
port.removeListener('data', handleResponse)
}
// 添加响应监听
port.on('data', handleResponse)
// 发送请求
port.write(request, (error) => {
if (error) {
removeListeners()
clearTimeout(responseTimeout)
reject(error)
}
})
})
})
// 读取输入寄存器
ipcMain.handle('serial:readInputRegisters', async (_, { slaveId, startAddress, quantity }) => {
return new Promise((resolve, reject) => {
if (!port) {
reject(new Error('串口未打开'))
return
}
// 构建Modbus请求报文
const request = Buffer.alloc(8)
request[0] = slaveId // 从站地址
request[1] = 0x04 // 功能码:读取输入寄存器
request.writeUInt16BE(startAddress, 2) // 起始地址
request.writeUInt16BE(quantity, 4) // 读取数量
// 计算CRC16
const crc = calculateCRC16(request.slice(0, 6))
request.writeUInt16LE(crc, 6)
// 设置响应超时处理
const responseTimeout = setTimeout(() => {
removeListeners()
reject(new Error('读取超时'))
}, 1000)
// 处理响应数据
const handleResponse = (data) => {
// 检查响应长度
if (data.length < 5) return
// 验证从站地址和功能码
if (data[0] !== slaveId || data[1] !== 0x04) return
// 验证CRC
const responseCrc = data.readUInt16LE(data.length - 2)
const calculatedCrc = calculateCRC16(data.slice(0, data.length - 2))
if (responseCrc !== calculatedCrc) {
removeListeners()
reject(new Error('CRC校验错误'))
return
}
// 解析数据
const byteCount = data[2]
const registers = []
// 确保字节数是偶数,因为每个寄存器占用2个字节
if (byteCount % 2 !== 0) {
removeListeners()
reject(new Error('数据长度错误'))
return
}
// 逐个解析寄存器值
for (let i = 0; i < byteCount; i += 2) {
if (i + 1 < byteCount) {
// 读取两个字节并组合成16位整数
const highByte = data[3 + i]
const lowByte = data[4 + i]
if (highByte === undefined || lowByte === undefined) {
removeListeners()
reject(new Error('数据格式错误'))
return
}
// 按照Modbus规范,使用大端序组合数据
const value = (highByte << 8) | lowByte
registers.push(value as never)
}
}
removeListeners()
clearTimeout(responseTimeout)
resolve(registers)
}
// 清理监听器
const removeListeners = () => {
port.removeListener('data', handleResponse)
}
// 添加响应监听
port.on('data', handleResponse)
// 发送请求
port.write(request, (error) => {
if (error) {
removeListeners()
clearTimeout(responseTimeout)
reject(error)
}
})
})
})
}
// CRC16计算函数
function calculateCRC16(buffer) {
let crc = 0xffff
for (let pos = 0; pos < buffer.length; pos++) {
crc ^= buffer[pos]
for (let i = 8; i !== 0; i--) {
if ((crc & 0x0001) !== 0) {
crc >>= 1
crc ^= 0xa001
} else {
crc >>= 1
}
}
}
return crc
}
import { ipcRenderer } from 'electron'
const serialAPI = {
// 获取可用的串口列表
getSerialPorts: () => ipcRenderer.invoke('serial:list'),
// 打开串口连接
openPort: (options) => ipcRenderer.invoke('serial:open', options),
// 关闭串口连接
closePort: () => ipcRenderer.invoke('serial:close'),
// 写入数据到串口
sendSerialData: (data) => ipcRenderer.invoke('serial:write', data),
// 监听串口数据
onSerialData: (callback) => ipcRenderer.on('serial:data', callback),
// 监听串口错误
onError: (callback) => ipcRenderer.on('serial:error', callback),
// 获取串口连接状态
getStatus: () => ipcRenderer.invoke('serial:status'),
// 读取线圈状态
readCoils: (params) => ipcRenderer.invoke('serial:readCoils', params),
// 写入单个线圈
writeSingleCoil: (params) => ipcRenderer.invoke('serial:writeSingleCoil', params),
// 读取离散输入寄存器
readDiscreteInputs: (params) => ipcRenderer.invoke('serial:readDiscreteInputs', params),
// 读取输入寄存器
readInputRegisters: (params) => ipcRenderer.invoke('serial:readInputRegisters', params)
}
export default serialAPI
import { contextBridge } from 'electron'
import serialAPI from './serial'
contextBridge.exposeInMainWorld('serialAPI', serialAPI);
获取
await serialAPI.getSerialPorts()