Skip to content

Commit 0168323

Browse files
committed
helps if I actually include the whole overlay.
1 parent f4fd889 commit 0168323

File tree

13 files changed

+2000
-0
lines changed

13 files changed

+2000
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
!overlay/
2+
!overlay/**
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<p>404 error</p>
5+
</html>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env python3
2+
import http.server
3+
import socketserver
4+
import threading
5+
import signal
6+
import sys
7+
import os
8+
import json
9+
from datetime import datetime
10+
11+
import mimetypes
12+
# Ensure mimetypes are set for common file types
13+
mimetypes.add_type('application/javascript', '.js')
14+
15+
16+
class PVSystemViewerHandler(http.server.BaseHTTPRequestHandler):
17+
def do_GET(self):
18+
script_dir = os.path.dirname(os.path.abspath(__file__))
19+
www_dir = os.path.join(script_dir, 'www') # Path to the www folder
20+
21+
if self.path == '/':
22+
file_path = os.path.join(www_dir, 'systemViewer.html')
23+
else:
24+
# Serve the requested file
25+
file_path = os.path.join(www_dir, self.path.lstrip('/'))
26+
27+
# Check if the file exists and is within the www directory
28+
if os.path.commonpath([www_dir, os.path.abspath(file_path)]) != www_dir or not os.path.isfile(file_path):
29+
self.send_response(404)
30+
self.send_header('Content-type', 'text/html')
31+
self.end_headers()
32+
self.wfile.write(b'<!DOCTYPE html><html lang="en"><p>404 Not Found</p></body></html>')
33+
return
34+
35+
# Determine the MIME type of the file
36+
mime_type, _ = mimetypes.guess_type(file_path)
37+
print(mime_type)
38+
self.send_response(200)
39+
self.send_header('Content-type', mime_type or 'application/octet-stream')
40+
self.end_headers()
41+
42+
# Serve the file content
43+
with open(file_path, 'rb') as f:
44+
self.wfile.write(f.read())
45+
46+
def log_message(self, format, *args):
47+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
48+
print(f"[{timestamp}] {format % args}")
49+
50+
class PVSystemViewerServer:
51+
def __init__(self, port=5804):
52+
self.port = port
53+
self.httpd = None
54+
self.server_thread = None
55+
self.shutdown_event = threading.Event()
56+
57+
def start(self):
58+
try:
59+
self.httpd = socketserver.TCPServer(("", self.port), PVSystemViewerHandler)
60+
# Allow socket reuse to prevent "Address already in use" errors
61+
self.httpd.allow_reuse_address = True
62+
63+
self.server_thread = threading.Thread(target=self.httpd.serve_forever)
64+
self.server_thread.daemon = True
65+
self.server_thread.start()
66+
print(f"PhotonVision System Viewer server started on port {self.port}")
67+
return True
68+
except Exception as e:
69+
print(f"Failed to start server: {e}")
70+
return False
71+
72+
def stop(self):
73+
print("Stopping PhotonVision System Viewer server...")
74+
self.shutdown_event.set()
75+
76+
if self.httpd:
77+
self.httpd.shutdown()
78+
self.httpd.server_close()
79+
80+
if self.server_thread and self.server_thread.is_alive():
81+
self.server_thread.join(timeout=2)
82+
83+
print("PhotonVision System Viewer server stopped")
84+
85+
# Global server instance for signal handler
86+
server_instance = None
87+
88+
def signal_handler(signum, frame):
89+
print(f"\nReceived signal {signum}, stopping server...")
90+
if server_instance:
91+
server_instance.stop()
92+
sys.exit(0)
93+
94+
def main():
95+
global server_instance
96+
97+
# Register signal handlers
98+
signal.signal(signal.SIGINT, signal_handler)
99+
signal.signal(signal.SIGTERM, signal_handler)
100+
101+
server_instance = PVSystemViewerServer(5804)
102+
103+
if server_instance.start():
104+
print("PhotonVision System Viewer service is running. Managed by systemd.")
105+
try:
106+
server_instance.shutdown_event.wait()
107+
except KeyboardInterrupt:
108+
signal_handler(signal.SIGINT, None)
109+
else:
110+
print("Failed to start PhotonVision System Viewer service")
111+
sys.exit(1)
112+
113+
if __name__ == "__main__":
114+
main()
Lines changed: 23 additions & 0 deletions
Loading
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/////////////////////////////////////////////////////////////////////////
2+
// Calibration - wrapper around NT4 to specifically extract cal information
3+
// and allow clients to interact with one or more calibrations
4+
//
5+
// Mirroring (I assume) NT4 architecture, it's heavily callback driven
6+
/////////////////////////////////////////////////////////////////////////
7+
8+
import { NT4_Client } from "./nt4.js";
9+
import { CalObj } from "./calobj.js";
10+
11+
export class NT4_CalInf {
12+
13+
///////////////////////////////////////
14+
// Public API
15+
16+
constructor(onNewCalAdded_in, //Gets called when a new calibration is available
17+
onCalValueUpdated_in, //Gets called when one calibration's value has changed.
18+
onConnect_in, //Gets called once client completes initial handshake with server
19+
onDisconnect_in) { //Gets called once client detects server has disconnected
20+
this.onNewCalAdded = onNewCalAdded_in;
21+
this.onCalValueUpdated = onCalValueUpdated_in;
22+
this.onConnect = onConnect_in;
23+
this.onDisconnect = onDisconnect_in;
24+
25+
this.allCals = new Map();
26+
27+
28+
this.nt4Client = new NT4_Client(window.location.hostname,
29+
this.topicAnnounceHandler.bind(this),
30+
this.topicUnannounceHandler.bind(this),
31+
this.valueUpdateHandler.bind(this),
32+
this.onConnect.bind(this),
33+
this.onDisconnect.bind(this)
34+
);
35+
36+
this.nt4Client.subscribeAllSamples(["/Calibrations"]);
37+
this.nt4Client.ws_connect();
38+
39+
}
40+
41+
//Submit a new calibration value
42+
setCalibrationValue(name, value){
43+
var valTopic = this.calNameToTopic(name, "desValue");
44+
this.nt4Client.addSample(valTopic, this.nt4Client.getServerTime_us(), value);
45+
}
46+
47+
///////////////////////////////////////////
48+
// Internal implementations
49+
50+
topicAnnounceHandler(topic){
51+
52+
if(this.isCalTopic(topic, "curValue")){
53+
var calName = this.topicToCalName(topic);
54+
55+
//we got something new related to calibrations...
56+
57+
//ensure we've got an object for this cal
58+
if(!this.allCals.has(calName)){
59+
var newCal = new CalObj();
60+
newCal.name = calName;
61+
newCal.units = topic.properties.units;
62+
newCal.min = topic.properties.min_cal;
63+
newCal.max = topic.properties.max_cal;
64+
newCal.default = topic.properties.default_val;
65+
66+
if(newCal.min == null){
67+
newCal.min = -Infinity;
68+
}
69+
70+
if(newCal.max == null){
71+
newCal.max = Infinity;
72+
}
73+
74+
//Publish a desVal topic for every curVal topic
75+
var desValTopic = this.nt4Client.publishNewTopic(this.calNameToTopic(calName, "desValue"), topic.type);
76+
this.nt4Client.setProperties(desValTopic, false, true);
77+
78+
this.allCals.set(calName, newCal);
79+
this.onNewCalAdded(newCal);
80+
81+
}
82+
}
83+
}
84+
85+
topicUnannounceHandler(topic){
86+
if(this.isCalTopic(topic, "curValue")){
87+
var oldTopic = this.allCals.get(this.topicToCalName(topic));
88+
this.allCals.delete(this.topicToCalName(topic));
89+
//TODO call user hook
90+
//TODO unpublish desired
91+
}
92+
}
93+
94+
95+
valueUpdateHandler(topic, timestamp, value){
96+
if(this.isCalTopic(topic, "curValue")){
97+
var calName = this.topicToCalName(topic);
98+
var updatedCal = this.allCals.get(calName);
99+
updatedCal.value = value;
100+
this.onCalValueUpdated(updatedCal);
101+
}
102+
}
103+
104+
105+
/////////////////////////////////////////////////
106+
// Helper Utiltiies
107+
108+
calNameToTopic(name, suffix){
109+
return "/Calibrations/" + name + "/" + suffix;
110+
}
111+
112+
isCalTopic(topic, suffix){
113+
if(suffix === undefined){
114+
suffix = ".*";
115+
}
116+
var replace = "\/Calibrations\/[a-zA-Z0-9 \._]+\/"+suffix;
117+
var re = new RegExp(replace,"g");
118+
return re.test(topic.name);
119+
}
120+
121+
topicToCalName(topic){
122+
var replace = "\/Calibrations\/([a-zA-Z0-9 \._]+)\/";
123+
var re = new RegExp(replace,"g");
124+
var arr = re.exec(topic.name);
125+
return arr[1];
126+
}
127+
128+
129+
130+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export class CalObj {
2+
name = null;
3+
units = null;
4+
min = null;
5+
max = null;
6+
default = null;
7+
value = null;
8+
9+
isFullyAnnounced() {
10+
return this.name != null &&
11+
this.units != null &&
12+
this.min != null &&
13+
this.max != null &&
14+
this.default != null &&
15+
this.value != null;
16+
}
17+
}

0 commit comments

Comments
 (0)