Skip to content

basic bundle support + minimal example#1

Merged
GModal merged 1 commit into
GModal:mainfrom
halee88:main
Oct 16, 2023
Merged

basic bundle support + minimal example#1
GModal merged 1 commit into
GModal:mainfrom
halee88:main

Conversation

@halee88

@halee88 halee88 commented Sep 28, 2023

Copy link
Copy Markdown
Contributor

This library is exactly what I needed for my project (löve2d + non-blocking/threaded sockets) but it was missing bundle support. I thought I would take a crack at it and thankfully it didn't take too many lines to get it done. I followed the solution for bundles in https://github.com/mhroth/tinyosc and took advantage of as much of the code you already wrote

Main additions:
isBundle(udpM) - looks for the #bundle prefix
oscUnpackBundle - like oscUnpack but returns a table 1 or more tuples {oscADDR, oscTYPE, oscDATA}

I updated the oscdump.lua example to include bundle support

@GModal

GModal commented Sep 29, 2023

Copy link
Copy Markdown
Owner

Hey, this looks pretty cool, great job on this.

I'll check out the new functions -- I wish more (any ?) Linux apps supported bundles. Maybe a quick c test with liblo using bundles would be enough.
Is it working well, say with specific hardware, for you?

Thx, Doug

@GModal

GModal commented Sep 30, 2023

Copy link
Copy Markdown
Owner

I've played with it, looks good.

My suggestion: the .isBundle() function should return the timetag if true, nil if false. Here's a possible version:

--check if packet is a bundle
function oLvosc.isBundle(udpM)
local iv = nil
local b = udpM:match("^#bundle")
	if b ~= nil then
		iv, _ = oLvpk.unpack("c8", string.sub(udpM, 9, 16))
	end
return iv
end

BTW, my .unpackTIME() function was working incorrectly, at least with liblo. It needed the unpack format strings to force big endian. ('>I>I', instead of 'II'). I didn't see that before, but I don't recall if timetag code was tested outside of Löve2d.

Extracting the 8 byte timetag directly in .isBundle() could be (should be?) moved to another function, as well, for reuse.

I expect to merge your pull fairly soon, and make those changes after. Having added this, I'll work on sending bundles...

@GModal

GModal commented Oct 8, 2023

Copy link
Copy Markdown
Owner

Still will merge the pull shortly. I'll do that, then update the tag.

Afterwards, I have new functions that unpack nested bundles recursively (and some support functions for searching and flattening the nested results to simple table lists). After a bit of more testing, I'll update the project with the recursive functions and re-tag.

So your contributions will be integrated (and noted in the source). You really bumped me back into working on this!

Until then, here are the new base library functions:

--check if packet is a bundle
function oLvosc.isBundle(udpM)
local iv = nil
local b = udpM:match("^#bundle")
	if b ~= nil then
		iv, _ = oLvpk.unpack("c8", string.sub(udpM, 9, 16))
	end
return iv
end

