[ATL-1533] Firmware&Certificate Uploader (#469)

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
This commit is contained in:
Francesco Stasi 2021-08-25 10:36:51 +02:00 committed by GitHub
parent 6233e1fa98
commit 302fb7b6af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 20276 additions and 19 deletions

View File

@ -58,3 +58,13 @@ The Config Service knows about your system, like for example the default sketch
#### Rebuild gRPC protocol interfaces #### Rebuild gRPC protocol interfaces
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command - Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
`yarn --cwd arduino-ide-extension generate-protocol` `yarn --cwd arduino-ide-extension generate-protocol`
### Customize Icons
ArduinoIde uses a customized version of FontAwesome.
In order to update/replace icons follow the following steps:
- import the file `arduino-icons.json` in [Icomoon](https://icomoon.io/app/#/projects)
- load it
- edit the icons as needed
- !! download the **new** `arduino-icons.json` file and put it in this repo
- Click on "Generate Font" in Icomoon, then download
- place the updated fonts in the `src/style/fonts` directory

File diff suppressed because one or more lines are too long

View File

@ -29,11 +29,12 @@
"@theia/monaco": "next", "@theia/monaco": "next",
"@theia/navigator": "next", "@theia/navigator": "next",
"@theia/outline-view": "next", "@theia/outline-view": "next",
"@theia/preferences": "next",
"@theia/output": "next", "@theia/output": "next",
"@theia/preferences": "next",
"@theia/search-in-workspace": "next", "@theia/search-in-workspace": "next",
"@theia/terminal": "next", "@theia/terminal": "next",
"@theia/workspace": "next", "@theia/workspace": "next",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2", "@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0", "@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3", "@types/btoa": "^1.2.3",
@ -140,7 +141,7 @@
"version": "0.18.3" "version": "0.18.3"
}, },
"fwuploader": { "fwuploader": {
"version": "1.0.2" "version": "2.0.0"
} }
} }
} }

View File

@ -223,7 +223,7 @@ import { CloudSketchbookWidget } from './widgets/cloud-sketchbook/cloud-sketchbo
import { CloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-widget'; import { CloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { createCloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-container'; import { createCloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-container';
import { CreateApi } from './create/create-api'; import { CreateApi } from './create/create-api';
import { ShareSketchDialog } from './dialogs.ts/cloud-share-sketch-dialog'; import { ShareSketchDialog } from './dialogs/cloud-share-sketch-dialog';
import { AuthenticationClientService } from './auth/authentication-client-service'; import { AuthenticationClientService } from './auth/authentication-client-service';
import { import {
AuthenticationService, AuthenticationService,
@ -237,6 +237,23 @@ import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget';
import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget'; import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget';
import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container'; import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container';
import { SketchCache } from './widgets/cloud-sketchbook/cloud-sketch-cache'; import { SketchCache } from './widgets/cloud-sketchbook/cloud-sketch-cache';
import { UploadFirmware } from './contributions/upload-firmware';
import {
UploadFirmwareDialog,
UploadFirmwareDialogProps,
UploadFirmwareDialogWidget,
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
import { UploadCertificate } from './contributions/upload-certificate';
import {
ArduinoFirmwareUploader,
ArduinoFirmwareUploaderPath,
} from '../common/protocol/arduino-firmware-uploader';
import {
UploadCertificateDialog,
UploadCertificateDialogProps,
UploadCertificateDialogWidget,
} from './dialogs/certificate-uploader/certificate-uploader-dialog';
const ElementQueries = require('css-element-queries/src/ElementQueries'); const ElementQueries = require('css-element-queries/src/ElementQueries');
@ -522,6 +539,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
) )
.inSingletonScope(); .inSingletonScope();
bind(ArduinoFirmwareUploader)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
context.container,
ArduinoFirmwareUploaderPath
)
)
.inSingletonScope();
// File-system extension // File-system extension
bind(FileSystemExt) bind(FileSystemExt)
.toDynamicValue((context) => .toDynamicValue((context) =>
@ -571,6 +597,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, About); Contribution.configure(bind, About);
Contribution.configure(bind, Debug); Contribution.configure(bind, Debug);
Contribution.configure(bind, Sketchbook); Contribution.configure(bind, Sketchbook);
Contribution.configure(bind, UploadFirmware);
Contribution.configure(bind, UploadCertificate);
Contribution.configure(bind, BoardSelection); Contribution.configure(bind, BoardSelection);
Contribution.configure(bind, OpenRecentSketch); Contribution.configure(bind, OpenRecentSketch);
Contribution.configure(bind, Help); Contribution.configure(bind, Help);
@ -713,4 +741,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
id: 'cloud-sketchbook-composite-widget', id: 'cloud-sketchbook-composite-widget',
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget), createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
})); }));
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
bind(UploadFirmwareDialogProps).toConstantValue({
title: 'UploadFirmware',
});
bind(UploadCertificateDialogWidget).toSelf().inSingletonScope();
bind(UploadCertificateDialog).toSelf().inSingletonScope();
bind(UploadCertificateDialogProps).toConstantValue({
title: 'UploadCertificate',
});
}); });

View File

