Skip to content

Commit 453dd16

Browse files
repkovskylaurensvalk
authored andcommitted
42114-volvo-articulated-hauler: Add RC project.
1 parent 709441c commit 453dd16

File tree

6 files changed

+290
-0
lines changed

6 files changed

+290
-0
lines changed
162 KB
Loading
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: "6x6 Volvo Articulated Hauler"
3+
number: 42114
4+
image:
5+
local: "42114-volvo-articulated-hauler.jpg"
6+
credit: "Repkovsky"
7+
layout: set
8+
description: "Add set description"
9+
---
198 KB
Loading
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: "Powered Up Remote Control"
3+
maintainer:
4+
user: "repkovsky"
5+
name: "Repkovsky"
6+
image:
7+
local: "42114_88010.jpg"
8+
description:
9+
"Control the Volvo articulated hauler with the Powered Up Remote!"
10+
video:
11+
youtube: "j4ZxOhXygNY"
12+
building_instructions:
13+
external: https://www.lego.com/cdn/product-assets/product.bi.core.pdf/6396116.pdf
14+
code: "#program"
15+
---
16+
17+
18+
# Program
19+
20+
The program for controlling Technic Volvo Articulated Hauler with remote control is more complicated than the other remote control programs because of the 3-gear gearbox, which works both in manual and automatic mode. Manual switching gears requires not only detection whether remote button is pressed or not, but rather detection of button pressing (or releasing) moment itself. This is realized using class '''Key'''. Automatic gearbox needs to detect proper time for switching gear up or down. This is possible by measuring speed of XL motor, when it is running. If the speed is systematically very low, the gear is decreased. If the speed is systematically close to maximum, the gear is increased. To make speed measurement robust to random variations, values obtained from speed sensor are filtered using simple [exponential smoothing|https://en.wikipedia.org/wiki/Exponential_smoothing]. Threshold values of speed, measurement time and smoothing constant are defined by constants `HI_SPEED, LO_SPEED`, `STABLE_SPEED_TIME` and `SMOOTHING`. If the automatic gearbox does not change gears even if Hauler reaches full speed, `HI_SPEED` should be decreased. Speed tracking, keeping the current state of gearbox and handling the remote/hub LEDs was implemented in the class '''Gearbox'''.
21+
22+
# Driving and switching gears
23+
24+
By default left controller controlls left/right steering, and right controller determines direction of driving. This can be changed easily by setting constant `LEFT_STEER_RIGHT_DRIVE` to False.
25+
26+
Gearbox can be used in two modes, as in the original LEGO smartphone app: *automatic* and *manual*. You can switch between the modes by pushing the green button on the remote. *Automatic* is default starting mode, but this can be easily changed by modifying constant `INIT_GEARBOX_AUTO`.
27+
28+
In the *automatic* mode, gears are changed when program detects that motor's speed is too slow or close to maximum speed. Current gear is indicated by the color of the remote's LED - 1: cyan, 2: blue, 3: magenta. To enable dumper (remote's LED: green), press right red button on the remote. To go back to driving mode, press left red button. If drive is idle for time longer than defined in constant `GEAR_RESET_TIMEOUT`, the gear is set to 1.
29+
30+
In the *manual* mode gear is decreased by pressing left red button, and increased with right red button. Gearbox positions are indicated by the color of LED: 1: yellow, 2: orange, 3: red, Dumper: green.
31+
32+
Sometimes gearbox tends to jam. If the target angle of gear selector is not reached within the time defined by `GEAR_SWITCH_TIMEOUT` (1.5sec by default), the automatic gearbox reset is performed. Hub's LED changes to red, while gearbox is recalibrated, setting gear to 1.
33+
34+
![](./remote_description.png)
35+
36+
{% include copy-code.html %}
37+
```python
38+
{% include_relative main.py %}
39+
```
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# Controlling LEGO Volvo Articulated Hauler (42114) with Bluetooth Remote (88010)
2+
# Version 1.2, works with PyBricks 3.1
3+
4+
from pybricks.hubs import TechnicHub
5+
from pybricks.pupdevices import Motor, Remote
6+
from pybricks.parameters import Port, Button, Color
7+
from pybricks.tools import wait
8+
9+
# control buttons swapping
10+
LEFT_STEER_RIGHT_DRIVE = True
11+
12+
# set gearbox to AUTO mode at startup?
13+
INIT_GEARBOX_AUTO = True
14+
15+
# steering settings
16+
STEER_ANGLE = 65
17+
STEER_SPEED = 1000
18+
STEER_HARDNESS = 4
19+
20+
class Gearbox:
21+
# if speed is stable above, automatic gearbox will increase gear
22+
HI_SPEED = 1400
23+
# if speed is stable below, automatic gearbox will decrease gear
24+
LO_SPEED = 300
25+
# time [ms] after gearbox switches to 1st gear when drive remains idle
26+
GEAR_RESET_TIMEOUT = 2000
27+
# time [ms] of speed stability measurement before automatic gear change
28+
STABLE_SPEED_TIME = 800
29+
# time [ms] for normal gear switch
30+
GEAR_SWITCH_TIMEOUT = 1500
31+
# speed measurement smoothing factor
32+
SMOOTHING = 0.05
33+
# colors of gearbox state indicator LED for automatic (True) and manual (False)
34+
# [gear 1, gear 2, gear 3, dumper]
35+
POS_COLOR = {True: [Color.CYAN, Color.BLUE, Color.MAGENTA, Color.GREEN],
36+
False: [Color.ORANGE, Color(h=15, s=100, v=100),
37+
Color(h=5, s=100, v=100), Color.GREEN]}
38+
def __init__(self, remote: Remote, hub: TechnicHub, drive: Motor):
39+
# assign external objects to properties of the class
40+
self.remote = remote
41+
self.hub = hub
42+
self.drive = drive
43+
# initialize control variables
44+
self.speed_timer = 0
45+
self.idle_timer = 0
46+
self.speed = 0
47+
# initialize L motor
48+
self.gearbox = Motor(Port.B)
49+
self.calibrate()
50+
# set defaults
51+
self.last_auto_pos = 0
52+
self.set_auto(INIT_GEARBOX_AUTO)
53+
54+
def calibrate(self):
55+
# calibrate gearbox motor by finding its physical rotation limit;
56+
# first, move left at full power to handle possible jam in gearbox
57+
self.gearbox.run_until_stalled(360)
58+
# second, correct the position
59+
self.gearbox.run_angle(360, -90)
60+
# finally move left with small power to avoid twisting 12-axle and measurement error
61+
stalled_angle = self.gearbox.run_until_stalled(360, duty_limit=10)
62+
# round to multiple of 90 degrees and subtract angle of physical block (90deg)
63+
base_angle = 90*round(stalled_angle/90)-90
64+
# adjust settings of possible motor positions
65+
self.pos_angle = [p+base_angle for p in [90, 0, -90, -180]]
66+
self.pos = 0
67+
68+
def set_position(self, pos):
69+
# limit positions to range 0,1,2,3
70+
pos = min(3, max(pos, 0))
71+
# apply new position, if it is different from the current one
72+
if self.pos != pos:
73+
# set remote control light according to mode and position
74+
self.remote.light.on(self.POS_COLOR[self.auto][pos])
75+
# stop drive to allow smooth gear change
76+
self.drive.stop()
77+
# rotate gearbox to angle that corresponds position
78+
self.gearbox.run_target(720, target_angle=self.pos_angle[pos], wait=False)
79+
# control time of gear change, to detect possible position mismatch
80+
change_time = 0
81+
while not self.gearbox.control.done() and change_time < self.GEAR_SWITCH_TIMEOUT:
82+
# measure the switching time
83+
change_time += 1
84+
wait(1)
85+
if change_time == self.GEAR_SWITCH_TIMEOUT:
86+
# timeout occured - something went wrong, true gearbox position
87+
# is different than expected - set hub LED to red
88+
self.hub.light.on(Color.RED)
89+
# stop switching and recalibrate
90+
self.gearbox.stop()
91+
self.calibrate()
92+
# 1st gear is set
93+
pos = 0
94+
self.remote.light.on(self.POS_COLOR[self.auto][pos])
95+
self.hub.light.on(Color.GREEN)
96+
# remember last automatic gear
97+
self.last_auto_pos = self.pos if pos == 3 else pos
98+
# update gear state variable
99+
self.pos = pos
100+
101+
def dumper(self):
102+
# return whether gearbox is set to drive dumper
103+
return self.pos == 3
104+
105+
def set_auto(self, auto):
106+
# set AUTO/MANUAL mode and update control light
107+
self.auto = auto
108+
self.remote.light.on(self.POS_COLOR[self.auto][self.pos])
109+
110+
def update_auto_gear(self):
111+
# in AUTO mode changes gear if speed is stable below/above LO_SPEED/HI_SPEED threshold
112+
if self.auto and not self.dumper():
113+
speed = self.drive.speed()
114+
# basic low-pass filtering (exponential smoothing)
115+
self.speed += self.SMOOTHING*(abs(speed)-self.speed)
116+
wait(10)
117+
if self.LO_SPEED < self.speed < self.HI_SPEED:
118+
# speed in medium range, reset timer
119+
self.speed_timer = 0
120+
else:
121+
# speed out of medium range, increase time of measurement
122+
self.speed_timer += 10
123+
if self.speed_timer > self.STABLE_SPEED_TIME:
124+
# speed is stable - reset timer
125+
self.speed_timer = 0
126+
# depending on speed and current position,
127+
# return lower, higher or None gear (no change)
128+
if self.pos > 0 and self.speed < self.LO_SPEED:
129+
self.set_position(self.pos - 1)
130+
elif self.pos < 2 and self.speed > self.HI_SPEED:
131+
self.set_position(self.pos + 1)
132+
133+
def idle(self, persists):
134+
if persists:
135+
# increase idle time
136+
wait(1)
137+
self.idle_timer += 1
138+
if self.auto and self.idle_timer >= self.GEAR_RESET_TIMEOUT:
139+
# reset gearbox to lowest gear
140+
self.idle_timer = 0
141+
gearbox.set_position(0)
142+
else:
143+
self.idle_timer = 0
144+
145+
class Key:
146+
def __init__(self):
147+
# variables to store current and previous state of buttons
148+
self.now_pressed = []
149+
self.prev_pressed = []
150+
151+
def update(self, remote):
152+
# copy list of keys pressed during last update
153+
self.prev_pressed = list(self.now_pressed)
154+
# update list of pressed keys
155+
self.now_pressed = remote.buttons.pressed()
156+
157+
def pressed(self, key):
158+
# return whether key is now pressed
159+
return key in self.now_pressed
160+
161+
def released(self, key):
162+
# return keys which were released after last update
163+
return key in self.prev_pressed and key not in self.now_pressed
164+
165+
def direction(positive, negative):
166+
# return resultant value of two boolean directions
167+
return int(bool(positive)) - int(bool(negative))
168+
169+
if __name__ == '__main__':
170+
CONNECT_FLASHING_TIME = [75]*5 + [1000]
171+
hub = TechnicHub()
172+
# Flashing led while waiting connection as remote do
173+
hub.light.blink(Color.WHITE, CONNECT_FLASHING_TIME)
174+
175+
# Connect to the remote.
176+
remote = Remote()
177+
print('Remote connected.')
178+
179+
# Wait for calibration
180+
hub.light.on(Color.YELLOW)
181+
182+
# initialize driving motor
183+
drive = Motor(Port.A)
184+
185+
# initialize steering motor
186+
steer = Motor(Port.D)
187+
kp, ki, _, _, _ = steer.control.pid()
188+
steer.control.limits(speed=STEER_SPEED)
189+
steer.control.pid(kp=kp*STEER_HARDNESS, ki=ki*STEER_HARDNESS)
190+
191+
# initialize gearbox
192+
gearbox = Gearbox(remote, hub, drive)
193+
194+
# initialize remote keys
195+
key = Key()
196+
if LEFT_STEER_RIGHT_DRIVE:
197+
BUTTON_DRIVE_FWD, BUTTON_DRIVE_BACK = Button.RIGHT_PLUS, Button.RIGHT_MINUS
198+
BUTTON_STEER_LEFT, BUTTON_STEER_RIGHT = Button.LEFT_PLUS, Button.LEFT_MINUS
199+
else:
200+
BUTTON_DRIVE_FWD, BUTTON_DRIVE_BACK = Button.LEFT_PLUS, Button.LEFT_MINUS
201+
BUTTON_STEER_LEFT, BUTTON_STEER_RIGHT = Button.RIGHT_PLUS, Button.RIGHT_MINUS
202+
203+
# Calibration completed, start the FUN!
204+
hub.light.on(Color.GREEN)
205+
206+
# main loop
207+
while True:
208+
key.update(remote)
209+
210+
# gearbox control
211+
if key.released(Button.LEFT):
212+
# manual - change to lower gear; auto - switch to driving
213+
new_pos = gearbox.last_auto_pos if gearbox.auto else gearbox.pos-1
214+
gearbox.set_position(new_pos)
215+
elif key.released(Button.RIGHT):
216+
# manual - change to higher gear/dumper; auto - switch to dumper
217+
new_pos = 3 if gearbox.auto else gearbox.pos+1
218+
gearbox.set_position(new_pos)
219+
elif key.released(Button.CENTER):
220+
# switch gearbox mode to the other one
221+
gearbox.set_auto(not gearbox.auto)
222+
223+
# drive control
224+
drive_direction = direction(key.pressed(BUTTON_DRIVE_FWD),
225+
key.pressed(BUTTON_DRIVE_BACK))
226+
if drive_direction in [-1,1]:
227+
# change gear automatically, if gearbox is in AUTO mode
228+
gearbox.update_auto_gear()
229+
# for dumper, direction of rotation must be inverted
230+
invert = 1 if gearbox.dumper() else -1
231+
drive.dc(invert*drive_direction*100.0)
232+
# report active drive
233+
gearbox.idle(False)
234+
else:
235+
drive.stop()
236+
# report idle drive, if not set to dumper
237+
gearbox.idle(not gearbox.dumper())
238+
239+
# steering control
240+
steer_direction = direction(key.pressed(BUTTON_STEER_RIGHT),
241+
key.pressed(BUTTON_STEER_LEFT))
242+
steer.run_target(STEER_SPEED, steer_direction*STEER_ANGLE, wait=False)
112 KB
Loading

0 commit comments

Comments
 (0)