forked from luc-github/ESP3D-WEBUI
-
Notifications
You must be signed in to change notification settings - Fork 29
/
fluidnc-web-sim.py
382 lines (324 loc) · 11 KB
/
fluidnc-web-sim.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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env python3
# Usage: ./fluidnc-web-sim.py [ FluidNC_IP_address ]
# Then browse to localhost:8080
# If the FluidNC_IP_address is omitted, fluidnc-web-sim will try to
# get the FluidNC IP address by resolving the address fluidnc.local
# This is a simple web server that serves index.html from a local file,
# and forwards other requests to a FluidNC system.
# It lets you test new WebUI builds without uploading index.html.gz to
# the FluidNC MCU.
# You might have to install some Python packages first, like zeroconf and flask
# For example "pip install flask zeroconf"
# It serves index.html directly from dist/index.html , which is where the
# WebUI build process puts that file.
#
# There is an alternate mode where this program can do some things directly,
# without needing a running FluidNC instance. To enable the alternate mode,
# set the 'proxy' variable below to False.
#
# When proxy is False, this program serves file from the "test_files/" directory.
# Its subdirectories 'localfs/' and 'sd/' contain files that are presented as
# though they were in the FluidNC FLASH and SD card filesystems.
#
# The non-proxy-mode emulation is incomplete. In particular, the following things
# are unimplemented:
# - File upload
# - OTA
# - changing settings
# - Running GCode - or anything that involves making FluidNC do any motion things
# But you can load the UI into your browser and do file downloads and whatnot
proxy = True
import asyncio
import os
import subprocess
import sys
import time
import websockets
import json
import warnings
import shutil
import requests
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange
try:
from flask import Flask, request, Response, send_from_directory
except:
print("flash module missing; try 'pip3 install flask'")
sys.exit(1)
def resolve_mdns(hostname):
resolved_addresses = []
class MyListener:
def update_service(self, zeroconf, type, name):
pass
def remove_service(self, zeroconf, type, name):
pass
def add_service(self, zeroconf, type, name):
info = zeroconf.get_service_info(type, name)
if info:
resolved_addresses.append(info.parsed_addresses()[0])
zeroconf = Zeroconf()
listener = MyListener()
browser = ServiceBrowser(zeroconf, '_http._tcp.local.', listener)
try:
zeroconf.get_service_info('_http._tcp.local.', hostname + '._http._tcp.local.')
finally:
zeroconf.close()
return resolved_addresses
fluidnc_ip = ''
def set_fluidnc_ip():
global fluidnc_ip
if len(sys.argv) == 2:
fluidnc_ip = sys.argv[1]
print( "Proxying to FluidNC at", fluidnc_ip)
return
addresses = resolve_mdns('fluidnc.local')
if len(addresses) == 0:
print('fluidnc.local does not resolve; specify the FluidNC IP address on the command line')
sys.exit(1)
if len(addresses) == 1:
fluidnc_ip = addresses[0]
print("Proxying to FluidNC at", fluidnc_ip)
else:
print('fluidnc.local resolves to multiple addresses; specify the FluidNC IP address on the command line')
sys.exit(1)
if proxy:
set_fluidnc_ip()
def fluidnc_url():
return 'http://' + fluidnc_ip + '/'
def fluidnc_websocket():
if proxy:
return '81:' + fluidnc_ip
return '8081:localhost'
# 8081:localhost
python_path = sys.executable
script_path = os.path.realpath(__file__)
domain = 'localhost'
http_port = '8080'
ws_port = '8081'
### Flask Stuff
app = Flask(__name__)
test_files = 'test_files'
def file_entry(directory, filename):
fullpath = os.path.join(directory, filename)
if os.path.isfile(fullpath):
size = os.path.getsize(fullpath)
else:
size = -1
return {
'name': filename,
'shortname': filename,
'size': size,
'datetime':''
}
def format_bytes(n):
if n < 1024:
return str(n) + ' B'
n = n / 1024
if n < 1024:
return str(round(n,2)) + ' KB'
n = n / 1024
if n < 1024:
return str(round(n,2)) + ' MB'
n = n / 1024
if n < 1024:
return str(round(n,2)) + ' GB'
n = n / 1024
return str(round(n,2)) + ' TB'
def make_files_list(fs, subdir, status):
print("mfl", fs, subdir)
if len(subdir) and subdir[0] == '/':
subdir = subdir[1:]
directory = os.path.join(test_files, fs, subdir)
print("dir", directory)
diskusage = shutil.disk_usage(directory)
disktotalsize = diskusage.total
diskusedsize = diskusage.used
files = {
'files': [],
'path': '',
'total': format_bytes(disktotalsize),
'used': '',
'occupation': 0,
'status': status
}
dirusedsize = 0
for filename in os.listdir(directory):
entry = file_entry(directory, filename)
size = entry['size']
files['files'].append(entry)
if size != -1:
dirusedsize = dirusedsize + size
occupation = 100 * diskusedsize / disktotalsize
files['used'] = format_bytes(diskusedsize)
files['occupation'] = int(round(occupation, 0))
return json.dumps(files)
sdfiles = '{"files":[{"name":"localfs","shortname":"localfs","size":"-1","datetime":""},{"name":"t2.nc","shortname":"t2.nc","size":"597","datetime":""},{"name":"SchluesselAutoX.nc","shortname":"SchluesselAutoX.nc","size":"61954","datetime":""}],"path":"","total":"14.83 GB","used":"384.00 KB","occupation":"0","status":"Ok"}'
gresp = "[GC:G1 G54 G17 G21 G90 G94 M5 M9 T0 F1000 S0]"
esp800resp = 'FW version: FluidNC v3.6.7 (Devt-5692a7c1-dirty) # FW target:grbl-embedded # FW HW:Direct SD # primary sd:/sd # secondary sd:none # authentication:no # webcommunication: Sync: ' + fluidnc_websocket() + ' # hostname:fluidnc # axis:3'
esp400resp = '''
{"EEPROM":[
{"F":"nvs",
"P":"Firmware/Build",
"H":"Firmware/Build",
"T":"S",
"V":"",
"S":"20",
"M":"0"
},
{"F":"tree",
"P":"/board",
"H":"/board",
"T":"S",
"V":"None",
"S":"255",
"M":"0"
}
]
}
'''
connection_list = []
CONNECTIONS = set()
def obsolete():
plainval = request.args.get('plain')
def do_proxy(request):
print("url1", request.url);
# Forward the request to the desired endpoint
url = request.url.replace(request.host_url, fluidnc_url())
print("url2", url);
try:
response = requests.request(
method=request.method,
url=url,
headers=request.headers,
data=request.get_data(),
cookies=request.cookies,
stream=True
)
# Create a Flask response with the remote server's response data
flask_response = response.raw
# Set response headers
headers = [(name, value) for name, value in response.headers.items()]
flask_response.headers.extend(headers)
return flask_response
except requests.exceptions.RequestException as e:
# Handle any errors that occur during the request
error_message = {'error': str(e)}
return error_message, 500
@app.route('/command')
def do_command():
plainval = request.args.get('plain')
# Respond directly to ESP800 instead of proxying it to FluidNC, because
# we want to provide the correct websocket address
if plainval == '[ESP800]':
return esp800resp
if proxy:
return do_proxy(request)
# If not proxying, respond to a few commands
if plainval != None:
if plainval == '[ESP400]':
return esp400resp
commandtextval = request.args.get('commandText')
if commandtextval != None:
print("commandText:", commandtextval)
if commandtextval == '$G':
if len(CONNECTIONS):
wsock = CONNECTIONS[0]
wsock.send(gresp)
return ""
def handle_files(fs, request):
if proxy:
return do_proxy(request)
method = request.method
action = request.args.get('action')
filename = request.args.get('filename')
path = request.args.get('path')
print("handle_files", method, action, filename, path)
if path == None:
path = ''
print("handle_files", action, filename, path)
if filename == None or filename == 'all':
filename = ''
if len(path) and path[0] == '/':
path = path[1:]
if path == '':
filepath = filename
else:
filepath = path + '/' + filename
localpath = os.path.join(test_files, fs, filepath)
if method == 'POST':
status = 'Upload not implemented'
else:
status = 'Ok'
if action == 'delete':
print("Deleting", filepath)
try:
os.remove(localpath)
except:
status = 'Cannot delete ' + filepath
elif action == 'deletedir':
try:
shutil.rmtree(localpath)
except:
status = 'Cannot delete ' + filepath
elif action == 'createdir':
try:
os.mkdir(localpath)
except:
status = 'Cannot create ' + filepath
if request.args.get('dontlist') == None:
return make_files_list(fs, path, status)
return ""
@app.route('/upload', methods=['GET', 'POST'])
def upload():
return handle_files('sd', request)
@app.route('/files', methods=['GET', 'POST'])
def do_files():
return handle_files('localfs', request)
@app.route('/<path:filename>', methods=['GET'])
def do_get_file(filename):
if proxy:
return do_proxy(request)
print("/ ", filename)
if filename.startswith('SD/'):
filename = filename[3:]
return send_from_directory(os.path.join(test_files, 'sd'), filename)
return send_from_directory(os.path.join(test_files, 'localfs'), filename)
@app.route('/', methods=['GET'])
def index():
return send_from_directory('dist', 'index.html')
### Websocket stuff
async def register(websocket):
CONNECTIONS.add(websocket)
async def unregister(websocket):
CONNECTIONS.remove(websocket)
async def notify_users(message, websocket):
global connection_list
for connection in CONNECTIONS:
if connection != websocket:
connection_list.append(connection)
await asyncio.wait([
connection.send(message) for connection in connection_list
])
async def message_control(websocket, path):
await register(websocket)
try:
await websocket.send("Connected")
async for message in websocket:
await notify_users(message, websocket)
finally:
await unregister(websocket)
if not proxy:
if len(sys.argv) == 2:
if sys.argv[1] == 'run_socket':
start_server = websockets.serve(message_control, domain, ws_port, subprotocols=['arduino'])
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
else:
time.sleep(1)
print("Triggering websocket server")
return_value = (subprocess.Popen([
python_path,
script_path,
'run_socket'
]))
print("Starting flask server")
app.run(port=http_port)