Skip to content

Commit

Permalink
Implemented serialport service for API devices
Browse files Browse the repository at this point in the history
  • Loading branch information
mkurczewski committed Dec 20, 2024
1 parent a8e1ebf commit 2197ffe
Show file tree
Hide file tree
Showing 19 changed files with 728 additions and 56 deletions.
6 changes: 3 additions & 3 deletions apps/app/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function createWindow(): void {
mainWindow.webContents.openDevTools()

mainWindow.on("ready-to-show", () => {
initSerialPort(ipcMain, mainWindow.webContents)
initSql(ipcMain)

mainWindow.show()
})

Expand Down Expand Up @@ -66,9 +69,6 @@ app.whenReady().then(() => {
// IPC test
ipcMain.on("ping", () => console.log("pong"))

initSerialPort(ipcMain)
initSql(ipcMain)

createWindow()

app.on("activate", function () {
Expand Down
10 changes: 7 additions & 3 deletions apps/app/tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
"electron.vite.config.*",
"src/main/**/*",
"src/preload/**/*",
"../../libs/app-*/main/**/*",
"../../libs/app-*/main/**/*.ts",
],
"exclude": [
"../../libs/app-*/main/**/*.test.ts"
],
"compilerOptions": {
"composite": true,
"resolveJsonModule": true,
"types": [
"node",
"electron-vite/node",
"./images.d.ts"
],
"paths": {
"app-*": ["../../libs/app-*/src/index.ts"],
"app-*": ["../../libs/app-*/src/index.ts"]
}
},
}
}
6 changes: 3 additions & 3 deletions apps/web/src/app/app.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

import { PortInfo } from "@serialport/bindings-interface"
import { render } from "@testing-library/react"
import App from "./app"
import { Device } from "app-serialport/models"

jest.mock("app-serialport/renderer", () => {
return {
AppSerialPort: {
list: jest.fn().mockResolvedValue([
onChange: jest.fn().mockResolvedValue([
{
vendorId: "0e8d",
productId: "2006",
path: "/dev/ttyUSB0.KOM123456789",
},
] as PortInfo[]),
] as Device[]),
write: jest.fn(),
},
}
Expand Down
142 changes: 137 additions & 5 deletions apps/web/src/app/serialport-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,151 @@

import { useCallback, useEffect, useRef } from "react"
import { AppSerialPort } from "app-serialport/renderer"
import { ChangedDevices } from "app-serialport/models"
import { isEmpty } from "lodash"

export const useSerialPortListener = () => {
const interval = useRef<NodeJS.Timeout>()
// const interval = useRef<NodeJS.Timeout>()
const ref = useRef(false)

const listenPorts = useCallback(async () => {
const ports = await AppSerialPort.list()
if (!ref.current) {
ref.current = true
const onAttach = async (added: NonNullable<ChangedDevices["added"]>) => {
const apiConfigurationResponse = await AppSerialPort.write(added.path, {
endpoint: "API_CONFIGURATION",
method: "GET",
body: {},
options: {
connectionTimeOut: 30000,
},
})
const entitiesConfigurationResponse = await AppSerialPort.write(
added.path,
{
endpoint: "ENTITIES_CONFIGURATION",
method: "GET",
body: {
entityType: "contacts",
},
options: {
connectionTimeOut: 30000,
},
}
)
console.log({ apiConfigurationResponse, entitiesConfigurationResponse })
}
AppSerialPort.onChange((changes) => {
console.log(changes)
if (!isEmpty(changes.added)) {
void onAttach(changes.added)
}
})
}

await AppSerialPort.write(ports[0].path, "Hello from the web app!")
// const req1 = () =>
// AppSerialPort.write(ports[0].path, {
// endpoint: "API_CONFIGURATION",
// method: "GET",
// body: {},
// options: {
// connectionTimeOut: 30000,
// },
// })

// const req2 = () =>
// AppSerialPort.write(ports[0].path, {
// endpoint: "ENTITIES_CONFIGURATION",
// method: "GET",
// body: {
// entityType: "contacts",
// },
// options: {
// connectionTimeOut: 30000,
// },
// })

// await AppSerialPort.write(ports[0].path, {
// endpoint: "API_CONFIGURATION",
// method: "GET",
// body: {},
// options: {
// connectionTimeOut: 30000,
// },
// })
// await new Promise((resolve) => setTimeout(resolve, 100))
// await AppSerialPort.write(ports[0].path, {
// endpoint: "ENTITIES_CONFIGURATION",
// method: "GET",
// body: {
// entityType: "contacts",
// },
// options: {
// connectionTimeOut: 30000,
// },
// })
// await new Promise((resolve) => setTimeout(resolve, 100))
// void AppSerialPort.write(ports[0].path, {
// endpoint: "ENTITIES_DATA",
// method: "GET",
// body: {
// entityType: "contacts",
// responseType: "json",
// },
// options: {
// connectionTimeOut: 30000,
// },
// })
// await new Promise((resolve) => setTimeout(resolve, 100))
// await AppSerialPort.write(ports[0].path, {
// endpoint: "ENTITIES_CONFIGURATION",
// method: "GET",
// body: {
// entityType: "contacts",
// },
// options: {
// connectionTimeOut: 30000,
// },
// })

Promise.all([
// AppSerialPort.write(ports[0].path, {
// endpoint: "API_CONFIGURATION",
// method: "GET",
// body: {},
// options: {
// connectionTimeOut: 30000,
// },
// }),
// AppSerialPort.write(ports[0].path, {
// endpoint: "ENTITIES_CONFIGURATION",
// method: "GET",
// body: {
// entityType: "contacts",
// },
// options: {
// connectionTimeOut: 30000,
// },
// }),
// AppSerialPort.write(ports[0].path, {
// endpoint: "ENTITIES_DATA",
// method: "GET",
// body: {
// entityType: "contacts",
// responseType: "json",
// },
// options: {
// connectionTimeOut: 30000,
// },
// }),
]).then((resp) => {
console.log(resp)
})
}, [])

useEffect(() => {
void listenPorts()
interval.current = setInterval(listenPorts, 2000)
return () => clearInterval(interval.current)
// interval.current = setInterval(listenPorts, 2000)
// return () => clearInterval(interval.current)
}, [listenPorts])
}
110 changes: 101 additions & 9 deletions libs/app-serialport/main/src/lib/app-serial-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,129 @@
*/

import { SerialPort } from "serialport"
import { PortInfo } from "@serialport/bindings-interface"
import { SerialPortApiDevice } from "./devices/api-device/serial-port-api-device"
import { SerialPortDevice } from "./devices/request-parser.interface"
import { APIRequestData, ChangedDevices, Device } from "app-serialport/models"
import EventEmitter from "events"

type DevicesChangeCallback = (data: ChangedDevices) => void

const isKnownDevice = (port: PortInfo): port is Device => {
return port.productId !== undefined && port.vendorId !== undefined
}

export class AppSerialPort {
private readonly instances: Map<string, SerialPort>
private readonly instances = new Map<string, SerialPortDevice>()
supportedDevices = [SerialPortApiDevice]
attachedDevices: Device[] = []
eventEmitter = new EventEmitter()

constructor() {
this.instances = new Map()
void this.checkForPortChanges()
setInterval(() => {
void this.checkForPortChanges()
}, 2000)
}

private async checkForPortChanges() {
const currentDevices = (await SerialPort.list()).filter((port) => {
if (!isKnownDevice(port)) {
return false
}
return this.supportedDevices.some((device) => {
return (
device.matchingVendorIds.includes(port.vendorId) &&
device.matchingProductIds.includes(port.productId)
)
})
}) as Device[]

const removedDevices = this.attachedDevices.filter((device) => {
return !currentDevices.find((newDevice) => newDevice.path === device.path)
})

this.attachedDevices = currentDevices

removedDevices.forEach((device) => {
this.removeInstance(device)
})

currentDevices.forEach((device) => {
this.ensureInstance(device.path)
})
}

private instanceExists(path: string) {
return this.instances.has(path)
}

private createInstance(path: string) {
const serialPort = new SerialPort({ path, baudRate: 9600 })
this.instances.set(path, serialPort)
const instance = this.getInstanceForDevice(path)
if (instance) {
const serialPort = new instance({ path, baudRate: 9600 })
this.instances.set(path, serialPort)
const change: Pick<ChangedDevices, "added"> = {
added: this.getDeviceByPath(path),
}
this.eventEmitter.emit("devicesChanged", change)
}
}

private removeInstance(device: Device) {
const serialPort = this.instances.get(device.path)
if (serialPort) {
serialPort.destroy()
this.instances.delete(device.path)
const change: Pick<ChangedDevices, "removed"> = {
removed: device,
}
this.eventEmitter.emit("devicesChanged", change)
}
}

private ensureInstance(path: string) {
if (!this.instanceExists(path)) {
this.createInstance(path)
}
return this.instances.get(path) as SerialPort
return this.instances.get(path)
}

private getDeviceByPath(path: string) {
return this.attachedDevices.find((device) => device.path === path)
}

private getInstanceForDevice(path: string) {
const port = this.getDeviceByPath(path)
if (!port) {
return
}
return this.supportedDevices.find((device) => {
return (
device.matchingVendorIds.includes(port.vendorId) &&
device.matchingProductIds.includes(port.productId)
)
})
}

changeBaudRate(path: string, baudRate: number) {
const serialPort = this.ensureInstance(path)
serialPort.update({ baudRate })
serialPort?.update({ baudRate })
}

write(path: string, data: string) {
const serialPort = this.ensureInstance(path)
serialPort.write(data)
async request(path: string, data: APIRequestData) {
return this.ensureInstance(path)?.request(data)
}

onDevicesChange(callback: DevicesChangeCallback) {
this.eventEmitter.on(
"devicesChanged",
(changes: Omit<ChangedDevices, "all">) => {
callback({
all: this.attachedDevices,
...changes,
})
}
)
}
}
Loading

0 comments on commit 2197ffe

Please sign in to comment.