@ -55,6 +55,11 @@ export const ArduinoConfigSchema: PreferenceSchema = {
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.', 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
default: true, default: true,
}, },
'arduino.board.certificates': {
type: 'string',
description: 'List of certificates that can be uploaded to boards',
default: '',
},
'arduino.sketchbook.showAllFiles': { 'arduino.sketchbook.showAllFiles': {
type: 'boolean', type: 'boolean',
description: description:
@ -123,6 +128,7 @@ export interface ArduinoConfiguration {
'arduino.window.autoScale': boolean; 'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number; 'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean; 'arduino.ide.autoUpdate': boolean;
'arduino.board.certificates': string;
'arduino.sketchbook.showAllFiles': boolean; 'arduino.sketchbook.showAllFiles': boolean;
'arduino.cloud.enabled': boolean; 'arduino.cloud.enabled': boolean;
'arduino.cloud.pull.warn': boolean; 'arduino.cloud.pull.warn': boolean;

View File

@ -0,0 +1,136 @@
import { inject, injectable } from 'inversify';
import {
Command,
MenuModelRegistry,
CommandRegistry,
Contribution,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { UploadCertificateDialog } from '../dialogs/certificate-uploader/certificate-uploader-dialog';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoPreferences } from '../arduino-preferences';
import {
arduinoCert,
certificateList,
} from '../dialogs/certificate-uploader/utils';
import { ArduinoFirmwareUploader } from '../../common/protocol/arduino-firmware-uploader';
@injectable()
export class UploadCertificate extends Contribution {
@inject(UploadCertificateDialog)
protected readonly dialog: UploadCertificateDialog;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
await this.dialog.open();
} finally {
this.dialogOpened = false;
}
},
isEnabled: () => !this.dialogOpened,
});
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
execute: async (certToRemove) => {
const certs = this.arduinoPreferences.get('arduino.board.certificates');
this.preferenceService.set(
'arduino.board.certificates',
certificateList(certs)
.filter((c) => c !== certToRemove)
.join(','),
PreferenceScope.User
);
},
isEnabled: (certToRemove) => certToRemove !== arduinoCert,
});
registry.registerCommand(UploadCertificate.Commands.UPLOAD_CERT, {
execute: async ({ fqbn, address, urls }) => {
return this.arduinoFirmwareUploader.uploadCertificates(
`-b ${fqbn} -a ${address} ${urls
.map((url: string) => `-u ${url}`)
.join(' ')}`
);
},
isEnabled: () => true,
});
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
execute: async (args: any) => {
this.contextMenuRenderer.render({
menuPath: ArduinoMenus.ROOT_CERTIFICATES__CONTEXT,
anchor: {
x: args.x,
y: args.y,
},
args: [args.cert],
});
},
isEnabled: () => true,
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadCertificate.Commands.OPEN.id,
label: UploadCertificate.Commands.OPEN.label,
order: '1',
});
registry.registerMenuAction(ArduinoMenus.ROOT_CERTIFICATES__CONTEXT, {
commandId: UploadCertificate.Commands.REMOVE_CERT.id,
label: UploadCertificate.Commands.REMOVE_CERT.label,
order: '1',
});
}
}
export namespace UploadCertificate {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-upload-certificate-open',
label: 'Upload SSL Root Certificates',
category: 'Arduino',
};
export const OPEN_CERT_CONTEXT: Command = {
id: 'arduino-certificate-open-context',
label: 'Open context',
category: 'Arduino',
};
export const REMOVE_CERT: Command = {
id: 'arduino-certificate-remove',
label: 'Remove',
category: 'Arduino',
};
export const UPLOAD_CERT: Command = {
id: 'arduino-certificate-upload',
label: 'Upload',
category: 'Arduino',
};
}
}

View File

@ -0,0 +1,49 @@
import { inject, injectable } from 'inversify';
import {
Command,
MenuModelRegistry,
CommandRegistry,
Contribution,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { UploadFirmwareDialog } from '../dialogs/firmware-uploader/firmware-uploader-dialog';
@injectable()
export class UploadFirmware extends Contribution {
@inject(UploadFirmwareDialog)
protected readonly dialog: UploadFirmwareDialog;
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadFirmware.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
await this.dialog.open();
} finally {
this.dialogOpened = false;
}
},
isEnabled: () => !this.dialogOpened,
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadFirmware.Commands.OPEN.id,
label: UploadFirmware.Commands.OPEN.label,
order: '0',
});
}
}
export namespace UploadFirmware {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-upload-firmware-open',
label: 'WiFi101 / WiFiNINA Firmware Updater',
category: 'Arduino',
};
}
}

View File

@ -97,6 +97,7 @@
"editorWhitespace.foreground": "#bfbfbf", "editorWhitespace.foreground": "#bfbfbf",
"editor.lineHighlightBackground": "#434f5410", "editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#ffcb00", "editor.selectionBackground": "#ffcb00",
"editorWidget.background": "#F7F9F9",
"focusBorder": "#7fcbcd99", "focusBorder": "#7fcbcd99",
"menubar.selectionBackground": "#ffffff", "menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121", "menubar.selectionForeground": "#212121",

View File

@ -0,0 +1,37 @@
import * as React from 'react';
export const CertificateAddComponent = ({
addCertificate,
}: {
addCertificate: (cert: string) => void;
}): React.ReactElement => {
const [value, setValue] = React.useState('');
const handleChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);
return (
<form
className="certificate-add"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
addCertificate(value);
setValue('');
}}
>
<label>
<div>Add URL to fetch SSL certificate</div>
<input
className="theia-input"
placeholder="Enter URL"
type="text"
name="add"
onChange={handleChange}
value={value}
/>
</label>
</form>
);
};

