diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7aeafbd..153af3a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,17 @@ +# Version 1.2.2 + +## Gamepad support +* After updating the XRPLib to version 2.1.3 or greater you can use a gamepad with XRPCode to drive your XRP. The Blocks palette has been updated with gamepad blocks. +* To drive your XRP you will need a program that responds to the gamepad interactions. There is a very small program in the XRPExamples directory. Be creative and create your own for different types of driving. +* We use the standard Web Gamepad support and recognize only one controller. If you want to know if your gamepad will work you can go to this website that will test if your gamepad is compatible with the web browser. +* If you don't have a gamepad you can use the keyboard. For the left joystick use WASD keys and for the right joystick use the IJKL keys. The number keys 1 - 0 are used for the buttons, in the same order as the pull down on the button gamepad block. There are no keys for the D-PAD. + +## Waiting dialog box +We noticed that when connecting and stopping a program via bluetooth that different operating systems and versions take different amounts of time. We now put up a working dialog box to let you know the connection is still being worked on. + +## Comment block added +If you would like to add comments to your Blockly program there is now a comment block. It can be found with the Text blocks. + # Version 1.2.1 ## Support for new XRP control board @@ -19,28 +33,6 @@ But this created a bug where the second time a program was run it could run out * Updates of XRPLib were only being allowed if you were connected via a USB cable. You can now also update the library over bluetooth. Although it will always be much faster when using a cable. -# Version 1.1.0 - -#### Bluetooth support - - -# Please read the steps below!! - -* Connect your XRP with a cable -* Let XRPCode upgrade the Micropython and XRPLib -* Under the RUN button will be the unique name of the XRP. You will want to write this on the XRP. -* Disconnect the XRP from the cable and turn on the XRP. -* When you click CONNECT select Bluetooth and it will bring up a list of XRPs that are not currently connected. (If your XRP does not show up press reset) -* Select your XRP and click Pair. -* Once connected XRPCode should be the same as if connected via a cable. YOU ARE NOW CABLE FREE! -* If the XRP is reset / turned off / too far away XRPCode will show RE-CONNECT XRP for 10 seconds and then switch to CONNECT. - * If the XRP is turned back on / brought closer XRPCode will auto re-connect to the XRP within that 10 seconds -* If you start the XRP and a program runs keeping the bluetooth from connecting then press reset and it will restart without running the program - -#### Fixed -* Input is now accepted in XRP applications -* The REPL stays live from run to run and has information from the last running program like globals and classes - * When STOP is used the REPL is reset. diff --git a/images/gamepad.png b/images/gamepad.png new file mode 100644 index 0000000..e4455f6 Binary files /dev/null and b/images/gamepad.png differ diff --git a/images/logo_x_rotate_in_place.gif b/images/logo_x_rotate_in_place.gif new file mode 100644 index 0000000..92c3867 Binary files /dev/null and b/images/logo_x_rotate_in_place.gif differ diff --git a/index.html b/index.html index b908fed..258e67a 100644 --- a/index.html +++ b/index.html @@ -7,21 +7,21 @@ XRP Code Editor - + - + - - + + - - - - - - + + + + + + - + @@ -82,12 +82,14 @@ class = "uk-align-right uk-text-small xrp-name"> -
+
-
+
+ + +
@@ -264,6 +239,13 @@

+
+
+

+
+
+
+
@@ -304,28 +286,28 @@

