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

[lua][coroutine][noerror] Having trouble debugging my coroutine implementation

Started by Yesurbius, 08 April 2013 - 07:58 AM
Yesurbius #1
Posted 08 April 2013 - 09:58 AM
The problem: I have two coroutines created. I have a while loop that resumes them sequentially again and again. When tested without coroutines (calling each function directly) it works. When coroutines are active, both functions will yield (one with a turtle command, one with sleep) but the turtle command (when completed) doesn't resume execution. The turtle ends up powering off. I've tested and the coroutine while loop is looping - the coroutines ARE being resumed. I see the turtle do only the first command then it hangs.

My suspicion: I'm probably using the coroutines dead wrong. I apologize - I am learning. I've checked the lua tutorials on coroutine .. I think I understand the single-thread concept… I think I understand yield - but I haven't used it in my program because I assume sleep and turtle movements yield until completed (which I've concluded on my own during troubleshooting)

What is my application? My application is a replacement for the go application. I call it 'work'. Calling work f10 r2 f10 will cause the turtle to go forward 10, right 2 turns then forward 10 again. Alternative it can read the commands from a text file (although I haven't tested this functionality). Once I get it working I also plan to have an interactive "training" mode where I can input the commands one at a time and it'll record them to a text file. The aim is to walk a turtle through a process once (ie. tilling the field) .. then be able to recall and re-execute that process anytime in the future.

Other thoughts: I realize that one solution is to simply remove the coroutines and manually check for fuel - I can do that for sure, but I'd like to learn why its failing under this design. Also I am running ComputerCraft 1.5.2 on a Minecraft 1.5.1 server (which is running on an Ubuntu box in my livingroom) Other than the default Forge mods and ComputerCraft, no other mods are installed.

The Code: (Pastebin Link)


local fuelMatchSlot=16  -- Slot holding Fuel
local fuelMin = 10	  -- Fuel leverl to trigger refuel
local fuelRefill = 50   -- Refuel to this amount
local seedMatchSlot=15  -- Slot holding Seed to plant

-- Determine Mode and start coroutines
function main( tArgs )
  local fHandler
  local cmds = ""
  local coGo
  local coFuel
  local status
  local yy

  -- Verify Command Line Arguments were correct
  if #tArgs < 1 then
	print( "Usage: work <routefile>	 -or-" )
	print( "	   work <command> <command> ... etc	-or-")
	print( "	   work program")
	return false
  end

  if #tArgs == 2 and tArgs[1] == "program" then
	interactiveMode(tArgs[2])
	return
  end
	-- test if we have a filename or a command
  if fs.exists(tArgs[1]) then
	fileOrCmds = "file"
  else
	fileOrCmds = "cmds"
  end

  -- Read in our command list from the file
  if fileOrCmds == "file" then
	if fs.exists(tArgs[1]) then
	  fHandler = fs.open(tArgs[1],"r")
	  if fHandler then
		cmds = fHandler.readAll
	  end
	end
  else -- Read in our commands from the command line
	-- Convert from a table into a space separated list
	for i,v in ipairs(tArgs) do
	  if cmds == "" then
		cmds = v
	  else
		cmds = cmds .. " " .. v
	  end
	end
  end

  -- Create our Coroutines
  coGo = coroutine.create(go)
  coFuel = coroutine.create(fuelManager)

  -- Continuously resume coroutines in sequence as they yield
  while true do

	-- Start/Resume our Go coroutine
	yy = coroutine.resume(coGo,cmds)
	if yy == false then
	  print("Coroutine Go Reports error")
	  print("Status: " .. coroutine.status(coGo))
	end
	-- If the coroutine has terminated, then exit our loop
	if coroutine.status(coGo) == "dead" then
	  break
	end

	-- Start/Resume our Fuel coroutine
	yy = coroutine.resume(coFuel)
	if yy == false then
	  print("Coroutine Fuel Reports error")
	  print("Status: " .. coroutine.status(coFuel))
	end
	-- If the coroutine has terminated, then exit our loop
	if coroutine.status(coFuel) == "dead" then
	  print("Out of Fuel")
	  break
	end
  end
  return true
end

-- Interactive Mode
function interactiveMode(saveFile)
  term.clear()
  print("Welcome to Interactive Mode")
  print("Instructions enter one command per line, followed by the enter key.")
  print("The turtle will execute the instructions and the instructions will")
  print("be recorded to the file '" .. saveFile .. "'.")
  print("")
  print("Enter the command 'end' to stop")
  print("")
  local fHandler = fs.open(saveFile,"a")
  while true do
	write("Command: ")
	cmd = io.read()
	if go(cmd) then
	  if string.lower(cmd) == "end" then
		break
	  end
	  fHandler.writeLine(cmd)
	end
  end
  fHandler.close()
end

-- Checks fuel status on predictable intervals
function fuelManager()
  local currentFuelLevel = 0
  local avgFuelConsumptionPerSecond = 1
  local estFuelLife = 0

  --- Keep this function active continously
  while true do
	--- Assess current Fuel Level
	currentFuelLevel = turtle.getFuelLevel()
	---   If fuel near Min level, then we need to refuel up to refuel level
	if currentFuelLevel <= fuelMin then
	  return false
	end
	--- Calculate approximate time until earliest fuel low warning
	estFuelLife = (currentFuelLevel * avgFuelConsumptionPerSecond)
	--- Sleep for half that time (coroutine should yield here)
	os.sleep(math.floor(estFuelLife/2))
  end -- endless loop
end

-- Processes a series of instructions
function go(cmds)
  local tArgs = { }
  local cmd = ""
  local nPos = 1
  local nCmd = 1
  local c = ""

  -- Convert List of cmds into a table
  while nPos < #cmds do
	c = string.sub(cmds,nPos,nPos)
	if c == " " then
	  if cmd ~= "" then
		tArgs[nCmd] = cmd
		cmd = ""
		nCmd = nCmd + 1
	  end
	else
	  cmd = cmd .. c
	end
	nPos = nPos + 1
  end
  if cmd ~= "" then
	tArgs[nCmd] = cmd
  end

  -- If no commands were found in the table then exit
  if #tArgs < 1 then
	print("No arguments given to go")
	return false
  end

  -- These are our defined handlers.   Each handler is defined with a condition and an action.
  -- before an action is executed, a condition must be met
  -- successful actions, will deducated distance values.  
  -- Failed conditions or actions will set distance to 0
  local tHandlers = {
		-- Forward, Back, Up, Down
		["F"] = { condition=function() return true end, action=turtle.forward },
		["B"] = { condition=function() return true end, action=turtle.back },
		["U"] = { condition=function() return true end, action=turtle.up },
		["D"] = { condition=function() return true end, action=turtle.down },
		-- Left, Right
		["L"] = { condition=function() return true end, action=turtle.turnLeft },
		["R"] = { condition=function() return true end, action=turtle.turnRight },
		-- Dig Over, Dig uNder
		["O"] = { condition=turtle.detect, action=turtle.digUp },
		["N"] = { condition=turtle.detect, action=turtle.digDown },
		-- Plant, Eject
		["P"] = { condition=function() return not(turtle.detectDown()) end, action=plantSeed },
		["E"] = { condition=function() return true end, action=dropExtras }
	  }

  -- Iterate for each command present
  local nArg = 1
  local sCmd = ""
  for nArg,sCmd in ipairs(tArgs) do
	local nDistance = 1
	-- Determine the Distance for the command
	if #sCmd > 1 then
	  local num = tonumber(string.sub(sCmd, 2))
	  if num then
		nDistance = num
	  else
		nDistance = 1
	  end
	else
	  nDistance = 1
	end
	sOperation = string.sub(sCmd,1,1)

	-- Use the function handler that corresponds with the command
	local fnHandler = tHandlers[string.upper(sOperation)]
	if fnHandler then
	  -- Set our condition and action functions
	  local condition = fnHandler["condition"]
	  local action = fnHandler["action"]
	  -- Repeat based on distance
	  while nDistance > 0 do
		local status = false  -- whether condition and action both returned true
		if condition() then
		  if action() then
			status = true
		  end
		else
		  status = true
		end
		-- If the action did not succeed, lets check some common reasons
		if status == false then
		  -- Out of Fuel?
		  if turtle.getFuelLevel() == 0 then
			print( "Out of fuel" )
			return false
		  else
			-- Best report his even though it may correct itself in half a second
			print(string.upper(sOperation) .. " returned error")
			sleep(0.5)
		  end
		else  -- action DID succeed, so lets reduce distance
		  nDistance = nDistance - 1
		end
	  end  -- while distance > 0
	else  -- if there was no valid function handler found ...
	  print( "No such Handler: " .. string.upper(sOperation) )
	  return false
	end
  end -- Process Next Command
  return true
end

-- Finds a seed in our inventory and plants it
-- Should be clear that there needs to be an empty block below in order to properly plant
function plantSeed()
  local slotNumber = 0
  local itemCount = 0
  local attempt = 0
  local seedItem = false

  for slotNumber=1,16 do
	-- There is a goto command in the new beta Lua .. cannot wait
	local skipIt = false
	while skipIt == false do
	  skipIt = true
	  if (slotNumber ~= fuelMatchSlot) and (slotNumber ~= seedMatchSlot) then
		-- See if there is any items in the slot
		itemCount = turtle.getItemCount(slotNumber)
		if itemCount == 0 then
		  -- goto skipIt
		  break
		end

		-- Select the Slot
		while turtle.select(slotNumber) == false do
		  attempt = attempt + 1
		  if attempt == 3 then
			print("Unable to Select Slot#" .. slotNumber)
			return false
		  end
		  -- Lets wait - maybe it will clear up
		  -- We will only make 3 attemps though
		  sleep(5)
		end

		-- Compare it to our seed slot
		seedItem = turtle.compareTo(seedMatchSlot)
		if not seedItem then
		  -- goto skipIt
		  break
		end

		-- We found a seed item - lets plant it
		if not turtle.placeDown() then
		  print("Could not place seed")
		  return false
		end
	  end  -- slot <> seedslot condition
	end -- while
	-- ::skipit::
  end  -- next slot number
  return true
end

-- Drops all items that are "extra"
-- at the time of writing, that means anything not fuel or seed
function dropExtras()
  local slotNumber = 0
  local keepItem = false
  local itemCount = 0
  local attempt = 0

  -- iterate through our slots
  for slotNumber=1,16 do
	-- Can remove these two lines when LUA gets a GOTO statement
	local skipIt = false
	while skipIt == false do
	  if (slotNumber ~= fuelMatchSlot) and (slotNumber ~= seedMatchSlot) then
		-- See if there is any items in the slot	
		itemCount = turtle.getItemCount(slotNumber)
		if itemCount == 0 then
		  -- goto skipIt
		  break
		end

		-- Select the Slot, give up after 3 attempts
		while turtle.select(slotNumber) == false do
		attempt = attempt + 1
		  if attempt == 3 then
			print("Unable to Select Slot#" .. slotNumber)
			return false
		  end
		  sleep(5)
		end  

		-- Is it an extra?
		keepItem = (turtle.compareTo(fuelMatchSlot) or turtle.compareTo(seedMatchSlot))
		if keepItem then
		  -- goto skipIt
		  break
		end

		-- We found an extra item - lets drop it
		turtle.dropDown()
	  end  -- valid slot check
	end  -- while loop
	-- :: skipIt ::
  end  -- next slot number
  return true
end

function reFuel()
  local slotNumber = 0
  local fuelItem = false
  local itemCount = 0
  local attempt = 0

  for slotNumber=1,16 do
	-- Remove these two lines when LUA gets a goto statement
	local skipIt = false
	while skipIt == false do
	  skipIt = true
	  if not (slotNumber == fuelMatchSlot) then

		-- See if there is any items in the slot	
		itemCount = turtle.getItemCount(slotNumber)
		if itemCount == 0 then
		  -- goto skipIt
		  break
		end

		-- Select the Slot
		while turtle.select(slotNumber) == false do
		  attempt = attempt + 1
		  if attempt == 3 then
			print("Unable to Select Slot#" .. slotNumber)
			return false
		  end
		 sleep(5)
		end

		-- Compare it to our fuel slot
		fuelItem = turtle.compareTo(fuelMatchSlot)
		if not fuelItem then
		  break
		end

		-- We found a fuel item - lets refuel to the refuel limit
		while turtle.getFuelLevel() < fuelRefill do
		  -- Attempt to Refuel with one unit
		  attempt = 0
		  while turtle.refuel(1) == false do
			attempt = attempt + 1
			if attempt == 1 then
			  print("Unable to refuel from slot #" .. slotNumber)
			end
			if attempt == 3 then
			  break   -- give up on this slot
			end
			os.sleep(1)
		  end

		  -- If this slot does not appear to refilling then skip the slot
		  if attempt == 3 then
			break
		  end

		  -- Keep refueling until we reach our refill level
		end  -- While Refueling
	  end  -- slot <> fuelslot condition
	  -- if we have refueled sufficiently, lets stop checking slots
	  if turtle.getFuelLevel() >= fuelRefill then
		break
	  end
	end  -- end while loop
  end  -- next slot number

  -- Return Refuel success
  if turtle.getFuelLevel() < fuelRefill then
	return false
  else
	return true
  end
end

local tArgs = { ... }

main(tArgs)

print("Finished")

Any assistance would be appreciated. Thank you in Advance.
Lyqyd #2
Posted 09 April 2013 - 06:28 AM
Split into new topic.

If you don't get a high-quality answer soonish, I'll try to take a look at this tonight or tomorrow.
PixelToast #3
Posted 09 April 2013 - 06:43 AM
-snip derp-
a critical bug, your improperly using coroutines badly
look at the parallel API, how it yeilds and passes it to the routines
you want to do something like that

ill add things to this post as i find bugs
JokerRH #4
Posted 09 April 2013 - 09:53 AM

-- Best report his even though it may correct itself in half a second
print(string.upper(sOperation) .. " returned error")
sleep(0.5)
[In function go]



-- Start/Resume our Go coroutine
yy = coroutine.resume(coGo,cmds)

sleep yields until it gets it's timer event. However, you resume it with cmd

Edit: same with refuel()
Yesurbius #5
Posted 09 April 2013 - 06:17 PM
As I eluded to - my coroutines were probably dead wrong. My understanding was that resume runs a function until that function yields - plain and simple.


look at the parallel API, how it yeilds and passes it to the routines
you want to do something like that

I took a look at the parallel API - while the code makes sense - I don't understand what its doing. So maybe you can help me with that. Lets take it from the top.

From the Wiki, the two functions for programs to call are:

parallel.waitForAny(function1, function2, …) Runs all the functions at the same time, and stops when any of them returns.
parallel.waitForAll(function1, function2, …) Runs all the functions at the same time, and stops when all of them have returned.

I'm going to add some comments into the code in a (failed) effort to make sense of it. Maybe you can help explain the code. I added the quotes into the lua comments so the forum highlights the comments easier.

parallel API

-- 'Calls itself recursively, with each invocation returning one more coroutine than the last'
-- 'When the end is reached, it returns nil'
local function create( first, ... )
  if first ~= nil then
	return coroutine.create(first), create( ... )
  end
  return nil
end

-- 'Not fully sure ..'
local function runUntilLimit( _routines, _limit )
  local count = #_routines
  local living = count
  local tFilters = {}
  local eventData = {}
  while true do

	for n=1,count do
	  local r = _routines[n]
	  if r then
	   -- 'Not sure what we are checking here:'
		if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then
		  -- 'The yield from r will return two parameters (ok, parm).'
		  -- 'I am guessing OK is an indicator if the coroutine exited fine.'
		  -- 'not sure what param is'
		  local ok, param = coroutine.resume( r, unpack(eventData) )  -- 'unpack() converts the eventData array into individual arguments'
		  if not ok then
			error( param )  -- returns an error if not OK.
							-- 'from my reading on error() it seems this is the same as: assert(ok,param)'
		  else
			tFilters[r] = param  -- 'not sure - need to know what param is to understand what tFilters is.'
		  end
		  -- 'if the coroutine has exited on its own .. then neutralize the coroutine from the coroutine array'
		  -- 'Reduce the counter variable for the number of active coroutines ..'
		  -- 'If we hit our limit then return which coroutine index stopped'
		  if coroutine.status( r ) == "dead" then
			_routines[n] = nil
			living = living - 1
			if living <= _limit then
			  return n
			end
		  end
		end
	  end
	end

	-- 'looks like this runs through all the coroutines and cleans them up if they are dead.'
	for n=1,count do
	  local r = _routines[n]
	  if r and coroutine.status( r ) == "dead" then
		_routines[n] = nil
		living = living - 1
		if living <= _limit then
		  return n
		end
	  end
	end

	-- 'No idea what this does.'
	eventData = { os.pullEventRaw() }
  end
end

-- 'Uses the create() function recursively to build an array of coroutines.'
-- 'It then runs the coroutines until any one of them terminates.'
function waitForAny( ... )
  local routines = { create( ... ) }  
  return runUntilLimit( routines, #routines - 1 )
end

-- 'Uses the create() function recursively to build an array of coroutines.'
-- 'It then runs the coroutines until all terminate.'
function waitForAll( ... )
  local routines = { create( ... ) }
  runUntilLimit( routines, 0 )
end
Yesurbius #6
Posted 09 April 2013 - 06:33 PM

-- Best report his even though it may correct itself in half a second
print(string.upper(sOperation) .. " returned error")
sleep(0.5)
[In function go]



-- Start/Resume our Go coroutine
yy = coroutine.resume(coGo,cmds)

sleep yields until it gets it's timer event. However, you resume it with cmd

Edit: same with refuel()

Hmm this may be an ah-ha moment … consider the simplified example:


function A()
  print("Hello")
  sleep(5)
  print("World")
end

function B()
  sleep(3)
  print("Cruel")
  sleep(5)
end

local coA = coroutine.create(A)
local coB = coroutine.create(B)/>

while true do
  coroutine.resume(coA)
  coroutine.resume(coB)
  if coroutine.status(coA) == "dead" or coroutine.status(coB) == "dead" then
	break
  end
end

How I imagined it would work is
  • it would create the two coroutines .. with coroutine.create
  • it would resume (or start) the first coroutine (-A-). .. and print "Hello", then going to sleep
  • When A sleeps, the Sleep() function would yield, passing control back to the main function
  • it would resume (or start) the second coroutine (-B-)
  • when B sleeps, the Sleep() function would yield, passing control back to the main function.
  • The while loop repeats again and again.
  • Eventually coroutine B's sleep() would finish first (only 3 seconds) .. and print "Cruel" … then sleep
  • Finally, the sleep routine from coroutine A would hit 5 seconds - print "World" and then terminate ..
  • .. thus ending the program.
If I'm understanding you correctly .. when sleep() yields .. its sending back some sort of timer event parameters .. that we need to do something with … otherwise when we resume .. it just yields again .. with no timer progression?

Am I close to being on the right track?
JokerRH #7
Posted 09 April 2013 - 10:28 PM
Am I close to being on the right track?

Yes, definetly :D/>

Every computer in cc is a coroutine running in minecraft.
os.pullEventRaw() yields and passes a filter back to the main function, which will resume it with a new event.
Sleep() will queue a time shifted event, so when the timer is at 0 it will cause a new timer event to appear.
Parallel will pull the event and if the filter matches (or there is no filter) it will pass the new event over.
To resume your sleep you'll have to do the same.

Edit:

local event = {}
local filterA
local filterB

while true do
  if not filterA or filterA == event[1] or event[1] == "terminate" then
	filterA = coroutine.resume(coA, unpack(event))
  end

  if not filterB or filterB == event[1] or event[1] == "terminate" then
   filterB = coroutine.resume(coB, unpack(event))
  end

  if event[1] == "terminate" then error("Terminated main routine") end

  event = {coroutine.yield()} --or {os.pullEventRaw()}
end
Yesurbius #8
Posted 10 April 2013 - 03:33 AM
I'm just out the door to work now … but I'm going to read up on this page (http://www.lua.org/pil/9.2.html) and see if I can wrap my head around what filters are.

What kind of events would pass through our little app? Timer? KeyStroke .. RedNetMessage received? etc?
JokerRH #9
Posted 10 April 2013 - 08:57 AM
What kind of events would pass through our little app? Timer? KeyStroke .. RedNetMessage received? etc?

They are listed on the wiki if you look for os.pullEvent, but yes, these are events :D/>
Yesurbius #10
Posted 10 April 2013 - 06:27 PM
I'm trying hard to wrap my head around this I swear. LOL

Am I correct in saying:
The Lua coroutines functions (outside ComputerCraft) can pass arguments to the coroutines by indicating a parameter to resume()….
… however .. in order to make computercraft work - those arguments must be event messages if your coroutine calls any computercraft function calls that have a yield in them (which I will guess is most).

So … is it even possible to pass your own arguments?


function A(MyArg)
  for i=1,5 do
	coroutine.yield(i .. ". " .. MyArg)
  end
end

local coA = coroutine.create(A)

while true do
  status, value = coroutine.resume(coA,"Test")
  if not status then
	break
  end
  if coroutine.status(coA) == "dead" then
	break
  end
  print(value)
end

Produces:

1. Test
2. Test
3. Test
4. Test
5. Test

That works as expected. But if I add a sleep command in there…


  function A(MyArg)
	for i=1,5 do
	  coroutine.yield(i .. ". " .. MyArg)
	  os.sleep(1)
	end
  end

it continuously repeats "Timer"

If I understand it correctly - its because the sleep command itself starts a timer event.
So I'm stuck here - not sure how you can handle the yield from the sleep command

Would it be possible to handle the sleep event explicitly?

if value == "timer" then
  // Pass on the Timer value somehow to computercraft
end


I must be missing something else too … because ..

courtine.resume takes at least one argument which is the coroutine thread. The subsequent arguments are arguments passed on to the thread.

Even in the parallel API snippet I posted earlier … I don't understand it - because you are at no time defining parameters for the parallel coroutines .. so the parallel coroutine functions you pass will not have any parameters … and yet .. the resume call is passing unpack(eventdata) …

Is there a filter somewhere in there? Although I see TFilters in the code - I don't see any code that would intercept and wrap the user-supplied coroutine passed to the parallel API. Therefore I don't see where the user-supplied coroutine can accept the unpack(eventdata) parameters ..

My head hurts LOL
Lyqyd #11
Posted 10 April 2013 - 07:28 PM
The event model in CC is relatively simple.

All parameters passed into a coroutine should be in the form `"event_name", parameters`.
All coroutines should treat parameters passed into them as events they are receiving.
All coroutine managers (which resume the coroutines, and are coroutines themselves) should accept a string parameter as a return value from a coroutine, and filter events based on this string.
All coroutine managers should distribute the events they receive, as appropriate, amongst the coroutines they manage.

Now, let's review the management loop from the parallel API:


local function runUntilLimit( _routines, _limit )
    local count = #_routines
    local living = count
    
    local tFilters = {}
    local eventData = {}
    while true do
        for n=1,count do
            local r = _routines[n]
            if r then
                if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then
                    local ok, param = coroutine.resume( r, unpack(eventData) )
                    if not ok then
                        error( param )
                    else
                        tFilters[r] = param
                    end
                    if coroutine.status( r ) == "dead" then
                        _routines[n] = nil
                        living = living - 1
                        if living <= _limit then
                            return n
                        end
                    end
                end
            end
        end
        for n=1,count do
            local r = _routines[n]
            if r and coroutine.status( r ) == "dead" then
                _routines[n] = nil
                living = living - 1
                if living <= _limit then
                    return n
                end
            end
        end
        eventData = { os.pullEventRaw() }
    end
end

I've copied the entire loop above. There are a couple helper functions that build the parameters for this function, but the important part is that the first parameter is a table of coroutines, and the second parameter is the limit–the minimum number of non-dead coroutines needed to continue.

The first part of the function intializes a couple tables and variables. This section is fairly self-explanatory:


    local count = #_routines
    local living = count
    
    local tFilters = {}
    local eventData = {}

Then we begin the loop itself:


    while true do
        for n=1,count do
            local r = _routines[n]
            if r then
                if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then
                    local ok, param = coroutine.resume( r, unpack(eventData) )
                    if not ok then
                        error( param )
                    else
                        tFilters[r] = param
                    end
                    if coroutine.status( r ) == "dead" then
                        _routines[n] = nil
                        living = living - 1
                        if living <= _limit then
                            return n
                        end
                    end
                end
            end
        end
        for n=1,count do
            local r = _routines[n]
            if r and coroutine.status( r ) == "dead" then
                _routines[n] = nil
                living = living - 1
                if living <= _limit then
                    return n
                end
            end
        end
        eventData = { os.pullEventRaw() }
    end

Let's break that down. We can split it into three parts to make it easier to digest.

First, we have the section that resumes each coroutine and passes it the event parameters. We loop through the coroutine table, and for each coroutine, we do a few things. First, we grab the coroutine value itself from the table and put it in a local variable "r". We check that this value is not nil or false (`if r then`). If it isn't nil or false, we assume it is a valid coroutine and continue on to check what sort of event we are dealing with. If the coroutine has no filter set, or the event string matches the filter string, or the event is "terminate", we continue on to resuming the coroutine. This is where filtering actually occurs. If we pass our filter check (no filter, matching filter, or "terminate" event), then we resume the coroutine, and unpack the eventData table and pass that along to it. We catch the return values, which is a success status, and the first parameter the coroutine specified when it yielded. If the success status was true (the coroutine ran successfully), we set the current filter to whatever parameter was returned by the coroutine. If it specified a filter, this will be the filter string. If it did not, this will be nil, clearing the filter.

Next, we check to see if the status of the current coroutine is "dead". If the coroutine is dead, we set the coroutine value for the current coroutine in our table of coroutines to nil. We also remove one from the count of live coroutines and check this against our limit. If we are at or below the limit, we immediately end execution and return.


   	 for n=1,count do
            local r = _routines[n]
            if r then
                if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then --compare the filter to the current event string.
                    local ok, param = coroutine.resume( r, unpack(eventData) )
                    if not ok then
                        error( param )
                    else
                        tFilters[r] = param
                    end
                    if coroutine.status( r ) == "dead" then
                        _routines[n] = nil
                        living = living - 1
                        if living <= _limit then
                            return n
                        end
                    end
                end
            end
        end

The second part is a secondary cleanup loop. You'll notice it has significant similarities to the cleanup check above, but this section will iterate through all coroutines, while the previous one only checks coroutines which were resumed for the current event. It might seem like only one of these is necessary, but that is not the case for a few reasons, which I can explain in further detail if necessary.


   	 for n=1,count do
            local r = _routines[n]
            if r and coroutine.status( r ) == "dead" then
                _routines[n] = nil
                living = living - 1
                if living <= _limit then
                    return n
                end
            end
        end

The last section of this is very small, but very crucial–it is the line which fetches events. After each event has been passed into all of the child coroutines which want it, this line will yield and wait to receive the next event. It packs it into the eventData table for use on the next pass through the loop.


   	 eventData = { os.pullEventRaw() }

Please reply with any further questions you may have. :)/>
JokerRH #12
Posted 10 April 2013 - 09:44 PM
Nice explanation!
In addition to that:
The Filter is passed to os.pullEventRaw(filter), wich will call coroutine.yield(filter). The coroutine runs until it hits the yield function and returns the filter. Next time it'll be resumed if the filter matches.

event, p1, p2, p3, p4 = coroutine.yield(filter)

Edit: You don't have to resume it with the event if you don't use sleep, pullEvent, rednet.receive etc. you can pass it whatever you want, but what I recommend is simply using os.queueEvent and you can create your own event.
Yesurbius #13
Posted 11 April 2013 - 05:12 AM
Thank you for the clear explanation Lyqyd.

So to attempt to answer my own question. If you need to pass parameters to a coroutine, its suggested (for the sake of making life easy) to design the coroutine to respond to one or more event, and pass the arguments in as parameters to an event.

Lets see if I got it through my head yet … (forgive me if my code is wrong - I'm at work where I cannot test it)


function count(eventID, StartingValue, IterateAmount, IterateCount )
  local currentValue = StartingValue
  local iterations = 0
  while true do
	if eventID == "terminate" then
	  break
	end
	if eventID="iterate" then
	  currentValue = currentValue + IterateAmount
	  iterations = iterations + 1
	  if iterations => IterateCount then
		return
	  end
	  coroutine.yield(eventID, currentValue)
	end
  end
end

local coIter = courtine.create(count)
local eventdata = {}
os.queueEvent("iterate",10,5,5)
while true do
  if (filter == nil) or (filter == eventdata[1]) or (eventdata[1] == "terminate") then
  status, value = coroutine.resume(coIter, eventdata)
  assert(status,value)
  filter = value
  if coroutine.status == "dead" then
	break
  end
  eventdata = { os.pullEventRaw() }
end

Did I finally wrap my head around it?


If I did then I'll take a stab next at adding a 1 second os.sleep() call into my coroutine. Is the source code for os.sleep() available anywhere?

Thanks
Lyqyd #14
Posted 11 April 2013 - 07:26 AM
I'm sure this isn't the exact source, but sleep is essentially:


function sleep(time)
  local id = os.startTimer(time)
  local returnedID
  repeat
    e, returnedID = os.pullEvent("timer")
  until id == returnedID
end

You came pretty close on your coroutine function. Please note that you need to catch the values that pullEvent, pullEventRaw, or coroutine.yield return; you don't pass them the variable names to put the information into. If you use os.pullEvent() instead of coroutine.yield, it will also handle the terminate event for you (it error()s, which should be fine if all you do is terminate the coroutine anyway, as this won't kill the coroutine manager). I might re-write it like this:


function count(eventID, StartingValue, IterateAmount, IterateCount )
  local currentValue = StartingValue
  local iterations = 0
  while true do
    event, param1 = os.pullEvent("iterate") --os.pullEvent handles terminate for us, and wraps coroutine.yield.
    currentValue = currentValue + IterateAmount
    iterations = iterations + 1
    if iterations => IterateCount then
      return
    end
  end
end

In the above example, os.pullEvent could be swapped out directly for coroutine.yield. The usage is identical, but you would have to add the handling for the terminate event back in.

Your coroutine manager does not do quite what you expect. You queue the first iterate event (ostensibly to pass in the initializing parameters), but then the resume call occurs before that event is pulled, so your coroutine is passed a blank event, which does not fill the initial values. You would want to resume it prior to entering the loop to get it primed. Something like this should do the trick:


coroutine.resume(coIter, "iterate", 10, 5, 5)

You do certainly seem to have a more firm grasp of the concepts now, so you should have fairly few problems once you're on the right track.
PixelToast #15
Posted 11 April 2013 - 07:28 AM
its in le bios.lua
anyway, this is wrong:

  if coroutine.status = "dead" then
		break
  end
needs to be:

  if coroutine.status(coIter) == "dead" then
		break
  end
diegodan1893 #16
Posted 11 April 2013 - 08:25 AM
its in le bios.lua
anyway, this is wrong:

  if coroutine.status = "dead" then
		break
  end
needs to be:

  if coroutine.status(coIter) = "dead" then
		break
  end

It needs to be

  if coroutine.status(coIter) == "dead" then
		break
  end

Don't forget that "==" is not the same as "=".
Yesurbius #17
Posted 11 April 2013 - 06:36 PM
Man I don't get this at all. Shouldn't be that hard . I get the concepts - I'm just not sure how they can be used.

I read that …
coroutine.yield will pass its parameters as the return value for resume
similarily, resume's parameters will be the return value for yield.

That cleared up some of the questions.

I'm struggling with the parameters right now.

The program I posted in the very beginning.

There is two routines: go and fuelManager.

Go needs a list of cmds in order to do its thing. Fuelmanager and go are both coroutines because they both need to run at the same time.

So in my main function - if I set up a coroutine manager .. its going to essentially pull an event from os.pullEvent() .. and if any of my managed coroutines are looking for that pulled event, it'll resume the coroutine.
Similarly, each coroutine will do its thing - and return when its pausing waiting for something to be done.

In the case of Go - the yields are going to be from turtle movements.
In the case of FuelManager - the yields are going to be from the sleep (and maybe the refuel - not sure)
So both coroutines are spending a lot of time yielding back .. and everything's all happy and merry until either Go or FuelManager exit .. then we're done - show's over.

The problem is .. how do you start the function? It needs a variable passed to it … a list of turtle commands.
You said that all coroutines should treat parameters passed to them as events they are receiving. So that would mean that they are resumed always with coroutine.resume(r,unpack(eventdata)). And eventdata always contains events .. so there is no piggybacking parameters in on that. Do I use a global variable?

I could us os.queueEvent() but I can't call that until after the first iteration of the coroutine manager runs .. and it has to be called from outside the loop - otherwise it'll repeat nonstop.
Lyqyd #18
Posted 11 April 2013 - 06:59 PM
Well, you can pass in whatever you want, whenever you want. It's simply that the convention in ComputerCraft is the event-based model. Here's a slightly modified version of your example above. Note that we don't bother with an event string when we resume the coroutine the first time (outside the management loop), so we can simply pass in the initial parameters to the function. I also made a few of the changes I mentioned above.


function count(StartingValue, IterateAmount, IterateCount )
    local currentValue = StartingValue
    local iterations = 0
    while true do
        local e, param = os.pullEvent("iterate")
        currentValue = currentValue + IterateAmount
        iterations = iterations + 1
        if iterations => IterateCount then
            return
        end
    end
end

local coIter = courtine.create(count)
local filter
local eventdata = {}
coroutine.resume(coIter, 10, 5, 5)
while true do
    eventdata = { os.pullEventRaw() }
    if (filter == nil) or (filter == eventdata[1]) or (eventdata[1] == "terminate") then
    status, value = coroutine.resume(coIter, unpack(eventdata))
    --assert(status,value) --not sure what this line was for.
    filter = value
    if coroutine.status(coIter) == "dead" then
        break
    end
end
Yesurbius #19
Posted 11 April 2013 - 09:25 PM
Believe it or not - I was lying awake in bed and this was bugging me LOL

But I realized a few things. First off .. I was thinking that when we are calling resume - we are calling the function itself… Not the case … we are restarting the execution of the thread within the function - so the parameters of the function don't matter at all to resuming…

So then I realized that it wouldn't matter if a queueEvent was placed within the main control loop - because that would simply ensure that our task is getting its share of attention from computercraft…

So I hammered out a quick version of my original program … from scratch … I'ts hanging up in Go .. which is frustrating .. I'll figure out what's causing that .. but looking beyond the syntactical substance .. is this the type of design I'm looking to achieve for my purpose?



function main(cmds)

  local routines = {}
  local eventdata = {}
  local filter = {}
  local status
  local n

  routines[1] = coroutine.create(go)
  routines[2] = coroutine.create(fuelManager)

  while true do
    for i=1,#routines do
      r = routines[i]
      if filter[r] == nil or filter[r] == eventdata[1] or eventdata[1] == "terminate" then
        status, param = coroutine.resume(r, unpack(eventdata))
        assert(status,param)
        if coroutine.status(r) == "dead" then
          return i
        end
        filter[r] = param
      end
    end

    for i=1,#routines do
      r = routines[i]
      if coroutine.status(r) == "dead" then
        return i
      end
    end

    os.queueEvent("docmds",unpack(cmds))
    eventdata = { coroutine.yield() }
  end

end

function go()
  local tcmds = { }
  local eventID, cmds = coroutine.yield("docmds")
  tcmds = tablify(cmds)
  print("event: " .. eventID)
  -- its hanging here - not sure why.
  for i=1,#tcmds do
    print(i .. ". " .. tcmds[i])
  end
  if eventID == "docmds" then
    for i=1,#cmds do
      print(cmds[i])
      sleep(1)
    end
  end
end

function fuelManager()
  FuelOnHand = 40
  while FuelOnHand > 10 do
    sleep(1)
    FuelOnHand = FuelOnHand - 5
  end
end

function tablify(strSeq)
  local retValue = {}
  local nCurrMatchPos = 0
  local nLastMatchPos = 0
  while true do
   nCurrMatchPos = string.find(strSeq, " ", nLastMatchPos + 1)
   if nCurrMatchPos then
     s = string.sub(strSeq,nLastMatchPos+1,nCurrMatchPos-1)
     if s ~= "" then
       table.insert(retValue,s)
       nLastMatchPos = nCurrMatchPos
     end     
   else
     break
   end
  end
  return retValue
end

local targs = { ... }
local func = ""
local i = 0
local func = main(targs)

if func == 1 then
  print("Commands Completed")
elseif func == 2 then
  print("Out of Gas")
else
  print("Not sure what happened ... " .. func)
end

Does it look like I finally figured this out? (despite not having a working version? LOL Only reason I'm posting it unfinished is because its 1:30am and I work at 6am LOL)

As a side note - maybe there is an easier way to get rid of tablify ..
JokerRH #20
Posted 12 April 2013 - 04:16 AM
I'ts hanging up in Go .. which is frustrating .. I'll figure out what's causing that ..

It might be the bug with queueEvent.
I already posted that in the bugs section but basically queueEvent won't work until there hasn't been another event before (That does not use queueEvent)
A simple sleep(0) in the beginning of the program should fix that… :D/>
Lyqyd #21
Posted 12 April 2013 - 06:35 AM
But I realized a few things. First off .. I was thinking that when we are calling resume - we are calling the function itself… Not the case … we are restarting the execution of the thread within the function - so the parameters of the function don't matter at all to resuming…

I… what? Have you been reading my posts? I said explicitly that the first time we call resume on a newly created coroutine, the parameters will be passed in as the initial parameters to the function. How much of what I've been writing have you been ignoring. No wonder I've been having to repeat myself.
JokerRH #22
Posted 12 April 2013 - 09:01 AM
But I realized a few things. First off .. I was thinking that when we are calling resume - we are calling the function itself… Not the case … we are restarting the execution of the thread within the function - so the parameters of the function don't matter at all to resuming…

I… what? Have you been reading my posts? I said explicitly that the first time we call resume on a newly created coroutine, the parameters will be passed in as the initial parameters to the function. How much of what I've been writing have you been ignoring. No wonder I've been having to repeat myself.

Having a bad day? :P/>
But Yesurbius is right. The first resume will start the function but any following one will resume inside of the function, right were it yielded
Lyqyd #23
Posted 12 April 2013 - 11:09 AM
He's implying that the function cannot be provided its initial parameters.
Yesurbius #24
Posted 12 April 2013 - 01:23 PM
Actually I was implying that given the way the event system is set up - you don't need to pass parameters in to the coroutine via the resume - you can pass it in to the coroutine via queueEvent (assuming you are setting your coroutine up to respond to events)

As for reading your posts - I can assure you I am reading them multiple times . I appreciate the time you are spending trying to answer my questions - it is far more than I expected to receive. I have gotten different pieces mixed up in my head at times however .. For example - we were using the count function as an example and I kind of thought that coroutines would be returning values everytime they yield. While its true that they can .. it was complicating the issue .. that's why I went back to the original example .. which actually has a need for a dedicated event system.

As for my code - does it look like I finally got the concept or is it also wrong? The code isn't working but I'm hoping its syntax error or a bug - and not my design.
Lyqyd #25
Posted 12 April 2013 - 01:48 PM
It looks like it should mostly work. I'm not really sure what you've got the assert() call in there for, though. The design looks much better than previous iterations. Your tablify function can be shortened. This should be approximately equivalent:


function tablify(strSeq)
    local retValue = {}
    for str in string.gmatch(strSeq, "%S+") do
        table.insert(retValue, str)
    end
    return retValue
end

Try removing the assert() line and see what you get.
Yesurbius #26
Posted 12 April 2013 - 07:11 PM
Well - I think its safe to say I have it figured out. My program is working flawlessly, even with the sleep calls in the coroutines (which was causing issues before). I also managed to remove the tablify altogether and streamline the code a tad.

Note I use the assert(param1,param2)as shorthand for writing if <param1> == false then error(param2) end

When I get some time I'll modify my original program in this style. This was the first turtle program I wanted to make, to learn coroutines. My real project is a turtle autorun to upload arbitrary programs to the turtle… which I think I am in a better position to tackle now.

Thanks for the help,



function main(cmds)

  local routines = {}
  local eventdata = {}
  local filter = {}
  local status
  local n

  routines[1] = coroutine.create(go)
  routines[2] = coroutine.create(fuelManager)

  while true do
	for i,r in ipairs(routines) do
	  if filter[r] == nil or filter[r] == eventdata[1] or eventdata[1] == "terminate" then
		status, param = coroutine.resume(r, unpack(eventdata))
		assert(status,paraam)
		filter[r] = param
		if coroutine.status(r) == "dead" then
		  return i
		end
	  end
	end

	for i,r in ipairs(routines) do
	  if coroutine.status(r) == "dead" then
		return i
	  end
	end

	os.queueEvent("docmds", unpack(cmds))
	eventdata = { coroutine.yield() }
  end

end

function go()
  local tcmds = { }
  tcmds = { coroutine.yield("docmds") }
  local eventID = tcmds[1]
  table.remove(tcmds,1)
  if eventID == "docmds" then
	for i,v in ipairs(tcmds) do
	   print(v)
	   sleep(1)
	end
  end
end

function fuelManager()
  FuelOnHand = 40
  while FuelOnHand > 10 do
	FuelOnHand = FuelOnHand - 5
	print("Fuel: " .. FuelOnHand)
	sleep(1)
  end
end

local targs = { ... }
local func = ""
local i = 0
local func = main(targs)

if func == 1 then
  print("Commands Completed")
elseif func == 2 then
  print("Out of Gas")
else
  print("Not sure what happened ... " .. func)
end
theoriginalbit #27
Posted 12 April 2013 - 07:16 PM
Note I use the assert(param1,param2)as shorthand for writing if <param1> == false then error(param2) end
That is fine, it is the correct usage of assert, to replace
if not <param1> then error(<param2>) end
You may be interested in my custom assert that I posted here and describe in more detail in my tutorial on handling and creating errors here.