View File

@ -0,0 +1,51 @@
import * as React from 'react';
export const CertificateListComponent = ({
certificates,
selectedCerts,
setSelectedCerts,
openContextMenu,
}: {
certificates: string[];
selectedCerts: string[];
setSelectedCerts: React.Dispatch<React.SetStateAction<string[]>>;
openContextMenu: (x: number, y: number, cert: string) => void;
}): React.ReactElement => {
const handleOnChange = (event: any) => {
const target = event.target;
const newSelectedCerts = selectedCerts.filter(
(cert) => cert !== target.name
);
if (target.checked) {
newSelectedCerts.push(target.name);
}
setSelectedCerts(newSelectedCerts);
};
const handleContextMenu = (event: React.MouseEvent, cert: string) => {
openContextMenu(event.clientX, event.clientY, cert);
};
return (
<div className="certificate-list">
{certificates.map((certificate, i) => (
<label
key={i}
className="certificate-row"
onContextMenu={(e) => handleContextMenu(e, certificate)}
>
<span className="fl1">{certificate}</span>
<input
type="checkbox"
name={certificate}
checked={selectedCerts.includes(certificate)}
onChange={handleOnChange}
/>
</label>
))}
</div>
);
};

View File

@ -0,0 +1,157 @@
import * as React from 'react';
import Tippy from '@tippyjs/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';
import { SelectBoardComponent } from './select-board-components';
import { CertificateAddComponent } from './certificate-add-new';
export const CertificateUploaderComponent = ({
availableBoards,
certificates,
addCertificate,
updatableFqbns,
uploadCertificates,
openContextMenu,
}: {
availableBoards: AvailableBoard[];
certificates: string[];
addCertificate: (cert: string) => void;
updatableFqbns: string[];
uploadCertificates: (
fqbn: string,
address: string,
urls: string[]
) => Promise<any>;
openContextMenu: (x: number, y: number, cert: string) => void;
}): React.ReactElement => {
const [installFeedback, setInstallFeedback] = React.useState<
'ok' | 'fail' | 'installing' | null
>(null);
const [showAdd, setShowAdd] = React.useState(false);
const [selectedCerts, setSelectedCerts] = React.useState<string[]>([]);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const installCertificates = async () => {
if (!selectedBoard || !selectedBoard.fqbn || !selectedBoard.port) {
return;
}
setInstallFeedback('installing');
try {
await uploadCertificates(
selectedBoard.fqbn,
selectedBoard.port.address,
selectedCerts
);
setInstallFeedback('ok');
} catch {
setInstallFeedback('fail');
}
};
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setSelectedBoard(board);
}
},
[selectedBoard]
);
return (
<>
<div className="dialogSection">
<div className="dialogRow">
<strong className="fl1">1. Select certificate to upload</strong>
<Tippy
content={
<CertificateAddComponent
addCertificate={(cert) => {
addCertificate(cert);
setShowAdd(false);
}}
/>
}
placement="bottom-end"
onClickOutside={() => setShowAdd(false)}
visible={showAdd}
interactive={true}
>
<button
type="button"
className="theia-button primary add-cert-btn"
onClick={() => {
showAdd ? setShowAdd(false) : setShowAdd(true);
}}
>
Add New <span className="fa fa-caret-down caret"></span>
</button>
</Tippy>
</div>
<div className="dialogRow">
<CertificateListComponent
certificates={certificates}
selectedCerts={selectedCerts}
setSelectedCerts={setSelectedCerts}
openContextMenu={openContextMenu}
/>
</div>
</div>
<div className="dialogSection">
<div className="dialogRow">
<strong>2. Select destination board and upload certificate</strong>
</div>
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
</div>
<div className="dialogRow">
<div className="upload-status">
{installFeedback === 'installing' && (
<div className="success">
<div className="spinner" />
Uploading certificates.
</div>
)}
{installFeedback === 'ok' && (
<div className="success">
<i className="fa fa-info status-icon" />
Cetificates uploaded.
</div>
)}
{installFeedback === 'fail' && (
<div className="warn">
<i className="fa fa-exclamation status-icon" />
Upload failed. Please try again.
</div>
)}
</div>
<button
type="button"
className="theia-button primary install-cert-btn"
onClick={installCertificates}
disabled={selectedCerts.length === 0 || !selectedBoard}
>
Upload
</button>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,190 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import { CertificateUploaderComponent } from './certificate-uploader-component';
import { ArduinoPreferences } from '../../arduino-preferences';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { certificateList, sanifyCertString } from './utils';
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
@injectable()
export class UploadCertificateDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected certificates: string[] = [];
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.arduinoPreferences.ready.then(() => {
this.certificates = certificateList(
this.arduinoPreferences.get('arduino.board.certificates')
);
});
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.board.certificates' &&
event.newValue !== event.oldValue
) {
this.certificates = certificateList(event.newValue);
this.update();
}
});
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
private addCertificate(certificate: string) {
const certString = sanifyCertString(certificate);
if (certString.length > 0) {
this.certificates.push(sanifyCertString(certificate));
}
this.preferenceService.set(
'arduino.board.certificates',
this.certificates.join(','),
PreferenceScope.User
);
}
protected openContextMenu(x: number, y: number, cert: string): void {
this.commandRegistry.executeCommand(
'arduino-certificate-open-context',
Object.assign({}, { x, y, cert })
);
}
protected uploadCertificates(
fqbn: string,
address: string,
urls: string[]
): Promise<any> {
this.busyCallback(true);
return this.commandRegistry
.executeCommand('arduino-certificate-upload', {
fqbn,
address,
urls,
})
.finally(() => this.busyCallback(false));
}
protected render(): React.ReactNode {
return (
<CertificateUploaderComponent
availableBoards={this.availableBoards}
certificates={this.certificates}
updatableFqbns={this.updatableFqbns}
addCertificate={this.addCertificate.bind(this)}
uploadCertificates={this.uploadCertificates.bind(this)}
openContextMenu={this.openContextMenu.bind(this)}
/>
);
}
}
@injectable()
export class UploadCertificateDialogProps extends DialogProps {}
@injectable()
export class UploadCertificateDialog extends AbstractDialog<void> {
@inject(UploadCertificateDialogWidget)
protected readonly widget: UploadCertificateDialogWidget;
private busy = false;
constructor(
@inject(UploadCertificateDialogProps)
protected readonly props: UploadCertificateDialogProps
) {
super({ title: 'Upload SSL Root Certificates' });
this.contentNode.classList.add('certificate-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
if (this.busy) {
return;
}
super.close();
}
busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
} else {
this.closeCrossNode.classList.remove('disabled');
}
}
}

