Skip to content

V2 #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 5, 2019
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -70,6 +70,34 @@ daemon.downloading.subscribe(download => {

```

## Version 2

Version 2 of the arduino-create-agent aims to provide a cleaner api based on promises.
It will remain confined to a v2 property on the daemon object until it will be stable.
At the moment it only supports tool management.

```js
daemon.agentV2Found.subscribe(daemonV2 => {
if (!daemonV2) {
// Your Agent doesn't support v2
}
// Your Agent supports v2
});

daemon.v2.installedTools()
.then(tools => console.debug(tools)) // [{"name":"avrdude","version":"6.3.0-arduino9","packager":"arduino"}]

let payload = {
name: 'avrdude',
version: '6.3.0-arduino9',
packager: 'arduino',
url: 'http://downloads.arduino.cc/tools/avrdude-6.3.0-arduino9-i686-w64-mingw32.zip',
checksum: 'SHA-256:f3c5cfa8d0b3b0caee81c5b35fb6acff89c342ef609bf4266734c6266a256d4f',
signature: '7628b488c7ffd21ae1ca657245751a4043c419fbab5c256a020fb53f17eb88686439f54f18e78a80b40fc2de742f79b78ed4338c959216dc8ae8279e482d2d4117eeaf34a281ce2369d1dc4356f782c0940d82610f1c892e913b637391c39e95d4d4dfe82d8dbc5350b833186a70a62c7952917481bad798a9c8b4905df91bd914fbdfd6e98ef75c8f7fb06284278da449ce05b27741d6eda156bbdb906d519ff7d7d5042379fdfc55962b3777fb9240b368552182758c297e39c72943d75d177f2dbb584b2210301250796dbe8af11f0cf06d762fe4f912294f4cdc8aff26715354cfb33010a81342fbbc438912eb424a39fc0c52a9b2bf722051a6f3b024bd'
}
daemon.v2.installTool(payload) // Will install the tool in the system
```

## Development and test features
Just run `npm run dev` and open your browser on http://localhost:8000

43 changes: 23 additions & 20 deletions demo/app.jsx
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import React from 'react';
import Daemon from '../src';

import { HEX } from './serial_mirror';
import V2 from './v2/v2.jsx';

const chromeExtensionID = 'hfejhkbipnickajaidoppbadcomekkde';

@@ -173,11 +174,11 @@ class App extends React.Component {
{device.Name} - IsOpen: <span className={device.IsOpen ? 'open' : 'closed'}>
{device.IsOpen ? 'true' : 'false'}
</span> - <a href="#" onClick={(e) => this.handleOpen(e, device.Name)}>
open
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matteosuppo pleeease, don't modify the formatting 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry vscode decided it was better that way. I didn't even notice. Should I put it the way it was?

Copy link
Contributor

@AlbyIanna AlbyIanna Apr 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, vscode has its own will sometimes. Yes please 👍

open
</a> - <a href="#" onClick={(e) => this.handleClose(e, device.Name)}>
close
close
</a> - <a href="#" onClick={(e) => handleBootloaderMode(e, device.Name)}>
bootloader mode
bootloader mode
</a>
</li>);

@@ -186,7 +187,7 @@ class App extends React.Component {
</li>);

const supportedBoards = this.state.supportedBoards.map((board, i) => <li key={i}>
{ board }
{board}
</li>);

let uploadClass;
@@ -213,24 +214,26 @@ class App extends React.Component {

return (
<div>
<V2 daemon={daemon}></V2>

<h1>Arduino Create Plugin Client Demo</h1>

<div className="section">
<h2>Plugin info</h2>

<p>
Agent status: <span className={ this.state.agentStatus ? 'found' : 'not-found' }>
{ this.state.agentStatus ? 'Found' : 'Not found' }
Agent status: <span className={this.state.agentStatus ? 'found' : 'not-found'}>
{this.state.agentStatus ? 'Found' : 'Not found'}
</span>
</p>
<p>
Channel status: <span className={ this.state.channelStatus ? 'found' : 'not-found' }>
{ this.state.channelStatus ? 'Connected' : 'Not connected' }
Channel status: <span className={this.state.channelStatus ? 'found' : 'not-found'}>
{this.state.channelStatus ? 'Connected' : 'Not connected'}
</span>
</p>

<pre>
{ this.state.agentInfo }
{this.state.agentInfo}
</pre>
</div>

@@ -239,12 +242,12 @@ class App extends React.Component {

<strong>serial:</strong>
<ul>
{ listSerialDevices }
{listSerialDevices}
</ul>

<strong>network:</strong>
<ul>
{ listNetworkDevices }
{listNetworkDevices}
</ul>

<p id="error"></p>
@@ -266,20 +269,20 @@ class App extends React.Component {
<h2>Serial Monitor</h2>

<form onSubmit={this.handleSend}>
<input id="serial-input" value={this.state.serialInput} onChange={this.handleChangeSerial}/>
<input id="serial-input" value={this.state.serialInput} onChange={this.handleChangeSerial} />
<input type="submit" value="Send" />
</form>

<textarea id="serial-textarea" value={ this.state.serialMonitorContent } readOnly/>
<textarea id="serial-textarea" value={this.state.serialMonitorContent} readOnly />
</div>

<div className="section">
<h2>Upload a sample sketch on a MKR1000 at /dev/ttyACM0</h2>
<button onClick={ handleUpload } disabled={ this.state.uploadStatus === daemon.UPLOAD_IN_PROGRESS }>Upload Sketch</button><br/>
<div>Upload status: <span className={ uploadClass }> { this.state.uploadStatus }</span> <span>{ this.state.uploadError }</span></div>
<button onClick={handleUpload} disabled={this.state.uploadStatus === daemon.UPLOAD_IN_PROGRESS}>Upload Sketch</button><br />
<div>Upload status: <span className={uploadClass}> {this.state.uploadStatus}</span> <span>{this.state.uploadError}</span></div>
</div>

{ daemon.downloading ? <div className="section">
{daemon.downloading ? <div className="section">
<h2>Download tool (not supported on Chrome OS)</h2>

<div>
@@ -300,19 +303,19 @@ class App extends React.Component {
</div>

<form onSubmit={handleDownloadTool}>
<div><input id="toolname" placeholder="Tool Name"/></div>
<div><input id="toolname" placeholder="Tool Name" /></div>
<div><input id="toolversion" placeholder="Tool Version" /></div>
<div><input id="package" placeholder="Package" /></div>
<div><input id="replacement" placeholder="Replacement strategy"/></div>
<div><input id="replacement" placeholder="Replacement strategy" /></div>

<input type="submit" value="Download" />
<div>Download status: <span className={ downloadClass }> { this.state.downloadStatus }</span></div>
<div>Download status: <span className={downloadClass}> {this.state.downloadStatus}</span></div>
</form>
</div> : null}

<div className="section">
<h2>Errors</h2>
<div className="error">{ this.state.error }</div>
<div className="error">{this.state.error}</div>
</div>
</div>
);
25 changes: 21 additions & 4 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
-->

<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -26,15 +27,20 @@
<style>
body {
color: #2c353a;
font-family: Lucida Grande,Lucida,Verdana,sans-serif;
font-family: Lucida Grande, Lucida, Verdana, sans-serif;
padding: 15px;
}

#error, .not-found, .closed, .error {
#error,
.not-found,
.closed,
.error {
color: red
}

.found, .open, .success {
.found,
.open,
.success {
color: green;
}

@@ -53,9 +59,20 @@
.section {
margin: 20px;
}

table {
border-collapse: collapse;
}

table td {
border: 1px solid black;
padding: 3px;
}
</style>
</head>

<body>
<div id="root"></div>
</body>
</html>

</html>
95 changes: 95 additions & 0 deletions demo/v2/install_tool.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import { flatMap } from 'rxjs/operators';

export class V2InstallTool extends React.Component {
constructor() {
super();
this.state = {
name: 'avrdude',
version: '6.3.0-arduino9',
packager: 'arduino',
url: 'http://downloads.arduino.cc/tools/avrdude-6.3.0-arduino9-i686-w64-mingw32.zip',
checksum: 'SHA-256:f3c5cfa8d0b3b0caee81c5b35fb6acff89c342ef609bf4266734c6266a256d4f',
signature: '7628b488c7ffd21ae1ca657245751a4043c419fbab5c256a020fb53f17eb88686439f54f18e78a80b40fc2de742f79b78ed4338c959216dc8ae8279e482d2d4117eeaf34a281ce2369d1dc4356f782c0940d82610f1c892e913b637391c39e95d4d4dfe82d8dbc5350b833186a70a62c7952917481bad798a9c8b4905df91bd914fbdfd6e98ef75c8f7fb06284278da449ce05b27741d6eda156bbdb906d519ff7d7d5042379fdfc55962b3777fb9240b368552182758c297e39c72943d75d177f2dbb584b2210301250796dbe8af11f0cf06d762fe4f912294f4cdc8aff26715354cfb33010a81342fbbc438912eb424a39fc0c52a9b2bf722051a6f3b024bd',
res: ''
};

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

componentDidMount() {
this.daemon = this.props.daemon;

this.daemon.agentV2Found.subscribe(daemonV2 => {
if (!daemonV2) {
return;
}
this.daemonV2 = daemonV2;
});
}

handleChange(event) {
this.setState({ [event.target.name]: event.target.value });
}

handleSubmit(event) {
event.preventDefault();

this.daemonV2.installTool({
name: this.state.name,
version: this.state.version,
packager: this.state.packager,
checksum: this.state.checksum,
signature: this.state.signature,
url: this.state.url,

})
.then(res => {
this.setState({
res: JSON.stringify(res, null, 2)
});
})
.catch(err => {
this.setState({
res: JSON.stringify(err, null, 2)
});
});
}

render() {
return (
<section>
<h2>Install a new tool</h2>
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" name="name" value={this.state.name} onChange={this.handleChange} />
</label><br />
<label>
Version:
<input type="text" name="version" value={this.state.version} onChange={this.handleChange} />
</label> <br />
<label>
Packager:
<input type="text" name="packager" value={this.state.packager} onChange={this.handleChange} />
</label> <br />
<label>
Url:
<input type="text" name="url" value={this.state.url} onChange={this.handleChange} />
</label> <br />
<label>
Checksum:
<input type="text" name="checksum" value={this.state.checksum} onChange={this.handleChange} />
</label> <br />
<label>
Signature:
<input type="text" name="signature" value={this.state.signature} onChange={this.handleChange} />
</label> <br />
<input type="submit" value="Submit" />
</form>
<textarea cols="100" rows="10" value={this.state.res} readOnly></textarea>
</section>
);
}
}
60 changes: 60 additions & 0 deletions demo/v2/v2.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { V2InstallTool } from './install_tool.jsx';

class V2 extends React.Component {
constructor() {
super();
this.state = {
tools: []
};
}

componentDidMount() {
this.daemon = this.props.daemon;

this.daemon.agentV2Found.subscribe(daemonV2 => {
if (!daemonV2) {
return;
}
this.daemonV2 = daemonV2;
this.daemonV2.installedTools().then(res => {
this.setState({
tools: res
});
});
});
}

render() {
const tools = this.state.tools.map((tool, i) => <tr key={i}>
<td>{tool.packager}</td>
<td>{tool.name}</td>
<td>{tool.version}</td>
</tr>);

return (
<section>
<h2>V2</h2>
<section>
<h3>Installed tools</h3>
<form onSubmit={this.handleInstallTool}>
<table>
<thead>
<tr>
<th>Packager</th>
<th>Name</th>
<th>Version</th>
</tr>
</thead>
<tbody>{tools}</tbody>
</table>
</form>

<V2InstallTool daemon={this.props.daemon}></V2InstallTool>
</section>
</section >
);
}
}

export default V2;
20 changes: 18 additions & 2 deletions src/socket-daemon.js
Original file line number Diff line number Diff line change
@@ -22,10 +22,11 @@ import io from 'socket.io-client';
import semVerCompare from 'semver-compare';
import { detect } from 'detect-browser';

import { timer } from 'rxjs';
import { timer, BehaviorSubject } from 'rxjs';
import { filter, takeUntil, first } from 'rxjs/operators';

import Daemon from './daemon';
import V2 from './socket-daemon.v2';

// Required agent version
const browser = detect();
@@ -62,10 +63,17 @@ export default class SocketDaemon extends Daemon {

this.openChannel(() => this.socket.emit('command', 'list'));

this.agentV2Found = new BehaviorSubject(null);

this.agentFound
.subscribe(agentFound => {
if (agentFound) {
this._wsConnect();
const v2 = new V2(this.pluginURL);
v2.init().then(() => {
this.v2 = v2;
this.agentV2Found.next(this.v2);
});
}
else {
this.findAgent();
@@ -458,7 +466,15 @@ export default class SocketDaemon extends Daemon {
_upload(uploadPayload, uploadCommandInfo) {
if (Array.isArray(uploadCommandInfo.tools)) {
uploadCommandInfo.tools.forEach(tool => {
this.downloadTool(tool.name, tool.version, tool.packager);
if (this.v2) {
this.downloading.next({ status: this.DOWNLOAD_IN_PROGRESS });
this.v2.installTool(tool).then(() => {
this.downloading.next({ status: this.DOWNLOAD_DONE });
});
}
else {
this.downloadTool(tool.name, tool.version, tool.packager);
}
});
}

52 changes: 52 additions & 0 deletions src/socket-daemon.v2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export default class SocketDaemonV2 {
constructor(daemonURL) {
this.daemonURL = `${daemonURL}/v2`;
}

// init tries an HEAD
init() {
return fetch(`${this.daemonURL}/pkgs/tools/installed`, {
method: 'HEAD',
}).then(res => {
if (res.status !== 200) {
throw Error('v2 not available');
}
return res;
});
}

// installedTools uses the new v2 apis to ask the daemon a list of the tools already present in the system
installedTools() {
return fetch(`${this.daemonURL}/pkgs/tools/installed`, {
method: 'GET',
}).then(res => res.json());
}

// installTool uses the new v2 apis to ask the daemon to download a specific tool on the system
// The expected payload is
// {
// "name": "avrdude",
// "version": "6.3.0-arduino9",
// "packager": "arduino",
// "url": "https://downloads.arduino.cc/...", // system-specific package containing the tool
// "signature": "e7Gh8309...", // proof that the url comes from a trusted source
// "checksum": "SHA256:90384nhfoso8..." // proof that the package wasn't tampered with
// }
installTool(payload) {
return fetch(`${this.daemonURL}/pkgs/tools/installed`, {
method: 'PUT',
body: JSON.stringify(payload)
}).then(res => res.json()
.then((json) => {
if (!res.ok) {
const error = Object.assign({}, json, {
status: res.status,
statusText: res.statusText,
});

return Promise.reject(error);
}
return json;
}));
}
}