Skip to content

Commit c98444b

Browse files
committed
Merge branch 'main' of https://github.com/circuitpython/web-editor into beta
2 parents 99ef261 + 661e802 commit c98444b

11 files changed

Lines changed: 169 additions & 53 deletions

File tree

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v5
13-
- uses: actions/setup-node@v4
13+
- uses: actions/setup-node@v5
1414
with:
1515
node-version: 22
1616
cache: 'npm'

.github/workflows/publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
runs-on: ubuntu-latest
2929
steps:
3030
- uses: actions/checkout@v5
31-
- uses: actions/setup-node@v4
31+
- uses: actions/setup-node@v5
3232
with:
3333
node-version: 22
3434
cache: 'npm'

index.html

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,23 @@ <h1>Web Bluetooth not available!</h1>
221221
<div class="step-number"></div>
222222
<div class="step-content">
223223
<h1>Request Bluetooth Device</h1>
224-
<p>CircuitPython boards with <a href="https://circuitpython.org/downloads?features=Bluetooth%2FBTLE">nrf chips need
225-
CircuitPython 7.0.0 or newer</a>. The first time a device is connected to your host,
226-
you'll need to enable public broadcasting by pressing reset when the faster, blue blink
227-
is happening on start up. The device will reset and the second, blue blink will be solid
228-
when done successfully.</p>
229-
<p>
224+
<p> See the <a href="https://learn.adafruit.com/wirelessly-code-your-bluetooth-device-with-circuitpython/device-setup">online documentation</a>
225+
for platform specific notes on how to use Bluetooth. Note that CircuitPython boards with <a href="https://circuitpython.org/downloads?features=Bluetooth%2FBTLE">nrf chips need
226+
CircuitPython 7.0.0 or newer</a>.
227+
</p>
228+
<p> The first time a device is connected to your host, you'll need to enable
229+
public broadcasting by pressing reset (or bootsel on some devices) when the faster, blue blink
230+
is happening on start up. The device will reset and the second, blue blink will be solid
231+
when done successfully.</p>
230232
<button class="purple-button" id="requestBluetoothDevice">Request Bluetooth Device</button>
231233
</p>
232234
</div>
233235
</section>
236+
<section>
237+
<div class="connect-status" hidden>
238+
<div class="connect-status-content"></div>
239+
</div>
240+
</section>
234241
</div>
235242
</div>
236243
<div class="popup-modal shadow connect-dialog closable" data-popup-modal="web-connect">
@@ -262,6 +269,14 @@ <h1>Navigate to your Device</h1>
262269
<p>Once your device is connected to your Local Area Network, you can navigate to
263270
<a id="device-link" href="http://circuitpython.local/code/">http://circuitpython.local/code/</a>. This opens
264271
a page on your device that loads this website onto the device and to avoid any cross domain security issues.</p>
272+
<p> If your device doesn't support the .local domain or the connection times out, connect with serial to read the
273+
IP address assigned to your device, then open http://device-ip-address:80/code/ from another tab in your browser</p>
274+
</p>
275+
</div>
276+
</section>
277+
<section>
278+
<div class="connect-status" hidden>
279+
<div class="connect-status-content"></div>
265280
</div>
266281
</section>
267282
</div>
@@ -296,11 +311,16 @@ <h1>Select Serial Device</h1>
296311
<h1>Select USB Host Folder</h1>
297312
<p>Select the root folder of your device. This is typically the CIRCUITPY Drive on your computer unless you renamed it. If your device does not appear as a drive on your computer, it will need to have the USB Host functionality enabled.</p>
298313
<p>
299-
<button class="purple-button hidden" id="useHostFolder">Use <span id="workingFolder"></span></button>
314+
<button class="purple-button hidden" id="useHostFolder"><span id="workingFolder"></span></button>
300315
<button class="purple-button first-item" id="selectHostFolder">Select New Folder</button>
301316
</p>
302317
</div>
303318
</section>
319+
<section>
320+
<div class="connect-status" hidden>
321+
<div class="connect-status-content"></div>
322+
</div>
323+
</section>
304324
</div>
305325
</div>
306326
<div class="popup-modal shadow closable" data-popup-modal="device-discovery">

