Skip to content
9 changes: 6 additions & 3 deletions backend/ipc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const fs = require('fs')
const registerMenu = require('./menu.js')
const serial = require('./serial/serial.js').sharedInstance

const {
openFolderDialog,
Expand All @@ -9,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 @@ -141,8 +144,8 @@ 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)
})
}
7 changes: 3 additions & 4 deletions backend/menu.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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')
Expand Down Expand Up @@ -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)
}
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}
18 changes: 0 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading