Skip to content

Commit 9f3c2df

Browse files
authored
Release v0.1.3
2 parents 7b2a852 + c2ab301 commit 9f3c2df

File tree

17 files changed

+522
-42
lines changed

17 files changed

+522
-42
lines changed

opsi/lifespan/lifespan.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,24 @@
99
from opsi.manager import Program
1010
from opsi.manager.manager_schema import ModulePath
1111
from opsi.util.concurrency import AsyncThread, ShutdownThread
12-
from opsi.util.networking import choose_port, get_nt_server
12+
from opsi.util.networking import choose_port
1313
from opsi.util.path import join
1414
from opsi.util.persistence import Persistence
1515
from opsi.webserver import WebServer
1616
from opsi.webserver.serialize import import_nodetree
1717

1818
from .webserverthread import WebserverThread
1919

20-
# import optional dependencies
2120
try:
2221
from networktables import NetworkTables
2322

2423
NT_AVAIL = True
2524
except ImportError:
2625
NT_AVAIL = False
2726

27+
28+
# import optional dependencies
29+
2830
try:
2931
from pystemd.systemd1 import Unit
3032

@@ -56,14 +58,6 @@ def register_modules(program, module_path):
5658
program.manager.register_module(ModulePath(moddir, path))
5759

5860

59-
def init_networktables(network):
60-
if network.nt_client:
61-
addr = get_nt_server(network)
62-
NetworkTables.startClient(addr)
63-
else:
64-
NetworkTables.startServer()
65-
66-
6761
class Lifespan:
6862
PORTS = (80, 8000)
6963
NT_AVAIL = NT_AVAIL
@@ -83,6 +77,7 @@ def __init__(self, args, *, catch_signal=False, load_persist=True, timeout=10):
8377
self.persist = Persistence(path=args.persist) if load_persist else None
8478

8579
if catch_signal:
80+
self.signalCount = 0
8681
signal.signal(signal.SIGINT, self.catch_signal)
8782
signal.signal(signal.SIGTERM, self.catch_signal)
8883

@@ -104,8 +99,6 @@ def using_systemd(self):
10499
return self._systemd
105100

106101
def make_threads(self):
107-
if self.NT_AVAIL and self.persist.network.nt_enabled:
108-
init_networktables(self.persist.network)
109102
program = Program(self)
110103

111104
path = opsi.__file__
@@ -146,6 +139,9 @@ def main_loop(self):
146139

147140
def catch_signal(self, signum, frame):
148141
self.shutdown()
142+
self.signalCount += 1
143+
if self.signalCount >= 2:
144+
self.terminate()
149145

150146
def terminate(self):
151147
LOGGER.critical("OpenSight failed to close gracefully! Terminating...")
@@ -158,8 +154,6 @@ def shutdown_threads(self):
158154
self.timer.start()
159155
except RuntimeError:
160156
pass
161-
if NT_AVAIL and self.persist.network.nt_enabled:
162-
NetworkTables.shutdown()
163157
LOGGER.info("Waiting for threads to shut down...")
164158
self.event.set()
165159

opsi/manager/cvwrapper.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import math
2+
13
import cv2
24

35
from .types import *
@@ -71,6 +73,60 @@ def hsv_threshold(img: Mat, hue: "Range", sat: "Range", lum: "Range") -> MatBW:
7173
return cv2.inRange(cv2.cvtColor(img.mat, cv2.COLOR_BGR2HSV), *ranges).view(MatBW)
7274

7375

