From 48881ce6903b0238cc880eccdc4917fb2ada197e Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 14:13:43 +0100 Subject: [PATCH 01/12] Use better name for callback --- ui/arduino/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index c4de3ab..435066a 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -180,7 +180,7 @@ async function store(state, emitter) { term.write(data) term.scrollToBottom() }) - serial.onDisconnect(() => emitter.emit('disconnect')) + serial.onConnectionLost(() => emitter.emit('disconnect')) emitter.emit('close-connection-dialog') emitter.emit('refresh-files') From 42f8f9c5e5a06a445e89cbfb1df4625daa25e6d8 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 14:14:25 +0100 Subject: [PATCH 02/12] Move serial logic to separate file --- backend/menu.js | 7 ++-- backend/serial.js | 102 ++++++++++++++++++++++++++++++++++++++++++++++ preload.js | 3 +- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 backend/serial.js diff --git a/backend/menu.js b/backend/menu.js index eb7810e..b372527 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,5 +1,6 @@ const { app, Menu } = require('electron') const path = require('path') +const Serial = require('./serial.js') const openAboutWindow = require('about-window').default const shortcuts = require('./shortcuts.js') const { type } = require('os') @@ -127,10 +128,8 @@ module.exports = function registerMenu(win, state = {}) { accelerator: '', click: async () => { try { - win.webContents.send('cleanup-before-reload') - setTimeout(() => { - win.reload() - }, 500) + await Serial.disconnect() + win.reload() } catch(e) { console.error('Reload from menu failed:', e) } diff --git a/backend/serial.js b/backend/serial.js new file mode 100644 index 0000000..35a49a3 --- /dev/null +++ b/backend/serial.js @@ -0,0 +1,102 @@ +const MicroPython = require('micropython.js') + +const board = new MicroPython() +board.chunk_size = 192 +board.chunk_sleep = 200 + +const Serial = { + loadPorts: async () => { + let ports = await board.list_ports() + return ports.filter(p => p.vendorId && p.productId) + }, + connect: async (path) => { + return board.open(path) + }, + disconnect: async () => { + return board.close() + }, + run: async (code) => { + return board.run(code) + }, + execFile: async (path) => { + return board.execfile(path) + }, + getPrompt: async () => { + return board.get_prompt() + }, + keyboardInterrupt: async () => { + await board.stop() + return Promise.resolve() + }, + reset: async () => { + await board.stop() + await board.exit_raw_repl() + await board.reset() + return Promise.resolve() + }, + eval: (d) => { + return board.eval(d) + }, + onData: (fn) => { + board.serial.on('data', fn) + }, + listFiles: async (folder) => { + return board.fs_ls(folder) + }, + ilistFiles: async (folder) => { + return board.fs_ils(folder) + }, + loadFile: async (file) => { + const output = await board.fs_cat_binary(file) + return output || '' + }, + removeFile: async (file) => { + return board.fs_rm(file) + }, + saveFileContent: async (filename, content, dataConsumer) => { + return board.fs_save(content || ' ', filename, dataConsumer) + }, + uploadFile: async (src, dest, dataConsumer) => { + return board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer) + }, + downloadFile: async (src, dest) => { + let contents = await Serial.loadFile(src) + return ipcRenderer.invoke('save-file', dest, contents) + }, + renameFile: async (oldName, newName) => { + return board.fs_rename(oldName, newName) + }, + onConnectionLost: async (fn) => { + board.serial.on('close', fn) + }, + createFolder: async (folder) => { + return await board.fs_mkdir(folder) + }, + removeFolder: async (folder) => { + return await board.fs_rmdir(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) => { + // !!!: Fix this on micropython.js level + // ???: Check if file exists is not part of mpremote specs + const output = await board.run(` +import os +try: + os.stat("${filePath}") + print(0) +except OSError: + print(1) +`) + return output[2] === '0' + } +} + +module.exports = Serial \ No newline at end of file diff --git a/preload.js b/preload.js index dd4f28f..a7c1361 100644 --- a/preload.js +++ b/preload.js @@ -1,6 +1,7 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') +const Serial = require('./backend/serial.js') const shortcuts = require('./backend/shortcuts.js').global const MicroPython = require('micropython.js') const { emit, platform } = require('process') @@ -191,4 +192,4 @@ const Window = { contextBridge.exposeInMainWorld('BridgeSerial', Serial) contextBridge.exposeInMainWorld('BridgeDisk', Disk) -contextBridge.exposeInMainWorld('BridgeWindow', Window) +contextBridge.exposeInMainWorld('BridgeWindow', Window) \ No newline at end of file From cc5a8ee4cbffbeafc3fbe16f306454ae6c2a9371 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 14:41:37 +0100 Subject: [PATCH 03/12] Decouple UI and logic of disconnection event --- backend/serial.js | 2 +- ui/arduino/store.js | 16 ++++++++++++---- ui/arduino/views/components/toolbar.js | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/serial.js b/backend/serial.js index 35a49a3..ec0d6b7 100644 --- a/backend/serial.js +++ b/backend/serial.js @@ -66,7 +66,7 @@ const Serial = { renameFile: async (oldName, newName) => { return board.fs_rename(oldName, newName) }, - onConnectionLost: async (fn) => { + onConnectionClosed: async (fn) => { board.serial.on('close', fn) }, createFolder: async (folder) => { diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 435066a..4c18e7b 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -114,7 +114,8 @@ async function store(state, emitter) { // CONNECTION DIALOG emitter.on('open-connection-dialog', async () => { log('open-connection-dialog') - emitter.emit('disconnect') + // UI should be in disconnected state, no need to update + await serial.disconnect() state.availablePorts = await getAvailablePorts() state.isConnectionDialogOpen = true emitter.emit('render') @@ -180,14 +181,16 @@ async function store(state, emitter) { term.write(data) term.scrollToBottom() }) - serial.onConnectionLost(() => emitter.emit('disconnect')) + + // Update the UI when the conncetion is closed + // This may happen when unplugging the board + serial.onConnectionClosed(() => emitter.emit('disconnected')) emitter.emit('close-connection-dialog') emitter.emit('refresh-files') emitter.emit('render') }) - emitter.on('disconnect', async () => { - await serial.disconnect() + emitter.on('disconnected', () => { state.isConnected = false state.panelHeight = PANEL_CLOSED state.boardFiles = [] @@ -196,6 +199,11 @@ async function store(state, emitter) { emitter.emit('render') updateMenu() }) + emitter.on('disconnect', async () => { + await serial.disconnect() + // Update the UI after closing the connection + emitter.emit('disconnected') + }) emitter.on('connection-timeout', async () => { state.isConnected = false state.isConnecting = false diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 0e3d497..70982b0 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -16,7 +16,7 @@ function Toolbar(state, emit) { ${Button({ icon: state.isConnected ? 'connect.svg' : 'disconnect.svg', tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, - onClick: () => emit('open-connection-dialog'), + onClick: () => state.isConnected ? emit('disconnect') : emit('open-connection-dialog'), active: state.isConnected })} From 88e0bda89b9f471a82e620bf05973378cc99e444 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 15:40:13 +0100 Subject: [PATCH 04/12] Use serial bridge to execute serial commands --- backend/bridge/serial-bridge.js | 89 +++++++++++++++ backend/ipc.js | 9 ++ backend/serial.js | 191 +++++++++++++++++--------------- preload.js | 3 +- ui/arduino/store.js | 156 +++++++++++++------------- 5 files changed, 279 insertions(+), 169 deletions(-) create mode 100644 backend/bridge/serial-bridge.js diff --git a/backend/bridge/serial-bridge.js b/backend/bridge/serial-bridge.js new file mode 100644 index 0000000..54f7221 --- /dev/null +++ b/backend/bridge/serial-bridge.js @@ -0,0 +1,89 @@ +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) => { + 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) => { + 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 \ No newline at end of file diff --git a/backend/ipc.js b/backend/ipc.js index 12f2127..f554010 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,4 +1,6 @@ const fs = require('fs') +const Serial = require('./serial.js') +let serial const registerMenu = require('./menu.js') const { @@ -9,6 +11,8 @@ const { } = require('./helpers.js') module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { + serial = new Serial(win) + ipcMain.handle('open-folder', async (event) => { console.log('ipcMain', 'open-folder') const folder = await openFolderDialog(win) @@ -145,4 +149,9 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { ipcMain.handle('prepare-reload', async (event) => { return win.webContents.send('before-reload') }) + + ipcMain.handle('serial', (event, command, ...args) => { + console.debug('Handling IPC serial command:', command, ...args) + return serial[command](...args) + }) } diff --git a/backend/serial.js b/backend/serial.js index ec0d6b7..72357c7 100644 --- a/backend/serial.js +++ b/backend/serial.js @@ -1,93 +1,104 @@ const MicroPython = require('micropython.js') -const board = new MicroPython() -board.chunk_size = 192 -board.chunk_sleep = 200 - -const Serial = { - loadPorts: async () => { - let ports = await board.list_ports() - return ports.filter(p => p.vendorId && p.productId) - }, - connect: async (path) => { - return board.open(path) - }, - disconnect: async () => { - return board.close() - }, - run: async (code) => { - return board.run(code) - }, - execFile: async (path) => { - return board.execfile(path) - }, - getPrompt: async () => { - return board.get_prompt() - }, - keyboardInterrupt: async () => { - await board.stop() - return Promise.resolve() - }, - reset: async () => { - await board.stop() - await board.exit_raw_repl() - await board.reset() - return Promise.resolve() - }, - eval: (d) => { - return board.eval(d) - }, - onData: (fn) => { - board.serial.on('data', fn) - }, - listFiles: async (folder) => { - return board.fs_ls(folder) - }, - ilistFiles: async (folder) => { - return board.fs_ils(folder) - }, - loadFile: async (file) => { - const output = await board.fs_cat_binary(file) - return output || '' - }, - removeFile: async (file) => { - return board.fs_rm(file) - }, - saveFileContent: async (filename, content, dataConsumer) => { - return board.fs_save(content || ' ', filename, dataConsumer) - }, - uploadFile: async (src, dest, dataConsumer) => { - return board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer) - }, - downloadFile: async (src, dest) => { - let contents = await Serial.loadFile(src) - return ipcRenderer.invoke('save-file', dest, contents) - }, - renameFile: async (oldName, newName) => { - return board.fs_rename(oldName, newName) - }, - onConnectionClosed: async (fn) => { - board.serial.on('close', fn) - }, - createFolder: async (folder) => { - return await board.fs_mkdir(folder) - }, - removeFolder: async (folder) => { - return await board.fs_rmdir(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) => { - // !!!: Fix this on micropython.js level - // ???: Check if file exists is not part of mpremote specs - const output = await board.run(` +class Serial { + constructor(win) { + 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.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}") @@ -95,8 +106,8 @@ try: except OSError: print(1) `) - return output[2] === '0' - } + return output[2] === '0' + } } module.exports = Serial \ No newline at end of file diff --git a/preload.js b/preload.js index a7c1361..7549868 100644 --- a/preload.js +++ b/preload.js @@ -5,6 +5,7 @@ const Serial = require('./backend/serial.js') const shortcuts = require('./backend/shortcuts.js').global const MicroPython = require('micropython.js') const { emit, platform } = require('process') +const SerialBridge = require('./backend/bridge/serial-bridge.js') const board = new MicroPython() board.chunk_size = 192 @@ -190,6 +191,6 @@ const Window = { getShortcuts: () => shortcuts } -contextBridge.exposeInMainWorld('BridgeSerial', Serial) +contextBridge.exposeInMainWorld('BridgeSerial', SerialBridge) contextBridge.exposeInMainWorld('BridgeDisk', Disk) contextBridge.exposeInMainWorld('BridgeWindow', Window) \ No newline at end of file diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 4c18e7b..1e49661 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1,5 +1,5 @@ const log = console.log -const serial = window.BridgeSerial +const serialBridge = window.BridgeSerial const disk = window.BridgeDisk const win = window.BridgeWindow @@ -115,7 +115,7 @@ async function store(state, emitter) { emitter.on('open-connection-dialog', async () => { log('open-connection-dialog') // UI should be in disconnected state, no need to update - await serial.disconnect() + await serialBridge.disconnect() state.availablePorts = await getAvailablePorts() state.isConnectionDialogOpen = true emitter.emit('render') @@ -149,14 +149,14 @@ async function store(state, emitter) { emitter.emit('connection-timeout') }, 3500) try { - await serial.connect(path) + await serialBridge.connect(path) } catch(e) { console.error(e) } // Stop whatever is going on // Recover from getting stuck in raw repl - await serial.getPrompt() + await serialBridge.getPrompt() clearTimeout(timeout_id) // Connected and ready state.isConnecting = false @@ -172,19 +172,19 @@ async function store(state, emitter) { if (!state.isTerminalBound) { state.isTerminalBound = true term.onData((data) => { - serial.eval(data) + serialBridge.eval(data) term.scrollToBottom() }) - serial.eval('\x02') + serialBridge.eval('\x02') } - serial.onData((data) => { + serialBridge.onData((data) => { term.write(data) term.scrollToBottom() }) // Update the UI when the conncetion is closed // This may happen when unplugging the board - serial.onConnectionClosed(() => emitter.emit('disconnected')) + serialBridge.onConnectionClosed(() => emitter.emit('disconnected')) emitter.emit('close-connection-dialog') emitter.emit('refresh-files') @@ -200,7 +200,7 @@ async function store(state, emitter) { updateMenu() }) emitter.on('disconnect', async () => { - await serial.disconnect() + await serialBridge.disconnect() // Update the UI after closing the connection emitter.emit('disconnected') }) @@ -236,8 +236,8 @@ async function store(state, emitter) { emitter.emit('open-panel') emitter.emit('render') try { - await serial.getPrompt() - await serial.run(code) + await serialBridge.getPrompt() + await serialBridge.run(code) } catch(e) { log('error', e) } @@ -249,7 +249,7 @@ async function store(state, emitter) { } emitter.emit('open-panel') emitter.emit('render') - await serial.getPrompt() + await serialBridge.getPrompt() }) emitter.on('reset', async () => { log('reset') @@ -258,7 +258,7 @@ async function store(state, emitter) { } emitter.emit('open-panel') emitter.emit('render') - await serial.reset() + await serialBridge.reset() emitter.emit('update-files') emitter.emit('render') }) @@ -334,9 +334,9 @@ async function store(state, emitter) { // Check if the current full path exists let fullPathExists = false if (openFile.source == 'board') { - await serial.getPrompt() - fullPathExists = await serial.fileExists( - serial.getFullPath( + await serialBridge.getPrompt() + fullPathExists = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -357,9 +357,9 @@ async function store(state, emitter) { if (openFile.source == 'board') { openFile.parentFolder = state.boardNavigationPath // Check for overwrite - await serial.getPrompt() - willOverwrite = await serial.fileExists( - serial.getFullPath( + await serialBridge.getPrompt() + willOverwrite = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -392,9 +392,9 @@ async function store(state, emitter) { const contents = openFile.editor.editor.state.doc.toString() try { if (openFile.source == 'board') { - await serial.getPrompt() - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.getPrompt() + await serialBridge.saveFileContent( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -470,7 +470,7 @@ async function store(state, emitter) { if (state.isConnected) { try { state.boardFiles = await getBoardFiles( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, '' @@ -548,8 +548,8 @@ async function store(state, emitter) { } // TODO: Remove existing file } - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.saveFileContent( + serialBridge.getFullPath( '/', state.boardNavigationPath, value @@ -619,15 +619,15 @@ async function store(state, emitter) { } // Remove existing folder await removeBoardFolder( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value ) ) } - await serial.createFolder( - serial.getFullPath( + await serialBridge.createFolder( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value @@ -709,7 +709,7 @@ async function store(state, emitter) { if (file.type == 'folder') { if (file.source === 'board') { await removeBoardFolder( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName @@ -726,8 +726,8 @@ async function store(state, emitter) { } } else { if (file.source === 'board') { - await serial.removeFile( - serial.getFullPath( + await serialBridge.removeFile( + serialBridge.getFullPath( '/', state.boardNavigationPath, file.fileName @@ -795,15 +795,15 @@ async function store(state, emitter) { if (file.type == 'folder') { await removeBoardFolder( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value ) ) } else if (file.type == 'file') { - await serial.removeFile( - serial.getFullPath( + await serialBridge.removeFile( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value @@ -855,13 +855,13 @@ async function store(state, emitter) { try { if (state.renamingFile == 'board') { - await serial.renameFile( - serial.getFullPath( + await serialBridge.renameFile( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName ), - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value @@ -932,8 +932,8 @@ async function store(state, emitter) { if (!isNewFile) { // Check if full path exists if (openFile.source == 'board') { - fullPathExists = await serial.fileExists( - serial.getFullPath( + fullPathExists = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, oldName @@ -961,8 +961,8 @@ async function store(state, emitter) { // Check if it will overwrite let willOverwrite = false if (openFile.source == 'board') { - willOverwrite = await serial.fileExists( - serial.getFullPath( + willOverwrite = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -994,9 +994,9 @@ async function store(state, emitter) { const contents = openFile.editor.editor.state.doc.toString() try { if (openFile.source == 'board') { - await serial.getPrompt() - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.getPrompt() + await serialBridge.saveFileContent( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, oldName @@ -1023,13 +1023,13 @@ async function store(state, emitter) { // RENAME FILE try { if (openFile.source == 'board') { - await serial.renameFile( - serial.getFullPath( + await serialBridge.renameFile( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, oldName ), - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -1057,9 +1057,9 @@ async function store(state, emitter) { const contents = openFile.editor.editor.state.doc.toString() try { if (openFile.source == 'board') { - await serial.getPrompt() - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.getPrompt() + await serialBridge.saveFileContent( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -1149,8 +1149,8 @@ async function store(state, emitter) { // load content and append it to the list of files to open let file = null if (selectedFile.source == 'board') { - const fileContent = await serial.loadFile( - serial.getFullPath( + const fileContent = await serialBridge.loadFile( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, selectedFile.fileName @@ -1230,7 +1230,7 @@ async function store(state, emitter) { const willOverwrite = await checkOverwrite({ source: 'board', fileNames: state.selectedFiles.map(f => f.fileName), - parentPath: serial.getFullPath( + parentPath: serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, '' @@ -1257,7 +1257,7 @@ async function store(state, emitter) { state.diskNavigationPath, file.fileName ) - const destPath = serial.getFullPath( + const destPath = serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName @@ -1271,7 +1271,7 @@ async function store(state, emitter) { } ) } else { - await serial.uploadFile( + await serialBridge.uploadFile( srcPath, destPath, (progress) => { state.transferringProgress = `${file.fileName}: ${progress}` @@ -1317,7 +1317,7 @@ async function store(state, emitter) { for (let i in state.selectedFiles) { const file = state.selectedFiles[i] - const srcPath = serial.getFullPath( + const srcPath = serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName @@ -1336,7 +1336,7 @@ async function store(state, emitter) { } ) } else { - await serial.downloadFile( + await serialBridge.downloadFile( srcPath, destPath, (e) => { state.transferringProgress = e @@ -1355,7 +1355,7 @@ async function store(state, emitter) { // NAVIGATION emitter.on('navigate-board-folder', (folder) => { log('navigate-board-folder', folder) - state.boardNavigationPath = serial.getNavigationPath( + state.boardNavigationPath = serialBridge.getNavigationPath( state.boardNavigationPath, folder ) @@ -1364,7 +1364,7 @@ async function store(state, emitter) { }) emitter.on('navigate-board-parent', () => { log('navigate-board-parent') - state.boardNavigationPath = serial.getNavigationPath( + state.boardNavigationPath = serialBridge.getNavigationPath( state.boardNavigationPath, '..' ) @@ -1394,7 +1394,7 @@ async function store(state, emitter) { win.onBeforeReload(async () => { // Perform any cleanup needed if (state.isConnected) { - await serial.disconnect() + await serialBridge.disconnect() state.isConnected = false state.panelHeight = PANEL_CLOSED state.boardFiles = [] @@ -1561,12 +1561,12 @@ function generateHash() { } async function getAvailablePorts() { - return await serial.loadPorts() + return await serialBridge.loadPorts() } async function getBoardFiles(path) { - await serial.getPrompt() - let files = await serial.ilistFiles(path) + await serialBridge.getPrompt() + let files = await serialBridge.ilistFiles(path) files = files.map(f => ({ fileName: f[0], type: f[1] === 0x4000 ? 'folder' : 'file' @@ -1584,9 +1584,9 @@ function checkDiskFile({ root, parentFolder, fileName }) { async function checkBoardFile({ root, parentFolder, fileName }) { if (root == null || parentFolder == null || fileName == null) return false - await serial.getPrompt() - return serial.fileExists( - serial.getFullPath(root, parentFolder, fileName) + await serialBridge.getPrompt() + return serialBridge.fileExists( + serialBridge.getFullPath(root, parentFolder, fileName) ) } @@ -1651,29 +1651,29 @@ function canEdit({ selectedFiles }) { async function removeBoardFolder(fullPath) { // TODO: Replace with getting the file tree from the board and deleting one by one - let output = await serial.execFile(await getHelperFullPath()) - await serial.run(`delete_folder('${fullPath}')`) + let output = await serialBridge.execFile(await getHelperFullPath()) + await serialBridge.run(`delete_folder('${fullPath}')`) } async function uploadFolder(srcPath, destPath, dataConsumer) { dataConsumer = dataConsumer || function() {} - await serial.createFolder(destPath) + await serialBridge.createFolder(destPath) let allFiles = await disk.ilistAllFiles(srcPath) for (let i in allFiles) { const file = allFiles[i] const relativePath = file.path.substring(srcPath.length) if (file.type === 'folder') { - await serial.createFolder( - serial.getFullPath( + await serialBridge.createFolder( + serialBridge.getFullPath( destPath, relativePath, '' ) ) } else { - await serial.uploadFile( + await serialBridge.uploadFile( disk.getFullPath(srcPath, relativePath, ''), - serial.getFullPath(destPath, relativePath, ''), + serialBridge.getFullPath(destPath, relativePath, ''), (progress) => { dataConsumer(progress, relativePath) } @@ -1685,8 +1685,8 @@ async function uploadFolder(srcPath, destPath, dataConsumer) { async function downloadFolder(srcPath, destPath, dataConsumer) { dataConsumer = dataConsumer || function() {} await disk.createFolder(destPath) - let output = await serial.execFile(await getHelperFullPath()) - output = await serial.run(`ilist_all('${srcPath}')`) + let output = await serialBridge.execFile(await getHelperFullPath()) + output = await serialBridge.run(`ilist_all('${srcPath}')`) let files = [] try { // Extracting the json output from serial response @@ -1706,9 +1706,9 @@ async function downloadFolder(srcPath, destPath, dataConsumer) { disk.getFullPath( destPath, relativePath, '') ) } else { - await serial.downloadFile( - serial.getFullPath(srcPath, relativePath, ''), - serial.getFullPath(destPath, relativePath, '') + await serialBridge.downloadFile( + serialBridge.getFullPath(srcPath, relativePath, ''), + serialBridge.getFullPath(destPath, relativePath, '') ) } } From 041468f796162cd0d8fa8f7d0896cfb4c4b9cdf0 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 16:43:58 +0100 Subject: [PATCH 05/12] Use shared instance of Serial in menu handler --- backend/ipc.js | 5 ++--- backend/menu.js | 4 ++-- backend/serial.js | 6 ++++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index f554010..91f352a 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,7 +1,6 @@ const fs = require('fs') -const Serial = require('./serial.js') -let serial const registerMenu = require('./menu.js') +const serial = require('./serial.js').sharedInstance const { openFolderDialog, @@ -11,7 +10,7 @@ const { } = require('./helpers.js') module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { - serial = new Serial(win) + serial.win = win // Required to send callback messages to renderer ipcMain.handle('open-folder', async (event) => { console.log('ipcMain', 'open-folder') diff --git a/backend/menu.js b/backend/menu.js index b372527..932d704 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,6 +1,6 @@ const { app, Menu } = require('electron') const path = require('path') -const Serial = require('./serial.js') +const serial = require('./serial.js').sharedInstance const openAboutWindow = require('about-window').default const shortcuts = require('./shortcuts.js') const { type } = require('os') @@ -128,7 +128,7 @@ module.exports = function registerMenu(win, state = {}) { accelerator: '', click: async () => { try { - await Serial.disconnect() + await serial.disconnect() win.reload() } catch(e) { console.error('Reload from menu failed:', e) diff --git a/backend/serial.js b/backend/serial.js index 72357c7..1e13e6c 100644 --- a/backend/serial.js +++ b/backend/serial.js @@ -1,7 +1,7 @@ const MicroPython = require('micropython.js') class Serial { - constructor(win) { + constructor(win = null) { this.win = win this.board = new MicroPython() this.board.chunk_size = 192 @@ -110,4 +110,6 @@ except OSError: } } -module.exports = Serial \ No newline at end of file +const sharedInstance = new Serial() + +module.exports = {sharedInstance, Serial} \ No newline at end of file From 0792f2fc6f42dbeb13aee1fd930bea6b7852bf1d Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Fri, 13 Dec 2024 23:35:42 +0100 Subject: [PATCH 06/12] Remove residues from rebase --- preload.js | 101 ----------------------------------------------------- 1 file changed, 101 deletions(-) diff --git a/preload.js b/preload.js index 7549868..58cb5c3 100644 --- a/preload.js +++ b/preload.js @@ -1,111 +1,10 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') -const Serial = require('./backend/serial.js') const shortcuts = require('./backend/shortcuts.js').global -const MicroPython = require('micropython.js') const { emit, platform } = require('process') const SerialBridge = require('./backend/bridge/serial-bridge.js') -const board = new MicroPython() -board.chunk_size = 192 -board.chunk_sleep = 200 - -const Serial = { - loadPorts: async () => { - let ports = await board.list_ports() - return ports.filter(p => p.vendorId && p.productId) - }, - connect: async (path) => { - return board.open(path) - }, - disconnect: async () => { - return board.close() - }, - run: async (code) => { - return board.run(code) - }, - execFile: async (path) => { - return board.execfile(path) - }, - getPrompt: async () => { - return board.get_prompt() - }, - keyboardInterrupt: async () => { - await board.stop() - return Promise.resolve() - }, - reset: async () => { - await board.stop() - await board.exit_raw_repl() - await board.reset() - return Promise.resolve() - }, - eval: (d) => { - return board.eval(d) - }, - onData: (fn) => { - board.serial.on('data', fn) - }, - listFiles: async (folder) => { - return board.fs_ls(folder) - }, - ilistFiles: async (folder) => { - return board.fs_ils(folder) - }, - loadFile: async (file) => { - const output = await board.fs_cat_binary(file) - return output || '' - }, - removeFile: async (file) => { - return board.fs_rm(file) - }, - saveFileContent: async (filename, content, dataConsumer) => { - return board.fs_save(content || ' ', filename, dataConsumer) - }, - uploadFile: async (src, dest, dataConsumer) => { - return board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer) - }, - downloadFile: async (src, dest) => { - let contents = await Serial.loadFile(src) - return ipcRenderer.invoke('save-file', dest, contents) - }, - renameFile: async (oldName, newName) => { - return board.fs_rename(oldName, newName) - }, - onDisconnect: async (fn) => { - board.serial.on('close', fn) - }, - createFolder: async (folder) => { - return await board.fs_mkdir(folder) - }, - removeFolder: async (folder) => { - return await board.fs_rmdir(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) => { - // !!!: Fix this on micropython.js level - // ???: Check if file exists is not part of mpremote specs - const output = await board.run(` -import os -try: - os.stat("${filePath}") - print(0) -except OSError: - print(1) -`) - return output[2] === '0' - } -} - const Disk = { openFolder: async () => { return ipcRenderer.invoke('open-folder') From c3e2bff8b51c5bf8b4e38bfe87cbeb70fbdfe7f8 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Sat, 14 Dec 2024 00:03:05 +0100 Subject: [PATCH 07/12] Remove code to handle reload event --- backend/ipc.js | 5 ----- index.js | 18 ------------------ preload.js | 10 ---------- ui/arduino/store.js | 12 ------------ 4 files changed, 45 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index 91f352a..67e20db 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -144,11 +144,6 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { win.webContents.send('check-before-close') }) - // handle disconnection before reload - ipcMain.handle('prepare-reload', async (event) => { - return win.webContents.send('before-reload') - }) - ipcMain.handle('serial', (event, command, ...args) => { console.debug('Handling IPC serial command:', command, ...args) return serial[command](...args) diff --git a/index.js b/index.js index aa5d989..a6fcc04 100644 --- a/index.js +++ b/index.js @@ -50,24 +50,6 @@ function createWindow () { win.show() }) - win.webContents.on('before-reload', async (event) => { - // Prevent the default reload behavior - event.preventDefault() - - try { - // Tell renderer to do cleanup - win.webContents.send('cleanup-before-reload') - - // Wait for cleanup then reload - setTimeout(() => { - // This will trigger a page reload, but won't trigger 'before-reload' again - win.reload() - }, 500) - } catch(e) { - console.error('Reload preparation failed:', e) - } - }) - const initialMenuState = { isConnected: false, view: 'editor' diff --git a/preload.js b/preload.js index 58cb5c3..c13715d 100644 --- a/preload.js +++ b/preload.js @@ -64,16 +64,6 @@ const Window = { }) }, - onBeforeReload: (callback) => { - ipcRenderer.on('cleanup-before-reload', async () => { - try { - await callback() - } catch(e) { - console.error('Cleanup before reload failed:', e) - } - }) - }, - beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), confirmClose: () => ipcRenderer.invoke('confirm-close'), isPackaged: () => ipcRenderer.invoke('is-packaged'), diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 1e49661..b8b8c23 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1391,18 +1391,6 @@ async function store(state, emitter) { emitter.emit('render') }) - win.onBeforeReload(async () => { - // Perform any cleanup needed - if (state.isConnected) { - await serialBridge.disconnect() - state.isConnected = false - state.panelHeight = PANEL_CLOSED - state.boardFiles = [] - state.boardNavigationPath = '/' - } - // Any other cleanup needed - }) - win.beforeClose(async () => { const hasChanges = !!state.openFiles.find(f => f.hasChanges) if (hasChanges) { From 18c4929ebb5fdccfa471860463fcd366a457c685 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Sat, 14 Dec 2024 00:10:32 +0100 Subject: [PATCH 08/12] Rename folder --- backend/ipc.js | 2 +- backend/menu.js | 2 +- backend/{bridge => serial}/serial-bridge.js | 0 backend/{ => serial}/serial.js | 0 preload.js | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) rename backend/{bridge => serial}/serial-bridge.js (100%) rename backend/{ => serial}/serial.js (100%) diff --git a/backend/ipc.js b/backend/ipc.js index 67e20db..ab06335 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,6 +1,6 @@ const fs = require('fs') const registerMenu = require('./menu.js') -const serial = require('./serial.js').sharedInstance +const serial = require('./serial/serial.js').sharedInstance const { openFolderDialog, diff --git a/backend/menu.js b/backend/menu.js index 932d704..6db8023 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,6 +1,6 @@ const { app, Menu } = require('electron') const path = require('path') -const serial = require('./serial.js').sharedInstance +const serial = require('./serial/serial.js').sharedInstance const openAboutWindow = require('about-window').default const shortcuts = require('./shortcuts.js') const { type } = require('os') diff --git a/backend/bridge/serial-bridge.js b/backend/serial/serial-bridge.js similarity index 100% rename from backend/bridge/serial-bridge.js rename to backend/serial/serial-bridge.js diff --git a/backend/serial.js b/backend/serial/serial.js similarity index 100% rename from backend/serial.js rename to backend/serial/serial.js diff --git a/preload.js b/preload.js index c13715d..f67d43c 100644 --- a/preload.js +++ b/preload.js @@ -3,7 +3,7 @@ const { contextBridge, ipcRenderer } = require('electron') const path = require('path') const shortcuts = require('./backend/shortcuts.js').global const { emit, platform } = require('process') -const SerialBridge = require('./backend/bridge/serial-bridge.js') +const SerialBridge = require('./backend/serial/serial-bridge.js') const Disk = { openFolder: async () => { From 612d0b9306204a27ebc1fc00593a85aad99148ad Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Sat, 14 Dec 2024 10:51:44 +0100 Subject: [PATCH 09/12] Remove duplicated event handler --- ui/arduino/store.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index b8b8c23..2b00817 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -201,8 +201,6 @@ async function store(state, emitter) { }) emitter.on('disconnect', async () => { await serialBridge.disconnect() - // Update the UI after closing the connection - emitter.emit('disconnected') }) emitter.on('connection-timeout', async () => { state.isConnected = false From 4248c1eeb1c141d52e30f8f3999a03c24ddcfd6f Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 16 Dec 2024 09:46:02 +0100 Subject: [PATCH 10/12] Remove event handlers on connection close --- backend/serial/serial.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/serial/serial.js b/backend/serial/serial.js index 1e13e6c..e702b17 100644 --- a/backend/serial/serial.js +++ b/backend/serial/serial.js @@ -56,6 +56,8 @@ class Serial { }) this.board.serial.on('close', () => { + this.board.serial.removeAllListeners("data") + this.board.serial.removeAllListeners("close") this.win.webContents.send('serial-on-connection-closed') }) } From d7422b10b5f626eb2111b0fbbb31c0aceb8a47b0 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 16 Dec 2024 10:31:48 +0100 Subject: [PATCH 11/12] Replace existing event listeners in serial bridge --- backend/serial/serial-bridge.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/serial/serial-bridge.js b/backend/serial/serial-bridge.js index 54f7221..715d21a 100644 --- a/backend/serial/serial-bridge.js +++ b/backend/serial/serial-bridge.js @@ -32,9 +32,13 @@ const SerialBridge = { return ipcRenderer.invoke('serial', 'eval', d) }, onData: (callback) => { - ipcRenderer.on('serial-on-data', (event, data) => { - callback(data) - }) + // 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) @@ -62,9 +66,13 @@ const SerialBridge = { return await ipcRenderer.invoke('serial', 'renameFile', oldName, newName) }, onConnectionClosed: async (callback) => { - ipcRenderer.on('serial-on-connection-closed', (event) => { - 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) From b5b52190f3d6e0f2362f8941230be18c9de90201 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 16 Dec 2024 10:31:57 +0100 Subject: [PATCH 12/12] Add code documentation --- ui/arduino/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 2b00817..09a373e 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -175,7 +175,7 @@ async function store(state, emitter) { serialBridge.eval(data) term.scrollToBottom() }) - serialBridge.eval('\x02') + serialBridge.eval('\x02') // Send Ctrl+B to enter normal repl mode } serialBridge.onData((data) => { term.write(data) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy