Skip to content

Commit 3d4a54b

Browse files
authored
Merge pull request #161 from arduino/development
Development to Main > Release
2 parents adfef3a + 8d7586a commit 3d4a54b

23 files changed

+752
-229
lines changed

Diff for: backend/ipc.js

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
const fs = require('fs')
2+
const registerMenu = require('./menu.js')
3+
const serial = require('./serial/serial.js').sharedInstance
4+
25
const {
36
openFolderDialog,
47
listFolder,
@@ -7,6 +10,8 @@ const {
710
} = require('./helpers.js')
811

912
module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) {
13+
serial.win = win // Required to send callback messages to renderer
14+
1015
ipcMain.handle('open-folder', async (event) => {
1116
console.log('ipcMain', 'open-folder')
1217
const folder = await openFolderDialog(win)
@@ -129,9 +134,18 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) {
129134
return response != opt.cancelId
130135
})
131136

137+
ipcMain.handle('update-menu-state', (event, state) => {
138+
registerMenu(win, state)
139+
})
140+
132141
win.on('close', (event) => {
133142
console.log('BrowserWindow', 'close')
134143
event.preventDefault()
135144
win.webContents.send('check-before-close')
136145
})
146+
147+
ipcMain.handle('serial', (event, command, ...args) => {
148+
console.debug('Handling IPC serial command:', command, ...args)
149+
return serial[command](...args)
150+
})
137151
}

Diff for: backend/menu.js

+77-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
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
5+
const shortcuts = require('./shortcuts.js')
6+
const { type } = require('os')
47

5-
module.exports = function registerMenu(win) {
8+
module.exports = function registerMenu(win, state = {}) {
69
const isMac = process.platform === 'darwin'
710
const template = [
811
...(isMac ? [{
912
label: app.name,
1013
submenu: [
1114
{ role: 'about'},
1215
{ type: 'separator' },
13-
{ role: 'services' },
1416
{ type: 'separator' },
15-
{ role: 'hide' },
17+
{ role: 'hide', accelerator: 'CmdOrCtrl+Shift+H' },
1618
{ role: 'hideOthers' },
1719
{ role: 'unhide' },
1820
{ type: 'separator' },
@@ -35,7 +37,6 @@ module.exports = function registerMenu(win) {
3537
{ role: 'copy' },
3638
{ role: 'paste' },
3739
...(isMac ? [
38-
{ role: 'pasteAndMatchStyle' },
3940
{ role: 'selectAll' },
4041
{ type: 'separator' },
4142
{
@@ -51,11 +52,66 @@ module.exports = function registerMenu(win) {
5152
])
5253
]
5354
},
55+
{
56+
label: 'Board',
57+
submenu: [
58+
{
59+
label: 'Connect',
60+
accelerator: shortcuts.menu.CONNECT,
61+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CONNECT)
62+
},
63+
{
64+
label: 'Disconnect',
65+
accelerator: shortcuts.menu.DISCONNECT,
66+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.DISCONNECT)
67+
},
68+
{ type: 'separator' },
69+
{
70+
label: 'Run',
71+
accelerator: shortcuts.menu.RUN,
72+
enabled: state.isConnected && state.view === 'editor',
73+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RUN)
74+
},
75+
{
76+
label: 'Run selection',
77+
accelerator: isMac ? shortcuts.menu.RUN_SELECTION : shortcuts.menu.RUN_SELECTION_WL,
78+
enabled: state.isConnected && state.view === 'editor',
79+
click: () => win.webContents.send('shortcut-cmd', (isMac ? shortcuts.global.RUN_SELECTION : shortcuts.global.RUN_SELECTION_WL))
80+
},
81+
{
82+
label: 'Stop',
83+
accelerator: shortcuts.menu.STOP,
84+
enabled: state.isConnected && state.view === 'editor',
85+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.STOP)
86+
},
87+
{
88+
label: 'Reset',
89+
accelerator: shortcuts.menu.RESET,
90+
enabled: state.isConnected && state.view === 'editor',
91+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RESET)
92+
},
93+
{ type: 'separator' }
94+
]
95+
},
5496
{
5597
label: 'View',
5698
submenu: [
57-
{ role: 'reload' },
58-
{ role: 'toggleDevTools' },
99+
{
100+
label: 'Editor',
101+
accelerator: shortcuts.menu.EDITOR_VIEW,
102+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.EDITOR_VIEW,)
103+
},
104+
{
105+
label: 'Files',
106+
accelerator: shortcuts.menu.FILES_VIEW,
107+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.FILES_VIEW)
108+
},
109+
{
110+
label: 'Clear terminal',
111+
accelerator: shortcuts.menu.CLEAR_TERMINAL,
112+
enabled: state.isConnected && state.view === 'editor',
113+
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLEAR_TERMINAL)
114+
},
59115
{ type: 'separator' },
60116
{ role: 'resetZoom' },
61117
{ role: 'zoomIn' },
@@ -67,6 +123,20 @@ module.exports = function registerMenu(win) {
67123
{
68124
label: 'Window',
69125
submenu: [
126+
{
127+
label: 'Reload',
128+
accelerator: '',
129+
click: async () => {
130+
try {
131+
await serial.disconnect()
132+
win.reload()
133+
} catch(e) {
134+
console.error('Reload from menu failed:', e)
135+
}
136+
}
137+
},
138+
{ role: 'toggleDevTools'},
139+
{ type: 'separator' },
70140
{ role: 'minimize' },
71141
{ role: 'zoom' },
72142
...(isMac ? [
@@ -75,7 +145,7 @@ module.exports = function registerMenu(win) {
75145
{ type: 'separator' },
76146
{ role: 'window' }
77147
] : [
78-
{ role: 'close' }
148+
79149
])
80150
]
81151
},
@@ -102,7 +172,6 @@ module.exports = function registerMenu(win) {
102172
openAboutWindow({
103173
icon_path: path.resolve(__dirname, '../ui/arduino/media/about_image.png'),
104174
css_path: path.resolve(__dirname, '../ui/arduino/views/about.css'),
105-
// about_page_dir: path.resolve(__dirname, '../ui/arduino/views/'),
106175
copyright: '© Arduino SA 2022',
107176
package_json_dir: path.resolve(__dirname, '..'),
108177
bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues",

Diff for: 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

Diff for: 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}

0 commit comments

Comments
 (0)