Skip to content

Commit

Permalink
Merge pull request #161 from arduino/development
Browse files Browse the repository at this point in the history
Development to Main > Release
  • Loading branch information
ubidefeo authored Dec 16, 2024
2 parents adfef3a + 8d7586a commit 3d4a54b
Show file tree
Hide file tree
Showing 23 changed files with 752 additions and 229 deletions.
14 changes: 14 additions & 0 deletions backend/ipc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const fs = require('fs')
const registerMenu = require('./menu.js')
const serial = require('./serial/serial.js').sharedInstance

const {
openFolderDialog,
listFolder,
Expand All @@ -7,6 +10,8 @@ const {
} = require('./helpers.js')

module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) {
serial.win = win // Required to send callback messages to renderer

ipcMain.handle('open-folder', async (event) => {
console.log('ipcMain', 'open-folder')
const folder = await openFolderDialog(win)
Expand Down Expand Up @@ -129,9 +134,18 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) {
return response != opt.cancelId
})

ipcMain.handle('update-menu-state', (event, state) => {
registerMenu(win, state)
})

win.on('close', (event) => {
console.log('BrowserWindow', 'close')
event.preventDefault()
win.webContents.send('check-before-close')
})

ipcMain.handle('serial', (event, command, ...args) => {
console.debug('Handling IPC serial command:', command, ...args)
return serial[command](...args)
})
}
85 changes: 77 additions & 8 deletions backend/menu.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
const { app, Menu } = require('electron')
const path = require('path')
const serial = require('./serial/serial.js').sharedInstance
const openAboutWindow = require('about-window').default
const shortcuts = require('./shortcuts.js')
const { type } = require('os')

