Skip to content

Commit dee7b19

Browse files
authored
Merge pull request #213 from Open-STEM/FranksDev
Handle XPP over USB Serial
2 parents d4d03fd + 10dbda9 commit dee7b19

8 files changed

Lines changed: 295 additions & 128 deletions

File tree

public/lib/XRPLib/dashboard.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ def __init__(self):
8383
self.rangefinder = Rangefinder.get_default_rangefinder()
8484
self.reflectance = Reflectance.get_default_reflectance()
8585
self.VoltageADC = ADC(Pin('BOARD_VIN_MEASURE'))
86-
self.CurrLADC = ADC(Pin('ML_CUR'))
87-
self.CurrRADC = ADC(Pin('MR_CUR'))
88-
self.Curr3ADC = ADC(Pin('M3_CUR'))
89-
self.Curr4ADC = ADC(Pin('M4_CUR'))
86+
#self.CurrLADC = ADC(Pin('ML_CUR'))
87+
#self.CurrRADC = ADC(Pin('MR_CUR'))
88+
#self.Curr3ADC = ADC(Pin('M3_CUR'))
89+
#self.Curr4ADC = ADC(Pin('M4_CUR'))
9090

9191
# Get XPP instance
9292
self._puppet = Puppet.get_default_puppet()
@@ -177,10 +177,10 @@ def _dashboard_update(self):
177177
self._puppet.set_variable('$encoder.4', self.motor_four.get_position_counts())
178178

179179
# Current sensor data
180-
self._puppet.set_variable('$current.left', self.CurrLADC.read_u16())
181-
self._puppet.set_variable('$current.right', self.CurrRADC.read_u16())
182-
self._puppet.set_variable('$current.3', self.Curr3ADC.read_u16())
183-
self._puppet.set_variable('$current.4', self.Curr4ADC.read_u16())
180+
#self._puppet.set_variable('$current.left', self.CurrLADC.read_u16())
181+
#self._puppet.set_variable('$current.right', self.CurrRADC.read_u16())
182+
#self._puppet.set_variable('$current.3', self.Curr3ADC.read_u16())
183+
#self._puppet.set_variable('$current.4', self.Curr4ADC.read_u16())
184184

185185
# Other sensors
186186
self._puppet.set_variable('$rangefinder.distance', self.rangefinder.distance())
@@ -211,6 +211,7 @@ def start(self, rate_hz=3):
211211
period_ms = int(1000 / rate_hz)
212212
self.update_timer.init(period=period_ms, mode=Timer.PERIODIC,
213213
callback=lambda t: self._dashboard_update())
214+
self._puppet.start()
214215

215216
def stop(self):
216217
"""
@@ -225,6 +226,7 @@ def stop(self):
225226

226227
# Stop timer
227228
self.update_timer.deinit()
229+
self._puppet.stop()
228230

229231
def set_value(self, name, value, rate_hz=3):
230232
"""

public/lib/XRPLib/gamepad.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def start(self):
8888
Signals the remote computer to begin sending gamepad data packets.
8989
Subscribes to all gamepad variables at a high rate (50 Hz).
9090
"""
91-
91+
self._puppet.start()
9292
self._puppet.send_program_start()
9393

9494
# Enable gamepad - signal to client to start sending
@@ -120,7 +120,8 @@ def stop(self):
120120
# Unsubscribe from all gamepad variables
121121
for var_name in self._VAR_NAMES.values():
122122
self._puppet.subscribe_variable(var_name, 0)
123-
123+
124+
self._puppet.stop()
124125
self._started = False
125126

126127
def get_value(self, index: int) -> float:

public/lib/XRPLib/puppet.py

Lines changed: 92 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
Uses a Network Tables-like architecture with variable ID mapping.
66
"""
77

8+
import select
89
import struct
910
import sys
1011
import time
1112
from machine import Timer
12-
from micropython import const
13+
from micropython import const,kbd_intr
1314

