Skip to content

Commit 4b7d3d5

Browse files
...
1 parent c659e6b commit 4b7d3d5

4 files changed

Lines changed: 995 additions & 9 deletions

File tree

scripts/line_follower.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import RPi.GPIO as GPIO
1+
from gpiozero import LineSensor
22
import time
33

44
# Define pin numbers
@@ -31,3 +31,102 @@
3131

3232
finally:
3333
GPIO.cleanup()
34+
35+
class LineFollower:
36+
def __init__(self, left_pin=17, right_pin=27, motors=None):
37+
self.left_sensor = LineSensor(left_pin)
38+
self.right_sensor = LineSensor(right_pin)
39+
self.motors = motors
40+
41+
# Speed tuning
42+
self.base_speed = 0.45
43+
self.turn_speed = 0.35
44+
self.max_speed = 0.6
45+
46+
# State
47+
self.last_direction = "forward"
48+
self.lost_count = 0
49+
50+
# Recovery system (like your obstacle avoider)
51+
self.recovery_mode = False
52+
self.recovery_direction = None
53+
self.recovery_start = 0
54+
self.recovery_time = 1.5
55+
56+
def read_sensors(self):
57+
return self.left_sensor.value, self.right_sensor.value
58+
59+
def _start_recovery(self):
60+
self.recovery_mode = True
61+
self.recovery_start = time.time()
62+
63+
# Commit to last known direction
64+
if self.last_direction == "left":
65+
self.recovery_direction = "left"
66+
elif self.last_direction == "right":
67+
self.recovery_direction = "right"
68+
else:
69+
self.recovery_direction = "left"
70+
71+
print(f"🔄 LINE LOST — recovering {self.recovery_direction.upper()}")
72+
73+
def _handle_recovery(self):
74+
if time.time() - self.recovery_start > self.recovery_time:
75+
self.recovery_mode = False
76+
self.lost_count = 0
77+
print("✅ Line reacquire timeout")
78+
return False
79+
80+
if self.recovery_direction == "left":
81+
self.motors.turn_left(self.turn_speed)
82+
else:
83+
self.motors.turn_right(self.turn_speed)
84+
85+
return True
86+
87+
def follow_line(self):
88+
if self.motors is None:
89+
return False
90+
91+
left, right = self.read_sensors()
92+
93+
# 🧠 RECOVERY MODE
94+
if self.recovery_mode:
95+
return self._handle_recovery()
96+
97+
# 🟢 ON TRACK
98+
if left and right:
99+
self.motors.forward(self.max_speed)
100+
self.last_direction = "forward"
101+
self.lost_count = 0
102+
return False
103+
104+
# ↩️ DRIFT RIGHT → correct LEFT
105+
elif not left and right:
106+
self.motors.turn_left(self.turn_speed)
107+
self.last_direction = "left"
108+
self.lost_count = 0
109+
return True
110+
111+
# ↪️ DRIFT LEFT → correct RIGHT
112+
elif left and not right:
113+
self.motors.turn_right(self.turn_speed)
114+
self.last_direction = "right"
115+
self.lost_count = 0
116+
return True
117+
118+
# ❌ LINE LOST
119+
else:
120+
self.lost_count += 1
121+
122+
if self.lost_count > 5:
123+
self._start_recovery()
124+
125+
self.motors.stop()
126+
return True
127+
128+
def cleanup(self):
129+
if self.motors:
130+
self.motors.stop()
131+
self.left_sensor.close()
132+
self.right_sensor.close()
File renamed without changes.

scripts/server.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
# server.py
2+
# KIDA Web Control Server (Improved)
23

3-
from flask import Flask, jsonify, request
4+
from flask import Flask, jsonify, request, Response
5+
import threading
6+
import queue
7+
import cv2
8+
from picamera2 import Picamera2
49

510
app = Flask(__name__)
11+
command_queue = queue.Queue()
12+
13+
# --- Camera Setup ---
14+
picam2 = Picamera2()
15+
picam2.configure(picam2.create_preview_configuration())
16+
picam2.start()
617

718
# --- Routes ---
819
@app.route('/')
920
def home():
10-
return "Welcome to KIDA's Flask Server!"
21+
return """
22+
<h1>KIDA Control</h1>
23+
<p>Status: Online</p>
24+
<img src="/video_feed" width="480">
25+
"""
1126

1227
@app.route('/status')
1328
def status():
@@ -19,12 +34,45 @@ def status():
1934

2035
@app.route('/command', methods=['POST'])
2136
def receive_command():
22-
data = request.get_json()
23-
command = data.get('command')
24-
# Process the command here (e.g., send to robot controller)
25-
print(f"📡 Received command: {command}")
26-
return jsonify({'received': command, 'status': 'executed'})
37+
try:
38+
data = request.get_json()
39+
command = data.get('command')
40+
41+
print(f"📡 Received command: {command}")
42+
command_queue.put(command)
43+
44+
return jsonify({'received': command, 'status': 'queued'})
45+
except Exception as e:
46+
return jsonify({'error': str(e)}), 400
47+
48+
# --- Video Streaming ---
49+
def generate_frames():
50+
while True:
51+
frame = picam2.capture_array()
52+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
53+
54+
_, buffer = cv2.imencode('.jpg', frame)
55+
frame_bytes = buffer.tobytes()
56+
57+
yield (b'--frame\r\n'
58+
b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
59+
60+
@app.route('/video_feed')
61+
def video_feed():
62+
return Response(generate_frames(),
63+
mimetype='multipart/x-mixed-replace; boundary=frame')
64+
65+
# --- Command Processor Thread ---
66+
def command_worker():
67+
while True:
68+
cmd = command_queue.get()
69+
print(f"🤖 Executing: {cmd}")
70+
71+
# Hook into your motor system here
72+
# Example:
73+
# if cmd == "forward": motors.forward()
2774

2875
# --- Start Server ---
2976
if __name__ == '__main__':
30-
app.run(host='0.0.0.0', port=5000, debug=True)
77+
threading.Thread(target=command_worker, daemon=True).start()
78+
app.run(host='0.0.0.0', port=5000, debug=False)

0 commit comments

Comments
 (0)