Skip to content

Commit 97dec61

Browse files
committed
Base electron app
- Electron bootstrap - Micropython Serial interface - Shared Main/Rendered context
1 parent 4574f6b commit 97dec61

File tree

4 files changed

+556
-0
lines changed

4 files changed

+556
-0
lines changed

index.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
2+
const path = require('path')
3+
const fs = require('fs')
4+
5+
let win = null // main window
6+
7+
async function openFolderDialog() {
8+
// https://stackoverflow.com/questions/46027287/electron-open-folder-dialog
9+
let dir = await dialog.showOpenDialog(win, { properties: [ 'openDirectory' ] })
10+
return dir.filePaths[0] || null
11+
}
12+
13+
ipcMain.handle('open-folder', async (event) => {
14+
console.log('ipcMain', 'open-folder')
15+
const folder = await openFolderDialog()
16+
let files = []
17+
if (folder) {
18+
files = fs.readdirSync(path.resolve(folder))
19+
// Filter out directories
20+
files = files.filter(f => {
21+
let filePath = path.resolve(folder, f)
22+
return !fs.lstatSync(filePath).isDirectory()
23+
})
24+
}
25+
return { folder, files }
26+
})
27+
28+
ipcMain.handle('load-file', (event, folder, filename) => {
29+
console.log('ipcMain', 'load-file', folder, filename )
30+
let filePath = path.resolve(folder, filename)
31+
let content = fs.readFileSync(filePath)
32+
return content
33+
})
34+
35+
ipcMain.handle('save-file', (event, folder, filename, content) => {
36+
console.log('ipcMain', 'save-file', folder, filename, content)
37+
let filePath = path.resolve(folder, filename)
38+
fs.writeFileSync(filePath, content, 'utf8')
39+
return true
40+
})
41+
42+
ipcMain.handle('update-folder', (event, folder) => {
43+
let files = fs.readdirSync(path.resolve(folder))
44+
// Filter out directories
45+
files = files.filter(f => {
46+
let filePath = path.resolve(folder, f)
47+
return !fs.lstatSync(filePath).isDirectory()
48+
})
49+
return { folder, files }
50+
})
51+
52+
ipcMain.handle('remove-file', (event, folder, filename) => {
53+
console.log('ipcMain', 'remove-file', folder, filename)
54+
let filePath = path.resolve(folder, filename)
55+
fs.unlinkSync(filePath)
56+
return true
57+
})
58+
59+
ipcMain.handle('rename-file', (event, folder, filename, newFilename) => {
60+
console.log('ipcMain', 'rename-file', folder, filename, newFilename)
61+
let filePath = path.resolve(folder, filename)
62+
let newFilePath = path.resolve(folder, newFilename)
63+
fs.renameSync(filePath, newFilePath)
64+
return newFilename
65+
})
66+
67+
function createWindow () {
68+
// Create the browser window.
69+
win = new BrowserWindow({
70+
width: 700,
71+
height: 640,
72+
webPreferences: {
73+
nodeIntegration: true,
74+
webSecurity: false,
75+
enableRemoteModule: false,
76+
preload: path.join(__dirname, "preload.js")
77+
}
78+
})
79+
// and load the index.html of the app.
80+
win.loadFile('ui/blank/index.html')
81+
}
82+
83+
app.whenReady().then(createWindow)

