flame-wro-fe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +1196 -0
- package/arduino/outputProxy/platformio.ini +16 -0
- package/arduino/outputProxy/src/main.cpp +276 -0
- package/index.js +8 -0
- package/package.json +31 -0
- package/raspySrc/main.py +12 -0
- package/raspySrc/raspy/control_panel.py +586 -0
- package/raspySrc/raspy/decision_log.py +85 -0
- package/raspySrc/raspy/drive_state.py +160 -0
- package/raspySrc/raspy/install_vehicle_service.sh +3 -0
- package/raspySrc/raspy/lap_direction.py +24 -0
- package/raspySrc/raspy/robot_runtime.py +1052 -0
- package/raspySrc/raspy/settings.json +198 -0
- package/raspySrc/raspy/settings.py +206 -0
- package/raspySrc/raspy/vehicle-runtime.service +12 -0
- package/raspySrc/raspy/vision_pipeline.py +142 -0
- package/raspySrc/raspy/vision_types.py +42 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StateMachine:
|
|
5
|
+
def __init__(self, behavior=None):
|
|
6
|
+
self.behavior = behavior or {}
|
|
7
|
+
self.current_state = "STARTING"
|
|
8
|
+
self.last_state_time = time.monotonic()
|
|
9
|
+
self.round_dir = int(self.behavior.get("fixed_round_dir", 0))
|
|
10
|
+
self.turns_left = int(self.behavior.get("turns", 12))
|
|
11
|
+
self.time_diff = 0.0
|
|
12
|
+
self.search_for_dir = self.round_dir == 0
|
|
13
|
+
self._round_dir_votes = 0
|
|
14
|
+
self._scheduled_state = None
|
|
15
|
+
self._scheduled_state_block = False
|
|
16
|
+
self._time_last_avoid = -999.0
|
|
17
|
+
self.next_pillar = None
|
|
18
|
+
self.is_pillar_round = bool(self.behavior.get("pillars", False))
|
|
19
|
+
|
|
20
|
+
if self.round_dir != 0:
|
|
21
|
+
self.round_dir = 1 if self.round_dir > 0 else -1
|
|
22
|
+
|
|
23
|
+
def transition_state(self, new_state):
|
|
24
|
+
self.current_state = new_state
|
|
25
|
+
self.last_state_time = time.monotonic()
|
|
26
|
+
|
|
27
|
+
def schedule_state_transition(self, new_state, delay_seconds, block=True):
|
|
28
|
+
self._scheduled_state = (new_state, time.monotonic() + float(delay_seconds))
|
|
29
|
+
self._scheduled_state_block = block
|
|
30
|
+
|
|
31
|
+
def add_round_dir_vote(self, vote):
|
|
32
|
+
if not self.search_for_dir or vote == 0:
|
|
33
|
+
return
|
|
34
|
+
self._round_dir_votes += 1 if vote > 0 else -1
|
|
35
|
+
threshold = int(self.behavior.get("round_dir_vote_threshold", 10))
|
|
36
|
+
if abs(self._round_dir_votes) >= threshold:
|
|
37
|
+
self.round_dir = 1 if self._round_dir_votes > 0 else -1
|
|
38
|
+
self.search_for_dir = False
|
|
39
|
+
self.transition_state("PD-CENTER")
|
|
40
|
+
|
|
41
|
+
def should_transition_state(self, portion_orange, portion_blue, pillars):
|
|
42
|
+
now = time.monotonic()
|
|
43
|
+
self.time_diff = now - self.last_state_time
|
|
44
|
+
|
|
45
|
+
if self._scheduled_state is not None:
|
|
46
|
+
new_state, scheduled_time = self._scheduled_state
|
|
47
|
+
if now >= scheduled_time:
|
|
48
|
+
self.transition_state(new_state)
|
|
49
|
+
self._scheduled_state = None
|
|
50
|
+
return True
|
|
51
|
+
if self._scheduled_state_block:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
if self.current_state == "STARTING":
|
|
55
|
+
if pillars and self.is_pillar_round:
|
|
56
|
+
next_pillar = pillars[0]
|
|
57
|
+
area = next_pillar.area
|
|
58
|
+
seen_frames = int(getattr(next_pillar, "seen_frames", 1))
|
|
59
|
+
if (
|
|
60
|
+
area > self.behavior.get("pillar_track_area", 190)
|
|
61
|
+
and seen_frames >= self.behavior.get("pillar_track_confirm_frames", 1)
|
|
62
|
+
and not next_pillar.ignore
|
|
63
|
+
):
|
|
64
|
+
self.next_pillar = next_pillar
|
|
65
|
+
self.transition_state("TRACKING-PILLAR")
|
|
66
|
+
return True
|
|
67
|
+
if self.round_dir != 0 and not self.search_for_dir:
|
|
68
|
+
self.transition_state("PD-CENTER")
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
if self.current_state == "PD-CENTER" and self.turns_left <= 0 and self._scheduled_state is None:
|
|
73
|
+
self.schedule_state_transition(
|
|
74
|
+
"DONE",
|
|
75
|
+
self.behavior.get("finish_delay_seconds", 3.1),
|
|
76
|
+
False,
|
|
77
|
+
)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
self.current_state in ("TURNING-L", "TURNING-R", "PD-CENTER")
|
|
82
|
+
and self.time_diff < self.behavior.get("state_hold_seconds", 0.4)
|
|
83
|
+
):
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
if pillars and self.is_pillar_round:
|
|
87
|
+
next_pillar = pillars[0]
|
|
88
|
+
area = next_pillar.area
|
|
89
|
+
seen_frames = int(getattr(next_pillar, "seen_frames", 1))
|
|
90
|
+
if self.current_state == "PD-CENTER":
|
|
91
|
+
self.next_pillar = next_pillar
|
|
92
|
+
if (
|
|
93
|
+
area > self.behavior.get("pillar_track_area", 190)
|
|
94
|
+
and seen_frames >= self.behavior.get("pillar_track_confirm_frames", 1)
|
|
95
|
+
and not next_pillar.ignore
|
|
96
|
+
):
|
|
97
|
+
self.transition_state("TRACKING-PILLAR")
|
|
98
|
+
return True
|
|
99
|
+
elif self.current_state in ("TRACKING-PILLAR", "PD-CENTER"):
|
|
100
|
+
if (
|
|
101
|
+
area > self.behavior.get("pillar_avoid_area", 530)
|
|
102
|
+
and seen_frames >= self.behavior.get("pillar_avoid_confirm_frames", 1)
|
|
103
|
+
and now - self._time_last_avoid > self.behavior.get("pillar_avoid_cooldown_seconds", 1.0)
|
|
104
|
+
):
|
|
105
|
+
self.transition_state("AVOIDING-R" if next_pillar.color == "RED" else "AVOIDING-G")
|
|
106
|
+
self.next_pillar = None
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
if self.current_state == "TRACKING-PILLAR" and not pillars:
|
|
110
|
+
self.transition_state("PD-CENTER")
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
if self.current_state in ("AVOIDING-R", "AVOIDING-G"):
|
|
114
|
+
if self.time_diff > self.behavior.get("pillar_avoid_seconds", 2.5):
|
|
115
|
+
self.transition_state("PD-CENTER")
|
|
116
|
+
self._time_last_avoid = now
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
if self.current_state in ("TURNING-L", "TURNING-R"):
|
|
120
|
+
if self.time_diff > self.behavior.get("turn_timeout_seconds", 0.85):
|
|
121
|
+
self.transition_state("PD-CENTER")
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
min_portion = self.behavior.get("line_marker_min_portion", 0.25)
|
|
126
|
+
if portion_blue > min_portion:
|
|
127
|
+
if self.current_state != "TURNING-R" and self.round_dir < 0:
|
|
128
|
+
self.turns_left -= 1
|
|
129
|
+
delay = self.behavior.get(
|
|
130
|
+
"pillar_turn_delay_seconds" if self.is_pillar_round else "turn_delay_seconds",
|
|
131
|
+
0.3 if self.is_pillar_round else 0.7,
|
|
132
|
+
)
|
|
133
|
+
self.schedule_state_transition("TURNING-L", delay)
|
|
134
|
+
else:
|
|
135
|
+
self.transition_state("PD-CENTER")
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
if portion_orange > min_portion:
|
|
139
|
+
if self.current_state != "TURNING-L" and self.round_dir > 0:
|
|
140
|
+
self.turns_left -= 1
|
|
141
|
+
delay = self.behavior.get(
|
|
142
|
+
"pillar_turn_delay_seconds" if self.is_pillar_round else "turn_delay_seconds",
|
|
143
|
+
0.3 if self.is_pillar_round else 0.7,
|
|
144
|
+
)
|
|
145
|
+
self.schedule_state_transition("TURNING-R", delay)
|
|
146
|
+
else:
|
|
147
|
+
self.transition_state("PD-CENTER")
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# Backwards-compatible names for older scripts.
|
|
153
|
+
def transitionState(self, new_state):
|
|
154
|
+
return self.transition_state(new_state)
|
|
155
|
+
|
|
156
|
+
def scheduleStateTransition(self, new_state, time_diff, block=True):
|
|
157
|
+
return self.schedule_state_transition(new_state, time_diff, block)
|
|
158
|
+
|
|
159
|
+
def shouldTransitionState(self, portion_orange, portion_blue, pillars):
|
|
160
|
+
return self.should_transition_state(portion_orange, portion_blue, pillars)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def find_round_dir(black_img):
|
|
6
|
+
if black_img is None or black_img.size == 0:
|
|
7
|
+
return 0
|
|
8
|
+
|
|
9
|
+
edges_img = cv2.Canny(black_img, 30, 90, 3)
|
|
10
|
+
if edges_img.size == 0:
|
|
11
|
+
return 0
|
|
12
|
+
|
|
13
|
+
edges_img[0, :] = 0
|
|
14
|
+
edges_img[-1, :] = 0
|
|
15
|
+
wall_heights = np.argmax(edges_img, axis=0)
|
|
16
|
+
wall_heights = np.where(wall_heights == 0, edges_img.shape[0] - 1, wall_heights)
|
|
17
|
+
differences = np.diff(wall_heights)
|
|
18
|
+
|
|
19
|
+
min_jump = 9
|
|
20
|
+
counter_clockwise = np.sum(differences > min_jump)
|
|
21
|
+
clockwise = np.sum(differences < -min_jump)
|
|
22
|
+
if clockwise == counter_clockwise:
|
|
23
|
+
return 0
|
|
24
|
+
return -1 if clockwise > counter_clockwise else 1
|