Skip to content

Commit 8d7586a

Browse files
authored
Merge pull request #160 from arduino/maintenance/serial-refactoring
Refactor serial connection handler - Take 2
2 parents bc53b85 + b5b5219 commit 8d7586a

File tree

8 files changed

+311
-228
lines changed

8 files changed

+311
-228
lines changed

backend/ipc.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const fs = require('fs')
22
const registerMenu = require('./menu.js')
3+
const serial = require('./serial/serial.js').sharedInstance
34

45
const {
56
openFolderDialog,
@@ -9,6 +10,8 @@ const {
910
} = require('./helpers.js')
1011

1112
module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) {
13+
serial.win = win // Required to send callback messages to renderer
14+
1215
ipcMain.handle('open-folder', async (event) => {
1316
console.log('ipcMain', 'open-folder')
1417
const folder = await openFolderDialog(win)
@@ -141,8 +144,8 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) {
141144
win.webContents.send('check-before-close')
142145
})
143146

144-
// handle disconnection before reload
145-
ipcMain.handle('prepare-reload', async (event) => {
146-
return win.webContents.send('before-reload')
147+
ipcMain.handle('serial', (event, command, ...args) => {
148+
console.debug('Handling IPC serial command:', command, ...args)
149+
return serial[command](...args)
147150
})
148151
}

backend/menu.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { app, Menu } = require('electron')
22
const path = require('path')
3+
const serial = require('./serial/serial.js').sharedInstance
34
const openAboutWindow = require('about-window').default
45
const shortcuts = require('./shortcuts.js')
56
const { type } = require('os')
@@ -127,10 +128,8 @@ module.exports = function registerMenu(win, state = {}) {
127128
accelerator: '',
128129
click: async () => {
129130
try {
130-
win.webContents.send('cleanup-before-reload')
131-
setTimeout(() => {
132-
win.reload()
133-
}, 500)
131+
await serial.disconnect()
132+
win.reload()
134133
} catch(e) {
135134
console.error('Reload from menu failed:', e)
136135
}

backend/serial/serial-bridge.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const { ipcRenderer } = require('electron')
2+
const path = require('path')
3+
4+
const SerialBridge = {
5+
loadPorts: async () => {
6+
return await ipcRenderer.invoke('serial', 'loadPorts')
7+
},
8+
connect: async (path) => {
9+
return await ipcRenderer.invoke('serial', 'connect', path)
10+
},
11+
disconnect: async () => {
12+
return await ipcRenderer.invoke('serial', 'disconnect')
13+
},
14+
run: async (code) => {
15+
return await ipcRenderer.invoke('serial', 'run', code)
16+
},
17+
execFile: async (path) => {
18+
return await ipcRenderer.invoke('serial', 'execFile', path)
19+
},
20+
getPrompt: async () => {
21+
return await ipcRenderer.invoke('serial', 'getPrompt')
22+
},
23+
keyboardInterrupt: async () => {
24+
await ipcRenderer.invoke('serial', 'keyboardInterrupt')
25+
return Promise.resolve()
26+
},
27+
reset: async () => {
28+
await ipcRenderer.invoke('serial', 'reset')
29+
return Promise.resolve()
30+
},
31+
eval: (d) => {
32+
return ipcRenderer.invoke('serial', 'eval', d)
33+
},
34+
onData: (callback) => {
35+
// Remove all previous listeners
36+
if (ipcRenderer.listeners("serial-on-data").length > 0) {
37+
ipcRenderer.removeAllListeners("serial-on-data")
38+
}
39+
ipcRenderer.on('serial-on-data', (event, data) => {
40+
callback(data)
41+
})
42+
},
43+
listFiles: async (folder) => {
44+
return await ipcRenderer.invoke('serial', 'listFiles', folder)
45+
},
46+
ilistFiles: async (folder) => {
47+
return await ipcRenderer.invoke('serial', 'ilistFiles', folder)
48+
},
49+
loadFile: async (file) => {
50+
return await ipcRenderer.invoke('serial', 'loadFile', file)
51+
},
52+
removeFile: async (file) => {
53+
return await ipcRenderer.invoke('serial', 'removeFile', file)
54+
},
55+
saveFileContent: async (filename, content, dataConsumer) => {
56+
return await ipcRenderer.invoke('serial', 'saveFileContent', filename, content, dataConsumer)
57+
},
58+
uploadFile: async (src, dest, dataConsumer) => {
59+
return await ipcRenderer.invoke('serial', 'uploadFile', src, dest, dataConsumer)
60+
},
61+
downloadFile: async (src, dest) => {
62+
let contents = await ipcRenderer.invoke('serial', 'loadFile', src)
63+
return ipcRenderer.invoke('save-file', dest, contents)
64+
},
65+
renameFile: async (oldName, newName) => {
66+
return await ipcRenderer.invoke('serial', 'renameFile', oldName, newName)
67+
},
68+
onConnectionClosed: async (callback) => {
69+
// Remove all previous listeners
70+
if (ipcRenderer.listeners("serial-on-connection-closed").length > 0) {
71+
ipcRenderer.removeAllListeners("serial-on-connection-closed")
72+
}
73+
ipcRenderer.on('serial-on-connection-closed', (event) => {
74+
callback()
75+
})
76+
},
77+
createFolder: async (folder) => {
78+
return await ipcRenderer.invoke('serial', 'createFolder', folder)
79+
},
80+
removeFolder: async (folder) => {
81+
return await ipcRenderer.invoke('serial', 'removeFolder', folder)
82+
},
83+
getNavigationPath: (navigation, target) => {
84+
return path.posix.join(navigation, target)
85+
},
86+
getFullPath: (root, navigation, file) => {
87+
return path.posix.join(root, navigation, file)
88+
},
89+
getParentPath: (navigation) => {
90+
return path.posix.dirname(navigation)
91+
},
92+
fileExists: async (filePath) => {
93+
return await ipcRenderer.invoke('serial', 'fileExists', filePath)
94+
}
95+
}
96+
97+
module.exports = SerialBridge

backend/serial/serial.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
const MicroPython = require('micropython.js')
2+
3+
class Serial {
4+
constructor(win = null) {
5+
this.win = win
6+
this.board = new MicroPython()
7+
this.board.chunk_size = 192
8+
this.board.chunk_sleep = 200
9+
}
10+
11+
async loadPorts() {
12+
let ports = await this.board.list_ports()
13+
return ports.filter(p => p.vendorId && p.productId)
14+
}
15+
16+
async connect(path) {
17+
await this.board.open(path)
18+
this.registerCallbacks()
19+
}
20+
21+
async disconnect() {
22+
return await this.board.close()
23+
}
24+
25+
async run(code) {
26+
return await this.board.run(code)
27+
}
28+
29+
async execFile(path) {
30+
return await this.board.execfile(path)
31+
}
32+
33+
async getPrompt() {
34+
return await this.board.get_prompt()
35+
}
36+
37+
async keyboardInterrupt() {
38+
await this.board.stop()
39+
return Promise.resolve()
40+
}
41+
42+
async reset() {
43+
await this.board.stop()
44+
await this.board.exit_raw_repl()
45+
await this.board.reset()
46+
return Promise.resolve()
47+
}
48+
49+
async eval(d) {
50+
return await this.board.eval(d)
51+
}
52+
53+
registerCallbacks() {
54+
this.board.serial.on('data', (data) => {
55+
this.win.webContents.send('serial-on-data', data)
56+
})
57+
58+
this.board.serial.on('close', () => {
59+
this.board.serial.removeAllListeners("data")
60+
this.board.serial.removeAllListeners("close")
61+
this.win.webContents.send('serial-on-connection-closed')
62+
})
63+
}
64+
65+
async listFiles(folder) {
66+
return await this.board.fs_ls(folder)
67+
}
68+
69+
async ilistFiles(folder) {
70+
return await this.board.fs_ils(folder)
71+
}
72+
73+
async loadFile(file) {
74+
const output = await this.board.fs_cat_binary(file)
75+
return output || ''
76+
}
77+
78+
async removeFile(file) {
79+
return await this.board.fs_rm(file)
80+
}
81+
82+
async saveFileContent(filename, content, dataConsumer) {
83+
return await this.board.fs_save(content || ' ', filename, dataConsumer)
84+
}
85+
86+
async uploadFile(src, dest, dataConsumer) {
87+
return await this.board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer)
88+
}
89+
90+
async renameFile(oldName, newName) {
91+
return await this.board.fs_rename(oldName, newName)
92+
}
93+
94+
async createFolder(folder) {
95+
return await this.board.fs_mkdir(folder)
96+
}
97+
98+
async removeFolder(folder) {
99+
return await this.board.fs_rmdir(folder)
100+
}
101+
102+
async fileExists(filePath) {
103+
const output = await this.board.run(`
104+
import os
105+
try:
106+
os.stat("${filePath}")
107+
print(0)
108+
except OSError:
109+
print(1)
110+
`)
111+
return output[2] === '0'
112+
}
113+
}
114+
115+
const sharedInstance = new Serial()
116+
117+
module.exports = {sharedInstance, Serial}

index.js

-18
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,6 @@ function createWindow () {
5050
win.show()
5151
})
5252

53-
win.webContents.on('before-reload', async (event) => {
54-
// Prevent the default reload behavior
55-
event.preventDefault()
56-
57-
try {
58-
// Tell renderer to do cleanup
59-
win.webContents.send('cleanup-before-reload')
60-
61-
// Wait for cleanup then reload
62-
setTimeout(() => {
63-
// This will trigger a page reload, but won't trigger 'before-reload' again
64-
win.reload()
65-
}, 500)
66-
} catch(e) {
67-
console.error('Reload preparation failed:', e)
68-
}
69-
})
70-
7153
const initialMenuState = {
7254
isConnected: false,
7355
view: 'editor'

0 commit comments

Comments
 (0)