This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Espen's profile picture

Send Arbitrary Commands Over Wifi

Started by Espen, 25 February 2012 - 03:50 PM
Espen #1
Posted 25 February 2012 - 04:50 PM
I'll leave the following intact for educational purposes.
But there is a much easier way to do it and if you want to skip to that easier solution, then scroll down to the fourth post.

Original Post:
I've been playing around with the wireless modems a bit and wanted to be able to send a command to the receiving computer which it then should execute.
But I didn't just want to map specific rednet-messages to specific functions on the receiving end.
I rather wanted to be able to send any command in string form to the receiver, have him interpret the command with all its arguments correctly from the string and finally (provided the command is a valid one for the receiver) execute that command.

This is only a proof-of-concept, if you will, and by no means fool-proof.
But at the moment it should work for all global functions + the shell API (which isn't in the global table).

The two core functions are getFunc( _sFuncCall ) which will return a function for the given string and getArgs( _sFuncCall ) which will return all the arguments in their proper type.
The following code contains both of these functions, along with two helper functions for string-trimming and -splitting, along with some example code for how you could make use of the two core functions:
Spoiler

--[[ HELPER FUNCTIONS ]]
-- Removes leading and trailing spaces.
function trim( s )
	return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end

-- explode(string, seperator)
function explode(p, d)
  local t, ll
  t={}
  ll=0
  if(#p == 1) then return {p} end
	while true do
	  l=string.find(p,d,ll,true) -- find the next d in the string
	  if l~=nil then -- if "not not" found then..
		table.insert(t, string.sub(p,ll,l-1)) -- Save it in our array.
		ll=l+1 -- save just after where we found it for searching next time.
	  else
		table.insert(t, string.sub(p,ll)) -- Save what's left in our array.
		break -- Break at end, as it should be, according to the lua manual.
	  end
	end
  return t
end

--[[ CORE FUNCTIONS ]]

-- Retrieves arguments from a string within brackets and converts them according to these rules:
-- "house"  => becomes the string 'house'
-- true	 => becomes the boolean 'true'
-- false	=> becomes the boolean 'false'
-- 123	  => becomes the number '123'
-- Returns:
--	  true and a table with the arguments, if successful.
--	  false and nil, if not successful.
function getArgs( _sFuncCall )
	local nStart, nEnd = string.find( _sFuncCall, "%(.*%)" )	-- Find content between brackets.
	nStart = nStart + 1 -- Adjust starting position.
	nEnd = nEnd - 1 -- Adjust ending position.
	local content = string.sub( _sFuncCall, nStart, nEnd )	-- Get content between brackets.
	content = trim( content )
	if #content < 1 then return true, nil end  -- There are no arguments, so we can stop here already and return nil.
	
	content = content..""   -- Temporary fix for the string.find() problem.
	content = explode( content, "," )   -- Separate contents at the commas.
	
	-- Replace table string values with the types they actually represent, e.g. replace a string 'true' with a boolean 'true', etc.
	for i = 1, #content do
		if content[i] ~= nil then
			content[i] = trim( content[i] )
			
			if string.lower( content[i] ) == "true" then
				-- Boolean TRUE
				content[i] = true
			elseif string.lower( content[i] ) == "false" then
				-- Boolean FALSE
				content[i] = false
			elseif tonumber( content[i] ) ~= nil then
				-- NUMBER
				content[i] = tonumber( content[i] )
			else
				-- STRING
				-- Assume the string is encased within double quotes.
				local nStart, nEnd = string.find( content[i], "%\".*%\"" )	-- Find content between double quotes.
				if nStart == nil then
					return false, "Error at argument '"..content[i].."'\nOnly strings, booleans and numbers are supported (yet)."
				end
				nStart = nStart + 1 -- Adjust starting position.
				nEnd = nEnd - 1 -- Adjust ending position.
				content[i] = string.sub( content[i], nStart, nEnd )	 -- Get content between double quotes.
				content[i] = content[i]..""	-- Temporary fix for the string.find() problem.
			end
		end
	end
	return true, content
end

-- _sFuncCall - Command-Call in string form.
-- Returns:
--	  The function to the given comand, if successful.
--	  nil, if not successful.
function getFunc( _sFuncCall )
	local tSub = explode( _sFuncCall, "." )
	local func = tSub[1]
	
	if func == "shell" then
		func = shell	-- 'shell' isn't in the global table, it's actually a program within which THIS program has been executed from.
	else
		func = _G[ func ]
	end
	
	if type( func ) == "table" then
		local nStart, nEnd = string.find( tSub[2], "%(.*%)" )   -- Find content between brackets.
		if nStart == nil then return nil end	-- There are no brackets, so we don't treat this as a function call.
		
		tSub[2] = string.sub( tSub[2], 1, nStart - 1 )	-- Everything, excluding the brackets and its contents.
		if type( func[ tSub[2] ] ) == "function" then
			return func[ tSub[2] ]
		else
			return nil
		end
	end
end

--[[ EXAMPLE ON HOW TO MAKE USE OF IT ]]
local modemSide = "right"
term.setCursorPos( 1, 1 )
term.clear()
print("Listening...")
rednet.open( modemSide )

while true do
	local sEvent, p1, p2 = os.pullEvent()
	
	if sEvent == "rednet_message" then
		print( "Message from Computer #"..p1 )
		print( "Command:\n"..p2 )
		
		local func = getFunc( p2 )
		if type( func ) == "function" then
			local ok, tArgs = getArgs( p2 )
			
			if ok and tArgs ~= nil then
				print( "Result:\n"..tostring( func( unpack( tArgs ) ) ) )
			elseif ok and tArgs == nil then
				print( "Result:\n"..tostring( func() ) )
			else
				print( tArgs )
			end
			print()
		end
	end
	
	if sEvent == "char" and p1 == "q" then break end
	if sEvent == "char" and p1 == "c" then term.setCursorPos( 1, 1 ) term.clear() print("Listening...") end
end

rednet.close( modemSide )
print("Finished")

USAGE:
What values the two core functions expect as input and what they return is pretty much explained within the comments of the code itself.
If you want to test the example code from above, do the following:
  1. Create a computer or turtle with a wireless modem.
  2. Modify the variable modemSide in the code above to point to the side where the modem is on the receiving computer/turtle.
  3. Start the program on the receiving computer/turtle.
Now from the sender you can send commands using rednet like this:
rednet.send( 2, "os.time()" )
This should return the time on computer #2 (on which the example program from above would be running).
rednet.send( 2, "redstone.setOutput(\"left\", true)" )
Turns on the left redstone output of the receiver.

Note that you have to escape double quotes if they are part of the command that you want to send!
Review the last send example for that.

Ok, that's all for now.
I haven't tested it extensively, as I was first trying to get the principal idea working.
So if you would like to give it a try and give some constructive feedback, I'd very much appreciate that. :(/>/>

EDIT: Fixed outputting of nil by adding tostring() within the prints.
Edited on 25 February 2012 - 07:42 PM
Liraal #2
Posted 25 February 2012 - 05:03 PM
that is great! i was trying to do something similar, but you were the first!(im still working out connecting problems:P)
Espen #3
Posted 25 February 2012 - 05:17 PM
that is great! i was trying to do something similar, but you were the first!(im still working out connecting problems:P)
Not to be rude, but being first means nothing to me. I'm just glad it could be of some help for someone.
Feel free to take it apart and expand it as much as you like. Would be nice if someone finds an easier or even more generalizable way of doing it. :(/>/>
Casper7526 #4
Posted 25 February 2012 - 08:07 PM
This should work

[Computer]

command = 'print ("Hello world")'
rednet.open("right")
rednet.broadcast(command)
id, mess = rednet.receive()
print (mess)

[Turtle]

rednet.open("right")
id, mess = rednet.receive()
a = loadstring(mess)
a() -- i know that will run it
print (a()) -- print it?
rednet.broadcast(a()) -- should send results..

I just wrote that off the top of my head, but you should be able to get something working with that :(/>/>
Espen #5
Posted 25 February 2012 - 08:37 PM
*headdesk*
Omg, I cannot believe loadstring never came to my mind. That is so much easier, more versatile and elegant.
Well at least I learned something while trying to do it in a more… complicated way. :(/>/>
Thanks buddy, much appreciated!
rdsn #6
Posted 01 April 2012 - 04:17 AM
I want to make use of Casper's solution, but would anyone care to explain how it works?
Espen #7
Posted 02 April 2012 - 02:29 PM
You basically send the lua code you want to execute over via rednet.send(ID, "LUA CODE") or rednet.broadcast("LUA CODE") to your destination (e.g. a turtle). There you pull both the id and the message via:
local id, message = rednet.receive()
Then you can call loadstring on the received message and store it into a varaible name, like this:
local receivedLuaCode = loadstring(message)

This loads the code contained in message as a function into receivedLuaCode.
You can then call receivedLuaCode like any other function to execute its code:
receivedLuaCode(var1, var2, ...)

So basically, loadstring loads a Lua chunk from a string, which you can then execute whenever you want.

Things to look up and keep in mind:Just look at Casper's last post, it's all there.
I'd suggest first playing around with loadstring() until you get it to work and then try rednet and see if you can get that to work as well.
If you know how both work, you just combine them. Have fun. :)/>/>
PixelToast #8
Posted 10 May 2012 - 06:59 PM
i love you
sam502 #9
Posted 14 August 2012 - 06:32 AM
Not to be like a uber noob or something… But the command

local id, message = rednet.receive()
freezes the computer : Any solutions?
Sammich Lord #10
Posted 14 August 2012 - 10:56 AM
Not to be like a uber noob or something… But the command

local id, message = rednet.receive()
freezes the computer : Any solutions?
Well it does not freeze the computer it just idles till it receives a rednet msg.
For example if you wanted it to only wait for a rednet msg for let's say 10 seconds you would do:

local id, msg = rednet.receive(10)
But if you want to have it be in the background the easiest way is to use the parallel API which a tutorial can be found in the forums.
Hope that helped. :P/>/>