From 8414392e6d57c9147d1ef33e109e5b03edb89e31 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 11 Aug 2025 19:24:39 +0200 Subject: [PATCH 01/43] Removed dependencies like numpy from canvas.py --- pyghthouse/data/canvas.py | 79 ++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index ee25d7a..013ec5d 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -1,26 +1,71 @@ -from typing import Union, Iterable -import numpy as np - - +"""TODO: Test methods: __init__ + set_image + _check_size + _check_values + get_image_bytes +""" class PyghthouseCanvas: - VALID_IMAGE_TYPE = Union[np.ndarray, Iterable, int] - IMAGE_SHAPE = (14, 28, 3) - def __init__(self, initial_image=None): - self.image = np.zeros(self.IMAGE_SHAPE, dtype=np.ubyte) - if initial_image is None: - self.image[:, :, 0] = 255 - else: + + self.size = (14,28,3) + self.image = [[[0] * self.size[2]] * self.size[1]] * self.size[1] + + if initial_image != None: self.set_image(initial_image) - def set_image(self, new_image: VALID_IMAGE_TYPE) -> np.array: - try: - self.image[:] = np.asarray(new_image).reshape(self.IMAGE_SHAPE) - except ValueError as e: - raise ValueError(f"{e}. Most likely, your image does not have the correct dimensions.") from None + + def set_image(self, new_image: list) -> True: + + self._check_size(new_image) + self._check_values(new_image) + + for y in range(len(self.image)): + for x in range(len(self.image[0])): + for rgb in range(self.image[0][0]): + + self.image[y][x][rgb] = new_image[y][x][rgb] return self.image + + def _check_size(self, other: list): + #TODO: Catch objects like [[[0,1,2],[0,1,2,3,4,5]]] + # Catch objects like [0, [], [[1,2,3]]] + try: + other_size = (len(other), len(other[0]), len(other[0][0])) + except (IndexError, TypeError): + raise TypeError(f"TypeError: Received image with missing dimensions. Are you sure this object is an image?") + + # Raise ValueError on wrong dimension size + if self.size == other_size: + return True + else: + raise ValueError(f"ValueError: The image does not have the correct dimensions. Dimensions should be {self.size} as (y,x,rgb), but received {other_size} ") + + + def _check_values(self, other: list): + + for y in range(self.size[0]): + for x in range(self.size[1]): + for rgb in range(self.size[2]): + + value = other[y][x][rgb] + + #TODO: Decide on either throw error on wrong type or do a typecast with a warning + if not isinstance(value, int): + raise TypeError(f"TypeError: Wrong type at ({y},{x},{rgb}). Type should be int but received {type(value)}") + + #TODO: Decide on either throw error on wrong value or change to closest value in bounds with a warning + if int(value) < 0 or int(value) > 255: + raise ValueError(f"ValueError: The value {value} at ({y},{x},{rgb}) is out of bounds. Value should be a number between 0 <= value <= 255") + + def get_image_bytes(self): - return self.image.tobytes() + + bytes_image = b'' + for y in range(self.size[0]): + for x in range(self.size[1]): + bytes_image += bytes(self.image[y][x]) + + return bytes_image From 13dc34b750e05a138203702da320f0d5aaf76cb9 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Tue, 12 Aug 2025 14:22:06 +0200 Subject: [PATCH 02/43] Removed numpy dependency from ph.py --- pyghthouse/ph.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 62f92ea..12ceaf9 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -3,8 +3,6 @@ from threading import Thread, Event, Lock from signal import signal, SIGINT -import numpy as np - from pyghthouse.data.canvas import PyghthouseCanvas from pyghthouse.connection.wsconnector import WSConnector @@ -235,13 +233,9 @@ def get_image_raw(self): with self.connector.lock: return self.canvas.image - @staticmethod - def empty_image_raw(): - return np.zeros((14, 28, 3)) - @staticmethod def empty_image(): - return Pyghthouse.empty_image_raw().tolist() + return [[[0] * 14] * 28] * 3 def set_image_callback(self, image_callback): with self.config_lock: From 96b593776ddbfa1d81e21d0528d2d0872edd2e5e Mon Sep 17 00:00:00 2001 From: Gedusim Date: Tue, 12 Aug 2025 14:23:35 +0200 Subject: [PATCH 03/43] Added and adjusted error handeling --- pyghthouse/data/canvas.py | 54 +++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index 013ec5d..d5504ae 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -1,6 +1,7 @@ """TODO: Test methods: __init__ set_image _check_size + _check_cells _check_values get_image_bytes """ @@ -18,6 +19,7 @@ def __init__(self, initial_image=None): def set_image(self, new_image: list) -> True: self._check_size(new_image) + self._check_cells(new_image) self._check_values(new_image) for y in range(len(self.image)): @@ -30,20 +32,44 @@ def set_image(self, new_image: list) -> True: def _check_size(self, other: list): - #TODO: Catch objects like [[[0,1,2],[0,1,2,3,4,5]]] - # Catch objects like [0, [], [[1,2,3]]] + try: other_size = (len(other), len(other[0]), len(other[0][0])) + + # Catch objects like [] or 0 except (IndexError, TypeError): - raise TypeError(f"TypeError: Received image with missing dimensions. Are you sure this object is an image?") + raise TypeError(f"TypeError: Received object with missing dimensions. Require a 3-dimensional list ") # Raise ValueError on wrong dimension size - if self.size == other_size: - return True - else: - raise ValueError(f"ValueError: The image does not have the correct dimensions. Dimensions should be {self.size} as (y,x,rgb), but received {other_size} ") + if self.size != other_size: + raise ValueError(f"ValueError: The image does not have the correct dimensions. Dimensions should be {self.size} as (y, x, rgb), but received {other_size}") + + def _check_cells(self, other: list): + + # Catch objects like [[[0,1,2],[0,1,2,3,4,5],[1]],1] + # TODO: Decide on either throw error on too large lists or do a warning + for y in range(self.size[0]): + try: + + if len(other[y]) != self.size[1]: + raise IndexError(f"ValueError: RGB list size shoule be {self.size[1]} but received {len(other[y])} at [{y}]") + + except (TypeError): + raise TypeError(f"TypeError: Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") + + for x in range(self.size[1]): + try: + + if len(other[y][x]) != self.size[2]: + raise IndexError(f"ValueError: RGB list size shoule be {self.size[2]} but received {len(other[y][x])} at [{y}][{x}]") + + except (TypeError): + raise TypeError(f"TypeError: Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") + + + def _check_values(self, other: list): for y in range(self.size[0]): @@ -53,19 +79,21 @@ def _check_values(self, other: list): value = other[y][x][rgb] #TODO: Decide on either throw error on wrong type or do a typecast with a warning - if not isinstance(value, int): - raise TypeError(f"TypeError: Wrong type at ({y},{x},{rgb}). Type should be int but received {type(value)}") + if not isinstance(value, int) and not isinstance(value, bytes): + raise TypeError(f"TypeError: Wrong type at ({y},{x},{rgb}). Type should be 'int' or 'bytes' but received '{type(value).__name__}'") #TODO: Decide on either throw error on wrong value or change to closest value in bounds with a warning if int(value) < 0 or int(value) > 255: - raise ValueError(f"ValueError: The value {value} at ({y},{x},{rgb}) is out of bounds. Value should be a number between 0 <= value <= 255") + raise ValueError(f"ValueError: Received value {value} at ({y},{x},{rgb}) is out of bounds. Value should be a number between 0 <= value <= 255") def get_image_bytes(self): - bytes_image = b'' + image_bytes = b'' + for y in range(self.size[0]): for x in range(self.size[1]): - bytes_image += bytes(self.image[y][x]) + + image_bytes += bytes(self.image[y][x]) - return bytes_image + return image_bytes From 8a9f7699c95ea413aa96b72050149b6531c2a6cc Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 14 Aug 2025 12:03:29 +0200 Subject: [PATCH 04/43] Removed bytes as accepted type due to errors in range check --- pyghthouse/data/canvas.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index d5504ae..71c97d3 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -79,12 +79,13 @@ def _check_values(self, other: list): value = other[y][x][rgb] #TODO: Decide on either throw error on wrong type or do a typecast with a warning - if not isinstance(value, int) and not isinstance(value, bytes): - raise TypeError(f"TypeError: Wrong type at ({y},{x},{rgb}). Type should be 'int' or 'bytes' but received '{type(value).__name__}'") + #TODO: Integrate other numerical types? + if not isinstance(value, int): + raise TypeError(f"TypeError: Wrong type at ({y},{x},{rgb}). Type should be 'int' but received '{type(value).__name__}'") - #TODO: Decide on either throw error on wrong value or change to closest value in bounds with a warning + #TODO: Decide on either throw error when out of range or change to closest value in range with a warning if int(value) < 0 or int(value) > 255: - raise ValueError(f"ValueError: Received value {value} at ({y},{x},{rgb}) is out of bounds. Value should be a number between 0 <= value <= 255") + raise ValueError(f"ValueError: Received value {value} at ({y},{x},{rgb}) is out of range. Value should be a number between 0 <= value <= 255") def get_image_bytes(self): From 76720853d3cb0186c9ee40c275bc0f4d10a6e684 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 14 Aug 2025 12:13:08 +0200 Subject: [PATCH 05/43] Fix double output of error type --- pyghthouse/data/canvas.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index 71c97d3..27886c3 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -38,11 +38,11 @@ def _check_size(self, other: list): # Catch objects like [] or 0 except (IndexError, TypeError): - raise TypeError(f"TypeError: Received object with missing dimensions. Require a 3-dimensional list ") + raise TypeError(f"Received object with missing dimensions. Require a 3-dimensional list ") # Raise ValueError on wrong dimension size if self.size != other_size: - raise ValueError(f"ValueError: The image does not have the correct dimensions. Dimensions should be {self.size} as (y, x, rgb), but received {other_size}") + raise ValueError(f"The image does not have the correct dimensions. Dimensions should be {self.size} as (y, x, rgb), but received {other_size}") @@ -54,19 +54,19 @@ def _check_cells(self, other: list): try: if len(other[y]) != self.size[1]: - raise IndexError(f"ValueError: RGB list size shoule be {self.size[1]} but received {len(other[y])} at [{y}]") + raise IndexError(f"RGB list size shoule be {self.size[1]} but received {len(other[y])} at [{y}]") except (TypeError): - raise TypeError(f"TypeError: Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") + raise TypeError(f"Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") for x in range(self.size[1]): try: if len(other[y][x]) != self.size[2]: - raise IndexError(f"ValueError: RGB list size shoule be {self.size[2]} but received {len(other[y][x])} at [{y}][{x}]") + raise IndexError(f"RGB list size shoule be {self.size[2]} but received {len(other[y][x])} at [{y}][{x}]") except (TypeError): - raise TypeError(f"TypeError: Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") + raise TypeError(f"Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") @@ -81,11 +81,11 @@ def _check_values(self, other: list): #TODO: Decide on either throw error on wrong type or do a typecast with a warning #TODO: Integrate other numerical types? if not isinstance(value, int): - raise TypeError(f"TypeError: Wrong type at ({y},{x},{rgb}). Type should be 'int' but received '{type(value).__name__}'") + raise TypeError(f"Wrong type at ({y},{x},{rgb}). Type should be 'int' but received '{type(value).__name__}'") #TODO: Decide on either throw error when out of range or change to closest value in range with a warning if int(value) < 0 or int(value) > 255: - raise ValueError(f"ValueError: Received value {value} at ({y},{x},{rgb}) is out of range. Value should be a number between 0 <= value <= 255") + raise ValueError(f"Received value {value} at ({y},{x},{rgb}) is out of range. Value should be a number between 0 <= value <= 255") def get_image_bytes(self): From 27d979d2540a73f1ae6287119b6d7898e7b57fd9 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 14 Aug 2025 12:35:10 +0200 Subject: [PATCH 06/43] Fix multiple lists with same reference and removed legacy numpy code --- pyghthouse/data/canvas.py | 8 ++++---- pyghthouse/ph.py | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index 27886c3..ac54a41 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -10,7 +10,7 @@ class PyghthouseCanvas: def __init__(self, initial_image=None): self.size = (14,28,3) - self.image = [[[0] * self.size[2]] * self.size[1]] * self.size[1] + self.image = [[[0 for k in range(self.size[2])] for j in range(self.size[1])] for i in range(self.size[0])] if initial_image != None: self.set_image(initial_image) @@ -22,9 +22,9 @@ def set_image(self, new_image: list) -> True: self._check_cells(new_image) self._check_values(new_image) - for y in range(len(self.image)): - for x in range(len(self.image[0])): - for rgb in range(self.image[0][0]): + for y in range(self.size[0]): + for x in range(self.size[1]): + for rgb in range(self.size[2]): self.image[y][x][rgb] = new_image[y][x][rgb] diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 12ceaf9..63548ae 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -227,15 +227,12 @@ def set_image(self, image): self.canvas.set_image(image) def get_image(self): - return self.get_image_raw().tolist() - - def get_image_raw(self): with self.connector.lock: return self.canvas.image @staticmethod def empty_image(): - return [[[0] * 14] * 28] * 3 + return [[[0 for k in range(3)] for j in range(28)] for i in range(14)] def set_image_callback(self, image_callback): with self.config_lock: From 99d0942cb79159ff464f7d15a7a6c65a7500822b Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 14 Aug 2025 12:46:14 +0200 Subject: [PATCH 07/43] Fix typo --- pyghthouse/data/canvas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index ac54a41..3b66c6c 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -54,7 +54,7 @@ def _check_cells(self, other: list): try: if len(other[y]) != self.size[1]: - raise IndexError(f"RGB list size shoule be {self.size[1]} but received {len(other[y])} at [{y}]") + raise IndexError(f"x list size should be {self.size[1]} but received {len(other[y])} at [{y}]") except (TypeError): raise TypeError(f"Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") @@ -63,7 +63,7 @@ def _check_cells(self, other: list): try: if len(other[y][x]) != self.size[2]: - raise IndexError(f"RGB list size shoule be {self.size[2]} but received {len(other[y][x])} at [{y}][{x}]") + raise IndexError(f"RGB list size should be {self.size[2]} but received {len(other[y][x])} at [{y}][{x}]") except (TypeError): raise TypeError(f"Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") From 490fc342e307ba9452d5fb1551e16aec05f5edad Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 14 Aug 2025 13:11:50 +0200 Subject: [PATCH 08/43] Finished testing and fixed bugs in canvas init method --- pyghthouse/data/canvas.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index 3b66c6c..77e1924 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -1,5 +1,4 @@ -"""TODO: Test methods: __init__ - set_image +"""TODO: Test methods: set_image _check_size _check_cells _check_values @@ -12,7 +11,7 @@ def __init__(self, initial_image=None): self.size = (14,28,3) self.image = [[[0 for k in range(self.size[2])] for j in range(self.size[1])] for i in range(self.size[0])] - if initial_image != None: + if initial_image is not None: self.set_image(initial_image) @@ -78,8 +77,7 @@ def _check_values(self, other: list): value = other[y][x][rgb] - #TODO: Decide on either throw error on wrong type or do a typecast with a warning - #TODO: Integrate other numerical types? + #TODO: Decide on either throw error on wrong type or do a typecast with a warning. Which types should be allowed? All numerical or only int? if not isinstance(value, int): raise TypeError(f"Wrong type at ({y},{x},{rgb}). Type should be 'int' but received '{type(value).__name__}'") From c55d75ddb52646de1f7e431cf01aeacc02547bc1 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 14 Aug 2025 14:20:06 +0200 Subject: [PATCH 09/43] Finished testing 'canvas.py'. TODOs remaining --- pyghthouse/data/canvas.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index 77e1924..34d1f4f 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -1,9 +1,3 @@ -"""TODO: Test methods: set_image - _check_size - _check_cells - _check_values - get_image_bytes -""" class PyghthouseCanvas: def __init__(self, initial_image=None): @@ -25,7 +19,7 @@ def set_image(self, new_image: list) -> True: for x in range(self.size[1]): for rgb in range(self.size[2]): - self.image[y][x][rgb] = new_image[y][x][rgb] + self.image[y][x][rgb] = int(new_image[y][x][rgb]) return self.image @@ -49,11 +43,12 @@ def _check_cells(self, other: list): # Catch objects like [[[0,1,2],[0,1,2,3,4,5],[1]],1] # TODO: Decide on either throw error on too large lists or do a warning + # TODO: Decide on either try-except block or TypeCheck for y in range(self.size[0]): try: if len(other[y]) != self.size[1]: - raise IndexError(f"x list size should be {self.size[1]} but received {len(other[y])} at [{y}]") + raise IndexError(f"x-list size should be {self.size[1]} but received {len(other[y])} at position [{y}]") except (TypeError): raise TypeError(f"Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") @@ -62,7 +57,7 @@ def _check_cells(self, other: list): try: if len(other[y][x]) != self.size[2]: - raise IndexError(f"RGB list size should be {self.size[2]} but received {len(other[y][x])} at [{y}][{x}]") + raise IndexError(f"RGB-list size should be {self.size[2]} but received {len(other[y][x])} at position [{y}][{x}]") except (TypeError): raise TypeError(f"Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") @@ -77,11 +72,12 @@ def _check_values(self, other: list): value = other[y][x][rgb] - #TODO: Decide on either throw error on wrong type or do a typecast with a warning. Which types should be allowed? All numerical or only int? + # TODO: Decide on either throw error on wrong type or do a typecast with a warning. + # Which types should be allowed? All numerical or only int? if not isinstance(value, int): raise TypeError(f"Wrong type at ({y},{x},{rgb}). Type should be 'int' but received '{type(value).__name__}'") - #TODO: Decide on either throw error when out of range or change to closest value in range with a warning + # TODO: Decide on either throw error when out of range or change to closest value in range with a warning if int(value) < 0 or int(value) > 255: raise ValueError(f"Received value {value} at ({y},{x},{rgb}) is out of range. Value should be a number between 0 <= value <= 255") From bc6a792f8e924ccf0e80e6cf6d6d267c2264662b Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 15 Sep 2025 11:27:04 +0200 Subject: [PATCH 10/43] Improved structure for better readability --- pyghthouse/data/canvas.py | 59 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index 34d1f4f..f8b84f2 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -1,9 +1,10 @@ class PyghthouseCanvas: - + + IMAGE_SHAPE = (14, 28, 3) + def __init__(self, initial_image=None): - self.size = (14,28,3) - self.image = [[[0 for k in range(self.size[2])] for j in range(self.size[1])] for i in range(self.size[0])] + self.image = [[[0 for k in range(self.IMAGE_SHAPE[2])] for j in range(self.IMAGE_SHAPE[1])] for i in range(self.IMAGE_SHAPE[0])] if initial_image is not None: self.set_image(initial_image) @@ -15,13 +16,25 @@ def set_image(self, new_image: list) -> True: self._check_cells(new_image) self._check_values(new_image) - for y in range(self.size[0]): - for x in range(self.size[1]): - for rgb in range(self.size[2]): + for y in range(self.IMAGE_SHAPE[0]): + for x in range(self.IMAGE_SHAPE[1]): + for rgb in range(self.IMAGE_SHAPE[2]): self.image[y][x][rgb] = int(new_image[y][x][rgb]) return self.image + + + def get_image_bytes(self): + + image_bytes = b'' + + for y in range(self.IMAGE_SHAPE[0]): + for x in range(self.IMAGE_SHAPE[1]): + + image_bytes += bytes(self.image[y][x]) + + return image_bytes def _check_size(self, other: list): @@ -34,8 +47,8 @@ def _check_size(self, other: list): raise TypeError(f"Received object with missing dimensions. Require a 3-dimensional list ") # Raise ValueError on wrong dimension size - if self.size != other_size: - raise ValueError(f"The image does not have the correct dimensions. Dimensions should be {self.size} as (y, x, rgb), but received {other_size}") + if self.IMAGE_SHAPE != other_size: + raise ValueError(f"The image does not have the correct dimensions. Dimensions should be {self.IMAGE_SHAPE} as (y, x, rgb), but received {other_size}") @@ -44,20 +57,20 @@ def _check_cells(self, other: list): # Catch objects like [[[0,1,2],[0,1,2,3,4,5],[1]],1] # TODO: Decide on either throw error on too large lists or do a warning # TODO: Decide on either try-except block or TypeCheck - for y in range(self.size[0]): + for y in range(self.IMAGE_SHAPE[0]): try: - if len(other[y]) != self.size[1]: - raise IndexError(f"x-list size should be {self.size[1]} but received {len(other[y])} at position [{y}]") + if len(other[y]) != self.IMAGE_SHAPE[1]: + raise IndexError(f"x-list size should be {self.IMAGE_SHAPE[1]} but received {len(other[y])} at position [{y}]") except (TypeError): raise TypeError(f"Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") - for x in range(self.size[1]): + for x in range(self.IMAGE_SHAPE[1]): try: - if len(other[y][x]) != self.size[2]: - raise IndexError(f"RGB-list size should be {self.size[2]} but received {len(other[y][x])} at position [{y}][{x}]") + if len(other[y][x]) != self.IMAGE_SHAPE[2]: + raise IndexError(f"RGB-list size should be {self.IMAGE_SHAPE[2]} but received {len(other[y][x])} at position [{y}][{x}]") except (TypeError): raise TypeError(f"Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") @@ -66,9 +79,9 @@ def _check_cells(self, other: list): def _check_values(self, other: list): - for y in range(self.size[0]): - for x in range(self.size[1]): - for rgb in range(self.size[2]): + for y in range(self.IMAGE_SHAPE[0]): + for x in range(self.IMAGE_SHAPE[1]): + for rgb in range(self.IMAGE_SHAPE[2]): value = other[y][x][rgb] @@ -80,15 +93,3 @@ def _check_values(self, other: list): # TODO: Decide on either throw error when out of range or change to closest value in range with a warning if int(value) < 0 or int(value) > 255: raise ValueError(f"Received value {value} at ({y},{x},{rgb}) is out of range. Value should be a number between 0 <= value <= 255") - - - def get_image_bytes(self): - - image_bytes = b'' - - for y in range(self.size[0]): - for x in range(self.size[1]): - - image_bytes += bytes(self.image[y][x]) - - return image_bytes From affe8c3855fd84b2efedc6a785e5b5bcbedea68c Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 15 Sep 2025 11:33:42 +0200 Subject: [PATCH 11/43] Made canvas.py methods thread safe --- pyghthouse/data/canvas.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index f8b84f2..fd2e2cd 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -1,9 +1,12 @@ +from threading import Lock + class PyghthouseCanvas: IMAGE_SHAPE = (14, 28, 3) def __init__(self, initial_image=None): + self.lock = Lock() self.image = [[[0 for k in range(self.IMAGE_SHAPE[2])] for j in range(self.IMAGE_SHAPE[1])] for i in range(self.IMAGE_SHAPE[0])] if initial_image is not None: @@ -16,11 +19,13 @@ def set_image(self, new_image: list) -> True: self._check_cells(new_image) self._check_values(new_image) - for y in range(self.IMAGE_SHAPE[0]): - for x in range(self.IMAGE_SHAPE[1]): - for rgb in range(self.IMAGE_SHAPE[2]): - - self.image[y][x][rgb] = int(new_image[y][x][rgb]) + with self.lock: + + for y in range(self.IMAGE_SHAPE[0]): + for x in range(self.IMAGE_SHAPE[1]): + for rgb in range(self.IMAGE_SHAPE[2]): + + self.image[y][x][rgb] = int(new_image[y][x][rgb]) return self.image @@ -29,10 +34,12 @@ def get_image_bytes(self): image_bytes = b'' - for y in range(self.IMAGE_SHAPE[0]): - for x in range(self.IMAGE_SHAPE[1]): - - image_bytes += bytes(self.image[y][x]) + with self.lock: + + for y in range(self.IMAGE_SHAPE[0]): + for x in range(self.IMAGE_SHAPE[1]): + + image_bytes += bytes(self.image[y][x]) return image_bytes From 157894660cc3f528fb5d223b754ffd7d23a3f23f Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 15 Sep 2025 12:03:22 +0200 Subject: [PATCH 12/43] Moved PHThread to a new file --- pyghthouse/controller/PHThread.py | 25 +++++++++++++++++++++++++ pyghthouse/controller/__init__.py | 0 pyghthouse/ph.py | 22 ---------------------- 3 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 pyghthouse/controller/PHThread.py create mode 100644 pyghthouse/controller/__init__.py diff --git a/pyghthouse/controller/PHThread.py b/pyghthouse/controller/PHThread.py new file mode 100644 index 0000000..0032d27 --- /dev/null +++ b/pyghthouse/controller/PHThread.py @@ -0,0 +1,25 @@ +from threading import Thread, Event +from time import sleep, time + +class PHThread(Thread): + + def __init__(self, parent): + super().__init__() + self.parent = parent + self._stop_event = Event() + + def stop(self): + self._stop_event.set() + + def stopped(self): + return self._stop_event.is_set() + + def run(self): + while not self.stopped(): + with self.parent.config_lock: + sleep_time = self.parent.send_interval - (time() % self.parent.send_interval) + sleep(sleep_time) + if self.parent.image_callback is not None: + image_from_callback = self.parent.image_callback() + self.parent.set_image(image_from_callback) + self.parent.connector.send(self.parent.canvas.get_image_bytes()) \ No newline at end of file diff --git a/pyghthouse/controller/__init__.py b/pyghthouse/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 63548ae..c8f893b 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -161,28 +161,6 @@ def handle(self, msg): def print_warning(msg): print(f"Warning: {msg['RNUM']} {msg['RESPONSE']} {', '.join(msg['WARNINGS'])}") - class PHThread(Thread): - - def __init__(self, parent): - super().__init__() - self.parent = parent - self._stop_event = Event() - - def stop(self): - self._stop_event.set() - - def stopped(self): - return self._stop_event.is_set() - - def run(self): - while not self.stopped(): - with self.parent.config_lock: - sleep_time = self.parent.send_interval - (time() % self.parent.send_interval) - sleep(sleep_time) - if self.parent.image_callback is not None: - image_from_callback = self.parent.image_callback() - self.parent.set_image(image_from_callback) - self.parent.connector.send(self.parent.canvas.get_image_bytes()) def __init__(self, username: str, token: str, address: str = "wss://lighthouse.uni-kiel.de/websocket", frame_rate: float = 30.0, image_callback=None, verbosity=VerbosityLevel.WARN_ONCE, From bb6f21c5a3e57413aa987eed1716e6facff90dd9 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 15 Sep 2025 12:59:32 +0200 Subject: [PATCH 13/43] Reduced default timeout to 10 seconds --- pyghthouse/connection/wsconnector.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index d451f0a..fa2325f 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -18,7 +18,7 @@ def __next__(self): def __iter__(self): return self - def __init__(self, username: str, token: str, address: str, on_msg=None, ignore_ssl_cert=False): + def __init__(self, username: str, token: str, address: str, on_msg=None, ignore_ssl_cert=False, timeout=10): self.username = username self.token = token self.address = address @@ -28,11 +28,14 @@ def __init__(self, username: str, token: str, address: str, on_msg=None, ignore_ self.reid = self.REID() self.running = False self.ignore_ssl_cert = ignore_ssl_cert - setdefaulttimeout(60) + self.timeout = 10 + if timeout > 0: + self.timeout = timeout + setdefaulttimeout(self.timeout) def send(self, data): with self.lock: - self.ws.send(packb(self.construct_package(data), use_bin_type=True), opcode=ABNF.OPCODE_BINARY) + self.ws.send_bytes(self.construct_package(data)) def start(self): self.stop() @@ -68,7 +71,7 @@ def _handle_msg(self, ws, msg): self.on_msg(msg) def construct_package(self, payload_data): - return { + data = { 'REID': next(self.reid), 'AUTH': {'USER': self.username, 'TOKEN': self.token}, 'VERB': 'PUT', @@ -76,3 +79,10 @@ def construct_package(self, payload_data): 'META': {}, 'PAYL': payload_data } + return packb(self.construct_package(data), use_bin_type=True) + + def set_timeout(self, timeout=10): + self.timeout = 10 + if timeout > 0: + self.timeout = timeout + setdefaulttimeout(self.timeout) From 820f80220a2c1a0b867727c4f12ff1fa6b2c8b2d Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 15 Sep 2025 13:11:30 +0200 Subject: [PATCH 14/43] Moved PHMessageHandler to wsconnector.py --- pyghthouse/connection/wsconnector.py | 29 +++++++++++++++++++++++++-- pyghthouse/controller/PHThread.py | 19 ++++++++++++------ pyghthouse/ph.py | 30 +++++----------------------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index fa2325f..38ffcbf 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -3,9 +3,33 @@ from msgpack import packb, unpackb from ssl import CERT_NONE +from ..ph import VerbosityLevel class WSConnector: + class PHMessageHandler: + + def __init__(self, verbosity=VerbosityLevel.WARN_ONCE): + self.verbosity = verbosity + self.warned_already = False + + def reset(self): + self.warned_already = False + + def handle(self, msg): + if msg['RNUM'] == 200: + if self.verbosity == VerbosityLevel.ALL: + print(msg) + elif self.verbosity == VerbosityLevel.WARN: + self.print_warning(msg) + elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: + self.print_warning(msg) + self.warned_already = True + + @staticmethod + def print_warning(msg): + print(f"Warning: {msg['RNUM']} {msg['RESPONSE']} {', '.join(msg['WARNINGS'])}") + class REID: def __init__(self): self._next = 0 @@ -18,11 +42,12 @@ def __next__(self): def __iter__(self): return self - def __init__(self, username: str, token: str, address: str, on_msg=None, ignore_ssl_cert=False, timeout=10): + def __init__(self, username: str, token: str, address: str, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert=False, timeout=10): self.username = username self.token = token self.address = address - self.on_msg = on_msg + self.message_handler = self.PHMessageHandler(verbosity) + self.on_msg = self.PHMessageHandler.handle() self.ws = None self.lock = Lock() self.reid = self.REID() diff --git a/pyghthouse/controller/PHThread.py b/pyghthouse/controller/PHThread.py index 0032d27..8d35b86 100644 --- a/pyghthouse/controller/PHThread.py +++ b/pyghthouse/controller/PHThread.py @@ -1,11 +1,18 @@ from threading import Thread, Event from time import sleep, time +from pyghthouse.data.canvas import PyghthouseCanvas +from pyghthouse.connection.wsconnector import WSConnector +from ..ph import VerbosityLevel + class PHThread(Thread): - def __init__(self, parent): + def __init__(self, image_callback, canvas:PyghthouseCanvas, username, token, address, ignore_ssl_cert=False): super().__init__() - self.parent = parent + self.callback = image_callback + self.canvas = canvas + self.connector = WSConnector(username, token, address, verbosity=VerbosityLevel.WARN_ONCE, + ignore_ssl_cert=ignore_ssl_cert) self._stop_event = Event() def stop(self): @@ -19,7 +26,7 @@ def run(self): with self.parent.config_lock: sleep_time = self.parent.send_interval - (time() % self.parent.send_interval) sleep(sleep_time) - if self.parent.image_callback is not None: - image_from_callback = self.parent.image_callback() - self.parent.set_image(image_from_callback) - self.parent.connector.send(self.parent.canvas.get_image_bytes()) \ No newline at end of file + if self.callback is not None: + image_from_callback = self.callback() + self.canvas.set_image(image_from_callback) + self.connector.send(self.parent.canvas.get_image_bytes()) \ No newline at end of file diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index c8f893b..960a578 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -1,3 +1,4 @@ +# TODO: Remove unused imports from enum import Enum from time import time, sleep from threading import Thread, Event, Lock @@ -5,6 +6,7 @@ from pyghthouse.data.canvas import PyghthouseCanvas from pyghthouse.connection.wsconnector import WSConnector +from pyghthouse.controller.PHThread import PHThread class VerbosityLevel(Enum): @@ -138,30 +140,6 @@ class Pyghthouse: (https://github.com/ProjectLighthouseCAU/pyghthouse/tree/master/examples). """ - class PHMessageHandler: - - def __init__(self, verbosity=VerbosityLevel.WARN_ONCE): - self.verbosity = verbosity - self.warned_already = False - - def reset(self): - self.warned_already = False - - def handle(self, msg): - if msg['RNUM'] == 200: - if self.verbosity == VerbosityLevel.ALL: - print(msg) - elif self.verbosity == VerbosityLevel.WARN: - self.print_warning(msg) - elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: - self.print_warning(msg) - self.warned_already = True - - @staticmethod - def print_warning(msg): - print(f"Warning: {msg['RNUM']} {msg['RESPONSE']} {', '.join(msg['WARNINGS'])}") - - def __init__(self, username: str, token: str, address: str = "wss://lighthouse.uni-kiel.de/websocket", frame_rate: float = 30.0, image_callback=None, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert=False): @@ -183,12 +161,13 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u def connect(self): self.connector.start() + # TODO: Block main-thread from continue running until all threads finished starting def start(self): if not self.connector.running: self.connect() self.stop() self.msg_handler.reset() - self.ph_thread = self.PHThread(self) + self.ph_thread = PHThread(self) self.ph_thread.start() def stop(self): @@ -216,6 +195,7 @@ def set_image_callback(self, image_callback): with self.config_lock: self.image_callback = image_callback + # TODO: Remove method or apply frame_rate check def set_frame_rate(self, frame_rate): with self.config_lock: self.send_interval = 1.0 / frame_rate From 151858adfb1c47b8a66974e60410fb24108de58c Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 17 Sep 2025 12:49:17 +0200 Subject: [PATCH 15/43] Refactor PHThread and add check for main_thread --- pyghthouse/controller/PHThread.py | 64 +++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/pyghthouse/controller/PHThread.py b/pyghthouse/controller/PHThread.py index 8d35b86..408ed65 100644 --- a/pyghthouse/controller/PHThread.py +++ b/pyghthouse/controller/PHThread.py @@ -1,4 +1,4 @@ -from threading import Thread, Event +from threading import Thread, Event, main_thread from time import sleep, time from pyghthouse.data.canvas import PyghthouseCanvas @@ -7,26 +7,68 @@ class PHThread(Thread): - def __init__(self, image_callback, canvas:PyghthouseCanvas, username, token, address, ignore_ssl_cert=False): + def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, username:str, token:str, address:str, verbosity:VerbosityLevel, ignore_ssl_cert:bool): super().__init__() + self.send_interval = send_interval self.callback = image_callback self.canvas = canvas - self.connector = WSConnector(username, token, address, verbosity=VerbosityLevel.WARN_ONCE, - ignore_ssl_cert=ignore_ssl_cert) + self.connector = WSConnector(username, token, address, + verbosity, ignore_ssl_cert) self._stop_event = Event() + self.main_thread = main_thread() + + self.ignore_main = False + if self.callback is not None: + self.ignore_main = True + def stop(self): self._stop_event.set() + def stopped(self): + + if not self.ignore_main and not self.main_thread.is_alive(): + return True + return self._stop_event.is_set() + def run(self): + self.connect() + while not self.stopped(): - with self.parent.config_lock: - sleep_time = self.parent.send_interval - (time() % self.parent.send_interval) - sleep(sleep_time) - if self.callback is not None: - image_from_callback = self.callback() - self.canvas.set_image(image_from_callback) - self.connector.send(self.parent.canvas.get_image_bytes()) \ No newline at end of file + sleep_time = self.send_interval - (time() % self.send_interval) + sleep(sleep_time) + self.send_image() + + self.disconnect() + + + def send_image(self): + + if self.callback is not None: + self.set_callback_image() + + bytes_image = self.canvas.get_bytes_image() + self.connector.send(bytes_image) + + + def set_callback_image(self): + + image_from_callback = self.callback() + try: + self.canvas.set_image(image_from_callback) + except: + self.disconnect() + raise + + + # TODO: Start websocket connection and wait until websocket is open + def connect(self): + pass + + + # TODO: Signal websocket to close connection + def disconnect(self): + pass \ No newline at end of file From f0e7126c950ae104bb0e5f9301fae82de305fa4c Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 17 Sep 2025 14:06:47 +0200 Subject: [PATCH 16/43] Refactor ph.py, moved VerbosityLevel to wsconnector.py --- pyghthouse/connection/wsconnector.py | 7 +- pyghthouse/controller/PHThread.py | 7 +- pyghthouse/ph.py | 106 ++++++++++++++------------- 3 files changed, 67 insertions(+), 53 deletions(-) diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index 38ffcbf..69a2ef4 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -2,8 +2,13 @@ from websocket import WebSocketApp, setdefaulttimeout, ABNF from msgpack import packb, unpackb from ssl import CERT_NONE +from enum import Enum -from ..ph import VerbosityLevel +class VerbosityLevel(Enum): + NONE = 0 + WARN_ONCE = 1 + WARN = 2 + ALL = 3 class WSConnector: diff --git a/pyghthouse/controller/PHThread.py b/pyghthouse/controller/PHThread.py index 408ed65..cb3d837 100644 --- a/pyghthouse/controller/PHThread.py +++ b/pyghthouse/controller/PHThread.py @@ -2,8 +2,7 @@ from time import sleep, time from pyghthouse.data.canvas import PyghthouseCanvas -from pyghthouse.connection.wsconnector import WSConnector -from ..ph import VerbosityLevel +from pyghthouse.connection.wsconnector import WSConnector, VerbosityLevel class PHThread(Thread): @@ -12,9 +11,10 @@ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, usern self.send_interval = send_interval self.callback = image_callback self.canvas = canvas + self.ready = Event() + self._stop_event = Event() self.connector = WSConnector(username, token, address, verbosity, ignore_ssl_cert) - self._stop_event = Event() self.main_thread = main_thread() self.ignore_main = False @@ -37,6 +37,7 @@ def stopped(self): def run(self): self.connect() + self.ready.set() while not self.stopped(): sleep_time = self.send_interval - (time() % self.send_interval) sleep(sleep_time) diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 960a578..1de0d3a 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -5,17 +5,10 @@ from signal import signal, SIGINT from pyghthouse.data.canvas import PyghthouseCanvas -from pyghthouse.connection.wsconnector import WSConnector from pyghthouse.controller.PHThread import PHThread +from pyghthouse.connection.wsconnector import VerbosityLevel - -class VerbosityLevel(Enum): - NONE = 0 - WARN_ONCE = 1 - WARN = 2 - ALL = 3 - - +# TODO: Adjust description and example for more clarity class Pyghthouse: """ A Python Lighthouse adapter. @@ -143,63 +136,78 @@ class Pyghthouse: def __init__(self, username: str, token: str, address: str = "wss://lighthouse.uni-kiel.de/websocket", frame_rate: float = 30.0, image_callback=None, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert=False): + if frame_rate > 60.0 or frame_rate <= 0: - raise ValueError("Frame rate must be greater than 0 and at most 60.") - self.username = username - self.token = token - self.address = address - self.send_interval = 1.0 / frame_rate - self.image_callback = image_callback + raise ValueError("frame rate must be greater than 0 and at most 60.") + + send_interval = 1.0 / frame_rate self.canvas = PyghthouseCanvas() - self.msg_handler = self.PHMessageHandler(verbosity) - self.connector = WSConnector(username, token, address, on_msg=self.msg_handler.handle, - ignore_ssl_cert=ignore_ssl_cert) - self.config_lock = Lock() - self.ph_thread = None + self.ph_thread = PHThread(send_interval, image_callback, self.canvas, + username, token, address, verbosity, ignore_ssl_cert) + + self.ready = self.ph_thread.ready signal(SIGINT, self._handle_sigint) - def connect(self): - self.connector.start() - # TODO: Block main-thread from continue running until all threads finished starting def start(self): - if not self.connector.running: - self.connect() - self.stop() - self.msg_handler.reset() - self.ph_thread = PHThread(self) - self.ph_thread.start() - - def stop(self): - if self.ph_thread is not None: - self.ph_thread.stop() - self.ph_thread.join() - def close(self): - self.stop() - self.connector.stop() + if self.ready.is_set() == True: + + # TODO: Raise exception and stop or give a warning? + self.close() + raise RuntimeError("Pyghthouse can only be started once") + + else: + + self.ph_thread.start() + self.ready.wait() + def set_image(self, image): - with self.connector.lock: + + if not self.ready.is_set(): + raise RuntimeError("cannot set an image before Pyghthouse started") + + try: self.canvas.set_image(image) + + except: + self.close() + raise + + + def close(self): + + if self.ready.is_set(): + + self.ph_thread.stop() + self.ph_thread.join() + + else: + raise RuntimeError("cannot stop Pyghthouse before it started") - def get_image(self): - with self.connector.lock: - return self.canvas.image @staticmethod def empty_image(): return [[[0 for k in range(3)] for j in range(28)] for i in range(14)] - def set_image_callback(self, image_callback): - with self.config_lock: - self.image_callback = image_callback - - # TODO: Remove method or apply frame_rate check - def set_frame_rate(self, frame_rate): - with self.config_lock: - self.send_interval = 1.0 / frame_rate def _handle_sigint(self, sig, frame): self.close() raise SystemExit(0) + + + # TODO: Remove method or change return to thread safe copy + def get_image(self): + return self.canvas.image + + + # TODO: Remove method or update the thread + def set_image_callback(self, image_callback): + self.image_callback = image_callback + + + # TODO: Remove method or apply frame_rate check and update interval of the thread + def set_frame_rate(self, frame_rate): + self.send_interval = 1.0 / frame_rate + From dac48569c39e2df70b74a7cea8290a3671636809 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 17 Sep 2025 14:38:01 +0200 Subject: [PATCH 17/43] Added thread safe copy, added depricated warning to old methods --- pyghthouse/__init__.py | 3 ++- pyghthouse/data/canvas.py | 23 +++++++++++++++++++---- pyghthouse/ph.py | 38 ++++++++++++++++++++++++++------------ 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/pyghthouse/__init__.py b/pyghthouse/__init__.py index 23aa611..bbea480 100644 --- a/pyghthouse/__init__.py +++ b/pyghthouse/__init__.py @@ -1 +1,2 @@ -from pyghthouse.ph import Pyghthouse, VerbosityLevel +from pyghthouse.ph import Pyghthouse +from pyghthouse.connection.wsconnector import VerbosityLevel diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index fd2e2cd..0a75700 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -30,7 +30,7 @@ def set_image(self, new_image: list) -> True: return self.image - def get_image_bytes(self): + def get_bytes_image(self): image_bytes = b'' @@ -42,6 +42,21 @@ def get_image_bytes(self): image_bytes += bytes(self.image[y][x]) return image_bytes + + + def copy_image(self): + + image_copy = [[[0 for k in range(self.IMAGE_SHAPE[2])] for j in range(self.IMAGE_SHAPE[1])] for i in range(self.IMAGE_SHAPE[0])] + + with self.lock: + + for y in range(self.IMAGE_SHAPE[0]): + for x in range(self.IMAGE_SHAPE[1]): + for rgb in range(self.IMAGE_SHAPE[2]): + + image_copy[y][x][rgb] = self.image[y][x][rgb] + + return image_copy def _check_size(self, other: list): @@ -51,7 +66,7 @@ def _check_size(self, other: list): # Catch objects like [] or 0 except (IndexError, TypeError): - raise TypeError(f"Received object with missing dimensions. Require a 3-dimensional list ") + raise TypeError(f"Received object with missing dimensions. Require a 3-dimensional list ") from None # Raise ValueError on wrong dimension size if self.IMAGE_SHAPE != other_size: @@ -71,7 +86,7 @@ def _check_cells(self, other: list): raise IndexError(f"x-list size should be {self.IMAGE_SHAPE[1]} but received {len(other[y])} at position [{y}]") except (TypeError): - raise TypeError(f"Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") + raise TypeError(f"Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") from None for x in range(self.IMAGE_SHAPE[1]): try: @@ -80,7 +95,7 @@ def _check_cells(self, other: list): raise IndexError(f"RGB-list size should be {self.IMAGE_SHAPE[2]} but received {len(other[y][x])} at position [{y}][{x}]") except (TypeError): - raise TypeError(f"Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") + raise TypeError(f"Require type 'list' at [{y}][{x}], but received object of type '{type(other[y][x]).__name__}'") from None diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 1de0d3a..b3d714c 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -1,8 +1,5 @@ -# TODO: Remove unused imports -from enum import Enum -from time import time, sleep -from threading import Thread, Event, Lock from signal import signal, SIGINT +from warnings import depricated from pyghthouse.data.canvas import PyghthouseCanvas from pyghthouse.controller.PHThread import PHThread @@ -195,19 +192,36 @@ def empty_image(): def _handle_sigint(self, sig, frame): self.close() raise SystemExit(0) - - # TODO: Remove method or change return to thread safe copy + def get_image(self): - return self.canvas.image + return self.canvas.copy_image() - # TODO: Remove method or update the thread - def set_image_callback(self, image_callback): - self.image_callback = image_callback + @depricated + def get_image_raw(self): + return self.get_image() + @depricated + @staticmethod + def empty_image_raw(): + return Pyghthouse.empty_image() + + @depricated + def set_image_callback(self, image_callback): + self.ph_thread.callback = image_callback - # TODO: Remove method or apply frame_rate check and update interval of the thread + @depricated def set_frame_rate(self, frame_rate): - self.send_interval = 1.0 / frame_rate + if frame_rate > 60.0 or frame_rate <= 0: + self.close() + raise ValueError("frame rate must be greater than 0 and at most 60.") + self.ph_thread.send_interval = 1.0 / frame_rate + @depricated + def connect(self): + return self.start() + + @depricated + def stop(self): + return self.close() From d30527200e92e872336bc7fe18245c96238175c6 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Tue, 23 Sep 2025 20:20:55 +0200 Subject: [PATCH 18/43] removed deprecated tag to support python versions below 3.13 --- pyghthouse/ph.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index b3d714c..f7230ef 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -1,5 +1,4 @@ from signal import signal, SIGINT -from warnings import depricated from pyghthouse.data.canvas import PyghthouseCanvas from pyghthouse.controller.PHThread import PHThread @@ -198,30 +197,24 @@ def get_image(self): return self.canvas.copy_image() - @depricated def get_image_raw(self): return self.get_image() - @depricated @staticmethod def empty_image_raw(): return Pyghthouse.empty_image() - @depricated def set_image_callback(self, image_callback): self.ph_thread.callback = image_callback - @depricated def set_frame_rate(self, frame_rate): if frame_rate > 60.0 or frame_rate <= 0: self.close() raise ValueError("frame rate must be greater than 0 and at most 60.") self.ph_thread.send_interval = 1.0 / frame_rate - @depricated def connect(self): return self.start() - @depricated def stop(self): return self.close() From 6b4c74713373faff7a3d94bf5b3800ef056bf7be Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 25 Sep 2025 13:21:53 +0200 Subject: [PATCH 19/43] Improved import structure --- pyghthouse/__init__.py | 2 +- pyghthouse/connection/data.py | 19 +++++++ pyghthouse/connection/handler.py | 24 +++++++++ pyghthouse/connection/wsconnector.py | 50 +++---------------- pyghthouse/controller/__init__.py | 0 pyghthouse/ph.py | 6 +-- .../{controller/PHThread.py => thread.py} | 6 ++- 7 files changed, 57 insertions(+), 50 deletions(-) create mode 100644 pyghthouse/connection/data.py create mode 100644 pyghthouse/connection/handler.py delete mode 100644 pyghthouse/controller/__init__.py rename pyghthouse/{controller/PHThread.py => thread.py} (91%) diff --git a/pyghthouse/__init__.py b/pyghthouse/__init__.py index bbea480..e772ec1 100644 --- a/pyghthouse/__init__.py +++ b/pyghthouse/__init__.py @@ -1,2 +1,2 @@ from pyghthouse.ph import Pyghthouse -from pyghthouse.connection.wsconnector import VerbosityLevel +from pyghthouse.connection.data import VerbosityLevel diff --git a/pyghthouse/connection/data.py b/pyghthouse/connection/data.py new file mode 100644 index 0000000..a780c55 --- /dev/null +++ b/pyghthouse/connection/data.py @@ -0,0 +1,19 @@ +from enum import Enum + +class REID: + def __init__(self): + self._next = 0 + + def __next__(self): + n = self._next + self._next += 1 + return n + + def __iter__(self): + return self + +class VerbosityLevel(Enum): + NONE = 0 + WARN_ONCE = 1 + WARN = 2 + ALL = 3 \ No newline at end of file diff --git a/pyghthouse/connection/handler.py b/pyghthouse/connection/handler.py new file mode 100644 index 0000000..4e86dc6 --- /dev/null +++ b/pyghthouse/connection/handler.py @@ -0,0 +1,24 @@ +from .data import VerbosityLevel + +class PHMessageHandler: + + def __init__(self, verbosity=VerbosityLevel.WARN_ONCE): + self.verbosity = verbosity + self.warned_already = False + + def reset(self): + self.warned_already = False + + def handle(self, msg): + if msg['RNUM'] == 200: + if self.verbosity == VerbosityLevel.ALL: + print(msg) + elif self.verbosity == VerbosityLevel.WARN: + self.print_warning(msg) + elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: + self.print_warning(msg) + self.warned_already = True + + @staticmethod + def print_warning(msg): + print(f"Warning: {msg['RNUM']} {msg['RESPONSE']} {', '.join(msg['WARNINGS'])}") diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index 69a2ef4..dbdefe5 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -2,60 +2,22 @@ from websocket import WebSocketApp, setdefaulttimeout, ABNF from msgpack import packb, unpackb from ssl import CERT_NONE -from enum import Enum -class VerbosityLevel(Enum): - NONE = 0 - WARN_ONCE = 1 - WARN = 2 - ALL = 3 +from .data import REID +from .data import VerbosityLevel +from .handler import PHMessageHandler class WSConnector: - class PHMessageHandler: - - def __init__(self, verbosity=VerbosityLevel.WARN_ONCE): - self.verbosity = verbosity - self.warned_already = False - - def reset(self): - self.warned_already = False - - def handle(self, msg): - if msg['RNUM'] == 200: - if self.verbosity == VerbosityLevel.ALL: - print(msg) - elif self.verbosity == VerbosityLevel.WARN: - self.print_warning(msg) - elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: - self.print_warning(msg) - self.warned_already = True - - @staticmethod - def print_warning(msg): - print(f"Warning: {msg['RNUM']} {msg['RESPONSE']} {', '.join(msg['WARNINGS'])}") - - class REID: - def __init__(self): - self._next = 0 - - def __next__(self): - n = self._next - self._next += 1 - return n - - def __iter__(self): - return self - def __init__(self, username: str, token: str, address: str, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert=False, timeout=10): self.username = username self.token = token self.address = address - self.message_handler = self.PHMessageHandler(verbosity) - self.on_msg = self.PHMessageHandler.handle() + self.message_handler = PHMessageHandler(verbosity) + self.on_msg = PHMessageHandler.handle() self.ws = None self.lock = Lock() - self.reid = self.REID() + self.reid = REID() self.running = False self.ignore_ssl_cert = ignore_ssl_cert self.timeout = 10 diff --git a/pyghthouse/controller/__init__.py b/pyghthouse/controller/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index f7230ef..9f9d03a 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -1,8 +1,8 @@ from signal import signal, SIGINT -from pyghthouse.data.canvas import PyghthouseCanvas -from pyghthouse.controller.PHThread import PHThread -from pyghthouse.connection.wsconnector import VerbosityLevel +from .data.canvas import PyghthouseCanvas +from .thread import PHThread +from .connection.wsconnector import VerbosityLevel # TODO: Adjust description and example for more clarity class Pyghthouse: diff --git a/pyghthouse/controller/PHThread.py b/pyghthouse/thread.py similarity index 91% rename from pyghthouse/controller/PHThread.py rename to pyghthouse/thread.py index cb3d837..138fdc4 100644 --- a/pyghthouse/controller/PHThread.py +++ b/pyghthouse/thread.py @@ -1,8 +1,8 @@ from threading import Thread, Event, main_thread from time import sleep, time -from pyghthouse.data.canvas import PyghthouseCanvas -from pyghthouse.connection.wsconnector import WSConnector, VerbosityLevel +from .data.canvas import PyghthouseCanvas +from .connection.wsconnector import WSConnector, VerbosityLevel class PHThread(Thread): @@ -67,9 +67,11 @@ def set_callback_image(self): # TODO: Start websocket connection and wait until websocket is open def connect(self): + self.connector.start() pass # TODO: Signal websocket to close connection def disconnect(self): + self.connector.stop() pass \ No newline at end of file From 79c2a56f1a9f37c723ef0fc401a5c4e31d4fbd39 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 25 Sep 2025 13:24:45 +0200 Subject: [PATCH 20/43] Renamed _thread.py --- pyghthouse/{thread.py => _thread.py} | 0 pyghthouse/ph.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pyghthouse/{thread.py => _thread.py} (100%) diff --git a/pyghthouse/thread.py b/pyghthouse/_thread.py similarity index 100% rename from pyghthouse/thread.py rename to pyghthouse/_thread.py diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 9f9d03a..7973f19 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -1,7 +1,7 @@ from signal import signal, SIGINT from .data.canvas import PyghthouseCanvas -from .thread import PHThread +from ._thread import PHThread from .connection.wsconnector import VerbosityLevel # TODO: Adjust description and example for more clarity From 31c4a6ca4bd875f9689a808ee16b65ba0c33b002 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Fri, 31 Oct 2025 15:52:30 +0100 Subject: [PATCH 21/43] Documentation of _thread.py --- pyghthouse/_thread.py | 106 +++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index 138fdc4..a2099ce 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -5,16 +5,71 @@ from .connection.wsconnector import WSConnector, VerbosityLevel class PHThread(Thread): + """ + A Pyghthouse-Thread for the Pyghthouse routine. + + The Pyghthouse routine consists of the following phases: + - Connection Phase: Connects the client to the webserver. + - Main routine: Loop for sending frames to the webserver. + - Ending Phase: Correctly closes the connection. + + Attributes + ---------- + send_interval : float + Time (in seconds) between each frame. + + canvas : PyghthouseCanvas + Each frame will send the currently stored image in canvas. + + image_callback : function() -> image, optional + Function to produce a new image each frame. + When *None* given, the image saved in *canvas* will be used instead. + Default is *None*. + + ready : Event + Event flag is set to *True* after successful connection to the server. Frames will be frequently send in *True* state. + + stop_event : Event + Indicates when the Pyghthouse routine should be stopped. + + Methods + ------- + **run()** + Starts Pyghthouse-Thread routine. This routine sends images in the selected interval. + + **stop()** + Ends Pyghthouse routine. + """ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, username:str, token:str, address:str, verbosity:VerbosityLevel, ignore_ssl_cert:bool): + """ + Initialize thread for Pyghthouse routine. + + Parameters + ---------- + send_interval : float + Time (in seconds) between each frame. + + canvas : PyghthouseCanvas + Each frame will send the currently stored image in canvas. + + image_callback : function() -> image, optional + Function to produce a new image each frame. \n + When *None* given, the image saved in *canvas* will be used instead. + Default is *None*. + """ super().__init__() self.send_interval = send_interval self.callback = image_callback self.canvas = canvas - self.ready = Event() - self._stop_event = Event() + self.connector = WSConnector(username, token, address, verbosity, ignore_ssl_cert) + + self.ready = self.connector.connected + self.stop_event = Event() + + # Used to react to unexpected end of main self.main_thread = main_thread() self.ignore_main = False @@ -23,11 +78,16 @@ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, usern def stop(self): + """ + Ends Pyghthouse routine. + """ self._stop_event.set() - def stopped(self): - + def _close(self): + """ + Ends Pyghthouse routine when *stop_event* is set or upon unexpected end of main. + """ if not self.ignore_main and not self.main_thread.is_alive(): return True @@ -35,43 +95,45 @@ def stopped(self): def run(self): - self.connect() + """ + Starts Pyghthouse-Thread routine. This routine sends images in the selected interval. + """ + self._connect() self.ready.set() - while not self.stopped(): + while not self._close(): sleep_time = self.send_interval - (time() % self.send_interval) sleep(sleep_time) - self.send_image() + self._send_image() - self.disconnect() + self._disconnect() - def send_image(self): - + def _send_image(self): + """ + Build and send current frame + """ if self.callback is not None: - self.set_callback_image() + self._get_callback_image() bytes_image = self.canvas.get_bytes_image() self.connector.send(bytes_image) - def set_callback_image(self): - + def _get_callback_image(self): + image_from_callback = self.callback() try: self.canvas.set_image(image_from_callback) except: - self.disconnect() + self._disconnect() raise - # TODO: Start websocket connection and wait until websocket is open - def connect(self): - self.connector.start() - pass + def _connect(self): + self.connector.open() + self.connector.connected.wait() - # TODO: Signal websocket to close connection - def disconnect(self): - self.connector.stop() - pass \ No newline at end of file + def _disconnect(self): + self.connector.stop() \ No newline at end of file From fbdfeb9c833189fffe662cdf3b9c00e488cb2ba7 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Fri, 31 Oct 2025 16:03:31 +0100 Subject: [PATCH 22/43] Adjusted documentation text --- pyghthouse/_thread.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index a2099ce..40a1f82 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -31,14 +31,6 @@ class PHThread(Thread): stop_event : Event Indicates when the Pyghthouse routine should be stopped. - - Methods - ------- - **run()** - Starts Pyghthouse-Thread routine. This routine sends images in the selected interval. - - **stop()** - Ends Pyghthouse routine. """ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, username:str, token:str, address:str, verbosity:VerbosityLevel, ignore_ssl_cert:bool): @@ -54,7 +46,7 @@ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, usern Each frame will send the currently stored image in canvas. image_callback : function() -> image, optional - Function to produce a new image each frame. \n + Function to produce a new image each frame. When *None* given, the image saved in *canvas* will be used instead. Default is *None*. """ @@ -121,7 +113,9 @@ def _send_image(self): def _get_callback_image(self): - + """ + Get image from callback function. + """ image_from_callback = self.callback() try: self.canvas.set_image(image_from_callback) From ceb7322a6c13bbdcff985a24143b703c6e345f52 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Tue, 11 Nov 2025 15:55:35 +0100 Subject: [PATCH 23/43] Refactored Websocket connection, adjusted _thread.py and ph.py. Added documentation --- pyghthouse/_thread.py | 98 ++++++--- pyghthouse/connection/data.py | 2 +- pyghthouse/connection/handler.py | 25 ++- pyghthouse/connection/wsconnector.py | 287 ++++++++++++++++++++++----- pyghthouse/ph.py | 72 ++++--- 5 files changed, 368 insertions(+), 116 deletions(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index 40a1f82..b3ad021 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -2,7 +2,7 @@ from time import sleep, time from .data.canvas import PyghthouseCanvas -from .connection.wsconnector import WSConnector, VerbosityLevel +from .connection.wsconnector_new import WSConnector, VerbosityLevel class PHThread(Thread): """ @@ -11,7 +11,11 @@ class PHThread(Thread): The Pyghthouse routine consists of the following phases: - Connection Phase: Connects the client to the webserver. - Main routine: Loop for sending frames to the webserver. - - Ending Phase: Correctly closes the connection. + - Ending Phase: Closes the connection and cleanup threads. + + In the main routine, this thread will build and send frames from + **canvas**. The time between each frame is indicated by + **send_interval**. Attributes ---------- @@ -19,11 +23,11 @@ class PHThread(Thread): Time (in seconds) between each frame. canvas : PyghthouseCanvas - Each frame will send the currently stored image in canvas. + Each frame will send the currently stored image in **canvas**. image_callback : function() -> image, optional Function to produce a new image each frame. - When *None* given, the image saved in *canvas* will be used instead. + When *None* given, the image saved in **canvas** will be used instead. Default is *None*. ready : Event @@ -31,9 +35,13 @@ class PHThread(Thread): stop_event : Event Indicates when the Pyghthouse routine should be stopped. + + error : Event + Event flag is set to **True** when an error accured inside the pyghthouse routine. """ - def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, username:str, token:str, address:str, verbosity:VerbosityLevel, ignore_ssl_cert:bool): + def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, + username:str, token:str, address:str, verbosity:VerbosityLevel, ignore_ssl_cert:bool, timeout=3): """ Initialize thread for Pyghthouse routine. @@ -43,72 +51,96 @@ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, usern Time (in seconds) between each frame. canvas : PyghthouseCanvas - Each frame will send the currently stored image in canvas. + Each frame will send the currently stored image in **canvas**. image_callback : function() -> image, optional Function to produce a new image each frame. - When *None* given, the image saved in *canvas* will be used instead. + When *None* given, the image saved in **canvas** will be used instead. Default is *None*. """ super().__init__() self.send_interval = send_interval self.callback = image_callback self.canvas = canvas + self.verbosity = verbosity self.connector = WSConnector(username, token, address, - verbosity, ignore_ssl_cert) + verbosity, ignore_ssl_cert, timeout=timeout) - self.ready = self.connector.connected + self.ready = Event() self.stop_event = Event() + self.error = Event() + self.exception = None # Used to react to unexpected end of main self.main_thread = main_thread() - self.ignore_main = False - if self.callback is not None: - self.ignore_main = True def stop(self): """ Ends Pyghthouse routine. """ - self._stop_event.set() + self.stop_event.set() - def _close(self): + def _is_stop(self): """ - Ends Pyghthouse routine when *stop_event* is set or upon unexpected end of main. + Ends Pyghthouse routine when **stop_event** is set or upon unexpected + end of main. + + Errors inside the routine also results into the stopping process. """ - if not self.ignore_main and not self.main_thread.is_alive(): + if self.stop_event.is_set(): + return True + + if self.connector.error.is_set(): + return True + + if not self.main_thread.is_alive(): + if self.verbosity == VerbosityLevel.ALL: + print("Main thread dead.") + return True - return self._stop_event.is_set() + return False def run(self): """ Starts Pyghthouse-Thread routine. This routine sends images in the selected interval. """ - self._connect() + if self.verbosity == VerbosityLevel.ALL: + print("Starting Pyghthouse routine.") + self._connect() self.ready.set() - while not self._close(): + + while not self._is_stop(): + + self._send_image() + sleep_time = self.send_interval - (time() % self.send_interval) sleep(sleep_time) - self._send_image() + if self.verbosity == VerbosityLevel.ALL: + print("Ending Pyghthouse routine.") + + self.ready.clear() self._disconnect() def _send_image(self): """ - Build and send current frame + Build and send current frame. + + If error accures in sending process, the connection will be closed. """ if self.callback is not None: self._get_callback_image() bytes_image = self.canvas.get_bytes_image() + self.connector.send(bytes_image) @@ -117,17 +149,31 @@ def _get_callback_image(self): Get image from callback function. """ image_from_callback = self.callback() + try: self.canvas.set_image(image_from_callback) - except: - self._disconnect() - raise + + except Exception as exception: + self.exception = exception + self.error.set() + + self.stop() def _connect(self): + """ + Opens websocket connection. + + This function will also wait till the opening process has been finished + """ self.connector.open() - self.connector.connected.wait() def _disconnect(self): - self.connector.stop() \ No newline at end of file + """ + Closes the connection. + + Closing the connection also stops the websocket thread. So this + function allows the Pyghthouse thread to exit properly. + """ + self.connector.close() \ No newline at end of file diff --git a/pyghthouse/connection/data.py b/pyghthouse/connection/data.py index a780c55..85b19b4 100644 --- a/pyghthouse/connection/data.py +++ b/pyghthouse/connection/data.py @@ -1,6 +1,6 @@ from enum import Enum -class REID: +class ReID: def __init__(self): self._next = 0 diff --git a/pyghthouse/connection/handler.py b/pyghthouse/connection/handler.py index 4e86dc6..f089281 100644 --- a/pyghthouse/connection/handler.py +++ b/pyghthouse/connection/handler.py @@ -1,23 +1,28 @@ from .data import VerbosityLevel +# TODO: Rename? To WarningHandler? class PHMessageHandler: - def __init__(self, verbosity=VerbosityLevel.WARN_ONCE): - self.verbosity = verbosity + def __init__(self, kwargs): + self.verbosity = kwargs["verbosity"] self.warned_already = False def reset(self): self.warned_already = False def handle(self, msg): - if msg['RNUM'] == 200: - if self.verbosity == VerbosityLevel.ALL: - print(msg) - elif self.verbosity == VerbosityLevel.WARN: - self.print_warning(msg) - elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: - self.print_warning(msg) - self.warned_already = True + # TODO: Decide to print error to console and keep going or to close connection and stop pyghthouse routine + if self.verbosity == VerbosityLevel.ALL: + print(msg) + + elif not msg['RNUM'] == 200: + + if self.verbosity == VerbosityLevel.WARN: + self.print_warning(msg) + + elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: + self.print_warning(msg) + self.warned_already = True @staticmethod def print_warning(msg): diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index dbdefe5..c1d8a5b 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -1,80 +1,263 @@ -from threading import Thread, Lock -from websocket import WebSocketApp, setdefaulttimeout, ABNF +from threading import Thread, Event +from websocket import WebSocketApp, setdefaulttimeout, WebSocketConnectionClosedException from msgpack import packb, unpackb from ssl import CERT_NONE -from .data import REID -from .data import VerbosityLevel +from .data import VerbosityLevel, ReID from .handler import PHMessageHandler class WSConnector: + """ + A connector to the Lighthouse API with websocket thread. - def __init__(self, username: str, token: str, address: str, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert=False, timeout=10): + Attributes + ---------- + connected : Event + Event flag is set to *True* when websocket thread is connected to the + webserver and **send** can be used. + + error : Event + Event flag is set to *True* when error accured and connection is closed. + + The flag **error** has a higher priotiy than the **connected** flag. + Meaning when **error** is set to *True*, the connection is closed + even when **connected** can be set to *True*. + + For further invastigation, check developer notes in **on_error** + """ + + def __init__(self, username:str, token:str, address:str, + verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert:bool=False, + handler=PHMessageHandler, timeout=3): + """ + WSConnector initialization. + + Parameters + ---------- + username: str + Username + token: str + API-Token + address: str + Address of webserver. + verbosity: VerbosityLevel + Indicate level of displayed informations. + ignore_ssl_cert: bool + Ignores ssl certification when set to *True*. Default ssl + certification is none, so no certificates from the other side are + required (or will be looked at if provided). + handler: PHMessageHandler + Handles the messages received from the network. + timeout: int or float + Set timeout of WebSocket connection. + """ + self.username = username self.token = token self.address = address - self.message_handler = PHMessageHandler(verbosity) - self.on_msg = PHMessageHandler.handle() - self.ws = None - self.lock = Lock() - self.reid = REID() - self.running = False - self.ignore_ssl_cert = ignore_ssl_cert - self.timeout = 10 + self.reID = ReID() + self.verbosity = verbosity + + self.connected = Event() + self.error = Event() + self.exception = None + + kwargs = {"verbosity": self.verbosity} + handler = handler(kwargs) + self.handle_message = handler.handle + + # TODO: Maybe move functions for state handeling to handler class? + self.ws = WebSocketApp(address, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close) + + kwargs = {"sslopt": {"cert_reqs": CERT_NONE}} + if ignore_ssl_cert: + kwargs = None + + self.timeout = 3 if timeout > 0: self.timeout = timeout setdefaulttimeout(self.timeout) + + self.thread = Thread(target=self.ws.run_forever, kwargs=kwargs) + + + def open(self): + """ + Opens websocket connection. + + Starts websocket thread which will open the websocket connection. + + After opening the websocket, the websocket thread sets the + **connected** event flag to *True* and is ready to send data. + + When an error accured upon opening, the **error** flag will be set to + *True* and **on_error** will be called. In this case, the connection + will be closed again and the **connected** flag will cleared to *False* + again. + """ + if self.verbosity == VerbosityLevel.ALL: + print("Opening websocket connection.") + + self.thread.start() + + if not self.connected.wait(self.timeout+0.5): + raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") + + if self.error.is_set(): + self.connected.clear() + + if self.verbosity == VerbosityLevel.ALL: + print(f"Error upon connecting to {self.address}") + + def send(self, data): - with self.lock: - self.ws.send_bytes(self.construct_package(data)) - - def start(self): - self.stop() - self.ws = WebSocketApp(self.address, - on_message=None if self.on_msg is None else self._handle_msg, - on_open=self._ready, on_error=self._fail) - self.lock.acquire() - kwargs = {"sslopt": {"cert_reqs": CERT_NONE}} if self.ignore_ssl_cert else None - Thread(target=self.ws.run_forever, kwargs=kwargs).start() - self.lock.acquire() # wait for connection to be established - self.lock.release() - - def _fail(self, ws, err): - self.lock.release() - raise err - - def stop(self): - if self.ws is not None: - with self.lock: - print("Closing the connection.") - self.running = False - self.ws.close() - self.ws = None - - def _ready(self, ws): - print(f"Connected to {self.address}.") - self.running = True - self.lock.release() - - def _handle_msg(self, ws, msg): - if isinstance(msg, bytes): - msg = unpackb(msg) - self.on_msg(msg) + """ + Send data via websocket connection. + + Raises a *WebSocketConnectionClosedException* when no connection is + present. + """ + if self.error.is_set(): + return + + if self.connected.is_set(): + try: + if self.verbosity == VerbosityLevel.ALL: + print("Sending frame.") + + self.ws.send_bytes(self.construct_package(data)) + + except Exception as e: + if self.verbosity == VerbosityLevel.ALL: + print("Failed send process.") + + self.close() + + else: + self.exception = WebSocketConnectionClosedException("Cannot send frames. Connection is closed.") + self.error.set() + + + + def close(self): + """ + Close websocket connection. + + This function can still be used when no connection is present. + """ + if self.verbosity == VerbosityLevel.ALL: + print("Closing connection.") + + self.connected.clear() + self.ws.close() + def construct_package(self, payload_data): data = { - 'REID': next(self.reid), + 'REID': next(self.reID), 'AUTH': {'USER': self.username, 'TOKEN': self.token}, 'VERB': 'PUT', 'PATH': ['user', self.username, 'model'], 'META': {}, 'PAYL': payload_data } - return packb(self.construct_package(data), use_bin_type=True) - + return packb(data, use_bin_type=True) + + def set_timeout(self, timeout=10): - self.timeout = 10 + self.timeout = 3 if timeout > 0: self.timeout = timeout setdefaulttimeout(self.timeout) + + + # Functions used by the websocket thread: + + def _on_open(self, ws: WebSocketApp): + """ + Called directly after websocket has been successfully opened. + """ + if self.verbosity == VerbosityLevel.ALL: + print(f"Connected to {self.address}") + + self.connected.set() + + + def _on_message(self, ws: WebSocketApp, message): + if self.verbosity == VerbosityLevel.ALL: + print("Received message:") + + if isinstance(message, bytes): + message = unpackb(message) + self.handle_message(message) + + + def _on_close(self, ws: WebSocketApp, close_status_code, close_msg): + """ + Called after websocket connection fully closed. + + ---------------- + Developer notes: + + When the websocket thread has been started, **on_close** will always be + called, even when an error accured upon opening the connection. This is + a result of the **teardown** function in WebSocketApp. + + So its possible that **on_close** will be called even when **on_open** + hasn't been called yet. + + For further invastigation, check the developer notes in **on_error**. + """ + if self.verbosity == VerbosityLevel.ALL: + print("Connection closed.") + + self.connected.clear() + + + def _on_error(self, ws: WebSocketApp, err: Exception): + """ + Further handeling of errors outside of WebSocketApp. + + This function will save the **exception** and set the flag **error** + to *True*. + + This function also sets the flag **connected** to *True* to avoid + blocking of **open** when an error accured upon opening the connection. + + ---------------- + Developer notes: + + This method is used by WebSocketApp as callback function. + The intend is to signal that an error accured in WebSocketApp and to + allow further error handeling outside of WebSocketApp. + + Because we use **run_forever** without a parameter for **reconnect**, + we use the standard of *0* from the websocket-client library. + This will always force a teardown of the connection without an attempt + to reconnect upon error. + + A teardown of the connection will also call the **_on_close** callback + function, even when **_on_error** has been the called. **_on_close** + will even be called when an error accured on the attempt to open the + websocket. + Meaning, **_on_close** can be called even when **_on_open** hasn't been + called yet. + + Do not raise the exception here! + -------------------------------- + This function will be called from the websocket thread. Raising an + exception here stops the teardown process and could result into + unexpected behaviour. + """ + + if self.verbosity == VerbosityLevel.ALL: + print(err) + + self.exception = err + self.error.set() + self.connected.set() \ No newline at end of file diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 7973f19..b1af55b 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -133,54 +133,66 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u frame_rate: float = 30.0, image_callback=None, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert=False): - if frame_rate > 60.0 or frame_rate <= 0: - raise ValueError("frame rate must be greater than 0 and at most 60.") + # Lower bound must be higher than default timeout of websocket + if frame_rate > 60.0 or frame_rate <= 0.5: + raise ValueError("Frame rate must be greater than 0.5 and at most 60.") + + self.send_interval = 1.0 / frame_rate + self.timeout = 3 - send_interval = 1.0 / frame_rate self.canvas = PyghthouseCanvas() - self.ph_thread = PHThread(send_interval, image_callback, self.canvas, - username, token, address, verbosity, ignore_ssl_cert) + self.ph_thread = PHThread(self.send_interval, image_callback, self.canvas, + username, token, address, verbosity, ignore_ssl_cert, self.timeout) - self.ready = self.ph_thread.ready signal(SIGINT, self._handle_sigint) def start(self): - if self.ready.is_set() == True: + if not self.ph_thread.ready.is_set(): + self.ph_thread.start() - # TODO: Raise exception and stop or give a warning? - self.close() - raise RuntimeError("Pyghthouse can only be started once") + if not self.ph_thread.ready.wait(self.timeout + self.send_interval + 0.5): + raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") else: - - self.ph_thread.start() - self.ready.wait() + + self.close() + raise RuntimeError("Pyghthouse can only be started once.") def set_image(self, image): - if not self.ready.is_set(): - raise RuntimeError("cannot set an image before Pyghthouse started") + # Check for errors + if self.ph_thread.error.is_set(): + raise self.ph_thread.exception + if self.ph_thread.connector.error.is_set(): + raise self.ph_thread.connector.exception + + # Check if Pyghthouse is running + if not self.ph_thread.ready.is_set(): + raise RuntimeError("Cannot set an image before Pyghthouse has started.") + + # Setting the image try: self.canvas.set_image(image) - except: self.close() raise - def close(self): - - if self.ready.is_set(): - + def stop(self): + """ + Stops Pyghthouse. + + Stops the Pyghthouse routine and closes the websocket connection. All + threads used by Pyghthouse will be stopped in the process. + + When Pyghthouse isn't running, no changes will be made. + """ + if self.ph_thread.ready.is_set(): self.ph_thread.stop() - self.ph_thread.join() - - else: - raise RuntimeError("cannot stop Pyghthouse before it started") @staticmethod @@ -197,24 +209,30 @@ def get_image(self): return self.canvas.copy_image() + # Deprecated def get_image_raw(self): return self.get_image() + # Deprecated @staticmethod def empty_image_raw(): return Pyghthouse.empty_image() + # Deprecated def set_image_callback(self, image_callback): self.ph_thread.callback = image_callback + # Deprecated def set_frame_rate(self, frame_rate): if frame_rate > 60.0 or frame_rate <= 0: self.close() raise ValueError("frame rate must be greater than 0 and at most 60.") self.ph_thread.send_interval = 1.0 / frame_rate + # Deprecated def connect(self): return self.start() - - def stop(self): - return self.close() + + # Deprecated + def close(self): + return self.stop() From 8d06f7914599c6c124874db5ea715356524a23e5 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Tue, 11 Nov 2025 15:58:47 +0100 Subject: [PATCH 24/43] Fix import --- pyghthouse/_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index b3ad021..b9ad979 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -2,7 +2,7 @@ from time import sleep, time from .data.canvas import PyghthouseCanvas -from .connection.wsconnector_new import WSConnector, VerbosityLevel +from .connection.wsconnector import WSConnector, VerbosityLevel class PHThread(Thread): """ From 3b39748071d8cd76be9c88aa31e67b6742d7e191 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Sat, 14 Feb 2026 12:30:36 +0100 Subject: [PATCH 25/43] Added wait function. Adjusted timeout. Edited comments --- pyghthouse/_thread.py | 17 +++-- pyghthouse/connection/wsconnector.py | 4 +- pyghthouse/ph.py | 95 ++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index b9ad979..f822f92 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -30,18 +30,23 @@ class PHThread(Thread): When *None* given, the image saved in **canvas** will be used instead. Default is *None*. + connected : Event + Event flag is set to *True* after successful connection to the server. + Frames will be frequently send in *True* state. + ready : Event - Event flag is set to *True* after successful connection to the server. Frames will be frequently send in *True* state. + Event flag is set to *True* in the send process of a frame. + This flag will always be *True* when connected is *True*, unless the even is unset Can be used for waiting operations. stop_event : Event Indicates when the Pyghthouse routine should be stopped. error : Event - Event flag is set to **True** when an error accured inside the pyghthouse routine. + Event flag is set to *True* when an error accured inside the pyghthouse routine. """ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, - username:str, token:str, address:str, verbosity:VerbosityLevel, ignore_ssl_cert:bool, timeout=3): + username:str, token:str, address:str, verbosity:VerbosityLevel, ignore_ssl_cert:bool, timeout=2.5): """ Initialize thread for Pyghthouse routine. @@ -67,6 +72,7 @@ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, self.connector = WSConnector(username, token, address, verbosity, ignore_ssl_cert, timeout=timeout) + self.connected = Event() self.ready = Event() self.stop_event = Event() self.error = Event() @@ -126,7 +132,6 @@ def run(self): if self.verbosity == VerbosityLevel.ALL: print("Ending Pyghthouse routine.") - self.ready.clear() self._disconnect() @@ -140,7 +145,7 @@ def _send_image(self): self._get_callback_image() bytes_image = self.canvas.get_bytes_image() - + self.ready.set() self.connector.send(bytes_image) @@ -167,6 +172,7 @@ def _connect(self): This function will also wait till the opening process has been finished """ self.connector.open() + self.connected.set() def _disconnect(self): @@ -176,4 +182,5 @@ def _disconnect(self): Closing the connection also stops the websocket thread. So this function allows the Pyghthouse thread to exit properly. """ + self.connected.clear() self.connector.close() \ No newline at end of file diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index c1d8a5b..0de0a11 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -28,7 +28,7 @@ class WSConnector: def __init__(self, username:str, token:str, address:str, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert:bool=False, - handler=PHMessageHandler, timeout=3): + handler=PHMessageHandler, timeout=2.5): """ WSConnector initialization. @@ -104,7 +104,7 @@ def open(self): self.thread.start() - if not self.connected.wait(self.timeout+0.5): + if not self.connected.wait(self.timeout + 0.2): raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") if self.error.is_set(): diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index b1af55b..a538cd2 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -1,4 +1,5 @@ from signal import signal, SIGINT +from time import sleep from .data.canvas import PyghthouseCanvas from ._thread import PHThread @@ -133,14 +134,32 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u frame_rate: float = 30.0, image_callback=None, verbosity=VerbosityLevel.WARN_ONCE, ignore_ssl_cert=False): - # Lower bound must be higher than default timeout of websocket + self.canvas = PyghthouseCanvas() + + """ + The timeout of the lamp controllers is 5 seconds, so to prevent unexpected behaviour of the light + installation, our libraries timeout needs to be faster than the timeout of our lamp controller. + To be more precise, we need to be faster than: + (socket) timeout + send_interval + network delay < 5 + + The socket timeout consists of the full send process from the libraries websocket thread to our model server + beacon. So for the network delay, we only need to consider the streaming of the model inside the Lighthouse + infrastructure. + We can approximate this delay to a range from 0.1 to 0.5 seconds, + depending on the servers load. + + With a frame rate of 0.5, we get a send_interval of 2 seconds. + + So we get a total worst-case of: + (socket) timeout + send_interval + network delay = total delay + 2.5 + 2.0 + ~0.5 ~ 5.0 + """ if frame_rate > 60.0 or frame_rate <= 0.5: raise ValueError("Frame rate must be greater than 0.5 and at most 60.") self.send_interval = 1.0 / frame_rate - self.timeout = 3 + self.timeout = 2.5 - self.canvas = PyghthouseCanvas() self.ph_thread = PHThread(self.send_interval, image_callback, self.canvas, username, token, address, verbosity, ignore_ssl_cert, self.timeout) @@ -148,11 +167,13 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u def start(self): - - if not self.ph_thread.ready.is_set(): + """ + Starts Pyghthouse routine. + """ + if not self.ph_thread.connected.is_set(): self.ph_thread.start() - if not self.ph_thread.ready.wait(self.timeout + self.send_interval + 0.5): + if not self.ph_thread.connected.wait(self.timeout + 0.3): raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") else: @@ -162,16 +183,15 @@ def start(self): def set_image(self, image): - - # Check for errors - if self.ph_thread.error.is_set(): - raise self.ph_thread.exception - - if self.ph_thread.connector.error.is_set(): - raise self.ph_thread.connector.exception - - # Check if Pyghthouse is running - if not self.ph_thread.ready.is_set(): + """ + Sets pyghthouse canvas to a new image. + + :param image: A 3D array where every entry is accessed via image[y][x][rgb]. + The dimension sizes are 14x28x3, meaning the last entry should be accessed with image[13][27][2]. + RGB entries only allow values in a range of 0 to 255 (one byte). + """ + # Check if Pyghthouse routine is running + if not self._routine_is_running(): raise RuntimeError("Cannot set an image before Pyghthouse has started.") # Setting the image @@ -182,21 +202,48 @@ def set_image(self, image): raise + def wait(self): + """ + Wait for finalization of the current frame. + + Recommended to prevent skipping of frames. + **Do not use for fast interactive animations**, like a game, because + waiting can result into delayed or ignored inputs! + + **set_image** sets the image as fast as possible. On the other hand, + the pyghthouse routine creates a frame with the last image set. + This will result into losing images, when we create our images faster than the frame rate. + To prevent the loss of an image, we can use **wait** to wait until the current frame has been build. + """ + self._routine_is_running() + + if not self.ph_thread.ready.wait(self.timeout + self.send_interval + 0.2): + raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") + + self.ph_thread.ready.clear() + + def stop(self): """ Stops Pyghthouse. Stops the Pyghthouse routine and closes the websocket connection. All threads used by Pyghthouse will be stopped in the process. + This process can take more time with lower frame rate. - When Pyghthouse isn't running, no changes will be made. + When Pyghthouse isn't running, no changes will be made. """ - if self.ph_thread.ready.is_set(): + if self.ph_thread.connected.is_set("Unexpected library behaviour. Reached wait timeout before socket timeout."): self.ph_thread.stop() @staticmethod def empty_image(): + """ + Returns an empty image. + + An empty image is a fully black image, meaning every RGB value is set to 0. + """ return [[[0 for k in range(3)] for j in range(28)] for i in range(14)] @@ -205,6 +252,18 @@ def _handle_sigint(self, sig, frame): raise SystemExit(0) + def _routine_is_running(self): + # Check for errors + if self.ph_thread.error.is_set(): + raise self.ph_thread.exception + + if self.ph_thread.connector.error.is_set(): + raise self.ph_thread.connector.exception + + # Check if Pyghthouse routine is running + return self.ph_thread.connected.is_set() + + def get_image(self): return self.canvas.copy_image() From 82377cd3f3bdf5c7a7c2840d3c7c9d7fc365b6d6 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Sat, 14 Feb 2026 12:50:20 +0100 Subject: [PATCH 26/43] Fixed routine check. Edited comments --- pyghthouse/ph.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index a538cd2..19dfdce 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -190,7 +190,6 @@ def set_image(self, image): The dimension sizes are 14x28x3, meaning the last entry should be accessed with image[13][27][2]. RGB entries only allow values in a range of 0 to 255 (one byte). """ - # Check if Pyghthouse routine is running if not self._routine_is_running(): raise RuntimeError("Cannot set an image before Pyghthouse has started.") @@ -215,7 +214,8 @@ def wait(self): This will result into losing images, when we create our images faster than the frame rate. To prevent the loss of an image, we can use **wait** to wait until the current frame has been build. """ - self._routine_is_running() + if not self._routine_is_running(): + raise RuntimeError("Cannot wait without a Pyghthouse routine running.") if not self.ph_thread.ready.wait(self.timeout + self.send_interval + 0.2): raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") @@ -227,13 +227,12 @@ def stop(self): """ Stops Pyghthouse. - Stops the Pyghthouse routine and closes the websocket connection. All - threads used by Pyghthouse will be stopped in the process. - This process can take more time with lower frame rate. + Stops the Pyghthouse routine and closes the websocket connection. All threads used by Pyghthouse will be + stopped in the process. This process can take more time with lower frame rate. When Pyghthouse isn't running, no changes will be made. """ - if self.ph_thread.connected.is_set("Unexpected library behaviour. Reached wait timeout before socket timeout."): + if self.ph_thread.connected.is_set(): self.ph_thread.stop() @@ -265,6 +264,9 @@ def _routine_is_running(self): def get_image(self): + """ + Returns an image copy of the current canvas image. + """ return self.canvas.copy_image() From ea99edb295cd99be5c1441b051c4e7cb301dfdf3 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Sat, 14 Feb 2026 13:10:05 +0100 Subject: [PATCH 27/43] Removed deprecated features --- pyghthouse/ph.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 19dfdce..9691740 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -269,15 +269,6 @@ def get_image(self): """ return self.canvas.copy_image() - - # Deprecated - def get_image_raw(self): - return self.get_image() - - # Deprecated - @staticmethod - def empty_image_raw(): - return Pyghthouse.empty_image() # Deprecated def set_image_callback(self, image_callback): @@ -288,12 +279,4 @@ def set_frame_rate(self, frame_rate): if frame_rate > 60.0 or frame_rate <= 0: self.close() raise ValueError("frame rate must be greater than 0 and at most 60.") - self.ph_thread.send_interval = 1.0 / frame_rate - - # Deprecated - def connect(self): - return self.start() - - # Deprecated - def close(self): - return self.stop() + self.ph_thread.send_interval = 1.0 / frame_rate \ No newline at end of file From fc5eaa72a6ffeec958e5cd378ee7e2fd78daa96f Mon Sep 17 00:00:00 2001 From: Gedusim Date: Tue, 17 Feb 2026 13:59:45 +0100 Subject: [PATCH 28/43] Fixed wait. Adjusted comments and error messages. Added warnings to deprecated features --- pyghthouse/_thread.py | 4 ++-- pyghthouse/connection/wsconnector.py | 2 +- pyghthouse/ph.py | 33 +++++++++++++++++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index f822f92..81f93f8 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -120,8 +120,7 @@ def run(self): print("Starting Pyghthouse routine.") self._connect() - self.ready.set() - + while not self._is_stop(): self._send_image() @@ -145,6 +144,7 @@ def _send_image(self): self._get_callback_image() bytes_image = self.canvas.get_bytes_image() + # Signal wait function in ph.py to continue self.ready.set() self.connector.send(bytes_image) diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index 0de0a11..72eb29a 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -105,7 +105,7 @@ def open(self): self.thread.start() if not self.connected.wait(self.timeout + 0.2): - raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") + raise RuntimeError("Unexpected behaviour. Reached wait timeout before socket timeout.") if self.error.is_set(): self.connected.clear() diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 9691740..1e0fbb7 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -174,7 +174,7 @@ def start(self): self.ph_thread.start() if not self.ph_thread.connected.wait(self.timeout + 0.3): - raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") + raise RuntimeError("Unexpected behaviour. Reached wait timeout before socket timeout.") else: @@ -186,6 +186,8 @@ def set_image(self, image): """ Sets pyghthouse canvas to a new image. + This function sets the image as fast as possible. To prevent a loss of an image, use **wait** between each **set_image** call. + :param image: A 3D array where every entry is accessed via image[y][x][rgb]. The dimension sizes are 14x28x3, meaning the last entry should be accessed with image[13][27][2]. RGB entries only allow values in a range of 0 to 255 (one byte). @@ -205,6 +207,8 @@ def wait(self): """ Wait for finalization of the current frame. + This function blocks the called thread until the current frame has been constructed. + Recommended to prevent skipping of frames. **Do not use for fast interactive animations**, like a game, because waiting can result into delayed or ignored inputs! @@ -217,12 +221,11 @@ def wait(self): if not self._routine_is_running(): raise RuntimeError("Cannot wait without a Pyghthouse routine running.") - if not self.ph_thread.ready.wait(self.timeout + self.send_interval + 0.2): - raise RuntimeError("Unexpected library behaviour. Reached wait timeout before socket timeout.") - self.ph_thread.ready.clear() - + if not self.ph_thread.ready.wait(self.timeout + self.send_interval + 0.2): + raise RuntimeError("Unexpected behaviour. Reached wait timeout before socket timeout.") + def stop(self): """ Stops Pyghthouse. @@ -270,13 +273,27 @@ def get_image(self): return self.canvas.copy_image() - # Deprecated def set_image_callback(self, image_callback): + """ + Sets a new callback function for image creation. + + This function is async to the pyghthouse routine, so non-deterministic behaviour is possible. + + To prevent image loss, it is recommended to synchronize with the pyghthouse routine by using **wait** for x + times where x is the amount of images send before calling this function. + """ self.ph_thread.callback = image_callback + + # Deprecated + def close(self): + print("Warning: close is a deprecated feature. It is recommended to use stop instead.") + self.stop() + # Deprecated def set_frame_rate(self, frame_rate): - if frame_rate > 60.0 or frame_rate <= 0: + print("Warning: set_frame_rate is a deprecated feature and can cause enexpected behaviour.") + if frame_rate > 60.0 or frame_rate <= 0.5: self.close() - raise ValueError("frame rate must be greater than 0 and at most 60.") + raise ValueError("frame rate must be greater than 0.5 and at most 60.") self.ph_thread.send_interval = 1.0 / frame_rate \ No newline at end of file From a4752c37937aaa3f5f78f253c1110f0e0beee44b Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 18 Feb 2026 11:34:49 +0100 Subject: [PATCH 29/43] Added error handeling to callback function. Edited documentation --- pyghthouse/_thread.py | 3 +-- pyghthouse/ph.py | 46 ++++++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index 81f93f8..8b53b4d 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -153,9 +153,8 @@ def _get_callback_image(self): """ Get image from callback function. """ - image_from_callback = self.callback() - try: + image_from_callback = self.callback() self.canvas.set_image(image_from_callback) except Exception as exception: diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 1e0fbb7..47e81e1 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -65,8 +65,8 @@ class Pyghthouse: Each color channel has a depth of 8 bits, i.e. is represented by a number between 0 and 255, inclusively. For instance, [255, 127, 0] is 100% red, 50% green and 0% blue, a.k.a. orange. - Images can be either flat or nested lists, as long as they have 14*28*3=1176 elements overall. The - Pyghthouse.empty_image() method returns a completely black image in the nested format, i.e. + Images are nested lists with 14*28*3=1176 elements overall. The Pyghthouse.empty_image() method returns a + completely black image in the nested format, i.e. [ [ [0, 0, 0,], @@ -84,7 +84,7 @@ class Pyghthouse: The following example creates a Pyghthouse and sets the 10th window of the 11th floor to orange. >>> from pyghthouse import Pyghthouse >>> p = Pyghthouse("YourUsername", "YourToken") - >>> p.start() # not necessary to set image, but necessary for sending. + >>> p.start() # necessary to set image and for sending. >>> img = Pyghthouse.empty_image() >>> img[3][9] = [255, 127, 0] >>> p.set_image(img) @@ -228,7 +228,7 @@ def wait(self): def stop(self): """ - Stops Pyghthouse. + Stops Pyghthouse routine. Stops the Pyghthouse routine and closes the websocket connection. All threads used by Pyghthouse will be stopped in the process. This process can take more time with lower frame rate. @@ -249,23 +249,6 @@ def empty_image(): return [[[0 for k in range(3)] for j in range(28)] for i in range(14)] - def _handle_sigint(self, sig, frame): - self.close() - raise SystemExit(0) - - - def _routine_is_running(self): - # Check for errors - if self.ph_thread.error.is_set(): - raise self.ph_thread.exception - - if self.ph_thread.connector.error.is_set(): - raise self.ph_thread.connector.exception - - # Check if Pyghthouse routine is running - return self.ph_thread.connected.is_set() - - def get_image(self): """ Returns an image copy of the current canvas image. @@ -273,7 +256,7 @@ def get_image(self): return self.canvas.copy_image() - def set_image_callback(self, image_callback): + def set_image_callback(self, image_callback=None): """ Sets a new callback function for image creation. @@ -281,10 +264,29 @@ def set_image_callback(self, image_callback): To prevent image loss, it is recommended to synchronize with the pyghthouse routine by using **wait** for x times where x is the amount of images send before calling this function. + + When the image_callback is set to *None* """ self.ph_thread.callback = image_callback + def _handle_sigint(self, sig, frame): + self.close() + raise SystemExit(0) + + + def _routine_is_running(self): + # Check for errors + if self.ph_thread.error.is_set(): + raise self.ph_thread.exception + + if self.ph_thread.connector.error.is_set(): + raise self.ph_thread.connector.exception + + # Check if Pyghthouse routine is running + return self.ph_thread.connected.is_set() + + # Deprecated def close(self): print("Warning: close is a deprecated feature. It is recommended to use stop instead.") From 9dcd608e990bdcf3ef7a8f6888667cc33b9f65ce Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 18 Feb 2026 11:47:40 +0100 Subject: [PATCH 30/43] Removed redundant signal handler --- pyghthouse/connection/wsconnector.py | 1 + pyghthouse/ph.py | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index 72eb29a..b121eea 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -62,6 +62,7 @@ def __init__(self, username:str, token:str, address:str, self.error = Event() self.exception = None + # TODO: Add closing function to the message handler, for cases like 401 unauthorized kwargs = {"verbosity": self.verbosity} handler = handler(kwargs) self.handle_message = handler.handle diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 47e81e1..fe12dd2 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -162,8 +162,6 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u self.ph_thread = PHThread(self.send_interval, image_callback, self.canvas, username, token, address, verbosity, ignore_ssl_cert, self.timeout) - - signal(SIGINT, self._handle_sigint) def start(self): @@ -270,11 +268,6 @@ def set_image_callback(self, image_callback=None): self.ph_thread.callback = image_callback - def _handle_sigint(self, sig, frame): - self.close() - raise SystemExit(0) - - def _routine_is_running(self): # Check for errors if self.ph_thread.error.is_set(): From 6a94e7abfa502417f093f81f4baa0922176c03f3 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 18 Feb 2026 12:32:28 +0100 Subject: [PATCH 31/43] Changed structure of handler to improve maintainability --- pyghthouse/connection/handler.py | 28 +++++++++++++++++----------- pyghthouse/connection/wsconnector.py | 1 - pyghthouse/ph.py | 8 ++++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pyghthouse/connection/handler.py b/pyghthouse/connection/handler.py index f089281..acb9cb8 100644 --- a/pyghthouse/connection/handler.py +++ b/pyghthouse/connection/handler.py @@ -14,16 +14,22 @@ def handle(self, msg): # TODO: Decide to print error to console and keep going or to close connection and stop pyghthouse routine if self.verbosity == VerbosityLevel.ALL: print(msg) - - elif not msg['RNUM'] == 200: - - if self.verbosity == VerbosityLevel.WARN: - self.print_warning(msg) - - elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: - self.print_warning(msg) - self.warned_already = True + return - @staticmethod - def print_warning(msg): + match msg["RNUM"]: + case 200: + pass + case 401: + self.print_warning(msg, "Are Username and Token correct?") + case _: + self.print_warning(msg) + + def print_warning(self, msg, hint=""): + if self.verbosity == VerbosityLevel.WARN_ONCE and self.warned_already: + return + print(f"Warning: {msg['RNUM']} {msg['RESPONSE']} {', '.join(msg['WARNINGS'])}") + if hint: + print(hint) + + self.warned_already = True diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index b121eea..72eb29a 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -62,7 +62,6 @@ def __init__(self, username:str, token:str, address:str, self.error = Event() self.exception = None - # TODO: Add closing function to the message handler, for cases like 401 unauthorized kwargs = {"verbosity": self.verbosity} handler = handler(kwargs) self.handle_message = handler.handle diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index fe12dd2..84567b7 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -176,7 +176,7 @@ def start(self): else: - self.close() + self.stop() raise RuntimeError("Pyghthouse can only be started once.") @@ -197,7 +197,7 @@ def set_image(self, image): try: self.canvas.set_image(image) except: - self.close() + self.stop() raise @@ -282,13 +282,13 @@ def _routine_is_running(self): # Deprecated def close(self): - print("Warning: close is a deprecated feature. It is recommended to use stop instead.") + print("Warning: close is a deprecated feature. Use stop instead.") self.stop() # Deprecated def set_frame_rate(self, frame_rate): print("Warning: set_frame_rate is a deprecated feature and can cause enexpected behaviour.") if frame_rate > 60.0 or frame_rate <= 0.5: - self.close() + self.stop() raise ValueError("frame rate must be greater than 0.5 and at most 60.") self.ph_thread.send_interval = 1.0 / frame_rate \ No newline at end of file From ddc0ede747e28c8b69bb135137e65fd40f20ef16 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 18 Feb 2026 13:55:00 +0100 Subject: [PATCH 32/43] Adjusted example and edited comments --- example.py | 62 ++++++++++++++++++++++++++++-------------------- pyghthouse/ph.py | 3 ++- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/example.py b/example.py index e970dcf..32630e6 100644 --- a/example.py +++ b/example.py @@ -4,63 +4,73 @@ A generel orientation of what you need: - Import of pyghthouse - Creating an instance of Pyghthouse -- start connection -- Sending images with either a given function or set_image(image) -- close connection (not needed but recommend) +- Start Pyghthouse routine +- Sending images with either a given function or set_image +- Stop Pyghthouse routine (not needed but recommend) More examples can be found in the examples folder. - -Note that this skript handles Pyghthouse as a local module compared to -the skripts in examples; These handle Pyghthouse as an installed package. -Check examples/README.md for more informations. ''' -from pyghthouse import Pyghthouse -import pyghthouse.utils as utils -from examples.config import UNAME, TOKEN - # Optional: This condition only executes if run as a skript. # Importing this program won't execute this block. if __name__ == '__main__': - - # Create instance of Pyghthouse and start connection + from pyghthouse import Pyghthouse + import pyghthouse.utils as utils + from examples.config import UNAME, TOKEN + + + # Create instance of Pyghthouse and start Pyghthouse routine username = UNAME token = TOKEN p = Pyghthouse(username, token) p.start() - # the image is a 3 dimensional list, meaning a list with [[[r,g,b]]] entries - # create a black image + # The image object is a 3 dimensional list. Each index is accessed via [row][collum][rgb] + # Create a black image img = p.empty_image() pos_x = 10 pos_y = 5 - # color entries are in rgb + + # The used color is in the RGB format. We use a list where each index represents a color channel. + # Index 0 for red, 1 for green, 2 for blue. + # Each color channel has a size of 1 byte, so values from 0 to 255 can be used. color = [100, 124, 24] - - # sends the given image + + # Set the image with one colored pixel img[pos_y][pos_x] = color p.set_image(img) key = input("Enter 'n' for the next image, enter any other key to skip\n") if key.upper() == "N": - # set rgb color with a converted hsv color + + # Our library also have convertors for other color formats. + # Now we convert a hsv color to an rgb color color = utils.from_hsv(0.5, 1.0, 0.7) - # set all pixels to the current color - for x in range(28): - for y in range(14): + # Set the color of all pixels + for y in range(14): + for x in range(28): img[y][x] = color + p.set_image(img) - key = input("Enter 'n' for the next image, enter any other key to skip\n") + + key = input("Enter 'n' for the next animation, enter any other key to skip\n") if key.upper() == "N": + color = [255, 255, 255] - # create 3 white lines + # Let 3 white lines appear for x in range(28): for y in range(3,10,3): + img[y][x] = color - p.set_image(img) + + p.set_image(img) + # set_image overwrites the old image. So to prevent the loss of an image, + # we wait until the frame has been build + p.wait() - p.close() \ No newline at end of file + + p.stop() \ No newline at end of file diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 84567b7..41718d3 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -184,7 +184,8 @@ def set_image(self, image): """ Sets pyghthouse canvas to a new image. - This function sets the image as fast as possible. To prevent a loss of an image, use **wait** between each **set_image** call. + This function overwrites the old image. Only the newest image will be converted to a frame by the pyghthouse + routine. To prevent the loss of an image, use **wait** after a **set_image** call. :param image: A 3D array where every entry is accessed via image[y][x][rgb]. The dimension sizes are 14x28x3, meaning the last entry should be accessed with image[13][27][2]. From dfccca70bd5c1879edf038318f36945e93a9c938 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 18 Feb 2026 14:04:58 +0100 Subject: [PATCH 33/43] Updated setup.py --- setup.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index cd5f6c4..a9514ea 100644 --- a/setup.py +++ b/setup.py @@ -7,15 +7,14 @@ name='pyghthouse', version='0.3.0', packages=find_packages(where='.'), - url='https://github.com/Musicted/pyghthouse', + url='https://github.com/ProjectLighthouseCAU/pyghthouse', license='MIT', - author='Gavin Lüdemann', - author_email='gavin.luedemann@gmail.com', + author='Gavin Lüdemann, Nico Penning', + author_email='gavin.luedemann@gmail.com, nico.penning1@gmail.com', description='Python Lighthouse adapter', long_description=long_description, long_description_content_type="text/markdown", install_requires=[ - 'numpy >= 1.23', 'websocket-client >= 1.6, < 2', 'msgpack >= 1.0, < 2', ], From c70c9bd4a4343a3a60da61c8bddca6c64fa77ac1 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 18 Feb 2026 14:58:59 +0100 Subject: [PATCH 34/43] Changed allowed values in canvas to numbers. Updated examples --- examples/huecircle.py | 3 +++ examples/noisefill.py | 3 +++ examples/rainbow.py | 10 +++++++++- examples/rgbfill.py | 15 +++++++++------ examples/rgbscan.py | 11 +++++++---- examples/twopoints.py | 3 +++ pyghthouse/data/canvas.py | 16 ++++++---------- 7 files changed, 40 insertions(+), 21 deletions(-) diff --git a/examples/huecircle.py b/examples/huecircle.py index f4a63d8..961ae41 100644 --- a/examples/huecircle.py +++ b/examples/huecircle.py @@ -32,3 +32,6 @@ def callback(): p = Pyghthouse(UNAME, TOKEN, image_callback=callback) print("Starting... use CTRL+C to stop.") p.start() + + while True: + p.wait() diff --git a/examples/noisefill.py b/examples/noisefill.py index ced1be6..1983778 100644 --- a/examples/noisefill.py +++ b/examples/noisefill.py @@ -24,3 +24,6 @@ def image_gen(): p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() + + while True: + p.wait() diff --git a/examples/rainbow.py b/examples/rainbow.py index d59b6c5..1cc0a95 100644 --- a/examples/rainbow.py +++ b/examples/rainbow.py @@ -4,9 +4,15 @@ def rainbow_generator(): + image = Pyghthouse.empty_image() while True: for i in range(180): - yield [from_hsv((i / 180 + j / (14 * 28)) % 1.0, 1.0, 1.0) for j in range(14 * 28)] + for x in range(28): + for y in range(14): + j = x + y*28 + image[y][x] = from_hsv((i / 180 + j / (14 * 28)) % 1.0, 1.0, 1.0) + yield image + rainbow = rainbow_generator() @@ -21,3 +27,5 @@ def callback(): print("Starting... use CTRL+C to stop.") p.start() + while True: + p.wait() \ No newline at end of file diff --git a/examples/rgbfill.py b/examples/rgbfill.py index 97f8d81..a58ce9d 100644 --- a/examples/rgbfill.py +++ b/examples/rgbfill.py @@ -8,15 +8,15 @@ def image_gen(): image = np.zeros((14, 28, 3)) yield image while True: - for x in range(14): - for y in range(28): + for y in range(14): + for x in range(28): for j in range(3): - image[x, y, j] = 255 + image[y, x, j] = 255 yield image - for y in range(28): - for x in range(14): + for x in range(28): + for y in range(14): for j in range(3): - image[x, y, j] = 0 + image[y, x, j] = 0 yield image @@ -26,3 +26,6 @@ def image_gen(): p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() + + while True: + p.wait() \ No newline at end of file diff --git a/examples/rgbscan.py b/examples/rgbscan.py index 28939fc..1cd2677 100644 --- a/examples/rgbscan.py +++ b/examples/rgbscan.py @@ -9,11 +9,11 @@ def image_gen(): yield image while True: for j in range(3): - for x in range(14): - for y in range(28): - image[x, y, j] = 255 + for y in range(14): + for x in range(28): + image[y, x, j] = 255 yield image - image[x, y, j] = 0 + image[y, x, j] = 0 g = image_gen() @@ -22,3 +22,6 @@ def image_gen(): p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() + + while True: + p.wait() \ No newline at end of file diff --git a/examples/twopoints.py b/examples/twopoints.py index ef446b0..b411cee 100644 --- a/examples/twopoints.py +++ b/examples/twopoints.py @@ -62,3 +62,6 @@ def callback(self): i = ImageMaker() p = Pyghthouse(UNAME, TOKEN, image_callback=i.callback, frame_rate=60) p.start() + + while True: + p.wait() \ No newline at end of file diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index 0a75700..e84df60 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -1,4 +1,5 @@ from threading import Lock +from numbers import Number class PyghthouseCanvas: @@ -70,20 +71,18 @@ def _check_size(self, other: list): # Raise ValueError on wrong dimension size if self.IMAGE_SHAPE != other_size: - raise ValueError(f"The image does not have the correct dimensions. Dimensions should be {self.IMAGE_SHAPE} as (y, x, rgb), but received {other_size}") + raise ValueError(f"The image does not have the correct dimension sizes. Dimensions should be {self.IMAGE_SHAPE} as (row, column, rgb), but received {other_size}") def _check_cells(self, other: list): # Catch objects like [[[0,1,2],[0,1,2,3,4,5],[1]],1] - # TODO: Decide on either throw error on too large lists or do a warning - # TODO: Decide on either try-except block or TypeCheck for y in range(self.IMAGE_SHAPE[0]): try: if len(other[y]) != self.IMAGE_SHAPE[1]: - raise IndexError(f"x-list size should be {self.IMAGE_SHAPE[1]} but received {len(other[y])} at position [{y}]") + raise IndexError(f"column-list size should be {self.IMAGE_SHAPE[1]} but received {len(other[y])} at position [{y}]") except (TypeError): raise TypeError(f"Require type 'list' at [{y}], but received object of type '{type(other[y]).__name__}'") from None @@ -107,11 +106,8 @@ def _check_values(self, other: list): value = other[y][x][rgb] - # TODO: Decide on either throw error on wrong type or do a typecast with a warning. - # Which types should be allowed? All numerical or only int? - if not isinstance(value, int): - raise TypeError(f"Wrong type at ({y},{x},{rgb}). Type should be 'int' but received '{type(value).__name__}'") + if not isinstance(value, Number): + raise TypeError(f"Wrong type at ({y},{x},{rgb}). Type should be a Number, like 'int', but received '{type(value).__name__}'") - # TODO: Decide on either throw error when out of range or change to closest value in range with a warning if int(value) < 0 or int(value) > 255: - raise ValueError(f"Received value {value} at ({y},{x},{rgb}) is out of range. Value should be a number between 0 <= value <= 255") + raise ValueError(f"Received value {value} at ({y},{x},{rgb}) is out of range. Value should be a number between 0 <= value <= 255") \ No newline at end of file From 793c891c6c8ca4e917ad3485e695dbbeec39b3c3 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Wed, 18 Feb 2026 15:58:44 +0100 Subject: [PATCH 35/43] Fixed timeout in wait when error occurs --- pyghthouse/_thread.py | 1 + pyghthouse/connection/wsconnector.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index 8b53b4d..4aca7fb 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -182,4 +182,5 @@ def _disconnect(self): function allows the Pyghthouse thread to exit properly. """ self.connected.clear() + self.ready.set() self.connector.close() \ No newline at end of file diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index 72eb29a..6365685 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -66,7 +66,6 @@ def __init__(self, username:str, token:str, address:str, handler = handler(kwargs) self.handle_message = handler.handle - # TODO: Maybe move functions for state handeling to handler class? self.ws = WebSocketApp(address, on_open=self._on_open, on_message=self._on_message, From 3bb62863683eb8dd1fb5e4e4f15b02f4bd189707 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 19 Feb 2026 14:52:46 +0100 Subject: [PATCH 36/43] Added method keep_running --- examples/huecircle.py | 4 +--- examples/noisefill.py | 4 +--- examples/rainbow.py | 4 +--- examples/rgbfill.py | 4 +--- examples/rgbscan.py | 4 +--- examples/twopoints.py | 4 +--- pyghthouse/ph.py | 15 ++++++++++++++- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/examples/huecircle.py b/examples/huecircle.py index 961ae41..36c89f3 100644 --- a/examples/huecircle.py +++ b/examples/huecircle.py @@ -32,6 +32,4 @@ def callback(): p = Pyghthouse(UNAME, TOKEN, image_callback=callback) print("Starting... use CTRL+C to stop.") p.start() - - while True: - p.wait() + p.keep_running() \ No newline at end of file diff --git a/examples/noisefill.py b/examples/noisefill.py index 1983778..63cc06e 100644 --- a/examples/noisefill.py +++ b/examples/noisefill.py @@ -24,6 +24,4 @@ def image_gen(): p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() - - while True: - p.wait() + p.keep_running() diff --git a/examples/rainbow.py b/examples/rainbow.py index 1cc0a95..8862bfe 100644 --- a/examples/rainbow.py +++ b/examples/rainbow.py @@ -26,6 +26,4 @@ def callback(): p = Pyghthouse(UNAME, TOKEN, image_callback=callback) print("Starting... use CTRL+C to stop.") p.start() - - while True: - p.wait() \ No newline at end of file + p.keep_running() \ No newline at end of file diff --git a/examples/rgbfill.py b/examples/rgbfill.py index a58ce9d..55d7a3f 100644 --- a/examples/rgbfill.py +++ b/examples/rgbfill.py @@ -26,6 +26,4 @@ def image_gen(): p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() - - while True: - p.wait() \ No newline at end of file + p.keep_running() \ No newline at end of file diff --git a/examples/rgbscan.py b/examples/rgbscan.py index 1cd2677..ef63a90 100644 --- a/examples/rgbscan.py +++ b/examples/rgbscan.py @@ -22,6 +22,4 @@ def image_gen(): p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() - - while True: - p.wait() \ No newline at end of file + p.keep_running() \ No newline at end of file diff --git a/examples/twopoints.py b/examples/twopoints.py index b411cee..72dd406 100644 --- a/examples/twopoints.py +++ b/examples/twopoints.py @@ -62,6 +62,4 @@ def callback(self): i = ImageMaker() p = Pyghthouse(UNAME, TOKEN, image_callback=i.callback, frame_rate=60) p.start() - - while True: - p.wait() \ No newline at end of file + p.keep_running() \ No newline at end of file diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 41718d3..3a670ce 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -224,7 +224,20 @@ def wait(self): if not self.ph_thread.ready.wait(self.timeout + self.send_interval + 0.2): raise RuntimeError("Unexpected behaviour. Reached wait timeout before socket timeout.") - + + + def keep_running(self): + """ + Keeps the main thread alive. + + This function blocks the main thread. This will keep the pyghthouse routine running. + + Recommended for the use of callback functions which should run until error or keyboard interrupt. + """ + while self._routine_is_running(): + self.wait() + + def stop(self): """ Stops Pyghthouse routine. From b17ab3162b25d3a80be0a0331abb40eb2edb60e9 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 19 Feb 2026 15:48:44 +0100 Subject: [PATCH 37/43] Removed redundant try ... except statement --- pyghthouse/ph.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 3a670ce..32545f4 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -195,11 +195,7 @@ def set_image(self, image): raise RuntimeError("Cannot set an image before Pyghthouse has started.") # Setting the image - try: - self.canvas.set_image(image) - except: - self.stop() - raise + self.canvas.set_image(image) def wait(self): From d4f4ead36dd92090e88b0464430ce1895c10520a Mon Sep 17 00:00:00 2001 From: Gedusim Date: Thu, 19 Feb 2026 20:04:32 +0100 Subject: [PATCH 38/43] Added changelog --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..00c309d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog + +## 0.4 - Refactoring, Bugfixes and adding new features + +### Features +* **Added method:** Added `Pyghthouse.wait` as a new feature to synchronize with the frame building. Commenly used to ensure building a frame with the previous call of `Pyghthouse.set_image` +* **Added method:** Added `Pyghthouse.keep_running` as a new feature to keep the main thread alive. Useable to let callback functions run until keyboard interrupt +* **Error handeling:** `PyghthouseCanvas.set_image` will now throw more useful errors upon invalid image object + +### Changes +* **main thread check:** The pyghthouse routine will now stop when the main thread has died. To keep the pyghthouse routine running, use `Pyghthouse.keep_running` +* **Wait for start:** `Pyghthouse.start` will now wait until the start sequence is completed + +### Removed +* **Removed dependency:** numpy has been removed as dependency to simplify the image structure +* **Redundant method:** `Pyghthouse.connect` was only intended for internal use and is now combined in `Pyghthouse.start` +* **Redundant behaviour:** signal handler and corresponding method `Pyghthouse._handle_sigint` is now replaced by the main thread check in `PHThread` +* **Unsupported method:** + + removed `Pyghthouse.get_image_raw` + + removed `Pyghthouse.empty_image_raw` + +### Bugfixes +* **Keyboard interrupt:** keyboard interrupt should now stop the whole program instead of only the main thread +* **Missing warning:** `VerbosityLevel.ALL` now prints all messages, instead of only messages with number 200 +* **Error handeling:** upon error inside the library, the Pyghthouse routine will now close properly +* **Fixed image mutations:** added locks for critical sections in `PyghthouseCanvas` to prevent rare image mutations. +* **Fixed connection deadlock:** added timeout to avoid deadlocks upon unexpected connection behaviour + +### Refactored +* **Added documentation** +* **Changed data structure:** Changed data structure of `PyghthouseCanvas` from a 3D numpy array to a 3D python list +* **Better maintainability:** Changed overall code structure to allow easier access to single code pieces +* **Changed internal package structure:** + + moved `PHThread` to the new script `_thread.py` + + moved `PHMessageHandler` into the new script `handler.py` + + moved `REID` into the new script `data.py` and renamed from `REID` to `ReID` + + moved `VerbosityLevel` into the new script `data.py` \ No newline at end of file diff --git a/setup.py b/setup.py index a9514ea..1ac301f 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='pyghthouse', - version='0.3.0', + version='0.4.0', packages=find_packages(where='.'), url='https://github.com/ProjectLighthouseCAU/pyghthouse', license='MIT', From a5a4d43dd2eb4c5a2a4202aff6965e8f25952650 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Fri, 20 Feb 2026 10:14:12 +0100 Subject: [PATCH 39/43] Adjusted comment structure for consistency --- CHANGELOG.md | 1 + pyghthouse/_thread.py | 19 ++++---- pyghthouse/connection/wsconnector.py | 70 +++++++++++----------------- pyghthouse/ph.py | 23 ++++++--- 4 files changed, 55 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c309d..152aa1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * **Error handeling:** `PyghthouseCanvas.set_image` will now throw more useful errors upon invalid image object ### Changes +* **start ... stop:** `Pyghthouse.stop` now does the same as `Pyghthouse.close`. For consistency, we recommend to use the start ... stop pattern for the Pyghthouse routine. * **main thread check:** The pyghthouse routine will now stop when the main thread has died. To keep the pyghthouse routine running, use `Pyghthouse.keep_running` * **Wait for start:** `Pyghthouse.start` will now wait until the start sequence is completed diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index 4aca7fb..ec2b54c 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -114,7 +114,7 @@ def _is_stop(self): def run(self): """ - Starts Pyghthouse-Thread routine. This routine sends images in the selected interval. + Starts Pyghthouse-Thread routine. This routine build and sends frames in the selected interval. """ if self.verbosity == VerbosityLevel.ALL: print("Starting Pyghthouse routine.") @@ -138,20 +138,21 @@ def _send_image(self): """ Build and send current frame. - If error accures in sending process, the connection will be closed. + If error accures process, the connection will be closed. """ if self.callback is not None: - self._get_callback_image() - + self._set_callback_image() bytes_image = self.canvas.get_bytes_image() + # Signal wait function in ph.py to continue self.ready.set() + self.connector.send(bytes_image) - def _get_callback_image(self): + def _set_callback_image(self): """ - Get image from callback function. + Set image from callback function. """ try: image_from_callback = self.callback() @@ -168,7 +169,7 @@ def _connect(self): """ Opens websocket connection. - This function will also wait till the opening process has been finished + This function will also wait until the opening process has been finished """ self.connector.open() self.connected.set() @@ -178,8 +179,8 @@ def _disconnect(self): """ Closes the connection. - Closing the connection also stops the websocket thread. So this - function allows the Pyghthouse thread to exit properly. + Closing the connection also stops the websocket thread. So this function allows the Pyghthouse thread to exit + properly. """ self.connected.clear() self.ready.set() diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index 6365685..d5d4c77 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -13,15 +13,13 @@ class WSConnector: Attributes ---------- connected : Event - Event flag is set to *True* when websocket thread is connected to the - webserver and **send** can be used. + Event flag is set to *True* when websocket thread is connected to the webserver and **send** can be used. error : Event Event flag is set to *True* when error accured and connection is closed. - The flag **error** has a higher priotiy than the **connected** flag. - Meaning when **error** is set to *True*, the connection is closed - even when **connected** can be set to *True*. + The flag **error** has a higher priotiy than the **connected** flag. Meaning when **error** is set to *True*, the + connection is closed even when **connected** can be set to *True*. For further invastigation, check developer notes in **on_error** """ @@ -43,9 +41,8 @@ def __init__(self, username:str, token:str, address:str, verbosity: VerbosityLevel Indicate level of displayed informations. ignore_ssl_cert: bool - Ignores ssl certification when set to *True*. Default ssl - certification is none, so no certificates from the other side are - required (or will be looked at if provided). + Ignores ssl certification when set to *True*. Default ssl certification is none, so no certificates from + the other side are required (or will be looked at if provided). handler: PHMessageHandler Handles the messages received from the network. timeout: int or float @@ -90,13 +87,11 @@ def open(self): Starts websocket thread which will open the websocket connection. - After opening the websocket, the websocket thread sets the - **connected** event flag to *True* and is ready to send data. + After opening the websocket, the websocket thread sets the **connected** event flag to *True* and is ready to + send data. - When an error accured upon opening, the **error** flag will be set to - *True* and **on_error** will be called. In this case, the connection - will be closed again and the **connected** flag will cleared to *False* - again. + When an error accured upon opening, the **error** flag will be set to *True* and **on_error()** will be called. + In this case, the connection will be closed again and the **connected** flag will cleared to *False* again. """ if self.verbosity == VerbosityLevel.ALL: print("Opening websocket connection.") @@ -118,8 +113,7 @@ def send(self, data): """ Send data via websocket connection. - Raises a *WebSocketConnectionClosedException* when no connection is - present. + Raises a *WebSocketConnectionClosedException* when no connection is present. """ if self.error.is_set(): return @@ -203,14 +197,12 @@ def _on_close(self, ws: WebSocketApp, close_status_code, close_msg): ---------------- Developer notes: - When the websocket thread has been started, **on_close** will always be - called, even when an error accured upon opening the connection. This is - a result of the **teardown** function in WebSocketApp. + When the websocket thread has been started, **on_close** will always be called, even when an error accured upon + opening the connection. This is a result of the **teardown()** function in WebSocketApp. - So its possible that **on_close** will be called even when **on_open** - hasn't been called yet. + So its possible that **on_close()** will be called even when **on_open()** hasn't been called yet. - For further invastigation, check the developer notes in **on_error**. + For further invastigation, check the developer notes in **on_error()**. """ if self.verbosity == VerbosityLevel.ALL: print("Connection closed.") @@ -222,36 +214,30 @@ def _on_error(self, ws: WebSocketApp, err: Exception): """ Further handeling of errors outside of WebSocketApp. - This function will save the **exception** and set the flag **error** - to *True*. + This function will save the **exception** and set the flag **error** to *True*. - This function also sets the flag **connected** to *True* to avoid - blocking of **open** when an error accured upon opening the connection. + This function also sets the flag **connected** to *True* to avoid blocking of **open** when an error accured + upon opening the connection. ---------------- Developer notes: - This method is used by WebSocketApp as callback function. - The intend is to signal that an error accured in WebSocketApp and to - allow further error handeling outside of WebSocketApp. + This method is used by WebSocketApp as callback function. The intend is to signal that an error accured in + WebSocketApp and to allow further error handeling outside of WebSocketApp. - Because we use **run_forever** without a parameter for **reconnect**, - we use the standard of *0* from the websocket-client library. - This will always force a teardown of the connection without an attempt - to reconnect upon error. + Because we use **run_forever()** without a parameter for **reconnect()**, we use the standard of *0* from the + websocket-client library. This will always force a teardown of the connection without an attempt to reconnect + upon error. - A teardown of the connection will also call the **_on_close** callback - function, even when **_on_error** has been the called. **_on_close** - will even be called when an error accured on the attempt to open the + A teardown of the connection will also call the **_on_close()** callback function, even when **_on_error()** + has been the called. **_on_close()** will even be called when an error accured on the attempt to open the websocket. - Meaning, **_on_close** can be called even when **_on_open** hasn't been - called yet. + Meaning, **_on_close()** can be called even when **_on_open()** hasn't been called yet. - Do not raise the exception here! + Do not raise an exception here! -------------------------------- - This function will be called from the websocket thread. Raising an - exception here stops the teardown process and could result into - unexpected behaviour. + This function will be called from the websocket thread. Raising an exception here stops the teardown process + and could result into unexpected behaviour. """ if self.verbosity == VerbosityLevel.ALL: diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 32545f4..a4f4cde 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -185,7 +185,7 @@ def set_image(self, image): Sets pyghthouse canvas to a new image. This function overwrites the old image. Only the newest image will be converted to a frame by the pyghthouse - routine. To prevent the loss of an image, use **wait** after a **set_image** call. + routine. To prevent the loss of an image, use **wait()** after **set_image()** call. :param image: A 3D array where every entry is accessed via image[y][x][rgb]. The dimension sizes are 14x28x3, meaning the last entry should be accessed with image[13][27][2]. @@ -194,7 +194,6 @@ def set_image(self, image): if not self._routine_is_running(): raise RuntimeError("Cannot set an image before Pyghthouse has started.") - # Setting the image self.canvas.set_image(image) @@ -208,10 +207,10 @@ def wait(self): **Do not use for fast interactive animations**, like a game, because waiting can result into delayed or ignored inputs! - **set_image** sets the image as fast as possible. On the other hand, + **set_image()** sets the image as fast as possible. On the other hand, the pyghthouse routine creates a frame with the last image set. This will result into losing images, when we create our images faster than the frame rate. - To prevent the loss of an image, we can use **wait** to wait until the current frame has been build. + To prevent the loss of an image, we can use **wait()** to wait until the current frame has been build. """ if not self._routine_is_running(): raise RuntimeError("Cannot wait without a Pyghthouse routine running.") @@ -270,10 +269,11 @@ def set_image_callback(self, image_callback=None): This function is async to the pyghthouse routine, so non-deterministic behaviour is possible. - To prevent image loss, it is recommended to synchronize with the pyghthouse routine by using **wait** for x + To prevent image loss, it is recommended to synchronize with the pyghthouse routine by using **wait()** for x times where x is the amount of images send before calling this function. - When the image_callback is set to *None* + When the image_callback is set to *None*, the Pyghthouse routine will stop using the callback function for + image generation. """ self.ph_thread.callback = image_callback @@ -292,7 +292,16 @@ def _routine_is_running(self): # Deprecated def close(self): - print("Warning: close is a deprecated feature. Use stop instead.") + """ + Same as **stop()**. + + Stops Pyghthouse routine. + + Stops the Pyghthouse routine and closes the websocket connection. All threads used by Pyghthouse will be + stopped in the process. This process can take more time with lower frame rate. + + When Pyghthouse isn't running, no changes will be made. + """ self.stop() # Deprecated From c7ce28cd4f5d4cd30532539081187daf2c34fe90 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 25 May 2026 14:53:54 +0200 Subject: [PATCH 40/43] Cleanup and adjusted comments --- pyghthouse/_thread.py | 14 ++-- pyghthouse/connection/wsconnector.py | 43 +++++------ pyghthouse/data/canvas.py | 38 ++++++++-- pyghthouse/ph.py | 105 +++++++++++---------------- 4 files changed, 104 insertions(+), 96 deletions(-) diff --git a/pyghthouse/_thread.py b/pyghthouse/_thread.py index ec2b54c..35a10fa 100644 --- a/pyghthouse/_thread.py +++ b/pyghthouse/_thread.py @@ -13,9 +13,8 @@ class PHThread(Thread): - Main routine: Loop for sending frames to the webserver. - Ending Phase: Closes the connection and cleanup threads. - In the main routine, this thread will build and send frames from - **canvas**. The time between each frame is indicated by - **send_interval**. + In the main routine, this thread will build and send frames from **canvas**. The time between each frame is + indicated by **send_interval**. Attributes ---------- @@ -36,13 +35,14 @@ class PHThread(Thread): ready : Event Event flag is set to *True* in the send process of a frame. - This flag will always be *True* when connected is *True*, unless the even is unset Can be used for waiting operations. + This flag will always be *True* when connected is *True*, unless the event is unset. + Used for waiting operations (see method `Pyghthouse.wait()`). stop_event : Event Indicates when the Pyghthouse routine should be stopped. error : Event - Event flag is set to *True* when an error accured inside the pyghthouse routine. + Event flag is set to *True* when an error occured inside the pyghthouse routine. """ def __init__(self, send_interval, image_callback, canvas:PyghthouseCanvas, @@ -138,7 +138,7 @@ def _send_image(self): """ Build and send current frame. - If error accures process, the connection will be closed. + Also sets the **ready** flag after successful frame creation. """ if self.callback is not None: self._set_callback_image() @@ -153,6 +153,8 @@ def _send_image(self): def _set_callback_image(self): """ Set image from callback function. + + When an error occures, the connection will be closed and the exeption will be stored for further handeling in `ph.py` """ try: image_from_callback = self.callback() diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index d5d4c77..93d5518 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -16,12 +16,12 @@ class WSConnector: Event flag is set to *True* when websocket thread is connected to the webserver and **send** can be used. error : Event - Event flag is set to *True* when error accured and connection is closed. + Event flag is set to *True* when error occured and connection is closed. The flag **error** has a higher priotiy than the **connected** flag. Meaning when **error** is set to *True*, the connection is closed even when **connected** can be set to *True*. - For further invastigation, check developer notes in **on_error** + For further invastigation, check developer notes in `_on_error()`. """ def __init__(self, username:str, token:str, address:str, @@ -73,10 +73,7 @@ def __init__(self, username:str, token:str, address:str, if ignore_ssl_cert: kwargs = None - self.timeout = 3 - if timeout > 0: - self.timeout = timeout - setdefaulttimeout(self.timeout) + self.set_timeout(timeout) self.thread = Thread(target=self.ws.run_forever, kwargs=kwargs) @@ -90,8 +87,8 @@ def open(self): After opening the websocket, the websocket thread sets the **connected** event flag to *True* and is ready to send data. - When an error accured upon opening, the **error** flag will be set to *True* and **on_error()** will be called. - In this case, the connection will be closed again and the **connected** flag will cleared to *False* again. + When an error occured upon opening, the **error** flag will be set to *True* and `_on_error()` will be called. + In this case, the connection will be closed again and the **connected** flag will be cleared to *False* again. """ if self.verbosity == VerbosityLevel.ALL: print("Opening websocket connection.") @@ -111,7 +108,7 @@ def open(self): def send(self, data): """ - Send data via websocket connection. + Sends data via websocket connection. Raises a *WebSocketConnectionClosedException* when no connection is present. """ @@ -139,9 +136,9 @@ def send(self, data): def close(self): """ - Close websocket connection. + Closes websocket connection. - This function can still be used when no connection is present. + This function can still be used when no connection is present. In this case, nothing will happen. """ if self.verbosity == VerbosityLevel.ALL: print("Closing connection.") @@ -163,13 +160,13 @@ def construct_package(self, payload_data): def set_timeout(self, timeout=10): - self.timeout = 3 + self.timeout = 2.5 if timeout > 0: self.timeout = timeout setdefaulttimeout(self.timeout) - # Functions used by the websocket thread: + ## Functions used by the websocket thread ## def _on_open(self, ws: WebSocketApp): """ @@ -197,12 +194,12 @@ def _on_close(self, ws: WebSocketApp, close_status_code, close_msg): ---------------- Developer notes: - When the websocket thread has been started, **on_close** will always be called, even when an error accured upon - opening the connection. This is a result of the **teardown()** function in WebSocketApp. + When the websocket thread has been started, `_on_close()` will always be called, even when an error occured upon + opening the connection. This is a result of the `teardown()` function in WebSocketApp. - So its possible that **on_close()** will be called even when **on_open()** hasn't been called yet. + So its possible that `_on_close()` will be called even when `_on_open()` hasn't been called yet. - For further invastigation, check the developer notes in **on_error()**. + For further invastigation, check the developer notes in `_on_error()`. """ if self.verbosity == VerbosityLevel.ALL: print("Connection closed.") @@ -216,23 +213,23 @@ def _on_error(self, ws: WebSocketApp, err: Exception): This function will save the **exception** and set the flag **error** to *True*. - This function also sets the flag **connected** to *True* to avoid blocking of **open** when an error accured + This function also sets the flag **connected** to *True* to avoid blocking of **open** when an error occured upon opening the connection. ---------------- Developer notes: - This method is used by WebSocketApp as callback function. The intend is to signal that an error accured in + This method is used by WebSocketApp as callback function. The intend is to signal that an error occured in WebSocketApp and to allow further error handeling outside of WebSocketApp. - Because we use **run_forever()** without a parameter for **reconnect()**, we use the standard of *0* from the + Because we use `run_forever()` without a parameter for `reconnect()`, we use the standard of *0* from the websocket-client library. This will always force a teardown of the connection without an attempt to reconnect upon error. - A teardown of the connection will also call the **_on_close()** callback function, even when **_on_error()** - has been the called. **_on_close()** will even be called when an error accured on the attempt to open the + A teardown of the connection will also call the `_on_close()` callback function, even when `_on_error()` + has been the called. `_on_close()` will even be called when an error occured on the attempt to open the websocket. - Meaning, **_on_close()** can be called even when **_on_open()** hasn't been called yet. + Meaning, `_on_close()` can be called even when `_on_open()` hasn't been called yet. Do not raise an exception here! -------------------------------- diff --git a/pyghthouse/data/canvas.py b/pyghthouse/data/canvas.py index e84df60..700aafe 100644 --- a/pyghthouse/data/canvas.py +++ b/pyghthouse/data/canvas.py @@ -15,11 +15,19 @@ def __init__(self, initial_image=None): def set_image(self, new_image: list) -> True: - + """ + Set the canvas to the **new_image**. + + Throws exeptions when an invalid image object has been given. + + This function is thread-safe. + """ + # Catch invalid image objects self._check_size(new_image) self._check_cells(new_image) self._check_values(new_image) + # Setting the image with self.lock: for y in range(self.IMAGE_SHAPE[0]): @@ -32,7 +40,11 @@ def set_image(self, new_image: list) -> True: def get_bytes_image(self): - + """ + Returns the currently saved image in a bytearray. + + This function is thread-safe. + """ image_bytes = b'' with self.lock: @@ -46,7 +58,11 @@ def get_bytes_image(self): def copy_image(self): + """ + Returns a copy of the currently saved image. + This function is thread-safe. + """ image_copy = [[[0 for k in range(self.IMAGE_SHAPE[2])] for j in range(self.IMAGE_SHAPE[1])] for i in range(self.IMAGE_SHAPE[0])] with self.lock: @@ -60,8 +76,12 @@ def copy_image(self): return image_copy - def _check_size(self, other: list): + ## Internal error checking functions ## + def _check_size(self, other: list): + """ + Check if we received a 3-dimensional list with the correct sizes. + """ try: other_size = (len(other), len(other[0]), len(other[0][0])) @@ -76,8 +96,12 @@ def _check_size(self, other: list): def _check_cells(self, other: list): - - # Catch objects like [[[0,1,2],[0,1,2,3,4,5],[1]],1] + """ + Check for the existence of all expected cells in our image object. Also catches if we have too many cells. + + We need this additional check because `_check_size()` only ensures that we received a 3-dimensional list. + """ + # Catch objects like [[[0,1,2],[0,1,2,3,4,5],[1]],1,...] for y in range(self.IMAGE_SHAPE[0]): try: @@ -99,7 +123,9 @@ def _check_cells(self, other: list): def _check_values(self, other: list): - + """ + Check if all cells have valid numbers. + """ for y in range(self.IMAGE_SHAPE[0]): for x in range(self.IMAGE_SHAPE[1]): for rgb in range(self.IMAGE_SHAPE[2]): diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index a4f4cde..4826ae0 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -5,7 +5,7 @@ from ._thread import PHThread from .connection.wsconnector import VerbosityLevel -# TODO: Adjust description and example for more clarity +# TODO: Adjust example for more clarity class Pyghthouse: """ A Python Lighthouse adapter. @@ -24,23 +24,24 @@ class Pyghthouse: token: str A valid API token belonging to the user name. This is *not* your password. To obtain a token, login to - lighthouse.uni-kiel.de, open the top-left rollover menu and click "API-Token anzeigen". + lighthouse.uni-kiel.de, open the rollover menu "API Token" on the right side and click "Reveal Token" or use + the copy button next to it. address: str (optional, default: "wss://lighthouse.uni-kiel.de/websocket") URI of the WebSocket endpoint. You should not need to change this. frame_rate: float (optional; 0 < frame_rate <= 60, default: 30) - Rate in 1/sec at which frames (images) are automatically sent to the lighthouse server. Also determines how - often the image_callback function is called. + Rate in 1/sec at which frames (images) are automatically sent to the lighthouse server. Also determines the + rate of how often the image_callback function is called. image_callback: function (optional) - A function that takes no arguments and generates a valid lighthouse image (cf Image Format). If set, this - function is called before an image is sent and is used to determine said image. + Takes a function with no arguments and generates a valid lighthouse image. If set, this function is called + to generate an image for sending each frame. The function is guaranteed to be called frame_rate times each second *unless* its execution takes longer than 1/frame_rate seconds, in which case execution will be slowed down accordingly. verbosity: pyghthouse.VerbosityLevel (optional, default: pyghthouse.VerbosityLevel.WARN_ONCE) - How many reply messages from the server are printed to the console. + How many log messages and reply messages from the server are printed to the console. Options: pyghthouse.VerbosityLevel.NONE: All messages are suppressed. @@ -49,7 +50,7 @@ class Pyghthouse: pyghthouse.VerbosityLevel.WARN: Print all warnings. pyghthouse.VerbosityLevel.ALL: - Print all messages. + Print all log and reply messages. Image Format ------------ @@ -81,6 +82,8 @@ class Pyghthouse: ] ] + Example + ------------ The following example creates a Pyghthouse and sets the 10th window of the 11th floor to orange. >>> from pyghthouse import Pyghthouse >>> p = Pyghthouse("YourUsername", "YourToken") @@ -88,43 +91,7 @@ class Pyghthouse: >>> img = Pyghthouse.empty_image() >>> img[3][9] = [255, 127, 0] >>> p.set_image(img) - - Full Example - ------------ - The following program renders a white dot that can be moved by up, down, left and right by typing W, S, A and D, - respectively (and pressing ENTER). - - >>> from pyghthouse import Pyghthouse, VerbosityLevel - >>> UNAME = "YourUsername" - >>> TOKEN = "YourToken" - >>> - >>> def clip(val, min_val, max_val): - >>> if val < min_val: - >>> return min_val - >>> if val > max_val: - >>> return max_val - >>> return val - >>> - >>> x = 0 - >>> y = 0 - >>> p = Pyghthouse(UNAME, TOKEN, verbosity=VerbosityLevel.NONE) - >>> p.start() - >>> while True: - >>> img = p.empty_image() - >>> img[y][x] = [255, 255, 255] - >>> p.set_image(img) - >>> s = input() - >>> for c in s.upper(): - >>> if c == 'A': - >>> x -= 1 - >>> elif c == 'D': - >>> x += 1 - >>> elif c == 'W': - >>> y -= 1 - >>> elif c == 'S': - >>> y += 1 - >>> x = clip(x, 0, 27) - >>> y = clip(y, 0, 13) + >>> p.wait() # ensures sending of the last image set by *set_image* There are more code examples in the git repository (https://github.com/ProjectLighthouseCAU/pyghthouse/tree/master/examples). @@ -152,9 +119,9 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u So we get a total worst-case of: (socket) timeout + send_interval + network delay = total delay - 2.5 + 2.0 + ~0.5 ~ 5.0 + 2.5 + 2.0 + 0.5 ~ 5.0 """ - if frame_rate > 60.0 or frame_rate <= 0.5: + if frame_rate <= 0.5 or frame_rate > 60.0: raise ValueError("Frame rate must be greater than 0.5 and at most 60.") self.send_interval = 1.0 / frame_rate @@ -167,15 +134,18 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u def start(self): """ Starts Pyghthouse routine. + + The pyghthouse routine can only be started once. A RuntimeError will be raised when trying to start an already + running instance of pyghthouse, causing the running instance to be stopped. """ if not self.ph_thread.connected.is_set(): self.ph_thread.start() + # Wait is not optional due to error checking of other functions if not self.ph_thread.connected.wait(self.timeout + 0.3): raise RuntimeError("Unexpected behaviour. Reached wait timeout before socket timeout.") else: - self.stop() raise RuntimeError("Pyghthouse can only be started once.") @@ -187,6 +157,8 @@ def set_image(self, image): This function overwrites the old image. Only the newest image will be converted to a frame by the pyghthouse routine. To prevent the loss of an image, use **wait()** after **set_image()** call. + Raises a RuntimeError upon trying to set an image when no pyghthouse routine is running. + :param image: A 3D array where every entry is accessed via image[y][x][rgb]. The dimension sizes are 14x28x3, meaning the last entry should be accessed with image[13][27][2]. RGB entries only allow values in a range of 0 to 255 (one byte). @@ -203,14 +175,16 @@ def wait(self): This function blocks the called thread until the current frame has been constructed. - Recommended to prevent skipping of frames. - **Do not use for fast interactive animations**, like a game, because - waiting can result into delayed or ignored inputs! + Recommended to prevent skipping/losing of important frames. + + **set_image()** sets the image as fast as possible. On the other hand, the pyghthouse routine creates a frame + every 1/frame_rate seconds with the last image set. + This will result into skipping/losing images, when we create our images faster than the frame rate. + To prevent the loss of an image, we use **wait()** to wait until the current frame has been finalized, so we + can guarantee that the last image set will be send. - **set_image()** sets the image as fast as possible. On the other hand, - the pyghthouse routine creates a frame with the last image set. - This will result into losing images, when we create our images faster than the frame rate. - To prevent the loss of an image, we can use **wait()** to wait until the current frame has been build. + **Do not use for fast interactive games** because waiting can result into delayed or ignored inputs! + For fast interactive games it is recommended to set the image as soon as an input is received. """ if not self._routine_is_running(): raise RuntimeError("Cannot wait without a Pyghthouse routine running.") @@ -258,7 +232,7 @@ def empty_image(): def get_image(self): """ - Returns an image copy of the current canvas image. + Returns a copy of the current canvas image. """ return self.canvas.copy_image() @@ -290,7 +264,8 @@ def _routine_is_running(self): return self.ph_thread.connected.is_set() - # Deprecated + ## Support of old features ## + def close(self): """ Same as **stop()**. @@ -304,10 +279,18 @@ def close(self): """ self.stop() - # Deprecated - def set_frame_rate(self, frame_rate): - print("Warning: set_frame_rate is a deprecated feature and can cause enexpected behaviour.") - if frame_rate > 60.0 or frame_rate <= 0.5: + + def set_frame_rate(self, frame_rate:float): + """ + Set frame_rate to a new value. + + The frame_rate must be greater than 0.5 and at most 60. + + This function executes asynchronous to the pyghthouse routine. It's not possible to guarantee a deterministic + behaviour of this function. + """ + if frame_rate <= 0.5 or frame_rate > 60.0: self.stop() raise ValueError("frame rate must be greater than 0.5 and at most 60.") + self.ph_thread.send_interval = 1.0 / frame_rate \ No newline at end of file From c38d42e9d71e44b3a1d6d4f78c0ee4e336759f62 Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 25 May 2026 16:32:46 +0200 Subject: [PATCH 41/43] Added example for set_image usage --- examples/whitefill.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 examples/whitefill.py diff --git a/examples/whitefill.py b/examples/whitefill.py new file mode 100644 index 0000000..0f41201 --- /dev/null +++ b/examples/whitefill.py @@ -0,0 +1,29 @@ +from pyghthouse import Pyghthouse, VerbosityLevel +from config import UNAME, TOKEN +from time import sleep + + +def main_loop(): + p = Pyghthouse(UNAME, TOKEN, frame_rate=60) + p.start() + + img = p.empty_image() + while True: + for y in range(14): + for x in range(28): + img[y][x] = (255, 255, 255) + p.set_image(img) + p.wait() + + sleep(1) + + for y in range(13, -1, -1): + for x in range(27, -1, -1): + img[y][x] = [0,0,0] + p.set_image(img) + # We skip frames here, but our frame_rate is high enough to hide it + sleep(0.01) + + +if __name__ == '__main__': + main_loop() \ No newline at end of file From 27592dfa805fba9599f94a8024fc925bf9fb30fe Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 25 May 2026 16:33:43 +0200 Subject: [PATCH 42/43] Fixed typos --- example.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/example.py b/example.py index 32630e6..a35a0df 100644 --- a/example.py +++ b/example.py @@ -1,19 +1,20 @@ ''' -This example should give a simple overview on how to use the Pyghthouse. +This example should give a simple overview on how to use Pyghthouse. A generel orientation of what you need: - Import of pyghthouse - Creating an instance of Pyghthouse - Start Pyghthouse routine -- Sending images with either a given function or set_image -- Stop Pyghthouse routine (not needed but recommend) +- Sending images with either a given callback function or by set_image +- Stop Pyghthouse routine (not needed but recommended) More examples can be found in the examples folder. ''' -# Optional: This condition only executes if run as a skript. +# Optional: This condition only executes if run as a script. # Importing this program won't execute this block. if __name__ == '__main__': + from pyghthouse import Pyghthouse import pyghthouse.utils as utils from examples.config import UNAME, TOKEN From 8b339b57e901c152de5d40413c067f9f844b04ce Mon Sep 17 00:00:00 2001 From: Gedusim Date: Mon, 25 May 2026 16:36:00 +0200 Subject: [PATCH 43/43] Adjusted examples README for easier project overview --- examples/README.md | 106 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/examples/README.md b/examples/README.md index 8459534..6f4f532 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,36 +9,94 @@ Some scripts have additional dependencies; Check `README.md` in the repository r If you set up config.py with your username and API token, you won't have to enter them every time you run a script. Be aware that API tokens are only valid for a few days. -##### Beware: -Python imports search for the modules in the current folder and in installed packages. So, if you haven't installed -pyghthouse as a package, you need to change the imports of the files in this folder to work. -In the repository root directory should be a file called `example.py`. This file shows the usage of imports of -pyghthouse without having it installed as a package. -Note: To use the imports the same way as `example.py`, your skript has to be in the same folder. -### Available functions -Here you can find a list of functions containing in this package. +## Ways of programming a pyghthouse script -`from_html(html_color)` -Converts an HTML color string like FF7F00 or #c0ffee to RGB -`from_hsv(h: float, s: float, v: float)` -Converts HSV (float values between 0 and 1) colors to RGB. +The Pyghthouse library has two intended ways of usage: -###### The class `Pyghthouse` with -`Pyghthouse(username: str, token: str, ...)` -Set up the `Pyghthouse` object. Needed arguments are `username` and `token`. Optional arguments and further explanation -can be found in the class definition (see `pyghthouse\ph.py`) +- Passing a callback function at creation with `Pyghthouse(..., callback=function)` or with the method `Pyghthouse.set_image_callback(callback)` -`Pyghthouse.empty_image()` -Creates a black image. -This function can also be called with an instance of the `Pyghthouse` object (*`.empty_image()`*) +- Setting the currently shown canvas with `Pyghthouse.set_image()` -`.set_image(image)` -Sends the given `image`. -`.close()` -Closes the connection. +### Programming with callback -*More functions can be found in `pyghthouse\ph.py`* \ No newline at end of file +The following examples uses callback functions: + +- `huecircle.py` +- `noisefill.py` +- `rainbow.py` +- `rgbfill.py` +- `rgbscan.py` +- `twopoints.py` + +In summary, we first need a callback function to create images when called. After that, we initialize the pyghthouse routine. + +When we now start the pyghthouse routine, images will be generated by the given callback function. Generated images are then send to the server. This will happen in an interval given by the frame_rate. + +The pyghthouse routine is now running asynchronous to the main script. + +**Important to notice** is, that the pyghthouse routine will end when the main script ends. To let a pyghthouse routine run forever (until error or keyboard interrupt), the `keep_running()` method can be used. + + +### Programming with set_image + +The following examples use `set_image`: + +- `movingdot.py` +- `whitefill.py` + +Compared to the callback function, `set_image` is a lot simpler. + +We again need to initialize and start the pyghthouse routine. After that, we only need to call `set_image(new_image)` to send images to the server. + +**Important to notice** is, that `set_image`does not wait until the image is send. This means that we can overwrite the currently set image by setting a new one without waiting for the send. So to ensure important frames to be send, we recommend to use the method `wait()`. + + + +## Pyghthouse package content + + +### The class `pyghthouse.Pyghthouse` with + +- `Pyghthouse(username: str, token: str, ...)`: + + Set up the `Pyghthouse` object. Needed arguments are `username` and `token`. Optional arguments, like `frame_rate` and further explanations can be found in the class definition (see `pyghthouse\ph.py`) + + +- `Pyghthouse.start()` + + Starts the pyghthouse routine and opens the websocket connection. + + +- `Pyghthouse.stop()` + + Stops the pyghthouse routine and closes the websocket connection. + + +- `Pyghthouse.empty_image()` + + A static method which creates a black image. + This function can also be called with an instance of the `Pyghthouse` object (*`.empty_image()`*) + + +- `Pyghthouse.set_image(image)` + + Set the Pyghthouse canvas to `image`. Every `1/frame_rate` seconds, the last image set is send by the pyghthouse routine. + + +*More functions can be found in `pyghthouse\ph.py`* + + +### Utils functions +Here are additional functions in the pyghthouse package. + +- `pyghthouse.utils.from_html(html_color)`: + + Converts an HTML color string like FF7F00 or #c0ffee to RGB + +- `pyghthouse.utils.from_hsv(h: float, s: float, v: float)`: + + Converts HSV (float values between 0 and 1) colors to RGB. \ No newline at end of file