Skip to content
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

Avr109 support #9

Merged
merged 5 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules
.eslintrc.js
lib
lib
examples
dist
20 changes: 20 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Publish Package to npmjs
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
scope: '@duinoapp'
- run: yarn
- run: yarn build
- run: yarn publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
lib
lib
dist
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2024 Fraser Bullock

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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

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.
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,108 @@ This project aims to achieve the following:
- Support ESP devices
- Platform for easy addition of new protocols

## Usage

install your favourite way
```bash
npm install @duinoapp/upload-multitool
yarn add @duinoapp/upload-multitool
pnpm add @duinoapp/upload-multitool
```

This package exports a few utilities, the main one is upload

```js
import { upload } from '@duinoapp/upload-multitool';
import type { ProgramConfig } from '@duinoapp/upload-multitool';
import { SerialPort } from 'serialport';

const serialport = new SerialPort({ path: '/dev/example', baudRate: 115200 });

const config = {
// for avr boards, the compiled hex
bin: compiled.hex,
// for esp boards, the compiled files and flash settings
files: compiled.files,
flashFreq: compiled.flashFreq,
flashMode: compiled.flashMode,
// baud rate to connect to bootloader
speed: 115200,
// baud rate to use for upload (ESP)
uploadSpeed: 115200,
// the tool to use, avrdude or esptool
tool: 'avr',
// the CPU of the device
cpu: 'atmega328p',
// a standard out interface ({ write(msg: string): void })
stdout: process.stdout,
// whether or not to log to stdout verbosely
verbose: true,
// handle reconnecting to AVR109 devices when connecting to the bootloader
// the device ID changes for the bootloader, meaning in some OS's a new connection is required
// avr109Reconnect?: (opts: ReconnectParams) => Promise<SerialPort>;
} as ProgramConfig;

const res = await upload(serial.port, config);

```

If you want to programmatically check if a tool/cpu is supported:

```js
import { isSupported } from '@duinoapp/upload-multitool';

console.log(isSupported('avr', 'atmega328p')); // true
```

Also exports some helpful utilities:

```js
import { WebSerialPort, SerialPortPromise, WebSerialPortPromise } from '@duinoapp/upload-multitool';

// WebSerialPort is a drop-in web replacement for serialport, with some useful static methods:

// Check whether the current browser supports the Web Serial API
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility
WebSerialPort.isSupported() // true/false

// request a serial connection from the user,
// first param takes requestPort options: https://developer.mozilla.org/en-US/docs/Web/API/Serial/requestPort#parameters
// second params takes the default open options
const serialport = WebSerialPort.requestPort({}, { baudRate: 115200 });
serialport.open((err) => {
if (!err) serialport.write('hello', (err2) => ...)
});

// get a list of the serial connections that have already been requested:
const list = WebSerialPort.list();

// A wrapper util around SerialPort that exposes the same methods but with promises
const serial = new SerialPortPromise(await WebSerialPort.requestPort());
await serial.open();
await serial.write('hello');

// A Merged class of both WebSerialPort and SerialPortPromise, probably use this one
const serial = WebSerialPortPromise.requestPort();
await serial.open();
await serial.write('hello');
```

### Upload return
The upload function will return an object:
```ts
{
// the time it took to complete the upload
time: number
// the final serial port used. In most cases the serial port passes in
// if you pass in a non promise port, internally it will wrap with SerialPortPromise
// if you pass in a promise port, it is likely the same object, you can check with the serialport.key value on SerialPortPromise
// if using AVR109 and a reconnect is needed, this will likely be a new connection.
serialport: SerialPortPromise | WebSerialPortPromise
}
```


## Get in touch
You can contact me in the #multitool-general channel of the duinoapp discord

Expand Down
236 changes: 236 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Upload Multitool</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../dist/index.umd.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/xterm.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="app">
<div>
<h1>Upload Multitool</h1>
<p>
This is a demo of the Upload Multitool.
It allows you to upload binaries to a microcontroller using a wide range of upload protocols.
</p>
<p>
Select a device test config below.
</p>
<select id="device"></select>
<button id="upload" disabled>Upload</button>
<button id="reconnect" disabled>Reconnect</button>
</div>
<div id="status"></div>
<div id="terminal"></div>
</div>
<script>
const { isSupported, upload, WebSerialPort } = uploadMultitool;

const setStatus = (status) => {
document.getElementById('status').innerHTML = status;
};
const asyncTimeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
setStatus('Loading...');

const deviceSelectEl = document.getElementById('device');
const uploadButtonEl = document.getElementById('upload');
const reconnectButtonEl = document.getElementById('reconnect');

const term = new Terminal();
term.open(document.getElementById('terminal'));

let config = { devices: {} };
let reconnectResolve;
let reconnectReject;
let reconnectOpts;

