Skip to content

Commit cd289eb

Browse files
committed
Merge branch 'main' of https://github.com/Open-STEM/XRPWeb into kq-dragdrop
2 parents d257326 + 1824014 commit cd289eb

44 files changed

Lines changed: 2921 additions & 524 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ jobs:
1111
release:
1212
name: Release pushed tag
1313
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
node-version: [20]
1417

1518
steps:
1619
- name: Checkout
@@ -26,12 +29,21 @@ jobs:
2629
run: npm run test
2730
- name: Build
2831
run: npm run build
32+
env:
33+
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
34+
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
35+
GOOGLE_DEVELOPER_KEY: ${{ secrets.GOOGLE_DEVELOPER_KEY }}
36+
GOOGLE_REDIRECT_URI: ${{ vars.GOOGLE_REDIRECT_URI }}
2937
- name: Archive site content
3038
uses: thedoctor0/zip-release@master
3139
with:
32-
type: 'zip'
40+
type: 'tar'
3341
directory: './dist'
34-
filename: xrpweb-v${{ github.run_number }}.zip
42+
filename: ../xrpweb-v${{ github.run_number }}.zip
43+
- name: Debug - List files
44+
run: |
45+
ls -la
46+
ls -la dist/
3547
- name: Create Release
3648
id: create_new_release
3749
uses: softprops/action-gh-release@v2.3.3
@@ -45,6 +57,6 @@ jobs:
4557
with:
4658
repo_token: ${{ secrets.GITHUB_TOKEN }}
4759
file: xrpweb-v${{ github.run_number }}.zip
48-
asset_name: xrpweb
60+
asset_name: artifacts.zip
4961
tag: ${{ github.ref }}
5062
overwrite: true

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"homepage": "https://experientialrobotics.org/",
55
"license": "GPL-2.0-only",
66
"private": true,
7-
"version": "2.0.0",
7+
"version": "2.0.11",
88
"type": "module",
99
"scripts": {
1010
"dev": "vite",

public/CHANGELOG.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Version 2.0.6
1+
# Version 2.0.12
22

33
#### Bluetooth support
44
<img height="10%" width="10%" src="src/assets/images/Bluetooth_FM_Black.png"/></img>
@@ -31,3 +31,9 @@
3131
* Google Drive integration is now available to save and load files
3232
* Third-party drivers can now be added to XRPCode
3333
* Theming support for light and dark modes
34+
* Support the New Puppet Protocol for communication with the XRP Bluetooth (advanced users) [See XPP](https://xrpcode.io/docs/puppet_protocol)
35+
36+
#### Bug Fixes
37+
* [File SaveAs dialog not using name](https://github.com/Open-STEM/XRPWeb/issues/205)
38+
* [Google Drive Export to PC resulted with zero bytes](https://github.com/Open-STEM/XRPWeb/issues/206)
39+
* [Google Drive Onetime notification](https://github.com/Open-STEM/XRPWeb/issues/198)

public/XRPLib.zip

8.29 KB
Binary file not shown.

public/lib/XRPLib/dashboard.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
from .encoded_motor import EncodedMotor
2+
from .rangefinder import Rangefinder
3+
from .imu import IMU
4+
from .reflectance import Reflectance
5+
from .puppet import Puppet, VAR_TYPE_INT, VAR_TYPE_FLOAT, PERM_READ_ONLY
6+
7+
from machine import Timer, ADC, Pin
8+
from micropython import const
9+
import time
10+
11+
12+
class Dashboard:
13+
14+
_DEFAULT_DASHBOARD_INSTANCE = None
15+
16+
# Variable type constants
17+
VAR_TYPE_FLOAT = const(2)
18+
19+
# Permission constants
20+
PERM_READ_ONLY = const(1)
21+
PERM_WRITE_ONLY = const(2)
22+
23+
# Backward compatibility constants
24+
YAW = const(0)
25+
ROLL = const(1)
26+
PTICH = const(2)
27+
ACCX = const(3)
28+
ACCY = const(4)
29+
ACCZ = const(5)
30+
ENCL = const(6)
31+
ENCR = const(7)
32+
ENC3 = const(8)
33+
ENC4 = const(9)
34+
CURRR = const(10)
35+
CURRL = const(11)
36+
CURR3 = const(12)
37+
CURR4 = const(13)
38+
DIST = const(14)
39+
REFL = const(15)
40+
REFR = const(16)
41+
VOLTAGE = const(17)
42+
43+
# Mapping from old index to XPP variable names
44+
_VAR_NAMES = {
45+
YAW: '$imu.yaw',
46+
ROLL: '$imu.roll',
47+
PTICH: '$imu.pitch',
48+
ACCX: '$imu.acc_x',
49+
ACCY: '$imu.acc_y',
50+
ACCZ: '$imu.acc_z',
51+
ENCL: '$encoder.left',
52+
ENCR: '$encoder.right',
53+
ENC3: '$encoder.3',
54+
ENC4: '$encoder.4',
55+
CURRL: '$current.left',
56+
CURRR: '$current.right',
57+
CURR3: '$current.3',
58+
CURR4: '$current.4',
59+
DIST: '$rangefinder.distance',
60+
REFL: '$reflectance.left',
61+
REFR: '$reflectance.right',
62+
VOLTAGE: '$voltage',
63+
}
64+
65+
@classmethod
66+
def get_default_dashboard(cls):
67+
"""
68+
Get the default XRP dashboard instance. This is a singleton, so only one instance of the dashboard sensor will ever exist.
69+
"""
70+
if cls._DEFAULT_DASHBOARD_INSTANCE is None:
71+
cls._DEFAULT_DASHBOARD_INSTANCE = cls()
72+
return cls._DEFAULT_DASHBOARD_INSTANCE
73+
74+
def __init__(self):
75+
"""
76+
Manages communication with dashboard data going to a remote computer via XPP protocol.
77+
"""
78+
self.left_motor = EncodedMotor.get_default_encoded_motor(index=1)
79+
self.right_motor = EncodedMotor.get_default_encoded_motor(index=2)
80+
self.motor_three = EncodedMotor.get_default_encoded_motor(index=3)
81+
self.motor_four = EncodedMotor.get_default_encoded_motor(index=4)
82+
self.imu = IMU.get_default_imu()
83+
self.rangefinder = Rangefinder.get_default_rangefinder()
84+
self.reflectance = Reflectance.get_default_reflectance()
85+
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'))
90+
91+
# Get XPP instance
92+
self._puppet = Puppet.get_default_puppet()
93+
94+
# Register all sensor variables
95+
self._register_variables()
96+
97+
# Create timer for periodic updates
98+
self.update_timer = Timer(-1)
99+
self._update_rate = 3 # Default 3 Hz
100+
101+
def _register_variables(self):
102+
"""
103+
Register all sensor variables with XPP.
104+
"""
105+
# IMU variables (float)
106+
for var_name in ['$imu.yaw', '$imu.roll', '$imu.pitch',
107+
'$imu.acc_x', '$imu.acc_y', '$imu.acc_z']:
108+
self._puppet.define_variable(var_name, VAR_TYPE_FLOAT, PERM_READ_ONLY)
109+
110+
# Encoder variables (int)
111+
for var_name in ['$encoder.left', '$encoder.right', '$encoder.3', '$encoder.4']:
112+
self._puppet.define_variable(var_name, VAR_TYPE_INT, PERM_READ_ONLY)
113+
114+
# Current sensor variables (int)
115+
for var_name in ['$current.left', '$current.right', '$current.3', '$current.4']:
116+
self._puppet.define_variable(var_name, VAR_TYPE_INT, PERM_READ_ONLY)
117+
118+
# Other sensor variables (float)
119+
for var_name in ['$rangefinder.distance', '$reflectance.left',
120+
'$reflectance.right', '$voltage']:
121+
self._puppet.define_variable(var_name, VAR_TYPE_FLOAT, PERM_READ_ONLY)
122+
123+
def sendIntValue(self, index, value):
124+
"""
125+
Send an integer value (backward compatibility method).
126+
Now uses XPP protocol.
127+
128+
:param index: Variable index constant
129+
:type index: int
130+
:param value: Integer value to send
131+
:type value: int
132+
"""
133+
if index not in self._VAR_NAMES:
134+
return
135+
136+
var_name = self._VAR_NAMES[index]
137+
try:
138+
self._puppet.set_variable(var_name, value)
139+
except:
140+
pass # Variable might not be registered yet
141+
142+
def sendFloatValue(self, index, value):
143+
"""
144+
Send a float value (backward compatibility method).
145+
Now uses XPP protocol.
146+
147+
:param index: Variable index constant
148+
:type index: int
149+
:param value: Float value to send
150+
:type value: float
151+
"""
152+
if index not in self._VAR_NAMES:
153+
return
154+
155+
var_name = self._VAR_NAMES[index]
156+
try:
157+
self._puppet.set_variable(var_name, value)
158+
except:
159+
pass # Variable might not be registered yet
160+
161+
def _dashboard_update(self):
162+
"""
163+
Update all sensor variables with current readings.
164+
"""
165+
# IMU data
166+
self._puppet.set_variable('$imu.yaw', self.imu.get_yaw())
167+
self._puppet.set_variable('$imu.roll', self.imu.get_roll())
168+
self._puppet.set_variable('$imu.pitch', self.imu.get_pitch())
169+
self._puppet.set_variable('$imu.acc_x', self.imu.get_acc_x())
170+
self._puppet.set_variable('$imu.acc_y', self.imu.get_acc_y())
171+
self._puppet.set_variable('$imu.acc_z', self.imu.get_acc_z())
172+
173+
# Encoder data
174+
self._puppet.set_variable('$encoder.left', self.left_motor.get_position_counts())
175+
self._puppet.set_variable('$encoder.right', self.right_motor.get_position_counts())
176+
self._puppet.set_variable('$encoder.3', self.motor_three.get_position_counts())
177+
self._puppet.set_variable('$encoder.4', self.motor_four.get_position_counts())
178+
179+
# 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())
184+
185+
# Other sensors
186+
self._puppet.set_variable('$rangefinder.distance', self.rangefinder.distance())
187+
self._puppet.set_variable('$reflectance.left', self.reflectance.get_left())
188+
self._puppet.set_variable('$reflectance.right', self.reflectance.get_right())
189+
190+
# Voltage
191+
voltage = self.VoltageADC.read_u16() / (1024*64/14)
192+
self._puppet.set_variable('$voltage', voltage)
193+
194+
def start(self, rate_hz=3):
195+
"""
196+
Start sending dashboard packets to the remote computer at the specified rate.
197+
198+
:param rate_hz: Update rate in Hz (default: 3)
199+
:type rate_hz: int
200+
"""
201+
self._update_rate = rate_hz
202+
203+
# Subscribe all variables at the specified rate
204+
for var_name in self._VAR_NAMES.values():
205+
try:
206+
self._puppet.subscribe_variable(var_name, rate_hz)
207+
except:
208+
pass # Variable might not be registered yet
209+
210+
# Also use timer for backward compatibility
211+
period_ms = int(1000 / rate_hz)
212+
self.update_timer.init(period=period_ms, mode=Timer.PERIODIC,
213+
callback=lambda t: self._dashboard_update())
214+
215+
def stop(self):
216+
"""
217+
Stop sending dashboard data packets.
218+
"""
219+
# Unsubscribe from all variables
220+
for var_name in self._VAR_NAMES.values():
221+
try:
222+
self._puppet.subscribe_variable(var_name, 0)
223+
except:
224+
pass
225+
226+
# Stop timer
227+
self.update_timer.deinit()
228+
229+
def set_value(self, name, value, rate_hz=3):
230+
"""
231+
Define a variable and subscribe to it at the specified rate.
232+
233+
:param name: The variable name
234+
:type name: str
235+
:param value: The value to set
236+
:type value: float
237+
:param rate_hz: The update rate in Hz (default: 3)
238+
:type rate_hz: int
239+
"""
240+
self._puppet.define_variable(name, VAR_TYPE_FLOAT, PERM_READ_ONLY)
241+
try:
242+
self._puppet.subscribe_variable(name, rate_hz)
243+
except:
244+
pass
245+
self._puppet.set_variable(name, value)
246+
247+
def get_value(self, name):
248+
"""
249+
Get the value of a variable.
250+
251+
:param name: The variable name
252+
:type name: str
253+
"""
254+
self._puppet.define_variable(name, VAR_TYPE_FLOAT, PERM_WRITE_ONLY)
255+
return self._puppet.get_variable(name)

public/lib/XRPLib/differential_drive.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ def __init__(self, left_motor: EncodedMotor, right_motor: EncodedMotor, imu: IMU
5050
self.wheel_diam = wheel_diam
5151
self.track_width = wheel_track
5252

53+
self.heading_pid = None
54+
self.current_heading = None
55+
self.reset_heading = True
56+
self.turning = False
57+
58+
if self.imu:
59+
# if the IMU is initialized, then create a PID controller that can be used
60+
# to maintain a constant heading when driving
61+
self.heading_pid = PID( kp = 0.075, kd=0.001, )
62+
5363
def set_effort(self, left_effort: float, right_effort: float) -> None:
5464
"""
5565
Set the raw effort of both motors individually
@@ -110,7 +120,33 @@ def arcade(self, straight:float, turn:float):
110120
scale = max(abs(straight), abs(turn))/(abs(straight) + abs(turn))
111121
left_speed = (straight - turn)*scale
112122
right_speed = (straight + turn)*scale
113-
self.set_effort(left_speed, right_speed)
123+
124+
if not self.heading_pid:
125+
# if not using IMU assist to maintain heading, just pass down the left and right motor
126+
# speeds to control movement
127+
self.set_effort(left_speed, right_speed)
128+
else:
129+
# else if IMU assist is enabled, then use the IMU with PID to
130+
# maintain a constant heading while driving.
131+
if turn == 0:
132+
# straight drive requested, then maintain the current heading
133+
if self.turning:
134+
# if previously turning, then clear the turn indicator and reset the course heading
135+
self.reset_heading = True
136+
self.turning = False
137+
138+
if self.reset_heading:
139+
self.reset_heading = False
140+
self.current_heading = self.imu.get_yaw()
141+
142+
# use the PID to set the heading correction based on the current heading
143+
heading_correction = self.heading_pid.update(self.current_heading - self.imu.get_yaw())
144+
145+
self.set_effort(left_speed - heading_correction, right_speed + heading_correction)
146+
else:
147+
# set the turning indicator and apply the left and right speeds
148+
self.turning = True
149+
self.set_effort(left_speed, right_speed)
114150

115151
def reset_encoder_position(self) -> None:
116152
"""

0 commit comments

Comments
 (0)