2
2
from modules .config import Config
3
3
from time import sleep
4
4
from pubsub import pub
5
+ import serial
6
+ from modules .neopixel .neopx import get_shared_esp32_serial
7
+ import threading
8
+ import sys
9
+ import os
10
+
11
+ # Add path to SerialShareManager
12
+ sys .path .append (os .path .dirname (os .path .dirname (os .path .dirname (os .path .dirname (__file__ )))))
13
+ from modules .SerialShareManager import get_serial
14
+
15
+ # No longer need a global ESP32 connection - using the shared one
5
16
6
17
class PiServo :
7
18
8
19
def __init__ (self , ** kwargs ):
9
20
"""
10
21
PiServo class
11
- :param kwargs: pin, range, start
22
+ :param kwargs: pin, range, start, protocol, id, name
12
23
:param pin: GPIO pin number
13
24
:param range: [min, max] angle range
14
25
:param start: initial angle
26
+ :param protocol: 'GPIO' (default) or 'ESP32'
27
+ :param id: Servo ID for identification
28
+ :param name: Servo name for logging and subscription
15
29
16
30
Install: pip install gpiozero
17
31
18
- Subscribes to 'piservo:move' to move servo
32
+ Subscribes to 'piservo:move' to move any servo
33
+ - Argument: angle (int) - angle to move servo
34
+
35
+ Subscribes to 'piservo:move:{name}' to move specific servo
19
36
- Argument: angle (int) - angle to move servo
20
37
21
38
Example:
22
- pub.sendMessage('piservo:move', angle=90)
39
+ pub.sendMessage('piservo:move', angle=90)
40
+ pub.sendMessage('piservo:move:ear', angle=45)
23
41
"""
24
-
25
42
self .pin = kwargs .get ('pin' )
26
43
self .range = kwargs .get ('range' )
27
44
self .start = kwargs .get ('start' , 0 )
45
+ self .id = kwargs .get ('id' , 0 )
46
+ self .name = kwargs .get ('name' , f'servo_{ self .id } ' )
28
47
self .servo = None
48
+ self .serial = None # Will be set if ESP32 protocol is used
49
+ self .verbose_logging = kwargs .get ('verbose_logging' , False ) # Add logging control
50
+
51
+ # Protokol desteği ekleme
52
+ accepted_protocols = ['GPIO' , 'ESP32' ]
53
+ self .protocol = kwargs .get ('protocol' , 'GPIO' )
54
+ if self .protocol not in accepted_protocols :
55
+ raise ValueError ("Invalid protocol specified. Choose one of: " + ", " .join (accepted_protocols ))
56
+
57
+ # ESP32 protocol handling with improved retry logic
58
+ if self .protocol == 'ESP32' :
59
+ # Subscribe to both events - the original and the new global event
60
+ pub .subscribe (self .on_serial_ready , 'esp32:serial_ready' )
61
+ pub .subscribe (self .on_serial_ready , 'esp32:serial_global_ready' )
62
+
63
+ # Try to get the shared serial connection now - it might already be available
64
+ self .serial = get_serial (f'servo_{ self .name } ' )
65
+ if not self .serial :
66
+ # Try the old way too as fallback
67
+ self .serial = get_shared_esp32_serial ()
68
+
69
+ # Start retry timer with increased initial delay
70
+ retry_timer = threading .Timer (5.0 , self .retry_get_serial )
71
+ retry_timer .daemon = True
72
+ retry_timer .start ()
73
+
74
+ # Main movement subscription
29
75
pub .subscribe (self .move , 'piservo:move' )
30
- # print(self.range)
31
- # self.move(0)
32
- # sleep(2)
33
- # self.move(self.range[0])
34
- # sleep(2)
35
- # self.move(self.range[1])
36
- # sleep(2)
37
- self .move (self .start )
38
76
77
+ # Servo-specific subscription
78
+ specific_topic = f'piservo:move:{ self .name } '
79
+ pub .subscribe (self .move , specific_topic )
80
+
81
+ # Initialize to starting position with increased delay
82
+ timer = threading .Timer (2.0 , lambda : self .move (self .start ))
83
+ timer .daemon = True
84
+ timer .start ()
85
+
86
+ def retry_get_serial (self ):
87
+ """Retry getting the serial connection with progressive delay"""
88
+ if self .serial is None :
89
+ # First try new method
90
+ self .serial = get_serial (f'servo_{ self .name } ' )
91
+ if not self .serial :
92
+ # Fall back to old method
93
+ self .serial = get_shared_esp32_serial ()
94
+
95
+ if self .serial :
96
+ if self .verbose_logging :
97
+ pub .sendMessage ('log' , msg = f'[SERVO] { self .name } connected to shared ESP32 serial on retry' )
98
+ self .move (self .start )
99
+ else :
100
+ # Progressive backoff - increase delay each time
101
+ delay = min (10.0 , 2.0 + len (self .name ) % 3 ) # Vary delay slightly per instance
102
+ retry_timer = threading .Timer (delay , self .retry_get_serial )
103
+ retry_timer .daemon = True
104
+ retry_timer .start ()
39
105
106
+ def on_serial_ready (self ):
107
+ """Called when the shared serial connection becomes available"""
108
+ # Get the serial connection and log
109
+ self .serial = get_shared_esp32_serial ()
110
+ if self .serial :
111
+ if self .verbose_logging :
112
+ pub .sendMessage ('log' , msg = f'[SERVO] { self .name } connected to shared ESP32 serial' )
113
+ # Move to start position once serial is available
114
+ self .move (self .start )
115
+
40
116
def move (self , angle ):
117
+ """
118
+ Move servo to specified angle
119
+ :param angle: Angle to move servo to
120
+ """
121
+ # Açı değerini izin verilen aralıkta tut
122
+ if self .range :
123
+ if angle < self .range [0 ]:
124
+ angle = self .range [0 ]
125
+ if self .verbose_logging :
126
+ pub .sendMessage ('log' , msg = f'[SERVO] { self .name } : Angle limited to minimum { self .range [0 ]} ' )
127
+ elif angle > self .range [1 ]:
128
+ angle = self .range [1 ]
129
+ if self .verbose_logging :
130
+ pub .sendMessage ('log' , msg = f'[SERVO] { self .name } : Angle limited to maximum { self .range [1 ]} ' )
131
+
132
+ # ESP32 protokolü kullanıyorsa
133
+ if self .protocol == 'ESP32' :
134
+ self .send_servo_command (angle )
135
+ return
136
+
137
+ # GPIO protokolü için orijinal kod
41
138
if self .servo is None :
42
139
self .servo = AngularServo (self .pin , min_angle = self .range [0 ], max_angle = self .range [1 ], initial_angle = self .start )
43
140
self .servo .angle = angle # Changes the angle (to move the servo)
44
141
sleep (1 ) # @TODO: Remove this sleep
45
- self .servo .detach () # Detaches the servo (to stop jitter)
142
+ self .servo .detach () # Detaches the servo (to stop jitter)
143
+
144
+ def send_servo_command (self , angle ):
145
+ """
146
+ Send servo command to ESP32
147
+ :param angle: Angle to move servo to
148
+ """
149
+ if self .protocol != 'ESP32' :
150
+ return
151
+
152
+ if self .serial is None :
153
+ if self .verbose_logging :
154
+ pub .sendMessage ('log' , msg = f'[SERVO] { self .name } : Cannot send command - no serial connection' )
155
+ # Try to reconnect to serial
156
+ self .serial = get_shared_esp32_serial ()
157
+ if self .serial is None :
158
+ return
159
+
160
+ # Constrain angle to valid range
161
+ if self .range :
162
+ if angle < self .range [0 ]:
163
+ angle = self .range [0 ]
164
+ elif angle > self .range [1 ]:
165
+ angle = self .range [1 ]
166
+
167
+ # Create and send command
168
+ cmd = f"SERVO { self .pin } { angle } \n "
169
+ # Only log actual movement commands when verbose is enabled
170
+ if self .verbose_logging :
171
+ pub .sendMessage ('log' , msg = f'[SERVO] { self .name } : Sending command: { cmd .strip ()} ' )
172
+
173
+ try :
174
+ self .serial .write (cmd .encode ())
175
+ sleep (0.1 ) # Brief pause for command processing
176
+ except Exception as e :
177
+ pub .sendMessage ('log' , msg = f'[SERVO] ERROR: Failed to send command to ESP32: { e } ' )
178
+ self .serial = None # Reset the serial connection to trigger a reconnect
179
+
180
+ def detach (self ):
181
+ """
182
+ Detach the servo to prevent jitter
183
+ """
184
+ if self .protocol == 'ESP32' and self .serial is not None :
185
+ cmd = f"SERVO_DETACH { self .pin } \n "
186
+ if self .verbose_logging :
187
+ pub .sendMessage ('log' , msg = f'[SERVO] { self .name } : Detaching servo on pin { self .pin } ' )
188
+ try :
189
+ self .serial .write (cmd .encode ())
190
+ except Exception as e :
191
+ pub .sendMessage ('log' , msg = f'[SERVO] ERROR: Failed to send detach command: { e } ' )
192
+ elif self .servo :
193
+ self .servo .detach ()
194
+
195
+ def __del__ (self ):
196
+ """
197
+ Clean up resources when object is destroyed
198
+ """
199
+ self .detach ()
0 commit comments