Monday, 8 February 2016

Kamil Mech, Sprint #1 - Tie everything together

Team Bohemian Rasbian


Summary

In this sprint I was assigned with the task of setting up IPC between all components. Thanks to team cooperation, this functionality was implemented without any major issues.

I would also like to thank lads for introducing their engineering expertise to me. Working on this project together with the engineering students turned out to be a fantastic experience. I'm looking forward to next iteration.

The final code of the whole project, along with documentation, can be found on my github


IPC Design

Our current model is a combination of a message bus and shared memory.

Main process adds, initializes and starts threads.
Threads are imported from modules folder. Each thread is defined in a separate file.
Each thread extends custom-written IPCThread class, which takes in API upon initialization.
API is a dictionary containing a set of functions from the main process.
Those functions use locks and ownership to avoid concurrency problems.
IPCThread abstracts out API from thread developers. All they care about is data, not comms.

API functions are: registerOutput, output, getInputs and message.

message(message)

Synchronously prints a string to the screen on behalf of the thread (displays thread's name). Used for debugging, warnings and other notifications.

Example: 
self.message("Face #4 just left camera viewport")

output(tag, value)

Sends output @ tag on behalf of the thread - if said thread has ownership of the tag.

Example: 
self.output("opencv", {"x": -45, "y": 45})

registerOutput(tag, default)

Takes in a tag and a default value. Ownership of this tag is assigned to thread and no other thread can output to this tag. Each thread can register as many outputs as they like, as long as it does not violate ownership. If thread never outputs to this tag, dependents will receive the default value as a safe fallback.

Example: 
self.registerOutput("opencv", {"x": 0, "y": 0})

getInputs()

Returns the most up to date copy of all outputs. This way each thread can depend on as many other threads as they like without violating data access. Threads are free to request data whenever they like.

Example:
opencv = self.getInputs().opencv
lookAt(opencv.x, opencv.y)


Shared Memory

Main process stores data in comms object.
In general: comms.tag = value OR comms[tag] = value
Outputs are write operations to comms object.
Inputs are clones of comms object.
All objects involved are either a Dictionary or a DynamicObject.
DynamicObject is a custom-written class which extends functionality of a dictionary.
It is possible to easily convert between the two.

It has JSON-like structure:
obj = DynamicObject({
    "a": 5,
    "b": True
    "c": [0, 1, 2, 3, 4, 5]
})


It allows two types of accessing a field in a variable:
obj.x = value
obj["x"] = value


Both of these characteristics make reflection programming very easy. As a result, code is very compacted, fast and precise (key:value mappings play the key role here).

This system was designed with scalability in mind. Extending the system is a matter of "plug and play".


Upgraded Audio

Heng did a good job with sound. Unfortunately, he was not present on the day I was building the audio thread. Turns out we needed to make some changes in the way audio is controlled. I allowed myself to expand Heng's code.

Some utilities were added:
One variable: strpath(folder location to allow local file loading)

Three functions: play, kill(means stop) and isPlaying.

Just like Heng's implementation, it operates by spawning a bash-driven subprocess. The main difference is the use of subprocess.Popen, which gives us more control over child process.


path = str(os.path.realpath(__file__)).split("/")
path.pop() # remove last element i.e. this filename
strpath = ""
for element in path:
    strpath += element + "/"
 

def play (self, filename):
    proc = subprocess.Popen("mpg123 -q " + strpath + filename + "", stdout = subprocess.PIPE, shell = True, preexec_fn=os.setsid)
    return proc
 

def kill (self, proc):
    os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
 

def isPlaying (self, proc):
    if (not proc):
        return False
    playing = (proc.poll() == None)
    return playing



Kamil Mech, post #2

No comments:

Post a Comment