Skip to content

Commit fe38394

Browse files
committed
Added serial monitor
1 parent b941977 commit fe38394

File tree

6 files changed

+265
-10
lines changed

6 files changed

+265
-10
lines changed

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fabricaio-app",
3-
"version": "0.2.6",
3+
"version": "0.2.7",
44
"description": "App for configuring Fabrica-IO devices",
55
"productName": "Fabrica-IO App",
66
"author": "Sam Groveman <Groveman@fabrica-io.com>",

src-electron/electron-main.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'
55
import { initialize, enable } from '@electron/remote/main/index.js'
66
import fs from 'fs/promises'
77
import { spawn, execFile } from 'child_process'
8-
import { SerialPort } from 'serialport'
8+
import { SerialPort, ReadlineParser } from 'serialport'
99
import AdmZip from 'adm-zip'
1010
import electronUpdater, { type AppUpdater } from 'electron-updater'
1111

@@ -26,6 +26,46 @@ let mainWindow: BrowserWindow | undefined
2626

2727
initialize()
2828

29+
// Serial port management
30+
let serialport: SerialPort | null = null
31+
let parser: ReadlineParser | null = null
32+
33+
// Close and cleanup serial port
34+
const closeSerialPort = async (): Promise<void> => {
35+
return new Promise((resolve) => {
36+
if (parser) {
37+
parser.removeAllListeners('data')
38+
parser.removeAllListeners('error')
39+
parser.destroy()
40+
parser = null
41+
}
42+
43+
if (serialport) {
44+
serialport.removeAllListeners('open')
45+
serialport.removeAllListeners('error')
46+
if (serialport.isOpen) {
47+
serialport.close((err) => {
48+
if (err) {
49+
console.error('Error closing port:', err)
50+
}
51+
console.log('Serial port closed event fired')
52+
if (serialport) {
53+
serialport.destroy()
54+
}
55+
serialport = null
56+
resolve()
57+
})
58+
} else {
59+
serialport.destroy()
60+
serialport = null
61+
resolve()
62+
}
63+
} else {
64+
resolve()
65+
}
66+
})
67+
}
68+
2969
function createWindow() {
3070
// process.env.PATH += ':/run/host/usr/bin' // Possible Flatpak fix not currently in use
3171
/**
@@ -320,6 +360,79 @@ ipcMain.handle('get-serial-ports', async () => {
320360
}
321361
})
322362

363+
// IPC handler to open serial port and start monitoring
364+
ipcMain.handle('serial:open', async (event, { path: portPath, baudRate }) => {
365+
try {
366+
// Close any existing port
367+
await closeSerialPort()
368+
await new Promise((resolve) => setTimeout(resolve, 100))
369+
370+
// Create new port with autoOpen disabled for manual control
371+
serialport = new SerialPort({
372+
path: portPath,
373+
baudRate: baudRate || 115200,
374+
autoOpen: false,
375+
})
376+
377+
// Create parser for line-based data (one event per line)
378+
parser = new ReadlineParser({ delimiter: '\n' })
379+
380+
// Register all event handlers before piping and opening the port
381+
serialport.once('open', () => {
382+
console.log('Serial port opened event fired')
383+
mainWindow?.webContents.send('serial:opened')
384+
})
385+
386+
serialport.on('error', (error) => {
387+
console.error('Serial port error event:', error.message)
388+
mainWindow?.webContents.send('serial:error', error.message)
389+
})
390+
391+
// Parser data handler - fires for each complete line (delimiter = '\n')
392+
parser.on('data', (line: string) => {
393+
console.log('Parser received line:', line)
394+
mainWindow?.webContents.send('serial:data', line)
395+
})
396+
397+
parser.on('error', (error) => {
398+
console.error('Parser error event:', error.message)
399+
mainWindow?.webContents.send('serial:error', `Parser error: ${error.message}`)
400+
})
401+
402+
// Pipe port to parser for later use if needed
403+
console.log('Piping serialport to parser')
404+
serialport.pipe(parser)
405+
406+
// Finally open the port
407+
console.log('Opening serial port:', portPath)
408+
serialport.open((err) => {
409+
if (err) {
410+
console.error('Failed to open port:', err.message)
411+
mainWindow?.webContents.send('serial:error', err.message)
412+
} else {
413+
console.log('Serial port opened successfully')
414+
}
415+
})
416+
417+
return true
418+
} catch (error) {
419+
console.error('Error in serial:open:', error)
420+
await closeSerialPort()
421+
return false
422+
}
423+
})
424+
425+
// IPC handler to close serial port
426+
ipcMain.handle('serial:close', async () => {
427+
try {
428+
await closeSerialPort()
429+
return true
430+
} catch (error) {
431+
console.error('Error closing serial port:', error)
432+
return false
433+
}
434+
})
435+
323436
// IPC for flashing chips using esptool
324437
ipcMain.handle('flash-firmware', async (event, data): Promise<boolean> => {
325438
let command: string

src-electron/electron-preload.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,35 @@ contextBridge.exposeInMainWorld('myWindowAPI', {
123123
},
124124
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
125125
})
126+
127+
// Serial port monitoring API
128+
contextBridge.exposeInMainWorld('serialAPI', {
129+
// Command handlers (promise-based)
130+
openPort: (path: string, baudRate: number): Promise<boolean> =>
131+
ipcRenderer.invoke('serial:open', { path, baudRate }),
132+
133+
closePort: (): Promise<boolean> => ipcRenderer.invoke('serial:close'),
134+
135+
// Event listeners (register persistent callbacks)
136+
onOpened: (callback: () => void) => {
137+
ipcRenderer.on('serial:opened', callback)
138+
},
139+
140+
onData: (callback: (line: string) => void) => {
141+
ipcRenderer.on('serial:data', (_event, line) => callback(line))
142+
},
143+
144+
onError: (callback: (error: string) => void) => {
145+
ipcRenderer.on('serial:error', (_event, error) => callback(error))
146+
},
147+
148+
onClosed: (callback: () => void) => {
149+
ipcRenderer.on('serial:closed', callback)
150+
},
151+
removeListeners: () => {
152+
ipcRenderer.removeAllListeners('serial:opened')
153+
ipcRenderer.removeAllListeners('serial:data')
154+
ipcRenderer.removeAllListeners('serial:error')
155+
ipcRenderer.removeAllListeners('serial:closed')
156+
},
157+
})

src/components/MenuBar.vue

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@
108108
</q-item-section>
109109
<q-item-section> Upload Firmware </q-item-section>
110110
</q-item>
111+
<q-item clickable v-close-popup @click="monitorSerialPort">
112+
<q-item-section side class="menu-icon">
113+
<q-icon name="terminal" />
114+
</q-item-section>
115+
<q-item-section>Monitor Serial Output</q-item-section>
116+
</q-item>
111117
<q-separator />
112118
<q-item clickable v-close-popup @click="setOTADevice">
113119
<q-item-section side class="menu-icon">
@@ -360,7 +366,13 @@
360366
<pre id="build-output"></pre>
361367
</q-card-section>
362368
<q-card-actions align="right">
363-
<q-btn flat label="OK" :disable="buildInProgress" color="primary" v-close-popup />
369+
<q-btn
370+
flat
371+
label="OK"
372+
:disable="buildInProgress || monitoring"
373+
color="primary"
374+
v-close-popup
375+
/>
364376
<q-btn flat label="Cancel" @click="cancelBuild" color="primary" />
365377
</q-card-actions>
366378
</q-card>
@@ -493,6 +505,7 @@ import { computed, ref, onMounted, onUnmounted } from 'vue'
493505
import { Dialog } from 'quasar'
494506
import type electronUpdater from 'electron-updater'
495507
import { useQuasar } from 'quasar'
508+
import { nextTick } from 'vue'
496509
497510
// Quasar object
498511
const $q = useQuasar()
@@ -685,6 +698,90 @@ const getPortLabel = (portPath: string): string => {
685698
return portOptions.value.find((port) => port.value === portPath)?.label || portPath
686699
}
687700
701+
// Serial monitoring variables
702+
const monitoring = ref(false)
703+
704+
// Monitors serial port
705+
const monitorSerialPort = async () => {
706+
if (monitoring.value || buildInProgress.value) {
707+
return
708+
}
709+
710+
if (portPath.value === '') {
711+
createDialog('Error', 'No serial port selected')
712+
return
713+
}
714+
715+
monitoring.value = true
716+
buildDialogOpen.value = true
717+
await nextTick()
718+
const outputElement = document.getElementById('build-output')
719+
720+
if (outputElement) {
721+
outputElement.textContent = `Opening serial port: ${selectedPort.value?.label || 'Unknown'}\n`
722+
}
723+
724+
try {
725+
// Register event handlers before opening
726+
window.serialAPI.onOpened(() => {
727+
if (outputElement) {
728+
outputElement.textContent += 'Serial port opened successfully. Monitoring...\n'
729+
}
730+
})
731+
732+
window.serialAPI.onData((line: string) => {
733+
const outputElement = document.getElementById('build-output')
734+
if (outputElement) {
735+
outputElement.textContent += line + '\n'
736+
outputElement.scrollTop = outputElement.scrollHeight
737+
}
738+
})
739+
740+
window.serialAPI.onError((error: string) => {
741+
if (outputElement) {
742+
outputElement.textContent += `Error: ${error}\n`
743+
}
744+
monitoring.value = false
745+
})
746+
747+
window.serialAPI.onClosed(() => {
748+
if (outputElement) {
749+
outputElement.textContent += 'Serial port closed.\n'
750+
}
751+
monitoring.value = false
752+
})
753+
754+
// Open the port
755+
const baud = portSelectMode.value === 'advanced' ? parseInt(serialBaud.value) || 115200 : 115200
756+
const success = await window.serialAPI.openPort(selectedPort.value?.value || '', baud)
757+
758+
if (!success) {
759+
if (outputElement) {
760+
outputElement.textContent += 'Failed to open serial port\n'
761+
}
762+
monitoring.value = false
763+
}
764+
} catch (error) {
765+
if (outputElement) {
766+
outputElement.textContent += `Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`
767+
}
768+
monitoring.value = false
769+
}
770+
}
771+
772+
// Stops monitoring the serial port
773+
const stopMonitoringSerialPort = async () => {
774+
// Remove any old listeners from previous sessions
775+
window.serialAPI.removeListeners?.()
776+
try {
777+
await window.serialAPI.closePort()
778+
monitoring.value = false
779+
} catch (error) {
780+
console.error('Error closing serial port:', error)
781+
monitoring.value = false
782+
}
783+
}
784+
688785
// OTA variables
689786
const OTADialogOpen = ref(false)
690787
const OTAUpdateDialog = ref(false)
@@ -1206,7 +1303,7 @@ const writeMain = async (storage: string, pins: number[], wifi: string): Promise
12061303
12071304
// Builds (compiles) project with PlatformIO docker container
12081305
const compileWithDocker = async (): Promise<boolean> => {
1209-
if (!buildInProgress.value) {
1306+
if (!buildInProgress.value && !monitoring.value) {
12101307
buildInProgress.value = true
12111308
buildDialogOpen.value = true
12121309
const command = 'docker'
@@ -1277,14 +1374,17 @@ const cancelBuild = async () => {
12771374
if (success) {
12781375
buildInProgress.value = false
12791376
}
1377+
} else if (monitoring.value) {
1378+
stopMonitoringSerialPort()
1379+
buildDialogOpen.value = false
12801380
} else {
12811381
buildDialogOpen.value = false
12821382
}
12831383
}
12841384
12851385
// Flashes firmware through serial
12861386
const flashFirmware = async (): Promise<boolean> => {
1287-
if (!buildInProgress.value) {
1387+
if (!buildInProgress.value && !monitoring.value) {
12881388
buildInProgress.value = true
12891389
buildDialogOpen.value = true
12901390
const dirChar = window.shell.platform === 'win32' ? '\\' : '/'

src/global.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,15 @@ declare global {
5858
>
5959
flashFirmware: (data: { port: string; baud: string; projPath: string }) => Promise<boolean>
6060
}
61+
serialAPI: {
62+
openPort: (path: string, baudRate: number) => Promise<boolean>
63+
closePort: () => Promise<boolean>
64+
onOpened: (callback: () => void) => void
65+
onData: (callback: (line: string) => void) => void
66+
onError: (callback: (error: string) => void) => void
67+
onClosed: (callback: () => void) => void
68+
removeAllListeners: () => Promise<boolean>
69+
removeListeners: () => void
70+
}
6171
}
6272
}

0 commit comments

Comments
 (0)