--recursively unpack a bundle
--	returns a table of nested msgs and sub-bundles
--      Original (non-recursive) oscUnpackBundle() function by halee88 (https://github.com/halee88)
function oLvosc.oscUnpackBundle(packet, level)
local mpos = 17 -- skip '#bundle+timestamp'
local packet_size
local num_bytes = string.len(packet)
local parsed_messages = {}
	local tc, _ = oLvpk.unpack("c8", string.sub(packet, 9, 16))
	local tsec, tfrac = oLvosc.unpackTIME(tc)
	table.insert(parsed_messages, {'bun', level, tsec, tfrac})  
	while mpos < num_bytes do
		packet_size = oLvpk.unpack(">i", packet:sub(mpos, mpos + 4))
		local bunElem = packet:sub(mpos + 4, mpos + 4 + packet_size)
		if oLvosc.isBundle(bunElem) then
			table.insert(parsed_messages, oLvosc.oscUnpackBundle(bunElem, level + 1))
		else
			local oscADDR, oscTYPE, oscDATA = oLvosc.oscUnpack(bunElem:sub(1, packet_size))
			table.insert(parsed_messages, {'tup', level, tc, oscADDR, oscTYPE, oscDATA})
		end
		mpos = mpos + 4 + packet_size
	end
	return parsed_messages
end

Bundles don't have names / labels, so I figure (other than searching the msg address) attaching the timetag and "level" (sub-bundle depth) to each msg is adequate for differentiating between other bundles...

@halee88

halee88 commented Oct 8, 2023

Copy link
Copy Markdown
Contributor Author

Nice work! I didn't realize bundles could be recursive in OSC. Flattening them is, I think, the right call

@GModal

GModal commented Oct 8, 2023

Copy link
Copy Markdown
Owner

The function that flattens the results table:

-- recursively flatten the nested tables in an unpacked bundle to a simple table list of the msgs
local function resultsToList(results)
	local rdata = {}
	if type(results) == 'table' then
		for _, elem in ipairs(results) do
			if type(elem) == 'table' then
				if elem[1] == 'tup'  then
					table.insert(rdata, elem)
				else
					local tmpdata = resultsToList(elem, rdata)
					for _,inlst in ipairs(tmpdata) do
						table.insert(rdata, inlst)
					end
				end
			end	
		end
	end
	return(rdata)
end

It's kinda cool, as this function flattens both the raw results from oLvosc.oscUnpackBundle() and the results from my bundleSearch() function. It's pretty simple to call resultsToList() after unpacking a bundle, and it appeals to me to let the end user decide if they can use the nested table.

There's also a bundleView() function that prints the nested table to the shell. It's output looks like:

BUNDLE:  ( Lev: 1  Time: [0:1] )
-MSG: /messageInBundle/1  ss  ( Lev: 1  Time: [0:1] )
 "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-MSG: /messageInBundle/2  fssi  ( Lev: 1  Time: [0:1] )
 65.688003540039 "RASAWASAWWWWW hello weird world" "The bundle 2" 55411
-MSG: /messageInBundle/3/msg1/added/again  ss  ( Lev: 1  Time: [0:1] )
 "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    BUNDLE:  ( Lev: 2  Time: [10:4294967292] )
    -MSG: /nestedBundle/n1  fssi  ( Lev: 2  Time: [10:4294967292] )
     65.688003540039 "RASAWASAWWWWW hello weird world" "The bundle 2" 55411
        BUNDLE:  ( Lev: 3  Time: [4:1] )
        -MSG: /deepBundle/n2  ss  ( Lev: 3  Time: [4:1] )
         "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-MSG: /messageInBundle/4  ss  ( Lev: 1  Time: [0:1] )
 "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    BUNDLE:  ( Lev: 2  Time: [4:1] )
    -MSG: /deepBundle/n2  ss  ( Lev: 2  Time: [4:1] )
     "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

Of course you could easily do this with the level information in a flattened list... :-)
(I wrote a very short c program using the liblo library to send the nested bundles packets for the testing, so I know it's a valid packet.)

I can certainly add resultsToList() to the oLvosc library, rather than just as an external example function.

@GModal

GModal commented Oct 12, 2023

Copy link
Copy Markdown
Owner

Code for creating valid bundle packets, for the oLvosc library. Not fully tested, but seems to be working OK...

-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
-- BUNDLE send code 
-- create a new bundle db structure
function oLvosc.newBundle(time)
	local bundle = {}
	bundle.ID = 'bun'
	bundle.time = time
	bundle.elemNum = 0
	bundle.size = 16 -- base size of a bundle without elems
	bundle.elems = {}
	return (bundle)
end

-- add a message to a bundle db structure
function oLvosc.addMsgToBundle(bundle, msgPack)
	local message = {}
	bundle.elemNum = bundle.elemNum + 1
	message.ID = 'msg'
	message.body = msgPack
	message.size = string.len(msgPack)
	bundle.size = bundle.size + message.size + 4 -- add new msgs size to bundle
	table.insert(bundle.elems, message)
end

-- add a populated bundle to an existing bundle in a db struture
--	No additional data can be added to this bundle after this operation
function oLvosc.addBundleToBundle(bundle, addBundle)
	bundle.elemNum = bundle.elemNum + 1
	bundle.size = bundle.size + addBundle.size + 4 -- add new bundle size to bundle
	table.insert(bundle.elems, addBundle)
end

-- create a bundle packet from a bundle db structure
--	call with the populated bundle db structure as the single arg: 
--	    local bpacket = oLvosc.oscBundlePack(myBundle_db)
--	returns a transmissible osc packet
-- send packet with standard oLvosc.sendOSC()
function oLvosc.oscBundlePack(bun_db, level)
	local lpacket = ''
	if level == nil then level = 1 end

	if type(bun_db) == 'table' and bun_db.ID == 'bun' then
		local tsec, tfrac = oLvosc.unpackTIME(bun_db.time) 
		lpacket = lpacket..'#bundle\0'..bun_db.time
		for  _,be in ipairs(bun_db.elems) do
			if be.ID == 'bun' then
				lpacket = lpacket..oLvpk.pack('string', '>i4',be.size)
				lpacket = lpacket..oLvosc.oscBundlePack(be, level + 1)
			elseif be.ID == 'msg' then
				lpacket = lpacket..oLvpk.pack('string', '>i4',be.size)..be.body
			end	
		end
	end
	return(lpacket)
end

@GModal
GModal merged commit aa86d27 into GModal:main Oct 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants