+ )}
+
+ {/* Drivetrain: continuous effort plus the blocking straight/turn
+ commands (controls freeze until a blocking motion completes,
+ so everything is gated on busy) */}
+ {hasDrive && (
+
+
+ Drivetrain
+ {driveBusy && (
+
+ controls paused until the robot arrives
+
+ )}
+
+
+ );
+};
+
+export default PuppetControlWidget;
diff --git a/src/components/dashboard/utils/puppetMode.ts b/src/components/dashboard/utils/puppetMode.ts
new file mode 100644
index 0000000..65b763a
--- /dev/null
+++ b/src/components/dashboard/utils/puppetMode.ts
@@ -0,0 +1,48 @@
+import AppMgr from '@/managers/appmgr';
+import { CommandToXRPMgr } from '@/managers/commandstoxrpmgr';
+
+/**
+ * Puppet mode: run the XRPLib puppet passthrough program on the robot so the
+ * dashboard can drive its actuators directly over XPP variable updates.
+ *
+ * The switch rides the same machinery as the editor's Run button
+ * (stopProgram → updateMainFile → executeLines); the passthrough ships with
+ * the XRPLib examples, so nothing is uploaded. Once it starts, the robot
+ * sends VAR_DEFs for its $servo.N variables — their arrival in the
+ * NetworkTable is the confirmation that puppet mode is live.
+ */
+
+/** On-robot path of the passthrough program (flashed with the XRPLib examples). */
+export const PUPPET_PASSTHROUGH_PATH = '/XRPExamples/puppet_passthrough.py';
+
+/**
+ * Start puppet mode on the connected XRP.
+ * Returns false when there is no connection or the run machinery is busy;
+ * the caller should watch the NetworkTable for $servo.N definitions to know
+ * the passthrough is actually up.
+ */
+export async function startPuppetPassthrough(): Promise {
+ const connection = AppMgr.getInstance().getConnection();
+ if (!connection?.isConnected()) {
+ return false;
+ }
+
+ const cmdMgr = CommandToXRPMgr.getInstance();
+
+ // Interrupt any running user program (guarded no-op when idle), and give
+ // the interrupt/REPL a moment to settle before starting the passthrough.
+ cmdMgr.stopProgram();
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
+ const lines = await cmdMgr.updateMainFile(PUPPET_PASSTHROUGH_PATH);
+ if (!lines) {
+ return false; // run machinery busy
+ }
+ await cmdMgr.executeLines(lines);
+ return true;
+}
+
+/** Stop puppet mode — identical to pressing STOP on a running program. */
+export function stopPuppetPassthrough(): void {
+ CommandToXRPMgr.getInstance().stopProgram();
+}
diff --git a/src/components/dashboard/utils/xppUtils.ts b/src/components/dashboard/utils/xppUtils.ts
index 8e400e2..2da916c 100644
--- a/src/components/dashboard/utils/xppUtils.ts
+++ b/src/components/dashboard/utils/xppUtils.ts
@@ -49,7 +49,10 @@ export function buildVarUpdatePacket(meta: CustomVarMeta, value: number): Uint8A
/**
* Send a variable update to the XRP.
- * Uses the active connection's writeToDevice.
+ * Uses the active connection's writeToDataDevice: over BLE that is the
+ * dedicated binary DATA characteristic (the only input the robot's puppet
+ * protocol listens to on BLE); over USB it falls through to the normal
+ * serial stream, which the puppet polls directly.
* Returns true if sent, false if no connection available.
*/
export async function sendVariableUpdate(meta: CustomVarMeta, value: number): Promise {
@@ -61,7 +64,7 @@ export async function sendVariableUpdate(meta: CustomVarMeta, value: number): Pr
const packet = buildVarUpdatePacket(meta, value);
try {
- await connection.writeToDevice(packet);
+ await connection.writeToDataDevice(packet);
return true;
} catch (err) {
console.error('Error sending variable update:', err);
diff --git a/src/components/dashboard/xrp-dashboard.tsx b/src/components/dashboard/xrp-dashboard.tsx
index b70fcdb..bdef55a 100644
--- a/src/components/dashboard/xrp-dashboard.tsx
+++ b/src/components/dashboard/xrp-dashboard.tsx
@@ -10,6 +10,7 @@ import {
} from "./sensors";
import CustomXPPSensor from "./sensors/CustomXPPSensor";
import CustomVariableWidget from "./sensors/CustomVariableWidget";
+import PuppetControlWidget from "./sensors/PuppetControlWidget";
import { getCustomSensor } from "./sensors/customRegistry";
import { GridStackOptions } from "gridstack";
import AddWidgets from "./AddWidget";
@@ -69,6 +70,8 @@ const COMPONENT_MAP = {
CustomVariable: ({ initialVarName }: { initialVarName?: string }) => (
),
+
+ PuppetControl: () => ,
};
const gridOptions: GridStackOptions = {
diff --git a/src/connections/bluetoothconnection.ts b/src/connections/bluetoothconnection.ts
index a072998..d44bdf9 100644
--- a/src/connections/bluetoothconnection.ts
+++ b/src/connections/bluetoothconnection.ts
@@ -440,21 +440,24 @@ export class BluetoothConnection extends Connection {
}
/**
- * writeToDataDevice
- * @param Uint8Array
+ * writeToDataDevice - write binary (XPP) data to the DATA characteristic,
+ * the only input the robot's puppet protocol listens to over BLE.
+ * Chained on the same queue as the REPL writes: GATT allows only one
+ * operation at a time per device, so an unqueued write here would race
+ * shell/REPL traffic and get dropped with "operation already in progress".
+ * @param data
*/
public async writeToDataDevice(data: Uint8Array) {
this.connLogger.debug('writeToDataDevice BLE: ' + data);
- try {
- //this.connLogger.debug("writing: " + this.TEXT_DECODER.decode(str));
- await this.bleDataWriter?.writeValue(data as BufferSource);
-
- } catch (error) {
- this.connLogger.debug(error);
- }
-
- return Promise.resolve(); // Indicate success
+ this.Queue = this.Queue.then(async () => {
+ try {
+ await this.bleDataWriter?.writeValue(data as BufferSource);
+ } catch (error) {
+ console.error('ble data write failed:', error);
+ }
+ });
+ return this.Queue;
}
/**
diff --git a/src/connections/connection.ts b/src/connections/connection.ts
index d9b06f4..d09e011 100644
--- a/src/connections/connection.ts
+++ b/src/connections/connection.ts
@@ -480,6 +480,19 @@ abstract class Connection {
this.connLogger.debug('Writing to device' + str);
}
+ /**
+ * writeToDataDevice - write binary (XPP) data to the device.
+ * Transports with a dedicated binary channel override this: over BLE
+ * the robot only feeds XPP from the DATA characteristic (the REPL
+ * characteristic is dupterm'd into stdin, where XPP bytes would be
+ * lost). Over USB there is a single stream, so the default of writing
+ * to the normal device stream is correct.
+ * @param data
+ */
+ public async writeToDataDevice(data: Uint8Array) {
+ await this.writeToDevice(data);
+ }
+
// Goes into raw mode and writes a command according to the XRP_SEND_BLOCK_SIZE then executes
public async writeUtilityCmdRaw(
cmdStr: string,