From f6f86110ff440da8fecdc012ff0f46fdb8aea739 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Thu, 3 Apr 2025 00:03:20 -0400 Subject: [PATCH 01/49] joystick control update Update to use a separate characteristic and send binary for just the values that have changed --- js/joystick_wrapper.js | 140 +++++++++++++++++++++++++++++------------ js/main.js | 6 +- js/repl.js | 24 +++++-- 3 files changed, 123 insertions(+), 47 deletions(-) diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index 95e7c67..62c602c 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -4,6 +4,21 @@ class Joystick{ + joysticksArray = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + + lastsentArray = []; + joysticks = { x1: 0.0, y1: 0.0, @@ -17,6 +32,18 @@ class Joystick{ bR: 0 } + //array indexes + x1 = 0; + y1 = 1; + x2 = 2; + y2 = 3; + bA = 4; + bB = 5; + bX = 6; + bY = 7; + bL = 8; + bR = 9; + //keycodes being used left1 = 'KeyA'; right1 = 'KeyD'; @@ -84,9 +111,10 @@ class Joystick{ this.startListening(); } startPackets(){ + this.lastsentArray = this.joysticksArray.slice(); this.startListening(); this.listening = true; - this.intervalID = setInterval(this.sendAPacket, 90); + this.intervalID = setInterval(this.sendAPacket, 60); } stopPackets(){ @@ -97,103 +125,135 @@ 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.sendPacket){ //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){ + await this.writeToDevice(sending); //(JSON.stringify(this.joysticks) + '\r'); + } + this.lastsentArray = this.joysticksArray.slice(); } } 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; } } 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; } } @@ -204,18 +264,18 @@ 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]; + this.joysticksArray[this.x1] = gamepad.axes[0]; + this.joysticksArray[this.y1] = gamepad.axes[1]; + this.joysticksArray[this.x2] = gamepad.axes[2]; + this.joysticksArray[this.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; + this.joysticksArray[this.bA] = gamepad.buttons[0].value; + this.joysticksArray[this.bB] = gamepad.buttons[0].value; + this.joysticksArray[this.bX] = gamepad.buttons[0].value; + this.joysticksArray[this.bY] = gamepad.buttons[0].value; + this.joysticksArray[this.bL] = gamepad.buttons[0].value; + this.joysticksArray[this.bR] = gamepad.buttons[0].value; } } diff --git a/js/main.js b/js/main.js index 7c1ebda..22f7a99 100644 --- a/js/main.js +++ b/js/main.js @@ -612,7 +612,11 @@ async function downloadFileFromPath(fullFilePaths) { var JOY = undefined; function registerJoy(_container, state){ JOY = new Joystick(_container, state); - JOY.writeToDevice = (data) => REPL.writeToDevice(data); + JOY.writeToDevice = (data) => { //REPL.writeToDevice(data); + if(REPL.DATABLE != undefined){ + REPL.DATABLE.writeValue(data); + } + }; REPL.startJoyPackets = () => JOY.startJoyPackets(); REPL.stopJoyPackets = () => JOY.stopJoyPackets(); } diff --git a/js/repl.js b/js/repl.js index f417f75..3e50523 100644 --- a/js/repl.js +++ b/js/repl.js @@ -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){ @@ -421,6 +424,7 @@ class ReplJS{ 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(); @@ -442,6 +446,7 @@ class ReplJS{ //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"); @@ -1853,8 +1858,15 @@ class ReplJS{ //console.log('Getting RX Characteristic...'); return this.btService.getCharacteristic(this.RX_CHARACTERISTIC_UUID); // Now you can use the characteristic to send data - }) .then (characteristic => { + + }) .then(characteristic => { + //console.log('Connected to TX Characteristic'); this.READBLE = characteristic; + //console.log('Getting DATA Characteristic...'); + return this.btService.getCharacteristic(this.DATA_CHARACTERISTIC_UUID); + // Now you can use the characteristic to send data + }).then (characteristic => { + this.DATABLE = characteristic; //this.READBLE.addEventListener('characteristicvaluechanged', this.readloopBLE); this.READBLE.startNotifications(); this.BLE_DEVICE.addEventListener('gattserverdisconnected', this.bleDisconnect); From e832b957769c7cf1f96e3a31548b77d22d07b2c7 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Thu, 1 May 2025 23:35:22 -0400 Subject: [PATCH 02/49] updated gamepad buttons Updated to map the buttons correctly. --- js/joystick_wrapper.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index 62c602c..3fbaa4d 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -271,11 +271,11 @@ class Joystick{ // Assuming at least 6 Buttons this.joysticksArray[this.bA] = gamepad.buttons[0].value; - this.joysticksArray[this.bB] = gamepad.buttons[0].value; - this.joysticksArray[this.bX] = gamepad.buttons[0].value; - this.joysticksArray[this.bY] = gamepad.buttons[0].value; - this.joysticksArray[this.bL] = gamepad.buttons[0].value; - this.joysticksArray[this.bR] = gamepad.buttons[0].value; + this.joysticksArray[this.bB] = gamepad.buttons[1].value; + this.joysticksArray[this.bX] = gamepad.buttons[2].value; + this.joysticksArray[this.bY] = gamepad.buttons[3].value; + this.joysticksArray[this.bL] = gamepad.buttons[4].value; + this.joysticksArray[this.bR] = gamepad.buttons[5].value; } } From 4067d9d74f312176101f806449038138934ba73e Mon Sep 17 00:00:00 2001 From: fgrossman Date: Thu, 1 May 2025 23:35:53 -0400 Subject: [PATCH 03/49] Update repl.js Identify hex values for vendor ID and product ID --- js/repl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/repl.js b/js/repl.js index 3e50523..ece8703 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 From 2e28c79fdf41463d70ec8fa0a317ea81ac591c2e Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 3 May 2025 11:45:02 -0400 Subject: [PATCH 04/49] Temp fix Always send the joystick data each time --- js/joystick_wrapper.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index 3fbaa4d..6d5cbd5 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -141,7 +141,8 @@ class Joystick{ 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) { + //BUGBUG: temp sending packets 0 - 3 (joysticks ) always + if (Math.abs(current[i] - last[i]) > tolerance || i < 4) { changes.push(i); // byte representing the array index changes.push(this.quantizeFloat(current[i])); // byte representing the new value } From 46873a86d3497f231eeaea839b5a99d7849508ab Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 20:23:13 -0400 Subject: [PATCH 05/49] Fixed re-entry bugs Fixed sending more packets before the last ones finished. --- js/joystick_wrapper.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index 6d5cbd5..bab1d72 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -63,6 +63,7 @@ class Joystick{ listening = false; sendPacket = false; + sendingPacket = false; controllerIndex = 0; @@ -114,6 +115,7 @@ class Joystick{ this.lastsentArray = this.joysticksArray.slice(); this.startListening(); this.listening = true; + this.sendingPacket = false; this.intervalID = setInterval(this.sendAPacket, 60); } @@ -142,7 +144,7 @@ class Joystick{ for (let i = 0; i < current.length; i++) { // Only consider sending a change if the difference exceeds the tolerance //BUGBUG: temp sending packets 0 - 3 (joysticks ) always - if (Math.abs(current[i] - last[i]) > tolerance || i < 4) { + 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 } @@ -155,7 +157,9 @@ class Joystick{ } async sendAPacket(){ + if(this.sendingPacket) return; if(this.sendPacket){ + this.sendingPacket = true; //if joystick then update the status before sending this.updateStatus(); const sending = this.getChangedBytes(this.joysticksArray, this.lastsentArray); @@ -163,6 +167,7 @@ class Joystick{ await this.writeToDevice(sending); //(JSON.stringify(this.joysticks) + '\r'); } this.lastsentArray = this.joysticksArray.slice(); + this.sendingPacket = false; } } From 309a0443beccb497e39c792ea4875f5e543dc7d0 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 20:23:56 -0400 Subject: [PATCH 06/49] Switch to write with response Was using just writeValue that has an optional response. --- js/main.js | 8 ++++++-- js/repl.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/js/main.js b/js/main.js index 22f7a99..52463e2 100644 --- a/js/main.js +++ b/js/main.js @@ -612,9 +612,13 @@ 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){ - REPL.DATABLE.writeValue(data); + try{ + await REPL.DATABLE.writeValueWithResponse(data); + }catch{ + console.log("error writing DATABLE data"); + } } }; REPL.startJoyPackets = () => JOY.startJoyPackets(); diff --git a/js/repl.js b/js/repl.js index ece8703..e1a0c9b 100644 --- a/js/repl.js +++ b/js/repl.js @@ -514,7 +514,7 @@ 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); } From 8bd784095094db6d808197a3c97d6f1e44beb336 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 20:24:52 -0400 Subject: [PATCH 07/49] Added gamepad blocks Added blocks for the gamepads --- js/xrp_blockly_toolbox.js | 15 +++++++++++++++ js/xrp_blocks.js | 28 ++++++++++++++++++++++++++++ js/xrp_blocks_python.js | 19 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/js/xrp_blockly_toolbox.js b/js/xrp_blockly_toolbox.js index 694e28d..d55c53a 100644 --- a/js/xrp_blockly_toolbox.js +++ b/js/xrp_blockly_toolbox.js @@ -255,6 +255,21 @@ var baseToolbox = { "blockxml": "\n\nxrp_1\n\n\n \n\n\n\n\n", }, ] + }, + { + "kind": "CATEGORY", + "name": "Gamepad", + "colour": "#5b99a5", // turquoise + "contents": [ + { + "kind": "BLOCK", + "type": "xrp_gp_get_value" + }, + { + "kind": "BLOCK", + "type": "xrp_gp_button_pressed" + }, + ] }, { "kind": "CATEGORY", diff --git a/js/xrp_blocks.js b/js/xrp_blocks.js index 20a66ab..2e1bd9a 100644 --- a/js/xrp_blocks.js +++ b/js/xrp_blocks.js @@ -565,6 +565,34 @@ 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(352); // 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"]]), "GPBUTTON") + .appendField("Pressed") + this.setOutput(true, null); + this.setColour(352); // crimson + this.setTooltip("Check to see if a gamepad button is pressed"); + this.setHelpUrl(""); + } +}; + + // Logic Blockly.Blocks['xrp_sleep'] = { init: function () { diff --git a/js/xrp_blocks_python.js b/js/xrp_blocks_python.js index bf23956..1574fef 100644 --- a/js/xrp_blocks_python.js +++ b/js/xrp_blocks_python.js @@ -341,6 +341,25 @@ 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.joystick import *'; + PY.definitions_[`gamepad_setup`] = `joy = Joystick.get_default_joystick()`; + PY.definitions_['gamepad_init'] = `joy.startBluetoothJoystick()`; + var value = block.getFieldValue("GPVALUE"); + var code = `joy.getJoystickValue(joy.${value})`; + return [code , Blockly.Python.ORDER_NONE]; +}; + +Blockly.Python['xrp_gp_button_pressed'] = function (block) { + PY.definitions_['import_gamepad'] = 'from XRPLib.joystick import *'; + PY.definitions_[`gamepad_setup`] = `joy = Joystick.get_default_joystick()`; + PY.definitions_['gamepad_init'] = `joy.startBluetoothJoystick()`; + var value = block.getFieldValue("GPBUTTON"); + var code = `joy.isJoystickButtonPressed(joy.${value})`; + return [code , Blockly.Python.ORDER_NONE]; +}; //Logic Blockly.Python['xrp_sleep'] = function (block) { From 5b2686fd6cb9f00c63c83de576ed3264034e373f Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 20:26:07 -0400 Subject: [PATCH 08/49] Update for Data characteristics Added a send and receive characteristics for just data. joystick and dashboard --- lib/ble/ble_uart_peripheral.py | 39 ++++++++++++++++++++++++++++++---- lib/ble/blerepl.py | 3 +++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/lib/ble/ble_uart_peripheral.py b/lib/ble/ble_uart_peripheral.py index 611d685..8ce09c2 100644 --- a/lib/ble/ble_uart_peripheral.py +++ b/lib/ble/ble_uart_peripheral.py @@ -17,6 +17,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,9 +30,21 @@ 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,), ) class BLEUART: @@ -39,13 +52,14 @@ 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): @@ -70,10 +84,16 @@ def _irq(self, event, data): self._rx_buffer = bytearray() import machine machine.reset() + 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 +110,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 +144,4 @@ def _append(adv_type, value): _append(_ADV_TYPE_NAME, name) _append(_ADV_TYPE_APPEARANCE, struct.pack(" Date: Sat, 17 May 2025 20:26:24 -0400 Subject: [PATCH 09/49] Create joystick.py Joystick handling for XRPLib --- lib/XRPLib/joystick.py | 68 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 lib/XRPLib/joystick.py diff --git a/lib/XRPLib/joystick.py b/lib/XRPLib/joystick.py new file mode 100644 index 0000000..48cafd8 --- /dev/null +++ b/lib/XRPLib/joystick.py @@ -0,0 +1,68 @@ +from ble.blerepl import uart +import sys +from micropython import const + +class Joystick: + + _DEFAULT_JOYSTICK_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) + + joyData = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0, + 0, + 0, + 0, + 0, + 0] + + @classmethod + def get_default_joystick(cls): + """ + Get the default XRP bluetooth joystick instance. This is a singleton, so only one instance of the reflectance sensor will ever exist. + """ + if cls._DEFAULT_JOYSTICK_INSTANCE is None: + cls._DEFAULT_JOYSTICK_INSTANCE = cls() + return cls._DEFAULT_JOYSTICK_INSTANCE + + def __init__(self): + """ + """ + + def startBluetoothJoystick(self): + 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 stopBluetoothJoystick(self): + sys.stdout.write(chr(27)) + sys.stdout.write(chr(102)) + + def getJoystickValue(self, index): + return -self.joyData[index] #returning the negative to make normal for user + + def isJoystickButtonPressed(self, index): + 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) + + From 2f8653b95fd27e52412e822489d0004f8c03deeb Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 22:05:34 -0400 Subject: [PATCH 10/49] Fix for github relative pages This will include any subdirectories we are executing from when fetching files from the server. This would fix staging as well. --- js/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/main.js b/js/main.js index 52463e2..21543e7 100644 --- a/js/main.js +++ b/js/main.js @@ -1251,8 +1251,10 @@ async function dialogMessage(message){ await UIkit.modal(elm).show(); } +const BASE = window.location.pathname.replace(/\/$/,''); + async function downloadFile(filePath) { - let response = await fetch(filePath); + let response = await fetch(BASE + filePath); if(response.status != 200) { throw new Error("Server Error"); From 8b9bb80d22e9abaf7a45a6769c21c9e3a0fbe8c1 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 22:10:19 -0400 Subject: [PATCH 11/49] need an / when not empty --- js/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/main.js b/js/main.js index 21543e7..8d57830 100644 --- a/js/main.js +++ b/js/main.js @@ -1252,6 +1252,9 @@ async function dialogMessage(message){ } const BASE = window.location.pathname.replace(/\/$/,''); +if(BASE != ""){ + BASE += '/' +} async function downloadFile(filePath) { let response = await fetch(BASE + filePath); From 0b952aa7bbb8a24522e4689545963d599b471cef Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 22:12:37 -0400 Subject: [PATCH 12/49] const error --- js/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/main.js b/js/main.js index 8d57830..02946fe 100644 --- a/js/main.js +++ b/js/main.js @@ -1251,7 +1251,7 @@ async function dialogMessage(message){ await UIkit.modal(elm).show(); } -const BASE = window.location.pathname.replace(/\/$/,''); +let BASE = window.location.pathname.replace(/\/$/,''); if(BASE != ""){ BASE += '/' } From b009e5f336b898b9c82da3829deb40546e9eee20 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 22:24:55 -0400 Subject: [PATCH 13/49] Don't need jekyll for github pages It kills trying to fetch __init__.py --- .nojekyll | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .nojekyll diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 From 410c3016a4a42ed5a1e068451ed404453a670fae Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 22:40:54 -0400 Subject: [PATCH 14/49] Include the new joystick file And changed to version 2.02 --- lib/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/package.json b/lib/package.json index edab641..eb6cee7 100644 --- a/lib/package.json +++ b/lib/package.json @@ -9,6 +9,7 @@ ["XRPLib/encoder.py", "github:Open-STEM/XRP_Micropython/XRPLib/encoder.py"], ["XRPLib/imu_defs.py", "github:Open-STEM/XRP_Micropython/XRPLib/imu_defs.py"], ["XRPLib/imu.py", "github:Open-STEM/XRP_Micropython/XRPLib/imu.py"], + ["XRPLib/joystick.py", "github:Open-STEM/XRP_Micropython/XRPLib/joystick.py"], ["XRPLib/motor_group.py", "github:Open-STEM/XRP_Micropython/XRPLib/motor_group.py"], ["XRPLib/motor.py", "github:Open-STEM/XRP_Micropython/XRPLib/motor.py"], ["XRPLib/pid.py", "github:Open-STEM/XRP_Micropython/XRPLib/pid.py"], @@ -28,5 +29,5 @@ "deps": [ ["github:pimoroni/phew", "latest"] ], - "version": "2.0.1" + "version": "2.0.2" } From 47ff2de7215dadfeda919445dc9d2bb7b83c95f9 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 17 May 2025 22:51:44 -0400 Subject: [PATCH 15/49] Reset XRP after update Forces a reload of the libraries --- js/repl.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/repl.js b/js/repl.js index e1a0c9b..64d5c84 100644 --- a/js/repl.js +++ b/js/repl.js @@ -1515,6 +1515,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() { From 6dcad52b63352e23de1a7f9fe77b50b39e12ecc0 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 21 May 2025 18:50:37 -0400 Subject: [PATCH 16/49] Added more buttons and new color Added trigger, back, start, and D-Pad buttons. Plus changed the color for gamepad blocks. --- js/joystick_wrapper.js | 57 ++++++++++++++++++++++++++++++--------- js/xrp_blockly_toolbox.js | 2 +- js/xrp_blocks.js | 8 +++--- lib/XRPLib/joystick.py | 15 ++++++----- 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index bab1d72..4c9af28 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -19,18 +19,6 @@ class Joystick{ lastsentArray = []; - 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 - } //array indexes x1 = 0; @@ -43,6 +31,14 @@ class Joystick{ 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'; @@ -59,6 +55,10 @@ class Joystick{ buttonY = 'Digit4'; bumperL = 'Digit5'; bumperR = 'Digit6'; + triggerL = 'Digit7'; + triggerR = 'Digit8'; + back = 'Digit9'; + start = 'Digit0'; listening = false; @@ -143,7 +143,6 @@ class Joystick{ const changes = []; for (let i = 0; i < current.length; i++) { // Only consider sending a change if the difference exceeds the tolerance - //BUGBUG: temp sending packets 0 - 3 (joysticks ) always 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 @@ -215,6 +214,18 @@ class Joystick{ case this.bumperR: 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){ @@ -261,6 +272,18 @@ class Joystick{ case this.bumperR: 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; } } @@ -282,6 +305,14 @@ class Joystick{ this.joysticksArray[this.bY] = gamepad.buttons[3].value; this.joysticksArray[this.bL] = gamepad.buttons[4].value; this.joysticksArray[this.bR] = gamepad.buttons[5].value; + this.joysticksArray[this.tL] = gamepad.buttons[6].value; + this.joysticksArray[this.tR] = gamepad.buttons[7].value; + this.joysticksArray[this.bK] = gamepad.buttons[8].value; + this.joysticksArray[this.sT] = gamepad.buttons[9].value; + this.joysticksArray[this.dU] = gamepad.buttons[12].value; + this.joysticksArray[this.dD] = gamepad.buttons[13].value; + this.joysticksArray[this.dL] = gamepad.buttons[14].value; + this.joysticksArray[this.dR] = gamepad.buttons[15].value; } } diff --git a/js/xrp_blockly_toolbox.js b/js/xrp_blockly_toolbox.js index d55c53a..f17f7ef 100644 --- a/js/xrp_blockly_toolbox.js +++ b/js/xrp_blockly_toolbox.js @@ -259,7 +259,7 @@ var baseToolbox = { { "kind": "CATEGORY", "name": "Gamepad", - "colour": "#5b99a5", // turquoise + "colour": "#ff9248", // turquoise "contents": [ { "kind": "BLOCK", diff --git a/js/xrp_blocks.js b/js/xrp_blocks.js index 2e1bd9a..80aa4f6 100644 --- a/js/xrp_blocks.js +++ b/js/xrp_blocks.js @@ -573,7 +573,7 @@ Blockly.Blocks['xrp_gp_get_value'] = { .appendField("Joystick:") .appendField(new Blockly.FieldDropdown([["X1", "X1"], ["X2", "X2"], ["Y1", "Y1"], ["Y2", "Y2"]]), "GPVALUE") this.setOutput(true, null); - this.setColour(352); // crimson + this.setColour("#ff9248"); // crimson this.setTooltip("Get the value of a gamepad joystick"); this.setHelpUrl(""); } @@ -583,10 +583,12 @@ 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"]]), "GPBUTTON") + .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(352); // crimson + this.setColour("#ff9248"); // crimson this.setTooltip("Check to see if a gamepad button is pressed"); this.setHelpUrl(""); } diff --git a/lib/XRPLib/joystick.py b/lib/XRPLib/joystick.py index 48cafd8..090f62a 100644 --- a/lib/XRPLib/joystick.py +++ b/lib/XRPLib/joystick.py @@ -16,18 +16,21 @@ class Joystick: 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,0,0,0,0,0,0] @classmethod def get_default_joystick(cls): From 20cd93a1efbdcc8c0f5dc506b2ce0d7f899ba8b4 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Mon, 26 May 2025 17:34:10 -0400 Subject: [PATCH 17/49] Do some error checking Don't try to return a button or joystick that the gamepad does not have --- js/joystick_wrapper.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index 4c9af28..80d7178 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -293,27 +293,27 @@ class Joystick{ const gamepad = gamepads[this.controllerIndex]; if (gamepad) { // Assuming at least 4 axis - this.joysticksArray[this.x1] = gamepad.axes[0]; - this.joysticksArray[this.y1] = gamepad.axes[1]; - this.joysticksArray[this.x2] = gamepad.axes[2]; - this.joysticksArray[this.y2] = gamepad.axes[3]; - - // Assuming at least 6 Buttons - this.joysticksArray[this.bA] = gamepad.buttons[0].value; - this.joysticksArray[this.bB] = gamepad.buttons[1].value; - this.joysticksArray[this.bX] = gamepad.buttons[2].value; - this.joysticksArray[this.bY] = gamepad.buttons[3].value; - this.joysticksArray[this.bL] = gamepad.buttons[4].value; - this.joysticksArray[this.bR] = gamepad.buttons[5].value; - this.joysticksArray[this.tL] = gamepad.buttons[6].value; - this.joysticksArray[this.tR] = gamepad.buttons[7].value; - this.joysticksArray[this.bK] = gamepad.buttons[8].value; - this.joysticksArray[this.sT] = gamepad.buttons[9].value; - this.joysticksArray[this.dU] = gamepad.buttons[12].value; - this.joysticksArray[this.dD] = gamepad.buttons[13].value; - this.joysticksArray[this.dL] = gamepad.buttons[14].value; - this.joysticksArray[this.dR] = gamepad.buttons[15].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; } } } From d2f834f4f8317e99ba7f00a4f20a39f31729be5f Mon Sep 17 00:00:00 2001 From: fgrossman Date: Mon, 26 May 2025 17:35:28 -0400 Subject: [PATCH 18/49] Changed API The joystick api turned to the gamepad api and calls were shortened. --- js/xrp_blocks_python.js | 14 ++++++-------- lib/XRPLib/{joystick.py => gamepad.py} | 22 ++++++++++++---------- lib/package.json | 4 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) rename lib/XRPLib/{joystick.py => gamepad.py} (77%) diff --git a/js/xrp_blocks_python.js b/js/xrp_blocks_python.js index 1574fef..ef2515a 100644 --- a/js/xrp_blocks_python.js +++ b/js/xrp_blocks_python.js @@ -344,20 +344,18 @@ Blockly.Python['xrp_ws_start_server'] = function (block) { // Gamepad Blockly.Python['xrp_gp_get_value'] = function (block) { - PY.definitions_['import_gamepad'] = 'from XRPLib.joystick import *'; - PY.definitions_[`gamepad_setup`] = `joy = Joystick.get_default_joystick()`; - PY.definitions_['gamepad_init'] = `joy.startBluetoothJoystick()`; + PY.definitions_['import_gamepad'] = 'from XRPLib.gamepad import *'; + PY.definitions_[`gamepad_setup`] = `gp = Gamepad.get_default_gamepad()`; var value = block.getFieldValue("GPVALUE"); - var code = `joy.getJoystickValue(joy.${value})`; + 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.joystick import *'; - PY.definitions_[`gamepad_setup`] = `joy = Joystick.get_default_joystick()`; - PY.definitions_['gamepad_init'] = `joy.startBluetoothJoystick()`; + PY.definitions_['import_gamepad'] = 'from XRPLib.gamepad import *'; + PY.definitions_[`gamepad_setup`] = `gp = Gamepad.get_default_gamepad()`; var value = block.getFieldValue("GPBUTTON"); - var code = `joy.isJoystickButtonPressed(joy.${value})`; + var code = `gp.is_button_pressed(gp.${value})`; return [code , Blockly.Python.ORDER_NONE]; }; diff --git a/lib/XRPLib/joystick.py b/lib/XRPLib/gamepad.py similarity index 77% rename from lib/XRPLib/joystick.py rename to lib/XRPLib/gamepad.py index 090f62a..188fe18 100644 --- a/lib/XRPLib/joystick.py +++ b/lib/XRPLib/gamepad.py @@ -2,9 +2,9 @@ import sys from micropython import const -class Joystick: +class Gamepad: - _DEFAULT_JOYSTICK_INSTANCE = None + _DEFAULT_GAMEPAD_INSTANCE = None X1 = const(0) Y1 = const(1) @@ -33,19 +33,20 @@ class Joystick: 0,0,0,0,0,0,0,0,0,0,0,0,0,0] @classmethod - def get_default_joystick(cls): + def get_default_gamepad(cls): """ Get the default XRP bluetooth joystick instance. This is a singleton, so only one instance of the reflectance sensor will ever exist. """ - if cls._DEFAULT_JOYSTICK_INSTANCE is None: - cls._DEFAULT_JOYSTICK_INSTANCE = cls() - return cls._DEFAULT_JOYSTICK_INSTANCE + 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): """ """ - def startBluetoothJoystick(self): + def start(self): for i in range(len(self.joyData)): self.joyData[i] = 0.0 uart.set_data_callback(self._data_callback) @@ -53,14 +54,15 @@ def startBluetoothJoystick(self): sys.stdout.write(chr(101)) - def stopBluetoothJoystick(self): + def stop(self): + uart.clear_data_callback() sys.stdout.write(chr(27)) sys.stdout.write(chr(102)) - def getJoystickValue(self, index): + def get_value(self, index): return -self.joyData[index] #returning the negative to make normal for user - def isJoystickButtonPressed(self, index): + def is_button_pressed(self, index): return self.joyData[index] > 0 def _data_callback(self, data): diff --git a/lib/package.json b/lib/package.json index eb6cee7..4d7e560 100644 --- a/lib/package.json +++ b/lib/package.json @@ -7,9 +7,9 @@ ["XRPLib/differential_drive.py", "github:Open-STEM/XRP_Micropython/XRPLib/differential_drive.py"], ["XRPLib/encoded_motor.py", "github:Open-STEM/XRP_Micropython/XRPLib/encoded_motor.py"], ["XRPLib/encoder.py", "github:Open-STEM/XRP_Micropython/XRPLib/encoder.py"], + ["XRPLib/gamepad.py", "github:Open-STEM/XRP_Micropython/XRPLib/gamepad.py"], ["XRPLib/imu_defs.py", "github:Open-STEM/XRP_Micropython/XRPLib/imu_defs.py"], ["XRPLib/imu.py", "github:Open-STEM/XRP_Micropython/XRPLib/imu.py"], - ["XRPLib/joystick.py", "github:Open-STEM/XRP_Micropython/XRPLib/joystick.py"], ["XRPLib/motor_group.py", "github:Open-STEM/XRP_Micropython/XRPLib/motor_group.py"], ["XRPLib/motor.py", "github:Open-STEM/XRP_Micropython/XRPLib/motor.py"], ["XRPLib/pid.py", "github:Open-STEM/XRP_Micropython/XRPLib/pid.py"], @@ -29,5 +29,5 @@ "deps": [ ["github:pimoroni/phew", "latest"] ], - "version": "2.0.2" + "version": "2.1.0" } From 20e113874790f622ef6a949a7fe2ddb88d4be237 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Mon, 26 May 2025 17:38:13 -0400 Subject: [PATCH 19/49] Don't restart program when rebooting Changed the main program to only not run the last program when a STOP was pushed for bluetooth. Also switched to just writeValue when writing STOP since the timeout in Windows is too long with writeValueWithResonse --- js/repl.js | 28 +++++++++++++++++++--------- lib/ble/ble_uart_peripheral.py | 4 ++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/js/repl.js b/js/repl.js index 64d5c84..46855d1 100644 --- a/js/repl.js +++ b/js/repl.js @@ -521,6 +521,11 @@ class ReplJS{ }); } + //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){ + await this.WRITEBLE.writeValue(this.str2ab(str)); + } + async softReset(){ return; this.startReaduntil("MPY: soft reboot"); @@ -1251,21 +1256,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" + @@ -1602,8 +1607,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:"); @@ -1637,7 +1642,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:" + "
  • Unplug the XRP checking the cable on both ends
  • " + @@ -1723,6 +1732,7 @@ class ReplJS{ await this.getOnBoardFSTree(); this.onConnect(); } + else{return;} this.LAST_RUN = undefined; this.BUSY = false; diff --git a/lib/ble/ble_uart_peripheral.py b/lib/ble/ble_uart_peripheral.py index 8ce09c2..01d3a95 100644 --- a/lib/ble/ble_uart_peripheral.py +++ b/lib/ble/ble_uart_peripheral.py @@ -82,6 +82,10 @@ 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() + # 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') import machine machine.reset() elif conn_handle in self._connections and value_handle == self._data_rx_handle: From 83ef048f7b78b7eaaafde1c0c2e6c2ece64070ac Mon Sep 17 00:00:00 2001 From: fgrossman Date: Mon, 26 May 2025 17:38:46 -0400 Subject: [PATCH 20/49] Stop the flow of gamepad data on a reset Calling stop on gamepad when a reset happens. --- lib/XRPLib/resetbot.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/XRPLib/resetbot.py b/lib/XRPLib/resetbot.py index de80714..d9d2545 100644 --- a/lib/XRPLib/resetbot.py +++ b/lib/XRPLib/resetbot.py @@ -33,11 +33,20 @@ 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() From 1c038b37b28807c179a266f97ac06b4a1612f613 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Mon, 26 May 2025 18:11:11 -0400 Subject: [PATCH 21/49] Try WithoutResponse on windows --- js/repl.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/js/repl.js b/js/repl.js index 46855d1..ef23b6b 100644 --- a/js/repl.js +++ b/js/repl.js @@ -523,7 +523,13 @@ class ReplJS{ //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){ - await this.WRITEBLE.writeValue(this.str2ab(str)); + try{ + await this.WRITEBLE.writeValueWithoutResponse(this.str2ab(str)); + } catch(error){ + console.error('ble stop write failed:', error); + //do nothing we expected an error + } + } async softReset(){ From 9c65fcf3d0de15285c1cb33bb8a42fe5a71ef3cb Mon Sep 17 00:00:00 2001 From: fgrossman Date: Mon, 26 May 2025 18:26:29 -0400 Subject: [PATCH 22/49] Don't wait for response on STOP When doing a STOPs and it is BLE then don't wait for the response as the WIndows timeout is long --- js/repl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/repl.js b/js/repl.js index ef23b6b..dd0272e 100644 --- a/js/repl.js +++ b/js/repl.js @@ -524,7 +524,7 @@ class ReplJS{ //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.writeValueWithoutResponse(this.str2ab(str)); + this.WRITEBLE.writeValue(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 @@ -1910,7 +1910,7 @@ class ReplJS{ 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); + await this.writeSTOPtoBleDevice(this.BLE_STOP_MSG); return; } From 61846df90fc360bef33c9de28c458a6939c08e19 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Mon, 26 May 2025 22:32:45 -0400 Subject: [PATCH 23/49] Windows test to cause discounnect Try it --- js/repl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/repl.js b/js/repl.js index dd0272e..0961c49 100644 --- a/js/repl.js +++ b/js/repl.js @@ -524,12 +524,12 @@ class ReplJS{ //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{ - this.WRITEBLE.writeValue(this.str2ab(str)); //don't wait since the Windows timeout is long + 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 } - + this.BLE_DEVICE.gatt.disconnect(); } async softReset(){ From 9b9ecb31ee33e1e5a4012d2ef2564cd2a1a21313 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Tue, 27 May 2025 14:28:11 -0400 Subject: [PATCH 24/49] Try a timeout and then discounnect --- js/repl.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/js/repl.js b/js/repl.js index 0961c49..da59405 100644 --- a/js/repl.js +++ b/js/repl.js @@ -24,6 +24,8 @@ class ReplJS{ this.BLE_DATA = null; this.BLE_DATA_RESOLVE = null; this.BLE_STOP_MSG = "##XRPSTOP##" + this.disconnectHappened = false; + // UUIDs for standard NORDIC UART service and characteristics @@ -35,7 +37,7 @@ class ReplJS{ this.XRP_SEND_BLOCK_SIZE = 250; // wired can handle 255 bytes, but BLE 5.0 is only 250 // Set true so most terminal output gets passed to javascript terminal - this.DEBUG_CONSOLE_ON = false; + this.DEBUG_CONSOLE_ON = true; this.COLLECT_RAW_DATA = false; this.COLLECTED_RAW_DATA = []; @@ -421,6 +423,7 @@ class ReplJS{ bleDisconnect(){ if(REPL.DEBUG_CONSOLE_ON) console.log("BLE Disconnected"); + REPL.disconnectHappened = true; REPL.BLE_DISCONNECT_TIME = Date.now(); REPL.WRITEBLE = undefined; REPL.READBLE = undefined; @@ -529,7 +532,11 @@ class ReplJS{ console.error('ble stop write failed:', error); //do nothing we expected an error } - this.BLE_DEVICE.gatt.disconnect(); + this.disconnectHappened = false; + await new Promise(resolve => setTimeout(resolve, 2000)); + if(! this.disconnectHappened){ + this.BLE_DEVICE.gatt.disconnect(); //the disconnect didn't happen so create a disconnect + } } async softReset(){ From 82291a0d0ae48cc0d7c0324826ae00c1399497c0 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 28 May 2025 01:17:16 -0400 Subject: [PATCH 25/49] If getPrimaryService fails try again For a few times --- js/repl.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/js/repl.js b/js/repl.js index da59405..eb84d7c 100644 --- a/js/repl.js +++ b/js/repl.js @@ -445,7 +445,19 @@ class ReplJS{ 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); + let attempts = 5; + 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, 50)); + } 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); From 82fef86445d5a6b166a4c5f59419735a06797d9e Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 28 May 2025 01:23:23 -0400 Subject: [PATCH 26/49] longer time out --- js/repl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/repl.js b/js/repl.js index eb84d7c..ca5a8e8 100644 --- a/js/repl.js +++ b/js/repl.js @@ -452,7 +452,7 @@ class ReplJS{ break; } catch (e) { if (/No Services found/.test(e.message) && i < attempts - 1) { - await new Promise(r => setTimeout(r, 50)); + await new Promise(r => setTimeout(r, 200)); } else { throw e; } From d98379c3e29efc3dbb5fe208d39702e1130556cb Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 28 May 2025 01:44:53 -0400 Subject: [PATCH 27/49] Another delay test --- js/repl.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/repl.js b/js/repl.js index ca5a8e8..575228e 100644 --- a/js/repl.js +++ b/js/repl.js @@ -445,6 +445,8 @@ class ReplJS{ 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(); + await new Promise(r => setTimeout(r, 300)); + let attempts = 5; for (let i = 0; i < attempts; i++) { try { From 39f831857ffd58d314c2b4428a656f0deadeca7b Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 28 May 2025 22:06:22 -0400 Subject: [PATCH 28/49] Fixes for ble connections Use common routines. Added some waits and retries. As well now display a waiting dialog when connecting or stopping with ble since it takes a lot more time than with a USB cable. --- images/logo_x_rotate_in_place.gif | Bin 0 -> 71877 bytes index.html | 7 ++ js/repl.js | 197 ++++++++++-------------------- 3 files changed, 71 insertions(+), 133 deletions(-) create mode 100644 images/logo_x_rotate_in_place.gif diff --git a/images/logo_x_rotate_in_place.gif b/images/logo_x_rotate_in_place.gif new file mode 100644 index 0000000000000000000000000000000000000000..92c38677683bc42c4fdece01b7bd1ab89c075a51 GIT binary patch literal 71877 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

    kumf&_zv#QwVRD}e< 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 QpGKYf;DXYs0|x{EI|d5HTmS$7 literal 0 HcmV?d00001 diff --git a/index.html b/index.html index b908fed..b7aed73 100644 --- a/index.html +++ b/index.html @@ -264,6 +264,13 @@

    +
    +
    +

    +
    +
    + +
    diff --git a/js/repl.js b/js/repl.js index 575228e..910c34f 100644 --- a/js/repl.js +++ b/js/repl.js @@ -37,7 +37,7 @@ class ReplJS{ this.XRP_SEND_BLOCK_SIZE = 250; // wired can handle 255 bytes, but BLE 5.0 is only 250 // Set true so most terminal output gets passed to javascript terminal - this.DEBUG_CONSOLE_ON = true; + this.DEBUG_CONSOLE_ON = false; this.COLLECT_RAW_DATA = false; this.COLLECTED_RAW_DATA = []; @@ -441,43 +441,49 @@ class ReplJS{ 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(); - await new Promise(r => setTimeout(r, 300)); - - let attempts = 5; - 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, 200)); - } 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); - 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{ + 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 = 5; + 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, 200)); + } 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); + window.alertMessage("Error connecting to the XRP. Please reset the XRP and then refresh this page and try again"); + this.BLE_DEVICE = undefined; + return false; + } + } + connectWithTimeout(device, timeoutMs) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { @@ -546,11 +552,6 @@ class ReplJS{ console.error('ble stop write failed:', error); //do nothing we expected an error } - this.disconnectHappened = false; - await new Promise(resolve => setTimeout(resolve, 2000)); - if(! this.disconnectHappened){ - this.BLE_DEVICE.gatt.disconnect(); //the disconnect didn't happen so create a disconnect - } } async softReset(){ @@ -1421,7 +1422,7 @@ class ReplJS{ async checkIfNeedUpdate(){ //if no micropython on the XRP if(!this.HAS_MICROPYTHON){ - await this.showMicropythonUpdate(); + //await this.showMicropythonUpdate(); return; } @@ -1673,7 +1674,7 @@ class ReplJS{ return false; } this.HAS_MICROPYTHON = false; - + /* let ans = await window.confirmMessage("XRPCode is having problems connecting to this XRP.
    " + "Two Options:" + "
    • Unplug the XRP checking the cable on both ends
    • " + @@ -1681,6 +1682,7 @@ class ReplJS{ "
    • Click OK and then plug the XRP in again
    " + "
    Or click CANCEL and XRPCode will reinstall MicroPython onto the XRP") return ans; + */ } // do a softreset, but time out if no response @@ -1699,43 +1701,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"); @@ -1750,26 +1719,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(); - } - else{return;} - + 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){ @@ -1880,41 +1850,16 @@ 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 => { - //console.log('Connected to TX Characteristic'); - this.READBLE = characteristic; - //console.log('Getting DATA Characteristic...'); - return this.btService.getCharacteristic(this.DATA_CHARACTERISTIC_UUID); - // Now you can use the characteristic to send data - }).then (characteristic => { - this.DATABLE = characteristic; - //this.READBLE.addEventListener('characteristicvaluechanged', this.readloopBLE); - this.READBLE.startNotifications(); - this.BLE_DEVICE.addEventListener('gattserverdisconnected', this.bleDisconnect); - this.finishConnect(); }) .catch(error => { + window.alertMessage("*Error connecting to XRP. Please reset the XRP, then refresh this page and try again"); console.log('Error: ' + error); }); + 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"); @@ -1930,28 +1875,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.writeSTOPtoBleDevice(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. @@ -1962,6 +1892,7 @@ class ReplJS{ var cmd = "import XRPLib.resetbot\n" await this.writeUtilityCmdRaw(cmd, true, 1); await this.getToNormal(3); + */ } async disconnect(){ From 44823dec9912d3329c47d237842746c1e79cfcb1 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 28 May 2025 22:22:23 -0400 Subject: [PATCH 29/49] Adjust some of the timeouts To avoid connection errors. --- js/repl.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/js/repl.js b/js/repl.js index 910c34f..e48418a 100644 --- a/js/repl.js +++ b/js/repl.js @@ -451,17 +451,18 @@ class ReplJS{ async bleConnect(){ try{ - const server = await this.connectWithTimeout(this.BLE_DEVICE, 10000); //wait for 10seconds to see if it reconnects 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 = 5; + let attempts = 7; 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, 200)); + await new Promise(r => setTimeout(r, 300)); } else { throw e; } @@ -478,7 +479,7 @@ class ReplJS{ // Perform operations after successful connection } catch (error) { console.log('timed out: ', error); - window.alertMessage("Error connecting to the XRP. Please reset the XRP and then refresh this page and try again"); + window.alertMessage("Error connecting to the XRP. Please refresh this page and try again"); this.BLE_DEVICE = undefined; return false; } @@ -1852,7 +1853,7 @@ class ReplJS{ this.BLE_DEVICE = device; }) .catch(error => { - window.alertMessage("*Error connecting to XRP. Please reset the XRP, then refresh this page and try again"); + window.alertMessage("*Error connecting to XRP. Please refresh this page and try again"); console.log('Error: ' + error); }); document.getElementById("IdWaiting_TitleText").innerText = 'Connecting to XRP...'; From eb0ef66f877f1ba51ee3380fec30c5d539579194 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 28 May 2025 22:44:59 -0400 Subject: [PATCH 30/49] Small delay before resetart This avoids getting errors on the Browser side. --- lib/ble/ble_uart_peripheral.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/ble/ble_uart_peripheral.py b/lib/ble/ble_uart_peripheral.py index 01d3a95..d0ae10f 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 @@ -47,6 +48,12 @@ (_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 @@ -86,8 +93,8 @@ def _irq(self, event, data): FILE_PATH = '/lib/ble/isrunning' with open(FILE_PATH, 'r+b') as file: file.write(b'\x01') - import machine - machine.reset() + # 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: From 06f9c2071f809c5993e9d3503b4775a178d026a8 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 28 May 2025 22:45:23 -0400 Subject: [PATCH 31/49] Increment release number For change to the ble code --- lib/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/package.json b/lib/package.json index 4d7e560..c83fa5e 100644 --- a/lib/package.json +++ b/lib/package.json @@ -29,5 +29,5 @@ "deps": [ ["github:pimoroni/phew", "latest"] ], - "version": "2.1.0" + "version": "2.1.1" } From 6d939f0808f6e328df38481dbb0199dfd4b6b358 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Fri, 30 May 2025 13:45:32 -0400 Subject: [PATCH 32/49] took out a print that caused problems An IRQ event was happening that messed up the directory on WIndows 11. --- lib/ble/ble_uart_peripheral.py | 4 ++-- lib/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ble/ble_uart_peripheral.py b/lib/ble/ble_uart_peripheral.py index d0ae10f..d9da813 100644 --- a/lib/ble/ble_uart_peripheral.py +++ b/lib/ble/ble_uart_peripheral.py @@ -103,8 +103,8 @@ def _irq(self, event, data): elif event == _IRQ_GATTS_INDICATE_DONE: if self._handler: self._handler() - else: - print("IRQ Event Code: " + str(event)) + #else: + #print("IRQ Event Code: " + str(event)) def any(self): return len(self._rx_buffer) diff --git a/lib/package.json b/lib/package.json index c83fa5e..d7ea096 100644 --- a/lib/package.json +++ b/lib/package.json @@ -29,5 +29,5 @@ "deps": [ ["github:pimoroni/phew", "latest"] ], - "version": "2.1.1" + "version": "2.1.2" } From b71f333ba7113c27d35c781bb2d503921e5fdce9 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Fri, 30 May 2025 13:45:54 -0400 Subject: [PATCH 33/49] Cleaned up an unused variable C --- js/repl.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/repl.js b/js/repl.js index e48418a..41152c8 100644 --- a/js/repl.js +++ b/js/repl.js @@ -24,7 +24,6 @@ class ReplJS{ this.BLE_DATA = null; this.BLE_DATA_RESOLVE = null; this.BLE_STOP_MSG = "##XRPSTOP##" - this.disconnectHappened = false; @@ -423,7 +422,6 @@ class ReplJS{ bleDisconnect(){ if(REPL.DEBUG_CONSOLE_ON) console.log("BLE Disconnected"); - REPL.disconnectHappened = true; REPL.BLE_DISCONNECT_TIME = Date.now(); REPL.WRITEBLE = undefined; REPL.READBLE = undefined; From 321cdeb231604c21cfb044ee2cfed3f6e3575a43 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Fri, 30 May 2025 23:54:33 -0400 Subject: [PATCH 34/49] Updated to version 1.2.2 of XRPCode --- index.html | 54 +++++++++++++++++++++++++++--------------------------- js/main.js | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index b7aed73..b5a1e22 100644 --- a/index.html +++ b/index.html @@ -7,21 +7,21 @@ XRP Code Editor - + - + - - + + - - - - - - + + + + + + - + @@ -311,28 +311,28 @@

    - + - - + + - - + + - - - + + + - - - - - - - - + + + + + + + + diff --git a/js/main.js b/js/main.js index 02946fe..d331573 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 From e4c61c864bdabd8d95d82224ef66d09e429f1718 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Fri, 30 May 2025 23:54:54 -0400 Subject: [PATCH 35/49] Update CHANGELOG.txt For this release --- CHANGELOG.txt | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7aeafbd..537920a 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.2 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. +* This can also be done in Python. The API will be documented soon, for now you can use the blocks and then use the view menu to view the Python to get how the API works. +* We use the standard Web Gamepad support and recognize only one controller. If you want to know if your gamepad will work you can google 'web gamepad tester' there are a few out there that will let you see if your gamepad works. +* 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 buttons for the D-PAD. +* There is a current bug that very quick keyboard actions can be missed. Press the key again if a release was missed. + +## 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. + + # 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. From 29af815eb2d91624cef0bb270c51b72349470c76 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Fri, 30 May 2025 23:55:18 -0400 Subject: [PATCH 36/49] Added gamepad blocks example --- lib/XRPExamples/gamepad_example.blocks | 15 +++++++++++++++ lib/package.json | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 lib/XRPExamples/gamepad_example.blocks diff --git a/lib/XRPExamples/gamepad_example.blocks b/lib/XRPExamples/gamepad_example.blocks new file mode 100644 index 0000000..56aea84 --- /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-05-30 23:46:21] +##XRPBLOCKS {"blocks":{"languageVersion":0,"blocks":[{"type":"controls_whileUntil","id":"#/DDnbE2JxVsLQo`C`e^","x":-363,"y":16,"fields":{"MODE":"WHILE"},"inputs":{"BOOL":{"block":{"type":"logic_negate","id":"+k{^!vL*n}Km]oQ8u,ye","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/package.json b/lib/package.json index d7ea096..28316a4 100644 --- a/lib/package.json +++ b/lib/package.json @@ -24,7 +24,9 @@ ["XRPExamples/installation_verification.py", "github:Open-STEM/XRP_Micropython/XRPExamples/installation_verification.py"], ["XRPExamples/led_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/misc_examples.py"], ["XRPExamples/sensor_examples.py", "github:Open-STEM/XRP_Micropython/XRPExamples/sensor_examples.py"], - ["XRPExamples/webserver_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/webserver_example.py"] + ["XRPExamples/webserver_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/webserver_example.py"], + ["XRPExamples/gamepad_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/gamepad_example.py"] + ], "deps": [ ["github:pimoroni/phew", "latest"] From b7408f64b78ead9b53c98a5f1af7ccce1f31b04f Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 31 May 2025 00:10:59 -0400 Subject: [PATCH 37/49] it is a blocks program not py --- lib/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/package.json b/lib/package.json index 28316a4..7a2dc1c 100644 --- a/lib/package.json +++ b/lib/package.json @@ -25,7 +25,7 @@ ["XRPExamples/led_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/misc_examples.py"], ["XRPExamples/sensor_examples.py", "github:Open-STEM/XRP_Micropython/XRPExamples/sensor_examples.py"], ["XRPExamples/webserver_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/webserver_example.py"], - ["XRPExamples/gamepad_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/gamepad_example.py"] + ["XRPExamples/gamepad_example.blocks", "github:Open-STEM/XRP_Micropython/XRPExamples/gamepad_example.blocks"] ], "deps": [ From a038a3579e0f6df46e08eab093f34539a376c6e5 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 7 Jun 2025 19:42:03 -0400 Subject: [PATCH 38/49] Fixed sticky keys There were sticky keys for the joystick --- js/joystick_wrapper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/joystick_wrapper.js b/js/joystick_wrapper.js index 80d7178..b80dda0 100644 --- a/js/joystick_wrapper.js +++ b/js/joystick_wrapper.js @@ -163,9 +163,9 @@ class Joystick{ this.updateStatus(); const sending = this.getChangedBytes(this.joysticksArray, this.lastsentArray); if(sending[1] > 0){ - await this.writeToDevice(sending); //(JSON.stringify(this.joysticks) + '\r'); + this.lastsentArray = this.joysticksArray.slice(); + await this.writeToDevice(sending); } - this.lastsentArray = this.joysticksArray.slice(); this.sendingPacket = false; } } From 731b466d73c5b436757e603d993d1ebfe721b425 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 7 Jun 2025 19:42:41 -0400 Subject: [PATCH 39/49] Wait for message Without the await the Message was not showing before an exception happened. --- js/repl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/repl.js b/js/repl.js index 41152c8..b4be762 100644 --- a/js/repl.js +++ b/js/repl.js @@ -477,7 +477,7 @@ class ReplJS{ // Perform operations after successful connection } catch (error) { console.log('timed out: ', error); - window.alertMessage("Error connecting to the XRP. Please refresh this page and try again"); + await window.alertMessage("Error connecting to the XRP. Please refresh this page and try again"); this.BLE_DEVICE = undefined; return false; } From 174e7ded1efabde7405d55d772b6142c340e0935 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Wed, 18 Jun 2025 14:53:43 -0400 Subject: [PATCH 40/49] Added icon for gamepad attached It puts a gamepad icon on the menu bar when it notices a gamepad is attached. --- images/gamepad.png | Bin 0 -> 46869 bytes index.html | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 images/gamepad.png diff --git a/images/gamepad.png b/images/gamepad.png new file mode 100644 index 0000000000000000000000000000000000000000..e4455f697c5a087112aaf91b69f202c523423a88 GIT binary patch literal 46869 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 mpHJ}5H~Rl{3 -
    +
    -
    +
    @@ -217,6 +217,10 @@

+ + + + From d187c7263b9074c1469eb5d603a13db63409d67c Mon Sep 17 00:00:00 2001 From: fgrossman Date: Fri, 11 Jul 2025 19:27:28 -0400 Subject: [PATCH 43/49] ble connection tweeks --- js/repl.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/js/repl.js b/js/repl.js index b4be762..6f7a6de 100644 --- a/js/repl.js +++ b/js/repl.js @@ -420,7 +420,7 @@ class ReplJS{ } - bleDisconnect(){ + async bleDisconnect(){ if(REPL.DEBUG_CONSOLE_ON) console.log("BLE Disconnected"); REPL.BLE_DISCONNECT_TIME = Date.now(); REPL.WRITEBLE = undefined; @@ -434,6 +434,8 @@ 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, 300)); REPL.bleReconnect(); } @@ -453,14 +455,14 @@ class ReplJS{ 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 = 7; + 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, 300)); + await new Promise(r => setTimeout(r, 400)); } else { throw e; } @@ -1834,6 +1836,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.") @@ -1851,9 +1855,16 @@ class ReplJS{ this.BLE_DEVICE = device; }) .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(); From 987931b996975a8eb7b6c3106ee3bb3c7a1be1ab Mon Sep 17 00:00:00 2001 From: fgrossman Date: Fri, 11 Jul 2025 19:28:07 -0400 Subject: [PATCH 44/49] Turn off spinner When attached to serial this is the best spot to stop the STOP button spinner. --- js/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/main.js b/js/main.js index d331573..60ea006 100644 --- a/js/main.js +++ b/js/main.js @@ -1022,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."); From 1f9db18ab6f8646928c9e8cf8ea71dfe59075a5e Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 12 Jul 2025 11:45:43 -0400 Subject: [PATCH 45/49] Added repeat until User button block --- js/repl.js | 1 - js/xrp_blockly_toolbox.js | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/js/repl.js b/js/repl.js index 6f7a6de..288e7b8 100644 --- a/js/repl.js +++ b/js/repl.js @@ -26,7 +26,6 @@ class ReplJS{ this.BLE_STOP_MSG = "##XRPSTOP##" - // 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" diff --git a/js/xrp_blockly_toolbox.js b/js/xrp_blockly_toolbox.js index f17f7ef..a24744f 100644 --- a/js/xrp_blockly_toolbox.js +++ b/js/xrp_blockly_toolbox.js @@ -321,14 +321,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 ", From 4aa83a307bd570c1cd378cc5179a93c8df7f6706 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sat, 12 Jul 2025 13:37:44 -0400 Subject: [PATCH 46/49] Added a comment block --- js/xrp_blockly_toolbox.js | 14 +++++++++++--- js/xrp_blocks.js | 14 +++++++++++++- js/xrp_blocks_python.js | 5 +++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/js/xrp_blockly_toolbox.js b/js/xrp_blockly_toolbox.js index a24744f..6b7fc0a 100644 --- a/js/xrp_blockly_toolbox.js +++ b/js/xrp_blockly_toolbox.js @@ -274,6 +274,10 @@ var baseToolbox = { { "kind": "CATEGORY", "contents": [ + { + "kind": "BLOCK", + "type": "comment" + }, { "kind": "BLOCK", "type": "xrp_sleep", @@ -422,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 ", @@ -483,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 80aa4f6..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. @@ -619,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 ef2515a..3b495b3 100644 --- a/js/xrp_blocks_python.js +++ b/js/xrp_blocks_python.js @@ -367,4 +367,9 @@ Blockly.Python['xrp_sleep'] = function (block) { return code; }; +Blockly.Python['comment'] = function(block) { + var text = block.getFieldValue('TEXT'); + return '# ' + text + '\n'; +}; + From cdbc96f444698a084836a94fbba79b6fc9f8e8dc Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sun, 13 Jul 2025 02:05:19 -0400 Subject: [PATCH 47/49] Update CHANGELOG.txt Chagelog updated for the release. --- CHANGELOG.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 537920a..153af3a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,16 +1,16 @@ # Version 1.2.2 ## Gamepad support -* After updating the XRPLib to version 2.1.2 you can use a gamepad with XRPCode to drive your XRP. The Blocks palette has been updated with gamepad blocks. +* 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. -* This can also be done in Python. The API will be documented soon, for now you can use the blocks and then use the view menu to view the Python to get how the API works. -* We use the standard Web Gamepad support and recognize only one controller. If you want to know if your gamepad will work you can google 'web gamepad tester' there are a few out there that will let you see if your gamepad works. -* 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 buttons for the D-PAD. -* There is a current bug that very quick keyboard actions can be missed. Press the key again if a release was missed. +* 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 From efec19fc51ea18e35ff141d5704bf76cd68f0cc8 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sun, 13 Jul 2025 16:12:09 -0400 Subject: [PATCH 48/49] small bug fixes Added to the disconnect delay for brower bug, Made the update persent not go past 100 by using floor instead of round, Improved stop function with serial --- js/repl.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/repl.js b/js/repl.js index 288e7b8..1d3a094 100644 --- a/js/repl.js +++ b/js/repl.js @@ -434,7 +434,7 @@ class ReplJS{ 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, 300)); + await new Promise(r => setTimeout(r, 400)); REPL.bleReconnect(); } @@ -1504,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. @@ -1652,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); From fdb30b452567c13673eeae963245ea582f1a5e24 Mon Sep 17 00:00:00 2001 From: fgrossman Date: Sun, 13 Jul 2025 16:19:48 -0400 Subject: [PATCH 49/49] Updated to release 2.1.3 of XRPLib --- lib/XRPExamples/gamepad_example.blocks | 4 +-- lib/XRPLib/gamepad.py | 46 ++++++++++++++++++++------ lib/XRPLib/imu.py | 1 + lib/XRPLib/resetbot.py | 3 +- lib/XRPLib/timeout.py | 8 ++--- lib/package.json | 9 +++-- 6 files changed, 48 insertions(+), 23 deletions(-) diff --git a/lib/XRPExamples/gamepad_example.blocks b/lib/XRPExamples/gamepad_example.blocks index 56aea84..d5e3795 100644 --- a/lib/XRPExamples/gamepad_example.blocks +++ b/lib/XRPExamples/gamepad_example.blocks @@ -11,5 +11,5 @@ while not (gp.is_button_pressed(gp.BACK)): -## [2025-05-30 23:46:21] -##XRPBLOCKS {"blocks":{"languageVersion":0,"blocks":[{"type":"controls_whileUntil","id":"#/DDnbE2JxVsLQo`C`e^","x":-363,"y":16,"fields":{"MODE":"WHILE"},"inputs":{"BOOL":{"block":{"type":"logic_negate","id":"+k{^!vL*n}Km]oQ8u,ye","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 +## [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 index 188fe18..9a6e661 100644 --- a/lib/XRPLib/gamepad.py +++ b/lib/XRPLib/gamepad.py @@ -25,7 +25,7 @@ class Gamepad: DPAD_L = const(16) DPAD_R = const(17) - joyData = [ + _joyData = [ 0.0, 0.0, 0.0, @@ -35,7 +35,7 @@ class Gamepad: @classmethod def get_default_gamepad(cls): """ - Get the default XRP bluetooth joystick instance. This is a singleton, so only one instance of the reflectance sensor will ever exist. + 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() @@ -44,30 +44,54 @@ def get_default_gamepad(cls): def __init__(self): """ - """ + Manages communication with gamepad data coming from a remote computer via bluetooth + """ def start(self): - for i in range(len(self.joyData)): - self.joyData[i] = 0.0 + """ + 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): - uart.clear_data_callback() + """ + 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): - return -self.joyData[index] #returning the negative to make normal for user + 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): - return self.joyData[index] > 0 + 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) + 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 d9d2545..7c9d321 100644 --- a/lib/XRPLib/resetbot.py +++ b/lib/XRPLib/resetbot.py @@ -44,7 +44,7 @@ def reset_hard(): reset_led() reset_servos() reset_webserver() - + if "XRPLib.gamepad" in sys.modules: reset_gamepad() @@ -59,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/package.json b/lib/package.json index 7a2dc1c..fb9c107 100644 --- a/lib/package.json +++ b/lib/package.json @@ -21,15 +21,14 @@ ["XRPLib/webserver.py", "github:Open-STEM/XRP_Micropython/XRPLib/webserver.py"], ["XRPExamples/__init__.py", "github:Open-STEM/XRP_Micropython/XRPExamples/__init__.py"], ["XRPExamples/drive_examples.py", "github:Open-STEM/XRP_Micropython/XRPExamples/drive_examples.py"], + ["XRPExamples/gamepad_example.blocks", "github:Open-STEM/XRP_Micropython/XRPExamples/gamepad_example.blocks"], ["XRPExamples/installation_verification.py", "github:Open-STEM/XRP_Micropython/XRPExamples/installation_verification.py"], - ["XRPExamples/led_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/misc_examples.py"], + ["XRPExamples/led_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/led_example.py"], ["XRPExamples/sensor_examples.py", "github:Open-STEM/XRP_Micropython/XRPExamples/sensor_examples.py"], - ["XRPExamples/webserver_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/webserver_example.py"], - ["XRPExamples/gamepad_example.blocks", "github:Open-STEM/XRP_Micropython/XRPExamples/gamepad_example.blocks"] - + ["XRPExamples/webserver_example.py", "github:Open-STEM/XRP_Micropython/XRPExamples/webserver_example.py"] ], "deps": [ ["github:pimoroni/phew", "latest"] ], - "version": "2.1.2" + "version": "2.1.3" }