micropython.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
const EventEmitter = require('events')
2+
const SerialPort = require('serialport')
3+
4+
const codeListFiles = `
5+
from os import listdir
6+
print('<BEGINREC>')
7+
print(listdir())
8+
print('<ENDREC>')
9+
`
10+
const codeLoadFile = (path) => {
11+
return `
12+
print('<BEGINREC>')
13+
with open('${path}', 'r') as f:
14+
line = f.readline()
15+
while line != '':
16+
print(line, end='')
17+
line = f.readline()
18+
print('<ENDREC>')
19+
`
20+
}
21+
22+
const codeRemoveFile = (path) => {
23+
return `
24+
from os import remove
25+
remove('${path}')
26+
`
27+
}
28+
29+
const codeRenameFile = (oldPath, newPath) => {
30+
return `
31+
from os import rename
32+
rename('${oldPath}', '${newPath}')
33+
`
34+
}
35+
36+
const codeCollectGarbage = `import gc
37+
gc.collect()`
38+
39+
class SerialConnection extends EventEmitter {
40+
constructor() {
41+
super()
42+
this.rawRepl = false
43+
this.loadingFile = false
44+
this.loadingFileList = false
45+
}
46+
/**
47+
* List all available serial ports (with vendor id)
48+
* @return {Promise} Resolves with an array of port objects
49+
*/
50+
static listAvailable() {
51+
return new Promise((resolve, reject) => {
52+
SerialPort.list().then((ports) => {
53+
const availablePorts = ports.filter((port) => {
54+
return !!port.vendorId
55+
})
56+
if (availablePorts) {
57+
resolve(availablePorts)
58+
} else {
59+
reject(new Error('No ports available'))
60+
}
61+
})
62+
})
63+
}
64+
/**
65+
* Opens a connection on a given port.
66+
* @param {String} port Port address to open the connection
67+
*/
68+
open(port) {
69+
this.port = new SerialPort(port, {
70+
baudRate: 115200,
71+
autoOpen: false
72+
})
73+
this.port.on('open', () => {
74+
this.emit('connected')
75+
this.port.write('\r')
76+
})
77+
this.port.on('data', (data) => this._eventHandler(data))
78+
this.port.open()
79+
}
80+
/**
81+
* Closes current connection.
82+
*/
83+
close() {
84+
this.emit('disconnected')
85+
if (this.port) {
86+
this.port.close()
87+
}
88+
}
89+
/**
90+
* Executes code in a string format. This code can contain multiple lines.
91+
* @param {String} code String of code to be executed. Line breaks must be `\n`
92+
*/
93+
execute(code) {
94+
// TODO: break code in lines and `_execRaw` line by line
95+
this.stop()
96+
this._enterRawRepl()
97+
this._executeRaw(code)
98+
.then(() => {
99+
this._exitRawRepl()
100+
})
101+
}
102+
/**
103+
* Evaluate a command/expression.
104+
* @param {String} command Command/expression to be evaluated
105+
*/
106+
evaluate(command) {
107+
this.port.write(Buffer.from(command))
108+
}
109+
/**
110+
* Send a "stop" command in order to interrupt any running code. For serial
111+
* REPL this command is "CTRL-C".
112+
*/
113+
stop() {
114+
this.port.write('\r\x03') // CTRL-C
115+
}
116+
/**
117+
* Send a command to "soft reset".
118+
*/
119+
softReset() {
120+
this.stop();
121+
this.port.write('\r\x04') // CTRL-D
122+
}
123+
/**
124+
* Prints on console the existing files on file system.
125+
*/
126+
listFiles() {
127+
this.data = ''
128+
this.loadingFileList = true
129+
this.execute(codeListFiles)
130+
}
131+
/**
132+
* Prints on console the content of a given file.
133+
* @param {String} path File's path
134+
*/
135+
loadFile(path) {
136+
this.data = ''
137+
this.loadingFile = true
138+
this.execute(codeLoadFile(path))
139+
}
140+
/**
141+
* Writes a given content to a file in the file system.
142+
* @param {String} path File's path
143+
* @param {String} content File's content
144+
*/
145+
writeFile(path, content) {
146+
if (!path || !content) {
147+
return
148+
}
149+
// TODO: Find anoter way to do it without binascii
150+
let pCode = `f = open('${path}', 'w')\n`
151+
// pCode += `import gc; gc.collect()\n`
152+
pCode += codeCollectGarbage + '\n'
153+
// `content` is what comes from the editor. We want to write it
154+
// line one by one on a file so we split by `\n`
155+
var lineCount = 0;
156+
var lines = content.split('\r\n')
157+
lines.forEach((line) => {
158+
if (line) {
159+
var nlMarker = line.indexOf('\n');
160+
var crMarker = line.indexOf('\r');
161+
// TODO: Sanitize line replace """ with \"""
162+
// To avoid the string escaping with weirdly we encode
163+
// the line plus the `\n` that we just removed to base64
164+
pCode += `f.write("""${line}""")`
165+
if(lineCount != lines.length - 1){
166+
pCode += `\nf.write('\\n')\n`
167+
}
168+
lineCount++;
169+
}
170+
})
171+
pCode += `\nf.close()\n`
172+
this.execute(pCode)
173+
}
174+
175+
/**
176+
* Removes file on a given path
177+
* @param {String} path File's path
178+
*/
179+
removeFile(path) {
180+
this.execute(codeRemoveFile(path))
181+
}
182+
183+
renameFile(oldPath, newPath) {
184+
this.execute(codeRenameFile(oldPath, newPath))
185+
}
186+
/**
187+
* Handles data comming from connection
188+
* @param {Buffer} buffer Data comming from connection
189+
*/
190+
_eventHandler(buffer) {
191+
const data = buffer.toString()
192+
this.emit('output', data)
193+
194+
// Getting data that should be sent to frontend
195+
// Loading file content, listing files, etc
196+
// if (data.indexOf('<REC>') !== -1) {
197+
// this.recordingData = true
198+
// }
199+
// if (this.recordingData) {
200+
// this.data += data
201+
// }
202+
// if (data.indexOf('<EOF>') !== -1) {
203+
// const iofREC = this.data.indexOf('<REC>')
204+
// const rec = this.data.indexOf('<REC>\r\n')+7
205+
// const eof = this.data.indexOf('<EOF>')
206+
// if (this.loadingFile) {
207+
// this.emit('file-loaded', this.data.slice(rec, eof))
208+
// this.loadingFile = false
209+
// }
210+
// if (this.loadingFileList) {
211+
// this.emit('file-list-loaded', this.data.slice(rec, eof))
212+
// this.loadingFileList = false
213+
// }
214+
// this.recordingData = false
215+
// }
216+
217+
if (this.rawRepl && data.indexOf('\n>>> ') != -1) {
218+
this.emit('execution-finished')
219+
this.rawRepl = false
220+
}
221+
222+
if (!this.rawRepl && data.indexOf('raw REPL;') != -1) {
223+
this.emit('execution-started')
224+
this.rawRepl = true
225+
}
226+
}
227+
/**
228+
* Put REPL in raw mode
229+
*/
230+
_enterRawRepl() {
231+
this.port.write('\r\x01') // CTRL-A
232+
}
233+
/**
234+
* Exit REPL raw mode
235+
*/
236+
_exitRawRepl() {
237+
this.port.write('\r\x04\r\x02') // CTRL-D // CTRL-B
238+
}
239+
/**
240+
* Writes a command to connected port
241+
* @param {String} command Command to be written on connected port
242+
*/
243+
_executeRaw(command) {
244+
const writePromise = (buffer) => {
245+
return new Promise((resolve, reject) => {
246+
setTimeout(() => {
247+
this.port.write(buffer, (err) => {
248+
if (err) return reject()
249+
resolve()
250+
})
251+
}, 1)
252+
})
253+
}
254+
const l = 1024
255+
let slices = []
256+
for(let i = 0; i < command.length; i+=l) {
257+
let slice = command.slice(i, i+l)
258+
slices.push(slice)
259+
}
260+
return new Promise((resolve, reject) => {
261+
slices.reduce((cur, next) => {
262+
return cur.then(() => {
263+
return writePromise(next)
264+
})
265+
}, Promise.resolve())
266+
.then()
267+
.then(() => {
268+
resolve()
269+
})
270+
})
271+
}
272+
}
273+
274+
module.exports = SerialConnection