1415
# Message framing constants
1516
MSG_START_1 = const(0xAA)
@@ -89,7 +90,7 @@
8990

9091
class Puppet:
9192
"""
92-
Core XRP Puppet Protocol implementation.
93+
Core XRP Puppet Protocol implementation.
9394
Manages bidirectional communication, variable registry, and message handling.
9495
"""
9596

@@ -129,6 +130,11 @@ def __init__(self):
129130
self._update_timer = Timer(-1)
130131
self._update_timer_running = False
131132

133+
# STDIO polling (for USB_STDIO transport)
134+
self._poll_timer = Timer(-1)
135+
self._poll_timer_running = False
136+
self._poll_stdin_poll = None
137+
132138
# Program state
133139
self._program_running = False
134140

@@ -142,46 +148,86 @@ def _init_transport(self):
142148
# Try BLE first
143149
try:
144150
from ble.blerepl import uart
145-
self._transport = uart
146-
self._transport_type = 'BLE'
147-
self._transport.set_data_callback(self._data_callback)
148-
return
149-
except (ImportError, AttributeError):
150-
pass
151-
152-
# Fallback to USB serial
153-
try:
154-
# For USB serial, we'll use sys.stdin/sys.stdout or machine.UART
155-
# Check if we can use UART
156-
try:
157-
from machine import UART
158-
# Try to open UART for USB serial (typically UART(0))
159-
self._transport = UART(0, baudrate=115200)
160-
self._transport_type = 'USB'
161-
# For USB serial, we'll need to poll in a timer
162-
self._usb_poll_timer = Timer(-2)
163-
self._usb_poll_timer.init(period=10, mode=Timer.PERIODIC,
164-
callback=lambda t: self._poll_usb())
151+
if len(uart._connections) > 0:
152+
self._transport = uart
153+
self._transport_type = 'BLE'
154+
self._transport.set_data_callback(self._data_callback)
165155
return
166-
except:
167-
# Last resort: use sys.stdin/stdout
168-
self._transport = sys
156+
else:
169157
self._transport_type = 'USB_STDIO'
158+
self._start_poll_timer()
170159
return
171-
except:
172-
pass
173-
174-
raise RuntimeError("No transport available (BLE or USB serial)")
175-
176-
def _poll_usb(self):
160+
except ImportError:
161+
self._transport_type = 'USB_STDIO'
162+
self._start_poll_timer()
163+
return
164+
165+
166+
def _poll_stdio(self):
177167
"""
178-
Poll USB serial for incoming data.
168+
Poll STDIO for incoming data using select.poll.
169+
Reads raw bytes from the stdin buffer.
179170
"""
180-
if self._transport_type == 'USB' and self._transport.any():
181-
data = self._transport.read(self._transport.any())
182-
if data:
183-
self._data_callback(data)
184-
171+
data_read = False
172+
while True:
173+
events = self._stdin_poll.poll(0)
174+
if not events:
175+
if data_read:
176+
self._process_rx_buffer()
177+
break
178+
data = sys.stdin.buffer.read(1)
179+
self._rx_buffer.extend(data)
180+
data_read = True
181+
182+
#if data:
183+
#print(f"Received data: {data}")
184+
#self._data_callback(data)
185+
186+
def _start_poll_timer(self):
187+
"""
188+
Start STDIO polling for USB_STDIO transport.
189+
Uses select.poll on the raw stdin buffer and a timer to check it.
190+
"""
191+
if self._transport_type != 'USB_STDIO' or self._poll_timer_running:
192+
return
193+
self._stdin_poll = select.poll()
194+
self._stdin_poll.register(sys.stdin.buffer, select.POLLIN)
195+
196+
kbd_intr(-1) #the data will have 03 in it, don't do a ctrl-c for that data.
197+
198+
self._poll_timer.init(period=20, mode=Timer.PERIODIC,
199+
callback=lambda t: self._poll_stdio())
200+
self._poll_timer_running = True
201+
202+
def _stop_poll_timer(self):
203+
"""
204+
Stop the STDIO polling timer and unregister from poll.
205+
"""
206+
if self._poll_timer_running:
207+
self._poll_timer.deinit()
208+
kbd_intr(03) #start watching for ctrl-c again
209+
self._poll_timer_running = False
210+
if self._stdin_poll is not None:
211+
try:
212+
self._stdin_poll.unregister(sys.stdin.buffer)
213+
except (OSError, AttributeError):
214+
pass
215+
self._stdin_poll = None
216+
def start(self):
217+
"""
218+
Start the STDIO polling.
219+
"""
220+
if self._transport_type == 'USB_STDIO':
221+
self._start_poll_timer()
222+
223+
def stop(self):
224+
"""
225+
Stop the STDIO polling.
226+
"""
227+
if self._transport_type == 'USB_STDIO':
228+
self._stop_poll_timer()
229+
230+
185231
def _data_callback(self, data):
186232
"""
187233
Handle incoming data from transport layer.
@@ -209,11 +255,19 @@ def _process_rx_buffer(self):
209255
if idx == -1:
210256
# No start sequence found, clear buffer except last byte
211257
if len(self._rx_buffer) > 1:
258+
#see if anything in the buffer is a ctrl-c
259+
for i in range(len(self._rx_buffer) - 1):
260+
if self._rx_buffer[i] == 0x03:
261+
self.stop() #stop the USB handler if that is what is running and that will reenable the ctrl-c handler
212262
self._rx_buffer = self._rx_buffer[-1:]
213263
return
214264

215265
# Remove everything before start sequence
216266
if idx > 0:
267+
#check for a ctrl-c in the non packet parts
268+
for i in range(idx - 1):
269+
if self._rx_buffer[i] == 0x03:
270+
self.stop() #stop the USB handler if that is what is running and that will reenable the ctrl-c handler
217271
self._rx_buffer = self._rx_buffer[idx:]
218272

219273
# Found start, move to length
@@ -275,11 +329,9 @@ def _write_data(self, data):
275329
"""
276330
if self._transport_type == 'BLE':
277331
self._transport.write_data(data)
278-
elif self._transport_type == 'USB':
279-
self._transport.write(data)
332+
280333
elif self._transport_type == 'USB_STDIO':
281334
sys.stdout.buffer.write(data)
282-
sys.stdout.buffer.flush()
283335

