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

Remote controlling a computer

Started by Dave-ee Jones, 08 August 2017 - 11:12 PM
Dave-ee Jones #1
Posted 09 August 2017 - 01:12 AM
So I know this has been done sooo many times but I am curious as to how people get around the problem of programs.

Problem 1:
If the server has a program that the client doesn't then how does the client see the server's screen when the screen may be showing a GUI that the client doesn't have the code for?

Potential answers:
Draw the pixels individually on the client's screen?
Have both computers the EXACT same (very annoying and almost makes the remote control program useless as the point is to have a new environment, but they are still in different positions in the MC world).

Problem 2:
If the client and the server have the same program but the server's version is newer so the GUI has changed slightly.

Problem 3:
They have the same program but the server's screen is bigger (more pixels), so how does it convert mouse clicks to the right position on the screen? That would include maths based on the differences in size to calculate where the user clicked, calculating the new coords from the old.
KingofGamesYami #2
Posted 09 August 2017 - 02:14 AM
Problem 1: things go through a buffer. You don't run the code on multiple computers, just one.

Problem 2: You don't run the code on multiple computers, just one.

Problem 3: The program would not scale to the display because it is running on the "slave" only. The results would be rendered exactly as they are seen on the smaller screen, leaving some empty space on your monitor.
The Crazy Phoenix #3
Posted 09 August 2017 - 02:52 AM
Problem 1: Not a true "problem", merely an illusion.

Solution 1: Drawing is done pixel-by-pixel regardless so the client doesn't need the GUI code. Even then, function wrapping allows one to know what functions are being called. It's simply a matter of calling on the client the matching function when called on the server (after syncing the screens of course)
For example, create a custom term table that wraps each term function by also sending the called function to the client, which simply calls them as it receives them. Then, you'd call term.redirect on that table to make term functions use it instead.

Problem 2: Depends on problem 1, therefore not an actual issue.

Solution 2: If it were an issue, the program could be transmitted from the server to the client, allowing the client to run the exact same program regardless of its local copy.

Problem 3: Somewhat of an issue, but the cause is misunderstood.

Solution 3: The mouse events contain information about the pixel touched. If the server's screen is bigger, you'd simply force the server to run at a lower resolution by changing term.getSize (ideally through redirection). If the screens aren't the same size, you can then simply discard mouse events that are out of bounds on the resized screen. On the server, this would be done by overriding os.pullEventRaw.
Dave-ee Jones #4
Posted 09 August 2017 - 04:49 AM
Okay, so, you're talking about redirecting the server's drawing functions to the client? I was looking through some RC programs and a lot of them seem to create custom 'term' functions that actually just redirect the output to a rednet/modem message, which makes sense I guess but that means the server doesn't have the same screen as the client, they should have consistent screens e.g. for remote help or showing someone how to program a certain function in a program, or even just remotely controlling a monitor.

In regards to inconsistent screen sizes I figured that the screens would have to be downsized to be consistent (well, virtually consistent).
Lupus590 #5
Posted 09 August 2017 - 12:19 PM
I'm going to explain how Lyqyd's nsh works as I think it will be helpful here. (Or at least my understanding of how it works.)

Nsh is installed on both the client and the remote host. The client tells the host what its screen capabilities are (size, has colour) and the host makes a new shell that screen size. (it should be noted that if multiple clients connect then they each get their own shell maded to their own screen size) The client tells the host what the user is doing (key presses and mouse input) and the host feeds this information to the newly created shell (or any programs running on the host under this shell) as if the user input was at the host computer (i.e. the programs on the host is unaware that the user is using nsh).

The program running on the host then changes the screen in responce to the user input as the program normaly would. The host's nsh program 'captures' this screen information and forwards it to the client, which displays the screen data as if it was its own, thereby giving the illusion that the client is running the program.

As proof of this, have a command computer be a nsh host and remote control it via a turtle, go into lua and check for the commands API (you should find it) and then check for the turtle API (you should not find it).

VNCD works simular but gives every client the same shell on the host and uses the host's screen size.
Edited on 09 August 2017 - 10:20 AM
The Crazy Phoenix #6
Posted 09 August 2017 - 10:10 PM
Okay, so, you're talking about redirecting the server's drawing functions to the client? I was looking through some RC programs and a lot of them seem to create custom 'term' functions that actually just redirect the output to a rednet/modem message, which makes sense I guess but that means the server doesn't have the same screen as the client, they should have consistent screens e.g. for remote help or showing someone how to program a certain function in a program, or even just remotely controlling a monitor.

In regards to inconsistent screen sizes I figured that the screens would have to be downsized to be consistent (well, virtually consistent).

If you want the same screen, then just call the server's term functions when you transmit to the client. For example…


-- Set modem, tch, rch here
local server_term = term.current()
local custom_term = {}
for name, func in pairs(server_term) do
	custom_term[name] = function(...)
		modem.transmit(tch, rch, {name = name, args = {...}})
		return func(...)
	end
end
term.redirect(custom_term)  -- Used to be current_term (it was a mistake)
-- Wait for the client to be running here
term.clear()
term.setCursorPos(1, 1)
-- Run program of choice here

