@@ -157,6 +157,7 @@ def __init__(self, port=DEFAULT_PORT_MATCH):
157157 self .ack_available = False
158158 self .in_name = self .out_name = None
159159 self ._reader = None
160+ self ._closing = False
160161
161162 def open (self ):
162163 self .port = self .port_match if self .port_match .startswith ("hw:" ) \
@@ -165,21 +166,25 @@ def open(self):
165166 raise RuntimeError ("Could not find an AMYboard ALSA MIDI device "
166167 "(match=%r); see `amidi -l`." % self .port_match )
167168 self .in_name = self .out_name = self .port
168- # Persistent receive stream: line-buffered so frames arrive in real time
169- # (block buffering would sit on the board's ACKs). Coexists fine with the
170- # one-shot `amidi -S` sends on the same rawmidi device.
171169 try :
172- self ._reader = subprocess .Popen (
173- ["stdbuf" , "-oL" , "amidi" , "-p" , self .port , "-d" ],
174- stdout = subprocess .PIPE , stderr = subprocess .DEVNULL , text = True , bufsize = 1 )
175- threading .Thread (target = self ._read_loop , daemon = True ).start ()
176- self .ack_available = True
170+ self ._spawn_reader ()
177171 except Exception as e :
178172 print ("[amyboardctl] no amidi read stream (%s); send-only" % e ,
179173 file = sys .stderr )
180174 return self
181175
176+ def _spawn_reader (self ):
177+ # Persistent receive stream: line-buffered so frames arrive in real time
178+ # (block buffering would sit on the board's ACKs). Coexists fine with the
179+ # one-shot `amidi -S` sends on the same rawmidi device.
180+ self ._reader = subprocess .Popen (
181+ ["stdbuf" , "-oL" , "amidi" , "-p" , self .port , "-d" ],
182+ stdout = subprocess .PIPE , stderr = subprocess .DEVNULL , text = True , bufsize = 1 )
183+ threading .Thread (target = self ._read_loop , daemon = True ).start ()
184+ self .ack_available = True
185+
182186 def close (self ):
187+ self ._closing = True
183188 if self ._reader :
184189 try :
185190 self ._reader .terminate ()
@@ -191,9 +196,10 @@ def close(self):
191196 def _read_loop (self ):
192197 """Parse the `amidi -d` hex dump into SysEx frames and dispatch each
193198 frame's payload (the bytes between F0 and F7) to on_sysex."""
199+ reader = self ._reader
194200 buf , insx = [], False
195201 try :
196- for line in self . _reader .stdout :
202+ for line in reader .stdout :
197203 for tok in line .split ():
198204 try :
199205 b = int (tok , 16 )
@@ -209,6 +215,33 @@ def _read_loop(self):
209215 buf .append (b )
210216 except Exception :
211217 pass
218+ # `amidi -d` dies when the board re-enumerates (its rawmidi node goes
219+ # away). That used to be silent — every later send_and_ack just timed
220+ # out and transfers aborted with no trace. Respawn it once the device
221+ # is back; if it never comes back, drop to send-only pacing and say so.
222+ if self ._closing :
223+ return
224+ print ("[amyboardctl] amidi read stream died (rc=%s); respawning"
225+ % reader .poll (), file = sys .stderr )
226+ self .ack_available = False
227+ deadline = time .time () + 15.0
228+ while not self ._closing and time .time () < deadline :
229+ time .sleep (1.0 )
230+ port = self .port_match if self .port_match .startswith ("hw:" ) \
231+ else _alsa_find (self .port_match )
232+ if not port :
233+ continue
234+ self .port = self .in_name = self .out_name = port
235+ try :
236+ self ._spawn_reader ()
237+ print ("[amyboardctl] amidi read stream back on %s" % port ,
238+ file = sys .stderr )
239+ return
240+ except Exception :
241+ continue
242+ if not self ._closing :
243+ print ("[amyboardctl] amidi read stream gone for good; "
244+ "continuing send-only (paced)" , file = sys .stderr )
212245
213246 def send (self , data ):
214247 cmd = ["amidi" , "-p" , self .port , "-S" , " " .join ("%02X" % b for b in data )]
0 commit comments