package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "micropython-lab",
3+
"version": "0.0.1",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"post-set-shell": "npm config set script-shell bash",
8+
"post-install": "npm install [email protected] $(if [ $(uname -m) = arm64 ]; then echo --arch=x64; fi) --save-dev",
9+
"post-rebuild": "electron-rebuild",
10+
"dev": "electron ./",
11+
"pack": "electron-builder --dir",
12+
"build": "npm run post-set-shell && electron-builder $(if [ $(uname -m) = arm64 ]; then echo --mac --x64; fi)",
13+
"postinstall": "npm run post-set-shell && npm run post-install && npm run post-rebuild"
14+
},
15+
"devDependencies": {
16+
"electron": "^8.5.5",
17+
"electron-builder": "^22.3.2",
18+
"electron-rebuild": "^1.10.0"
19+
},
20+
"build": {
21+
"artifactName": "${productName}-${os}_${arch}.${ext}",
22+
"mac": {
23+
"target": "zip"
24+
},
25+
"win": {
26+
"target": "zip"
27+
},
28+
"linux": {
29+
"target": "zip"
30+
}
31+
},
32+
"author": "",
33+
"license": "ISC",
34+
"dependencies": {
35+
"serialport": "^8.0.7"
36+
}
37+
}

0 commit comments

Comments
 (0)