From ac3b3b1e18cd2a51b4c851d2c417284a206d5204 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Van=C4=9Bk?= <linuxtardis@gmail.com>
Date: Wed, 24 May 2023 13:16:04 +0200
Subject: [PATCH] Update linefollower for presentation

---
 programs/examples/ev3/linefollower.py | 153 +++++++++++++++-----------
 1 file changed, 91 insertions(+), 62 deletions(-)

diff --git a/programs/examples/ev3/linefollower.py b/programs/examples/ev3/linefollower.py
index 540d333..9ecbbd4 100644
--- a/programs/examples/ev3/linefollower.py
+++ b/programs/examples/ev3/linefollower.py
@@ -1,6 +1,7 @@
 import sys
 
 import utime
+from micropython import const
 
 from lib.cube.sh1106 import SH1106_I2C
 from lib.ev3 import ColorSensor, Motor
@@ -11,29 +12,88 @@ CALIBRATION_RPM = 30
 CALIBRATION_TIME = 1000
 CALIBRATION_WAIT_UNTIL_DONE = 100
 REG_SP = 0.5
-REG_KP = 75
-REG_KD = 5000
-REG_BIAS = 40
-REG_OUT_SIGN = +1
-OPTIMAL_DT = 10
+REG_KP = 120
+REG_KD = 20000
+REG_KD_DNOW = 0.50
+REG_BIAS = 60
+APPROX_DT = 2
+
+MODE_KP = const(0)
+MODE_KD = const(1)
+MODE_SPD = const(2)
+MODE_DNOW = const(3)
+NUM_MODES = const(4)
+
+
+class TunerUI:
+    def __init__(self, cube: Robot):
+        self.Kp = REG_KP
+        self.Kd = REG_KD
+        self.Dnow = REG_KD_DNOW
+        self.bias = REG_BIAS
+        self.cube = cube
+        self.mode = MODE_KP
+        self.last_buttons = cube.buttons.pressed()
+
+    def render_ui(self):
+        display = self.cube.display
+        display.fill(0)
+        display.centered_text("Open-Cube", 0)
+        display.centered_text("Linefollower", 8)
+
+        display.centered_text("Live tuning", 24)
+        display.text(f"{'*' if self.mode == MODE_KP else ' '} Kp = {self.Kp:.1f}", 0, 32)
+        display.text(f"{'*' if self.mode == MODE_KD else ' '} Kd = {self.Kd:.1f}", 0, 40)
+        display.text(f"{'*' if self.mode == MODE_SPD else ' '} Pwr = {self.bias:.1f}", 0, 48)
+        display.text(f"{'*' if self.mode == MODE_DNOW else ' '} Dnow = {self.Dnow:.2f}", 0, 56)
+        display.show()
+
+    def update_ui(self, force_render: bool = False):
+        now = self.cube.buttons.pressed()
+        last = self.last_buttons
+        self.last_buttons = now
+
+        if now[Button.UP] and not last[Button.UP]:
+            self.mode = (self.mode - 1) % NUM_MODES
+            force_render = True
+        if now[Button.DOWN] and not last[Button.DOWN]:
+            self.mode = (self.mode + 1) % NUM_MODES
+            force_render = True
+        if now[Button.LEFT] and not last[Button.LEFT]:
+            if self.mode == MODE_KP:
+                self.Kp -= 5
+            elif self.mode == MODE_KD:
+                self.Kd -= 500
+            elif self.mode == MODE_SPD:
+                self.bias -= 5
+            elif self.mode == MODE_DNOW:
+                self.Dnow -= 0.1
+            force_render = True
+        if now[Button.RIGHT] and not last[Button.RIGHT]:
+            if self.mode == MODE_KP:
+                self.Kp += 5
+            elif self.mode == MODE_KD:
+                self.Kd += 500
+            elif self.mode == MODE_SPD:
+                self.bias += 5
+            elif self.mode == MODE_DNOW:
+                self.Dnow += 0.1
+            force_render = True
+
+        if force_render:
+            self.render_ui()
 
 
 class LineFollower:
     def __init__(self, robot: Robot):
         self.robot = robot
         self.display = robot.display
-        self.cs = ColorSensor(Sensor.S1)
+        self.cs = ColorSensor(Port.S1)
         self.lm = Motor(Port.M1)
-        self.rm = Motor(Port.M4)
+        self.rm = Motor(Port.M2)
         self.light_min = None
         self.light_max = None
-        self.light_sp = None
-        self.loop_times = [0] * 1000
-        self.loop_times_idx = 0
-        self.loop_count = 0
-        self.loop_min = None
-        self.loop_max = None
-        self.loop_skip = True
+        self.ui = TunerUI(robot)
 
     def wait_for_sensors(self):
         self.display.fill(0)
@@ -48,63 +108,32 @@ class LineFollower:
         self.wait_for_sensors()
         self.calibrate_light()
 
-        last_t = utime.ticks_ms()
-        utime.sleep_ms(1)
         err_last = None
+        derr_last = None
 
-        self.display.fill(0)
-        self.display.text("Running...", 0, 0, 1)
-        self.display.show()
-        while True:
-            now_t = utime.ticks_ms()
-            delta_t = utime.ticks_diff(now_t, last_t)
-            last_t = now_t
+        self.ui.render_ui()
 
+        while True:
             light = self.read_calibrated_light()
             err = REG_SP - light
 
-            if err_last is not None:
-                derr = (err - err_last) / delta_t
-            else:
+            if err_last is None:
                 derr = 0
+            else:
+                derr_now = (err - err_last) / APPROX_DT
+                if derr_last is None:
+                    derr = derr_now
+                else:
+                    derr = self.ui.Dnow * derr_now + (1 - self.ui.Dnow) * derr_last
+                derr_last = derr
             err_last = err
 
-            u = REG_KP * err + REG_KD * derr
-
-            spd_r = REG_BIAS + u * REG_OUT_SIGN
-            spd_l = REG_BIAS - u * REG_OUT_SIGN
-            self.rm.dc(round(-spd_r))
-            self.lm.dc(round(-spd_l))
-
-            self.poll_buttons()
-
-            elapsed = utime.ticks_diff(utime.ticks_ms(), last_t)
-            if elapsed < OPTIMAL_DT:
-                utime.sleep_ms(OPTIMAL_DT - elapsed - 1)
-
-    def poll_buttons(self):
-        global REG_BIAS
-        self.robot.buttons.read_value()
-        buttons = self.robot.buttons.pressed()
-
-        pressed = None
-        if buttons[Button.UP]:
-            REG_BIAS += 5
-            pressed = Button.UP
-        elif buttons[Button.DOWN]:
-            REG_BIAS -= 5
-            pressed = Button.DOWN
-        if pressed is not None:
-            self.display.fill(0)
-            self.display.text("> Custom speed  <", 0, 0, 1)
-            self.display.text(f"Kp={REG_KP:3d}", 0, 10, 1)
-            self.display.text(f"Kd={REG_KD:3d}", 0, 18, 1)
-            self.display.text(f"Spd={REG_BIAS:3d}", 0, 26, 1)
-            self.display.show()
-            while True:
-                self.robot.buttons.read_value()
-                if self.robot.buttons.pressed()[pressed]:
-                    utime.sleep_ms(1)
+            u = self.ui.Kp * err + self.ui.Kd * derr
+
+            self.lm.dc(round(self.ui.bias + u))
+            self.rm.dc(round(self.ui.bias - u))
+
+            self.ui.update_ui()
 
     def calibrate_light(self):
         self.display.fill(0)
-- 
GitLab