当前位置: 首页 > article >正文

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()

封装

使用


http://www.kler.cn/a/572415.html

相关文章:

  • Qt/C++音视频开发-检查是否含有B帧/转码推流/拉流显示/监控拉流推流/海康大华宇视监控
  • 基于Python的新闻采集与分析:新闻平台的全面数据采集实践
  • 爬虫技术结合淘宝商品快递费用API接口(item_fee):电商物流数据的高效获取与应用
  • 用DeepSeek-R1-Distill-data-110k蒸馏中文数据集 微调Qwen2.5-7B-Instruct!
  • 【leetcode】实现Tire(前缀树)
  • FastGPT 源码:基于 LLM 实现 Rerank (含Prompt)
  • android_viewtracker 原理
  • 【cuda学习日记】5.4 常量内存
  • leetcode383 赎金信
  • 【详解 | 辨析】“单跳多跳,单天线多天线,单信道多信道” 之间的对比
  • Git-cherry pick
  • 迷你世界脚本世界UI接口:UI
  • c++面试常见问题:虚表指针存在于内存哪个分区
  • Node.js学习分享(上)
  • python爬虫数据库概述
  • 【Java】IO流
  • Linux·数据库INSERT优化
  • PyTorch 与 NVIDIA GPU 的适配版本及安装
  • NO.23十六届蓝桥杯备战|二维数组|创建|初始化|遍历|memset(C++)
  • Kconfig与CMake初步模块化工程3