76+
def v_threshold(img: Mat, val: "Range") -> MatBW:
77+
"""
78+
Args:
79+
img: Mat
80+
val: Value range (min, max) (0 - 255)
81+
Returns:
82+
Black+White Mat
83+
"""
84+
return cv2.inRange(img.mat, val[0], val[1]).view(MatBW)
85+
86+
87+
def hough_circles(
88+
img: Mat,
89+
dp: int,
90+
min_dist: int,
91+
param1: int,
92+
param2: int,
93+
min_radius: int,
94+
max_radius: int,
95+
) -> "Circles":
96+
return cv2.HoughCircles(
97+
img,
98+
method=cv2.HOUGH_GRADIENT,
99+
dp=dp,
100+
minDist=min_dist,
101+
param1=param1,
102+
param2=param2,
103+
minRadius=min_radius,
104+
maxRadius=max_radius,
105+
)
106+
107+
108+
def hough_lines(
109+
img: Mat,
110+
rho: int,
111+
threshold: int,
112+
min_length: int,
113+
max_gap: int,
114+
theta: float = math.pi / 180.0,
115+
) -> "Segments":
116+
return cv2.HoughLinesP(
117+
img,
118+
rho=rho,
119+
theta=theta,
120+
threshold=threshold,
121+
minLineLength=min_length,
122+
maxLineGap=max_gap,
123+
)
124+
125+
126+
def canny(img: Mat, threshold_lower, threshold_upper) -> MatBW:
127+
return cv2.Canny(img, threshold_lower, threshold_upper)
128+
129+
74130
ERODE_DILATE_CONSTS = {
75131
"kernel": None,
76132
"anchor": (-1, -1),
@@ -159,3 +215,11 @@ def joinBW(img1: MatBW, img2: MatBW) -> MatBW:
159215

160216
def greyscale(img: Mat) -> Mat:
161217
return cv2.cvtColor(img.mat, cv2.COLOR_BGR2GRAY).view(Mat).mat
218+
219+
220+
def bgr_to_hsv(img: Mat) -> Mat:
221+
return cv2.cvtColor(img.mat, cv2.COLOR_BGR2HSV)
222+
223+
224+
def abs_diff(img: Mat, scalar: ndarray):
225+
return cv2.absdiff(img, scalar)

opsi/manager/manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ def register_module(self, path: ModulePath):
9999
hook = hooks_tuple[0][1]
100100
self.hooks[info.package] = hook
101101
setattr(hook, "pipeline", self.pipeline)
102+
setattr(hook, "persist", self.pipeline.program.lifespan.persist)
103+
hook.startup()
102104
elif len(hooks_tuple) > 1:
103105
LOGGER.error(f"Only one Hook per module allowed: {info.package}")
104106
return
@@ -118,3 +120,7 @@ def register_module(self, path: ModulePath):
118120
self.funcs[func.type] = func
119121

120122
self.modules[info.package] = ModuleItem(info, funcs)
123+
124+
def shutdown(self):
125+
for hook in self.hooks.values():
126+
hook.shutdown()

opsi/manager/manager_schema.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
32
from dataclasses import Field, dataclass, fields, is_dataclass
43
from typing import (
54
Any,
@@ -170,13 +169,72 @@ def __init__(self, visible=True):
170169
self.visible = visible
171170
self.app = Router()
172171
self.url = "" # will be replaced during webserver init
173-
174-
def cancel_dependents(self):
172+
self.cache = {"skip": {}, "deps": {}}
173+
self.listeners = {"startup": set(), "shutdown": set(), "pipeline_update": set()}
174+
self.lastPipeline = None
175+
176+
def update_cache(self):
177+
if not self.lastPipeline == self.pipeline.nodes:
178+
self.cache = {"skip": {}, "deps": {}}
179+
self.lastPipeline = self.pipeline.nodes
180+
181+
def get_skips(self, node):
182+
self.update_cache()
183+
skip = self.cache["skip"].get(node)
184+
if skip is None:
185+
skip = self.pipeline.get_dependents(node)
186+
self.cache["skip"][node] = skip
187+
return skip
188+
189+
def get_output_deps(self, node, output):
190+
self.update_cache()
191+
192+
if node not in self.cache["deps"]:
193+
self.cache["deps"][node] = {}
194+
195+
deps = self.cache["deps"][node].get(output)
196+
197+
if deps is None:
198+
deps = []
199+
for i in self.pipeline.nodes.values():
200+
for link in i.inputLinks.values():
201+
if link.node is node and link.name == output:
202+
deps.append(i)
203+
self.cache["deps"][node][output] = deps
204+
205+
return deps
206+
207+
def cancel_node(self, node):
175208
try:
176-
self.pipeline.cancel_dependents(self.pipeline.current)
209+
# reset path cache if pipeline has changed
210+
skip = self.get_skips(node)
211+
self.pipeline.cancel_nodes(skip)
177212
except:
178213
raise ValueError("Pipeline not available! Cannot cancel dependents.")
179214

215+
def cancel_current(self):
216+
self.cancel_node(self.pipeline.current)
217+
218+
def cancel_output(self, output: str):
219+
node = self.pipeline.current
220+
deps = self.get_output_deps(node, output)
221+
for dep in deps:
222+
self.cancel_node(dep)
223+
224+
def add_listener(self, event: str, function: callable):
225+
self.listeners[event].add(function)
226+
227+
def remove_listener(self, event: str, function: callable):
228+
self.listeners[event].discard(function)
229+
230+
def startup(self):
231+
for func in self.listeners["startup"]:
232+
func()
233+
234+
def shutdown(self):
235+
for func in self.listeners["shutdown"]:
236+
func()
237+
180238

181239
def ishook(hook):
182240
return isinstance(hook, Hook)

opsi/manager/netdict.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
List[str],
2323
]
2424

25+
_missing = object()
2526

2627
class NetworkDict:
2728
def __init__(self, table: str, networktable: NetworkTablesInstance = NetworkTables):
@@ -65,11 +66,11 @@ def get_subtable(self, table: str) -> "NetworkDict":
6566

6667
# Getters and setters
6768

68-
def get(self, name: str, default: NT_TYPES = None) -> NT_TYPES:
69+
def get(self, name: str, default: NT_TYPES = _missing) -> NT_TYPES:
6970
entry = self._get_entry(name)
7071

7172
if entry is None:
72-
if default is not None: # If a default is specified
73+
if default is not _missing: # If a default is specified
7374
return default
7475

7576
raise KeyError(name)

opsi/manager/pipeline.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import time
33
from dataclasses import fields
44
from itertools import chain
5+
from queue import deque
56
from typing import Any, Dict, List, NamedTuple, Optional, Set, Type
67
from uuid import UUID
7-
from queue import deque
88

99
from toposort import toposort
1010

@@ -142,14 +142,14 @@ def mainloop(self):
142142
# avoid clogging logs with errors
143143
time.sleep(0.5)
144144

145-
def cancel_dependents(self, node):
145+
def get_dependents(self, node):
146146
visited = set()
147147
queue = deque()
148148
path = {}
149149

150150
# First, add all side effect nodes to queue
151-
for id, node in self.nodes.items():
152-
if node.func_type.has_sideeffect:
151+
for id, i in self.nodes.items():
152+
if i.func_type.has_sideeffect:
153153
queue.append(id)
154154

155155
# Then, do a DFS over queue, adding all reachable nodes to visited
@@ -159,26 +159,31 @@ def cancel_dependents(self, node):
159159

160160
if id in visited:
161161
continue
162-
163162
if id == node.id:
164163
break
165164

166165
for input in self.nodes[id].inputLinks.values():
167166
link = input
168-
169167
if link is None:
170168
continue
171-
172169
queue.append(link.node.id)
173170
path[link.node] = self.nodes[id]
174171

175-
# Iterate through path and skip all nodes which were visited
176172
pathTemp = node
173+
skip_nodes = []
177174
while pathTemp is not None:
178175
# Don't skip supplied node, since that would be applied next run
179-
if pathTemp is not node:
180-
pathTemp.skip = True
176+
# if pathTemp is not node:
177+
skip_nodes.append(pathTemp)
181178
pathTemp = path.get(pathTemp)
179+
return skip_nodes
180+
181+
def cancel_nodes(self, nodes):
182+
for node in nodes:
183+
node.skip = True
184+
185+
# def cancel_dependents(self, node, path):
186+
# Iterate through path and skip all nodes which were visited
182187

183188
def create_node(self, func: Type[Function], uuid: UUID):
184189
"""
@@ -214,10 +219,12 @@ def prune_nodetree(self, new_node_ids):
214219
new_node_ids = set(new_node_ids)
215220
removed = old_node_ids - new_node_ids
216221

222+
self.clear()
217223
# remove deleted nodes
218224
for uuid in removed:
219225
try:
220226
self.nodes[uuid].dispose()
227+
del self.adjList[self.nodes[uuid]]
221228
del self.nodes[uuid]
222229
except KeyError:
223230
pass

opsi/manager/program.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ def mainloop(self, shutdown):
4646
task.run() # won't send exceptions because runs in seperate thead
4747
LOGGER.info("Program main loop is shutting down...")
4848
self.pipeline.clear()
49+
self.manager.shutdown()
4950
self.shutdown.clear()

opsi/manager/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ def create(self, min, max):
6161
# Making new classes allows me to do simple equality testing
6262

6363

64+
class Circles(ndarray):
65+
pass
66+
67+
68+
class Segments(ndarray):
69+
pass
70+
71+
72+
class Lines(ndarray):
73+
pass
74+
75+
6476
class Contour(ndarray):
6577
pass
6678

0 commit comments

Comments
 (0)