-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.py
245 lines (200 loc) · 8.29 KB
/
server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import socket
from numpy import indices
from rpi_ws281x import PixelStrip, Color, ws
import threading
import time
import math
from runOnServerStart import startScript
from errno import ENETUNREACH
# server vars
PORT = 1337
SERVER = None # will be defined later
ADDR = None # will be defined later
DISCONNECT_MESSAGE = b"DISCONNECT"
KEEPALIVE_MESSAGE = b"KEEPALIVE"
DEFAULT_BUFFER_SIZE = 128
# LED vars
LED_COUNT = 30
LED_PIN = 18
LED_FREQ = 800000
LED_DMA = 10
LED_BRIGHTNESS = 255
LED_INVERT = False
LED_CHANNEL = 0
LED_STRIP = ws.SK6812W_STRIP
# LED init
strip = PixelStrip(LED_COUNT, LED_PIN, LED_FREQ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL, LED_STRIP)
strip.begin()
# simple indicator LED to show that the server is starting
indicatorLedID = 0
strip.setPixelColor(indicatorLedID, Color(0, 0, 255))
strip.show()
time.sleep(0.2)
strip.setPixelColor(indicatorLedID, 0)
strip.show()
# attempt to get device local IP until successful
# This will both give us the IP (if needed), and make the server wait until networking is up
tempSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
try:
tempSock.connect(("1.1.1.1", 80)) # doesn't need to be able to connect, just needs to think it can. can be any non-loopback IP
strip.setPixelColor(indicatorLedID, Color(0, 255, 0))
strip.show()
time.sleep(0.5)
break
except IOError as e:
if e.errno == ENETUNREACH:
strip.setPixelColor(indicatorLedID, Color(0, 0, 255))
strip.show()
time.sleep(0.4)
strip.setPixelColor(indicatorLedID, 0)
strip.show()
time.sleep(0.1)
else:
# something else broke
strip.setPixelColor(indicatorLedID, Color(255, 0, 0))
strip.show()
raise
SERVER = tempSock.getsockname()[0]
print(f"Device IP is {SERVER}")
ADDR = (SERVER, PORT)
# server init
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(("", PORT)) # bind to anything so it can be accessed from network or localhost
def decodeAndApplyCommand(command, verbose=False):
# command must be a bytes object
# check header integrity
if int(command[:24].hex(), 16):
if verbose: print("Malformed Header")
return 1 # error code 1 (incorrect header)
# as this function does not handle headers, discard it
command = command[24:]
# the first ceil(LED_COUNT/8) bytes are writeMask bytes, so extract them
writeMaskByteCount = math.ceil(LED_COUNT/8)
writeMaskBytes = command[:writeMaskByteCount]
command = command[writeMaskByteCount:] # remove writeMask from command
writeMaskStr = bin(int(writeMaskBytes.hex(), 16)).lstrip("0b")
# check if writemask needs fixing
if len(writeMaskStr) < LED_COUNT:
# bin() removed leading zeros, readd them
numOfLeadingZerosNeeded = LED_COUNT - len(writeMaskStr)
writeMaskStr = ("0" * numOfLeadingZerosNeeded) + writeMaskStr
if verbose: print(f"writeMaskStr is {writeMaskStr}")
# find expected length of message
numOfExpectedColourDatapoints = writeMaskStr.count("1")
if verbose: print(f"Expecting a length of {numOfExpectedColourDatapoints}")
# check length of message
if len(command)/4 < numOfExpectedColourDatapoints:
if verbose: print("Message too short")
return 3 # error code 3 (message is too short)
elif len(command)/4 > numOfExpectedColourDatapoints:
if verbose: print("Message is too long")
return 2 # error code 2 (message is too long)
# loop over the remaining bytes, extracting in groups of 4 and applying
colourDataReadPoint = 0
for i in range(0, LED_COUNT):
if int(writeMaskStr[i]):
#print(command[4*colourDataReadPoint:4*(colourDataReadPoint+1)].hex())
if verbose: print(f"Setting pixel {i} to colour {hex(int(command[4*colourDataReadPoint:4*(colourDataReadPoint+1)].hex(), 16))}")
strip.setPixelColor(i, int(command[4*colourDataReadPoint:4*(colourDataReadPoint+1)].hex(), 16))
colourDataReadPoint += 1
# apply changes to strip
strip.show()
if verbose: print("Command execution succeeded")
# return 0 for success I guess?
time.sleep(0.001)
return 0
def handle_client(conn, addr): # to be run in a seperate thread for every connection
bufferSize = DEFAULT_BUFFER_SIZE
# send client number of LEDs connected
conn.send(LED_COUNT.to_bytes(2, byteorder="big"))
print(f"Informed client {addr} of LED count {LED_COUNT}")
# client's first message must define buffer size
while True:
bufferSizeExchange = conn.recv(bufferSize)
if len(bufferSizeExchange) != 0:
bufferSize = int.from_bytes(bufferSizeExchange, byteorder="big")
print(f"Client {addr} has set buffer size to {bufferSize}")
# confirm buffer size to client
conn.send(bufferSize.to_bytes(2, byteorder="big"))
break
# start timeout by taking current time
lastMessageTime = time.time()
# listen for client commands
while True:
# end connection if server is shutting down
if shutdownEvent.is_set():
# tell client to disconnect
conn.send(b"S_SHUTDOWN")
print(f"Connection to {addr} has been ended on thread {threading.get_ident()}")
# exit thread
break
skipExec = False
conn.settimeout(15)
try:
command = conn.recv(bufferSize)
except socket.timeout:
command = "" # set command to be empty so the if statement skips and we just wait again
except ConnectionResetError:
# I have absolutely no clue why but sometimes when I stop a client program it throws this error in the server
# so I'm just catching it and calling it "disconnecting ungracefully" because that sounds like I know what
# I'm doing
print(f"Client {addr} has disconnected ungracefully")
break
if len(command) != 0:
# reset timeout
lastMessageTime = time.time()
# test if the message is a disconnect message
if command.startswith(DISCONNECT_MESSAGE):
print(f"Client {addr} has disconnected gracefully")
break # exit completely, allowing function to end
# test if the message is a keepAlive
if command.startswith(KEEPALIVE_MESSAGE):
skipExec = True # skip the current exec and wait again for an actual command
if not skipExec:
try:
# sends the error codes in the function to the client, currently unused
conn.send(decodeAndApplyCommand(command).to_bytes(1, byteorder="big"))
except:
print(f"Failed to parse in connection to {addr}")
conn.send((4).to_bytes(1, byteorder="big")) # error codes, currently unused
else:
if time.time() - lastMessageTime >= 30:
print(f"Client {addr} has timed out")
break
time.sleep(0.001)
def runServer():
server.listen(5)
while True:
conn, addr = server.accept()
# server.accept() is blocking, so spin off a thread to handle the new connection
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.start()
print(f"{threading.activeCount() - 1} active connections")
def runStartupScript():
time.sleep(0.2) # wait a moment for the server to finish starting
startScript() # run init script
print("Start Script Finished")
print("Server Starting")
startup = threading.Thread(target=runStartupScript)
startup.start()
shutdownEvent = threading.Event()
try:
runServer()
except KeyboardInterrupt:
print("Server is shutting down")
for i in range(LED_COUNT): # clear all pixels
strip.setPixelColor(i, 0)
strip.setPixelColor(indicatorLedID, Color(0, 0, 255))
strip.show()
shutdownEvent.set()
# wait for all threads to stop
while threading.active_count() > 1:
time.sleep(0.01)
strip.setPixelColor(indicatorLedID, Color(0, 255, 0))
strip.show()
time.sleep(0.5)
strip.setPixelColor(indicatorLedID, 0)
strip.show()
print("Server has shut down")