799 lines
25 KiB
Python
799 lines
25 KiB
Python
import os
|
|
import json
|
|
import tornado
|
|
import sockjs.tornado
|
|
import datetime
|
|
import subprocess
|
|
import socket
|
|
from tornado.web import HTTPError
|
|
from tornado import gen
|
|
from tornado.escape import url_unescape
|
|
import re
|
|
import bbctrl
|
|
from urllib.request import urlopen
|
|
import iw_parse
|
|
import io
|
|
import zipfile
|
|
import shutil
|
|
|
|
def call_get_output(cmd):
|
|
p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
|
|
s = p.communicate()[0].decode('utf-8')
|
|
if p.returncode: raise HTTPError(400, 'Command failed')
|
|
return s
|
|
|
|
|
|
def get_username():
|
|
return call_get_output(['getent', 'passwd', '1001']).split(':')[0]
|
|
|
|
|
|
def set_username(username):
|
|
if subprocess.call(['usermod', '-l', username, get_username()]):
|
|
raise HTTPError(400, 'Failed to set username to "%s"' % username)
|
|
|
|
|
|
def check_password(password):
|
|
# Get current password
|
|
s = call_get_output(['getent', 'shadow', get_username()])
|
|
current = s.split(':')[1].split('$')
|
|
|
|
# Check password type
|
|
if len(current) < 2 or current[1] != '1':
|
|
raise HTTPError(401, "Password invalid")
|
|
|
|
# Check current password
|
|
cmd = ['openssl', 'passwd', '-salt', current[2], '-1', password]
|
|
s = call_get_output(cmd).strip()
|
|
|
|
if s.split('$') != current: raise HTTPError(401, 'Wrong password')
|
|
|
|
|
|
|
|
class RebootHandler(bbctrl.APIHandler):
|
|
def put_ok(self):
|
|
self.get_ctrl().lcd.goodbye('Rebooting...')
|
|
subprocess.Popen(['reboot'])
|
|
|
|
class ShutdownHandler(bbctrl.APIHandler):
|
|
def put_ok(self):
|
|
subprocess.Popen(['shutdown','-h','now'])
|
|
|
|
|
|
class LogHandler(bbctrl.RequestHandler):
|
|
def get(self):
|
|
with open(self.get_ctrl().log.get_path(), 'r') as f:
|
|
self.write(f.read())
|
|
|
|
|
|
def set_default_headers(self):
|
|
fmt = socket.gethostname() + '-%Y%m%d.log'
|
|
filename = datetime.date.today().strftime(fmt)
|
|
self.set_header('Content-Disposition', 'filename="%s"' % filename)
|
|
self.set_header('Content-Type', 'text/plain')
|
|
|
|
|
|
class MessageAckHandler(bbctrl.APIHandler):
|
|
def put_ok(self, id):
|
|
self.get_ctrl().state.ack_message(int(id))
|
|
|
|
|
|
class BugReportHandler(bbctrl.RequestHandler):
|
|
def get(self):
|
|
import tarfile, io
|
|
|
|
buf = io.BytesIO()
|
|
tar = tarfile.open(mode = 'w:bz2', fileobj = buf)
|
|
|
|
def check_add(path, arcname = None):
|
|
if os.path.isfile(path):
|
|
if arcname is None: arcname = path
|
|
tar.add(path, self.basename + '/' + arcname)
|
|
|
|
def check_add_basename(path):
|
|
check_add(path, os.path.basename(path))
|
|
|
|
ctrl = self.get_ctrl()
|
|
path = ctrl.log.get_path()
|
|
check_add_basename(path)
|
|
for i in range(1, 8):
|
|
check_add_basename('%s.%d' % (path, i))
|
|
check_add_basename('/var/log/syslog')
|
|
check_add('config.json')
|
|
check_add(ctrl.get_upload(ctrl.state.get('selected', '')))
|
|
|
|
tar.close()
|
|
|
|
self.write(buf.getvalue())
|
|
|
|
|
|
def set_default_headers(self):
|
|
fmt = socket.gethostname() + '-%Y%m%d-%H%M%S'
|
|
self.basename = datetime.datetime.now().strftime(fmt)
|
|
filename = self.basename + '.tar.bz2'
|
|
self.set_header('Content-Disposition', 'filename="%s"' % filename)
|
|
self.set_header('Content-Type', 'application/x-bzip2')
|
|
|
|
|
|
class HostnameHandler(bbctrl.APIHandler):
|
|
def get(self): self.write_json(socket.gethostname())
|
|
|
|
def put(self):
|
|
if self.get_ctrl().args.demo:
|
|
raise HTTPError(400, 'Cannot set hostname in demo mode')
|
|
|
|
if 'hostname' in self.json:
|
|
if subprocess.call(['/usr/local/bin/sethostname',
|
|
self.json['hostname'].strip()]) == 0:
|
|
self.write_json('ok')
|
|
return
|
|
|
|
raise HTTPError(400, 'Failed to set hostname')
|
|
|
|
|
|
class NetworkData(bbctrl.APIHandler):
|
|
|
|
def get(self):
|
|
try:
|
|
ipAddresses = subprocess.check_output(
|
|
"ip -4 addr | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'", shell=True).decode().split()
|
|
ipAddresses.remove("127.0.0.1")
|
|
regex = re.compile(r'/255$/')
|
|
filtered = [i for i in ipAddresses if not regex.match(i)]
|
|
ipAddresses = filtered[0]
|
|
except:
|
|
ipAddresses = "Not Connected"
|
|
try:
|
|
wifi = subprocess.check_output(
|
|
"sudo iw dev wlan0 info | grep ssid", shell=True).decode().split()
|
|
wifi.pop(0)
|
|
wifiName = " ".join(wifi)
|
|
except:
|
|
wifiName = "not connected"
|
|
self.write_json({
|
|
'ipAddresses': ipAddresses,
|
|
'wifi': wifiName
|
|
})
|
|
|
|
class NetworkHandler(bbctrl.APIHandler):
|
|
|
|
def get(self):
|
|
try:
|
|
ipAddresses = subprocess.check_output(
|
|
"ip -4 addr | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'", shell=True).decode().split()
|
|
ipAddresses.remove("127.0.0.1")
|
|
regex = re.compile(r'/255$/')
|
|
filtered = [i for i in ipAddresses if not regex.match(i)]
|
|
ipAddresses = filtered[0]
|
|
|
|
except:
|
|
ipAddresses = "Not Connected"
|
|
|
|
hostname = socket.gethostname()
|
|
|
|
try:
|
|
wifi = json.loads(call_get_output(['config-wifi', '-j']))
|
|
except:
|
|
wifi = {'enabled': False}
|
|
|
|
try:
|
|
lines = iw_parse.call_iwlist().decode("utf-8").split("\n")
|
|
wifi['networks'] = iw_parse.get_parsed_cells(lines)
|
|
except:
|
|
wifi['networks'] = []
|
|
|
|
self.write_json({
|
|
'ipAddresses': ipAddresses,
|
|
'hostname': hostname,
|
|
'wifi': wifi
|
|
})
|
|
|
|
def put(self):
|
|
if self.get_ctrl().args.demo:
|
|
raise HTTPError(400, 'Cannot configure WiFi in demo mode')
|
|
|
|
if not 'wifi' in self.json:
|
|
raise HTTPError(400, 'Payload is missing wifi config information')
|
|
|
|
wifi = self.json['wifi']
|
|
cmd = ['config-wifi', '-r']
|
|
|
|
if not wifi['enabled']:
|
|
cmd += ['-d']
|
|
else:
|
|
if 'ssid' in wifi:
|
|
cmd += ['-s', wifi['ssid']]
|
|
|
|
if 'password' in wifi:
|
|
cmd += ['-p', wifi['password']]
|
|
|
|
if subprocess.call(cmd) == 0:
|
|
self.write_json('ok')
|
|
return
|
|
|
|
raise HTTPError(400, 'Failed to configure wifi')
|
|
|
|
|
|
class UsernameHandler(bbctrl.APIHandler):
|
|
def get(self): self.write_json(get_username())
|
|
|
|
|
|
def put_ok(self):
|
|
if self.get_ctrl().args.demo:
|
|
raise HTTPError(400, 'Cannot set username in demo mode')
|
|
|
|
if 'username' in self.json: set_username(self.json['username'])
|
|
else: raise HTTPError(400, 'Missing "username"')
|
|
|
|
|
|
class PasswordHandler(bbctrl.APIHandler):
|
|
def put(self):
|
|
if self.get_ctrl().args.demo:
|
|
raise HTTPError(400, 'Cannot set password in demo mode')
|
|
|
|
if 'current' in self.json and 'password' in self.json:
|
|
check_password(self.json['current'])
|
|
|
|
# Set password
|
|
s = '%s:%s' % (get_username(), self.json['password'])
|
|
s = s.encode('utf-8')
|
|
|
|
p = subprocess.Popen(['chpasswd', '-c', 'MD5'],
|
|
stdin = subprocess.PIPE)
|
|
p.communicate(input = s)
|
|
|
|
if p.returncode == 0:
|
|
self.write_json('ok')
|
|
return
|
|
|
|
raise HTTPError(401, 'Failed to set password')
|
|
|
|
|
|
class ConfigLoadHandler(bbctrl.APIHandler):
|
|
def get(self):
|
|
self.write_json(self.get_ctrl().config.load())
|
|
|
|
|
|
class ConfigDownloadHandler(bbctrl.APIHandler):
|
|
def set_default_headers(self):
|
|
fmt = socket.gethostname() + '-%Y%m%d.zip'
|
|
filename = datetime.date.today().strftime(fmt)
|
|
self.set_header('Content-Type', 'application/octet-stream')
|
|
self.set_header('Content-Disposition',
|
|
'attachment; filename="%s"' % filename)
|
|
|
|
def get(self,filename):
|
|
buffer = io.BytesIO()
|
|
zip_file = zipfile.ZipFile(buffer, mode="w")
|
|
config_path = self.get_path('config.json')
|
|
try:
|
|
if os.path.exists(config_path):
|
|
zip_file.write(config_path,'config.json')
|
|
else:
|
|
json_bytes = json.dumps({'version': self.version}).encode("utf-8")
|
|
zip_file.writestr("config.json",json_bytes)
|
|
|
|
except Exception: self.log.exception('Internal error: Failed to download config')
|
|
if not filename:
|
|
zip_file.close()
|
|
buffer.seek(0)
|
|
self.write(buffer.getvalue())
|
|
self.finish()
|
|
|
|
filename = filename[1:]
|
|
files = filename.split(',')
|
|
|
|
for filename in files:
|
|
filename = os.path.basename(url_unescape(filename))
|
|
filepath = self.get_upload(filename)
|
|
zip_file.write(filepath, filename)
|
|
|
|
zip_file.close()
|
|
buffer.seek(0)
|
|
|
|
self.write(buffer.getvalue())
|
|
self.finish()
|
|
|
|
class ConfigRestoreHandler(bbctrl.APIHandler):
|
|
def put(self):
|
|
if 'zipfile' not in self.request.files:
|
|
raise HTTPError(400, 'No file uploaded')
|
|
|
|
zip_file = self.request.files['zipfile'][0]
|
|
temp_dir = './config-temp'
|
|
|
|
if not os.path.exists(temp_dir):
|
|
os.mkdir(temp_dir)
|
|
|
|
files_path = os.path.join(temp_dir, zip_file['filename'])
|
|
|
|
with open(files_path, 'wb') as f:
|
|
f.write(zip_file['body'])
|
|
|
|
if not os.path.exists(self.get_upload()):
|
|
os.mkdir(self.get_upload())
|
|
|
|
|
|
with zipfile.ZipFile(files_path, 'r') as zip_ref:
|
|
zip_ref.extractall(temp_dir+'/extracted')
|
|
|
|
extension = (".nc", ".ngc", ".gcode", ".gc")
|
|
|
|
for root, dirs, files in os.walk(temp_dir+'/extracted'):
|
|
for file in files:
|
|
file_path = os.path.join(root, file)
|
|
|
|
#Updating the config.json
|
|
if file =="config.json":
|
|
with open(file_path, 'r') as json_file:
|
|
json_data = json.load(json_file)
|
|
json_data["macros_list"] = [{"file_name": item["file_name"]} for item in json_data["gcode_list"]]
|
|
keys_to_remove = ['non_macros_list','gcode_list']
|
|
for key in keys_to_remove:
|
|
if key in json_data:
|
|
del json_data[key]
|
|
self.get_ctrl().config.save(json_data)
|
|
|
|
#moving the gcodes from temp to uploads
|
|
elif file.endswith(extension):
|
|
filename = self.get_upload(file).encode('utf8')
|
|
os.link(file_path,filename)
|
|
self.get_ctrl().preplanner.invalidate(file_path)
|
|
self.get_ctrl().state.add_file(file_path)
|
|
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
class ConfigSaveHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().config.save(self.json)
|
|
|
|
|
|
class ConfigResetHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().config.reset()
|
|
|
|
|
|
class FirmwareUpdateHandler(bbctrl.APIHandler):
|
|
def prepare(self): pass
|
|
|
|
|
|
def put_ok(self):
|
|
if not 'firmware' in self.request.files:
|
|
raise HTTPError(401, 'Missing "firmware"')
|
|
|
|
firmware = self.request.files['firmware'][0]
|
|
|
|
if not os.path.exists('firmware'): os.mkdir('firmware')
|
|
|
|
with open('firmware/update.tar.bz2', 'wb') as f:
|
|
f.write(firmware['body'])
|
|
|
|
self.get_ctrl().lcd.goodbye('Upgrading firmware')
|
|
subprocess.Popen(['/usr/local/bin/update-bbctrl'])
|
|
|
|
|
|
class UpgradeHandler(bbctrl.APIHandler):
|
|
def put_ok(self):
|
|
self.get_ctrl().lcd.goodbye('Upgrading firmware')
|
|
subprocess.Popen(['/usr/local/bin/upgrade-bbctrl'])
|
|
|
|
|
|
class PathHandler(bbctrl.APIHandler):
|
|
@gen.coroutine
|
|
def get(self, filename, dataType, *args):
|
|
if not os.path.exists(self.get_upload(filename)):
|
|
raise HTTPError(404, 'File not found')
|
|
|
|
preplanner = self.get_ctrl().preplanner
|
|
future = preplanner.get_plan(filename)
|
|
|
|
try:
|
|
delta = datetime.timedelta(seconds = 1)
|
|
data = yield gen.with_timeout(delta, future)
|
|
|
|
except gen.TimeoutError:
|
|
progress = preplanner.get_plan_progress(filename)
|
|
self.write_json(dict(progress = progress))
|
|
return
|
|
|
|
try:
|
|
if data is None: return
|
|
meta, positions, speeds = data
|
|
|
|
if dataType == '/positions': data = positions
|
|
elif dataType == '/speeds': data = speeds
|
|
else:
|
|
self.get_ctrl().state.set_bounds(meta['bounds'])
|
|
self.write_json(meta)
|
|
return
|
|
|
|
filename = filename + '-' + dataType[1:]
|
|
self.set_header('Content-Disposition', 'filename="%s"' % filename)
|
|
self.set_header('Content-Type', 'application/octet-stream')
|
|
self.set_header('Content-Encoding', 'gzip')
|
|
self.set_header('Content-Length', str(len(data)))
|
|
|
|
# Respond with chunks to avoid long delays
|
|
SIZE = 102400
|
|
chunks = [data[i:i + SIZE] for i in range(0, len(data), SIZE)]
|
|
for chunk in chunks:
|
|
self.write(chunk)
|
|
yield self.flush()
|
|
|
|
except tornado.iostream.StreamClosedError as e: pass
|
|
|
|
|
|
class HomeHandler(bbctrl.APIHandler):
|
|
def put_ok(self, axis, action, *args):
|
|
if axis is not None: axis = ord(axis[1:2].lower())
|
|
|
|
if action == '/set':
|
|
if not 'position' in self.json:
|
|
raise HTTPError(400, 'Missing "position"')
|
|
|
|
self.get_ctrl().mach.home(axis, self.json['position'])
|
|
|
|
elif action == '/clear': self.get_ctrl().mach.unhome(axis)
|
|
else: self.get_ctrl().mach.home(axis)
|
|
|
|
|
|
class StartHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.start()
|
|
|
|
|
|
class EStopHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.estop()
|
|
|
|
|
|
class ClearHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.clear()
|
|
|
|
|
|
class StopHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.stop()
|
|
|
|
|
|
class PauseHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.pause()
|
|
|
|
|
|
class UnpauseHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.unpause()
|
|
|
|
|
|
class OptionalPauseHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.optional_pause()
|
|
|
|
|
|
class StepHandler(bbctrl.APIHandler):
|
|
def put_ok(self): self.get_ctrl().mach.step()
|
|
|
|
|
|
class PositionHandler(bbctrl.APIHandler):
|
|
def put_ok(self, axis):
|
|
self.get_ctrl().mach.set_position(axis, float(self.json['position']))
|
|
|
|
|
|
class OverrideFeedHandler(bbctrl.APIHandler):
|
|
def put_ok(self, value): self.get_ctrl().mach.override_feed(float(value))
|
|
|
|
|
|
class OverrideSpeedHandler(bbctrl.APIHandler):
|
|
def put_ok(self, value): self.get_ctrl().mach.override_speed(float(value))
|
|
|
|
|
|
class ModbusReadHandler(bbctrl.APIHandler):
|
|
def put_ok(self):
|
|
self.get_ctrl().mach.modbus_read(int(self.json['address']))
|
|
|
|
|
|
class ModbusWriteHandler(bbctrl.APIHandler):
|
|
def put_ok(self):
|
|
self.get_ctrl().mach.modbus_write(int(self.json['address']),
|
|
int(self.json['value']))
|
|
|
|
|
|
class JogHandler(bbctrl.APIHandler):
|
|
def put_ok(self):
|
|
# Handle possible out of order jog command processing
|
|
if 'ts' in self.json:
|
|
ts = self.json['ts']
|
|
id = self.get_cookie('client-id')
|
|
|
|
if not hasattr(self.app, 'last_jog'):
|
|
self.app.last_jog = {}
|
|
|
|
last = self.app.last_jog.get(id, 0)
|
|
self.app.last_jog[id] = ts
|
|
|
|
if ts < last: return # Out of order
|
|
|
|
self.get_ctrl().mach.jog(self.json)
|
|
|
|
|
|
displayRotatePattern = re.compile(r'display_rotate\s*=\s*(\d)')
|
|
transformationMatrixPattern = re.compile(
|
|
r'(\n)(\s+)(MatchIsTouchscreen.*?\n)(\s+Option\s+\"TransformationMatrix\".*?\n)(.*?EndSection)',
|
|
re.DOTALL)
|
|
matchIsTouchscreenPattern = re.compile(
|
|
r'(\n)(\s+)(MatchIsTouchscreen.*?\n)(.*?EndSection)', re.DOTALL)
|
|
class ScreenRotationHandler(bbctrl.APIHandler):
|
|
|
|
@gen.coroutine
|
|
def get(self):
|
|
with open("/boot/config.txt", 'rt') as config:
|
|
lines = config.readlines()
|
|
for line in lines:
|
|
if line.startswith('display_rotate'):
|
|
self.write_json({
|
|
'rotated':
|
|
int(displayRotatePattern.search(line).group(1)) != 0
|
|
})
|
|
return
|
|
|
|
self.write_json({'rotated': False})
|
|
return
|
|
|
|
@gen.coroutine
|
|
def put_ok(self):
|
|
rotated = self.json['rotated']
|
|
|
|
subprocess.Popen([
|
|
'/usr/local/bin/edit-boot-config',
|
|
'display_rotate={}'.format(2 if rotated else 0)
|
|
])
|
|
|
|
with open("/usr/share/X11/xorg.conf.d/40-libinput.conf",
|
|
'rt') as config:
|
|
text = config.read()
|
|
text = transformationMatrixPattern.sub(r'\1\2\3\5', text)
|
|
if rotated:
|
|
text = matchIsTouchscreenPattern.sub(
|
|
r'\1\2\3\2Option "TransformationMatrix" "-1 0 1 0 -1 1 0 0 1"\1\4',
|
|
text)
|
|
with open("/usr/share/X11/xorg.conf.d/40-libinput.conf",
|
|
'wt') as config:
|
|
config.write(text)
|
|
|
|
subprocess.run('reboot')
|
|
|
|
|
|
class TimeHandler(bbctrl.APIHandler):
|
|
|
|
def get(self):
|
|
timeinfo = call_get_output(['timedatectl'])
|
|
timezones = call_get_output(
|
|
['timedatectl', 'list-timezones', '--no-pager'])
|
|
self.get_log('TimeHandler').info('Time stuff: {}, {}'.format(
|
|
timeinfo, timezones))
|
|
|
|
self.write_json({'timeinfo': timeinfo, 'timezones': timezones})
|
|
|
|
def put_ok(self):
|
|
datetime = self.json['datetime']
|
|
timezone = self.json['timezone']
|
|
subprocess.Popen(['timedatectl', 'set-time', datetime])
|
|
subprocess.Popen(['timedatectl', 'set-timezone', timezone])
|
|
|
|
|
|
class RemoteDiagnosticsHandler(bbctrl.APIHandler):
|
|
|
|
def get(self):
|
|
code = self.get_query_argument("code", "")
|
|
command = self.get_query_argument("command", "")
|
|
|
|
log = self.get_log('RemoteDiagnostics')
|
|
|
|
if command == "disconnect":
|
|
subprocess.Popen(['killall', 'ngrok'])
|
|
self.write_json({'message': "Succesfully disconnected"})
|
|
|
|
if command == "connect":
|
|
try:
|
|
url = 'https://tinyurl.com/1f-remote?code={}'.format(code)
|
|
with urlopen(url) as response:
|
|
body = response.read()
|
|
|
|
os.makedirs("/tmp/ngrok", exist_ok=True)
|
|
with open("/tmp/ngrok/1f-ngrok.sh", 'wb') as f:
|
|
f.write(body)
|
|
|
|
subprocess.Popen(['/bin/bash', "/tmp/ngrok/1f-ngrok.sh"])
|
|
self.write_json({'success': True})
|
|
except Exception as e:
|
|
log.info("Failed: {}".format(str(e)))
|
|
self.write_json({
|
|
'success': False,
|
|
'code': e.code or None,
|
|
'message': e.reason or "Unknown"
|
|
})
|
|
|
|
# Base class for Web Socket connections
|
|
class ClientConnection(object):
|
|
def __init__(self, app):
|
|
self.app = app
|
|
self.count = 0
|
|
|
|
|
|
def heartbeat(self):
|
|
self.timer = self.app.ioloop.call_later(3, self.heartbeat)
|
|
self.send({'heartbeat': self.count})
|
|
self.count += 1
|
|
|
|
|
|
def send(self, msg): raise HTTPError(400, 'Not implemented')
|
|
|
|
|
|
def on_open(self, id = None):
|
|
self.ctrl = self.app.get_ctrl(id)
|
|
|
|
self.ctrl.state.add_listener(self.send)
|
|
self.ctrl.log.add_listener(self.send)
|
|
self.is_open = True
|
|
self.heartbeat()
|
|
self.app.opened(self.ctrl)
|
|
|
|
|
|
def on_close(self):
|
|
self.app.ioloop.remove_timeout(self.timer)
|
|
self.ctrl.state.remove_listener(self.send)
|
|
self.ctrl.log.remove_listener(self.send)
|
|
self.is_open = False
|
|
self.app.closed(self.ctrl)
|
|
|
|
|
|
def on_message(self, data):
|
|
self.ctrl.mach.mdi(data)
|
|
|
|
|
|
# Used by CAMotics
|
|
class WSConnection(ClientConnection, tornado.websocket.WebSocketHandler):
|
|
def __init__(self, app, request, **kwargs):
|
|
ClientConnection.__init__(self, app)
|
|
tornado.websocket.WebSocketHandler.__init__(
|
|
self, app, request, **kwargs)
|
|
|
|
def send(self, msg):
|
|
self.write_message(msg)
|
|
|
|
def open(self):
|
|
self.on_open()
|
|
|
|
|
|
# Used by Web frontend
|
|
class SockJSConnection(ClientConnection, sockjs.tornado.SockJSConnection):
|
|
def __init__(self, session):
|
|
ClientConnection.__init__(self, session.server.app)
|
|
sockjs.tornado.SockJSConnection.__init__(self, session)
|
|
|
|
|
|
def send(self, msg):
|
|
try:
|
|
sockjs.tornado.SockJSConnection.send(self, msg)
|
|
except:
|
|
self.close()
|
|
|
|
|
|
def on_open(self, info):
|
|
cookie = info.get_cookie('client-id')
|
|
if cookie is None: self.send(dict(sid = '')) # Trigger client reset
|
|
else:
|
|
id = cookie.value
|
|
|
|
ip = info.ip
|
|
if 'X-Real-IP' in info.headers: ip = info.headers['X-Real-IP']
|
|
self.app.get_ctrl(id).log.get('Web').info('Connection from %s' % ip)
|
|
super().on_open(id)
|
|
|
|
|
|
class StaticFileHandler(tornado.web.StaticFileHandler):
|
|
def set_extra_headers(self, path):
|
|
self.set_header('Cache-Control',
|
|
'no-store, no-cache, must-revalidate, max-age=0')
|
|
|
|
class Web(tornado.web.Application):
|
|
def __init__(self, args, ioloop):
|
|
self.args = args
|
|
self.ioloop = ioloop
|
|
self.ctrls = {}
|
|
|
|
# Init camera
|
|
if not args.disable_camera:
|
|
if self.args.demo: log = bbctrl.log.Log(args, ioloop, 'camera.log')
|
|
else: log = self.get_ctrl().log
|
|
self.camera = bbctrl.Camera(ioloop, args, log)
|
|
else: self.camera = None
|
|
|
|
# Init controller
|
|
if not self.args.demo:
|
|
self.get_ctrl()
|
|
self.monitor = bbctrl.MonitorTemp(self)
|
|
|
|
handlers = [
|
|
(r'/websocket', WSConnection),
|
|
(r'/api/log', LogHandler),
|
|
(r'/api/message/(\d+)/ack', MessageAckHandler),
|
|
(r'/api/bugreport', BugReportHandler),
|
|
(r'/api/reboot', RebootHandler),
|
|
(r'/api/shutdown', ShutdownHandler),
|
|
(r'/api/hostname', HostnameHandler),
|
|
(r'/api/wifi', NetworkData),
|
|
(r'/api/network', NetworkHandler),
|
|
(r'/api/remote/username', UsernameHandler),
|
|
(r'/api/remote/password', PasswordHandler),
|
|
(r'/api/config/load', ConfigLoadHandler),
|
|
(r'/api/config/download(/[^/]+)?', ConfigDownloadHandler),
|
|
(r'/api/config/save', ConfigSaveHandler),
|
|
(r'/api/config/reset', ConfigResetHandler),
|
|
(r'/api/config/restore',ConfigRestoreHandler),
|
|
(r'/api/firmware/update', FirmwareUpdateHandler),
|
|
(r'/api/upgrade', UpgradeHandler),
|
|
(r'/api/file(/[^/]+)?', bbctrl.FileHandler),
|
|
(r'/api/path/([^/]+)((/positions)|(/speeds))?', PathHandler),
|
|
(r'/api/home(/[xyzabcXYZABC]((/set)|(/clear))?)?', HomeHandler),
|
|
(r'/api/start', StartHandler),
|
|
(r'/api/estop', EStopHandler),
|
|
(r'/api/clear', ClearHandler),
|
|
(r'/api/stop', StopHandler),
|
|
(r'/api/pause', PauseHandler),
|
|
(r'/api/unpause', UnpauseHandler),
|
|
(r'/api/pause/optional', OptionalPauseHandler),
|
|
(r'/api/step', StepHandler),
|
|
(r'/api/position/([xyzabcXYZABC])', PositionHandler),
|
|
(r'/api/override/feed/([\d.]+)', OverrideFeedHandler),
|
|
(r'/api/override/speed/([\d.]+)', OverrideSpeedHandler),
|
|
(r'/api/modbus/read', ModbusReadHandler),
|
|
(r'/api/modbus/write', ModbusWriteHandler),
|
|
(r'/api/jog', JogHandler),
|
|
(r'/api/video', bbctrl.VideoHandler),
|
|
(r'/api/screen-rotation', ScreenRotationHandler),
|
|
(r'/api/time', TimeHandler),
|
|
(r'/api/remote-diagnostics', RemoteDiagnosticsHandler),
|
|
(r'/(.*)', StaticFileHandler,
|
|
{'path': bbctrl.get_resource('http/'),
|
|
'default_filename': 'index.html'}),
|
|
]
|
|
|
|
router = sockjs.tornado.SockJSRouter(SockJSConnection, '/sockjs')
|
|
router.app = self
|
|
|
|
tornado.web.Application.__init__(self, router.urls + handlers)
|
|
|
|
try:
|
|
self.listen(args.port, address = args.addr)
|
|
|
|
except Exception as e:
|
|
raise Exception('Failed to bind %s:%d: %s' % (
|
|
args.addr, args.port, e))
|
|
|
|
print('Listening on http://%s:%d/' % (args.addr, args.port))
|
|
|
|
|
|
def opened(self, ctrl): ctrl.clear_timeout()
|
|
|
|
|
|
def closed(self, ctrl):
|
|
# Time out clients in demo mode
|
|
if self.args.demo: ctrl.set_timeout(self._reap_ctrl, ctrl)
|
|
|
|
|
|
def _reap_ctrl(self, ctrl):
|
|
ctrl.close()
|
|
del self.ctrls[ctrl.id]
|
|
|
|
|
|
def get_ctrl(self, id = None):
|
|
if not id or not self.args.demo: id = ''
|
|
|
|
if not id in self.ctrls:
|
|
ctrl = bbctrl.Ctrl(self.args, self.ioloop, id)
|
|
self.ctrls[id] = ctrl
|
|
|
|
else: ctrl = self.ctrls[id]
|
|
|
|
return ctrl
|
|
|
|
|
|
# Override default logger
|
|
def log_request(self, handler):
|
|
log = self.get_ctrl(handler.get_cookie('client-id')).log.get('Web')
|
|
log.info("%d %s", handler.get_status(), handler._request_summary())
|