光伏pc端应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

625 lines
25 KiB

/*
* @Author: 李皓 hao_li_work@163.com
* @Date: 2026-07-28 16:31:45
* @LastEditors: 李皓 hao_li_work@163.com
* @LastEditTime: 2026-07-28 16:44:07
* @FilePath: \pc\preload.js
* @Description:
* @Version: 1.0.0
*/
const { contextBridge, ipcRenderer } = require('electron')
const nodeNet = require('net')
const dns = require('dns')
const { execFile } = require('child_process')
function tryConnect(host, port, timeout) {
return new Promise((resolve) => {
const socket = new nodeNet.Socket()
socket.setTimeout(timeout)
socket.on('connect', () => { socket.destroy(); resolve(true) })
socket.on('error', () => { socket.destroy(); resolve(false) })
socket.on('timeout', () => { socket.destroy(); resolve(false) })
socket.connect(port, host)
})
}
function normalizePingHost(input) {
const raw = String(input || '').trim()
if (!raw) return ''
try {
return new URL(raw).hostname
} catch {}
return raw.replace(/^https?:\/\//i, '').split('/')[0].split(':')[0]
}
function trySystemPing(host, timeout) {
return new Promise((resolve) => {
const wait = Math.max(1000, Number(timeout) || 2000)
execFile('ping', ['-n', '1', '-w', String(wait), host], { windowsHide: true }, (err) => {
resolve(!err)
})
})
}
const net = {
async ping(host, timeout) {
const target = normalizePingHost(host)
if (!target) return false
const wait = Math.max(1000, Number(timeout) || 2000)
const hosts = [...new Set([target, 'www.qq.com', 'www.microsoft.com'])]
for (const currentHost of hosts) {
const isIp = nodeNet.isIP(currentHost) !== 0
try {
if (!isIp) await dns.promises.lookup(currentHost)
} catch {
if (!await trySystemPing(currentHost, wait)) continue
}
for (const port of [443, 80, 53]) {
if (await tryConnect(currentHost, port, wait)) {
ipcRenderer.send('app-network-status', true)
return true
}
}
if (await trySystemPing(currentHost, wait)) {
ipcRenderer.send('app-network-status', true)
return true
}
}
const online = !!globalThis.navigator?.onLine
ipcRenderer.send('app-network-status', online)
return online
}
}
function currentRouteText() {
return window.location.hash || window.location.pathname || '#/'
}
function reportRoute() {
ipcRenderer.send('app-route-changed', currentRouteText())
}
function installRouteReporter() {
reportRoute()
window.addEventListener('hashchange', reportRoute)
window.addEventListener('popstate', reportRoute)
const timer = setInterval(reportRoute, 1000)
window.addEventListener('beforeunload', () => clearInterval(timer))
}
installRouteReporter()
function injectShellDrawer() {
if (document.getElementById('__electron_shell_drawer__')) return
const style = document.createElement('style')
style.id = '__electron_shell_drawer_style__'
style.textContent = `
#__electron_shell_drawer__{position:fixed;left:0;top:0;bottom:0;z-index:2147483647;pointer-events:none;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
#__electron_shell_drawer__ *{box-sizing:border-box}
#__electron_shell_handle__{position:absolute;left:0;top:50%;transform:translateY(-50%);width:18px;height:120px;border-radius:0 12px 12px 0;background:#2f8cff;color:#fff;display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.16)}
#__electron_shell_panel__{position:absolute;left:0;top:56px;bottom:56px;width:320px;max-width:88vw;background:rgba(255,255,255,.98);border-right:1px solid rgba(0,0,0,.08);box-shadow:6px 0 24px rgba(0,0,0,.12);transform:translateX(-100%);transition:transform .18s ease;pointer-events:auto;overflow:auto;padding:12px}
#__electron_shell_drawer__.open #__electron_shell_panel__{transform:translateX(0)}
#__electron_shell_header__{font-size:13px;font-weight:600;margin:0 0 10px}
#__electron_shell_row__{display:flex;gap:8px;align-items:center;margin:8px 0}
#__electron_shell_url__{width:100%;padding:8px 10px;border:1px solid #cfd7e3;border-radius:6px;font-size:13px;outline:none}
.__electron_shell_btn__{border:1px solid #c7d7ef;background:#fff;color:#1f3f6d;border-radius:6px;padding:7px 10px;font-size:12px;cursor:pointer}
.__electron_shell_btn_primary__{background:#2f8cff;border-color:#2f8cff;color:#fff}
#__electron_shell_status__{font-size:12px;line-height:1.5;color:#445}
#__electron_shell_routes__{margin-top:8px;font-size:12px;word-break:break-all;color:#234}
`
document.head.appendChild(style)
const wrap = document.createElement('div')
wrap.id = '__electron_shell_drawer__'
wrap.innerHTML = `
<div id="__electron_shell_handle__">›</div>
<div id="__electron_shell_panel__">
<div id="__electron_shell_header__">壳层入口</div>
<div id="__electron_shell_status__"></div>
<div id="__electron_shell_row__">
<input id="__electron_shell_url__" type="text" placeholder="输入 http(s):// 地址" />
</div>
<div id="__electron_shell_row__">
<button class="__electron_shell_btn__ __electron_shell_btn_primary__" id="__electron_shell_open__">打开网址</button>
<button class="__electron_shell_btn__" id="__electron_shell_local__">本地包</button>
</div>
<div id="__electron_shell_row__">
<button class="__electron_shell_btn__" id="__electron_shell_copy__">复制当前页</button>
</div>
<div id="__electron_shell_routes__"></div>
</div>
`
document.body.appendChild(wrap)
const handle = wrap.querySelector('#__electron_shell_handle__')
const panel = wrap.querySelector('#__electron_shell_panel__')
const input = wrap.querySelector('#__electron_shell_url__')
const status = wrap.querySelector('#__electron_shell_status__')
const routes = wrap.querySelector('#__electron_shell_routes__')
const openBtn = wrap.querySelector('#__electron_shell_open__')
const localBtn = wrap.querySelector('#__electron_shell_local__')
const copyBtn = wrap.querySelector('#__electron_shell_copy__')
const syncState = async () => {
const url = await app.shellGetTargetUrl().catch(() => '')
input.value = url || ''
status.textContent = [
`网络:${globalThis.navigator?.onLine ? '在线' : '离线'}`,
`倾角:${JSON.stringify(sensor.sensorList ? JSON.parse(sensor.sensorList()).data?.some?.(el => String(el?.name || '').includes('HWT6053')) : false)}`,
].join(' | ')
routes.textContent = `当前:${window.location.hash || window.location.pathname || '#/'}`
}
handle.addEventListener('click', () => wrap.classList.toggle('open'))
openBtn.addEventListener('click', async () => {
const url = input.value.trim()
await app.shellSetTargetUrl(url)
location.reload()
})
localBtn.addEventListener('click', async () => {
await app.shellSetTargetUrl('')
location.reload()
})
copyBtn.addEventListener('click', async () => {
await navigator.clipboard?.writeText(window.location.href).catch(() => {})
})
setInterval(syncState, 1500)
syncState()
}
const shellOpenObserver = new MutationObserver(() => {
if (document.body && !document.getElementById('__electron_shell_drawer__')) injectShellDrawer()
})
shellOpenObserver.observe(document.documentElement || document, { childList: true, subtree: true })
const appCallbacks = {
onFileWrite: null,
onFileRead: null,
onFileDelete: null,
onPageModeChanged: null,
onShellOpenUrlDialog: null
}
ipcRenderer.on('app-file-write-callback', (_event, token, filePath, success, err) => {
if (typeof appCallbacks.onFileWrite === 'function') appCallbacks.onFileWrite(token, filePath, success, err)
})
ipcRenderer.on('app-file-read-callback', (_event, token, filePath, content) => {
if (typeof appCallbacks.onFileRead === 'function') appCallbacks.onFileRead(token, filePath, content)
})
ipcRenderer.on('app-file-delete-callback', (_event, token, filePath, success) => {
if (typeof appCallbacks.onFileDelete === 'function') appCallbacks.onFileDelete(token, filePath, success)
})
ipcRenderer.on('app-page-mode', (_event, mode) => {
if (typeof appCallbacks.onPageModeChanged === 'function') appCallbacks.onPageModeChanged(mode)
})
ipcRenderer.on('app-shell-open-url-dialog', () => {
if (typeof appCallbacks.onShellOpenUrlDialog === 'function') appCallbacks.onShellOpenUrlDialog()
})
function getRouteQueryValue(name) {
try {
const hash = window.location.hash || ''
const queryText = hash.includes('?') ? hash.slice(hash.indexOf('?') + 1) : window.location.search.slice(1)
return new URLSearchParams(queryText).get(name)
} catch {
return null
}
}
function tryParseJson(value) {
try {
return value ? JSON.parse(value) : null
} catch {
return null
}
}
function restoreZcDataFromList() {
const id = getRouteQueryValue('id')
if (!id) return null
const list = tryParseJson(localStorage.getItem('List')) || []
const current = list.find(item => String(item?.idString) === String(id))
if (!current) return null
const restored = current.zcName
? list.filter(item => item?.zcName === current.zcName)
: [current]
if (!restored.length) return null
const value = JSON.stringify(restored)
try {
localStorage.setItem('zcData', value)
ipcRenderer.sendSync('set-cache-sync', 'zcData', value)
} catch {}
return value
}
function isZcDataUsable(value) {
const id = getRouteQueryValue('id')
if (!id) return true
const data = tryParseJson(value)
return Array.isArray(data) && data.some(item => String(item?.idString) === String(id))
}
const app = {
clearCache() { return ipcRenderer.sendSync('app-clear-cache-sync') },
versionCode() { return ipcRenderer.sendSync('app-version-code-sync') },
shellGetTargetUrl() { return ipcRenderer.invoke('app-shell-get-target-url') },
shellSetTargetUrl(targetUrl) { return ipcRenderer.invoke('app-shell-set-target-url', targetUrl) },
setCache(key, value) {
try {
if (key != null) localStorage.setItem(String(key), String(value ?? ''))
} catch {}
return ipcRenderer.sendSync('set-cache-sync', key, value)
},
getCache(key) {
const value = ipcRenderer.sendSync('get-cache-sync', key)
if (value != null) {
if (key !== 'zcData' || isZcDataUsable(value)) return value
const restored = restoreZcDataFromList()
if (restored != null) return restored
return value
}
try {
const localValue = localStorage.getItem(String(key))
if (localValue != null) {
if (key !== 'zcData' || isZcDataUsable(localValue)) return localValue
const restored = restoreZcDataFromList()
if (restored != null) return restored
return localValue
}
} catch {}
if (key === 'zcData') return restoreZcDataFromList()
return null
},
offlinePage() { ipcRenderer.send('app-offline-page') },
onlinePage() { ipcRenderer.send('app-online-page') },
exportFile(filename, content, token) { ipcRenderer.send('app-export-file', filename, content, token) },
fileList(dir) { return ipcRenderer.sendSync('app-file-list-sync', dir) },
fileWriteString(filePath, content, token) { ipcRenderer.send('app-file-write-string', filePath, content, token) },
fileReadString(filePath, token) { ipcRenderer.send('app-file-read-string', filePath, token) },
fileDelete(filePath, token) { ipcRenderer.send('app-file-delete', filePath, token) },
get onFileWrite() { return appCallbacks.onFileWrite },
set onFileWrite(fn) { appCallbacks.onFileWrite = fn },
get onFileRead() { return appCallbacks.onFileRead },
set onFileRead(fn) { appCallbacks.onFileRead = fn },
get onFileDelete() { return appCallbacks.onFileDelete },
set onFileDelete(fn) { appCallbacks.onFileDelete = fn },
get onPageModeChanged() { return appCallbacks.onPageModeChanged },
set onPageModeChanged(fn) { appCallbacks.onPageModeChanged = fn },
get onShellOpenUrlDialog() { return appCallbacks.onShellOpenUrlDialog },
set onShellOpenUrlDialog(fn) { appCallbacks.onShellOpenUrlDialog = fn }
}
const sensorCallbacks = {
onSensorAttached: null,
onSensorDetached: null,
onOperationCallback: null
}
ipcRenderer.on('sensor-attached', (_event, data) => {
if (typeof sensorCallbacks.onSensorAttached === 'function') sensorCallbacks.onSensorAttached(data.id, data.name)
})
ipcRenderer.on('sensor-detached', (_event, data) => {
if (typeof sensorCallbacks.onSensorDetached === 'function') sensorCallbacks.onSensorDetached(data.id, data.name)
})
ipcRenderer.on('sensor-operation-callback', (_event, id, opId, result) => {
if (typeof sensorCallbacks.onOperationCallback === 'function') sensorCallbacks.onOperationCallback(id, opId, result)
})
const sensor = {
initAll() { return ipcRenderer.sendSync('sensor-init-all-sync') },
flush() { return ipcRenderer.sendSync('sensor-flush-sync') },
sensorList() {
const data = ipcRenderer.sendSync('sensor-list-sync')
return JSON.stringify({ code: 0, data: Array.isArray(data) ? data : [] })
},
init(id) { return ipcRenderer.sendSync('sensor-init-sync', id) },
open(id) { return ipcRenderer.sendSync('sensor-open-sync', id) },
close(id) { return ipcRenderer.sendSync('sensor-close-sync', id) },
operation(id, opId, args) { return ipcRenderer.sendSync('sensor-operation-sync', id, opId, args) },
operationAsync(id, opId, args) { ipcRenderer.send('sensor-operation-async', id, opId, args) },
get onSensorAttached() { return sensorCallbacks.onSensorAttached },
set onSensorAttached(fn) { sensorCallbacks.onSensorAttached = fn },
get onSensorDetached() { return sensorCallbacks.onSensorDetached },
set onSensorDetached(fn) { sensorCallbacks.onSensorDetached = fn },
get onOperationCallback() { return sensorCallbacks.onOperationCallback },
set onOperationCallback(fn) { sensorCallbacks.onOperationCallback = fn }
}
const appSensorDataArr = []
const nativeAppSensor = {
version: '1.0.0',
name: 'appSensor',
open() { return ipcRenderer.sendSync('app-sensor-open-sync') },
close() { return ipcRenderer.sendSync('app-sensor-close-sync') },
isOpen() { return ipcRenderer.sendSync('app-sensor-is-open-sync') }
}
ipcRenderer.on('sensor-data', (_event, data) => {
if (data && data.sensor && data.method) {
const index = appSensorDataArr.findIndex(item => item.sensor === data.sensor && item.method === data.method)
if (index >= 0) {
appSensorDataArr.splice(index, 1, data)
} else {
appSensorDataArr.push(data)
}
}
window.postMessage({ __electronSensorData: data }, '*')
})
function injectAppSensorBridge() {
const source = `
;(() => {
const dataArr = []
const callbacks = { onValueCallback: null }
function upsertSensorData(data) {
const targetArr = window.appSensor && Array.isArray(window.appSensor.dataArr)
? window.appSensor.dataArr
: dataArr
if (data && data.sensor && data.method) {
const index = targetArr.findIndex(item => item.sensor === data.sensor && item.method === data.method)
if (index >= 0) targetArr.splice(index, 1, data)
else targetArr.push(data)
}
if (typeof callbacks.onValueCallback === 'function') callbacks.onValueCallback(data)
}
Object.defineProperty(window, 'appSensor', {
configurable: true,
enumerable: true,
value: {
version: '1.0.0',
name: 'appSensor',
dataArr,
open() { return window.__electronNativeAppSensor.open() },
close() { return window.__electronNativeAppSensor.close() },
isOpen() { return window.__electronNativeAppSensor.isOpen() },
get onValueCallback() { return callbacks.onValueCallback },
set onValueCallback(fn) { callbacks.onValueCallback = fn }
}
})
window.addEventListener('message', event => {
const payload = event.data
if (!payload || !payload.__electronSensorData) return
upsertSensorData(payload.__electronSensorData)
})
})()
`
const script = document.createElement('script')
script.textContent = source
;(document.documentElement || document.head).appendChild(script)
script.remove()
}
function injectAppSensorBridgeWhenReady() {
if (document.documentElement || document.head) {
injectAppSensorBridge()
return
}
const timer = setInterval(() => {
if (!(document.documentElement || document.head)) return
clearInterval(timer)
injectAppSensorBridge()
}, 0)
}
injectAppSensorBridgeWhenReady()
const appLocationCallbacks = {
onOpened: null,
onLocationChanged: null
}
ipcRenderer.on('app-location-opened', (_event, status) => {
if (typeof appLocationCallbacks.onOpened === 'function') appLocationCallbacks.onOpened(status)
})
ipcRenderer.on('app-location-changed', (_event, location) => {
if (typeof appLocationCallbacks.onLocationChanged === 'function') appLocationCallbacks.onLocationChanged(location)
})
const appLocation = {
isSupport() { return ipcRenderer.sendSync('app-location-is-support-sync') },
isOpen() { return ipcRenderer.sendSync('app-location-is-open-sync') },
open() { ipcRenderer.send('app-location-open') },
close() { ipcRenderer.send('app-location-close') },
get onOpened() { return appLocationCallbacks.onOpened },
set onOpened(fn) { appLocationCallbacks.onOpened = fn },
get onLocationChanged() { return appLocationCallbacks.onLocationChanged },
set onLocationChanged(fn) { appLocationCallbacks.onLocationChanged = fn }
}
const usbSerialCallbacks = {
onDeviceAttached: null,
onDeviceDetached: null,
onDeviceDiscovered: null,
onDeviceProbeResult: null,
onDeviceOpened: null,
onDeviceState: null,
onDeviceData: null
}
for (const [eventName, cbName] of [
['usb-callback-device-attached', 'onDeviceAttached'],
['usb-callback-device-detached', 'onDeviceDetached'],
['usb-callback-device-discovered', 'onDeviceDiscovered'],
['usb-callback-device-probe-result', 'onDeviceProbeResult'],
['usb-callback-device-opened', 'onDeviceOpened'],
['usb-callback-device-state', 'onDeviceState'],
['usb-callback-device-data', 'onDeviceData']
]) {
ipcRenderer.on(eventName, (_event, jsonStr) => {
let parsed = jsonStr
try {
if (typeof jsonStr === 'string') parsed = JSON.parse(jsonStr)
} catch {}
console.log(`[Electron USB] ${cbName}`, parsed)
const cb = usbSerialCallbacks[cbName]
if (typeof cb === 'function') cb(jsonStr)
window.postMessage({
__electronUsbSerialEvent: true,
callbackName: cbName,
value: jsonStr
}, '*')
})
}
const usbSerial = {
get onDeviceAttached() { return usbSerialCallbacks.onDeviceAttached },
set onDeviceAttached(fn) { usbSerialCallbacks.onDeviceAttached = fn },
get onDeviceDetached() { return usbSerialCallbacks.onDeviceDetached },
set onDeviceDetached(fn) { usbSerialCallbacks.onDeviceDetached = fn },
get onDeviceDiscovered() { return usbSerialCallbacks.onDeviceDiscovered },
set onDeviceDiscovered(fn) { usbSerialCallbacks.onDeviceDiscovered = fn },
get onDeviceProbeResult() { return usbSerialCallbacks.onDeviceProbeResult },
set onDeviceProbeResult(fn) { usbSerialCallbacks.onDeviceProbeResult = fn },
get onDeviceOpened() { return usbSerialCallbacks.onDeviceOpened },
set onDeviceOpened(fn) { usbSerialCallbacks.onDeviceOpened = fn },
get onDeviceState() { return usbSerialCallbacks.onDeviceState },
set onDeviceState(fn) { usbSerialCallbacks.onDeviceState = fn },
get onDeviceData() { return usbSerialCallbacks.onDeviceData },
set onDeviceData(fn) { usbSerialCallbacks.onDeviceData = fn },
startDiscovery() { ipcRenderer.send('usb-start-discovery') },
stopDiscovery() { ipcRenderer.send('usb-stop-discovery') },
getDiscoveredDevices() { return ipcRenderer.invoke('usb-get-discovered') },
deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) {
ipcRenderer.send('usb-device-probe', { deviceName, portNumber, baudRate, hexCmd, timeoutMs })
},
deviceOpen(deviceName, portNumber, baudRate) {
ipcRenderer.send('usb-device-open', { deviceName, portNumber, baudRate })
},
deviceClose(deviceName, portNumber) {
ipcRenderer.send('usb-device-close', { deviceName, portNumber })
},
deviceWrite(deviceName, portNumber, hexData) {
ipcRenderer.send('usb-device-write', { deviceName, portNumber, hexData })
}
}
function injectUsbSerialBridge() {
const source = `
;(() => {
if (window.usbSerial) return
const callbacks = {
onDeviceAttached: null,
onDeviceDetached: null,
onDeviceDiscovered: null,
onDeviceProbeResult: null,
onDeviceOpened: null,
onDeviceState: null,
onDeviceData: null
}
Object.defineProperty(window, 'usbSerial', {
configurable: true,
enumerable: true,
value: {
get onDeviceAttached() { return callbacks.onDeviceAttached },
set onDeviceAttached(fn) { callbacks.onDeviceAttached = fn },
get onDeviceDetached() { return callbacks.onDeviceDetached },
set onDeviceDetached(fn) { callbacks.onDeviceDetached = fn },
get onDeviceDiscovered() { return callbacks.onDeviceDiscovered },
set onDeviceDiscovered(fn) { callbacks.onDeviceDiscovered = fn },
get onDeviceProbeResult() { return callbacks.onDeviceProbeResult },
set onDeviceProbeResult(fn) { callbacks.onDeviceProbeResult = fn },
get onDeviceOpened() { return callbacks.onDeviceOpened },
set onDeviceOpened(fn) { callbacks.onDeviceOpened = fn },
get onDeviceState() { return callbacks.onDeviceState },
set onDeviceState(fn) { callbacks.onDeviceState = fn },
get onDeviceData() { return callbacks.onDeviceData },
set onDeviceData(fn) { callbacks.onDeviceData = fn },
startDiscovery() { window.__electronNativeUsbSerial.startDiscovery() },
stopDiscovery() { window.__electronNativeUsbSerial.stopDiscovery() },
getDiscoveredDevices() { return window.__electronNativeUsbSerial.getDiscoveredDevices() },
deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) {
return window.__electronNativeUsbSerial.deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs)
},
deviceOpen(deviceName, portNumber, baudRate) {
return window.__electronNativeUsbSerial.deviceOpen(deviceName, portNumber, baudRate)
},
deviceClose(deviceName, portNumber) {
return window.__electronNativeUsbSerial.deviceClose(deviceName, portNumber)
},
deviceWrite(deviceName, portNumber, hexData) {
return window.__electronNativeUsbSerial.deviceWrite(deviceName, portNumber, hexData)
}
}
})
window.addEventListener('message', event => {
const payload = event.data
if (!payload || !payload.__electronUsbSerialEvent) return
const cb = callbacks[payload.callbackName]
if (typeof cb === 'function') cb(payload.value)
})
})()
`
const script = document.createElement('script')
script.textContent = source
;(document.documentElement || document.head).appendChild(script)
script.remove()
}
function injectUsbSerialBridgeWhenReady() {
if (document.documentElement || document.head) {
injectUsbSerialBridge()
return
}
const timer = setInterval(() => {
if (!(document.documentElement || document.head)) return
clearInterval(timer)
injectUsbSerialBridge()
}, 0)
}
injectUsbSerialBridgeWhenReady()
const bluetoothSppCallbacks = {
onDeviceSelected: null,
onDeviceOpened: null,
onDeviceDataReceived: null,
onDeviceStateChanged: null
}
ipcRenderer.on('bluetooth-device-selected', (_event, success, address) => {
if (typeof bluetoothSppCallbacks.onDeviceSelected === 'function') {
bluetoothSppCallbacks.onDeviceSelected(success, address)
}
})
ipcRenderer.on('bluetooth-device-opened', (_event, success, mac) => {
if (typeof bluetoothSppCallbacks.onDeviceOpened === 'function') {
bluetoothSppCallbacks.onDeviceOpened(success, mac)
}
})
ipcRenderer.on('bluetooth-device-data-received', (_event, mac, data) => {
if (typeof bluetoothSppCallbacks.onDeviceDataReceived === 'function') {
bluetoothSppCallbacks.onDeviceDataReceived(mac, data)
}
})
ipcRenderer.on('bluetooth-device-state-changed', (_event, mac, state) => {
if (typeof bluetoothSppCallbacks.onDeviceStateChanged === 'function') {
bluetoothSppCallbacks.onDeviceStateChanged(mac, state)
}
})
const bluetoothSpp = {
deviceSelect(namePrefix) { ipcRenderer.send('bluetooth-device-select', namePrefix) },
deviceOpen(mac) { ipcRenderer.send('bluetooth-device-open', mac) },
deviceWrite(mac, data) { ipcRenderer.send('bluetooth-device-write', mac, data) },
deviceClose(mac) { ipcRenderer.send('bluetooth-device-close', mac) },
get onDeviceSelected() { return bluetoothSppCallbacks.onDeviceSelected },
set onDeviceSelected(fn) { bluetoothSppCallbacks.onDeviceSelected = fn },
get onDeviceOpened() { return bluetoothSppCallbacks.onDeviceOpened },
set onDeviceOpened(fn) { bluetoothSppCallbacks.onDeviceOpened = fn },
get onDeviceDataReceived() { return bluetoothSppCallbacks.onDeviceDataReceived },
set onDeviceDataReceived(fn) { bluetoothSppCallbacks.onDeviceDataReceived = fn },
get onDeviceStateChanged() { return bluetoothSppCallbacks.onDeviceStateChanged },
set onDeviceStateChanged(fn) { bluetoothSppCallbacks.onDeviceStateChanged = fn }
}
contextBridge.exposeInMainWorld('app', app)
contextBridge.exposeInMainWorld('net', net)
contextBridge.exposeInMainWorld('sensor', sensor)
contextBridge.exposeInMainWorld('__electronNativeAppSensor', nativeAppSensor)
contextBridge.exposeInMainWorld('appLocation', appLocation)
contextBridge.exposeInMainWorld('__electronNativeUsbSerial', usbSerial)
contextBridge.exposeInMainWorld('bluetoothSpp', bluetoothSpp)