├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs ├── screenshots │ ├── serial.gif │ ├── tool-bar.png │ └── verify.gif └── toolbar.md ├── lib ├── arduino-upload.coffee ├── boards.coffee ├── output-view.coffee ├── serial-view.coffee └── util.coffee ├── menus └── arduino-upload.cson ├── package.json ├── spec ├── arduino-upload-spec.coffee └── arduino-upload-view-spec.coffee └── styles └── arduino-upload.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.2 2 | * Derp, forgot to update the import 3 | 4 | ## 1.3.1 5 | * Depend on `serialport-builds-electron` now so that I can upload custom electron builds to windows users 6 | 7 | ## 1.3.0 8 | * Windows support should be working 9 | 10 | ## 1.2.0 11 | * Board can be set now via status bar 12 | * Better arduino default path detection for macos 13 | 14 | ## 1.1.1 15 | * Actual error output when uploading 16 | 17 | ## 1.1.0 18 | * multiple bugfixes 19 | * cleaner code 20 | * quicker compiling 21 | * add support to programmers 22 | 23 | ## 1.0.3 24 | * fix [#10](https://github.com/Sorunome/arduino-upload/issues/6) 25 | 26 | ## 1.0.2 27 | * add support for arduino.org boards 28 | 29 | ## 1.0.1 30 | * fix [#6](https://github.com/Sorunome/arduino-upload/issues/6) 31 | 32 | ## 1.0.0 33 | * First major release 34 | * Added screenshots to readme 35 | * Made serialport a hard dependency, works with atom 1.10 36 | 37 | ## 0.7.1 38 | * SerialPort is now an optional dependency, please figure out how to get it working... 39 | * Fixed windows support (I hope) 40 | 41 | ## 0.7.0 42 | * commented out SerialPort until it is working via atom.... (node-pre-gyp issue....) 43 | 44 | ## 0.6.0 45 | * Added serial sending 46 | 47 | ## 0.5.0 48 | * Added verification 49 | * Building now copies .hex, .elf and .eep to sketch directory 50 | 51 | ## 0.4.0 52 | * Port to upload to is now determined by this package instead of the IDE 53 | * Made boards configurable 54 | 55 | ## 0.3.1 56 | * files when clicked on in error output are now activated even in other pane 57 | 58 | ## 0.3.0 59 | * Filenames in error output are now clickable, also jumps to line 60 | 61 | ## 0.2.0 62 | * Added Serial monitor 63 | 64 | ## 0.1.0 - First Release 65 | * Allowing building the sketch 66 | * Allowing uploading the sketch 67 | * Nice error output 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Sorunome 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino Upload Package 2 | 3 | Did you not like the look of the Arduino IDE but were stuck with it nevertheless? Not anymore, this package provides some essential features of the Arduino IDE. 4 | 5 | * Verify a sketch 6 | * Build a sketch 7 | * Upload a sketch 8 | * Serial Monitor 9 | * Error output when compiling 10 | * Files are clickable in build output --> cursor jumps to that line in that file 11 | * Switch board directly in the status bar 12 | 13 | ## Installation 14 | `apm install arduino-upload` 15 | (Requires atom 1.10 or later) 16 | 17 | ## Troubleshooting 18 | * The verify/build output does not appear? Make sure your Arduino official IDE language is set to English. 19 | * Windows 20 | * Getting `Uncaught Error: spawn EPERM`? Be sure you don't have the Microsoft Store version of the Arduino IDE. - [#65](https://github.com/Sorunome/arduino-upload/issues/65) 21 | * Can't install because of the serialport package? Try installing the [Visual C++ 2015 building tools](https://github.com/felixrieseberg/windows-build-tools) by running `.\npm install --global --production windows-build-tools` from the directory `%LocalAppData%\atom\app-[VERSION]\resources\app\apm\bin` in an adminastrative PowerShell (this uses atoms integrated npm instead of requiring an external instance). Then update apm config with `apm config set msvs_version [VERSION]` (the version installed by the tool should be 2015) and `apm config set python [PATH]\python.exe` (where PATH is the path to the python executable). - [#46](https://github.com/Sorunome/arduino-upload/issues/46) 22 | 23 | 24 | ## Available commands 25 | * `arduino-upload:verify` - Verifies the sketches (checking for error output), deletes all sources, though 26 | * `arduino-upload:build` - Builds the current sketch, the .hex, .elf, .eep and .bin are copied to the sketch directory 27 | * `arduino-upload:upload` - Uploads the current sketch to a connected arduino 28 | * `arduino-upload:serial-monitor` - Opens the serial monitor of a connected arduino 29 | 30 | You can place these commands in a toolbar using the [**Flex Tool Bar**](https://atom.io/packages/flex-tool-bar) package. [Here's how](https://github.com/Sorunome/arduino-upload/blob/master/docs/toolbar.md). 31 | 32 | ## Screenshots 33 | Verifying a program: 34 | 35 | ![verify](https://raw.githubusercontent.com/Sorunome/arduino-upload/master/docs/screenshots/verify.gif) 36 | 37 | Serial monitor: 38 | 39 | ![serial](https://raw.githubusercontent.com/Sorunome/arduino-upload/master/docs/screenshots/serial.gif) 40 | -------------------------------------------------------------------------------- /docs/screenshots/serial.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sorunome/arduino-upload/151cce0a1db218dbed7866db62d37052f367dcee/docs/screenshots/serial.gif -------------------------------------------------------------------------------- /docs/screenshots/tool-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sorunome/arduino-upload/151cce0a1db218dbed7866db62d37052f367dcee/docs/screenshots/tool-bar.png -------------------------------------------------------------------------------- /docs/screenshots/verify.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sorunome/arduino-upload/151cce0a1db218dbed7866db62d37052f367dcee/docs/screenshots/verify.gif -------------------------------------------------------------------------------- /docs/toolbar.md: -------------------------------------------------------------------------------- 1 | # Arduino Upload Package 2 | 3 | ## Add a toolbar 4 | 5 | You can install `tool-bar` and `flex-tool-bar` Atom packages and use them to add a toolbar for Arduino commands, similar to the one in the official IDE. 6 | 7 | apm install tool-bar 8 | apm install flex-tool-bar 9 | 10 | The following is an example for the Flex Tool Bar's default configuration file, `toolbar.cson`. It adds the buttons for Arduino **Verify** and **Upload** commands and another to open the **Serial Monitor**. 11 | 12 | Please note that Flex Tool Bar can use many different configuration files and each different type of configuration file has it's own formatting and syntax rules. 13 | 14 | ```coffeescript 15 | [ 16 | { 17 | type: "button" 18 | icon: "check" 19 | callback: ["arduino-upload:verify"] 20 | tooltip: "Arduino: Verify" 21 | enable: { grammar: "arduino" } 22 | } 23 | { 24 | type: "button" 25 | icon: "arrow-right" 26 | callback: ["arduino-upload:upload"] 27 | tooltip: "Arduino: Upload" 28 | enable: { grammar: "arduino" } 29 | } 30 | { 31 | type: "button" 32 | icon: "terminal" 33 | callback: "arduino-upload:serial-monitor" 34 | tooltip: "Arduino: Serial Monitor" 35 | enable: { grammar: "arduino" } 36 | } 37 | { 38 | type: "spacer" 39 | } 40 | { 41 | type: "button" 42 | icon: "gear" 43 | callback: "flex-tool-bar:edit-config-file" 44 | tooltip: "Edit Tool Bar" 45 | } 46 | ] 47 | ``` 48 | 49 | Here's the result: 50 | 51 | ![tool-bar.png](screenshots/tool-bar.png) 52 | -------------------------------------------------------------------------------- /lib/arduino-upload.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {spawn} = require 'child_process' 3 | String::strip = -> if String::trim? then @trim() else @replace /^\s+|\s+$/g, "" 4 | 5 | fs = require 'fs' 6 | path = require 'path' 7 | OutputView = require './output-view' 8 | SerialView = require './serial-view' 9 | tmp = require 'tmp' 10 | { separator, getArduinoPath } = require './util' 11 | Boards = require './boards' 12 | 13 | try 14 | serialport = require 'serialport' 15 | catch e 16 | serialport = null 17 | try 18 | usbDetect = require 'usb-detection' 19 | catch e 20 | usbDetect = null 21 | 22 | 23 | boards = new Boards 24 | output = null 25 | serial = null 26 | serialeditor = null 27 | 28 | removeDir = (dir) -> 29 | if fs.existsSync dir 30 | for file in fs.readdirSync dir 31 | path = dir + '/' + file 32 | if fs.lstatSync(path).isDirectory() 33 | removeDir path 34 | else 35 | fs.unlinkSync path 36 | 37 | fs.rmdirSync dir 38 | module.exports = ArduinoUpload = 39 | config: 40 | arduinoExecutablePath: 41 | title: 'Arduino Executable Path' 42 | description: '''The path to the Arduino IDE executable or debug executable 43 | . This checks the default install location automatically.''' 44 | type: 'string' 45 | default: 'arduino' 46 | autosave: 47 | title: 'AutoSave' 48 | description: '''Enables auto-saving before building, uploading, or 49 | verifying. The serial monitor will be closed for this operation, but can 50 | be made to reopen automatically.''' 51 | type: 'string' 52 | default: 'disabled' 53 | enum: ['disabled', 'save', 'save+reopen'] 54 | baudRate: 55 | title: 'BAUD Rate' 56 | description: '''Sets the BAUD rate for the serial connection, if this is 57 | changed the serial monitor will need to be reopened.''' 58 | type: 'number' 59 | default: '9600' 60 | board: 61 | title: 'Arduino board' 62 | description: '''If kept blank, this will take the settings from the 63 | Arduino IDE. The board uses the pattern as described 64 | here''' 65 | type: 'string' 66 | default: '' 67 | lineEnding: 68 | title: 'Default line ending in serial monitor' 69 | description: ''' 70 | 0 - No line ending
71 | 1 - Newline
72 | 2 - Carriage return
73 | 3 - Both NL & CR''' 74 | type: 'integer' 75 | default: 1 76 | minimum: 0 77 | maximum: 3 78 | vendorsArduino: { 79 | 0x2341: true # Arduino 80 | 0x2a03: true # Arduino M0 Pro (perhaps other devices?) 81 | 0x03eb: true # Atmel 82 | # knockoff producers 83 | 0x0403: [ 0x6001 ] # FTDI 84 | 0x1a86: [ 0x7523 ] # QuinHeng 85 | 0x0403: [ 0x6001 ] # Future Technology Devices International, Ltd 86 | 0x10c4: [ 0xea60 ] # Silicon Labs CP210x USB to UART Bridge 87 | } 88 | vendorsProgrammer: { 89 | 0x03eb: [ 0x2141 ] # Atmel ICE debugger 90 | 0x1781: [ 0x0c9f ] # USBtinyISP Clones 91 | } 92 | buildFolders: [] 93 | activate: (state) -> 94 | # Setup to use the new composite disposables API for registering commands 95 | @subscriptions = new CompositeDisposable 96 | @subscriptions.add atom.commands.add "atom-workspace", 97 | 'arduino-upload:verify': => @build false 98 | @subscriptions.add atom.commands.add "atom-workspace", 99 | 'arduino-upload:build': => @build true 100 | @subscriptions.add atom.commands.add "atom-workspace", 101 | 'arduino-upload:upload': => @upload() 102 | @subscriptions.add atom.commands.add "atom-workspace", 103 | 'arduino-upload:serial-monitor': => @openserial() 104 | 105 | output = new OutputView 106 | atom.workspace.addBottomPanel(item:output) 107 | output.hide() 108 | 109 | boards.init() 110 | boards.load() 111 | atom.config.onDidChange 'arduino-upload.arduinoExecutablePath', ({newValue, oldValue}) => 112 | boards.load() 113 | atom.config.onDidChange 'arduino-upload.board', ({newValue, oldValue}) => 114 | boards.set newValue 115 | 116 | atom.workspace.observeActivePaneItem (editor) => 117 | if @isArduinoProject().isArduino 118 | boards.show() 119 | else 120 | boards.hide() 121 | 122 | deactivate: -> 123 | for own s, f of @buildFolders 124 | removeDir f 125 | @subscriptions.dispose() 126 | output?.remove() 127 | boards.destroy() 128 | @closeserial() 129 | 130 | consumeStatusBar: (statusBar) -> 131 | boards.init() 132 | boards.setStatusBar statusBar 133 | 134 | additionalArduinoOptions: (path, port = false) -> 135 | options = ['-v'] 136 | if atom.config.get('arduino-upload.board') != '' 137 | options = options.concat ['--board', atom.config.get('arduino-upload.board')] 138 | if typeof port != 'boolean' 139 | if port == 'PROGRAMMER' 140 | options.push '--useprogrammer' 141 | else if port != 'ARDUINO' 142 | options = options.concat ['--port', port] 143 | if not @buildFolders[path] 144 | @buildFolders[path] = tmp.dirSync().name 145 | options = options.concat ['--pref', 'build.path='+@buildFolders[path]] 146 | return options 147 | isArduinoProject: () -> 148 | editor = atom.workspace.getActivePaneItem() 149 | file = editor?.buffer?.file?.getPath()?.split separator 150 | file?.pop() 151 | name = file?.pop() 152 | file?.push name 153 | workpath = file?.join separator 154 | name += '.ino' 155 | file?.push name 156 | file = file?.join separator 157 | isArduino = fs.existsSync file 158 | return {isArduino, workpath, file, name} 159 | _build: (options, callback, onerror, port = false) -> 160 | serialWasOpen = false 161 | autosaveConf = atom.config.get('arduino-upload.autosave') 162 | if (autosaveConf == 'save') || (autosaveConf == 'save+reopen') 163 | if (autosaveConf == 'save+reopen') && (serial != null) 164 | serialWasOpen = true 165 | @closeserial() 166 | atom.commands.dispatch(atom.views.getView(atom.workspace.getActiveTextEditor()), 'window:save-all') 167 | {isArduino, workpath, file, name} = @isArduinoProject() 168 | if not isArduino 169 | atom.notifications.addError "The file isn't part of an Arduino sketch!" 170 | callback false 171 | return 172 | 173 | dispError = false 174 | output.reset() 175 | atom.notifications.addInfo 'Building...' 176 | 177 | options = [file].concat(options).concat @additionalArduinoOptions file, port 178 | stdoutput = spawn getArduinoPath(), options 179 | 180 | error = false 181 | 182 | stdoutput.on 'error', (err) => 183 | atom.notifications.addError "Can't find the Arduino IDE, please install it and set Arduino Executable Path in the settings! (" + err + ")" 184 | callback false 185 | error = true 186 | stdoutput.stdout.on 'data', (data) => 187 | if data.toString().strip().indexOf('Sketch') == 0 || data.toString().strip().indexOf('Global') == 0 188 | atom.notifications.addInfo data.toString() 189 | 190 | stdoutput.stderr.on 'data', (data) => 191 | console.log data.toString() 192 | overrideError = false 193 | if onerror 194 | overrideError = onerror(data) 195 | if data.toString().strip() == "exit status 1" 196 | console.log "ERROR OUTPUT OFF" 197 | dispError = false 198 | if dispError && !overrideError 199 | console.log data.toString() 200 | output.addLine data.toString(), @buildFolders[file], workpath 201 | if -1 != data.toString().toLowerCase().indexOf "verifying" 202 | console.log "ERROR OUTPUT ACTIVATED" 203 | dispError = true 204 | 205 | stdoutput.on 'close', (code) => 206 | if error 207 | return 208 | info = { 209 | 'buildFolder': @buildFolders[file] 210 | 'name': name 211 | 'workpath': workpath 212 | } 213 | callback code, info 214 | output.finish() 215 | if (autosaveConf == 'save+reopen') && (serialWasOpen) 216 | @openserial() 217 | build: (keep) -> 218 | @_build ['--verify'], (code, info) => 219 | if code != false 220 | if code != 0 221 | atom.notifications.addError 'Build failed!' 222 | else if keep 223 | for ending in ['.eep', '.elf', '.hex', '.bin'] 224 | fs.createReadStream(info.buildFolder + separator + info.name + ending).pipe(fs.createWriteStream(info.workpath + separator + info.name + ending)) 225 | 226 | upload: -> 227 | @getPort (port) => 228 | if port == '' 229 | atom.notifications.addError 'No Arduino found!' 230 | return 231 | callback = (code, info) => 232 | if code != false 233 | if code == 0 234 | atom.notifications.addInfo 'Successfully uploaded sketch' 235 | else 236 | if uploading 237 | atom.notifications.addError "Couldn't upload the sketch to the board, is it connected?" 238 | else 239 | atom.notifications.addError 'Build failed!' 240 | if serial != null 241 | @_openserialport port, false 242 | uploading = false 243 | onerror = (data) => 244 | s = data.toString().toLowerCase() 245 | if (s.indexOf("avrdude:") != -1 || s.indexOf("uploading") == 0) && !uploading 246 | uploading = true 247 | atom.notifications.addInfo 'Uploading...' 248 | return uploading 249 | if serial == null 250 | # no serial connection open to halt 251 | @_build ['--upload'], callback, onerror, port 252 | return 253 | @serialNormalClose = false 254 | serial.close (err) => 255 | @_build ['--upload'], callback, onerror, port 256 | isArduino: (vid, pid, vendors = false) -> 257 | if typeof vid == 'string' 258 | vid = parseInt vid, 16 259 | if typeof pid == 'string' 260 | pid = parseInt pid, 16 261 | if !vendors 262 | vendors = @vendorsArduino 263 | for own v, p of vendors 264 | if vid == parseInt v 265 | if p && typeof p == 'boolean' 266 | return true 267 | if -1 != p.indexOf pid 268 | return true 269 | return false 270 | _getPort: (callback) -> 271 | serialport.list().then (ports) => 272 | console.log ports 273 | p = '' 274 | for port in ports 275 | if @isArduino(port.vendorId, port.productId) 276 | p = port.path; 277 | break 278 | callback p 279 | getPort: (callback) -> 280 | if serialport == null and usbDetect == null 281 | console.log 'NOTHING TO CHECK' 282 | callback 'ARDUINO' 283 | return 284 | if usbDetect == null 285 | console.log 'ONLY SERIALPORT' 286 | @_getPort callback 287 | return 288 | usbDetect.find (err, ports) => 289 | for port in ports 290 | if @isArduino port.vendorId, port.productId, @vendorsProgrammer 291 | callback 'PROGRAMMER' 292 | return 293 | if serialport == null 294 | callback 'ARDUINO' 295 | return 296 | @_getPort callback 297 | serialNormalClose: true 298 | _openserialport: (port, start = true)-> 299 | try 300 | serialport serial = new serialport(port, { 301 | baudRate: atom.config.get('arduino-upload.baudRate') 302 | parser: serialport.parsers.raw 303 | }); 304 | @serialNormalClose = true 305 | serial.on 'open', (data) => 306 | if start 307 | atom.notifications.addInfo 'Serial port opened' 308 | serial.on 'data', (data) => 309 | serialeditor?.insertText data 310 | serial.on 'close', (data) => 311 | if @serialNormalClose 312 | @closeserial() 313 | atom.notifications.addInfo 'Serial port closed' 314 | serial.on 'error', (data) => 315 | console.log data 316 | @closeserial() 317 | atom.notifications.addInfo "Can't open serial port" 318 | catch e 319 | @closeserial() 320 | atom.notifications.addError e.toString() 321 | openserialport: -> 322 | if serial!=null 323 | atom.notifications.addInfo 'Serial port already open' 324 | return 325 | p = '' 326 | @getPort (port) => 327 | if port == 'PROGRAMMER' 328 | atom.notifications.addError "Can't use a programmer as serial monitor!" 329 | @closeserial() 330 | return 331 | if port == '' or port == 'ARDUINO' 332 | atom.notifications.addError 'No Arduino found!' 333 | @closeserial() 334 | return 335 | 336 | @_openserialport(port) 337 | openserial: -> 338 | if serialport == null 339 | atom.notifications.addInfo 'Serialport dependency not present!' 340 | return 341 | if serial!=null 342 | return 343 | 344 | serialeditor = new SerialView 345 | serialeditor.open => 346 | serialeditor.onDidDestroy => 347 | @closeserial() 348 | serialeditor.onSend (s) => 349 | serial?.write s 350 | @openserialport() 351 | closeserial: -> 352 | serial?.close (err) -> 353 | return 354 | serial = null 355 | 356 | serialeditor?.destroy() 357 | serialeditor = null 358 | -------------------------------------------------------------------------------- /lib/boards.coffee: -------------------------------------------------------------------------------- 1 | {SelectListView} = require 'atom-space-pen-views' 2 | {spawn} = require 'child_process' 3 | Promise = require 'bluebird' 4 | String::strip = -> if String::trim? then @trim() else @replace /^\s+|\s+$/g, "" 5 | fs = require 'fs' 6 | 7 | readdir = Promise.promisify(fs.readdir) 8 | readFile = Promise.promisify(fs.readFile) 9 | { separator, getArduinoPath } = require './util' 10 | module.exports = 11 | class Boards extends SelectListView 12 | inited: false 13 | loaded: false 14 | boards: {} 15 | statusBar: null 16 | selectNode: null 17 | init: => 18 | if @inited 19 | return 20 | @div = document.createElement 'div' 21 | @div.className = 'inline-block arduino-upload' 22 | @div.style.display = 'none' 23 | @inited = true 24 | addBoards: (pkg, arch, file) -> 25 | return readFile(file).then((data) => 26 | re = /(\w+)\.name=([^\r\n]+)/g 27 | data = data.toString() 28 | while matches = re.exec data 29 | @boards[pkg+':'+arch+':'+matches[1]] = matches[2] 30 | ).catch((err) => 31 | # do nothing 32 | ) 33 | destroy: (partial = false) -> 34 | if @loaded 35 | boards = {} 36 | loaded = false 37 | if not partial 38 | @statusBar?.destroy() 39 | parseNewPath: (path) -> 40 | return readdir(path).then((files) => 41 | Promise.each(files, (pkg) => 42 | path2 = path + separator + pkg + separator + 'hardware' 43 | readdir(path2).then((files) => 44 | Promise.each(files, (arch) => 45 | path3 = path2 + separator + arch 46 | readdir(path3).then((files) => 47 | Promise.each(files, (version) => 48 | path4 = path3 + separator + version 49 | return @addBoards pkg, arch, path4 + separator + 'boards.txt' 50 | ) 51 | ).catch((err) => 52 | # do nothing 53 | ) 54 | ) 55 | ).catch((err) => 56 | # do nothing 57 | ) 58 | ) 59 | ).catch((err) => 60 | # do nothing 61 | ) 62 | parseOldPath: (path) -> 63 | return readdir(path).then((files) => 64 | Promise.each(files, (pkg) => 65 | path2 = path + separator + pkg 66 | readdir(path2).then((files) => 67 | Promise.each(files, (arch) => 68 | return @addBoards pkg, arch, path2 + separator + arch + separator + 'boards.txt' 69 | ) 70 | ).catch((err) => 71 | # do nothing 72 | ) 73 | ) 74 | ).catch((err) => 75 | # do nothing 76 | ) 77 | load: -> 78 | @div.innerHTML = 'Loading arduino boards...' 79 | if @loaded 80 | @destroy true 81 | # first we parse the arduino15 file structure 82 | path = '' 83 | if /^win/.test process.platform 84 | path = process.env.LOCALAPPDATA + separator + 'Arduino15' 85 | else if process.platform == 'darwin' 86 | path = process.env.HOME + separator + 'Arduino15' 87 | else 88 | path = process.env.HOME + separator + '.arduino15' 89 | path += separator + 'packages' 90 | @parseNewPath(path).then( => 91 | if /^win/.test process.platform 92 | path = getArduinoPath().split(separator) 93 | path.pop() 94 | path = path.join(separator) + separator + 'hardware' 95 | return @parseOldPath(path) 96 | ).then( => 97 | # parse the pre-arduino 1.5 boards 98 | stdoutput = spawn getArduinoPath(), ['--get-pref', 'sketchbook.path'] 99 | return new Promise((resolve, reject) => 100 | res = '' 101 | stdoutput.stdout.on 'data', (data) => 102 | res += data.toString() 103 | stdoutput.on 'close', (code) => 104 | if code 105 | reject code 106 | else 107 | res = res.split('\r').join('').split('\n') 108 | while !res[res.length - 1] # remove the empty lines 109 | res.pop() 110 | res = res[res.length - 1] 111 | resolve res 112 | ).then((path) => 113 | path = path.strip() + separator + 'hardware' 114 | @parseOldPath(path) 115 | ) 116 | ).finally( => 117 | console.log "Boards found:", @boards 118 | @loaded = true 119 | @populateStatusBar() 120 | ) 121 | set: (val) -> 122 | if !@loaded || !@selectNode 123 | return # trash 124 | if @boards[val] || val == '' 125 | @selectNode.value = val 126 | else 127 | @selectNode.value = 'ignore' 128 | populateStatusBar: -> 129 | @div.innerHTML = '' 130 | @selectNode = document.createElement 'select' 131 | 132 | op = new Option() 133 | op.value = '' 134 | op.text = '== Arduino Setting ==' 135 | @selectNode.options.add op 136 | op = new Option() 137 | op.value = 'ignore' 138 | op.text = '== Custom Setting ==' 139 | @selectNode.options.add op 140 | for board of @boards 141 | op = new Option() 142 | op.value = board 143 | op.text = @boards[board] 144 | @selectNode.options.add op 145 | @div.appendChild @selectNode 146 | 147 | cfg = atom.config.get 'arduino-upload.board' 148 | if @boards[cfg] || cfg == '' 149 | @selectNode.value = cfg 150 | else 151 | @selectNode.value = 'ignore' 152 | @selectNode.onchange = -> 153 | if @value != 'ignore' 154 | atom.config.set 'arduino-upload.board', @value 155 | setStatusBar: (sb) -> 156 | @statusBar = sb.addRightTile item: @div, priority: 5 157 | hide: -> 158 | @div.style.display = 'none' 159 | show: -> 160 | @div.style.display = '' 161 | -------------------------------------------------------------------------------- /lib/output-view.coffee: -------------------------------------------------------------------------------- 1 | {View} = require 'atom-space-pen-views' 2 | escape = (s) -> 3 | s.replace(/&/g,'&' ).replace(/ 12 | @div class: 'arduino-upload info-view', => 13 | @button click: 'close', class: 'btn', 'close' 14 | @pre class: 'output', @message 15 | @initialize: -> 16 | super 17 | addLine: (line, tmppath = '', path = '') -> 18 | if tmppath && path 19 | line = line.replace /((?:\/|\n|^)[\w\-\/\.]+:)(\d)*/gi, (match) => 20 | # ok here we parse whcih filenames are clickable 21 | 22 | # extra holds the additional path in front, whch isn't clickable (such as the sketch path, or the tmp path) 23 | extra = '' 24 | # line holds the line number to jump to 25 | line = -1 26 | # match is the current thing to parse 27 | if !match.endsWith ':' 28 | line = match.substring match.lastIndexOf(':')+1 29 | match = match.substring 0, match.lastIndexOf(':') # we need this anyways to strip the last char 30 | 31 | # check if it is in the tmp directory. stripping that from the link 32 | if match.strip().startsWith tmppath 33 | extra += tmppath 34 | match = match.substring tmppath.length 35 | # strip optional trailing separator 36 | if match[0] == separator 37 | extra += separator 38 | match = match.substring 1 39 | # make sure that we are in the sketch folder 40 | if match.strip().substring(0, 7) == 'sketch' + separator 41 | extra += 'sketch' + separator 42 | match = match.substring 7 43 | 44 | # generate nice output 45 | file = match 46 | match += ':' 47 | if line != -1 48 | match += line 49 | file = file.strip() 50 | if separator != file.substring 0,1 51 | file = path + separator + file 52 | if file.lastIndexOf('.')==-1 || file.lastIndexOf('.')+20 < file.length 53 | file += '.ino' 54 | extra+''+match+'' 55 | @message += line 56 | this 57 | reset: -> 58 | @message = '' 59 | finish: -> 60 | if @message.trim() == '' 61 | @message = '' 62 | @hide() 63 | return 64 | @find('pre').html @message 65 | 66 | for elem in @find('pre a') 67 | elem.addEventListener 'click', -> 68 | for pane in atom.workspace.getPanes() 69 | for editor in pane.getItems() 70 | if editor.getPath && editor.getPath() == @dataset.file 71 | pane.activateItem editor 72 | if @dataset.line != -1 73 | editor.setCursorBufferPosition [@dataset.line - 1,0] 74 | editor.scrollToCursorPosition() # center the cursor 75 | pane.activate() 76 | return 77 | atom.workspace.open(@dataset.file).then (editor) => 78 | if @dataset.line != -1 79 | editor.setCursorBufferPosition [@dataset.line - 1,0] 80 | editor.scrollToCursorPosition() # center the cursor 81 | @show() 82 | close: -> 83 | @hide() 84 | -------------------------------------------------------------------------------- /lib/serial-view.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class SerialView 3 | editor: null 4 | editorView:null 5 | onDidDestroy: (callback) -> 6 | @editor.onDidDestroy => 7 | callback() 8 | sendCallback: null 9 | sendText: -> 10 | input = @editorView?.querySelector '#arduino-upload-serial-input' 11 | if input && @sendCallback 12 | add = ['','\n','\r','\r\n'] 13 | add = add[@editorView?.querySelector('#arduino-upload-serial-lineending').value] 14 | console.log @editorView?.querySelector('#arduino-upload-serial-lineending').value 15 | console.log add 16 | @sendCallback input.value+add 17 | input.value = '' 18 | onSend: (@sendCallback) -> 19 | open: (callback) -> 20 | atom.workspace.open('Serial Monitor').then (editor) => 21 | @editor = editor 22 | @editor.setText '' 23 | @editorView = @editor.element 24 | 25 | div = document.createElement 'div' 26 | div.className = 'arduino-upload-serial' 27 | div.innerHTML = '' 28 | @editorView.childNodes[0].style.height = 'calc(100% - 1.8em)' 29 | @editorView.appendChild div 30 | 31 | input = @editorView.querySelector '#arduino-upload-serial-input' 32 | input.addEventListener 'focus', (e) => 33 | e.preventDefault() 34 | e.stopPropagation() 35 | input.addEventListener 'keydown', (e) => 36 | e.stopPropagation() 37 | if e.keyCode == 13 # we hit enter 38 | @sendText() 39 | select = @editorView.querySelector '#arduino-upload-serial-lineending' 40 | select.value = atom.config.get 'arduino-upload.lineEnding' 41 | select.addEventListener 'change', -> 42 | atom.config.set 'arduino-upload.lineEnding',@value 43 | @editorView.querySelector('#arduino-upload-serial-send').addEventListener 'click', (e) => 44 | e.preventDefault() 45 | @sendText() 46 | callback() 47 | destroy: -> 48 | @editor?.destroy() 49 | sendCallback = null 50 | @editorView = null 51 | insertText: (s) -> 52 | if s instanceof Uint8Array 53 | s = new TextDecoder('utf-8').decode s 54 | console.log s 55 | @editor?.insertText s 56 | -------------------------------------------------------------------------------- /lib/util.coffee: -------------------------------------------------------------------------------- 1 | @separator = '/' 2 | if /^win/.test process.platform 3 | @separator = '\\' 4 | 5 | @getArduinoPath = () => 6 | execPath = atom.config.get 'arduino-upload.arduinoExecutablePath' 7 | if execPath == 'arduino' 8 | # we want the default path which is actually dependent on our os 9 | if process.platform == 'darwin' 10 | execPath = '/Applications/Arduino.app/Contents/MacOS/Arduino' 11 | if /^win/.test process.platform 12 | execPath = 'C:\\Program Files (x86)\\Arduino\\arduino_debug.exe' # arduino_debug.exe to not launch any GUI stuff 13 | return execPath 14 | -------------------------------------------------------------------------------- /menus/arduino-upload.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/hacking-atom-package-word-count#menus for more details 2 | 'menu': [ 3 | { 4 | 'label': 'Packages' 5 | 'submenu': [ 6 | 'label': 'Arduino Upload' 7 | 'submenu': [ 8 | { 9 | 'label': 'Verify' 10 | 'command': 'arduino-upload:verify' 11 | }, 12 | { 13 | 'label': 'Build' 14 | 'command': 'arduino-upload:build' 15 | }, 16 | { 17 | 'label': 'Upload' 18 | 'command': 'arduino-upload:upload' 19 | }, 20 | { 21 | 'label': 'Serial Monitor' 22 | 'command': 'arduino-upload:serial-monitor' 23 | } 24 | ] 25 | ] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arduino-upload", 3 | "main": "./lib/arduino-upload", 4 | "version": "1.8.0", 5 | "description": "Upload your arduino sketches without using the IDE", 6 | "keywords": [ 7 | "arduino", 8 | "upload", 9 | "sketch", 10 | "serial", 11 | "compile" 12 | ], 13 | "repository": "https://github.com/Sorunome/arduino-upload", 14 | "license": "MIT", 15 | "engines": { 16 | "atom": ">=1.0.0" 17 | }, 18 | "consumedServices": { 19 | "status-bar": { 20 | "versions": { 21 | ">=1.0.0": "consumeStatusBar" 22 | } 23 | } 24 | }, 25 | "dependencies": { 26 | "atom-space-pen-views": ">=2.0.3", 27 | "bluebird": ">=3.5.0", 28 | "serialport": ">=8.0.6", 29 | "tmp": ">=0.0.31", 30 | "usb-detection": ">=4.8.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spec/arduino-upload-spec.coffee: -------------------------------------------------------------------------------- 1 | ArduinoUpload = require '../lib/arduino-upload' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "ArduinoUpload", -> 9 | [workspaceElement, activationPromise] = [] 10 | 11 | beforeEach -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | activationPromise = atom.packages.activatePackage('arduino-upload') 14 | 15 | describe "when the arduino-upload:toggle event is triggered", -> 16 | it "hides and shows the modal panel", -> 17 | # Before the activation event the view is not on the DOM, and no panel 18 | # has been created 19 | expect(workspaceElement.querySelector('.arduino-upload')).not.toExist() 20 | 21 | # This is an activation event, triggering it will cause the package to be 22 | # activated. 23 | atom.commands.dispatch workspaceElement, 'arduino-upload:toggle' 24 | 25 | waitsForPromise -> 26 | activationPromise 27 | 28 | runs -> 29 | expect(workspaceElement.querySelector('.arduino-upload')).toExist() 30 | 31 | arduinoUploadElement = workspaceElement.querySelector('.arduino-upload') 32 | expect(arduinoUploadElement).toExist() 33 | 34 | arduinoUploadPanel = atom.workspace.panelForItem(arduinoUploadElement) 35 | expect(arduinoUploadPanel.isVisible()).toBe true 36 | atom.commands.dispatch workspaceElement, 'arduino-upload:toggle' 37 | expect(arduinoUploadPanel.isVisible()).toBe false 38 | 39 | it "hides and shows the view", -> 40 | # This test shows you an integration test testing at the view level. 41 | 42 | # Attaching the workspaceElement to the DOM is required to allow the 43 | # `toBeVisible()` matchers to work. Anything testing visibility or focus 44 | # requires that the workspaceElement is on the DOM. Tests that attach the 45 | # workspaceElement to the DOM are generally slower than those off DOM. 46 | jasmine.attachToDOM(workspaceElement) 47 | 48 | expect(workspaceElement.querySelector('.arduino-upload')).not.toExist() 49 | 50 | # This is an activation event, triggering it causes the package to be 51 | # activated. 52 | atom.commands.dispatch workspaceElement, 'arduino-upload:toggle' 53 | 54 | waitsForPromise -> 55 | activationPromise 56 | 57 | runs -> 58 | # Now we can test for view visibility 59 | arduinoUploadElement = workspaceElement.querySelector('.arduino-upload') 60 | expect(arduinoUploadElement).toBeVisible() 61 | atom.commands.dispatch workspaceElement, 'arduino-upload:toggle' 62 | expect(arduinoUploadElement).not.toBeVisible() 63 | -------------------------------------------------------------------------------- /spec/arduino-upload-view-spec.coffee: -------------------------------------------------------------------------------- 1 | ArduinoUploadView = require '../lib/arduino-upload-view' 2 | 3 | describe "ArduinoUploadView", -> 4 | it "has one valid test", -> 5 | expect("life").toBe "easy" 6 | -------------------------------------------------------------------------------- /styles/arduino-upload.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .arduino-upload > pre { 8 | max-height:200pt; 9 | } 10 | 11 | .editor .arduino-upload-serial { 12 | position: absolute; 13 | bottom: 0; 14 | left: 0; 15 | width: 100%; 16 | } 17 | /* copied from https://github.com/atom/atom/blob/master/static/buttons.less */ 18 | .editor .arduino-upload-serial button { 19 | color: @text-color; 20 | border-radius: @component-border-radius; 21 | border: none; 22 | text-shadow: none; 23 | 24 | height: @component-line-height + 2px; 25 | line-height: @component-line-height; 26 | 27 | padding: 0 @component-padding; 28 | font-size: @font-size; 29 | z-index: 0; 30 | 31 | background-color: @button-background-color; 32 | &:hover { 33 | background-color: @button-background-color-hover; 34 | } 35 | &.selected, 36 | &.selected:hover { 37 | // we want the selected button to behave like the :hover button; it's on top of the other buttons. 38 | z-index: 1; 39 | background-color: @button-background-color-selected; 40 | } 41 | } 42 | .editor .arduino-upload-serial select { 43 | color: @text-color; 44 | border-radius: @component-border-radius; 45 | border: none; 46 | text-shadow: none; 47 | 48 | height: @component-line-height + 2px; 49 | line-height: @component-line-height; 50 | 51 | padding: 0 @component-padding; 52 | font-size: @font-size; 53 | z-index: 0; 54 | 55 | background-color: @button-background-color; 56 | &:hover { 57 | background-color: @button-background-color-hover; 58 | } 59 | &.selected, 60 | &.selected:hover { 61 | // we want the selected button to behave like the :hover button; it's on top of the other buttons. 62 | z-index: 1; 63 | background-color: @button-background-color-selected; 64 | } 65 | } 66 | .editor .arduino-upload-serial #arduino-upload-serial-input { 67 | color: @text-color; 68 | border-color:@input-border-color; 69 | border-radius: @component-border-radius; 70 | border: none; 71 | text-shadow: none; 72 | 73 | height: @component-line-height + 2px; 74 | line-height: @component-line-height; 75 | 76 | padding: 0 @component-padding; 77 | font-size: @font-size; 78 | z-index: 0; 79 | 80 | background-color: @input-background-color; 81 | 82 | width: ~"calc(100% - 18em)"; // just tried out what looked OK 83 | } 84 | 85 | .status-bar .arduino-upload select { 86 | color: @text-color; 87 | border: none; 88 | text-shadow: none; 89 | 90 | z-index: 0; 91 | 92 | background-color: @button-background-color; 93 | &:hover { 94 | background-color: @button-background-color-hover; 95 | } 96 | &.selected, 97 | &.selected:hover { 98 | // we want the selected button to behave like the :hover button; it's on top of the other buttons. 99 | z-index: 1; 100 | background-color: @button-background-color-selected; 101 | } 102 | } 103 | --------------------------------------------------------------------------------