Instead of only transmitting. This sample will work if the client's screen is not smaller in at least one dimension.
You can use term functions that aren't the current term, you just use their table as if it were the term API (without redirect, native and current of course).
Edited on 10 August 2017 - 05:51 AM
Dave-ee Jones #7
Posted 10 August 2017 - 12:42 AM
Okay, so, you're talking about redirecting the server's drawing functions to the client? I was looking through some RC programs and a lot of them seem to create custom 'term' functions that actually just redirect the output to a rednet/modem message, which makes sense I guess but that means the server doesn't have the same screen as the client, they should have consistent screens e.g. for remote help or showing someone how to program a certain function in a program, or even just remotely controlling a monitor.

In regards to inconsistent screen sizes I figured that the screens would have to be downsized to be consistent (well, virtually consistent).

If you want the same screen, then just call the server's term functions when you transmit to the client. For example…


-- Set modem, tch, rch here
local server_term = term.current()
local custom_term = {}
for name, func in pairs(server_term) do
	custom_term[name] = function(...)
		modem.transmit(tch, rch, {name = name, args = {...}})
		return func(...)
	end
end
term.redirect(current_term)
-- Wait for the client to be running here
term.clear()
term.setCursorPos(1, 1)
-- Run program of choice here

Instead of only transmitting. This sample will work if the client's screen is not smaller in at least one dimension.
You can use term functions that aren't the current term, you just use their table as if it were the term API (without redirect, native and current of course).

I'm assuming you mean

term.redirect(custom_term)
on line 10? Otherwise it makes no sense .. that I can see anyway.

So basically what you've done is redirected all GUI functions to the client, instead of doing them on the host. Clever, I didn't think that you could redirect to a remote client, but I knew you would have to redirect the GUI functions at some point otherwise both computers need the same programs.

I don't think I quite understand what that for loop is doing. I can see that it's copying the contents of the current term to a new 'term' (table that contains redone functions that send the term info to the other client), but I'm not sure how it gets the information of the existing function to the new function and add a transmit into it - NEVER MIND. I just realised. It's not actually doing anything that the normal GUI functions do it's just sending what function was used to the remote client. Makes sense now :o/>

So how would you tell the client to use the functions given by the transmitting modem?
Bomb Bloke #8
Posted 10 August 2017 - 12:51 AM
So basically what you've done is redirected all GUI functions to the client, instead of doing them on the host.

"As well as" doing them on the host. Look at what the wrapper functions return.

So how would you tell the client to use the functions given by the transmitting modem?

The client is receiving the name of a key in the term table, as well as the arguments to pass to the corresponding function.

term[ receivedTable.name ]( unpack( receivedtable.args ) )
Dave-ee Jones #9
Posted 11 August 2017 - 12:10 AM
So basically what you've done is redirected all GUI functions to the client, instead of doing them on the host.

"As well as" doing them on the host. Look at what the wrapper functions return.

So how would you tell the client to use the functions given by the transmitting modem?

The client is receiving the name of a key in the term table, as well as the arguments to pass to the corresponding function.

term[ receivedTable.name ]( unpack( receivedtable.args ) )

What's the term table? If you're referring to his code isn't his code the server?..

Delete this embarrassing moment please :)/>
Edited on 10 August 2017 - 10:30 PM
KingofGamesYami #10
Posted 11 August 2017 - 12:24 AM
This.
Dave-ee Jones #11
Posted 11 August 2017 - 06:56 AM
Okay this is my current program. I think there's a memory leak or something because it loves to crash/timeout after a few seconds (during which time I cannot even get out of the computer because there's so much lag lol).

The program has a "host" and "join" mode, along with help (unfinished). I think I get the general idea it's just very hard to keep it all in my head at any one time, lots to think about. Should write some pseudocode first but I got a bit excited..

Spoiler
local nChannel = 5317
local nChannelReply = 5318
local oModem = peripheral.wrap("top")
local bRunning = true

local function printUsage()
	print("Usage:")
	print(shell.getRunningProgram().." help")
	print(shell.getRunningProgram().." host [channel] [channelReply]")
	print(shell.getRunningProgram().." join [channel] [channelReply]")
end

local tArgs = { ... }

if #tArgs < 1 then
	printUsage()
	return
else
	if tArgs[1] == "help" then
		print("host: Host the service for others to connect to")
		print("join: Join the service hosted by a computer")
		print("channel: Channel used by "..shell.getRunningProgram())
		return
	end
end

-- Runner - GUI layer
local function host_RUN()
	local term_server = term.current()
	local term_client = {}
	
	for name, func in pairs(term_server) do
		term_client[name] = function(...)
			oModem.transmit(nChannel, nChannelReply, {p = "HSH_TERM_FUNCTION", n = name, a = {...}})
			return func(...)
		end
	end

	term.redirect(term_client)
end