View File

@ -0,0 +1,91 @@
import * as React from 'react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
type BoardOption = { value: string; label: string };
export const SelectBoardComponent = ({
availableBoards,
updatableFqbns,
onBoardSelect,
selectedBoard,
busy,
}: {
availableBoards: AvailableBoard[];
updatableFqbns: string[];
onBoardSelect: (board: AvailableBoard | null) => void;
selectedBoard: AvailableBoard | null;
busy: boolean;
}): React.ReactElement => {
const [selectOptions, setSelectOptions] = React.useState<BoardOption[]>([]);
const [selectBoardPlaceholder, setSelectBoardPlaceholder] =
React.useState('');
const selectOption = React.useCallback(
(boardOpt: BoardOption) => {
onBoardSelect(
(boardOpt &&
availableBoards.find((board) => board.fqbn === boardOpt.value)) ||
null
);
},
[availableBoards, onBoardSelect]
);
React.useEffect(() => {
// if there is activity going on, skip updating the boards (avoid flickering)
if (busy) {
return;
}
let placeholderTxt = 'Select a board...';
let selBoard = -1;
const updatableBoards = availableBoards.filter(
(board) => board.port && board.fqbn && updatableFqbns.includes(board.fqbn)
);
const boardsList: BoardOption[] = updatableBoards.map((board, i) => {
if (board.selected) {
selBoard = i;
}
return {
label: `${board.name} at ${board.port?.address}`,
value: board.fqbn || '',
};
});
if (boardsList.length === 0) {
placeholderTxt = 'No supported board connected';
}
setSelectBoardPlaceholder(placeholderTxt);
setSelectOptions(boardsList);
if (selectedBoard) {
selBoard = boardsList
.map((boardOpt) => boardOpt.value)
.indexOf(selectedBoard.fqbn || '');
}
selectOption(boardsList[selBoard] || null);
}, [busy, availableBoards, selectOption, updatableFqbns, selectedBoard]);
return (
<ArduinoSelect
id="board-select"
menuPosition="fixed"
isDisabled={selectOptions.length === 0 || busy}
placeholder={selectBoardPlaceholder}
options={selectOptions}
value={
(selectedBoard && {
value: selectedBoard.fqbn,
label: `${selectedBoard.name} at ${selectedBoard.port?.address}`,
}) ||
null
}
tabSelectsValue={false}
onChange={selectOption}
/>
);
};

View File

@ -0,0 +1,38 @@
export const arduinoCert = 'arduino.cc:443';
export function sanifyCertString(cert: string): string {
const regex = /^(?:.*:\/\/)*(\S+\.+[^:]*):*(\d*)*$/gm;
const m = regex.exec(cert);
if (!m) {
return '';
}
const domain = m[1] || '';
const port = m[2] || '443';
if (domain.length === 0 || port.length === 0) {
return '';
}
return `${domain}:${port}`;
}
export function certificateList(certificates: string): string[] {
let certs = certificates
.split(',')
.map((cert) => sanifyCertString(cert.trim()))
.filter((cert) => {
// remove empty certificates
if (!cert || cert.length === 0) {
return false;
}
return true;
});
// add arduino certificate at the top of the list
certs = certs.filter((cert) => cert !== arduinoCert);
certs.unshift(arduinoCert);
return certs;
}

View File

