Skip to content

Commit a76651d

Browse files
authored
Merge pull request #9 from duinoapp/avr109-support
Avr109 support
2 parents 68bde07 + 140df3e commit a76651d

29 files changed

+2170
-130
lines changed

.eslintignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22
.eslintrc.js
3-
lib
3+
lib
4+
examples
5+
dist

.github/workflows/publish.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Publish Package to npmjs
2+
on:
3+
release:
4+
types: [published]
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
# Setup .npmrc file to publish to npm
11+
- uses: actions/setup-node@v4
12+
with:
13+
node-version: '20.x'
14+
registry-url: 'https://registry.npmjs.org'
15+
scope: '@duinoapp'
16+
- run: yarn
17+
- run: yarn build
18+
- run: yarn publish --access public
19+
env:
20+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
2-
lib
2+
lib
3+
dist

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2024 Fraser Bullock
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,108 @@ This project aims to achieve the following:
2323
- Support ESP devices
2424
- Platform for easy addition of new protocols
2525

26+
## Usage
27+
28+
install your favourite way
29+
```bash
30+
npm install @duinoapp/upload-multitool
31+
yarn add @duinoapp/upload-multitool
32+
pnpm add @duinoapp/upload-multitool
33+
```
34+
35+
This package exports a few utilities, the main one is upload
36+
37+
```js
38+
import { upload } from '@duinoapp/upload-multitool';
39+
import type { ProgramConfig } from '@duinoapp/upload-multitool';
40+
import { SerialPort } from 'serialport';
41+
42+
const serialport = new SerialPort({ path: '/dev/example', baudRate: 115200 });
43+
44+
const config = {
45+
// for avr boards, the compiled hex
46+
bin: compiled.hex,
47+
// for esp boards, the compiled files and flash settings
48+
files: compiled.files,
49+
flashFreq: compiled.flashFreq,
50+
flashMode: compiled.flashMode,
51+
// baud rate to connect to bootloader
52+
speed: 115200,
53+
// baud rate to use for upload (ESP)
54+
uploadSpeed: 115200,
55+
// the tool to use, avrdude or esptool
56+
tool: 'avr',
57+
// the CPU of the device
58+
cpu: 'atmega328p',
59+
// a standard out interface ({ write(msg: string): void })
60+
stdout: process.stdout,
61+
// whether or not to log to stdout verbosely
62+
verbose: true,
63+
// handle reconnecting to AVR109 devices when connecting to the bootloader
64+
// the device ID changes for the bootloader, meaning in some OS's a new connection is required
65+
// avr109Reconnect?: (opts: ReconnectParams) => Promise<SerialPort>;
66+
} as ProgramConfig;
67+
68+
const res = await upload(serial.port, config);
69+
70+
```
71+
72+
If you want to programmatically check if a tool/cpu is supported:
73+
74+
```js
75+
import { isSupported } from '@duinoapp/upload-multitool';
76+
77+
console.log(isSupported('avr', 'atmega328p')); // true
78+
```
79+
80+
Also exports some helpful utilities:
81+
82+
```js
83+
import { WebSerialPort, SerialPortPromise, WebSerialPortPromise } from '@duinoapp/upload-multitool';
84+
85+
// WebSerialPort is a drop-in web replacement for serialport, with some useful static methods:
86+
87+
// Check whether the current browser supports the Web Serial API
88+
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility
89+
WebSerialPort.isSupported() // true/false
90+
91+
// request a serial connection from the user,
92+
// first param takes requestPort options: https://developer.mozilla.org/en-US/docs/Web/API/Serial/requestPort#parameters
93+
// second params takes the default open options
94+
const serialport = WebSerialPort.requestPort({}, { baudRate: 115200 });
95+
serialport.open((err) => {
96+
if (!err) serialport.write('hello', (err2) => ...)
97+
});
98+
99+
// get a list of the serial connections that have already been requested:
100+
const list = WebSerialPort.list();
101+
102+
// A wrapper util around SerialPort that exposes the same methods but with promises
103+
const serial = new SerialPortPromise(await WebSerialPort.requestPort());
104+
await serial.open();
105+
await serial.write('hello');
106+
107+
// A Merged class of both WebSerialPort and SerialPortPromise, probably use this one
108+
const serial = WebSerialPortPromise.requestPort();
109+
await serial.open();
110+
await serial.write('hello');
111+
```
112+
113+
### Upload return
114+
The upload function will return an object:
115+
```ts
116+
{
117+
// the time it took to complete the upload
118+
time: number
119+
// the final serial port used. In most cases the serial port passes in
120+
// if you pass in a non promise port, internally it will wrap with SerialPortPromise
121+
// if you pass in a promise port, it is likely the same object, you can check with the serialport.key value on SerialPortPromise
122+
// if using AVR109 and a reconnect is needed, this will likely be a new connection.
123+
serialport: SerialPortPromise | WebSerialPortPromise
124+
}
125+
```
126+
127+
26128
## Get in touch
27129
You can contact me in the #multitool-general channel of the duinoapp discord
28130

