davidgranstrom / losc

Open Sound Control (OSC) for lua/luajit
MIT License
20 stars 2 forks source link

WIP: Add non-blocking mode #34

Open Simon-L opened 11 months ago

Simon-L commented 11 months ago

Please do not merge as is :)

Related to #33 and #32 Attempted to add necessary methods for non-blocking, as far as I can tell this is working.

Basically it sets a timeout on receive so poll() just returns and other things can happen inbetween polling for new messages, what do you think? I haven't checked using other plugins with these changes.

Note that the new example has a temporary line to run from the source repo and bypas globally installed losc

davidgranstrom commented 11 months ago

Hi @Simon-L and thanks for this PR! I think poll would be a great addition to the API. I have a little different thought about the API though, how about if poll would return the OSC packet and it would be up to the user to dispatch it? This would make the API a bit more flexible IMHO in regards to different threading scenarios (e.g. sending the packet to a different thread for dispatch)

The signature could look like this:

--- Poll raw OSC packets.
-- @param[opt] ... Plugin specific arguments.
-- @return status, osc packet bytes or nil/error
-- @usage
-- -- nil is a valid return value so always check status and packet
-- -- before accessing the data.
-- local status, packet = losc:poll()
-- if status and packet then
--   -- do something with packet
-- end
function losc:poll(...)
  if not self.plugin then
    error('"poll" must be implemented using a plugin.')
  end
  return pcall(self.plugin.poll, self.plugin, ...)
end

This would be the implementation (for luasocket)

-- non-blocking
function M:open(host, port, timeout)
  host = host or self.options.recvAddr
  host = socket.dns.toip(host)
  port = port or self.options.recvPort
  self.handle:setsockname(host, port)
  self.handle:settimeout(timeout or 0.0)
end

function M:poll()
  return (self.handle:receive())
end

And the main loop of your example would look like this:

local i = 0
while true do
  print("Loop iteration " .. i)
  require'socket'.select(nil, nil, 0.25) -- equivalent for sleep() to simulate other tasks
  local status, data = osc:poll()
  if status and data then
    local ok, err = pcall(Pattern.dispatch, data, udp)
    if not ok then
      print(err)
    end
  end
  i = i + 1
end

We could also add losc:dispatch(data) to the API as a convenience function for the above.

Let me know what you think!

halee88 commented 11 months ago

@davidgranstrom I like the idea of having the dispatch be separate from the poll()/plugins. I think this could be taken further and fully separate the dispatch/handler system into its own class (dispatcher as a sibling of pattern, packet, serializer) and could be completely optional for users of your library (eg. an app that just forwards raw packets to another endpoint wouldn't necessarily care about osc addresses) That being said I don't mind poll() executing the dispatching directly. It follows the original spirit of your library and it works great in my usage case!

Simon-L commented 11 months ago

Hey there thanks for the thoughts. I absolutely agree about not automatically dispatching although semantically speaking a non-blocking mode doesn't imply dispatching any differently than the blocking mode, but I get the idea!

This also brings to the table the idea of being able to (re)route osc messages.

@halee88 As far as I can tell from the code and @davidgranstrom's examples above, Pattern.dispatch is already very much decoupled from the rest of the library, which is very handy for what you suggest.

halee88 commented 11 months ago

@Simon-L ahh good catch! I didn't see that pattern had encapsulated everything needed