@ -42,10 +42,6 @@ export const ShareSketchComponent = ({
createApi: CreateApi; createApi: CreateApi;
domain?: string; domain?: string;
}): React.ReactElement => { }): React.ReactElement => {
// const [publicVisibility, setPublicVisibility] = React.useState<boolean>(
// treeNode.isPublic
// );
const [loading, setloading] = React.useState<boolean>(false); const [loading, setloading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => { const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {

View File

@ -8,7 +8,7 @@ import {
} from '@theia/core/lib/browser/dialogs'; } from '@theia/core/lib/browser/dialogs';
@injectable() @injectable()
export class DoNotAskAgainConfirmDialogProps extends ConfirmDialogProps { export class DoNotAskAgainDialogProps extends ConfirmDialogProps {
readonly onAccept: () => Promise<void>; readonly onAccept: () => Promise<void>;
} }
@ -17,8 +17,8 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
protected readonly doNotAskAgainCheckbox: HTMLInputElement; protected readonly doNotAskAgainCheckbox: HTMLInputElement;
constructor( constructor(
@inject(DoNotAskAgainConfirmDialogProps) @inject(DoNotAskAgainDialogProps)
protected readonly props: DoNotAskAgainConfirmDialogProps protected readonly props: DoNotAskAgainDialogProps
) { ) {
super(props); super(props);
this.controlPanel.removeChild(this.errorMessageNode); this.controlPanel.removeChild(this.errorMessageNode);

View File

@ -0,0 +1,204 @@
import * as React from 'react';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SelectBoardComponent } from '../certificate-uploader/select-board-components';
type FirmwareOption = { value: string; label: string };
export const FirmwareUploaderComponent = ({
availableBoards,
firmwareUploader,
updatableFqbns,
flashFirmware,
isOpen,
}: {
availableBoards: AvailableBoard[];
firmwareUploader: ArduinoFirmwareUploader;
updatableFqbns: string[];
flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
isOpen: any;
}): React.ReactElement => {
// boolean states for buttons
const [firmwaresFetching, setFirmwaresFetching] = React.useState(false);
const [installFeedback, setInstallFeedback] = React.useState<
'ok' | 'fail' | 'installing' | null
>(null);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const [availableFirmwares, setAvailableFirmwares] = React.useState<
FirmwareInfo[]
>([]);
React.useEffect(() => {
setAvailableFirmwares([]);
}, [isOpen]);
const [selectedFirmware, setSelectedFirmware] =
React.useState<FirmwareOption | null>(null);
const [firmwareOptions, setFirmwareOptions] = React.useState<
FirmwareOption[]
>([]);
const fetchFirmwares = React.useCallback(async () => {
setInstallFeedback(null);
setFirmwaresFetching(true);
if (!selectedBoard) {
return;
}
// fetch the firmwares for the selected board
const firmwaresForFqbn = await firmwareUploader.availableFirmwares(
selectedBoard.fqbn || ''
);
setAvailableFirmwares(firmwaresForFqbn);
const firmwaresOpts = firmwaresForFqbn.map((f) => ({
label: f.firmware_version,
value: f.firmware_version,
}));
setFirmwareOptions(firmwaresOpts);
if (firmwaresForFqbn.length > 0) setSelectedFirmware(firmwaresOpts[0]);
setFirmwaresFetching(false);
}, [firmwareUploader, selectedBoard]);
const installFirmware = React.useCallback(async () => {
setInstallFeedback('installing');
const firmwareToFlash = availableFirmwares.find(
(firmware) => firmware.firmware_version === selectedFirmware?.value
);
try {
const installStatus =
!!firmwareToFlash &&
!!selectedBoard?.port &&
(await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
setInstallFeedback((installStatus && 'ok') || 'fail');
} catch {
setInstallFeedback('fail');
}
}, [firmwareUploader, selectedBoard, selectedFirmware, availableFirmwares]);
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setAvailableFirmwares([]);
setSelectedBoard(board);
}
},
[selectedBoard]
);
return (
<>
<div className="dialogSection">
<div className="dialogRow">
<label htmlFor="board-select">Select board</label>
</div>
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
<button
type="button"
className="theia-button secondary"
disabled={
selectedBoard === null ||
firmwaresFetching ||
installFeedback === 'installing'
}
onClick={fetchFirmwares}
>
Check Updates
</button>
</div>
</div>
{availableFirmwares.length > 0 && (
<>
<div className="dialogSection">
<div className="dialogRow">
<label htmlFor="firmware-select" className="fl1">
Select firmware version
</label>
<ArduinoSelect
id="firmware-select"
menuPosition="fixed"
isDisabled={
!selectedBoard ||
firmwaresFetching ||
installFeedback === 'installing'
}
options={firmwareOptions}
value={selectedFirmware}
tabSelectsValue={false}
onChange={(value) => {
if (value) {
setInstallFeedback(null);
setSelectedFirmware(value);
}
}}
/>
<button
type="button"
className="theia-button primary"
disabled={
selectedFirmware === null ||
firmwaresFetching ||
installFeedback === 'installing'
}
onClick={installFirmware}
>
Install
</button>
</div>
</div>
<div className="dialogSection">
{installFeedback === null && (
<div className="dialogRow warn">
<i className="fa fa-exclamation status-icon" />
Installation will overwrite the Sketch on the board.
</div>
)}
{installFeedback === 'installing' && (
<div className="dialogRow success">
<div className="spinner" />
Installing firmware.
</div>
)}
{installFeedback === 'ok' && (
<div className="dialogRow success">
<i className="fa fa-info status-icon" />
Firmware succesfully installed.
</div>
)}
{installFeedback === 'fail' && (
<div className="dialogRow warn">
<i className="fa fa-exclamation status-icon" />
Installation failed. Please try again.
</div>
)}
</div>
</>
)}
</>
);
};

View File

@ -0,0 +1,141 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import { FirmwareUploaderComponent } from './firmware-uploader-component';
import { UploadFirmware } from '../../contributions/upload-firmware';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
protected flashFirmware(firmware: FirmwareInfo, port: string): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
protected render(): React.ReactNode {
return (
<form>
<FirmwareUploaderComponent
availableBoards={this.availableBoards}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
);
}
}
@injectable()
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends AbstractDialog<void> {
@inject(UploadFirmwareDialogWidget)
protected readonly widget: UploadFirmwareDialogWidget;
private busy = false;
constructor(
@inject(UploadFirmwareDialogProps)
protected readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
if (this.busy) {
return;
}
this.widget.close();
super.close();
}
busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
} else {
this.closeCrossNode.classList.remove('disabled');
}
}
}