examples/index.html

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Upload Multitool</title>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<script src="../dist/index.umd.js"></script>
8+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/xterm.css">
9+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.js"></script>
10+
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
11+
</head>
12+
<body>
13+
<noscript>
14+
You need to enable JavaScript to run this app.
15+
</noscript>
16+
<div id="app">
17+
<div>
18+
<h1>Upload Multitool</h1>
19+
<p>
20+
This is a demo of the Upload Multitool.
21+
It allows you to upload binaries to a microcontroller using a wide range of upload protocols.
22+
</p>
23+
<p>
24+
Select a device test config below.
25+
</p>
26+
<select id="device"></select>
27+
<button id="upload" disabled>Upload</button>
28+
<button id="reconnect" disabled>Reconnect</button>
29+
</div>
30+
<div id="status"></div>
31+
<div id="terminal"></div>
32+
</div>
33+
<script>
34+
const { isSupported, upload, WebSerialPort } = uploadMultitool;
35+
36+
const setStatus = (status) => {
37+
document.getElementById('status').innerHTML = status;
38+
};
39+
const asyncTimeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
40+
setStatus('Loading...');
41+
42+
const deviceSelectEl = document.getElementById('device');
43+
const uploadButtonEl = document.getElementById('upload');
44+
const reconnectButtonEl = document.getElementById('reconnect');
45+
46+
const term = new Terminal();
47+
term.open(document.getElementById('terminal'));
48+
49+
let config = { devices: {} };
50+
let reconnectResolve;
51+
let reconnectReject;
52+
let reconnectOpts;
53+
54+
const getFilters = (deviceConfig) => {
55+
const filters = [];
56+
if (deviceConfig.vendorIds && deviceConfig.productIds) {
57+
deviceConfig.vendorIds.forEach((vendorId) => {
58+
deviceConfig.productIds.forEach((productId) => {
59+
filters.push({
60+
usbVendorId: parseInt(vendorId, 16),
61+
usbProductId: parseInt(productId, 16),
62+
});
63+
});
64+
});
65+
} else if (deviceConfig.espChip || deviceConfig.mac) {
66+
filters.push({ usbVendorId: 0x1a86, usbProductId: 0x7523 });
67+
}
68+
return filters;
69+
};
70+
71+
const getBin = async (file, fqbn) => {
72+
const key = Math.random().toString(16).substring(7);
73+
const code = await fetch(`../test/code/${file}.ino`)
74+
.then((r) => r.text())
75+
.then(txt => txt.replace(/{{key}}/g, key));
76+
const res = await fetch(`${config.compileServer}/v3/compile`, {
77+
method: 'POST',
78+
headers: {
79+
'Content-Type': 'application/json',
80+
},
81+
body: JSON.stringify({
82+
fqbn,
83+
files: [{
84+
content: code,
85+
name: `${file}/${file}.ino`,
86+
}],
87+
}),
88+
}).then((r) => r.json());
89+
return { bin: res.hex, key, code, ...res };
90+
};
91+
92+
const validateUpload = (serial, key) => new Promise((resolve, reject) => {
93+
let cleanup;
94+
const timeout = setTimeout(() => {
95+
cleanup(new Error('Timeout validating upload'));
96+
}, 10000);
97+
const onData = (data) => {
98+
if (data.toString('ascii').includes(key)) {
99+
cleanup();
100+
}
101+
};
102+
const onError = (err) => {
103+
cleanup(err);
104+
};
105+
cleanup = (err) => {
106+
clearTimeout(timeout);
107+
serial.removeListener('data', onData);
108+
serial.removeListener('error', onError);
109+
if (err) {
110+
reject(err);
111+
} else {
112+
resolve();
113+
}
114+
};
115+
serial.on('data', onData);
116+
serial.on('error', onError);
117+
serial.write('ping\n');
118+
});
119+
120+
deviceSelectEl.addEventListener('change', async (e) => {
121+
const device = e.target.value;
122+
const deviceConfig = config.devices[device];
123+
if (!deviceConfig) return;
124+
const { tool, cpu, name } = deviceConfig;
125+
const isSupp = isSupported(tool, cpu);
126+
setStatus(`${name} is ${isSupp ? '' : 'not '}Supported!`);
127+
uploadButtonEl.disabled = !isSupp;
128+
129+
});
130+
131+
reconnectButtonEl.addEventListener('click', async () => {
132+
if (!deviceSelectEl.value) return;
133+
const deviceConfig = config.devices[deviceSelectEl.value];
134+
if (!deviceConfig) return;
135+
if (!reconnectResolve) return;
136+
const filters = getFilters(deviceConfig);
137+
try {
138+
const port = await WebSerialPort.requestPort(
139+
{ filters },
140+
reconnectOpts,
141+
);
142+
if (!port) throw new Error(`could not locate ${deviceConfig.name}`);
143+
else reconnectResolve(port);
144+
} catch (err) {
145+
reconnectReject(err);
146+
}
147+
reconnectButtonEl.disabled = true;
148+
});
149+
150+
uploadButtonEl.addEventListener('click', async () => {
151+
if (!deviceSelectEl.value) return;
152+
const deviceConfig = config.devices[deviceSelectEl.value];
153+
if (!deviceConfig) return;
154+
uploadButtonEl.disabled = true;
155+
term.clear();
156+
157+
try {
158+
setStatus('Requesting Device...');
159+
filters = getFilters(deviceConfig);
160+
WebSerialPort.list().then(console.log);
161+
let serial = await WebSerialPort.requestPort(
162+
{ filters },
163+
{ baudRate: deviceConfig.speed || 115200 },
164+
);
165+
166+
setStatus('Compiling Device Code...');
167+
const {
168+
bin, files, flashMode, flashFreq, key,
169+
} = await getBin(deviceConfig.code, deviceConfig.fqbn);
170+
171+
setStatus('Uploading...');
172+
const res = await upload(serial, {
173+
bin,
174+
files,
175+
flashMode,
176+
flashFreq,
177+
speed: deviceConfig.speed,
178+
uploadSpeed: deviceConfig.uploadSpeed,
179+
tool: deviceConfig.tool,
180+
cpu: deviceConfig.cpu,
181+
verbose: true,
182+
stdout: term,
183+
avr109Reconnect: async (opts) => {
184+
console.log(opts);
185+
// await asyncTimeout(200);
186+
const list = await WebSerialPort.list();
187+
const dev = list.find(d => deviceConfig.productIds.includes(d.productId) && deviceConfig.vendorIds.includes(d.vendorId));
188+
console.log(dev, dev?.port);
189+
if (dev) return new WebSerialPort(dev.port, opts);
190+
reconnectOpts = opts;
191+
return new Promise((resolve, reject) => {
192+
reconnectResolve = resolve;
193+
reconnectReject = reject;
194+
reconnectButtonEl.disabled = false;
195+
});
196+
}
197+
});
198+
199+
serial = res.serialport;
200+
201+
setStatus('Validating Upload...');
202+
await validateUpload(serial, key);
203+
setStatus('Cleaning Up...');
204+
await serial.close();
205+
setStatus(`Done! Success! Awesome! (${res.time}ms)`);
206+
} catch (err) {
207+
console.error(err);
208+
setStatus(`Error: ${err.message}`);
209+
}
210+
uploadButtonEl.disabled = false;
211+
});
212+
213+
(async () => {
214+
config = jsyaml.load(await fetch('../test/test-config.yml').then(r => r.text()));
215+
console.log(config);
216+
Object.keys(config.devices).forEach(id => {
217+
const device = config.devices[id];
218+
const option = document.createElement('option');
219+
option.value = id;
220+
option.innerText = device.name;
221+
deviceSelectEl.appendChild(option);
222+
});
223+
deviceSelectEl.value = '';
224+
225+
if (!isSupported('avr', 'atmega328p')) {
226+
return setStatus('Error: Could not load uploader.');
227+
}
228+
if (!navigator.serial) {
229+
return setStatus('Error: Could not load web Serial API.');
230+
}
231+
setStatus('Ready.');
232+
console.log(await WebSerialPort.list());
233+
})();
234+
</script>
235+
</body>
236+
</html>

0 commit comments

Comments
 (0)