284336
def _pack_message(self, msg_type, payload_data):
285337
"""

public/lib/XRPLib/resetbot.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def reset_servos():
2727
# Turn off both Servos
2828
Servo.get_default_servo(1).free()
2929
Servo.get_default_servo(2).free()
30+
try: #if 2350 then there are 4 servos
31+
Servo.get_default_servo(3).free()
32+
Servo.get_default_servo(4).free()
33+
except:
34+
pass
3035

3136
def reset_webserver():
3237
from XRPLib.webserver import Webserver

public/lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@
3232
"deps": [
3333
["github:pimoroni/phew", "latest"]
3434
],
35-
"version": "2.2.0"
35+
"version": "2.2.1"
3636
}

src/connections/bluetoothconnection.ts

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ export class BluetoothConnection extends Connection {
3939

4040
private Table: TableMgr | undefined = undefined;
4141

42-
// XPP Protocol constants for packet detection
43-
private readonly XPP_START_SEQ: number[] = [0xAA, 0x55];
44-
private readonly XPP_END_SEQ: number[] = [0x55, 0xAA];
45-
private xppBuffer: Uint8Array = new Uint8Array(0); // Buffer for incomplete XPP packets
46-
4742
constructor(connMgr: ConnectionMgr) {
4843
super();
4944
this.connMgr = connMgr;
@@ -181,72 +176,6 @@ export class BluetoothConnection extends Connection {
181176
}
182177

183178

184-
/**
185-
* Extracts complete XPP packets from the buffer and returns them.
186-
* XPP packet format: [0xAA 0x55] [Type] [Length] [Payload] [0x55 0xAA]
187-
* @param data - New data to add to the buffer
188-
* @returns Array of complete XPP packets
189-
*/
190-
private extractCompleteXPPPackets(data: Uint8Array): Uint8Array[] {
191-
// Add new data to buffer
192-
this.xppBuffer = this.concatUint8Arrays(this.xppBuffer, data);
193-
194-
const completePackets: Uint8Array[] = [];
195-
196-
while (this.xppBuffer.length >= 4) { // Minimum packet size is 4 bytes (start + type + length + end)
197-
// Look for start sequence [0xAA 0x55]
198-
const startIndex = this.xppBuffer.findIndex((val, idx) =>
199-
idx < this.xppBuffer.length - 1 &&
200-
val === this.XPP_START_SEQ[0] &&
201-
this.xppBuffer[idx + 1] === this.XPP_START_SEQ[1]
202-
);
203-
204-
if (startIndex === -1) {
205-
// No start sequence found, clear buffer
206-
this.xppBuffer = new Uint8Array(0);
207-
break;
208-
}
209-
210-
// Remove any data before the start sequence
211-
if (startIndex > 0) {
212-
this.xppBuffer = this.xppBuffer.subarray(startIndex);
213-
}
214-
215-
// Check if we have enough data for type and length (at least 4 bytes: start + type + length)
216-
if (this.xppBuffer.length < 4) {
217-
break; // Need more data
218-
}
219-
220-
// Extract length (byte 3 after start sequence)
221-
// Type is at byte 2, but we don't need it for packet extraction
222-
const payloadLength = this.xppBuffer[3];
223-
224-
// Calculate total packet size: start(2) + type(1) + length(1) + payload + end(2)
225-
const totalPacketSize = 2 + 1 + 1 + payloadLength + 2;
226-
227-
// Check if we have the complete packet
228-
if (this.xppBuffer.length < totalPacketSize) {
229-
break; // Need more data
230-
}
231-
232-
// Verify end sequence
233-
const endIndex = totalPacketSize - 2;
234-
if (this.xppBuffer[endIndex] === this.XPP_END_SEQ[0] &&
235-
this.xppBuffer[endIndex + 1] === this.XPP_END_SEQ[1]) {
236-
// Complete packet found
237-
const completePacket = this.xppBuffer.subarray(0, totalPacketSize);
238-
completePackets.push(completePacket);
239-
240-
// Remove processed packet from buffer
241-
this.xppBuffer = this.xppBuffer.subarray(totalPacketSize);
242-
} else {
243-
// Invalid end sequence, skip this start sequence and continue
244-
this.xppBuffer = this.xppBuffer.subarray(2);
245-
}
246-
}
247-
248-
return completePackets;
249-
}
250179

251180
/**
252181
* readWorker - this worker read data from the XRP robot
@@ -266,11 +195,10 @@ export class BluetoothConnection extends Connection {
266195
valuesD = await this.get2BLEData();
267196
if(valuesD != undefined) {
268197
// Extract complete XPP packets and only process those
269-
const completePackets = this.extractCompleteXPPPackets(valuesD);
270-
for (const packet of completePackets) {
271-
this.joyStick?.handleXPPMessage(packet);
272-
this.Table?.readFromDevice(packet);
273-
//NOTE: if there is one more of these then we should switch to a subscription model.
198+
// Note: regularData is ignored since bleDataReader only receives XPP packets
199+
const { packets } = this.extractCompleteXPPPackets(valuesD);
200+
for (const packet of packets) {
201+
this.processXPPPacket(packet, this.Table);
274202
}
275203
}
276204
}

0 commit comments

Comments
 (0)