-- Listener - takes events from clients
local function host_LISTEN()
	while bRunning do
		local event, side, _nCh, _nChR, msg, dist = os.pullEvent()

		if event == "modem_message" then
			if _nCh == nChannelReply and _nChR == nChannel and msg.p == "HSH_EVENT" then
				msg.e[2] = msg.e[2] or nil
				msg.e[3] = msg.e[3] or nil
				msg.e[4] = msg.e[4] or nil
				msg.e[5] = msg.e[5] or nil
				msg.e[6] = msg.e[6] or nil
				os.queueEvent(msg.e[1],msg.e[2],msg.e[3],msg.e[4],msg.e[5],msg.e[6])
			end
		end
	end
end

-- Runner - passes events to host
local function join_RUN()
	while bRunning do
		local event = { os.pullEvent() }
		oModem.transmit(nChannel, nChannelReply, {p = "HSH_EVENT", e = unpack(event)})
	end
end

-- Listener - takes functions called by the host
local function join_LISTEN()
	oModem.open(nChannel)
	
	while bRunning do
		local event, side, _nCh, _nChR, msg, dist = os.pullEvent()
		
		if event == "modem_message" then
			if _nCh == nChannelReply and _nChR == nChannel and msg.p == "HSH_TERM_FUNCTION" then
				term[msg.n](unpack(msg.a))
			end
		end
	end
end

if tArgs[1] == "host" then
	if tArgs[2] then
		nChannel = tonumber(tArgs[2])
	end

	if tArgs[3] then
		nChannelReply = tonumber(tArgs[3])
	end

	local cor1 = coroutine.create(host_LISTEN)
	local cor2 = coroutine.create(host_RUN)

	while bRunning do
		coroutine.resume(cor1)
		coroutine.resume(cor2)
	end
elseif tArgs[1] == "join" then
	if tArgs[2] then
		nChannel = tonumber(tArgs[2])
	end

	if tArgs[3] then
		nChannelReply = tonumber(tArgs[3])
	end

	local cor1 = coroutine.create(join_LISTEN)
	local cor2 = coroutine.create(join_RUN)

	while bRunning do
		coroutine.resume(cor1)
		coroutine.resume(cor2)
	end
end
Edited on 11 August 2017 - 05:38 AM
Bomb Bloke #12
Posted 11 August 2017 - 07:56 AM
These blocks are problematic:

        local cor1 = coroutine.create(join_LISTEN)
        local cor2 = coroutine.create(join_RUN)

        while bRunning do
                coroutine.resume(cor1)
                coroutine.resume(cor2)
        end

The code of a ComputerCraft system runs within a coroutine. That coroutine has to yield on a regular basis, typically done by calling os.pullEvent() (which basically just calls coroutine.yield() for you), or ComputerCraft will crash the script (if possible - failing that, it shuts the computer down).

If you have your computer's coroutine start new child coroutines, then those yield back to just their parent: your main coroutine still has to yield back to its parent (that being ComputerCraft's main computer-handling loop) to avoid locking up the flow of execution and triggering yield protection.

Ferrying event data back and forth between your child coroutines and ComputerCraft's main coroutine handler is a fair bit more complicated than what you're doing there. Luckily the parallel API can handle it all for you; you can simply replace the likes of the quoted block with the likes of this:

parallel.waitForAny(join_LISTEN, join_RUN)
Dave-ee Jones #13
Posted 14 August 2017 - 02:41 AM
These blocks are problematic:

		local cor1 = coroutine.create(join_LISTEN)
		local cor2 = coroutine.create(join_RUN)

		while bRunning do
				coroutine.resume(cor1)
				coroutine.resume(cor2)
		end

The code of a ComputerCraft system runs within a coroutine. That coroutine has to yield on a regular basis, typically done by calling os.pullEvent() (which basically just calls coroutine.yield() for you), or ComputerCraft will crash the script (if possible - failing that, it shuts the computer down).

If you have your computer's coroutine start new child coroutines, then those yield back to just their parent: your main coroutine still has to yield back to its parent (that being ComputerCraft's main computer-handling loop) to avoid locking up the flow of execution and triggering yield protection.

Ferrying event data back and forth between your child coroutines and ComputerCraft's main coroutine handler is a fair bit more complicated than what you're doing there. Luckily the parallel API can handle it all for you; you can simply replace the likes of the quoted block with the likes of this:

parallel.waitForAny(join_LISTEN, join_RUN)

Rightio, I'll give it a try. It's weird though because I've done this before with no problem. Although it was different and had lots of loops and yielding so I'm not sure what could be interfering there :P/>
Dave-ee Jones #14
Posted 15 August 2017 - 03:14 AM
Okay I didn't manage to get your idea working but I deleted that project and recreated it, making some changes and it works fine and really well now! It's got a stop key (atm it's default is Left Ctrl) which when pressed either disconnects from host (if you're the client) or stops the server (if you're the host).

The client can send events to the host so any keys pressed, buttons on your mouse clicked etc. they all pass to the host. Since it's over a modem network (not rednet) it means multiple computers can access the host (there's a view only mode as well which is taken as an argument when you run the program, stops clients from pushing events).

Still not sure how I feel about 3 computers or so trying to send input to the host (which is part of the reason I added view only..). I might make some other commands as well to send/receive files between computers, more 'protocols' etc. Works well so far though, no errors.

Thanks for the help!

Uploaded the code to Pastebin here.