js/common/plotter.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ export function plotValues(chartObj, serialMessage, bufferSize) {
4848

4949
// handle possible tuple in textLine
5050
if (textLine.startsWith("(") && textLine.endsWith(")")) {
51-
textLine = "[" + textLine.substring(1, textLine.length - 1) + "]";
51+
textValues = textLine.substring(1, textLine.length - 1).trim();
52+
// Python tuples can end with a comma, but JS arrays cannot
53+
if (textValues.endsWith(",")) {
54+
textValues = textValues.substring(0, textValues.length - 1);
55+
}
56+
textLine = "[" + textValues + "]";
5257
console.log("after tuple conversion: " + textLine);
5358
}
5459

js/common/utilities.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@ function isLocal() {
5656
return (isMdns() || location.hostname == "localhost" || isIp()) && (location.pathname == "/code/");
5757
}
5858

59+
// Test to see if browser is running on Microsoft Windows OS
60+
function isMicrosoftWindows() {
61+
// Newer test on Chromium
62+
if (navigator.userAgentData?.platform === "Windows") {
63+
return true;
64+
} else if (navigator.userAgent.includes("Windows")) {
65+
return true;
66+
}
67+
return false;
68+
}
69+
70+
// Test to see if browser is running on Microsoft Windows OS
71+
function isChromeOs() {
72+
if (navigator.userAgent.includes("CrOS")) {
73+
return true;
74+
}
75+
return false;
76+
}
77+
5978
// Parse out the url parameters from the current url
6079
function getUrlParams() {
6180
// This should look for and validate very specific values
@@ -146,6 +165,8 @@ export {
146165
isMdns,
147166
isIp,
148167
isLocal,
168+
isMicrosoftWindows,
169+
isChromeOs,
149170
getUrlParams,
150171
getUrlParam,
151172
timeout,

js/workflows/ble.js

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,13 @@ class BLEWorkflow extends Workflow {
7171
stepOne.classList.add("hidden");
7272
}
7373
try {
74+
this.clearConnectStatus();
7475
const devices = await bluetooth.getDevices();
7576
console.log(devices);
7677
this.connectionStep(devices.length > 0 ? 2 : 1);
77-
} catch (e) {
78-
console.log("New Permissions backend for Web Bluetooth not enabled. Go to chrome://flags/#enable-web-bluetooth-new-permissions-backend to enable.", e);
78+
} catch (error) {
79+
console.error(error);
80+
this.showConnectStatus(this._suggestBLEConnectActions(error));
7981
}
8082
} else {
8183
modal.querySelectorAll('.step:not(:first-of-type)').forEach((stepItem) => {
@@ -128,7 +130,7 @@ class BLEWorkflow extends Workflow {
128130
}
129131
catch (error) {
130132
console.error(error);
131-
await this._showMessage(error);
133+
this.showConnectStatus(this._suggestBLEConnectActions(error));
132134
}
133135
}
134136
}
@@ -152,7 +154,9 @@ class BLEWorkflow extends Workflow {
152154
try {
153155
this.bleServer = await device.gatt.connect();
154156
} catch (error) {
155-
await this._showMessage("Failed to connect to device. Try forgetting device from OS bluetooth devices and try again.");
157+
console.log(error);
158+
// TODO(ericzundel): Add to suggestBLEConnectAction if we can determine the exception type
159+
this.showConnectStatus("Failed to connect to device. Try forgetting device from OS bluetooth devices and try again.");
156160
// Disable the reconnect button
157161
this.connectionStep(1);
158162
}
@@ -169,31 +173,25 @@ class BLEWorkflow extends Workflow {
169173

170174
this.debugLog("connecting to " + device.name);
171175
try {
176+
this.clearConnectStatus();
172177
console.log('Watching advertisements from "' + device.name + '"...');
173178
console.log('If no advertisements are received, make sure the device is powered on and in range. You can also try resetting the device.');
174179
await device.watchAdvertisements({signal: abortController.signal});
175180
}
176181
catch (error) {
177182
console.error(error);
178-
await this._showMessage(error);
183+
this.showConnectStatus(this._suggestBLEConnectActions(error));
179184
}
180185
}
181186

182187
// Request Bluetooth Device
183188
async onRequestBluetoothDeviceButtonClick(e) {
184-
//try {
185-
console.log('Requesting any Bluetooth device...');
186-
this.debugLog("Requesting device. Cancel if empty and try existing");
187-
let device = await this.requestDevice();
189+
console.log('Requesting any Bluetooth device...');
190+
this.debugLog("Requesting device. Cancel if empty and try existing");
191+
let device = await this.requestDevice();
188192

189-
console.log('> Requested ' + device.name);
190-
await this.connectToBluetoothDevice(device);
191-
/*}
192-
catch (error) {
193-
console.error(error);
194-
await this._showMessage(error);
195-
this.debugLog('No device selected. Try to connect to existing.');
196-
}*/
193+
console.log('> Requested ' + device.name);
194+
await this.connectToBluetoothDevice(device);
197195
}
198196

199197
async switchToDevice(device) {
@@ -279,6 +277,16 @@ class BLEWorkflow extends Workflow {
279277
async showInfo(documentState) {
280278
return await this.infoDialog.open(this, documentState);
281279
}
280+
281+
// Analyze an exception and make user friendly suggestions
282+
_suggestBLEConnectActions(error) {
283+
if (error.name == "TypeError" &&
284+
(error.message.includes("getDevices is not a function")
285+
|| error.message.includes("watchAdvertisements is not a function"))) {
286+
return "Bluetooth API not available. Make sure you are loading from a secure context (HTTPS), then go to chrome://flags/#enable-web-bluetooth-new-permissions-backend to enable.";
287+
}
288+
return `Connect via Bluetooth returned error: ${error}`;
289+
}
282290
}
283291

284292
export {BLEWorkflow};

js/workflows/usb.js

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {GenericModal, DeviceInfoModal} from '../common/dialogs.js';
44
import {FileOps} from '@adafruit/circuitpython-repl-js'; // Use this to determine which FileTransferClient to load
55
import {FileTransferClient as ReplFileTransferClient} from '../common/repl-file-transfer.js';
66
import {FileTransferClient as FSAPIFileTransferClient} from '../common/fsapi-file-transfer.js';
7+
import { isChromeOs, isMicrosoftWindows } from '../common/utilities.js';
78

89
let btnRequestSerialDevice, btnSelectHostFolder, btnUseHostFolder, lblWorkingfolder;
910

@@ -100,6 +101,7 @@ class USBWorkflow extends Workflow {
100101
// the device on the stored port is currently connected by checking if the
101102
// readable and writable properties are null.
102103

104+
// Can throw a Security Error if permissions are not granted
103105
let allDevices = await navigator.serial.getPorts();
104106
let connectedDevices = [];
105107
for (let device of allDevices) {
@@ -112,7 +114,8 @@ class USBWorkflow extends Workflow {
112114

113115
if (connectedDevices.length == 1) {
114116
device = connectedDevices[0];
115-
console.log(await device.getInfo());
117+
deviceInfo = await device.getInfo()
118+
console.log(`Got previously connected device: ${deviceInfo}`);
116119
try {
117120
// Attempt to connect to the saved device. If it's not found, this will fail.
118121
await this._switchToDevice(device);
@@ -121,37 +124,35 @@ class USBWorkflow extends Workflow {
121124
await device.forget();
122125

123126
console.log("Failed to automatically connect to saved device. Prompting user to select a device.");
127+
// If the user doesn't select a port, an exception is thrown
124128
device = await navigator.serial.requestPort();
125-
console.log(device);
126129
}
127130
} else {
128-
console.log('Requesting any serial device...');
129-
try {
130-
device = await navigator.serial.requestPort();
131-
} catch (e) {
132-
console.log(e);
133-
return false;
134-
}
131+
console.log('No previously connected device. Prompting user to select a device.');
132+
// If the user doesn't select a port, an exception is thrown
133+
device = await navigator.serial.requestPort();
135134
}
135+
console.log(`Selected device: ${device}`);
136+
136137

137138
// If we didn't automatically use a saved device
138139
if (!this._serialDevice) {
139140
console.log('> Requested ', device);
140141
await this._switchToDevice(device);
141142
}
142-
console.log(this._serialDevice);
143+
143144
if (this._serialDevice != null) {
145+
console.log(`Current serial device is: ${this._serialDevice}. Proceeding to step 2.`);
144146
this.connectionStep(2);
145147
return true;
146148
}
147-
149+
console.log("Couldn't connect to serial port");
148150
return false;
149151
}
150152

151153
async showConnect(documentState) {
152154
let p = this.connectDialog.open();
153155
let modal = this.connectDialog.getModal();
154-
155156
btnRequestSerialDevice = modal.querySelector('#requestSerialDevice');
156157
btnSelectHostFolder = modal.querySelector('#selectHostFolder');
157158
btnUseHostFolder = modal.querySelector('#useHostFolder');
@@ -165,22 +166,27 @@ class USBWorkflow extends Workflow {
165166

166167
btnRequestSerialDevice.disabled = true;
167168
btnSelectHostFolder.disabled = true;
169+
this.clearConnectStatus();
168170
let serialConnect = async (event) => {
169171
try {
172+
this.clearConnectStatus();
170173
await this.connectToSerial();
171174
} catch (e) {
172-
//console.log(e);
173-
//alert(e.message);
174-
//alert("Unable to connect to device. Make sure it is not already in use.");
175-
// TODO: I think this also occurs if the user cancels the requestPort dialog
175+
console.log('connectToSerial() returned error: ', e);
176+
this.showConnectStatus(this._suggestSerialConnectActions(e));
176177
}
177178
};
178179
btnRequestSerialDevice.removeEventListener('click', serialConnect);
179180
btnRequestSerialDevice.addEventListener('click', serialConnect);
180181

181182
btnSelectHostFolder.removeEventListener('click', this._btnSelectHostFolderCallback)
182183
this._btnSelectHostFolderCallback = async (event) => {
183-
await this._selectHostFolder();
184+
try {
185+
this.clearConnectStatus();
186+
await this._selectHostFolder();
187+
} catch (e) {
188+
this.showConnectStatus(this._suggestFileConnectActions(e));
189+
}
184190
};
185191
btnSelectHostFolder.addEventListener('click', this._btnSelectHostFolderCallback);
186192

@@ -247,7 +253,11 @@ class USBWorkflow extends Workflow {
247253
console.log("New folder name:", folderName);
248254
if (folderName) {
249255
// Set the working folder label
250-
lblWorkingfolder.innerHTML = folderName;
256+
if (isMicrosoftWindows() || isChromeOs()) {
257+
lblWorkingfolder.innerHTML = "OK";
258+
} else {
259+
lblWorkingfolder.innerHTML = `Use ${folderName}`;
260+
}
251261
btnUseHostFolder.classList.remove("hidden");
252262
btnSelectHostFolder.innerHTML = "Select Different Folder";
253263
btnSelectHostFolder.classList.add("inverted");
@@ -269,9 +279,8 @@ class USBWorkflow extends Workflow {
269279

270280
this._serialDevice = device;
271281
console.log("switch to", this._serialDevice);
272-
await this._serialDevice.open({baudRate: 115200}); // TODO: Will fail if something else is already connected or it isn't found.
273-
274-
// Start the read loop
282+
await this._serialDevice.open({baudRate: 115200}); // Throws if something else is already connected or it isn't found.
283+
console.log("Starting Read Loop");
275284
this._readLoopPromise = this._readSerialLoop().catch(
276285
async function(error) {
277286
await this.onDisconnected();
@@ -368,6 +377,29 @@ print(binascii.hexlify(microcontroller.cpu.uid).decode('ascii').upper())`
368377
console.log("Read Loop Stopped. Closing Serial Port.");
369378
}
370379

380+
// Analyzes the error returned from the WebSerial API and returns human readable feedback.
381+
_suggestSerialConnectActions(error) {
382+
if (error.name == "NetworkError" && error.message.includes("Failed to open serial port")) {
383+
return "The serial port could not be opened. Make sure the correct port is selected and no other program is using it. For more information, see the JavaScript console.";
384+
} else if (error.name == "NotFoundError" && error.message.includes("No port selected")) {
385+
return "No serial port was selected. Press the 'Connect to Device' button to try again.";
386+
} else if (error.name == "SecurityError") {
387+
return "Permissions to access the serial port were not granted. Please check your browser settings and try again.";
388+
}
389+
return `Connect to Serial Port returned error: ${error}`;
390+
}
391+
392+
// Analyzes the error from the FSAPI and returns human readable feedback
393+
_suggestFileConnectActions(error) {
394+
if (error.name == "SecurityError") {
395+
return "Permissions to access the filesystem were not granted. Please check your browser settings and try again.";
396+
} else if (error.name == "AbortError") {
397+
return "No folder selected. Press the 'Select New Folder' button to try again.";
398+
} else if (error.name == "TypeError")
399+
return `Connect to Filesystem returned error: ${error}`;
400+
401+
}
402+
371403
async showInfo(documentState) {
372404
return await this.infoDialog.open(this, documentState);
373405
}

0 commit comments

Comments
 (0)