= props.messages;
17 | const htmlMessages = messages.map((m: MonitorMessage, i: number) => {
18 | if (m.type === MonitorType.MessageFromChip) {
19 | return (
20 | {ReactHtmlParser(converter.toHtml(m.message))}
21 | );
22 | }
23 | return (
24 |
25 | {m.message}
26 | <
27 |
28 | );
29 | });
30 | return {htmlMessages}
;
31 | }
32 |
33 | handleKeyDown(e: any) {
34 | if (e.key === "Enter") {
35 | const msg = document.getElementById("message") as HTMLInputElement;
36 | this.sendMessageToChip(msg.value);
37 | msg.value = "";
38 | }
39 | }
40 |
41 | handleClick() {
42 | const msg = document.getElementById("message") as HTMLInputElement;
43 | this.sendMessageToChip(msg.value);
44 | msg.value = "";
45 | }
46 |
47 | sendMessageToChip(message: string) {
48 | if (!message || message === "") {
49 | return;
50 | }
51 | this.props.onNewMessage(message);
52 | this.scrollToLatestMessage();
53 | }
54 |
55 | scrollToLatestMessage() {
56 | const messagesPane = document.getElementById("messagesPane");
57 | if (messagesPane) {
58 | messagesPane.scrollTop = messagesPane.scrollHeight;
59 | }
60 | }
61 |
62 | render() {
63 | const htmlMessages = this.props.messages.map((m: MonitorMessage, i: number) => {
64 | if (m.type === MonitorType.MessageFromChip) {
65 | return (
66 | {ReactHtmlParser(converter.toHtml(m.message))}
67 | );
68 | }
69 | return (
70 |
71 | {m.message}
72 | <
73 |
74 | );
75 | });
76 | return
77 |
78 |
79 | this.handleKeyDown(_e)} id="message" className="input fixed-height-2em" type="text" placeholder="Send message to the chip" />
80 |
81 |
82 | this.handleClick()}>Send
83 |
84 |
85 |
88 |
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/src/browser/esp-remote-monitor-contribution.ts:
--------------------------------------------------------------------------------
1 | import { injectable, inject } from "inversify";
2 | import { MessageService, MenuModelRegistry, MAIN_MENU_BAR } from "@theia/core";
3 | import { AbstractViewContribution } from "@theia/core/lib/browser";
4 | import { Command, CommandRegistry } from "@theia/core/lib/common/command";
5 | import { WorkspaceService } from "@theia/workspace/lib/browser";
6 | import { MonitorWidget } from "./monitor-widget";
7 |
8 | export const EspRemoteMonitorCommand: Command = {
9 | id: "EspRemoteMonitor.command",
10 | label: "Start ESP Remote Monitor",
11 | };
12 |
13 | @injectable()
14 | export class EspRemoteMonitorWidgetContribution extends AbstractViewContribution<
15 | MonitorWidget
16 | > {
17 | constructor(
18 | @inject(WorkspaceService)
19 | private readonly workspaceService: WorkspaceService,
20 | @inject(MessageService) private readonly messageService: MessageService
21 | ) {
22 | super({
23 | widgetId: MonitorWidget.ID,
24 | widgetName: MonitorWidget.LABEL,
25 | toggleCommandId: EspRemoteMonitorCommand.id,
26 | defaultWidgetOptions: {
27 | area: "bottom",
28 | },
29 | });
30 | }
31 |
32 | registerCommands(command: CommandRegistry) {
33 | command.registerCommand(EspRemoteMonitorCommand, {
34 | execute: async () => {
35 | if (!this.workspaceService.opened) {
36 | return this.messageService.error(
37 | "Open a ESP-IDF workspace folder first."
38 | );
39 | }
40 | super.openView({
41 | reveal: true,
42 | activate: false,
43 | });
44 | },
45 | });
46 | }
47 |
48 | registerMenus(menus: MenuModelRegistry) {
49 | const REMOTE = [...MAIN_MENU_BAR, "10_remote"];
50 | menus.registerSubmenu(REMOTE, "Remote");
51 | menus.registerMenuAction(REMOTE, {
52 | commandId: EspRemoteMonitorCommand.id,
53 | label: "Remote Monitor 👀",
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/src/browser/esp-remote-monitor-frontend-module.ts:
--------------------------------------------------------------------------------
1 | import { ContainerModule } from "inversify";
2 | import { MonitorWidget } from "./monitor-widget";
3 | import {
4 | bindViewContribution,
5 | FrontendApplicationContribution,
6 | WidgetFactory,
7 | } from "@theia/core/lib/browser";
8 | import { EspRemoteMonitorWidgetContribution } from "./esp-remote-monitor-contribution";
9 |
10 | import "../../src/browser/style/index.css";
11 |
12 | export default new ContainerModule((bind) => {
13 | bindViewContribution(bind, EspRemoteMonitorWidgetContribution);
14 | bind(FrontendApplicationContribution).toService(
15 | EspRemoteMonitorWidgetContribution
16 | );
17 | bind(MonitorWidget).toSelf();
18 | bind(WidgetFactory)
19 | .toDynamicValue((ctx) => ({
20 | id: MonitorWidget.ID,
21 | createWidget: () => ctx.container.get(MonitorWidget),
22 | }))
23 | .inSingletonScope();
24 | });
25 |
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/src/browser/monitor-widget.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react/index";
2 | import { inject, injectable, postConstruct } from "inversify";
3 | import { ReactWidget } from "@theia/core/lib/browser/widgets/react-widget";
4 | import { serialize } from "bson";
5 | import { MessageService } from "@theia/core";
6 | import { MessageProtocol } from "../common/message";
7 | import { MonitorType } from "../common/monitor";
8 | import { RemoteMonitor } from "./remoteMonitor";
9 | import { MonitorComponent } from "./components/monitor";
10 | export interface MonitorMessage {
11 | type: MonitorType;
12 | message: any;
13 | }
14 |
15 | @injectable()
16 | export class MonitorWidget extends ReactWidget {
17 | static readonly ID = "esp.remote.flasher.widget";
18 | static readonly LABEL = "Remote Flasher for ESP Chip";
19 |
20 | messages: Array;
21 |
22 | @inject(MessageService)
23 | protected readonly messageService!: MessageService;
24 |
25 | @postConstruct()
26 | protected async init(): Promise {
27 | this.id = MonitorWidget.ID;
28 | this.title.label = MonitorWidget.LABEL;
29 | this.title.caption = MonitorWidget.LABEL;
30 | this.title.closable = true;
31 | this.title.iconClass = "fa fa-window-maximize";
32 | this.messages = new Array();
33 | this.update();
34 | }
35 |
36 | private addNewMessage(message: MonitorMessage) {
37 | this.messages.push(message);
38 | this.update();
39 | }
40 |
41 | private handleUserMsg(message: string) {
42 | const remoteMonitor = RemoteMonitor.init();
43 | if (remoteMonitor.isMonitoring()) {
44 | const msgProtocol = new MessageProtocol("monitor");
45 | msgProtocol.add("monitor-type", MonitorType.MessageToChip);
46 | msgProtocol.add("message", message);
47 |
48 | remoteMonitor.sendMessageToChip(serialize(msgProtocol.message));
49 |
50 | this.addNewMessage({
51 | type: MonitorType.MessageToChip,
52 | message,
53 | });
54 | }
55 | }
56 |
57 | protected async onAfterAttach() {
58 | const remoteMonitor = RemoteMonitor.init();
59 | remoteMonitor.onMessageFromChip((ev) => {
60 | this.addNewMessage({
61 | type: ev.event,
62 | message: ev.data.message.toString(),
63 | });
64 | });
65 | remoteMonitor.onConnectionClosed(() => {
66 | this.messageService.warn(
67 | "Lost connection with the IDF Web IDE Desktop Companion App"
68 | );
69 | this.dispose();
70 | });
71 |
72 | try {
73 | if (!remoteMonitor.isMonitoring()) {
74 | await remoteMonitor.start();
75 | }
76 | } catch (error) {
77 | console.log(error);
78 | this.messageService.error("Error with IDF Web IDE Desktop Companion App");
79 | this.dispose();
80 | }
81 | }
82 |
83 | protected onBeforeDetach() {
84 | const remoteMonitor = RemoteMonitor.init();
85 | remoteMonitor.stop();
86 | }
87 |
88 | protected render() {
89 | return ( this.handleUserMsg(message)}
92 | />);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/src/browser/remoteMonitor.ts:
--------------------------------------------------------------------------------
1 | import { Emitter } from "@theia/core";
2 | import { serialize, deserialize } from "bson";
3 | import { MessageProtocol } from "../common/message";
4 | import {
5 | MonitorErrors,
6 | MonitorEvents,
7 | MonitorEventWithData,
8 | MonitorType,
9 | RemoteMonitorManager,
10 | } from "../common/monitor";
11 |
12 | export class RemoteMonitor implements RemoteMonitorManager {
13 | private static instance: RemoteMonitor;
14 | private clientHandler: WebSocket;
15 | private readonly address: string;
16 | private isRunning: boolean;
17 |
18 | private readonly onConnectionClosedEmitter: Emitter<
19 | MonitorEvents.ConnectionClosed
20 | > = new Emitter();
21 | readonly onConnectionClosed = this.onConnectionClosedEmitter.event;
22 |
23 | private readonly onMessageFromChipEmitter: Emitter<
24 | MonitorEventWithData
25 | > = new Emitter();
26 | readonly onMessageFromChip = this.onMessageFromChipEmitter.event;
27 |
28 | public static init(): RemoteMonitor {
29 | if (!RemoteMonitor.instance) {
30 | RemoteMonitor.instance = new RemoteMonitor("ws://localhost:3362/monitor");
31 | }
32 | return RemoteMonitor.instance;
33 | }
34 |
35 | private constructor(addr: string) {
36 | this.address = addr;
37 | }
38 |
39 | public isMonitoring(): boolean {
40 | return this.isRunning;
41 | }
42 |
43 | async start() {
44 | const messageProtocol = new MessageProtocol("monitor");
45 | messageProtocol.add("monitor-type", MonitorType.Start);
46 |
47 | return new Promise((resolve, reject) => {
48 | if (this.isRunning) {
49 | reject(new Error(MonitorErrors.AlreadyRunning));
50 | }
51 | this.isRunning = true;
52 | this.clientHandler = new WebSocket(this.address);
53 | this.clientHandler.onerror = (ev) => {
54 | this.isRunning = false;
55 | reject(ev);
56 | };
57 | this.clientHandler.onclose = (ev) => {
58 | this.isRunning = false;
59 | if (ev.reason === MonitorEvents.ConnectionClosedByUser) {
60 | console.log("monitor closed by user");
61 | return;
62 | }
63 | this.onConnectionClosedEmitter.fire(MonitorEvents.ConnectionClosed);
64 | };
65 |
66 | this.clientHandler.onmessage = (ev) => {
67 | this.getMsgFromChipHandler(ev);
68 | };
69 |
70 | this.clientHandler.onopen = () => {
71 | this.sendMessageToChip(serialize(messageProtocol.message));
72 | resolve(null);
73 | };
74 | });
75 | }
76 |
77 | async stop() {
78 | if (
79 | this.isRunning &&
80 | this.clientHandler.readyState === this.clientHandler.OPEN
81 | ) {
82 | this.clientHandler.close(3001, MonitorEvents.ConnectionClosedByUser);
83 | }
84 | }
85 |
86 | public sendMessageToChip(msg: Buffer) {
87 | if (this.clientHandler.readyState === this.clientHandler.OPEN) {
88 | this.clientHandler.send(msg);
89 | }
90 | }
91 |
92 | private async getMsgFromChipHandler(event: MessageEvent) {
93 | try {
94 | let data: any = event.data;
95 | if (data && data.arrayBuffer) {
96 | data = await data.arrayBuffer();
97 | }
98 | const message = deserialize(data);
99 | if (message && message["monitor-type"]) {
100 | switch (message["monitor-type"]) {
101 | case MonitorType.MessageFromChip:
102 | if (this.isRunning) {
103 | return this.onMessageFromChipEmitter.fire({
104 | event: MonitorType.MessageFromChip,
105 | data: message,
106 | });
107 | }
108 | this.onMessageFromChipEmitter.fire({
109 | event: MonitorType.MessageFromChip,
110 | data: message,
111 | });
112 | break;
113 | case MonitorType.MonitorError:
114 | return this.onMessageFromChipEmitter.fire({
115 | event: MonitorType.MonitorError,
116 | data: message,
117 | });
118 | default:
119 | break;
120 | }
121 | } else {
122 | return console.warn(
123 | "[Monitor 👀]: Unrecognized message received from the chip",
124 | message
125 | );
126 | }
127 | } catch (err) {
128 | console.log("[Monitor 👀]: Failed to parse the incoming message");
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/src/browser/style/index.css:
--------------------------------------------------------------------------------
1 | .input {
2 | background-color: var(--theia-input-background);
3 | border-color: var(--theia-inputOption-activeBorder);
4 | color: var(--theia-input-foreground);
5 | }
6 | .input::placeholder{
7 | color: var(--theia-input-placeholderForeground);
8 | }
9 | .input:hover {
10 | border-color: var(--theia-inputOption-activeBorder);
11 | }
12 | .input:active,
13 | .input:focus {
14 | border-color: var(--theia-inputOption-activeBorder);
15 | }
16 |
17 | .button {
18 | background-color: var(--theia-button-background);
19 | border-color: var(--theia-button-background);
20 | color: var(--theia-button-foreground);
21 | }
22 | .button:disabled {
23 | background-color: var(--theia-notifications-background);
24 | }
25 | .button:active {
26 | box-shadow: none;
27 | }
28 | .button:hover:disabled {
29 | color: var(--theia-button-foreground);
30 | }
31 | .button:hover:enabled,
32 | button > span.icon:hover,
33 | button > span.icon > i:hover {
34 | background-color: var(--theia-button-hoverBackground);
35 | border-color: var(--theia-button-background);
36 | color: var(--theia-button-foreground);
37 | }
38 | .button:focus, .button.is-focused {
39 | border-color: var(--theia-button-background);
40 | color: var(--theia-button-foreground);
41 | }
42 |
43 | .notification {
44 | color: var(--theia-foreground);
45 | background-color: var(--theia-menu-background);
46 | }
47 |
48 |
49 | .fixed-height-2em {
50 | height: 2em;
51 | }
52 |
53 | .fixed-height-100-per-minus-4em{
54 | height: calc(100% - 4em);
55 | }
56 |
57 | .background-transparent {
58 | background-color: transparent;
59 | }
60 |
61 | .is-scrollable {
62 | overflow: auto;
63 | }
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/src/common/message.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 |
3 | export class MessageProtocol {
4 | private _message: { [key: string]: string };
5 |
6 | constructor(messageType: string) {
7 | this._message = {};
8 | this.addMandatoryFields(messageType);
9 | }
10 |
11 | public get message(): { [key: string]: string } {
12 | return this._message;
13 | }
14 |
15 | public add(key: string, value: any) {
16 | this._message[key] = value;
17 | }
18 |
19 | private addMandatoryFields(messageType: string) {
20 | this._message.version = "0.0.1";
21 | this._message.messageType = messageType;
22 | this._message._uuid = this.genUUID();
23 | }
24 |
25 | private genUUID(): string {
26 | return uuidv4();
27 | }
28 | }
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/src/common/monitor.ts:
--------------------------------------------------------------------------------
1 | export enum MonitorType {
2 | Start = "start",
3 | Stop = "stop",
4 | MessageFromChip = "message-from-chip",
5 | MessageToChip = "message-to-chip",
6 | MonitorError = "monitor-error",
7 | }
8 |
9 | export enum MonitorEvents {
10 | ConnectionClosed = "connection-closed",
11 | ConnectionClosedByUser = "connection-closed-by-user",
12 | MonitorError = "monitor-error",
13 | MessageFromChip = "message-from-chip",
14 | MessageFromChipWithoutListener = "message-from-chip-without-listener",
15 | }
16 |
17 | export interface MonitorEventWithData {
18 | event: MonitorType;
19 | data: any;
20 | }
21 |
22 | export enum MonitorErrors {
23 | AlreadyRunning = "monitor-already-running",
24 | }
25 |
26 | export interface RemoteMonitorManager {
27 | start(): any;
28 | stop(): any;
29 | }
--------------------------------------------------------------------------------
/extensions/esp-remote-monitor/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../base.tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src"
9 | ]
10 | }
--------------------------------------------------------------------------------
/extensions/esp-remote-welcome-page/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "esp-remote-welcome-page",
3 | "keywords": [
4 | "theia-extension"
5 | ],
6 | "version": "0.0.1",
7 | "files": [
8 | "lib",
9 | "src"
10 | ],
11 | "devDependencies": {
12 | "rimraf": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "prepare": "yarn run clean && yarn run build",
17 | "clean": "rimraf lib",
18 | "build": "tsc",
19 | "watch": "tsc -w"
20 | },
21 | "theiaExtensions": [
22 | {
23 | "frontend": "lib/browser/esp-remote-welcome-page-frontend-module"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/extensions/esp-remote-welcome-page/src/browser/esp-remote-welcome-page-contribution.ts:
--------------------------------------------------------------------------------
1 | import { injectable, inject, postConstruct } from "inversify";
2 | import { WorkspaceService } from "@theia/workspace/lib/browser";
3 | import { PreviewUri } from "@theia/preview/lib/browser";
4 | import { FrontendApplicationContribution, OpenerService, open } from "@theia/core/lib/browser";
5 | import URI from "@theia/core/lib/common/uri";
6 | const WELCOME_PAGE_STORAGE_SERVICE_KEY = "esp_idf.welcomePageDisplayed"
7 | @injectable()
8 | export class ESPWelcomePageFrontendApplicationContribution implements FrontendApplicationContribution {
9 | private storageService: Storage | undefined;
10 | constructor(
11 | @inject(WorkspaceService) private readonly workspaceService: WorkspaceService,
12 | @inject(OpenerService) private readonly openerService: OpenerService,
13 | ) { }
14 | @postConstruct()
15 | init() {
16 | this.storageService = (window && window.localStorage) ? window.localStorage : undefined;
17 | }
18 | async onDidInitializeLayout() {
19 | if (this.workspaceService.opened) {
20 | if (this.storageService?.getItem(WELCOME_PAGE_STORAGE_SERVICE_KEY) === null) {
21 | const uri1 = new URI("/home/idf-web-ide//WELCOME.md");
22 | const uri2 = PreviewUri.encode(uri1);
23 | this.storageService?.setItem(WELCOME_PAGE_STORAGE_SERVICE_KEY, "true");
24 | open(this.openerService, uri2, { preview: true });
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/extensions/esp-remote-welcome-page/src/browser/esp-remote-welcome-page-frontend-module.ts:
--------------------------------------------------------------------------------
1 | import { ESPWelcomePageFrontendApplicationContribution } from './esp-remote-welcome-page-contribution';
2 | import { FrontendApplicationContribution } from "@theia/core/lib/browser";
3 |
4 | import { ContainerModule } from "inversify";
5 |
6 | export default new ContainerModule(bind => {
7 | bind(FrontendApplicationContribution).to(ESPWelcomePageFrontendApplicationContribution);
8 | });
--------------------------------------------------------------------------------
/extensions/esp-remote-welcome-page/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../base.tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src"
9 | ]
10 | }
--------------------------------------------------------------------------------
/extensions/esp-webserial/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "esp-webserial",
3 | "keywords": [
4 | "theia-extension"
5 | ],
6 | "version": "0.0.1",
7 | "files": [
8 | "lib",
9 | "src"
10 | ],
11 | "dependencies": {
12 | "@theia/core": "latest",
13 | "esptool-js": "^0.3.1",
14 | "crypto-js": "^4.1.1"
15 | },
16 | "devDependencies": {
17 | "@types/w3c-web-serial": "^1.0.3",
18 | "@types/crypto-js": "^4.1.1",
19 | "rimraf": "latest",
20 | "typescript": "latest"
21 | },
22 | "scripts": {
23 | "prepare": "yarn run clean && yarn run build",
24 | "clean": "rimraf lib",
25 | "build": "tsc",
26 | "watch": "tsc -w"
27 | },
28 | "theiaExtensions": [
29 | {
30 | "frontend": "lib/browser/esp-webserial-frontend-module",
31 | "backend": "lib/node/esp-webserial-backend-module"
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/extensions/esp-webserial/src/browser/esp-webserial-frontend-contribution.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Command,
3 | CommandContribution,
4 | CommandRegistry,
5 | ILogger,
6 | MAIN_MENU_BAR,
7 | MenuContribution,
8 | MenuModelRegistry,
9 | QuickInputService,
10 | } from "@theia/core/lib/common";
11 | import { MessageService } from "@theia/core/lib/common/message-service";
12 | import { WorkspaceService } from "@theia/workspace/lib/browser";
13 | import { inject, injectable } from "inversify";
14 | import {
15 | IEspLoaderTerminal,
16 | ESPLoader,
17 | FlashOptions,
18 | Transport,
19 | LoaderOptions,
20 | } from "esptool-js";
21 | import { OutputChannelManager } from "@theia/output/lib/browser/output-channel";
22 | import { TerminalWidget } from "@theia/terminal/lib/browser/base/terminal-widget";
23 | import { TerminalService } from "@theia/terminal/lib/browser/base/terminal-service";
24 | import { EspWebSerialBackendService, PartitionInfo } from "../common/protocol";
25 | import { enc, MD5 } from "crypto-js";
26 |
27 | const EspWebSerialDisconnectCommand: Command = {
28 | id: "EspWebSerial",
29 | label: "Disconnect device to Esptool",
30 | };
31 |
32 | const EspWebSerialFlashCommand: Command = {
33 | id: "EspWebSerialFlash",
34 | label: "Flash with WebSerial ⚡️",
35 | };
36 |
37 | const EspWebSerialMonitorCommand: Command = {
38 | id: "EspWebSerialMonitor",
39 | label: "Monitor with WebSerial ⚡️",
40 | };
41 |
42 | @injectable()
43 | export class EspWebSerialCommandContribution implements CommandContribution {
44 | constructor(
45 | @inject(EspWebSerialBackendService)
46 | protected readonly espWebSerialBackendService: EspWebSerialBackendService,
47 | @inject(ILogger) protected readonly logger: ILogger,
48 | @inject(MessageService) private readonly messageService: MessageService,
49 | @inject(QuickInputService)
50 | private readonly quickInputService: QuickInputService,
51 | @inject(WorkspaceService)
52 | protected readonly workspaceService: WorkspaceService,
53 | @inject(OutputChannelManager)
54 | protected readonly outputChannelManager: OutputChannelManager,
55 | @inject(TerminalService)
56 | protected readonly terminalService: TerminalService
57 | ) {}
58 |
59 | chip: string;
60 | port: SerialPort | undefined;
61 | connected = false;
62 | isConsoleClosed = false;
63 | transport: Transport | undefined;
64 | esploader: ESPLoader;
65 | terminal: TerminalWidget;
66 |
67 | registerCommands(registry: CommandRegistry): void {
68 | registry.registerCommand(EspWebSerialDisconnectCommand, {
69 | execute: async () => {
70 | if (this.transport) {
71 | await this.transport.disconnect();
72 | await this.transport.waitForUnlock(1000);
73 | this.transport = undefined;
74 | }
75 | if (this.port) {
76 | await this.port?.close();
77 | this.port = undefined;
78 | }
79 | if (this.terminal) {
80 | this.terminal.dispose();
81 | }
82 | },
83 | });
84 |
85 | registry.registerCommand(EspWebSerialFlashCommand, {
86 | execute: async () => {
87 | if (this.workspaceService.opened) {
88 | if (!navigator.serial) {
89 | return undefined;
90 | }
91 | this.port = await navigator.serial.requestPort();
92 | if (!this.port) {
93 | return undefined;
94 | }
95 | this.transport = new Transport(this.port);
96 |
97 | const workspaceStat = this.workspaceService.tryGetRoots();
98 | const progress = await this.messageService.showProgress({
99 | text: "Flashing with WebSerial...",
100 | });
101 | progress.report({
102 | message: "Getting binaries from project...",
103 | work: { done: 10, total: 100 },
104 | });
105 | try {
106 | const items = [
107 | { label: "921600" },
108 | { label: "460800" },
109 | { label: "230400" },
110 | { label: "115200" },
111 | ];
112 | const selectedBaudRate =
113 | await this.quickInputService?.showQuickPick(items, {
114 | placeholder: "Choose connection baudrate",
115 | });
116 | const baudRate = selectedBaudRate
117 | ? parseInt(selectedBaudRate.label)
118 | : 921600;
119 | const outputChnl =
120 | this.outputChannelManager.getChannel("WebSerial Flash");
121 | outputChnl.show({ preserveFocus: true });
122 | const clean = () => {
123 | outputChnl.clear();
124 | };
125 | const writeLine = (data: string) => {
126 | outputChnl.appendLine(data);
127 | };
128 | const write = (data: string) => {
129 | outputChnl.append(data);
130 | };
131 |
132 | const loaderTerminal: IEspLoaderTerminal = {
133 | clean,
134 | write,
135 | writeLine,
136 | };
137 | const loaderOptions = {
138 | transport: this.transport,
139 | baudrate: baudRate,
140 | terminal: loaderTerminal,
141 | } as LoaderOptions;
142 | this.esploader = new ESPLoader(loaderOptions);
143 | this.connected = true;
144 | this.chip = await this.esploader.main_fn();
145 | const msgProtocol =
146 | await this.espWebSerialBackendService.getFlashSectionsForCurrentWorkspace(
147 | workspaceStat[0].resource.toString()
148 | );
149 | const fileArray = msgProtocol._message.sections as PartitionInfo[];
150 | fileArray.forEach((element) => {
151 | outputChnl.appendLine(
152 | `File ${element.name} Address: ${element.address} HASH: ${MD5(
153 | enc.Latin1.parse(element.data)
154 | ).toString()}`
155 | );
156 | });
157 | const flashSize = msgProtocol._message.flash_size;
158 | const flashMode = msgProtocol._message.flash_mode;
159 | const flashFreq = msgProtocol._message.flash_freq;
160 |
161 | progress.report({
162 | message: `Flashing device (size: ${flashSize} mode: ${flashMode} frequency: ${flashFreq})...`,
163 | });
164 | const flashOptions = {
165 | fileArray,
166 | flashSize: "keep",
167 | flashMode: "keep",
168 | flashFreq: "keep",
169 | eraseAll: false,
170 | compress: true,
171 | reportProgress: (
172 | fileIndex: number,
173 | written: number,
174 | total: number
175 | ) => {
176 | progress.report({
177 | message: `${fileArray[fileIndex].name}.bin: (${written}/${total})`,
178 | });
179 | outputChnl.appendLine(`Image ${fileArray[fileIndex].name}.bin: (${written}/${total})`);
180 | },
181 | calculateMD5Hash: (image: string) =>
182 | MD5(enc.Latin1.parse(image)).toString(),
183 | } as FlashOptions;
184 | await this.esploader.write_flash(flashOptions);
185 | progress.cancel();
186 | this.messageService.info("Done flashing");
187 | await this.transport.disconnect();
188 | await this.transport.waitForUnlock(1000);
189 | this.transport = undefined;
190 | this.port = undefined;
191 | } catch (error) {
192 | progress.cancel();
193 | const errMsg =
194 | error && error.message
195 | ? error.message
196 | : typeof error === "string"
197 | ? error
198 | : "Something went wrong";
199 | console.log(error);
200 | this.messageService.error(errMsg);
201 | }
202 | } else {
203 | this.messageService.info("Open a workspace first.");
204 | }
205 | },
206 | });
207 |
208 | registry.registerCommand(EspWebSerialMonitorCommand, {
209 | execute: async () => {
210 | if (this.transport === undefined) {
211 | if (!navigator.serial) {
212 | return undefined;
213 | }
214 | this.port = await navigator.serial.requestPort();
215 | if (!this.port) {
216 | return undefined;
217 | }
218 | this.transport = new Transport(this.port);
219 | await this.transport.connect();
220 | }
221 |
222 | try {
223 | this.terminal = await this.terminalService.newTerminal({
224 | id: "webserial-monitor",
225 | title: "Monitor with WebSerial",
226 | });
227 | await this.terminal.start();
228 | this.terminalService.open(this.terminal);
229 | this.isConsoleClosed = false;
230 | this.terminal.onDidDispose(async () => {
231 | this.isConsoleClosed = true;
232 | await this.transport?.disconnect();
233 | this.transport = undefined;
234 | this.port = undefined;
235 | });
236 |
237 | this.terminal.onKey(async (keyEvent) => {
238 | console.log(
239 | `terminal 'onKey' event: { key: '${keyEvent.key}', code: ${keyEvent.domEvent.code} }`
240 | );
241 | if (
242 | keyEvent.domEvent.code === "KeyC" ||
243 | keyEvent.domEvent.code === "BracketRight"
244 | ) {
245 | this.terminal.dispose();
246 | }
247 | });
248 |
249 | while (true && !this.isConsoleClosed) {
250 | let val = await this.transport.rawRead();
251 | if (typeof val !== "undefined") {
252 | let valStr = Buffer.from(val.buffer).toString();
253 | this.terminal.write(valStr);
254 | } else {
255 | break;
256 | }
257 | }
258 | await this.transport.waitForUnlock(1500);
259 | } catch (error) {
260 | const err = error && error.message ? error.message : error;
261 | console.log(error);
262 | this.messageService.error(err);
263 | }
264 | },
265 | });
266 | }
267 | }
268 |
269 | @injectable()
270 | export class EspWebSerialMenuContribution implements MenuContribution {
271 | registerMenus(menus: MenuModelRegistry): void {
272 | const REMOTE = [...MAIN_MENU_BAR, "10_remote"];
273 | menus.registerSubmenu(REMOTE, "Remote");
274 | menus.registerMenuAction(REMOTE, {
275 | commandId: EspWebSerialDisconnectCommand.id,
276 | label: EspWebSerialDisconnectCommand.label,
277 | });
278 | menus.registerMenuAction(REMOTE, {
279 | commandId: EspWebSerialFlashCommand.id,
280 | label: EspWebSerialFlashCommand.label,
281 | });
282 | menus.registerMenuAction(REMOTE, {
283 | commandId: EspWebSerialMonitorCommand.id,
284 | label: EspWebSerialMonitorCommand.label,
285 | });
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/extensions/esp-webserial/src/browser/esp-webserial-frontend-module.ts:
--------------------------------------------------------------------------------
1 | import { CommandContribution, MenuContribution } from "@theia/core";
2 | import { ContainerModule } from "inversify";
3 | import { WebSocketConnectionProvider } from "@theia/core/lib/browser";
4 | import {
5 | EspWebSerialCommandContribution,
6 | EspWebSerialMenuContribution,
7 | } from "./esp-webserial-frontend-contribution";
8 | import {
9 | EspWebSerialBackendService,
10 | ESP_WEBSERIAL_FLASHER,
11 | } from "../common/protocol";
12 |
13 | export default new ContainerModule((bind) => {
14 | bind(CommandContribution)
15 | .to(EspWebSerialCommandContribution)
16 | .inSingletonScope();
17 | bind(MenuContribution).to(EspWebSerialMenuContribution);
18 |
19 | bind(EspWebSerialBackendService)
20 | .toDynamicValue((ctx) => {
21 | const connection = ctx.container.get(WebSocketConnectionProvider);
22 | return connection.createProxy(
23 | ESP_WEBSERIAL_FLASHER
24 | );
25 | })
26 | .inSingletonScope();
27 | });
28 |
--------------------------------------------------------------------------------
/extensions/esp-webserial/src/common/message.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 |
3 | export class MessageProtocol {
4 | public _message: { [key: string]: any };
5 |
6 | constructor(messageType: string) {
7 | this._message = {};
8 | this.addMandatoryFields(messageType);
9 | }
10 |
11 | public get message(): { [key: string]: string } {
12 | return this._message;
13 | }
14 |
15 | public add(key: string, value: any) {
16 | this._message[key] = value;
17 | }
18 |
19 | private addMandatoryFields(messageType: string) {
20 | this._message.version = "0.0.1";
21 | this._message.messageType = messageType;
22 | this._message._uuid = this.genUUID();
23 | }
24 |
25 | private genUUID(): string {
26 | return uuidv4();
27 | }
28 | }
--------------------------------------------------------------------------------
/extensions/esp-webserial/src/common/protocol.ts:
--------------------------------------------------------------------------------
1 | import { Event } from "@theia/core/lib/common";
2 | import { MessageProtocol } from "./message";
3 |
4 | export const EspWebSerialBackendService = Symbol("EspWebSerialBackendService");
5 | export const ESP_WEBSERIAL_FLASHER = "/services/esp-webserial";
6 |
7 | export interface EspWebSerialBackendService {
8 | getFlashSectionsForCurrentWorkspace(
9 | workspace: string
10 | ): Promise;
11 | }
12 |
13 | export const WebSerialFlasherClient = Symbol("WebSerialBackendClient");
14 |
15 | export interface WebSerialClient {
16 | connect(): Promise;
17 | flash(data: Buffer): void;
18 | onDidCloseConnection: Event;
19 | onFlashDone: Event;
20 | onFlassError: Event;
21 | setIsFlashing(v: boolean): void;
22 | }
23 |
24 | export enum FlashErrors {
25 | BuildRequiredBeforeFlash = "BUILD_REQUIRED_BEFORE_FLASH",
26 | JsonFileParseError = "JSON_FILE_PARSE_ERROR",
27 | }
28 |
29 | export enum FlashEvents {
30 | ConnectionClosed = "connection-closed",
31 | FlashDone = "flash-done",
32 | FlashError = "flash-error",
33 | }
34 |
35 | export interface PartitionInfo {
36 | name: string;
37 | data: string;
38 | address: number;
39 | }
40 |
--------------------------------------------------------------------------------
/extensions/esp-webserial/src/node/esp-webserial-backend-module.ts:
--------------------------------------------------------------------------------
1 | import { ConnectionHandler, JsonRpcConnectionHandler } from "@theia/core";
2 | import { ContainerModule } from "inversify";
3 | import {
4 | EspWebSerialBackendService,
5 | ESP_WEBSERIAL_FLASHER,
6 | } from "../common/protocol";
7 | import { EspWebSerialBackendServiceImpl } from "./esp-webserial-client";
8 |
9 | export default new ContainerModule((bind) => {
10 | bind(EspWebSerialBackendService)
11 | .to(EspWebSerialBackendServiceImpl)
12 | .inSingletonScope();
13 | bind(ConnectionHandler)
14 | .toDynamicValue(
15 | (ctx) =>
16 | new JsonRpcConnectionHandler(ESP_WEBSERIAL_FLASHER, () => {
17 | return ctx.container.get(
18 | EspWebSerialBackendService
19 | );
20 | })
21 | )
22 | .inSingletonScope();
23 | });
24 |
--------------------------------------------------------------------------------
/extensions/esp-webserial/src/node/esp-webserial-client.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from "inversify";
2 | import { pathExists, readJSON, readFile } from "fs-extra";
3 | import { join, parse } from "path";
4 | import { EspWebSerialBackendService, PartitionInfo } from "../common/protocol";
5 | import { MessageProtocol } from "../common/message";
6 |
7 | @injectable()
8 | export class EspWebSerialBackendServiceImpl
9 | implements EspWebSerialBackendService
10 | {
11 | private readonly flashInfoFileName: string = "flasher_args.json";
12 |
13 | getFlashSectionsForCurrentWorkspace(workspace: string) {
14 | return new Promise(async (resolve, reject) => {
15 | const workspacePath = workspace.replace("file://", "");
16 | const flashInfoFileName = join(
17 | workspacePath,
18 | "build",
19 | this.flashInfoFileName
20 | );
21 | const isBuilt = await pathExists(flashInfoFileName);
22 | if (!isBuilt) {
23 | return reject("Build before flashing");
24 | }
25 | const flashFileJson = await readJSON(flashInfoFileName);
26 | const binPromises: Promise[] = [];
27 | Object.keys(flashFileJson["flash_files"]).forEach((offset) => {
28 | const fileName = parse(flashFileJson["flash_files"][offset]).name;
29 | const filePath = join(
30 | workspacePath,
31 | "build",
32 | flashFileJson["flash_files"][offset]
33 | );
34 | binPromises.push(this.readFileIntoBuffer(filePath, fileName, offset));
35 | });
36 | const binaries = await Promise.all(binPromises);
37 | const message = new MessageProtocol("flash");
38 | message.add("sections", binaries);
39 | message.add("flash_size", flashFileJson["flash_settings"]["flash_size"]);
40 | message.add("flash_mode", flashFileJson["flash_settings"]["flash_mode"]);
41 | message.add("flash_freq", flashFileJson["flash_settings"]["flash_freq"]);
42 | return resolve(message);
43 | });
44 | }
45 |
46 | private async readFileIntoBuffer(
47 | filePath: string,
48 | name: string,
49 | offset: string
50 | ) {
51 | const fileData = await readFile(filePath, "binary");
52 | return {
53 | data: fileData,
54 | name,
55 | address: parseInt(offset),
56 | } as PartitionInfo;
57 | }
58 |
59 | dispose(): void {}
60 | }
61 |
--------------------------------------------------------------------------------
/extensions/esp-webserial/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../base.tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "rootDir": "src",
6 | "outDir": "lib"
7 | },
8 | "include": ["src"]
9 | }
10 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "lerna": "2.4.0",
3 | "version": "0.0.0",
4 | "useWorkspaces": true,
5 | "npmClient": "yarn",
6 | "command": {
7 | "run": {
8 | "stream": true
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "prepare": "lerna run prepare",
5 | "rebuild:browser": "theia rebuild:browser",
6 | "rebuild:electron": "theia rebuild:electron",
7 | "start:browser": "yarn rebuild:browser && yarn --cwd browser-app start:debug --hostname=127.0.0.1 --port=8080",
8 | "start:electron": "yarn rebuild:electron && yarn --cwd electron-app start",
9 | "watch": "lerna run --parallel watch"
10 | },
11 | "devDependencies": {
12 | "lerna": "2.4.0"
13 | },
14 | "workspaces": [
15 | "extensions/*",
16 | "browser-app"
17 | ],
18 | "version": "0.0.2"
19 | }
20 |
--------------------------------------------------------------------------------