module.exports = function registerMenu(win) {
module.exports = function registerMenu(win, state = {}) {
const isMac = process.platform === 'darwin'
const template = [
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about'},
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hide', accelerator: 'CmdOrCtrl+Shift+H' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
Expand All @@ -35,7 +37,6 @@ module.exports = function registerMenu(win) {
{ role: 'copy' },
{ role: 'paste' },
...(isMac ? [
{ role: 'pasteAndMatchStyle' },
{ role: 'selectAll' },
{ type: 'separator' },
{
Expand All @@ -51,11 +52,66 @@ module.exports = function registerMenu(win) {
])
]
},
{
label: 'Board',
submenu: [
{
label: 'Connect',
accelerator: shortcuts.menu.CONNECT,
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CONNECT)
},
{
label: 'Disconnect',
accelerator: shortcuts.menu.DISCONNECT,
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.DISCONNECT)
},
{ type: 'separator' },
{
label: 'Run',
accelerator: shortcuts.menu.RUN,
enabled: state.isConnected && state.view === 'editor',
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RUN)
},
{
label: 'Run selection',
accelerator: isMac ? shortcuts.menu.RUN_SELECTION : shortcuts.menu.RUN_SELECTION_WL,
enabled: state.isConnected && state.view === 'editor',
click: () => win.webContents.send('shortcut-cmd', (isMac ? shortcuts.global.RUN_SELECTION : shortcuts.global.RUN_SELECTION_WL))
},
{
label: 'Stop',
accelerator: shortcuts.menu.STOP,
enabled: state.isConnected && state.view === 'editor',
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.STOP)
},
{
label: 'Reset',
accelerator: shortcuts.menu.RESET,
enabled: state.isConnected && state.view === 'editor',
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RESET)
},
{ type: 'separator' }
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'toggleDevTools' },
{
label: 'Editor',
accelerator: shortcuts.menu.EDITOR_VIEW,
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.EDITOR_VIEW,)
},
{
label: 'Files',
accelerator: shortcuts.menu.FILES_VIEW,
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.FILES_VIEW)
},
{
label: 'Clear terminal',
accelerator: shortcuts.menu.CLEAR_TERMINAL,
enabled: state.isConnected && state.view === 'editor',
click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLEAR_TERMINAL)
},
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
Expand All @@ -67,6 +123,20 @@ module.exports = function registerMenu(win) {
{
label: 'Window',
submenu: [
{
label: 'Reload',
accelerator: '',
click: async () => {
try {
await serial.disconnect()
win.reload()
} catch(e) {
console.error('Reload from menu failed:', e)
}
}
},
{ role: 'toggleDevTools'},
{ type: 'separator' },
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac ? [
Expand All @@ -75,7 +145,7 @@ module.exports = function registerMenu(win) {
{ type: 'separator' },
{ role: 'window' }
] : [
{ role: 'close' }

])
]
},
Expand All @@ -102,7 +172,6 @@ module.exports = function registerMenu(win) {
openAboutWindow({
icon_path: path.resolve(__dirname, '../ui/arduino/media/about_image.png'),
css_path: path.resolve(__dirname, '../ui/arduino/views/about.css'),
// about_page_dir: path.resolve(__dirname, '../ui/arduino/views/'),
copyright: '© Arduino SA 2022',
package_json_dir: path.resolve(__dirname, '..'),
bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues",
Expand Down
97 changes: 97 additions & 0 deletions backend/serial/serial-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const { ipcRenderer } = require('electron')
const path = require('path')

const SerialBridge = {
loadPorts: async () => {
return await ipcRenderer.invoke('serial', 'loadPorts')
},
connect: async (path) => {
return await ipcRenderer.invoke('serial', 'connect', path)
},
disconnect: async () => {
return await ipcRenderer.invoke('serial', 'disconnect')
},
run: async (code) => {
return await ipcRenderer.invoke('serial', 'run', code)
},
execFile: async (path) => {
return await ipcRenderer.invoke('serial', 'execFile', path)
},
getPrompt: async () => {
return await ipcRenderer.invoke('serial', 'getPrompt')
},
keyboardInterrupt: async () => {
await ipcRenderer.invoke('serial', 'keyboardInterrupt')
return Promise.resolve()
},
reset: async () => {
await ipcRenderer.invoke('serial', 'reset')
return Promise.resolve()
},
eval: (d) => {
return ipcRenderer.invoke('serial', 'eval', d)
},
onData: (callback) => {
// Remove all previous listeners
if (ipcRenderer.listeners("serial-on-data").length > 0) {
ipcRenderer.removeAllListeners("serial-on-data")
}
ipcRenderer.on('serial-on-data', (event, data) => {
callback(data)
})
},
listFiles: async (folder) => {
return await ipcRenderer.invoke('serial', 'listFiles', folder)
},
ilistFiles: async (folder) => {
return await ipcRenderer.invoke('serial', 'ilistFiles', folder)
},
loadFile: async (file) => {
return await ipcRenderer.invoke('serial', 'loadFile', file)
},
removeFile: async (file) => {
return await ipcRenderer.invoke('serial', 'removeFile', file)
},
saveFileContent: async (filename, content, dataConsumer) => {
return await ipcRenderer.invoke('serial', 'saveFileContent', filename, content, dataConsumer)
},
uploadFile: async (src, dest, dataConsumer) => {
return await ipcRenderer.invoke('serial', 'uploadFile', src, dest, dataConsumer)
},
downloadFile: async (src, dest) => {
let contents = await ipcRenderer.invoke('serial', 'loadFile', src)
return ipcRenderer.invoke('save-file', dest, contents)
},
renameFile: async (oldName, newName) => {
return await ipcRenderer.invoke('serial', 'renameFile', oldName, newName)
},
onConnectionClosed: async (callback) => {
// Remove all previous listeners
if (ipcRenderer.listeners("serial-on-connection-closed").length > 0) {
ipcRenderer.removeAllListeners("serial-on-connection-closed")
}
ipcRenderer.on('serial-on-connection-closed', (event) => {
callback()
})
},
createFolder: async (folder) => {
return await ipcRenderer.invoke('serial', 'createFolder', folder)
},
removeFolder: async (folder) => {
return await ipcRenderer.invoke('serial', 'removeFolder', folder)
},
getNavigationPath: (navigation, target) => {
return path.posix.join(navigation, target)
},
getFullPath: (root, navigation, file) => {
return path.posix.join(root, navigation, file)
},
getParentPath: (navigation) => {
return path.posix.dirname(navigation)
},
fileExists: async (filePath) => {
return await ipcRenderer.invoke('serial', 'fileExists', filePath)
}
}

module.exports = SerialBridge
117 changes: 117 additions & 0 deletions backend/serial/serial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const MicroPython = require('micropython.js')

class Serial {
constructor(win = null) {
this.win = win
this.board = new MicroPython()
this.board.chunk_size = 192
this.board.chunk_sleep = 200
}

async loadPorts() {
let ports = await this.board.list_ports()
return ports.filter(p => p.vendorId && p.productId)
}

async connect(path) {
await this.board.open(path)
this.registerCallbacks()
}

async disconnect() {
return await this.board.close()
}

async run(code) {
return await this.board.run(code)
}

async execFile(path) {
return await this.board.execfile(path)
}

async getPrompt() {
return await this.board.get_prompt()
}

async keyboardInterrupt() {
await this.board.stop()
return Promise.resolve()
}

async reset() {
await this.board.stop()
await this.board.exit_raw_repl()
await this.board.reset()
return Promise.resolve()
}

async eval(d) {
return await this.board.eval(d)
}

registerCallbacks() {
this.board.serial.on('data', (data) => {
this.win.webContents.send('serial-on-data', data)
})

this.board.serial.on('close', () => {
this.board.serial.removeAllListeners("data")
this.board.serial.removeAllListeners("close")
this.win.webContents.send('serial-on-connection-closed')
})
}

async listFiles(folder) {
return await this.board.fs_ls(folder)
}

async ilistFiles(folder) {
return await this.board.fs_ils(folder)
}

async loadFile(file) {
const output = await this.board.fs_cat_binary(file)
return output || ''
}

async removeFile(file) {
return await this.board.fs_rm(file)
}

async saveFileContent(filename, content, dataConsumer) {
return await this.board.fs_save(content || ' ', filename, dataConsumer)
}

async uploadFile(src, dest, dataConsumer) {
return await this.board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer)
}

async renameFile(oldName, newName) {
return await this.board.fs_rename(oldName, newName)
}

async createFolder(folder) {
return await this.board.fs_mkdir(folder)
}

async removeFolder(folder) {
return await this.board.fs_rmdir(folder)
}

async fileExists(filePath) {
const output = await this.board.run(`
import os
try:
os.stat("${filePath}")
print(0)
except OSError:
print(1)
`)
return output[2] === '0'
}
}

const sharedInstance = new Serial()

module.exports = {sharedInstance, Serial}
Loading

0 comments on commit 3d4a54b

Please sign in to comment.