View File

@ -86,8 +86,13 @@ export namespace ArduinoMenus {
// -- Tools // -- Tools
export const TOOLS = [...MAIN_MENU_BAR, '4_tools']; export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
// `Auto Format`, `Library Manager...`, `Boards Manager...` // `Auto Format`, `Archive Sketch`, `Manage Libraries...`, `Serial Monitor`
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main']; export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
// `WiFi101 / WiFiNINA Firmware Updater`
export const TOOLS__FIRMWARE_UPLOADER_GROUP = [
...TOOLS,
'1_firmware_uploader',
];
// `Board`, `Port`, and `Get Board Info`. // `Board`, `Port`, and `Get Board Info`.
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection']; export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader` // Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
@ -143,6 +148,11 @@ export namespace ArduinoMenus {
...SKETCH_CONTROL__CONTEXT, ...SKETCH_CONTROL__CONTEXT,
'2_resources', '2_resources',
]; ];
// -- ROOT SSL CERTIFICATES
export const ROOT_CERTIFICATES__CONTEXT = [
'arduino-root-certificates--context',
];
} }
/** /**

View File

@ -0,0 +1,74 @@
.certificate-uploader-dialog {
width: 600px;
}
.certificate-uploader-dialog .theia-select {
border: none !important;
}
.certificate-uploader-dialog .arduino-select__control {
height: 31px;
background: var(--theia-menubar-selectionBackground) !important;
}
.certificate-uploader-dialog .dialogRow > button{
margin-right: 3px;
}
.certificate-uploader-dialog .certificate-list {
border: 1px solid #BDC7C7;
border-radius: 2px;;
background: var(--theia-menubar-selectionBackground) !important;
overflow: auto;
height: 120px;
flex: 1;
}
.certificate-uploader-dialog .certificate-list .certificate-row {
display: flex;
padding: 6px 10px 5px 10px
}
.certificate-uploader-dialog .certificate-list .certificate-row:hover {
background-color: var(--theia-list-activeSelectionBackground);
}
.certificate-uploader-dialog .upload-status {
display: flex;
align-items: center;
flex: 1;
}
.certificate-uploader-dialog .success {
display: flex;
align-items: center;
color: #1DA086;
}
.certificate-uploader-dialog .warn {
color: #C11F09;
}
.certificate-uploader-dialog .status-icon {
margin-right: 10px;
}
.certificate-uploader-dialog .add-cert-btn {
display: flex;
align-items: center;
justify-content: space-between;
}
.certificate-uploader-dialog .add-cert-btn .caret {
margin-left: 6px;
}
.certificate-add {
padding: 16px;
background-color: var(--theia-list-hoverBackground);
border-radius: 3px;
border: 1px solid #BDC7C7;
}
.certificate-add input {
margin-top: 12px;
padding: 0 12px;
width: 100%;
box-sizing: border-box;
}

View File

@ -0,0 +1,65 @@
.p-Widget.dialogOverlay .dialogBlock {
border-radius: 3px;
padding: 0 28px;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.25);
min-height: 0px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
padding: 36px 0 28px;
font-weight: 500;
background-color: transparent;
font-size: var(--theia-ui-font-size2);
color: var(--theia-list-inactiveSelectionForeground);
min-height: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogControl {
padding: 0 0 36px;
min-height: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent {
padding: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent > div {
padding: 0 0 12px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection {
margin-top: 28px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child {
margin-top: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow {
margin-top: 16px;
display: flex;
align-items: center;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
background: var(--theia-icon-loading) center center no-repeat;
animation: theia-spin 1.25s linear infinite;
width: 30px;
height: 30px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child {
margin-top: 0px;
}
.fl1{
flex: 1;
}
.status-icon {
margin-right: 6px;
font-size: 17px;
}
.fa.disabled {
opacity: .4;
}

View File

@ -0,0 +1,32 @@
.firmware-uploader-dialog {
width: 600px;
}
.firmware-uploader-dialog .theia-select {
border: none !important;
}
.firmware-uploader-dialog .arduino-select__control {
height: 31px;
background: var(--theia-menubar-selectionBackground) !important;
}
.firmware-uploader-dialog .dialogRow > button{
width: 33%;
margin-right: 3px;
}
.firmware-uploader-dialog #firmware-select {
flex: unset;
}
.firmware-uploader-dialog .success {
color: #1DA086;
}
.firmware-uploader-dialog .warn {
color: #C11F09;
}
.firmware-uploader-dialog .status-icon {
margin-right: 10px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 667 KiB

View File

@ -1,15 +1,19 @@
@import './list-widget.css'; @import './list-widget.css';
@import './boards-config-dialog.css'; @import './boards-config-dialog.css';
@import './main.css'; @import './main.css';
@import './dialogs.css';
@import './monitor.css'; @import './monitor.css';
@import './arduino-select.css'; @import './arduino-select.css';
@import './status-bar.css'; @import './status-bar.css';
@import './terminal.css'; @import './terminal.css';
@import './editor.css'; @import './editor.css';
@import './settings-dialog.css'; @import './settings-dialog.css';
@import './firmware-uploader-dialog.css';
@import './certificate-uploader-dialog.css';
@import './debug.css'; @import './debug.css';
@import './sketchbook.css'; @import './sketchbook.css';
@import './cloud-sketchbook.css'; @import './cloud-sketchbook.css';
@import './fonts.css';
.theia-input.warning:focus { .theia-input.warning:focus {
outline-width: 1px; outline-width: 1px;

View File

@ -169,10 +169,6 @@
margin-left: 10px; margin-left: 10px;
} }
.p-Widget.dialogOverlay .dialogBlock {
background-color: var(--theia-arduino-foreground);
}
#arduino-open-sketch-control--toolbar--container { #arduino-open-sketch-control--toolbar--container {
background-color: var(--theia-arduino-toolbar-background); background-color: var(--theia-arduino-toolbar-background);
} }

View File

@ -50,7 +50,9 @@
.additional-urls-dialog .link:hover { .additional-urls-dialog .link:hover {
color: var(--theia-textLink-activeForeground); color: var(--theia-textLink-activeForeground);
} }
.arduino-settings-dialog .react-tabs__tab-panel {
padding-bottom: 25px;
}
.arduino-settings-dialog .react-tabs__tab-list { .arduino-settings-dialog .react-tabs__tab-list {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -0,0 +1 @@
Sketchcache = is a cache that holds sketches and fileStat objects.

View File

@ -15,7 +15,7 @@ import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CloudSketchbookTree } from './cloud-sketchbook-tree'; import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { CloudUserCommands } from '../../auth/cloud-user-commands'; import { CloudUserCommands } from '../../auth/cloud-user-commands';
import { ShareSketchDialog } from '../../dialogs.ts/cloud-share-sketch-dialog'; import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
import { CreateApi } from '../../create/create-api'; import { CreateApi } from '../../create/create-api';
import { import {
PreferenceService, PreferenceService,

View File

@ -25,7 +25,7 @@ import {
LocalCacheUri, LocalCacheUri,
} from '../../local-cache/local-cache-fs-provider'; } from '../../local-cache/local-cache-fs-provider';
import { CloudSketchbookCommands } from './cloud-sketchbook-contributions'; import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
import { DoNotAskAgainConfirmDialog } from '../../dialogs.ts/dialogs'; import { DoNotAskAgainConfirmDialog } from '../../dialogs/do-not-ask-again-dialog';
import { SketchbookTree } from '../sketchbook/sketchbook-tree'; import { SketchbookTree } from '../sketchbook/sketchbook-tree';
import { firstToUpperCase } from '../../../common/utils'; import { firstToUpperCase } from '../../../common/utils';
import { ArduinoPreferences } from '../../arduino-preferences'; import { ArduinoPreferences } from '../../arduino-preferences';

View File

@ -0,0 +1,17 @@
export const ArduinoFirmwareUploaderPath =
'/services/arduino-firmware-uploader';
export const ArduinoFirmwareUploader = Symbol('ArduinoFirmwareUploader');
export type FirmwareInfo = {
board_name: string;
board_fqbn: string;
module: string;
firmware_version: string;
Latest: boolean;
};
export interface ArduinoFirmwareUploader {
list(fqbn?: string): Promise<FirmwareInfo[]>;
flash(firmware: FirmwareInfo, port: string): Promise<string>;
uploadCertificates(command: string): Promise<any>;
updatableBoards(): Promise<string[]>;
availableFirmwares(fqbn: string): Promise<FirmwareInfo[]>;
}

View File

@ -1,5 +1,10 @@
export const ExecutableServicePath = '/services/executable-service'; export const ExecutableServicePath = '/services/executable-service';
export const ExecutableService = Symbol('ExecutableService'); export const ExecutableService = Symbol('ExecutableService');
export interface ExecutableService { export interface ExecutableService {
list(): Promise<{ clangdUri: string; cliUri: string; lsUri: string }>; list(): Promise<{
clangdUri: string;
cliUri: string;
lsUri: string;
fwuploaderUri: string;
}>;
} }

View File

@ -0,0 +1,80 @@
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../common/protocol/arduino-firmware-uploader';
import { injectable, inject, named } from 'inversify';
import { ExecutableService } from '../common/protocol';
import { getExecPath, spawnCommand } from './exec-util';
import { ILogger } from '@theia/core/lib/common/logger';
@injectable()
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
@inject(ExecutableService)
protected executableService: ExecutableService;
protected _execPath: string | undefined;
@inject(ILogger)
@named('fwuploader')
protected readonly logger: ILogger;
protected onError(error: any): void {
this.logger.error(error);
}
async getExecPath(): Promise<string> {
if (this._execPath) {
return this._execPath;
}
this._execPath = await getExecPath('arduino-fwuploader');
return this._execPath;
}
async runCommand(args: string[]): Promise<any> {
const execPath = await this.getExecPath();
return await spawnCommand(`"${execPath}"`, args, this.onError.bind(this));
}
async uploadCertificates(command: string): Promise<any> {
return await this.runCommand(['certificates', 'flash', command]);
}
async list(fqbn?: string): Promise<FirmwareInfo[]> {
const fqbnFlag = fqbn ? ['--fqbn', fqbn] : [];
const firmwares: FirmwareInfo[] =
JSON.parse(
await this.runCommand([
'firmware',
'list',
...fqbnFlag,
'--format',
'json',
])
) || [];
return firmwares.reverse();
}
async updatableBoards(): Promise<string[]> {
return (await this.list()).reduce(
(a, b) => (a.includes(b.board_fqbn) ? a : [...a, b.board_fqbn]),
[] as string[]
);
}
async availableFirmwares(fqbn: string): Promise<FirmwareInfo[]> {
return await this.list(fqbn);
}
async flash(firmware: FirmwareInfo, port: string): Promise<string> {
return await this.runCommand([
'firmware',
'flash',
'--fqbn',
firmware.board_fqbn,
'--address',
port,
'--module',
`${firmware.module}@${firmware.firmware_version}`,
]);
}
}

View File

@ -1,5 +1,10 @@
import { ContainerModule } from 'inversify'; import { ContainerModule } from 'inversify';
import { ArduinoDaemonImpl } from './arduino-daemon-impl'; import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import {
ArduinoFirmwareUploader,
ArduinoFirmwareUploaderPath,
} from '../common/protocol/arduino-firmware-uploader';
import { ILogger } from '@theia/core/lib/common/logger'; import { ILogger } from '@theia/core/lib/common/logger';
import { import {
BackendApplicationContribution, BackendApplicationContribution,
@ -80,6 +85,7 @@ import {
AuthenticationServiceClient, AuthenticationServiceClient,
AuthenticationServicePath, AuthenticationServicePath,
} from '../common/protocol/authentication-service'; } from '../common/protocol/authentication-service';
import { ArduinoFirmwareUploaderImpl } from './arduino-firmware-uploader-impl';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope(); bind(BackendApplication).toSelf().inSingletonScope();
@ -245,6 +251,18 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
) )
.inSingletonScope(); .inSingletonScope();
bind(ArduinoFirmwareUploaderImpl).toSelf().inSingletonScope();
bind(ArduinoFirmwareUploader).toService(ArduinoFirmwareUploaderImpl);
bind(BackendApplicationContribution).toService(ArduinoFirmwareUploaderImpl);
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(ArduinoFirmwareUploaderPath, () =>
context.container.get(ArduinoFirmwareUploader)
)
)
.inSingletonScope();
// Logger for the Arduino daemon // Logger for the Arduino daemon
bind(ILogger) bind(ILogger)
.toDynamicValue((ctx) => { .toDynamicValue((ctx) => {
@ -254,6 +272,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope() .inSingletonScope()
.whenTargetNamed('daemon'); .whenTargetNamed('daemon');
// Logger for the Arduino daemon
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('fwuploader');
})
.inSingletonScope()
.whenTargetNamed('fwuploader');
// Logger for the "serial discovery". // Logger for the "serial discovery".
bind(ILogger) bind(ILogger)
.toDynamicValue((ctx) => { .toDynamicValue((ctx) => {

View File

@ -1989,6 +1989,11 @@
"@phosphor/signaling" "^1.3.1" "@phosphor/signaling" "^1.3.1"
"@phosphor/virtualdom" "^1.2.0" "@phosphor/virtualdom" "^1.2.0"
"@popperjs/core@^2.8.3":
version "2.9.3"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e"
integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==
"@primer/octicons-react@^9.0.0": "@primer/octicons-react@^9.0.0":
version "9.6.0" version "9.6.0"
resolved "https://registry.yarnpkg.com/@primer/octicons-react/-/octicons-react-9.6.0.tgz#996f621cb063757a4985cd6b45e59ed00e3444bf" resolved "https://registry.yarnpkg.com/@primer/octicons-react/-/octicons-react-9.6.0.tgz#996f621cb063757a4985cd6b45e59ed00e3444bf"
@ -2628,6 +2633,13 @@
moment "2.24.0" moment "2.24.0"
valid-filename "^2.0.1" valid-filename "^2.0.1"
"@tippyjs/react@^4.2.5":
version "4.2.5"
resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.5.tgz#9b5837db93a1cac953962404df906aef1a18e80d"
integrity sha512-YBLgy+1zznBNbx4JOoOdFXWMLXjBh9hLPwRtq3s8RRdrez2l3tPBRt2m2909wZd9S1KUeKjOOYYsnitccI9I3A==
dependencies:
tippy.js "^6.3.1"
"@types/anymatch@*": "@types/anymatch@*":
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
@ -15616,6 +15628,13 @@ timers-browserify@^2.0.4:
dependencies: dependencies:
setimmediate "^1.0.4" setimmediate "^1.0.4"
tippy.js@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.1.tgz#3788a007be7015eee0fd589a66b98fb3f8f10181"
integrity sha512-JnFncCq+rF1dTURupoJ4yPie5Cof978inW6/4S6kmWV7LL9YOSEVMifED3KdrVPEG+Z/TFH2CDNJcQEfaeuQww==
dependencies:
"@popperjs/core" "^2.8.3"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"