@@ -156,36 +156,58 @@ def read_until(self, ending, timeout=30):
156156 data += chunk
157157 return data
158158
159+ def _try_get_prompt (self , action , wait = 0.5 , drain_before = True ):
160+ """Send *action* bytes, then wait briefly for ``>>> ``."""
161+ if drain_before :
162+ self ._drain (0.3 )
163+ self .stream .write (action )
164+ time .sleep (wait )
165+ data = self ._drain (1.0 )
166+ if b">>> " in data :
167+ return True
168+ return False
169+
159170 def wait_for_boot (self , timeout = 30 ):
160171 t0 = time .monotonic ()
161172 data = b""
173+ # 1. Try ENTER first — device may already be at a prompt
174+ if self ._try_get_prompt (b"\r \n " , wait = 0.5 ):
175+ return
176+ # 2. Try Ctrl-B to exit raw REPL
177+ if self ._try_get_prompt (b"\x02 " , wait = 0.5 ):
178+ self .stream .write (b"\r \n " )
179+ time .sleep (0.2 )
180+ self ._drain (0.3 )
181+ return
182+ # 3. Main loop: poll for data, send Ctrl-C after 2s of silence
162183 ctrl_c_sent = False
163184 while time .monotonic () - t0 < timeout :
164185 if self ._data_waiting (0.1 ):
165186 chunk = self .stream .read (4096 )
166187 if chunk :
167188 data += chunk
168189 if b">>> " in data :
169- self ._drain (0.5 )
170- self .stream .write (b"\n " )
171- time .sleep (0.2 )
172190 self ._drain (0.3 )
173191 return
174192 elif not ctrl_c_sent and time .monotonic () - t0 > 2 :
175193 self .stream .write (b"\x03 " )
176194 time .sleep (0.3 )
177195 ctrl_c_sent = True
178- self .stream .write (b"\x02 " )
179- time .sleep (0.2 )
180- self ._drain (0.3 )
181- data = self .read_until (b">>> " , timeout = 5 )
182- if not data .endswith (b">>> " ):
183- raise TimeoutError (
184- "aioREPL prompt not found.\n " + data .decode ("utf-8" , "replace" )[- 2000 :]
185- )
186- self .stream .write (b"\n " )
187- time .sleep (0.3 )
188- self ._drain (0.5 )
196+ # 4. Fallback: drain, try Ctrl-B, then do one last read
197+ tail = self ._drain (1.0 )
198+ data += tail
199+ if self ._try_get_prompt (b"\x02 " , wait = 0.5 , drain_before = False ):
200+ return
201+ data += self .read_until (b">>> " , timeout = 5 )
202+ if b">>> " in data :
203+ self ._drain (0.3 )
204+ return
205+ leftover = self ._drain (2.0 )
206+ data += leftover
207+ raise TimeoutError (
208+ "aioREPL prompt not found.\n "
209+ + data .decode ("utf-8" , "replace" )[- 3000 :]
210+ )
189211
190212 def exec (self , code , timeout = 30 ):
191213 """
@@ -340,9 +362,24 @@ def exec_multiline(self, code):
340362 def eval (self , expr ):
341363 return self .repl .eval (expr )
342364
365+ # -- disk space ----------------------------------------------------
366+
367+ def check_free_space (self ):
368+ raw = self .exec (
369+ "import os; fs=os.statvfs('/'); print(fs[0]*fs[3])"
370+ )
371+ return int (raw .strip ().decode ("utf-8" ))
372+
343373 # -- screen capture ------------------------------------------------
344374
345375 def screenshot (self ):
376+ free = self .check_free_space ()
377+ needed = self ._width * self ._height * 3
378+ if free < needed :
379+ raise RuntimeError (
380+ "Insufficient free space for screenshot: "
381+ "{} bytes free, need at least {} bytes" .format (free , needed )
382+ )
346383 tmp = "/tmp/_mpos_shot.raw"
347384 code = (
348385 "import lvgl as lv; "
@@ -518,7 +555,20 @@ def exec_multiline(self, code):
518555 def eval (self , expr ):
519556 return self .repl .eval (expr )
520557
558+ def check_free_space (self ):
559+ raw = self .exec (
560+ "import os; fs=os.statvfs('/'); print(fs[0]*fs[3])"
561+ )
562+ return int (raw .strip ().decode ("utf-8" ))
563+
521564 def screenshot (self ):
565+ free = self .check_free_space ()
566+ needed = self ._width * self ._height * 3
567+ if free < needed :
568+ raise RuntimeError (
569+ "Insufficient free space for screenshot on device: "
570+ "{} bytes free, need at least {} bytes" .format (free , needed )
571+ )
522572 tmp = "/_mpos_shot.raw"
523573 code = (
524574 "import lvgl as lv; "
@@ -675,6 +725,9 @@ def eval(self, expr):
675725 def screenshot (self ):
676726 return self ._backend .screenshot ()
677727
728+ def check_free_space (self ):
729+ return self ._backend .check_free_space ()
730+
678731 def press (self , x , y ):
679732 self ._backend .press (x , y )
680733
@@ -715,7 +768,7 @@ def main():
715768 parser = argparse .ArgumentParser (description = "MicroPythonOS Controller" )
716769 parser .add_argument (
717770 "action" , nargs = "?" , default = "exec" ,
718- help = "Action: exec, eval, screenshot (default: exec)" ,
771+ help = "Action: exec, eval, screenshot, startapp, checkfreespace (default: exec)" ,
719772 )
720773 parser .add_argument ("args" , nargs = "*" , help = "Arguments" )
721774 parser .add_argument ("--binary" , help = "Path to lvgl_micropy_unix binary" )
@@ -724,11 +777,16 @@ def main():
724777 "--serial-port" , help = "Serial port for device (e.g. /dev/ttyACM0)"
725778 )
726779 parser .add_argument ("--baudrate" , type = int , default = 115200 )
780+ parser .add_argument (
781+ "--no-reset" , action = "store_true" ,
782+ help = "Skip DTR/RTS reset on serial connect (device already running)"
783+ )
727784 args = parser .parse_args ()
728785
729786 if args .serial_port :
730787 ctrl = MPOSController (
731- backend = "serial" , port = args .serial_port , baudrate = args .baudrate
788+ backend = "serial" , port = args .serial_port ,
789+ baudrate = args .baudrate , reset = not args .no_reset
732790 )
733791 else :
734792 ctrl = MPOSController (binary = args .binary , heapsize = args .heapsize )
@@ -761,6 +819,24 @@ def main():
761819 with open (path , "wb" ) as f :
762820 f .write (bmp )
763821 print ("Wrote" , path , "({} bytes)" .format (len (bmp )))
822+ elif args .action == "startapp" :
823+ if not args .args :
824+ print ("error: app name required" , file = sys .stderr )
825+ return 1
826+ appname = args .args [0 ]
827+ with ctrl :
828+ out = ctrl .exec ("from mpos import AppManager ; AppManager.start_app({!r})" .format (appname ))
829+ sys .stdout .buffer .write (out )
830+ sys .stdout .buffer .write (b"\n " )
831+ elif args .action == "checkfreespace" :
832+ with ctrl :
833+ free = ctrl .check_free_space ()
834+ needed = ctrl .display_size [0 ] * ctrl .display_size [1 ] * 3
835+ print ("Free: {} bytes, need for screenshot: {} bytes" .format (free , needed ))
836+ if free < needed :
837+ print ("WARNING: not enough space for a screenshot!" )
838+ else :
839+ print ("OK" )
764840 else :
765841 parser .print_help ()
766842 return 1
0 commit comments