44from hashlib import sha256
55from io import BytesIO
66
7+ try :
8+ from os import dupterm
9+ except ImportError :
10+ # unix mpy doesn't have dupterm; define a no-op version
11+ def dupterm (stream_object , index = 0 ):
12+ return stream_object
13+
14+
715from micropython import const
816
917from mqterm import VERSION
@@ -22,6 +30,7 @@ def __init__(self, cmd, args=[], client_id="localhost", **kwargs):
2230 f"Wrong number of arguments for { cmd } ; expected { self .argc } "
2331 )
2432
33+ self .globals = kwargs .get ("globals" , {})
2534 self .cmd = cmd
2635 self .args = args
2736 self .client_id = client_id
@@ -45,13 +54,31 @@ def ready(self):
4554 return True
4655
4756 @classmethod
48- def from_cmd (cls , cmd_str , client_id = None ):
49- """Create a job from a command string, e.g. 'get_file file1.txt'."""
50- cmd , * args = cmd_str .split ()
57+ def from_cmd (cls , cmd_str , client_id = None , globals = {}):
58+ """Create a job from a command string, e.g. 'cat file1.txt'."""
59+ # Split command string into command and following arguments
60+ try :
61+ cmd , remainder = cmd_str .split (" " , 1 )
62+ except ValueError :
63+ cmd , remainder = cmd_str , None
64+
65+ # Lookup command in command table
5166 if cmd not in COMMANDS :
5267 raise ValueError (f"Unknown command: '{ cmd } '" )
5368 job_cls = COMMANDS [cmd ]
54- return job_cls (cmd , args , client_id )
69+
70+ # For eval, preserve the remainder as a single string argument and de-quote it
71+ # Otherwise, split remainder into separate arguments
72+ if remainder :
73+ if cmd == "eval" :
74+ args = [remainder .strip ("\" '" )]
75+ else :
76+ args = remainder .split (" " )
77+ else :
78+ args = []
79+
80+ # Create and return the job instance
81+ return job_cls (cmd , args , client_id , globals = globals )
5582
5683
5784class SequentialJob (Job ):
@@ -266,10 +293,7 @@ def output(self):
266293 op = machine .reset if self .mode == "hard" else machine .soft_reset
267294 except AttributeError :
268295 raise OSError ("Operation not supported on this platform" )
269- if self .mode == "hard" :
270- logging .critical (msg )
271- else :
272- logging .warning (msg )
296+ logging .critical (msg )
273297
274298 # Schedule reboot in three seconds
275299 async def reboot_callback (op ):
@@ -282,6 +306,41 @@ async def reboot_callback(op):
282306 return BytesIO (msg .encode ("utf-8" ))
283307
284308
309+ class RunPyJob (Job ):
310+ """A job to evaluate Python script on the device."""
311+
312+ argc = 1
313+
314+ def output (self ):
315+ """Eval or exec given Python and return the result."""
316+ expr = self .args [0 ]
317+ try :
318+ result = self .do_eval (expr )
319+ except SyntaxError : # Not an expression, try exec
320+ result = self .do_exec (expr )
321+ if isinstance (result , str ): # Ensure bytes output
322+ result = result .encode ("utf-8" )
323+ return BytesIO (result )
324+
325+ def do_eval (self , expr ):
326+ """Evaluate a Python expression and return the result."""
327+ op = compile (expr , "<string>" , "eval" )
328+ result = eval (op , self .globals , None )
329+ return repr (result )
330+
331+ def do_exec (self , expr ):
332+ """Execute a Python statement and return the output."""
333+ out_buf = BytesIO ()
334+ old_term = dupterm (out_buf )
335+ try :
336+ op = compile (expr , "<string>" , "exec" )
337+ exec (op , self .globals , None )
338+ result = out_buf .getvalue ().strip ()
339+ finally :
340+ dupterm (old_term )
341+ return result
342+
343+
285344# Map commands to associated job names
286345COMMANDS = {
287346 "whoami" : WhoAmIJob ,
@@ -291,4 +350,5 @@ async def reboot_callback(op):
291350 "cp" : PutFileJob ,
292351 "ota" : FirmwareUpdateJob ,
293352 "reboot" : RebootJob ,
353+ "eval" : RunPyJob ,
294354}
0 commit comments