diff --git a/ag/__init__.py b/ag/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ag/ag.Aggregate.pyx b/ag/ag.Aggregate.pyx new file mode 100644 index 0000000..2e3d271 --- /dev/null +++ b/ag/ag.Aggregate.pyx @@ -0,0 +1,160 @@ +from collections import OrderedDict + +cdef class Aggregate: + def __init__(Aggregate self, *modules): + self._attrs = {} # attr_name, module + self._modules = OrderedDict() # module_name, module + self._provided = {} # keyword, module + self._common = {} # method_name, method_wrapper + + for module in modules: + self._link_module(module) + + def _list_modules(Aggregate self): + d = {} + + for attr, module in self._attrs.items(): + if module not in d: + d[module] = set() + + d[module].add(attr) + + return d + + def _link_module(Aggregate self, Module module): + # check for name (=type) collision + if module._name in self._modules.keys(): + raise ModuleCollision(module._name, None, "name", None) + + # check if requirements are satisfied + unsatisfied_deps = module._requires - set(self._provided.keys()) + if unsatisfied_deps: + raise ModuleDependencyError(module._name, unsatisfied_deps) + + # check for new module declaring a common method that we already provide as non-common + new_commons = module._common - set(self._common.keys()) + common_collisions = {nc for nc in new_commons if nc in self._attrs and not nc in self._common.keys()} + if common_collisions: + colliding_module_names = {self._attrs[x]._name for x in common_collisions} + raise ModuleCollision(module._name, colliding_module_names, "non-common method", common_collisions) + + # check for an attr collision + module_attrs = {x for x in dir(module) if x[0] != "_"} + attr_collisions = (module_attrs - module._common) & (set(self._attrs.keys()) | set(self._common.keys())) + if attr_collisions: + colliding_module_names = set() + for collision in attr_collisions: + if collision in self._attrs: + colliding_module_names.add(self._attrs[collision]._name) + + if collision in self._common: + colliding_module_names.add(self._common[collision]._name) + + raise ModuleCollision(module._name, colliding_module_names, "attribute", attr_collisions) + + # check for a provided keyword collision + provided_collisions = module._provides & set(self._provided.keys()) + if provided_collisions: + colliding_module_names = {self._provided[x]._name for x in provided_collisions} + raise ModuleCollision(module._name, colliding_module_names, "provided keyword", provided_collisions) + + # link the module + self._modules[module._name] = module + + for keyword in module._provides: + self._provided[keyword] = module + + for attr in (module_attrs - module._common): + self._attrs[attr] = module + + # create and/or populate CommonMethod wrappers to common methods + for method_name in module._common: + if method_name not in module_attrs: + raise CommonMethodMissing(method_name, module._name) + + if method_name not in self._common: + self._common[method_name] = CommonMethod(method_name) + + self._common[method_name].link_module(module) + + # hand the module a reference to us + module._top = self + + # call the module's _on_link method, if it has one + if hasattr(module, "_on_link"): + module._on_link() + + def _unlink_module(Aggregate self, str module_name): + if not module_name in self._modules: + raise ModuleDoesntExist(module_name) + + module = self._modules[module_name] + + # check reverse dependencies + global_deps = set() + for m in self._modules.values(): + global_deps.update(m._requires) + + reverse_deps = module._provides & global_deps + if reverse_deps: + raise ModuleDependencyError(module_name, reverse_deps, unlink=True) + + # remove from all pools + for aname, mod in list(self._attrs.items()): + if mod._name == module_name: + del self._attrs[aname] + + del self._modules[module_name] + + for ename in module._provides: + del self._provided[ename] + + # remove _common wrappers + for method_name in module._common: + self._common[method_name].unlink_module(module_name) + + # clear _top reference + module._top = None + + def _merge_in(Aggregate self, Aggregate other_ag): + for module_name, module in other_ag._modules.items(): + if module_name not in self._modules: + self._link_module(module) + + def __getattr__(Aggregate self, str aname): + if aname in self._attrs: + return getattr(self._attrs[aname], aname) + elif aname in self._common: + return self._common[aname] + else: + raise AttributeError("Aggregate has no attribute '%s'" %(aname)) + + def __setattr__(Aggregate self, str aname, avalue): + if aname not in self._attrs: + raise AttributeError("Aggregate has no attribute '%s'" %(aname)) + else: + setattr(self._attrs[aname], aname, avalue) + + def _get_type(Aggregate self): + return tuple(self._modules.keys()) + + def __repr__(Aggregate self): + module_count = len(self._modules) + + if module_count: + lines = ["Aggregate("] + + + for i, module in enumerate(self._modules.values()): + if i + 1 < module_count: + comma = "," + else: + comma = "" + + lines.append(" %s%s" %(repr(module), comma)) + + lines.append(")") + + return "\n".join(lines) + else: + return "Aggregate()" diff --git a/ag/ag.CommonMethod.pyx b/ag/ag.CommonMethod.pyx new file mode 100644 index 0000000..afb0e86 --- /dev/null +++ b/ag/ag.CommonMethod.pyx @@ -0,0 +1,30 @@ +cdef class CommonMethod: + def __init__(CommonMethod self, str method_name): + self.method_name = method_name + self.module_names = [] + self.modules = [] + + def link_module(CommonMethod self, Module module): + self.module_names.append(module._name) + self.modules.append(module) + + def unlink_module(CommonMethod self, module_name): + i = self.module_names.index(module_name) + + del self.module_names[i] + del self.modules[i] + + def __call__(CommonMethod self, *args): + return_values = [] + + for module in self.modules: + return_values.append(getattr(module, self.method_name).__call__(*args)) + + return return_values + + def __repr__(self): + return "CommonMethod(name=%s)" %(self.method_name) + + @property + def _name(self): + return repr(self) diff --git a/ag/ag.Module.pyx b/ag/ag.Module.pyx new file mode 100644 index 0000000..5014381 --- /dev/null +++ b/ag/ag.Module.pyx @@ -0,0 +1,20 @@ +cdef class Module: + def __init__(self, name, provides=set(), requires=set(), common=set()): + """ + * Modules are identified by their name which has to be unique within the Aggregate's namespace. + * The provides sequence contains keywords which are copied into Aggregate's _provided list. + Two modules providing the same export can't be linked into the same Aggregate. + * The requires sequence contains keywords that have to be already present in the Aggregate's + _provided list before linking. + * All names in the Module's namespace that don't begin with an underscore will be exported + into the Aggregate's namespace. + * If you want to call one method on multiple modules, these modules must all export the method name + in their "common" set. Otherwise a name collision is raised. Their methods will be called in the + same order in which the modules were linked. + """ + + self._top = None + self._name = name + self._provides = set(provides) + self._requires = set(requires) + self._common = set(common) diff --git a/ag/ag.header.pyx b/ag/ag.header.pyx new file mode 100644 index 0000000..e8eebb5 --- /dev/null +++ b/ag/ag.header.pyx @@ -0,0 +1,49 @@ +""" +Overwatch Aggregate Object type + +TODO Method overrides: _common gets renamed to _call_all and we add _call_last. +""" + +class ModuleCollision(Exception): + def __init__(self, colliding_module_name, resident_module_names, item_type, items): + self.colliding_module_name = colliding_module_name + self.resident_module_names = resident_module_names + self.item_type = item_type + self.items = items + + def __str__(self): + if self.item_type == "name": + return "Unable to link module '%s': name already present in Aggregate." %(self.colliding_module_name) + else: + return "Unable to link module '%s': %s(s) '%s' already provided by module(s) '%s'." %(self.colliding_module_name, + self.item_type, "', '".join(self.items), "', '".join(self.resident_module_names)) + +class ModuleDependencyError(Exception): + def __init__(self, module_name, dependencies, unlink=False): + self.module_name = module_name + self.dependencies = dependencies + self.unlink = unlink + + def __str__(self): + if self.unlink: + return "Unable to unlink module '%s': Aggregate depends on its export(s) '%s'." %(self.module_name, + "', '".join(self.dependencies)) + else: + return "Unable to link module '%s': requirement '%s' unsatisfied by Aggregate." %(self.module_name, + "', '".join(self.dependencies)) + +class CommonMethodMissing(Exception): + def __init__(self, method_name, module_name): + self.method_name = method_name + self.module_name = module_name + + def __str__(self): + return "Unable to link module '%s': method '%s' (declared as common) does not exist in module." %( + self.module_name, self.method_name) + +class ModuleDoesntExist(Exception): + def __init__(self, module_name): + self.module_name = module_name + + def __str__(self): + return "Unable to unlink module '%s': not linked to Aggregate." %(self.module_name) diff --git a/ag/build.sh b/ag/build.sh new file mode 100755 index 0000000..e69de29 diff --git a/build.sh b/build-all.sh similarity index 67% rename from build.sh rename to build-all.sh index 61568a3..5a5fb11 100755 --- a/build.sh +++ b/build-all.sh @@ -1,11 +1,10 @@ #! /bin/zsh # encoding: utf-8 -for dir in src-[0-9][0-9]_* +for dir in ag core m serial do echo "Building in ${dir}" cd "$dir" ./build.sh cd .. - ln -s $dir/*.so . done diff --git a/src-00_core/TODO b/core/TODO similarity index 100% rename from src-00_core/TODO rename to core/TODO diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src-00_core/build.sh b/core/build.sh similarity index 100% rename from src-00_core/build.sh rename to core/build.sh diff --git a/src-00_core/src/core.00-header.pyx b/core/core.00-header.pyx similarity index 100% rename from src-00_core/src/core.00-header.pyx rename to core/core.00-header.pyx diff --git a/src-00_core/src/core.10-text.pyx b/core/core.10-text.pyx similarity index 100% rename from src-00_core/src/core.10-text.pyx rename to core/core.10-text.pyx diff --git a/src-00_core/src/core.20-Output.pyx b/core/core.20-Output.pyx similarity index 100% rename from src-00_core/src/core.20-Output.pyx rename to core/core.20-Output.pyx diff --git a/src-00_core/src/core.30-File.pyx b/core/core.30-File.pyx similarity index 100% rename from src-00_core/src/core.30-File.pyx rename to core/core.30-File.pyx diff --git a/src-00_core/src/core.40-Main.pyx b/core/core.40-Main.pyx similarity index 100% rename from src-00_core/src/core.40-Main.pyx rename to core/core.40-Main.pyx diff --git a/src-00_core/src/core.50-misc.pyx b/core/core.50-misc.pyx similarity index 100% rename from src-00_core/src/core.50-misc.pyx rename to core/core.50-misc.pyx diff --git a/src-00_core/src/core.51-import.pyx b/core/core.51-import.pyx similarity index 100% rename from src-00_core/src/core.51-import.pyx rename to core/core.51-import.pyx diff --git a/m/__init__.py b/m/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/m/m.header.pyx b/m/m.header.pyx new file mode 100644 index 0000000..e8cf03f --- /dev/null +++ b/m/m.header.pyx @@ -0,0 +1,23 @@ +""" +Vector And Matrix Math + +TODO some nice description +""" + +from libc.stdlib cimport malloc, realloc, free +from libc.stdint cimport uint8_t, uint16_t, uint64_t +from libc.math cimport sin, cos, sqrt + +class MathError(Exception): + def __init__(self, description): + self.description = description + + def __str__(self): + return self.description + +class GeneralError(Exception): + def __init__(self, description): + self.description = description + + def __str__(self): + return self.description diff --git a/m/m.mat4.pyx b/m/m.mat4.pyx new file mode 100644 index 0000000..76254c8 --- /dev/null +++ b/m/m.mat4.pyx @@ -0,0 +1,332 @@ +cdef class mat4: + """ + A float 4x4 matrix. + + All arrays are column-major, i.e. OpenGL style: + + 0 4 8 12 + 1 5 9 13 + 2 6 10 14 + 3 7 11 15 + + The matrix implements stacking useful for graphics. + """ + + def __cinit__(mat4 self): + to_alloc = 16 * sizeof(float) + self.stack = malloc(to_alloc) + + if not self.stack: + raise MemoryError("Unable to malloc %d B for mat4." %(to_alloc)) + + self.m = self.stack + self.size = 1 + + def _debug(mat4 self): + print("--- self.stack = %d" %(self.stack)) + print("--- self.m = %d (+%d)" %(self.m, self.m - self.stack)) + print("--- self.size = %d" %(self.size)) + + def __init__(mat4 self, *args): + """ + Create a ma4t. + + Accepts any number of parameters between 0 and 16 to fill the + matrix from the upper left corner going down (column-wise). + """ + + length = len(args) + + if length == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + length = len(args) + + if length > 16: + raise MathError("Attempt to initialize a mat4 with %d arguments." %(length)) + + self.load_from(args) + + def __dealloc__(mat4 self): + free(self.stack) + + def __getstate__(mat4 self): + state = [] + + for i in range(self.m - self.stack + 16): + state.append(self.stack[i]) + + return state + + def __setstate__(mat4 self, state): + length = len(state) + matrices = length//16 + + if not matrices*16 == length: + raise GeneralError("mat4 __setstate__ got %d floats as a state" %(length)) + + self.m = self.stack + + slot_full = False + for start in range(0, length, 16): + if slot_full: + self.push() + slot_full = False + + self.load_from(state[start:start+16]) + slot_full = True + + def __getitem__(mat4 self, int i): + if i > 16 or i < 0: + raise IndexError("element index out of range(16)") + + return self.m[i] + + def __setitem__(self, int i, value): + if i > 16 or i < 0: + raise IndexError("element index out of range(16)") + + self.m[i] = value + + def push(mat4 self): + """ + Push the current matrix into the stack and load up an empty one (a zero matrix) + """ + + # self.m points to the current matrix + # self.stack points to the first matrix + # self.size how many matrices are allocated + + # ensure there's room for one more + cdef unsigned int used = 1 + (self.m - self.stack) / 16 + cdef unsigned int empty = self.size - used + cdef float *tmp + + if not empty: + self.size += 1 + to_alloc = self.size * 16 * sizeof(float) + tmp = realloc(self.stack, to_alloc) + + if tmp: + self.stack = tmp + else: + raise MemoryError("Unable to malloc %d B for mat4." %(to_alloc)) + + # advance the pointer to the new one + self.m = self.stack + 16 * used + + # at this point there's at least enough space for one matrix + # copy the old matrix into the new one + cdef uint8_t i + cdef float *old_m = self.m - 16 + for i in range(16): + self.m[i] = old_m[i] + + def pop(mat4 self): + """ + Pop a matrix from the stack. + """ + + if self.m == self.stack: + raise IndexError("pop from an empty stack") + + self.m -= 16 + + def get_list(mat4 self): + L = [] + + for i in range(16): + L.append(self.m[i]) + + return L + + def load_from(mat4 self, L): + """ + Fill the current matrix from a either a list of values, column-major, + or another matrix. This method doesn't modify the stack, only the + current matrix is read and modified. + + If the number of values isn't 16, it will be padded to 16 by zeros. + If it's larger, GeneralError will be raised. + """ + + if isinstance(L, mat4): + L = L.get_list() + length = 16 + else: + length = len(L) + + if length > 16: + raise GeneralError("supplied list is longer than 16") + + for i in range(16): + if i < length: + self.m[i] = L[i] + else: + self.m[i] = 0.0 + + def zero(mat4 self): + """Fill the matrix with zeroes.""" + + for i in range(16): + self.m[i] = 0.0 + + def identity(mat4 self): + """Make the matrix an identity.""" + + self.zero() + + self.m[0] = 1.0 + self.m[5] = 1.0 + self.m[10] = 1.0 + self.m[15] = 1.0 + + def transpose(mat4 self): + """Transpose the matrix.""" + + cdef float tmp + + tmp = self.m[1] + self.m[1] = self.m[4] + self.m[4] = tmp + + tmp = self.m[2] + self.m[2] = self.m[8] + self.m[8] = tmp + + tmp = self.m[3] + self.m[3] = self.m[12] + self.m[12] = tmp + + tmp = self.m[7] + self.m[7] = self.m[13] + self.m[13] = tmp + + tmp = self.m[11] + self.m[11] = self.m[14] + self.m[14] = tmp + + tmp = self.m[6] + self.m[6] = self.m[9] + self.m[9] = tmp + + def invert(mat4 self): + """Invert the matrix.""" + + cdef float tmp[16] + cdef float det + + tmp[0] = self.m[5]*self.m[10]*self.m[15] - self.m[5]*self.m[11]*self.m[14] - self.m[9]*self.m[6]*self.m[15] + self.m[9]*self.m[7]*self.m[14] + self.m[13]*self.m[6]*self.m[11] - self.m[13]*self.m[7]*self.m[10] + tmp[4] = -self.m[4]*self.m[10]*self.m[15] + self.m[4]*self.m[11]*self.m[14] + self.m[8]*self.m[6]*self.m[15] - self.m[8]*self.m[7]*self.m[14] - self.m[12]*self.m[6]*self.m[11] + self.m[12]*self.m[7]*self.m[10] + tmp[8] = self.m[4]*self.m[9]*self.m[15] - self.m[4]*self.m[11]*self.m[13] - self.m[8]*self.m[5]*self.m[15] + self.m[8]*self.m[7]*self.m[13] + self.m[12]*self.m[5]*self.m[11] - self.m[12]*self.m[7]*self.m[9] + tmp[12] = -self.m[4]*self.m[9]*self.m[14] + self.m[4]*self.m[10]*self.m[13] + self.m[8]*self.m[5]*self.m[14] - self.m[8]*self.m[6]*self.m[13] - self.m[12]*self.m[5]*self.m[10] + self.m[12]*self.m[6]*self.m[9] + + det = self.m[0]*tmp[0] + self.m[1]*tmp[4] + self.m[2]*tmp[8] + self.m[3]*tmp[12] + + # epsilon pulled straight out of Uranus + if det < 0.00005 and det > -0.00005: + print("det=%.1f" %(det)) + return + + tmp[1] = -self.m[1]*self.m[10]*self.m[15] + self.m[1]*self.m[11]*self.m[14] + self.m[9]*self.m[2]*self.m[15] - self.m[9]*self.m[3]*self.m[14] - self.m[13]*self.m[2]*self.m[11] + self.m[13]*self.m[3]*self.m[10] + tmp[5] = self.m[0]*self.m[10]*self.m[15] - self.m[0]*self.m[11]*self.m[14] - self.m[8]*self.m[2]*self.m[15] + self.m[8]*self.m[3]*self.m[14] + self.m[12]*self.m[2]*self.m[11] - self.m[12]*self.m[3]*self.m[10] + tmp[9] = -self.m[0]*self.m[9]*self.m[15] + self.m[0]*self.m[11]*self.m[13] + self.m[8]*self.m[1]*self.m[15] - self.m[8]*self.m[3]*self.m[13] - self.m[12]*self.m[1]*self.m[11] + self.m[12]*self.m[3]*self.m[9] + tmp[13] = self.m[0]*self.m[9]*self.m[14] - self.m[0]*self.m[10]*self.m[13] - self.m[8]*self.m[1]*self.m[14] + self.m[8]*self.m[2]*self.m[13] + self.m[12]*self.m[1]*self.m[10] - self.m[12]*self.m[2]*self.m[9] + tmp[2] = self.m[1]*self.m[6]*self.m[15] - self.m[1]*self.m[7]*self.m[14] - self.m[5]*self.m[2]*self.m[15] + self.m[5]*self.m[3]*self.m[14] + self.m[13]*self.m[2]*self.m[7] - self.m[13]*self.m[3]*self.m[6] + tmp[6] = -self.m[0]*self.m[6]*self.m[15] + self.m[0]*self.m[7]*self.m[14] + self.m[4]*self.m[2]*self.m[15] - self.m[4]*self.m[3]*self.m[14] - self.m[12]*self.m[2]*self.m[7] + self.m[12]*self.m[3]*self.m[6] + tmp[10] = self.m[0]*self.m[5]*self.m[15] - self.m[0]*self.m[7]*self.m[13] - self.m[4]*self.m[1]*self.m[15] + self.m[4]*self.m[3]*self.m[13] + self.m[12]*self.m[1]*self.m[7] - self.m[12]*self.m[3]*self.m[5] + tmp[14] = -self.m[0]*self.m[5]*self.m[14] + self.m[0]*self.m[6]*self.m[13] + self.m[4]*self.m[1]*self.m[14] - self.m[4]*self.m[2]*self.m[13] - self.m[12]*self.m[1]*self.m[6] + self.m[12]*self.m[2]*self.m[5] + tmp[3] = -self.m[1]*self.m[6]*self.m[11] + self.m[1]*self.m[7]*self.m[10] + self.m[5]*self.m[2]*self.m[11] - self.m[5]*self.m[3]*self.m[10] - self.m[9]*self.m[2]*self.m[7] + self.m[9]*self.m[3]*self.m[6] + tmp[7] = self.m[0]*self.m[6]*self.m[11] - self.m[0]*self.m[7]*self.m[10] - self.m[4]*self.m[2]*self.m[11] + self.m[4]*self.m[3]*self.m[10] + self.m[8]*self.m[2]*self.m[7] - self.m[8]*self.m[3]*self.m[6] + tmp[11] = -self.m[0]*self.m[5]*self.m[11] + self.m[0]*self.m[7]*self.m[9] + self.m[4]*self.m[1]*self.m[11] - self.m[4]*self.m[3]*self.m[9] - self.m[8]*self.m[1]*self.m[7] + self.m[8]*self.m[3]*self.m[5] + tmp[15] = self.m[0]*self.m[5]*self.m[10] - self.m[0]*self.m[6]*self.m[9] - self.m[4]*self.m[1]*self.m[10] + self.m[4]*self.m[2]*self.m[9] + self.m[8]*self.m[1]*self.m[6] - self.m[8]*self.m[2]*self.m[5] + + det = 1.0 / det + self.m[0] = tmp[0] * det + self.m[1] = tmp[1] * det + self.m[2] = tmp[2] * det + self.m[3] = tmp[3] * det + self.m[4] = tmp[4] * det + self.m[5] = tmp[5] * det + self.m[6] = tmp[6] * det + self.m[7] = tmp[7] * det + self.m[8] = tmp[8] * det + self.m[9] = tmp[9] * det + self.m[10] = tmp[10] * det + self.m[11] = tmp[11] * det + self.m[12] = tmp[12] * det + self.m[13] = tmp[13] * det + self.m[14] = tmp[14] * det + self.m[15] = tmp[15] * det + + def mulm(mat4 self, mat4 B, bint inplace=False): + """ + Return a matrix that is the result of multiplying this matrix by another. + + M = self * mat4 B + """ + + cdef uint8_t i + cdef mat4 tmp = mat4() + + tmp.m[0] = self.m[0] * B.m[0] + self.m[4] * B.m[1] + self.m[8] * B.m[2] + self.m[12] * B.m[3] + tmp.m[1] = self.m[1] * B.m[0] + self.m[5] * B.m[1] + self.m[9] * B.m[2] + self.m[13] * B.m[3] + tmp.m[2] = self.m[2] * B.m[0] + self.m[6] * B.m[1] + self.m[10] * B.m[2] + self.m[14] * B.m[3] + tmp.m[3] = self.m[3] * B.m[0] + self.m[7] * B.m[1] + self.m[11] * B.m[2] + self.m[15] * B.m[3] + tmp.m[4] = self.m[0] * B.m[4] + self.m[4] * B.m[5] + self.m[8] * B.m[6] + self.m[12] * B.m[7] + tmp.m[5] = self.m[1] * B.m[4] + self.m[5] * B.m[5] + self.m[9] * B.m[6] + self.m[13] * B.m[7] + tmp.m[6] = self.m[2] * B.m[4] + self.m[6] * B.m[5] + self.m[10] * B.m[6] + self.m[14] * B.m[7] + tmp.m[7] = self.m[3] * B.m[4] + self.m[7] * B.m[5] + self.m[11] * B.m[6] + self.m[15] * B.m[7] + tmp.m[8] = self.m[0] * B.m[8] + self.m[4] * B.m[9] + self.m[8] * B.m[10] + self.m[12] * B.m[11] + tmp.m[9] = self.m[1] * B.m[8] + self.m[5] * B.m[9] + self.m[9] * B.m[10] + self.m[13] * B.m[11] + tmp.m[10] = self.m[2] * B.m[8] + self.m[6] * B.m[9] + self.m[10] * B.m[10] + self.m[14] * B.m[11] + tmp.m[11] = self.m[3] * B.m[8] + self.m[7] * B.m[9] + self.m[11] * B.m[10] + self.m[15] * B.m[11] + tmp.m[12] = self.m[0] * B.m[12] + self.m[4] * B.m[13] + self.m[8] * B.m[14] + self.m[12] * B.m[15] + tmp.m[13] = self.m[1] * B.m[12] + self.m[5] * B.m[13] + self.m[9] * B.m[14] + self.m[13] * B.m[15] + tmp.m[14] = self.m[2] * B.m[12] + self.m[6] * B.m[13] + self.m[10] * B.m[14] + self.m[14] * B.m[15] + tmp.m[15] = self.m[3] * B.m[12] + self.m[7] * B.m[13] + self.m[11] * B.m[14] + self.m[15] * B.m[15] + + if inplace: + for i in range(16): + self.m[i] = tmp.m[i] + else: + return tmp + + def mulv(mat4 self, vec3 v): + """ + Return a vec3 that is the result of multiplying this matrix by a vec3. + + u = self * v + """ + + cdef mat4 tmp = vec3() + + tmp.v[0] = v.v[0]*self.m[0] + v.v[1]*self.m[4] + v.v[2]*self.m[8] + self.m[12] + tmp.v[1] = v.v[0]*self.m[1] + v.v[1]*self.m[5] + v.v[2]*self.m[9] + self.m[13] + tmp.v[2] = v.v[0]*self.m[2] + v.v[1]*self.m[6] + v.v[2]*self.m[10] + self.m[14] + + return tmp + + def mulf(mat4 self, f): + """ + Return a matrix that is the result of multiplying this matrix by a scalar. + + M = self * f + """ + + cdef mat4 tmp = mat4() + cdef int i + + for i in range(16): + tmp.m[i] = self.m[i] * f + + return tmp + + def __repr__(mat4 self): + lines = [] + + lines.append("mat4(%.1f %.1f %.1f %.1f" %(self.m[0], self.m[4], self.m[8], self.m[12])) + lines.append(" %.1f %.1f %.1f %.1f" %(self.m[1], self.m[5], self.m[9], self.m[13])) + lines.append(" %.1f %.1f %.1f %.1f" %(self.m[2], self.m[6], self.m[10], self.m[14])) + lines.append(" %.1f %.1f %.1f %.1f)" %(self.m[3], self.m[7], self.m[11], self.m[15])) + + return "\n".join(lines) diff --git a/m/m.vec3.pyx b/m/m.vec3.pyx new file mode 100644 index 0000000..61b0641 --- /dev/null +++ b/m/m.vec3.pyx @@ -0,0 +1,113 @@ +cdef class vec3: + """ + A float 3D vector. + + >>> v = vec3(1, 1, 0) + >>> w = vec3(0, 1, 1) + >>> v.length + 1.4142135623730951 + >>> v.dot(w) + 1.0 + >>> v.cross(w) + vec4(1.00, 1.00, 1.00) + >>> v + w + vec4(1.00, 2.00, 1.00) + >>> w - v + vec4(-1.00, 0.00, 1.00) + + """ + + def __init__(vec3 self, *args): + """ + Create a vec3. + + Accepts any number of parameters between 0 and 3 to fill the vector from the left. + """ + + length = len(args) + + if length == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + length = len(args) + + if length > 3: + raise MathError("Attempt to initialize a vec3 with %d arguments." %(length)) + + for i in range(3): + if i < length: + self.v[i] = args[i] + else: + self.v[i] = 0.0 + + def __getitem__(vec3 self, int i): + if i >= 3 or i < 0: + raise IndexError("element index out of range(3)") + + return self.v[i] + + def __setitem__(vec3 self, int i, float value): + if i >= 3 or i < 0: + raise IndexError("element index out of range(3)") + + self.v[i] = value + + def __repr__(vec3 self): + return "vec3(%.2f, %.2f, %.2f)" %(self.v[0], self.v[1], self.v[2]) + + def __getstate__(vec3 self): + return (self.v[0], self.v[1], self.v[2]) + + def __setstate__(vec3 self, state): + self.v[0] = state[0] + self.v[1] = state[1] + self.v[2] = state[2] + + @property + def length(vec3 self): + """Contains the geometric length of the vector.""" + + return sqrt(self.v[0]**2 + self.v[1]**2 + self.v[2]**2) + + def normalized(vec3 self): + """Returns this vector, normalized.""" + + length = self.length + + return vec3(self.v[0] / length, self.v[1] / length, self.v[2] / length) + + def __add__(vec3 L, vec3 R): + return vec3(L.v[0] + R.v[0], L.v[1] + R.v[1], L.v[2] + R.v[2]) + + def __sub__(vec3 L, vec3 R): + return vec3(L.v[0] - R.v[0], L.v[1] - R.v[1], L.v[2] - R.v[2]) + + def __neg__(vec3 self): + return vec3(-self.v[0], -self.v[1], -self.v[2]) + + def dot(vec3 L, vec3 R): + """ + Returns the dot product of the two vectors. + + E.g. u.dot(v) -> u . v + """ + + return L.v[0] * R.v[0] + L.v[1] * R.v[1] + L.v[2] * R.v[2] + + def cross(vec3 L, vec3 R): + """ + Returns the cross product of the two vectors. + + E.g. u.cross(v) -> u x v + + """ + + return vec3(L.v[1]*R.v[2] - L.v[2]*R.v[1], L.v[0]*R.v[2] - L.v[2]*R.v[0], L.v[0]*R.v[1] - L.v[1]*R.v[0]) + + def __mul__(vec3 L, R): + """ + Multiplication of a vec3 by a float. + + The float has to be on the right. + """ + + return vec3(L.v[0] * R, L.v[1] * R, L.v[2] * R) diff --git a/serial/__init__.py b/serial/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/serial/build.sh b/serial/build.sh new file mode 100755 index 0000000..e69de29 diff --git a/serial/com.py b/serial/com.py new file mode 100644 index 0000000..e49d23d --- /dev/null +++ b/serial/com.py @@ -0,0 +1,190 @@ +#! /bin/env python3 +# encoding: utf-8 + +import time +import serial + +from . import et + +prefix = et.prefix + +""" +===== OverTalk protocol spec ===== +==== Transport layer ==== + +frame = [ 0x2a | destination (1) | source (1) | payload length (1) | payload (n) | checksum (1) ] + +Any time an 0x2a is encountered the parser should flush its state and begin reading a new frame. +Bytes 0x2a, 0x2b, 0x11 and 0x13 are prefixed with 0x2b and inverted (XORed with 0xff) to avoid collisions. + +Frames with destinations unknown to an OverShell are sent to the default interface, or dropped if +that's the interface they came from. + +==== Command layer ==== +Payload consists of one or more commands: + +get_command = [ 0x00 | register (1) | target register (1) ] +set_command = [ 0x01 | register (1) | length (1) | value (n) ] + +With a get_command the sender requests the receiver to read its own "register" and issue +a set_command that sets the sender's "target register" to that value. + +A set_command does what is says on the box. +""" + +class Transport: + """ + OverTalk Transport layer implementation. + + Reads data from multiple interfaces and either router frames or receives them. + A received frame is then made available via a public attribute. + + Interfaces are objects that implement methods read() (returns one byte of data as int), write(buffer, len), + and waiting property (contains number of bytes waiting). + """ + def __init__(self, my_id, interfaces, def_route): + """ + @param my_id OverTalk address of this Transport instance + @param interfaces a dict of interfaces keyed by their IDs + @param def_route ID of default interface for sending + """ + + assert my_id not in interfaces + assert def_route in interfaces + + self._print = et.Output("over.com.Transport", default_suffix="\n", timestamp=True) + + self.my_id = my_id + self.def_route = def_route + self.interfaces = interfaces + + self.destination_unknown = 0 + self.incoming = None + + self._print("on-line") + + def update(self): + self.incoming = None + + for interface_id, interface in self.interfaces.items(): + if interface.waiting: + interface.last_data_received = time.time() + byte = interface.read() + + if byte == 0x2a: + interface.rxbuffer = [] + interface.reading_escape = False + interface.reading_frame = True + + if byte == 0x2b: + interface.reading_escape = True + continue + + if interface.reading_escape: + byte ^= 0xff + interface.reading_escape = False + + if interface.reading_frame: + interface.rxbuffer.append(byte) + + if self.verify_frame(interface): + self.process_frame(interface_id, interface.rxbuffer) + interface.rxbuffer = [] + + def verify_frame(self, interface): + buffer_length = len(interface.rxbuffer) + + if buffer_length >= 4: + # at this point we can read frame's declared size + if buffer_length >= interface.rxbuffer[3] + 5: # header (4) + payload + checksum (1) + # a frame has been read, huzzah! + interface.reading_frame = False + + # checksum + if sum(interface.rxbuffer[:-1]) % 0x100 == interface.rxbuffer[-1]: + return True + + else: + interface.malformed_frames += 1 + self._print("broken frame received: %s" %(interface.rxbuffer)) + interface.rxbuffer = [] + + return False + + def escape_frame(self, frame): + assert frame[0] == 0x2a + + frame_escaped = [0x2a] + + for byte in frame[1:]: + if byte in (0x2a, 0x2b, 0x11, 0x13): + frame_escaped.append(0x2b) + byte ^= 0xff + + frame_escaped.append(byte) + + return frame_escaped + + def process_frame(self, source_interface_id, frame): + payload = frame[4:-1] + destination = frame[1] + + if destination == self.my_id: + self.incoming = (frame[2], payload) + else: + if destination in self.interfaces: + self._print("routing frame to [%d]" %(destination)) + self.interfaces[destination].write(self.escape_frame(frame)) + else: + if source_interface_id == self.def_route: + self.destination_unknown += 1 + self._print("unknown destination %d for frame: %s" %(destination, + repr(frame)), prefix.fail) + else: + self._print("routing frame to default route [%d]" %(self.def_route)) + self.interfaces[self.def_route].write(self.escape_frame(frame)) + + def send_data(self, destination, data): + frame = [0x2a, destination, self.my_id, len(data)] + list(data) + frame.append(sum(frame) % 0x100) + frame = self.escape_frame(frame) + + if destination in self.interfaces: + s = self.interfaces[destination] + else: + s = self.interfaces[self.def_route] + + s.write(frame) + +class TTL_Interface: + def __init__(self, interface="/dev/ttyUSB0", baudrate=57600): + try: + self.s = serial.Serial(interface, baudrate, timeout=1) + except serial.serialutil.SerialException: + self.s = None + + self.rxbuffer = [] + self.reading_escape = False + self.reading_frame = False + + self.malformed_frames = 0 + self.last_data_received = 0 + + @property + def waiting(self): + if self.s: + return self.s.inWaiting() + else: + return 0 + + def read(self): + if self.s: + return ord(self.s.read()) + else: + return 0 + + def write(self, data): + print("Sending:", ''.join([hex(x)[2:].zfill(2) for x in data])) + + if self.s: + self.s.write(bytes(data)) diff --git a/src-00_core/src/interface/core.pxd b/src-00_core/src/interface/core.pxd deleted file mode 100644 index cc6e086..0000000 --- a/src-00_core/src/interface/core.pxd +++ /dev/null @@ -1,2 +0,0 @@ -cdef class Managed: - pass