- + - - + + - - + + - - - + + + - - - - - - - - + + + + + + + + diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index 95e7c67..69c41d5 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -4,19 +4,50 @@ class Joystick{ - joysticks = { - x1: 0.0, - y1: 0.0, - x2: 0.0, - y2: 0.0, - bA: 0, - bB: 0, - bX: 0, - bY: 0, - bL: 0, - bR: 0 - } + joysticksArray = [ + 0.0, //x1 + 0.0, //y1 + 0.0, //x2 + 0.0, //y2 + 0, //bA + 0, //bB + 0, //bX + 0, //bY + 0, //bL + 0, //bR + 0, //tL + 0, //tR + 0, //bK + 0, //sT + 0, //dU + 0, //dD + 0, //dL + 0 //dR + ] + + lastsentArray = []; + + //array indexes + x1 = 0; + y1 = 1; + x2 = 2; + y2 = 3; + bA = 4; + bB = 5; + bX = 6; + bY = 7; + bL = 8; + bR = 9; + tL = 10; + tR = 11; + bK = 12; + sT = 13; + dU = 14; + dD = 15; + dL = 16; + dR = 17; + //keycodes being used left1 = 'KeyA'; right1 = 'KeyD'; @@ -32,10 +63,15 @@ class Joystick{ buttonY = 'Digit4'; bumperL = 'Digit5'; bumperR = 'Digit6'; + triggerL = 'Digit7'; + triggerR = 'Digit8'; + back = 'Digit9'; + start = 'Digit0'; listening = false; sendPacket = false; + sendingPacket = false; controllerIndex = 0; @@ -77,16 +113,19 @@ class Joystick{ this.intervalID = undefined; this.sendAPacket = this.sendAPacket.bind(this); + this.startListening(); //start listening for events + // ### CALLBACKS ### // Functions defined outside this module but used inside this.writeToDevice = undefined; - this.startListening(); } startPackets(){ + this.lastsentArray = this.joysticksArray.slice(); this.startListening(); this.listening = true; - this.intervalID = setInterval(this.sendAPacket, 90); + this.sendingPacket = false; + this.intervalID = setInterval(this.sendAPacket, 60); } stopPackets(){ @@ -97,103 +136,162 @@ class Joystick{ } } + /** + * Quantizes a float in the range [-1, 1] into an integer from 0 to 255. + * For example: + * -1 -> 0 + * 0 -> ~127/128 + * 1 -> 255 + */ + quantizeFloat(value) { + // Scale value from [-1,1] to [0,255] + return Math.round((value + 1) * 127.5); + } + + getChangedBytes(current, last, tolerance = 0.001) { + const changes = []; + for (let i = 0; i < current.length; i++) { + // Only consider sending a change if the difference exceeds the tolerance + if (Math.abs(current[i] - last[i]) > tolerance ) { + changes.push(i); // byte representing the array index + changes.push(this.quantizeFloat(current[i])); // byte representing the new value + } + } + const header = [0x55, changes.length]; + const results = new Uint8Array(header.length + changes.length); + results.set(header); + results.set(changes, header.length); + return results; + } + async sendAPacket(){ + if(this.sendingPacket) return; if(this.sendPacket){ + this.sendingPacket = true; //if joystick then update the status before sending this.updateStatus(); - await this.writeToDevice(JSON.stringify(this.joysticks) + '\r'); + const sending = this.getChangedBytes(this.joysticksArray, this.lastsentArray); + if(sending[1] > 0){ + this.lastsentArray = this.joysticksArray.slice(); + await this.writeToDevice(sending); + } + this.sendingPacket = false; } } startMovement(keyCode){ switch(keyCode) { case this.left1: - this.joysticks.x1 = -1.0 + this.joysticksArray[this.x1] = -1.0 break; case this.right1: - this.joysticks.x1 = 1.0 + this.joysticksArray[this.x1] = 1.0 break; case this.up1: - this.joysticks.y1 = -1.0 + this.joysticksArray[this.y1] = -1.0 break; case this.down1: - this.joysticks.y1 = 1.0 + this.joysticksArray[this.y1] = 1.0 break; case this.left2: - this.joysticks.x2 = -1.0 + this.joysticksArray[this.x2] = -1.0 break; case this.right2: - this.joysticks.x2 = 1.0 + this.joysticksArray[this.x2] = 1.0 break; case this.up2: - this.joysticks.y2 = -1.0 + this.joysticksArray[this.y2] = -1.0 break; case this.down2: - this.joysticks.y2 = 1.0 + this.joysticksArray[this.y2] = 1.0 break; case this.buttonA: - this.joysticks.bA = 1; + this.joysticksArray[this.bA] = 1; break; case this.buttonB: - this.joysticks.bB = 1; + this.joysticksArray[this.bB] = 1; break; case this.buttonX: - this.joysticks.bX = 1; + this.joysticksArray[this.bX] = 1; break; case this.buttonY: - this.joysticks.bY = 1; + this.joysticksArray[this.bY] = 1; break; case this.bumperL: - this.joysticks.bL = 1; + this.joysticksArray[this.bL] = 1; break; case this.bumperR: - this.joysticks.bR = 1; + this.joysticksArray[this.bR] = 1; + break; + case this.triggerL: + this.joysticksArray[this.tL] = 1; + break; + case this.triggerR: + this.joysticksArray[this.tR] = 1; + break; + case this.back: + this.joysticksArray[this.bK] = 1; + break; + case this.start: + this.joysticksArray[this.sT] = 1; break; } } stopMovement(keyCode){ switch(keyCode) { case this.left1: - this.joysticks.x1 = 0 + this.joysticksArray[this.x1] = 0 break; case this.right1: - this.joysticks.x1 = 0 + this.joysticksArray[this.x1] = 0 break; case this.up1: - this.joysticks.y1 = 0 + this.joysticksArray[this.y1] = 0 break; case this.down1: - this.joysticks.y1 = 0 + this.joysticksArray[this.y1] = 0 break; case this.left2: - this.joysticks.x2 = 0 + this.joysticksArray[this.x2] = 0 break; case this.right2: - this.joysticks.x2 = 0 + this.joysticksArray[this.x2] = 0 break; case this.up2: - this.joysticks.y2 = 0 + this.joysticksArray[this.y2] = 0 break; case this.down2: - this.joysticks.y2 = 0 + this.joysticksArray[this.y2] = 0 break; case this.buttonA: - this.joysticks.bA = 0; + this.joysticksArray[this.bA] = 0; break; case this.buttonB: - this.joysticks.bB = 0; + this.joysticksArray[this.bB] = 0; break; case this.buttonX: - this.joysticks.bX = 0; + this.joysticksArray[this.bX] = 0; break; case this.buttonY: - this.joysticks.bY = 0; + this.joysticksArray[this.bY] = 0; break; case this.bumperL: - this.joysticks.bL = 0; + this.joysticksArray[this.bL] = 0; break; case this.bumperR: - this.joysticks.bR = 0; + this.joysticksArray[this.bR] = 0; + break; + case this.triggerL: + this.joysticksArray[this.tL] = 0; + break; + case this.triggerR: + this.joysticksArray[this.tR] = 0; + break; + case this.back: + this.joysticksArray[this.bK] = 0; + break; + case this.start: + this.joysticksArray[this.sT] = 0; break; } } @@ -204,19 +302,27 @@ class Joystick{ const gamepad = gamepads[this.controllerIndex]; if (gamepad) { // Assuming at least 4 axis - this.joysticks.x1 = gamepad.axes[0]; - this.joysticks.y1 = gamepad.axes[1]; - this.joysticks.x2 = gamepad.axes[2]; - this.joysticks.y2 = gamepad.axes[3]; - - // Assuming at least 6 Buttons - this.joysticks.bA = gamepad.buttons[0].value; - this.joysticks.bB = gamepad.buttons[0].value; - this.joysticks.bX = gamepad.buttons[0].value; - this.joysticks.bY = gamepad.buttons[0].value; - this.joysticks.bL = gamepad.buttons[0].value; - this.joysticks.bR = gamepad.buttons[0].value; + // Read axes (ensure enough axes exist) + this.joysticksArray[this.x1] = gamepad.axes.length > 0 ? gamepad.axes[0] : 0.0; + this.joysticksArray[this.y1] = gamepad.axes.length > 1 ? gamepad.axes[1] : 0.0; + this.joysticksArray[this.x2] = gamepad.axes.length > 2 ? gamepad.axes[2] : 0.0; + this.joysticksArray[this.y2] = gamepad.axes.length > 3 ? gamepad.axes[3] : 0.0; + // Read buttons (ensure enough buttons exist) + this.joysticksArray[this.bA] = gamepad.buttons.length > 0 ? gamepad.buttons[0].value : 0; + this.joysticksArray[this.bB] = gamepad.buttons.length > 1 ? gamepad.buttons[1].value : 0; + this.joysticksArray[this.bX] = gamepad.buttons.length > 2 ? gamepad.buttons[2].value : 0; + this.joysticksArray[this.bY] = gamepad.buttons.length > 3 ? gamepad.buttons[3].value : 0; + this.joysticksArray[this.bL] = gamepad.buttons.length > 4 ? gamepad.buttons[4].value : 0; + this.joysticksArray[this.bR] = gamepad.buttons.length > 5 ? gamepad.buttons[5].value : 0; + this.joysticksArray[this.tL] = gamepad.buttons.length > 6 ?gamepad.buttons[6].value : 0; + this.joysticksArray[this.tR] = gamepad.buttons.length > 7 ?gamepad.buttons[7].value : 0; + this.joysticksArray[this.bK] = gamepad.buttons.length > 8 ?gamepad.buttons[8].value : 0; + this.joysticksArray[this.sT] = gamepad.buttons.length > 9 ?gamepad.buttons[9].value : 0; + this.joysticksArray[this.dU] = gamepad.buttons.length > 12 ?gamepad.buttons[12].value : 0; + this.joysticksArray[this.dD] = gamepad.buttons.length > 13 ?gamepad.buttons[13].value : 0; + this.joysticksArray[this.dL] = gamepad.buttons.length > 14 ?gamepad.buttons[14].value : 0; + this.joysticksArray[this.dR] = gamepad.buttons.length > 15 ?gamepad.buttons[15].value : 0; } } } @@ -253,11 +359,13 @@ class Joystick{ window.addEventListener("gamepadconnected", (event) => { this.controllerIndex = event.gamepad.index; + document.getElementById('IDGamePad').style.display = "block"; }); window.addEventListener("gamepaddisconnected", (event) => { if (this.controllerIndex === event.gamepad.index) { this.controllerIndex = -1; + document.getElementById('IDGamePad').style.display = "none"; } }); } diff --git a/js/main.js b/js/main.js index 7c1ebda..60ea006 100644 --- a/js/main.js +++ b/js/main.js @@ -7,7 +7,7 @@ import { configNonBeta } from './nonbetaConfig.js'; VERSION NUMBERS */ -const showChangelogVersion = "1.2.1"; //update all instances of ?version= in the index file to match the version. This is needed for local cache busting +const showChangelogVersion = "1.2.2"; //update all instances of ?version= in the index file to match the version. This is needed for local cache busting window.latestMicroPythonVersion = [1, 25, 0]; // this is needed because version 1.25.0 is not released yet and so the version number is not changing. Some boards @@ -612,7 +612,15 @@ async function downloadFileFromPath(fullFilePaths) { var JOY = undefined; function registerJoy(_container, state){ JOY = new Joystick(_container, state); - JOY.writeToDevice = (data) => REPL.writeToDevice(data); + JOY.writeToDevice = async (data) => { //REPL.writeToDevice(data); + if(REPL.DATABLE != undefined){ + try{ + await REPL.DATABLE.writeValueWithResponse(data); + }catch{ + console.log("error writing DATABLE data"); + } + } + }; REPL.startJoyPackets = () => JOY.startJoyPackets(); REPL.stopJoyPackets = () => JOY.stopJoyPackets(); } @@ -1014,6 +1022,9 @@ function registerEditor(_container, state) { document.getElementById('IDRunBTN').style.display = "block"; document.getElementById('IDStopBTN').style.display = "none"; + if(REPL.BLE_DEVICE == undefined){ + UIkit.modal(document.getElementById("IDWaitingParent")).hide(); //stop the spinner + } if(REPL.RUN_ERROR && REPL.RUN_ERROR.includes("[Errno 2] ENOENT", 0)){ await window.alertMessage("The program that you were trying to RUN has not been saved to this XRP.
To RUN this program save the file to XRP and click RUN again."); @@ -1243,8 +1254,13 @@ async function dialogMessage(message){ await UIkit.modal(elm).show(); } +let BASE = window.location.pathname.replace(/\/$/,''); +if(BASE != ""){ + BASE += '/' +} + async function downloadFile(filePath) { - let response = await fetch(filePath); + let response = await fetch(BASE + filePath); if(response.status != 200) { throw new Error("Server Error"); diff --git a/js/repl.js b/js/repl.js index f417f75..1d3a094 100644 --- a/js/repl.js +++ b/js/repl.js @@ -8,9 +8,9 @@ class ReplJS{ this.TEXT_DECODER = new TextDecoder(); // Used to read text from MicroPython this.USB_VENDOR_ID_BETA = 11914; // For filtering ports during auto or manual selection - this.USB_VENDOR_ID = 6991; // For filtering ports during auto or manual selection + this.USB_VENDOR_ID = 6991; //x1b4f // For filtering ports during auto or manual selection this.USB_PRODUCT_ID_BETA = 5; // For filtering ports during auto or manual selection - this.USB_PRODUCT_ID = 70; // For filtering ports during auto or manual selection + this.USB_PRODUCT_ID = 70; //x46 // For filtering ports during auto or manual selection this.USB_PRODUCT_MAC_ID = 10; // For filtering ports during auto or manual selection @@ -19,6 +19,7 @@ class ReplJS{ this.btService = undefined; this.READBLE = undefined; this.WRITEBLE = undefined; + this.DATABLE = undefined; this.LASTBLEREAD = undefined; this.BLE_DATA = null; this.BLE_DATA_RESOLVE = null; @@ -28,7 +29,8 @@ class ReplJS{ // UUIDs for standard NORDIC UART service and characteristics this.UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; this.TX_CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" - this.RX_CHARACTERISTIC_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + this.RX_CHARACTERISTIC_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + this.DATA_CHARACTERISTIC_UUID = "92ae6088-f24d-4360-b1b1-a432a8ed36ff" this.XRP_SEND_BLOCK_SIZE = 250; // wired can handle 255 bytes, but BLE 5.0 is only 250 @@ -326,7 +328,7 @@ class ReplJS{ this.startJoyPackets(); values = tempValue = []; } - if(tempValue[index+1] == 102){ + else if(tempValue[index+1] == 102){ //Stop Joystick packets on the input stream this.stopJoyPackets(); values = tempValue = []; @@ -345,7 +347,7 @@ class ReplJS{ // 1 - When we are running a program, we want all incoming lines to be pushed to the terminal // 2 - Except the very first 'OK'. There are timing issues and this was the best place to catch it. // This makes the user output look a lot nicer with out the 'OK' showing up. - if(this.SPECIAL_FORCE_OUTPUT_FLAG){ + if(this.SPECIAL_FORCE_OUTPUT_FLAG && values.length > 0){ if (this.CATCH_OK){ let v = this.TEXT_DECODER.decode(values) if(v.startsWith("OK")){ @@ -358,8 +360,9 @@ class ReplJS{ this.onData(this.TEXT_DECODER.decode(values)); } } - - this.COLLECTED_DATA += this.TEXT_DECODER.decode(values); + if(values.length > 0){ + this.COLLECTED_DATA += this.TEXT_DECODER.decode(values); + } // If raw flag set true, collect raw data for now if(this.COLLECT_RAW_DATA == true){ @@ -416,11 +419,12 @@ class ReplJS{ } - bleDisconnect(){ + async bleDisconnect(){ if(REPL.DEBUG_CONSOLE_ON) console.log("BLE Disconnected"); REPL.BLE_DISCONNECT_TIME = Date.now(); REPL.WRITEBLE = undefined; REPL.READBLE = undefined; + REPL.DATABLE = undefined; REPL.DISCONNECT = true; // Will stop certain events and break any EOT waiting functions if(!REPL.STOP){ //If they pushed the STOP button then don't make it look disconnected it will be right back REPL.onDisconnect(); @@ -429,33 +433,57 @@ class ReplJS{ REPL.RUN_BUSY = false; REPL.STOP = false; REPL.BUSY = false; + //bug must wait for a bit before trying to reconnect. Known Web Bluetooth bug. + await new Promise(r => setTimeout(r, 400)); REPL.bleReconnect(); } async bleReconnect(){ if(this.DISCONNECT){ - try { - if(this.DEBUG_CONSOLE_ON) console.log("Trying ble auto reconnect..."); - const server = await this.connectWithTimeout(this.BLE_DEVICE, 10000); //wait for 10seconds to see if it reconnects - //const server = await this.BLE_DEVICE.gatt.connect(); - this.btService = await server.getPrimaryService(this.UART_SERVICE_UUID); - //console.log('Getting TX Characteristic...'); - this.WRITEBLE = await this.btService.getCharacteristic(this.TX_CHARACTERISTIC_UUID); - this.READBLE = await this.btService.getCharacteristic(this.RX_CHARACTERISTIC_UUID); - this.READBLE.startNotifications(); - this.finishConnect(); - if (this.DEBUG_CONSOLE_ON) console.log("fcg: out of tryAutoConnect"); - return true; - // Perform operations after successful connection - } catch (error) { - console.log('timed out: ', error); - this.BLE_DEVICE = undefined; + if(this.DEBUG_CONSOLE_ON) console.log("Trying ble auto reconnect..."); + if(! REPL.bleConnect()){ REPL.onDisconnect(); document.getElementById('IDConnectBTN').disabled = false; } } } + async bleConnect(){ + try{ + await new Promise(r => setTimeout(r, 300)); + const server = await this.connectWithTimeout(this.BLE_DEVICE, 10000); //wait for 10seconds to see if it reconnects + //await new Promise(r => setTimeout(r, 300)); + + let attempts = 10; + for (let i = 0; i < attempts; i++) { + try { + this.btService = await server.getPrimaryService(this.UART_SERVICE_UUID); + break; + } catch (e) { + if (/No Services found/.test(e.message) && i < attempts - 1) { + await new Promise(r => setTimeout(r, 400)); + } else { + throw e; + } + } + } + //console.log('Getting TX Characteristic...'); + this.WRITEBLE = await this.btService.getCharacteristic(this.TX_CHARACTERISTIC_UUID); + this.READBLE = await this.btService.getCharacteristic(this.RX_CHARACTERISTIC_UUID); + this.DATABLE = await this.btService.getCharacteristic(this.DATA_CHARACTERISTIC_UUID); + this.READBLE.startNotifications(); + this.finishConnect(); + if (this.DEBUG_CONSOLE_ON) console.log("fcg: out of tryAutoConnect"); + return true; + // Perform operations after successful connection + } catch (error) { + console.log('timed out: ', error); + await window.alertMessage("Error connecting to the XRP. Please refresh this page and try again"); + this.BLE_DEVICE = undefined; + return false; + } + } + connectWithTimeout(device, timeoutMs) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { @@ -509,13 +537,23 @@ class ReplJS{ async bleQueue(value){ this.Queue = this.Queue.then(async () => { try { - await this.WRITEBLE.writeValue(value); + await this.WRITEBLE.writeValueWithResponse(value); } catch (error) { console.error('ble write failed:', error); } }); } + //This is writing to cause the XRP to reboot, so we don't expect a response from the BLE. So, we will not ask for one. + async writeSTOPtoBleDevice(str){ + try{ + await this.WRITEBLE.writeValueWithResponse(this.str2ab(str)); //don't wait since the Windows timeout is long + } catch(error){ + console.error('ble stop write failed:', error); + //do nothing we expected an error + } + } + async softReset(){ return; this.startReaduntil("MPY: soft reboot"); @@ -1246,21 +1284,21 @@ class ReplJS{ " file.seek(0)\n" + " file.write(b'\\x00')\n" + " doNothing = True\n" + - " else:\n" + - " file.seek(0)\n" + - " file.write(b'\\x01')\n" + + //" else:\n" + + //" file.seek(0)\n" + + //" file.write(b'\\x01')\n" + " if(not doNothing):\n" + " with open('"+fileToEx+"', mode='r') as exfile:\n" + " code = exfile.read()\n"+ " execCode = compile(code, '" +fileToEx2+"', 'exec')\n" + " exec(execCode)\n" + - " with open(FILE_PATH, 'r+b') as file:\n" + - " file.write(b'\\x00')\n" + + //" with open(FILE_PATH, 'r+b') as file:\n" + + //" file.write(b'\\x00')\n" + "except Exception as e:\n" + " import sys\n" + " sys.print_exception(e)\n"+ - " with open(FILE_PATH, 'r+b') as file:\n" + - " file.write(b'\\x00')\n" + + //" with open(FILE_PATH, 'r+b') as file:\n" + + //" file.write(b'\\x00')\n" + "finally:\n"+ " import gc\n" + " gc.collect()\n" + @@ -1384,7 +1422,7 @@ class ReplJS{ async checkIfNeedUpdate(){ //if no micropython on the XRP if(!this.HAS_MICROPYTHON){ - await this.showMicropythonUpdate(); + //await this.showMicropythonUpdate(); return; } @@ -1466,7 +1504,7 @@ class ReplJS{ let jresp = JSON.parse(response); var urls = jresp.urls; window.setPercent(1, "Updating XRPLib..."); - let percent_per = Math.round(99 / (urls.length + window.phewList.length + window.bleList.length + 1)); + let percent_per = Math.floor(99 / (urls.length + window.phewList.length + window.bleList.length + 1)); let cur_percent = 1 + percent_per; await this.deleteFileOrDir("/lib/XRPLib"); //delete all the files first to avoid any confusion. @@ -1510,6 +1548,8 @@ class ReplJS{ window.resetPercentDelay(); await this.getOnBoardFSTree(); UIkit.modal(document.getElementById("IDProgressBarParent")).hide(); + await window.alertMessage("The XRP must be restarted for changes to take affect. \n If XRP does not reconnect after 30 seconds refresh the browser and connect manually"); + await this.writeToDevice(this.CTRL_CMD_SOFTRESET); } async updateMicroPython() { @@ -1595,8 +1635,8 @@ class ReplJS{ if (result == undefined){ if(this.BLE_DEVICE != undefined){ - await this.writeToDevice(this.BLE_STOP_MSG); - return true; //BUGBUG: not sure what happens if it now doesn't connect. + await this.writeSTOPtoBleDevice(this.BLE_STOP_MSG); + return false; } this.startReaduntil("KeyboardInterrupt:"); @@ -1612,6 +1652,8 @@ class ReplJS{ } //try multiple times to get to the prompt var gotToPrompt = false; + await this.getToNormal(); + //await this.writeToDevice("\r" + this.CTRL_CMD_NORMALMODE); for(let i=0;i<20;i++){ this.startReaduntil(">>>"); await this.writeToDevice("\r" + this.CTRL_CMD_KINTERRUPT); @@ -1630,7 +1672,11 @@ class ReplJS{ async checkIfMP(){ if(! await this.stopTheRobot()){ + if(this.BLE_DEVICE != undefined){ + return false; + } this.HAS_MICROPYTHON = false; + /* let ans = await window.confirmMessage("XRPCode is having problems connecting to this XRP.
" + "Two Options:" + "" + "
Or click CANCEL and XRPCode will reinstall MicroPython onto the XRP") return ans; + */ } // do a softreset, but time out if no response @@ -1656,43 +1703,10 @@ class ReplJS{ await this.PORT.open({ baudRate: 115200 }); this.WRITER = await this.PORT.writable.getWriter(); // Make a writer since this is the first time port opened return true; - /* - this.readLoop(); // Start read loop - if(await this.checkIfMP()){ - if(this.HAS_MICROPYTHON == false){ //something went wrong, just get out of here - return; - } - this.BUSY = false; - await this.getToNormal(); - await this.getOnBoardFSTree(); - this.onConnect(); - } - - this.BUSY = false; - await this.checkIfNeedUpdate(); - this.IDSet(); - */ }catch(err){ if(err.name == "InvalidStateError"){ if(this.DEBUG_CONSOLE_ON) console.log("%cPort already open, everything good to go!", "color: lime"); return true; - /* - if (await this.checkIfMP()){ - if(this.HAS_MICROPYTHON == false){ //something went wrong, just get out of here - return; - } - this.onConnect(); - this.BUSY = false; - await this.getToNormal(); - - await this.getOnBoardFSTree(); - } - - this.BUSY = false; - await this.checkIfNeedUpdate(); - this.IDSet(); - */ - }else if(err.name == "NetworkError"){ //alert("Opening port failed, is another application accessing this device/port?"); if(this.DEBUG_CONSOLE_ON) console.log("%cOpening port failed, is another application accessing this device/port?", "color: red"); @@ -1707,25 +1721,27 @@ class ReplJS{ async finishConnect(){ this.DISCONNECT = false; this.readLoop(); - if(await this.checkIfMP()){ - if(this.HAS_MICROPYTHON == false){ //something went wrong, just get out of here - return; - } - this.BUSY = false; - await this.getToNormal(); - await this.getOnBoardFSTree(); - this.onConnect(); - } - + if(! await this.checkIfMP()){ + if(this.BLE_DEVICE != undefined){return;} //if ble then we are restarting and waiting for a reconnect + await window.alertMessage("MicroPython not found. Please try connecting again"); + return; + } + await this.getToNormal(); + await this.getOnBoardFSTree(); + this.onConnect(); this.LAST_RUN = undefined; this.BUSY = false; if(this.PORT != undefined){ //if we connected via USB then we can release the BLE terminal await this.resetTerminal(); } - await this.resetIsRunning(); + //await this.resetIsRunning(); //Shouldn't need this anymore Is running only happens with a stop. await this.checkIfNeedUpdate(); this.IDSet(); this.pluginCheck(); + if(this.BLE_DEVICE != undefined){ + UIkit.modal(document.getElementById("IDWaitingParent")).hide(); + + } } async tryAutoConnect(){ if(this.BUSY == true){ @@ -1821,6 +1837,8 @@ class ReplJS{ this.BLE_DEVICE = undefined; //just in case we were connected before. + let UserCancled = false; + var elapseTime = (Date.now() - this.BLE_DISCONNECT_TIME) / 1000; if (elapseTime > 60){ await window.alertMessage("Error while detecting bluetooth devices. \nPlease refresh the browser and try again.") @@ -1836,34 +1854,23 @@ class ReplJS{ .then(device => { //console.log('Connecting to device...'); this.BLE_DEVICE = device; - return device.gatt.connect(); - }) - .then(servers => { - //console.log('Getting UART Service...'); - return servers.getPrimaryService(this.UART_SERVICE_UUID); - }) - .then(btService => { - this.btService = btService; - //console.log('Getting TX Characteristic...'); - return btService.getCharacteristic(this.TX_CHARACTERISTIC_UUID); - }) - .then(characteristic => { - //console.log('Connected to TX Characteristic'); - this.WRITEBLE = characteristic; - //console.log('Getting RX Characteristic...'); - return this.btService.getCharacteristic(this.RX_CHARACTERISTIC_UUID); - // Now you can use the characteristic to send data - }) .then (characteristic => { - this.READBLE = characteristic; - //this.READBLE.addEventListener('characteristicvaluechanged', this.readloopBLE); - this.READBLE.startNotifications(); - this.BLE_DEVICE.addEventListener('gattserverdisconnected', this.bleDisconnect); - this.finishConnect(); }) .catch(error => { + if(error.code == 8){ + UserCancled = true; + return; + } + window.alertMessage("*Error connecting to XRP. Please refresh this page and try again"); console.log('Error: ' + error); }); + + if(UserCancled) return; + document.getElementById("IdWaiting_TitleText").innerText = 'Connecting to XRP...'; + UIkit.modal(document.getElementById("IDWaitingParent")).show(); + + await this.bleConnect(); + this.BLE_DEVICE.addEventListener('gattserverdisconnected', this.bleDisconnect); this.MANNUALLY_CONNECTING = false; this.BUSY = false; if (this.DEBUG_CONSOLE_ON) console.log("fcg: out of ConnectBLE"); @@ -1879,28 +1886,13 @@ class ReplJS{ if(this.RUN_BUSY){ //if the program is running do ctrl-c until we know it has stopped this.STOP = true; //let the executeLines code know when it stops, it stopped because the STOP button was pushed this.SPECIAL_FORCE_OUTPUT_FLAG = false; //turn off showing output so they don't see the keyboardInterrupt and stack trace. - if(this.BLE_DEVICE != undefined){ - await this.writeToDevice(this.BLE_STOP_MSG); - return; - } - - var count = 1; - /* - We are BUSY, this means that there is another thread that started the program. - Because they could be in a timer we are going to hammer ctrl-c until we know they are out of the program. - The problem with this is that we will end up sending a ctrl-c during the finally that is running the resetbot. - */ - while (this.STOP) { - await this.writeToDevice("\r" + this.CTRL_CMD_KINTERRUPT); // ctrl-C to interrupt any running program - count += 1; - if (count > 20){ - break; - } - } + document.getElementById("IdWaiting_TitleText").innerText = 'Stopping XRP...'; + UIkit.modal(document.getElementById("IDWaitingParent")).show(); + this.stopTheRobot(); //document.getElementById('IDRunBTN').style.display = "block"; return - } + /* // I don't think this code will run anymore since there is no stop button when a program is not running. //The user pushed STOP while things were idle. Lets make sure the robot is stopped and run restbot. @@ -1911,6 +1903,7 @@ class ReplJS{ var cmd = "import XRPLib.resetbot\n" await this.writeUtilityCmdRaw(cmd, true, 1); await this.getToNormal(3); + */ } async disconnect(){ diff --git a/js/xrp_blockly_toolbox.js b/js/xrp_blockly_toolbox.js index 694e28d..6b7fc0a 100644 --- a/js/xrp_blockly_toolbox.js +++ b/js/xrp_blockly_toolbox.js @@ -255,10 +255,29 @@ var baseToolbox = { "blockxml": "\n\nxrp_1\n\n\n \n\n\n\n\n", }, ] + }, + { + "kind": "CATEGORY", + "name": "Gamepad", + "colour": "#ff9248", // turquoise + "contents": [ + { + "kind": "BLOCK", + "type": "xrp_gp_get_value" + }, + { + "kind": "BLOCK", + "type": "xrp_gp_button_pressed" + }, + ] }, { "kind": "CATEGORY", "contents": [ + { + "kind": "BLOCK", + "type": "comment" + }, { "kind": "BLOCK", "type": "xrp_sleep", @@ -306,14 +325,20 @@ var baseToolbox = { "contents": [ { "kind": "BLOCK", - "blockxml": "\n \n \n 10\n \n \n ", - "type": "controls_repeat_ext" + "blockxml": "\n UNTIL\n \n \n \n", + "type": "controls_whileUntil" }, { "kind": "BLOCK", "blockxml": "\n WHILE\n ", "type": "controls_whileUntil" }, + { + "kind": "BLOCK", + "blockxml": "\n \n \n 10\n \n \n ", + "type": "controls_repeat_ext" + }, + { "kind": "BLOCK", "blockxml": "\n i\n \n \n 1\n \n \n \n \n 10\n \n \n \n \n 1\n \n \n ", @@ -401,7 +426,13 @@ var baseToolbox = { }, { "kind": "CATEGORY", + "name": "Text", + "colour": "#5ba58c", // seafoam green "contents": [ + { + "kind": "BLOCK", + "type": "comment" + }, { "kind": "BLOCK", "blockxml": "\n \n \n abc\n \n \n ", @@ -462,9 +493,7 @@ var baseToolbox = { "blockxml": "\n \n TEXT\n \n \n abc\n \n \n ", "type": "text_prompt_ext" } - ], - "name": "Text", - "colour": "#5ba58c" // seafoam green + ] }, { "kind": "CATEGORY", diff --git a/js/xrp_blocks.js b/js/xrp_blocks.js index 20a66ab..514e7eb 100644 --- a/js/xrp_blocks.js +++ b/js/xrp_blocks.js @@ -1,4 +1,3 @@ - /* This file creates each Block item for Blockly. You can set and update the colors here based off the HUE value. @@ -565,6 +564,36 @@ Blockly.Blocks['xrp_ws_connect_server'] = { } }; +// Gamepad + +Blockly.Blocks['xrp_gp_get_value'] = { + init: function () { + this.appendDummyInput() + .appendField("Joystick:") + .appendField(new Blockly.FieldDropdown([["X1", "X1"], ["X2", "X2"], ["Y1", "Y1"], ["Y2", "Y2"]]), "GPVALUE") + this.setOutput(true, null); + this.setColour("#ff9248"); // crimson + this.setTooltip("Get the value of a gamepad joystick"); + this.setHelpUrl(""); + } +}; + +Blockly.Blocks['xrp_gp_button_pressed'] = { + init: function () { + this.appendDummyInput() + .appendField("Button:") + .appendField(new Blockly.FieldDropdown([["A", "BUTTON_A"], ["B", "BUTTON_B"], ["X", "BUTTON_X"], ["Y", "BUTTON_Y"], ["Bumper Left", "BUMPER_L"], ["Bumper Right", "BUMPER_R"], + ["Trigger Left", "TRIGGER_L"],["Trigger Right", "TRIGGER_R"],["Back", "BACK"], ["Start", "START"], + ["D-PAD Up", "DPAD_UP"],["D-PAD Down", "DPAD_DN"],["D-PAD Left", "DPAD_L"],["D-PAD Right", "DPAD_R"]]), "GPBUTTON") + .appendField("Pressed") + this.setOutput(true, null); + this.setColour("#ff9248"); // crimson + this.setTooltip("Check to see if a gamepad button is pressed"); + this.setHelpUrl(""); + } +}; + + // Logic Blockly.Blocks['xrp_sleep'] = { init: function () { @@ -589,3 +618,16 @@ Blockly.Blocks['xrp_sleep'] = { // Lists --> eggplant purple // Variables --> grey // Functions --> medium purple + +Blockly.Blocks['comment'] = { + init: function() { + this.appendDummyInput() + .appendField("Comment") + .appendField(new Blockly.FieldTextInput(""), "TEXT"); + this.setColour(60); // yellow + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setTooltip("Add a comment to your code."); + this.setHelpUrl(""); + } +}; diff --git a/js/xrp_blocks_python.js b/js/xrp_blocks_python.js index bf23956..3b495b3 100644 --- a/js/xrp_blocks_python.js +++ b/js/xrp_blocks_python.js @@ -341,6 +341,23 @@ Blockly.Python['xrp_ws_start_server'] = function (block) { return code; }; +// Gamepad + +Blockly.Python['xrp_gp_get_value'] = function (block) { + PY.definitions_['import_gamepad'] = 'from XRPLib.gamepad import *'; + PY.definitions_[`gamepad_setup`] = `gp = Gamepad.get_default_gamepad()`; + var value = block.getFieldValue("GPVALUE"); + var code = `gp.get_value(gp.${value})`; + return [code , Blockly.Python.ORDER_NONE]; +}; + +Blockly.Python['xrp_gp_button_pressed'] = function (block) { + PY.definitions_['import_gamepad'] = 'from XRPLib.gamepad import *'; + PY.definitions_[`gamepad_setup`] = `gp = Gamepad.get_default_gamepad()`; + var value = block.getFieldValue("GPBUTTON"); + var code = `gp.is_button_pressed(gp.${value})`; + return [code , Blockly.Python.ORDER_NONE]; +}; //Logic Blockly.Python['xrp_sleep'] = function (block) { @@ -350,4 +367,9 @@ Blockly.Python['xrp_sleep'] = function (block) { return code; }; +Blockly.Python['comment'] = function(block) { + var text = block.getFieldValue('TEXT'); + return '# ' + text + '\n'; +}; + diff --git a/lib/XRPExamples/gamepad_example.blocks b/lib/XRPExamples/gamepad_example.blocks new file mode 100644 index 0000000..d5e3795 --- /dev/null +++ b/lib/XRPExamples/gamepad_example.blocks @@ -0,0 +1,15 @@ +from XRPLib.gamepad import * +from XRPLib.differential_drive import DifferentialDrive + +gp = Gamepad.get_default_gamepad() + +differentialDrive = DifferentialDrive.get_default_differential_drive() + + +while not (gp.is_button_pressed(gp.BACK)): + differentialDrive.arcade((gp.get_value(gp.Y1)), (gp.get_value(gp.X1))) + + + +## [2025-07-13 01:36:24] +##XRPBLOCKS {"blocks":{"languageVersion":0,"blocks":[{"type":"controls_whileUntil","id":"#/DDnbE2JxVsLQo`C`e^","x":-363,"y":16,"fields":{"MODE":"UNTIL"},"inputs":{"BOOL":{"block":{"type":"xrp_gp_button_pressed","id":"DtLypJc;SU2xT4A~S3nV","fields":{"GPBUTTON":"BACK"}}},"DO":{"block":{"type":"xrp_arcade","id":"(|]Iln#JYa9hzgEz+(4g","inputs":{"STRAIGHT":{"shadow":{"type":"math_number","id":"FOj1uvC$:~x/+cX}d(:|","fields":{"NUM":0.8}},"block":{"type":"xrp_gp_get_value","id":"s#GS_Dt)B8Cb9+4ctpwk","fields":{"GPVALUE":"Y1"}}},"TURN":{"shadow":{"type":"math_number","id":"n1jAKrjST!+3#bfChh$d","fields":{"NUM":0.2}},"block":{"type":"xrp_gp_get_value","id":"*]`U9XHyYnA?Gp%U;$NF","fields":{"GPVALUE":"X1"}}}}}}}}]}} \ No newline at end of file diff --git a/lib/XRPLib/gamepad.py b/lib/XRPLib/gamepad.py new file mode 100644 index 0000000..9a6e661 --- /dev/null +++ b/lib/XRPLib/gamepad.py @@ -0,0 +1,97 @@ +from ble.blerepl import uart +import sys +from micropython import const + +class Gamepad: + + _DEFAULT_GAMEPAD_INSTANCE = None + + X1 = const(0) + Y1 = const(1) + X2 = const(2) + Y2 = const(3) + BUTTON_A = const(4) + BUTTON_B = const(5) + BUTTON_X = const(6) + BUTTON_Y = const(7) + BUMPER_L = const(8) + BUMPER_R = const(9) + TRIGGER_L = const(10) + TRIGGER_R = const(11) + BACK = const(12) + START = const(13) + DPAD_UP = const(14) + DPAD_DN = const(15) + DPAD_L = const(16) + DPAD_R = const(17) + + _joyData = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0] + + @classmethod + def get_default_gamepad(cls): + """ + Get the default XRP bluetooth joystick instance. This is a singleton, so only one instance of the gamepad sensor will ever exist. + """ + if cls._DEFAULT_GAMEPAD_INSTANCE is None: + cls._DEFAULT_GAMEPAD_INSTANCE = cls() + cls._DEFAULT_GAMEPAD_INSTANCE.start() + return cls._DEFAULT_GAMEPAD_INSTANCE + + def __init__(self): + """ + Manages communication with gamepad data coming from a remote computer via bluetooth + + """ + def start(self): + """ + Signals the remote computer to begin sending gamepad data packets. + """ + for i in range(len(self._joyData)): + self._joyData[i] = 0.0 + uart.set_data_callback(self._data_callback) + sys.stdout.write(chr(27)) + sys.stdout.write(chr(101)) + + + def stop(self): + """ + Signals the remote computer to stop sending gamepad data packets. + """ + sys.stdout.write(chr(27)) + sys.stdout.write(chr(102)) + + def get_value(self, index:int) -> float: + """ + Get the current value of a joystick axis + + :param index: The joystick axis index + Gamepad.X1, Gamepad.Y1, Gamepad.X2, Gamepad.Y2 + :type int + :returns: The value of the joystick between -1 and 1 + :rtype: float + """ + return -self._joyData[index] #returning the negative to make normal for user + + def is_button_pressed(self, index:int) -> bool: + """ + Checks if a specific button is currently pressed. + + :param index: The button index + Gamepad.BUTTON_A, Gamepad.TRIGGER_L, Gamepad.DPAD_UP, etc + :type int + :returns: The value of the button 1 or 0 + :rtype: bool + """ + return self._joyData[index] > 0 + + def _data_callback(self, data): + if(data[0] == 0x55 and len(data) == data[1] + 2): + for i in range(2, data[1] + 2, 2): + self._joyData[data[i]] = round(data[i + 1]/127.5 - 1, 2) + + diff --git a/lib/XRPLib/imu.py b/lib/XRPLib/imu.py index 5c154ad..cd7acbf 100644 --- a/lib/XRPLib/imu.py +++ b/lib/XRPLib/imu.py @@ -7,6 +7,7 @@ from .imu_defs import * from uctypes import struct, addressof except (TypeError, ModuleNotFoundError): + LSM_ADDR_PRIMARY = 0x6A # Import wrapped in a try/except so that autodoc generation can process properly pass from machine import I2C, Pin, Timer, disable_irq, enable_irq diff --git a/lib/XRPLib/resetbot.py b/lib/XRPLib/resetbot.py index de80714..7c9d321 100644 --- a/lib/XRPLib/resetbot.py +++ b/lib/XRPLib/resetbot.py @@ -33,12 +33,21 @@ def reset_webserver(): # Shut off the webserver and close network connections Webserver.get_default_webserver().stop_server() +def reset_gamepad(): + from XRPLib.gamepad import Gamepad + # Stop the browser from sending more gamepad data + Gamepad.get_default_gamepad().stop() + def reset_hard(): + reset_gamepad() reset_motors() reset_led() reset_servos() reset_webserver() +if "XRPLib.gamepad" in sys.modules: + reset_gamepad() + if "XRPLib.encoded_motor" in sys.modules: reset_motors() @@ -50,3 +59,4 @@ def reset_hard(): if "XRPLib.webserver" in sys.modules: reset_webserver() + diff --git a/lib/XRPLib/timeout.py b/lib/XRPLib/timeout.py index bc4e6a6..1b2bd58 100644 --- a/lib/XRPLib/timeout.py +++ b/lib/XRPLib/timeout.py @@ -1,15 +1,15 @@ import time class Timeout: - def __init__(self, timeout): + def __init__(self, timeout: float): """ Starts a timer that will expire after the given timeout. :param timeout: The timeout, in seconds :type timeout: float """ - self.timeout = timeout - self.start_time = time.time() + self.timeout = timeout*1000 + self.start_time = time.ticks_ms() def is_done(self): """ @@ -17,4 +17,4 @@ def is_done(self): """ if self.timeout is None: return False - return time.time() - self.start_time > self.timeout \ No newline at end of file + return time.ticks_ms() - self.start_time > self.timeout \ No newline at end of file diff --git a/lib/ble/ble_uart_peripheral.py b/lib/ble/ble_uart_peripheral.py index 611d685..d9da813 100644 --- a/lib/ble/ble_uart_peripheral.py +++ b/lib/ble/ble_uart_peripheral.py @@ -4,6 +4,7 @@ #from .ble_advertising import advertising_payload import struct from micropython import const +from machine import Timer #Advertise info # org.bluetooth.characteristic.gap.appearance.xml @@ -17,6 +18,7 @@ _IRQ_GATTS_WRITE = const(3) _IRQ_GATTS_INDICATE_DONE = const(20) +_FLAG_READ = const(0x0002) _FLAG_WRITE = const(0x0008) _FLAG_NOTIFY = const(0x0010) @@ -29,23 +31,42 @@ bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), _FLAG_WRITE, ) + +# Define a new UUID for the binary data characteristic. +_UART_DATA_RX = ( + bluetooth.UUID("92ae6088-f24d-4360-b1b1-a432a8ed36ff"), + _FLAG_WRITE, +) +# Define a new UUID for the binary data characteristic. +_UART_DATA_TX = ( + bluetooth.UUID("92ae6088-f24d-4360-b1b1-a432a8ed36fe"), + _FLAG_NOTIFY, +) + _UART_SERVICE = ( _UART_UUID, - (_UART_TX, _UART_RX), + (_UART_TX, _UART_RX, _UART_DATA_RX, _UART_DATA_TX,), ) +_timer = Timer(-1) + +def delayedRestart(_arg): + import machine + machine.reset() + class BLEUART: def __init__(self, ble, name="mpy-uart", rxbuf=100): self._ble = ble self._ble.active(True) self._ble.irq(self._irq) - ((self._tx_handle, self._rx_handle),) = self._ble.gatts_register_services((_UART_SERVICE,)) + ((self._tx_handle, self._rx_handle, self._data_rx_handle, self._data_tx_handle),) = self._ble.gatts_register_services((_UART_SERVICE,)) # Increase the size of the rx buffer and enable append mode. self._ble.gatts_set_buffer(self._rx_handle, rxbuf, True) self._connections = set() self._rx_buffer = bytearray() self._handler = None self._payload = self._advertising_payload(name, _ADV_APPEARANCE_GENERIC_COMPUTER) + self._data_callback = None self._advertise() def irq(self, handler): @@ -68,12 +89,22 @@ def _irq(self, event, data): self._rx_buffer += self._ble.gatts_read(self._rx_handle) if(self._rx_buffer.find(b'##XRPSTOP#' + b'#') != -1): #WARNING: This is broken up so it won't restart during an update or this file. self._rx_buffer = bytearray() - import machine - machine.reset() + # set the bit in isrunning to tell main not to re-run the program so the IDE can connect + FILE_PATH = '/lib/ble/isrunning' + with open(FILE_PATH, 'r+b') as file: + file.write(b'\x01') + # slight delay to avoid errors on the browser side. + _timer.init(mode=Timer.PERIODIC, period=50, callback=delayedRestart) + elif conn_handle in self._connections and value_handle == self._data_rx_handle: + new_data = self._ble.gatts_read(self._data_rx_handle) + if self._data_callback: + self._data_callback(new_data) + #schedule(self._data_callback, new_data) elif event == _IRQ_GATTS_INDICATE_DONE: if self._handler: self._handler() - + #else: + #print("IRQ Event Code: " + str(event)) def any(self): return len(self._rx_buffer) @@ -90,6 +121,17 @@ def write(self, data): for conn_handle in self._connections: self._ble.gatts_indicate(conn_handle, self._tx_handle, data) + def write_data(self, data): + #print("write_data:" + data) + for conn_handle in self._connections: + self._ble.gatts_notify(conn_handle, self._data_tx_handle, data) + + def set_data_callback(self, callback): + self._data_callback = callback + + def clear_data_callback(self): + self._data_callback = None + def close(self): for conn_handle in self._connections: self._ble.gap_disconnect(conn_handle) @@ -113,4 +155,4 @@ def _append(adv_type, value): _append(_ADV_TYPE_NAME, name) _append(_ADV_TYPE_APPEARANCE, struct.pack("