const getFilters = (deviceConfig) => {
const filters = [];
if (deviceConfig.vendorIds && deviceConfig.productIds) {
deviceConfig.vendorIds.forEach((vendorId) => {
deviceConfig.productIds.forEach((productId) => {
filters.push({
usbVendorId: parseInt(vendorId, 16),
usbProductId: parseInt(productId, 16),
});
});
});
} else if (deviceConfig.espChip || deviceConfig.mac) {
filters.push({ usbVendorId: 0x1a86, usbProductId: 0x7523 });
}
return filters;
};

const getBin = async (file, fqbn) => {
const key = Math.random().toString(16).substring(7);
const code = await fetch(`../test/code/${file}.ino`)
.then((r) => r.text())
.then(txt => txt.replace(/{{key}}/g, key));
const res = await fetch(`${config.compileServer}/v3/compile`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fqbn,
files: [{
content: code,
name: `${file}/${file}.ino`,
}],
}),
}).then((r) => r.json());
return { bin: res.hex, key, code, ...res };
};

const validateUpload = (serial, key) => new Promise((resolve, reject) => {
let cleanup;
const timeout = setTimeout(() => {
cleanup(new Error('Timeout validating upload'));
}, 10000);
const onData = (data) => {
if (data.toString('ascii').includes(key)) {
cleanup();
}
};
const onError = (err) => {
cleanup(err);
};
cleanup = (err) => {
clearTimeout(timeout);
serial.removeListener('data', onData);
serial.removeListener('error', onError);
if (err) {
reject(err);
} else {
resolve();
}
};
serial.on('data', onData);
serial.on('error', onError);
serial.write('ping\n');
});

deviceSelectEl.addEventListener('change', async (e) => {
const device = e.target.value;
const deviceConfig = config.devices[device];
if (!deviceConfig) return;
const { tool, cpu, name } = deviceConfig;
const isSupp = isSupported(tool, cpu);
setStatus(`${name} is ${isSupp ? '' : 'not '}Supported!`);
uploadButtonEl.disabled = !isSupp;

});

reconnectButtonEl.addEventListener('click', async () => {
if (!deviceSelectEl.value) return;
const deviceConfig = config.devices[deviceSelectEl.value];
if (!deviceConfig) return;
if (!reconnectResolve) return;
const filters = getFilters(deviceConfig);
try {
const port = await WebSerialPort.requestPort(
{ filters },
reconnectOpts,
);
if (!port) throw new Error(`could not locate ${deviceConfig.name}`);
else reconnectResolve(port);
} catch (err) {
reconnectReject(err);
}
reconnectButtonEl.disabled = true;
});

uploadButtonEl.addEventListener('click', async () => {
if (!deviceSelectEl.value) return;
const deviceConfig = config.devices[deviceSelectEl.value];
if (!deviceConfig) return;
uploadButtonEl.disabled = true;
term.clear();

try {
setStatus('Requesting Device...');
filters = getFilters(deviceConfig);
WebSerialPort.list().then(console.log);
let serial = await WebSerialPort.requestPort(
{ filters },
{ baudRate: deviceConfig.speed || 115200 },
);

setStatus('Compiling Device Code...');
const {
bin, files, flashMode, flashFreq, key,
} = await getBin(deviceConfig.code, deviceConfig.fqbn);

setStatus('Uploading...');
const res = await upload(serial, {
bin,
files,
flashMode,
flashFreq,
speed: deviceConfig.speed,
uploadSpeed: deviceConfig.uploadSpeed,
tool: deviceConfig.tool,
cpu: deviceConfig.cpu,
verbose: true,
stdout: term,
avr109Reconnect: async (opts) => {
console.log(opts);
// await asyncTimeout(200);
const list = await WebSerialPort.list();
const dev = list.find(d => deviceConfig.productIds.includes(d.productId) && deviceConfig.vendorIds.includes(d.vendorId));
console.log(dev, dev?.port);
if (dev) return new WebSerialPort(dev.port, opts);
reconnectOpts = opts;
return new Promise((resolve, reject) => {
reconnectResolve = resolve;
reconnectReject = reject;
reconnectButtonEl.disabled = false;
});
}
});

serial = res.serialport;

setStatus('Validating Upload...');
await validateUpload(serial, key);
setStatus('Cleaning Up...');
await serial.close();
setStatus(`Done! Success! Awesome! (${res.time}ms)`);
} catch (err) {
console.error(err);
setStatus(`Error: ${err.message}`);
}
uploadButtonEl.disabled = false;
});

(async () => {
config = jsyaml.load(await fetch('../test/test-config.yml').then(r => r.text()));
console.log(config);
Object.keys(config.devices).forEach(id => {
const device = config.devices[id];
const option = document.createElement('option');
option.value = id;
option.innerText = device.name;
deviceSelectEl.appendChild(option);
});
deviceSelectEl.value = '';

if (!isSupported('avr', 'atmega328p')) {
return setStatus('Error: Could not load uploader.');
}
if (!navigator.serial) {
return setStatus('Error: Could not load web Serial API.');
}
setStatus('Ready.');
console.log(await WebSerialPort.list());
})();
</script>
</body>
</html>
Loading