Wifi manager (#148)

Updates to include ability to change wifi networks.
This commit is contained in:
jordanruthe
2021-05-13 20:53:58 -04:00
committed by GitHub
parent e75a10a888
commit 7e3b919c62
7 changed files with 808 additions and 255 deletions

View File

@@ -49,6 +49,31 @@ try:
except: except:
pass pass
def get_network_interfaces():
stream = os.popen("ip addr | grep ^'[0-9]' | cut -d ' ' -f 2 | grep -o '[a-zA-Z0-9\.]*'")
return [i for i in stream.read().strip().split('\n') if not i.startswith('lo')]
def get_wireless_interfaces():
p = subprocess.Popen(["which","iwconfig"], stdout=subprocess.PIPE)
while p.poll() is None:
time.sleep(.1)
if p.poll() != 0:
return None
try:
p = subprocess.Popen(["iwconfig"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = p.stdout.read().decode('ascii').split('\n')
except:
logging.info("Error with running iwconfig command")
return None
interfaces = []
for line in result:
match = re.search('^(\S+)\s+.*$', line)
if match:
interfaces.append(match.group(1))
return interfaces
def get_software_version(): def get_software_version():
prog = ('git', '-C', os.path.dirname(__file__), 'describe', '--always', prog = ('git', '-C', os.path.dirname(__file__), 'describe', '--always',

View File

@@ -2,82 +2,197 @@ import os, signal
import json import json
import logging import logging
import re import re
import socket
import subprocess import subprocess
import threading import threading
import time
from contextlib import suppress from contextlib import suppress
from threading import Thread from threading import Thread
RESCAN_INTERVAL = 120 from subprocess import PIPE, Popen, STDOUT
from queue import Queue, Empty
class WifiManager(Thread): import gi
iw_regexes = [ gi.require_version("Gtk", "3.0")
re.compile(r"^ESSID:\"(?P<essid>.*)\"$"), from gi.repository import Gtk, Gdk, GLib
re.compile(r"^Protocol:(?P<protocol>.+)$"),
re.compile(r"^Mode:(?P<mode>.+)$"), RESCAN_INTERVAL = 180
re.compile(r"^Frequency:(?P<frequency>[\d.]+) (?P<frequency_units>.+) \(Channel (?P<channel>\d+)\)$"), KS_SOCKET_FILE = "/tmp/.KS_wpa_supplicant"
re.compile(r"^Encryption key:(?P<encryption>.+)$"),
re.compile(r"^Quality=(?P<signal_quality>\d+)/(?P<signal_total>\d+)\s+Signal level=(?P<signal_level_dBm>.+) d.+$"), class WifiManager():
re.compile(r"^Signal level=(?P<signal_quality>\d+)/(?P<signal_total>\d+).*$")
]
networks_in_supplicant = [] networks_in_supplicant = []
wpa = {
"wpa": re.compile(r"IE:\ WPA\ Version\ 1$"),
"wpa2": re.compile(r"IE:\ IEEE\ 802\.11i/WPA2\ Version\ 1$")
}
connected = False connected = False
_stop_loop = False _stop_loop = False
thread = None
def __init__(self, *args, **kwargs): def __init__(self, interface, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.loop = None self.loop = None
self._poll_task = None self._poll_task = None
self._scanning = False self._scanning = False
self._callbacks = {
"connected": [],
"connecting_status": [],
"scan_results": []
}
self._stop_loop = False
self.connected = False
self.connected_ssid = None
self.connecting_info = []
self.event = threading.Event()
self.initialized = False
self.interface = interface
self.networks = {} self.networks = {}
self.supplicant_networks = {}
self.queue = Queue()
self.tasks = []
self.timeout = None
self.scan_time = 0
if os.path.exists(KS_SOCKET_FILE):
os.remove(KS_SOCKET_FILE)
try:
self.soc = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.soc.bind(KS_SOCKET_FILE)
self.soc.connect("/var/run/wpa_supplicant/%s" % interface)
except:
logging.info("Error connecting to wifi socket: %s" % interface)
return
self.wpa_thread = WpaSocket(self, self.queue, self.callback)
self.wpa_thread.start()
self.initialized = True
self.wpa_cli("ATTACH", False)
self.wpa_cli("SCAN", False)
GLib.idle_add(self.read_wpa_supplicant)
self.timeout = GLib.timeout_add_seconds(RESCAN_INTERVAL, self.rescan)
def add_callback(self, name, callback):
if name in self._callbacks and callback not in self._callbacks[name]:
self._callbacks[name].append(callback)
def add_network(self, ssid, psk):
for id in list(self.supplicant_networks):
if self.supplicant_networks[id]['ssid'] == ssid:
#Modify network
return
# TODO: Add wpa_cli error checking
network_id = self.wpa_cli("ADD_NETWORK")
commands = [
'ENABLE_NETWORK %s' % (network_id),
'SET_NETWORK %s ssid "%s"' % (network_id, ssid.replace('"','\"')),
'SET_NETWORK %s psk "%s"' % (network_id, psk.replace('"','\"'))
]
self.wpa_cli_batch(commands)
self.read_wpa_supplicant() self.read_wpa_supplicant()
id = None
def run(self): for i in list(self.supplicant_networks):
event = threading.Event() if self.supplicant_networks[i]['ssid'] == ssid:
logging.debug("Setting up wifi event loop") id = i
while self._stop_loop == False:
try:
self.scan()
event.wait(RESCAN_INTERVAL)
except:
logging.exception("Poll wifi error")
def stop(self):
self.loop.call_soon_threadsafe(self.loop.stop)
def stop_loop(self):
self._stop_loop = True
def get_current_wifi(self, interface="wlan0"):
p = subprocess.Popen(["iwconfig",interface], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
content = p.stdout.read().decode('utf-8').split('\n')
essid = None
mac = None
for line in content:
match = re.match(r'^.*ESSID:"(.*)"$', line.strip())
if match:
essid = match.group(1)
continue
match = re.match(r'^.*Access\s+Point:\s+([0-9A-Fa-f:]+)$', line.strip())
if match:
mac = match.group(1)
break break
if essid is None or mac is None: if id == None:
self.connected = False logging.info("Error adding network")
return None return False
self.connected = True
return [essid, mac]
def get_network_info(self, essid=None, mac=None): self.save_wpa_conf()
if essid is not None and essid in self.networks: return True
return self.networks[essid]
if mac is not None and essid is None: def callback(self, type, msg):
if type in self._callbacks:
for cb in self._callbacks[type]:
Gdk.threads_add_idle(
GLib.PRIORITY_DEFAULT_IDLE,
cb,
msg)
def connect(self, ssid):
id = None
for netid, net in self.supplicant_networks.items():
if net['ssid'] == ssid:
id = netid
break
if id == None:
logging.info("Wifi network is not defined in wpa_supplicant")
return False
logging.info("Attempting to connect to wifi: %s" % id)
self.connecting_info = ["Attempting to connect to %s" % ssid]
self.wpa_cli("SELECT_NETWORK %s" % id)
self.save_wpa_conf()
def delete_network(self, ssid):
id = None
for i in list(self.supplicant_networks):
if self.supplicant_networks[i]['ssid'] == ssid:
id = i
break
if id == None:
logging.debug("Unable to find network in wpa_supplicant")
return
self.wpa_cli("REMOVE_NETWORK %s" % id)
for id in list(self.supplicant_networks):
if self.supplicant_networks[id]['ssid'] == ssid:
del self.supplicant_networks[id]
break
self.save_wpa_conf()
def get_connected_ssid(self):
return self.connected_ssid
def get_current_wifi(self, interface="wlan0"):
logging.info("Getting current wifi information")
status = self.wpa_cli("STATUS").split('\n')
vars = {}
for line in status:
arr = line.split('=')
vars[arr[0]] = "=".join(arr[1:])
prev_ssid = self.connected_ssid
if "ssid" in vars and "bssid" in vars:
self.connected = True
self.connected_ssid = vars['ssid']
for ssid, val in self.networks.items():
if ssid == vars['ssid']:
self.networks[ssid]['connected'] = True
else:
self.networks[ssid]['connected'] = False
if prev_ssid != self.connected_ssid:
for cb in self._callbacks['connected']:
Gdk.threads_add_idle(
GLib.PRIORITY_DEFAULT_IDLE,
cb, self.connected_ssid, prev_ssid)
return [vars['ssid'], vars['bssid']]
else:
logging.info("Resetting connected_ssid")
self.connected = False
self.connected_ssid = None
for ssid, val in self.networks.items():
self.networks[ssid]['connected'] = False
if prev_ssid != self.connected_ssid:
for cb in self._callbacks['connected']:
Gdk.threads_add_idle(
GLib.PRIORITY_DEFAULT_IDLE,
cb, self.connected_ssid, prev_ssid)
return None
def get_current_wifi_idle_add(self):
self.get_current_wifi()
return False
def get_network_info(self, ssid=None, mac=None):
if ssid is not None and ssid in self.networks:
return self.networks[ssid]
if mac is not None and ssid is None:
for net in self.networks: for net in self.networks:
if mac == net['mac']: if mac == net['mac']:
return net return net
@@ -86,77 +201,272 @@ class WifiManager(Thread):
def get_networks(self): def get_networks(self):
return list(self.networks) return list(self.networks)
def get_supplicant_networks(self):
return self.supplicant_networks
def is_connected(self): def is_connected(self):
return self.connected return self.connected
def parse(self, data): def is_initialized(self):
aps = [] return self.initialized
lines = data.split('\n')
for line in lines:
line = line.strip()
match = re.match(r'^Cell\s+([0-9]+)\s+-\s+Address:\s+(?P<mac>[0-9A-Fa-f:]+)$', line)
if match:
aps.append({"mac":match.group(2)})
continue
if len(aps) < 1:
continue
for w, wreg in self.wpa.items():
t = wreg.search(line)
if t is not None:
aps[-1].update({'encryption': w})
for exp in self.iw_regexes:
result = exp.search(line)
if result is not None:
if "encryption" in result.groupdict():
if result.groupdict()['encryption'] == 'on' :
aps[-1].update({'encryption': 'wep'})
else:
aps[-1].update({'encryption': 'off'})
else:
aps[-1].update(result.groupdict())
return aps
def read_wpa_supplicant(self): def read_wpa_supplicant(self):
wpaconf = "/etc/wpa_supplicant/wpa_supplicant.conf" results = self.wpa_cli("LIST_NETWORKS").split('\n')
if not os.path.exists(wpaconf): results.pop(0)
return None self.supplicant_networks = {}
regexes = [
re.compile(r'^ssid\s*=\s*"(?P<ssid>.*)"$'),
re.compile(r'^psk\s*=\s*"(?P<psk>.*)"$')
]
networks = []
p = subprocess.Popen(["sudo","cat",wpaconf], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
contents = p.stdout.read().decode('utf-8').split('\n')
for line in contents:
if re.match(r'^network\s*=\s*{$', line.strip()):
networks.append({})
continue
if len(networks) < 1:
continue
for exp in regexes:
result = exp.search(line.strip())
if result is not None:
networks[-1].update(result.groupdict())
self.networks_in_supplicant = [] self.networks_in_supplicant = []
for network in networks: for net in [n.split('\t') for n in results]:
self.networks_in_supplicant.append(network) self.supplicant_networks[net[0]] = {
"ssid": net[1],
"bssid": net[2],
"flags": net[3] if len(net) == 4 else ""
}
self.networks_in_supplicant.append(self.supplicant_networks[net[0]])
def remove_callback(self, name, callback):
if name in self._callbacks and callback in self._callbacks[name]:
self._callbacks[name].remove(callback)
def rescan(self):
self.wpa_cli("SCAN", False)
return True
def save_wpa_conf(self):
logging.info("Saving WPA config")
self.wpa_cli("SAVE_CONFIG")
def scan_results(self, interface='wlan0'):
new_networks = []
deleted_networks = list(self.networks)
logging.info("Trying to get scan results")
results = self.wpa_cli("SCAN_RESULTS").split('\n')
results.pop(0)
aps = []
for res in results:
match = re.match("^([a-f0-9:]+)\s+([0-9]+)\s+([\-0-9]+)\s+(\S+)\s+(.+)?", res)
if match:
net = {
"mac": match.group(1),
"channel": WifiChannels.lookup(match.group(2))[1],
"connected": False,
"configured": False,
"frequency": match.group(2),
"flags": match.group(4),
"signal_level_dBm": match.group(3),
"ssid": match.group(5)
}
if "WPA2" in net['flags']:
net['encryption'] = "WPA2"
elif "WPA" in net['flags']:
net['encryption'] = "WPA"
elif "WEP" in net['flags']:
net['encryption'] = "WEP"
else:
net['encryption'] = "off"
aps.append(net)
def scan(self, interface='wlan0'):
p = subprocess.Popen(["sudo","iwlist",interface,"scan"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
aps = self.parse(p.stdout.read().decode('utf-8'))
cur_info = self.get_current_wifi() cur_info = self.get_current_wifi()
self.networks = {} self.networks = {}
for ap in aps: for ap in aps:
self.networks[ap['essid']] = ap self.networks[ap['ssid']] = ap
if cur_info is not None and cur_info[0] == ap['essid'] and cur_info[1] == ap['mac']: if cur_info is not None and cur_info[0] == ap['ssid'] and cur_info[1].lower() == ap['mac'].lower():
self.networks[ap['essid']]['connected'] = True self.networks[ap['ssid']]['connected'] = True
for net in self.networks_in_supplicant:
if ap['essid'] == net['ssid'] and "psk" in net: for net in list(self.networks):
ap['psk'] = net['psk'] if net in deleted_networks:
break deleted_networks.remove(net)
else:
new_networks.append(net)
if len(new_networks) > 0 or len(deleted_networks) > 0:
for cb in self._callbacks['scan_results']:
Gdk.threads_add_idle(
GLib.PRIORITY_DEFAULT_IDLE,
cb, new_networks, deleted_networks)
def wpa_cli(self, command, wait=True):
if wait == False:
self.wpa_thread.skip_command()
self.soc.send(command.encode())
if wait == True:
resp = self.queue.get()
return resp
def wpa_cli_batch(self, commands):
for cmd in commands:
self.wpa_cli(cmd)
class WpaSocket(Thread):
def __init__ (self, wm, queue, callback):
super().__init__()
self.queue = queue
self.callback = callback
self.soc = wm.soc
self._stop_loop = False
self.skip_commands = 0
self.wm = wm
def run(self):
event = threading.Event()
logging.debug("Setting up wifi event loop")
while self._stop_loop == False:
try:
msg = self.soc.recv(4096).decode().strip()
except:
# TODO: Socket error
continue
if msg.startswith("<"):
if "CTRL-EVENT-SCAN-RESULTS" in msg:
logging.info("Adding scan_results to callbacks")
Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.wm.scan_results)
elif "CTRL-EVENT-DISCONNECTED" in msg:
self.callback("connecting_status", msg)
match = re.match('<3>CTRL-EVENT-DISCONNECTED bssid=(\S+) reason=3 locally_generated=1', msg)
if match:
for net in self.wm.networks:
if self.wm.networks[net]['mac'] == match.group(1):
self.wm.networks[net]['connected'] = False
break
elif "Trying to associate" in msg:
self.callback("connecting_status", msg)
elif "CTRL-EVENT-REGDOM-CHANGE" in msg:
self.callback("connecting_status", msg)
elif "CTRL-EVENT-CONNECTED" in msg:
Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.wm.get_current_wifi_idle_add)
self.callback("connecting_status", msg)
else:
if self.skip_commands > 0:
self.skip_commands = self.skip_commands - 1
else:
self.queue.put(msg)
logging.info("Wifi event loop ended")
def skip_command(self):
self.skip_commands = self.skip_commands + 1
def stop(self):
self._stop_loop = True
class WifiChannels:
@staticmethod
def lookup(freq):
if freq == "2412":
return ("2.4","1")
if freq == "2417":
return ("2.4","2")
if freq == "2422":
return ("2.4","3")
if freq == "2427":
return ("2.4","4")
if freq == "2432":
return ("2.4","5")
if freq == "2437":
return ("2.4","6")
if freq == "2442":
return ("2.4","7")
if freq == "2447":
return ("2.4","8")
if freq == "2452":
return ("2.4","9")
if freq == "2457":
return ("2.4","10")
if freq == "2462":
return ("2.4","11")
if freq == "2467":
return ("2.4","12")
if freq == "2472":
return ("2.4","13")
if freq == "2484":
return ("2.4","14")
if freq == "5035":
return ("5","7")
if freq == "5040":
return ("5","8")
if freq == "5045":
return ("5","9")
if freq == "5055":
return ("5","11")
if freq == "5060":
return ("5","12")
if freq == "5080":
return ("5","16")
if freq == "5170":
return ("5","34")
if freq == "5180":
return ("5","36")
if freq == "5190":
return ("5","38")
if freq == "5200":
return ("5","40")
if freq == "5210":
return ("5","42")
if freq == "5220":
return ("5","44")
if freq == "5230":
return ("5","46")
if freq == "5240":
return ("5","48")
if freq == "5260":
return ("5","52")
if freq == "5280":
return ("5","56")
if freq == "5300":
return ("5","60")
if freq == "5320":
return ("5","64")
if freq == "5500":
return ("5","100")
if freq == "5520":
return ("5","104")
if freq == "5540":
return ("5","108")
if freq == "5560":
return ("5","112")
if freq == "5580":
return ("5","116")
if freq == "5600":
return ("5","120")
if freq == "5620":
return ("5","124")
if freq == "5640":
return ("5","128")
if freq == "5660":
return ("5","132")
if freq == "5680":
return ("5","136")
if freq == "5700":
return ("5","140")
if freq == "5720":
return ("5","144")
if freq == "5745":
return ("5","149")
if freq == "5765":
return ("5","153")
if freq == "5785":
return ("5","157")
if freq == "5805":
return ("5","161")
if freq == "5825":
return ("5","165")
if freq == "4915":
return ("5","183")
if freq == "4920":
return ("5","184")
if freq == "4925":
return ("5","185")
if freq == "4935":
return ("5","187")
if freq == "4940":
return ("5","188")
if freq == "4945":
return ("5","189")
if freq == "4960":
return ("5","192")
if freq == "4980":
return ("5","196")
return None;

View File

@@ -1,5 +1,7 @@
import gi import gi
import json
import logging import logging
import netifaces
import os import os
import re import re
@@ -14,7 +16,6 @@ def create_panel(*args):
class NetworkPanel(ScreenPanel): class NetworkPanel(ScreenPanel):
networks = {} networks = {}
network_list = [] network_list = []
interface = "wlan0"
def initialize(self, menu): def initialize(self, menu):
_ = self.lang.gettext _ = self.lang.gettext
@@ -25,8 +26,20 @@ class NetworkPanel(ScreenPanel):
stream = os.popen('hostname -A') stream = os.popen('hostname -A')
hostname = stream.read() hostname = stream.read()
# Get IP Address # Get IP Address
stream = os.popen('hostname -I') gws = netifaces.gateways()
ip = stream.read() if "default" in gws and netifaces.AF_INET in gws["default"]:
self.interface = gws["default"][netifaces.AF_INET][1]
else:
ints = netifaces.interfaces()
if 'lo' in ints:
ints.pop('lo')
self.interfaces = ints[0]
res = netifaces.ifaddresses(self.interface)
if netifaces.AF_INET in res and len(res[netifaces.AF_INET]) > 0:
ip = res[netifaces.AF_INET][0]['addr']
else:
ip = "0.0.0.0"
self.labels['networks'] = {} self.labels['networks'] = {}
@@ -48,41 +61,65 @@ class NetworkPanel(ScreenPanel):
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
box.set_vexpand(True) box.set_vexpand(True)
box.pack_start(sbox, False, False, 0)
box.pack_start(scroll, True, True, 0)
self.labels['networklist'] = Gtk.Grid() self.labels['networklist'] = Gtk.Grid()
self.files = {} self.files = {}
GLib.idle_add(self.load_networks) if self._screen.wifi != None and self._screen.wifi.is_initialized():
box.pack_start(sbox, False, False, 0)
box.pack_start(scroll, True, True, 0)
scroll.add(self.labels['networklist']) GLib.idle_add(self.load_networks)
scroll.add(self.labels['networklist'])
#self.labels['networkinfo'] = Gtk.Label( self._screen.wifi.add_callback("connected", self.connected_callback)
# _("Network Info") + "\n\n%s%s" % (hostname, ip) self._screen.wifi.add_callback("scan_results", self.scan_callback)
#) self.timeout = GLib.timeout_add_seconds(5, self.update_all_networks)
#self.labels['networkinfo'].get_style_context().add_class('temperature_entry') else:
#grid.attach(self.labels['networkinfo'], 1, 0, 1, 1) self.labels['networkinfo'] = Gtk.Label("")
self.labels['networkinfo'].get_style_context().add_class('temperature_entry')
box.pack_start(self.labels['networkinfo'], False, False, 0)
self.update_single_network_info()
self.timeout = GLib.timeout_add_seconds(5, self.update_single_network_info)
self.content.add(box) self.content.add(box)
self.labels['main_box'] = box
def load_networks(self): def load_networks(self):
networks = self._screen.wifi.get_networks() networks = self._screen.wifi.get_networks()
conn_ssid = self._screen.wifi.get_connected_ssid()
if conn_ssid in networks:
networks.remove(conn_ssid)
self.add_network(conn_ssid, False)
for net in networks: for net in networks:
self.add_network(net, False) self.add_network(net, False)
self.update_all_networks()
self.content.show_all() self.content.show_all()
def add_network(self, essid, show=True): def add_network(self, ssid, show=True):
_ = self.lang.gettext _ = self.lang.gettext
netinfo = self._screen.wifi.get_network_info(essid) if ssid == None:
return
ssid = ssid.strip()
if ssid in list(self.networks):
logging.info("SSID already listed")
return
netinfo = self._screen.wifi.get_network_info(ssid)
if netinfo == None: if netinfo == None:
logging.debug("Couldn't get netinfo")
return return
# For now, only add connected network configured_networks = self._screen.wifi.get_supplicant_networks()
if "connected" not in netinfo: network_id = -1
return for net in list(configured_networks):
if configured_networks[net]['ssid'] == ssid:
network_id = net
frame = Gtk.Frame() frame = Gtk.Frame()
frame.set_property("shadow-type",Gtk.ShadowType.NONE) frame.set_property("shadow-type",Gtk.ShadowType.NONE)
@@ -90,42 +127,15 @@ class NetworkPanel(ScreenPanel):
name = Gtk.Label() name = Gtk.Label()
name.set_markup("<big><b>%s</b></big>" % (essid)) name.set_markup("<big><b>%s</b></big>" % (ssid))
name.set_hexpand(True) name.set_hexpand(True)
name.set_halign(Gtk.Align.START) name.set_halign(Gtk.Align.START)
name.set_line_wrap(True) name.set_line_wrap(True)
name.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) name.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
stream = os.popen('ip add show dev %s' % self.interface)
content = stream.read()
ipv4_re = re.compile(r'inet ([0-9\.]+)/[0-9]+', re.MULTILINE)
ipv6_re = re.compile(r'inet6 ([a-fA-F0-9:\.]+)/[0-9+]', re.MULTILINE)
match = ipv4_re.search(content)
ipv4 = ""
if match:
ipv4 = "<b>%s:</b> %s " % (_("IPv4"), match.group(1))
match = ipv6_re.search(content)
ipv6 = ""
if match:
ipv6 = "<b>%s:</b> %s " % (_("IPv6"), match.group(1))
stream = os.popen('hostname -f')
hostname = stream.read().strip()
connected = ""
if "connected" in netinfo:
connected = "<b>%s</b>\n<b>%s:</b> %s\n%s%s\n" % (_("Connected"),_("Hostname"),hostname, ipv4, ipv6)
elif "psk" in netinfo:
connected = "Password saved."
freq = "2.4 GHz" if netinfo['frequency'][0:1] == "2" else "5 Ghz"
info = Gtk.Label() info = Gtk.Label()
info.set_markup("%s%s <small>%s %s %s %s%s</small>" % ( connected,
"" if netinfo['encryption'] == "off" else netinfo['encryption'].upper(),
freq, _("Channel"), netinfo['channel'], netinfo['signal_level_dBm'], _("dBm")
))
info.set_halign(Gtk.Align.START) info.set_halign(Gtk.Align.START)
#info.set_markup(self.get_file_info_str(essid)) #info.set_markup(self.get_file_info_str(ssid))
labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
labels.add(name) labels.add(name)
labels.add(info) labels.add(info)
@@ -133,37 +143,270 @@ class NetworkPanel(ScreenPanel):
labels.set_valign(Gtk.Align.CENTER) labels.set_valign(Gtk.Align.CENTER)
labels.set_halign(Gtk.Align.START) labels.set_halign(Gtk.Align.START)
actions = self._gtk.ButtonImage("print",None,"color3") connect = self._gtk.ButtonImage("load",None,"color3")
#actions.connect("clicked", self.confirm_print, essid) connect.connect("clicked", self.connect_network, ssid)
actions.set_hexpand(False) connect.set_hexpand(False)
actions.set_halign(Gtk.Align.END) connect.set_halign(Gtk.Align.END)
delete = self._gtk.ButtonImage("delete","","color3")
delete.connect("clicked", self.remove_wifi_network, ssid)
delete.set_size_request(60,0)
delete.set_hexpand(False)
delete.set_halign(Gtk.Align.END)
network = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) network = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
network.set_hexpand(True) network.set_hexpand(True)
network.set_vexpand(False) network.set_vexpand(False)
network.add(labels) network.add(labels)
if not "connected" in netinfo:
network.add(actions)
self.networks[essid] = frame buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
if network_id != -1:
buttons.pack_end(delete, False, False, 0)
if netinfo['connected'] == False:
buttons.pack_end(connect, False, False, 0)
network.add(buttons)
self.networks[ssid] = frame
frame.add(network) frame.add(network)
reverse = False reverse = False
nets = sorted(self.networks, reverse=reverse)
pos = nets.index(essid)
if "connected" in netinfo:
pos = 0
elif self._screen.wifi.is_connected():
pos += 1
self.labels['networks'][essid] = { pos = 0
if netinfo['connected'] == True:
pos = 0
else:
connected_ssid = self._screen.wifi.get_connected_ssid()
nets = list(self.networks)
if connected_ssid != None:
if connected_ssid in nets:
nets.remove(connected_ssid)
nets = sorted(nets, reverse=reverse)
pos = nets.index(ssid)
if connected_ssid != None:
pos += 1
self.labels['networks'][ssid] = {
"connect": connect,
"delete": delete,
"info": info, "info": info,
"name": name, "name": name,
"row": network "row": network
} }
self.labels['networklist'].insert_row(pos) self.labels['networklist'].insert_row(pos)
self.labels['networklist'].attach(self.networks[essid], 0, pos, 1, 1) self.labels['networklist'].attach(self.networks[ssid], 0, pos, 1, 1)
if show == True: if show == True:
self.labels['networklist'].show_all() self.labels['networklist'].show()
def add_new_network(self, widget, ssid, connect=False):
networks = self._screen.wifi.get_networks()
psk = self.labels['network_psk'].get_text()
result = self._screen.wifi.add_network(ssid, psk)
self.close_add_network(widget, ssid)
if connect == True:
if result == True:
self.connect_network(widget, ssid, False)
else:
self._screen.show_popup_message("Error adding network %s" % ssid)
def check_missing_networks(self):
networks = self._screen.wifi.get_networks()
for net in list(self.networks):
if net in networks:
networks.remove(net)
for net in networks:
self.add_network(net)
self.labels['networklist'].show_all()
def close_add_network(self, widget, ssid):
for child in self.content.get_children():
self.content.remove(child)
self.content.add(self.labels['main_box'])
self.content.show()
def close_dialog(self, widget, response_id):
widget.destroy()
def connected_callback(self, ssid, prev_ssid):
logging.info("Now connected to a new network")
if ssid != None:
self.remove_network(ssid)
if prev_ssid != None:
self.remove_network(prev_ssid)
self.check_missing_networks()
def connect_network(self, widget, ssid, showadd=True):
_ = self.lang.gettext
snets = self._screen.wifi.get_supplicant_networks()
isdef = False
for id, net in snets.items():
if net['ssid'] == ssid:
isdef = True
break
if isdef == False:
if showadd == True:
self.show_add_network(widget, ssid)
return
self.prev_network = self._screen.wifi.get_connected_ssid()
buttons = [
{"name": _("Close"), "response": Gtk.ResponseType.CANCEL}
]
scroll = Gtk.ScrolledWindow()
scroll.set_property("overlay-scrolling", False)
scroll.set_hexpand(True)
scroll.set_vexpand(True)
scroll.set_size_request(800,400)
self.labels['connecting_info'] = Gtk.Label(_("Starting WiFi Re-association"))
self.labels['connecting_info'].set_halign(Gtk.Align.START)
self.labels['connecting_info'].set_valign(Gtk.Align.START)
scroll.add(self.labels['connecting_info'])
dialog = self._gtk.Dialog(self._screen, buttons, scroll, self.close_dialog)
self._screen.show_all()
if ssid in self.networks:
self.remove_network(ssid)
if self.prev_network in self.networks:
self.remove_network(self.prev_network)
#GLib.timeout_add(500, self.add_network, self.prev_network)
self._screen.wifi.add_callback("connecting_status", self.connecting_status_callback)
self._screen.wifi.connect(ssid)
def connecting_status_callback(self, msg):
self.labels['connecting_info'].set_text(self.labels['connecting_info'].get_text() + "\n" + msg)
self.labels['connecting_info'].show_all()
def remove_network(self, ssid, show=True):
if ssid not in self.networks:
return
i = 0
while self.labels['networklist'].get_child_at(0, i) != None:
if self.networks[ssid] == self.labels['networklist'].get_child_at(0, i):
self.labels['networklist'].remove_row(i)
self.labels['networklist'].show()
del self.networks[ssid]
del self.labels['networks'][ssid]
return
i = i+1
return
def remove_network_wid(self, widget, ssid):
self.remove_network(ssid)
def remove_wifi_network(self, widget, ssid):
self._screen.wifi.delete_network(ssid)
self.remove_network(ssid)
self.check_missing_networks()
def scan_callback(self, new_networks, old_networks):
for net in old_networks:
self.remove_network(net, False)
for net in new_networks:
self.add_network(net, False)
self.content.show_all()
def show_add_network(self, widget, ssid):
_ = self.lang.gettext
for child in self.content.get_children():
self.content.remove(child)
if "add_network" in self.labels:
del self.labels['add_network']
self.labels['add_network'] = Gtk.VBox()
self.labels['add_network'].set_valign(Gtk.Align.START)
box = Gtk.Box(spacing=5)
box.set_size_request(self._gtk.get_content_width(), self._gtk.get_content_height() -
self._screen.keyboard_height - 20)
box.set_hexpand(True)
box.set_vexpand(False)
self.labels['add_network'].add(box)
l = self._gtk.Label("%s %s:" % (_("PSK for"), ssid))
l.set_hexpand(False)
entry = Gtk.Entry()
entry.set_hexpand(True)
save = self._gtk.ButtonImage("sd",_("Save"),"color3")
save.set_hexpand(False)
save.connect("clicked", self.add_new_network, ssid, True)
self.labels['network_psk'] = entry
box.pack_start(l, False, False, 5)
box.pack_start(entry, True, True, 5)
box.pack_start(save, False, False, 5)
self.show_create = True
self.labels['network_psk'].set_text('')
self.content.add(self.labels['add_network'])
self.content.show()
self._screen.show_keyboard()
self.labels['network_psk'].grab_focus_without_selecting()
def update_all_networks(self):
for network in list(self.networks):
self.update_network_info(network)
return True
def update_network_info(self, ssid):
_ = self.lang.gettext
if ssid not in self.networks or ssid not in self.labels['networks']:
return
netinfo = self._screen.wifi.get_network_info(ssid)
if netinfo == None:
logging.debug("Couldn't get netinfo for update")
return
connected = ""
if netinfo['connected'] == True:
stream = os.popen('hostname -f')
hostname = stream.read().strip()
ifadd = netifaces.ifaddresses(self.interface)
ipv4 = ""
ipv6 = ""
if netifaces.AF_INET in ifadd and len(ifadd[netifaces.AF_INET]) > 0:
ipv4 = "<b>%s:</b> %s " % (_("IPv4"), ifadd[netifaces.AF_INET][0]['addr'])
if netifaces.AF_INET6 in ifadd and len(ifadd[netifaces.AF_INET6]) > 0:
ipv6 = ipv6 = "<b>%s:</b> %s " % (_("IPv6"), ifadd[netifaces.AF_INET6][0]['addr'].split('%')[0])
connected = "<b>%s</b>\n<b>%s:</b> %s\n%s%s\n" % (_("Connected"),_("Hostname"),hostname, ipv4, ipv6)
elif "psk" in netinfo:
connected = "Password saved."
freq = "2.4 GHz" if netinfo['frequency'][0:1] == "2" else "5 Ghz"
self.labels['networks'][ssid]['info'].set_markup("%s%s <small>%s %s %s %s%s</small>" % ( connected,
"" if netinfo['encryption'] == "off" else netinfo['encryption'].upper(),
freq, _("Channel"), netinfo['channel'], netinfo['signal_level_dBm'], _("dBm")
))
self.labels['networks'][ssid]['info'].show_all()
def update_single_network_info(self):
_ = self.lang.gettext
stream = os.popen('hostname -f')
hostname = stream.read().strip()
ifadd = netifaces.ifaddresses(self.interface)
ipv4 = ""
ipv6 = ""
if netifaces.AF_INET in ifadd and len(ifadd[netifaces.AF_INET]) > 0:
ipv4 = "<b>%s:</b> %s " % (_("IPv4"), ifadd[netifaces.AF_INET][0]['addr'])
if netifaces.AF_INET6 in ifadd and len(ifadd[netifaces.AF_INET6]) > 0:
ipv6 = ipv6 = "<b>%s:</b> %s " % (_("IPv6"), ifadd[netifaces.AF_INET6][0]['addr'].split('%')[0])
connected = "<b>%s</b>\n\n<small><b>%s</b></small>\n<b>%s:</b> %s\n%s\n%s\n" % (self.interface, _("Connected"),_("Hostname"),
hostname, ipv4, ipv6)
self.labels['networkinfo'].set_markup(connected)
self.labels['networkinfo'].show_all()

View File

@@ -7,6 +7,7 @@ import time
import threading import threading
import json import json
import netifaces
import requests import requests
import websocket import websocket
import importlib import importlib
@@ -88,8 +89,12 @@ class KlipperScreen(Gtk.Window):
self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', fallback=True) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', fallback=True)
self._config = KlipperScreenConfig(configfile, self.lang, self) self._config = KlipperScreenConfig(configfile, self.lang, self)
self.wifi = WifiManager() self.network_interfaces = netifaces.interfaces()
self.wifi.start() self.wireless_interfaces = [int for int in self.network_interfaces if int.startswith('w')]
self.wifi = None
if len(self.wireless_interfaces) > 0:
logging.info("Found wireless interfaces: %s" % self.wireless_interfaces)
self.wifi = WifiManager(self.wireless_interfaces[0])
logging.debug("OS Language: %s" % os.getenv('LANG')) logging.debug("OS Language: %s" % os.getenv('LANG'))

View File

@@ -1,6 +1,7 @@
humanize==3.5.0 humanize==3.5.0
jinja2==2.11.3 jinja2==2.11.3
matplotlib==3.4.1 matplotlib==3.4.1
netifaces==0.10.9
requests==2.25.1 requests==2.25.1
vext==0.7.6 vext==0.7.6
websocket-client==0.59.0 websocket-client==0.59.0

View File

@@ -0,0 +1,8 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
<g style="fill:#ffffff;">
<path d="m256 441.142c8.284 0 15-6.716 15-15v-201.409c0-8.284-6.716-15-15-15s-15 6.716-15 15v201.409c0 8.284 6.716 15 15 15z"/>
<path d="m173.412 427.552c.78 8.263 8.115 14.303 16.344 13.523 8.248-.779 14.302-8.096 13.523-16.344l-19.018-201.409c-.779-8.247-8.083-14.303-16.344-13.523-8.248.779-14.302 8.096-13.523 16.344z"/>
<path d="m322.244 441.076c8.238.779 15.564-5.269 16.344-13.523l19.018-201.409c.779-8.248-5.276-15.565-13.523-16.344-8.26-.784-15.565 5.276-16.344 13.523l-19.018 201.409c-.779 8.247 5.276 15.565 13.523 16.344z"/>
<path d="m57.646 168.875h8.967l43.448 330.083c.982 7.463 7.344 13.042 14.872 13.042h262.135c7.528 0 13.889-5.579 14.872-13.042l43.448-330.083h8.967c8.284 0 15-6.716 15-15v-65.629c0-8.284-6.716-15-15-15h-128.357v-5.911c0-37.128-30.207-67.335-67.335-67.335h-5.325c-37.128 0-67.335 30.207-67.335 67.335v5.911h-128.357c-8.284 0-15 6.716-15 15v65.629c0 8.284 6.715 15 15 15zm316.267 313.125h-235.826l-41.215-313.125h318.257zm-157.911-414.665c0-20.586 16.749-37.335 37.335-37.335h5.325c20.586 0 37.335 16.749 37.335 37.335v5.911h-79.995zm-143.356 35.911h366.709v35.629c-3.207 0-362.709 0-366.709 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,71 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="iso-8859-1"?>
<svg <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
xmlns:dc="http://purl.org/dc/elements/1.1/" <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
xmlns:cc="http://creativecommons.org/ns#" viewBox="0 0 478.703 478.703" style="stroke: #ffffff; fill: #ffffff; enable-background:new 0 0 478.703 478.703;" xml:space="preserve">
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" <g>
xmlns:svg="http://www.w3.org/2000/svg" <g>
xmlns="http://www.w3.org/2000/svg" <path style="stroke: #ffffff; fill: #ffffff;" d="M454.2,189.101l-33.6-5.7c-3.5-11.3-8-22.2-13.5-32.6l19.8-27.7c8.4-11.8,7.1-27.9-3.2-38.1l-29.8-29.8
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" c-5.6-5.6-13-8.7-20.9-8.7c-6.2,0-12.1,1.9-17.1,5.5l-27.8,19.8c-10.8-5.7-22.1-10.4-33.8-13.9l-5.6-33.2
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" c-2.4-14.3-14.7-24.7-29.2-24.7h-42.1c-14.5,0-26.8,10.4-29.2,24.7l-5.8,34c-11.2,3.5-22.1,8.1-32.5,13.7l-27.5-19.8
width="64" c-5-3.6-11-5.5-17.2-5.5c-7.9,0-15.4,3.1-20.9,8.7l-29.9,29.8c-10.2,10.2-11.6,26.3-3.2,38.1l20,28.1
height="64" c-5.5,10.5-9.9,21.4-13.3,32.7l-33.2,5.6c-14.3,2.4-24.7,14.7-24.7,29.2v42.1c0,14.5,10.4,26.8,24.7,29.2l34,5.8
viewBox="0 0 64 64" c3.5,11.2,8.1,22.1,13.7,32.5l-19.7,27.4c-8.4,11.8-7.1,27.9,3.2,38.1l29.8,29.8c5.6,5.6,13,8.7,20.9,8.7c6.2,0,12.1-1.9,17.1-5.5
version="1.1" l28.1-20c10.1,5.3,20.7,9.6,31.6,13l5.6,33.6c2.4,14.3,14.7,24.7,29.2,24.7h42.2c14.5,0,26.8-10.4,29.2-24.7l5.7-33.6
id="svg10" c11.3-3.5,22.2-8,32.6-13.5l27.7,19.8c5,3.6,11,5.5,17.2,5.5l0,0c7.9,0,15.3-3.1,20.9-8.7l29.8-29.8c10.2-10.2,11.6-26.3,3.2-38.1
sodipodi:docname="settings.svg" l-19.8-27.8c5.5-10.5,10.1-21.4,13.5-32.6l33.6-5.6c14.3-2.4,24.7-14.7,24.7-29.2v-42.1
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)"> C478.9,203.801,468.5,191.501,454.2,189.101z M451.9,260.401c0,1.3-0.9,2.4-2.2,2.6l-42,7c-5.3,0.9-9.5,4.8-10.8,9.9
<metadata c-3.8,14.7-9.6,28.8-17.4,41.9c-2.7,4.6-2.5,10.3,0.6,14.7l24.7,34.8c0.7,1,0.6,2.5-0.3,3.4l-29.8,29.8c-0.7,0.7-1.4,0.8-1.9,0.8
id="metadata16"> c-0.6,0-1.1-0.2-1.5-0.5l-34.7-24.7c-4.3-3.1-10.1-3.3-14.7-0.6c-13.1,7.8-27.2,13.6-41.9,17.4c-5.2,1.3-9.1,5.6-9.9,10.8l-7.1,42
<rdf:RDF> c-0.2,1.3-1.3,2.2-2.6,2.2h-42.1c-1.3,0-2.4-0.9-2.6-2.2l-7-42c-0.9-5.3-4.8-9.5-9.9-10.8c-14.3-3.7-28.1-9.4-41-16.8
<cc:Work c-2.1-1.2-4.5-1.8-6.8-1.8c-2.7,0-5.5,0.8-7.8,2.5l-35,24.9c-0.5,0.3-1,0.5-1.5,0.5c-0.4,0-1.2-0.1-1.9-0.8l-29.8-29.8
rdf:about=""> c-0.9-0.9-1-2.3-0.3-3.4l24.6-34.5c3.1-4.4,3.3-10.2,0.6-14.8c-7.8-13-13.8-27.1-17.6-41.8c-1.4-5.1-5.6-9-10.8-9.9l-42.3-7.2
<dc:format>image/svg+xml</dc:format> c-1.3-0.2-2.2-1.3-2.2-2.6v-42.1c0-1.3,0.9-2.4,2.2-2.6l41.7-7c5.3-0.9,9.6-4.8,10.9-10c3.7-14.7,9.4-28.9,17.1-42
<dc:type c2.7-4.6,2.4-10.3-0.7-14.6l-24.9-35c-0.7-1-0.6-2.5,0.3-3.4l29.8-29.8c0.7-0.7,1.4-0.8,1.9-0.8c0.6,0,1.1,0.2,1.5,0.5l34.5,24.6
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> c4.4,3.1,10.2,3.3,14.8,0.6c13-7.8,27.1-13.8,41.8-17.6c5.1-1.4,9-5.6,9.9-10.8l7.2-42.3c0.2-1.3,1.3-2.2,2.6-2.2h42.1
<dc:title>folder</dc:title> c1.3,0,2.4,0.9,2.6,2.2l7,41.7c0.9,5.3,4.8,9.6,10,10.9c15.1,3.8,29.5,9.7,42.9,17.6c4.6,2.7,10.3,2.5,14.7-0.6l34.5-24.8
</cc:Work> c0.5-0.3,1-0.5,1.5-0.5c0.4,0,1.2,0.1,1.9,0.8l29.8,29.8c0.9,0.9,1,2.3,0.3,3.4l-24.7,34.7c-3.1,4.3-3.3,10.1-0.6,14.7
</rdf:RDF> c7.8,13.1,13.6,27.2,17.4,41.9c1.3,5.2,5.6,9.1,10.8,9.9l42,7.1c1.3,0.2,2.2,1.3,2.2,2.6v42.1H451.9z"/>
</metadata> <path style="stroke: #ffffff" d="M239.4,136.001c-57,0-103.3,46.3-103.3,103.3s46.3,103.3,103.3,103.3s103.3-46.3,103.3-103.3S296.4,136.001,239.4,136.001
<defs z M239.4,315.601c-42.1,0-76.3-34.2-76.3-76.3s34.2-76.3,76.3-76.3s76.3,34.2,76.3,76.3S281.5,315.601,239.4,315.601z"/>
id="defs14" /> </g>
<sodipodi:namedview </g>
pagecolor="#bfbfbf"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="837"
id="namedview12"
showgrid="false"
inkscape:pagecheckerboard="false"
inkscape:zoom="5.4137931"
inkscape:cx="22.518245"
inkscape:cy="36.93885"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg10"
inkscape:snap-bbox="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:document-rotation="0" />
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
<title
id="title2">folder</title>
<desc
id="desc4">Created with Sketch.</desc>
<path
inkscape:connector-curvature="0"
style="fill:none;stroke:#ffffff;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 43.839401,23.937493 3.706641,-2.524183 0.259084,-0.134983 5.3744,-0.294648 1.48338,3.511368 -3.957867,3.647777 -0.27739,0.09165 -4.393702,0.897813 0.03883,5.53838 4.405864,0.836122 0.278643,0.08775 4.008616,3.591932 -1.434,3.53181 -5.378001,-0.21926 -0.260948,-0.131335 -3.74167,-2.471971 -3.888769,3.943682 2.524183,3.706641 0.134983,0.259084 0.294648,5.3744 -3.511369,1.48338 -3.647776,-3.957867 -0.09165,-0.27739 -0.897814,-4.393702 -5.538379,0.03883 -0.836123,4.405864 -0.08775,0.278643 -3.591932,4.008616 -3.53181,-1.434 0.21926,-5.378001 0.131334,-0.260948 2.471972,-3.74167 -3.943682,-3.888769 -3.706642,2.524183 -0.259083,0.134983 -5.3744,0.294648 -1.4833806,-3.511368 3.9578676,-3.647777 0.277389,-0.09165 4.393703,-0.897814 -0.03882,-5.538379 -4.405864,-0.836123 -0.278644,-0.08775 -4.0086153,-3.591932 1.4339993,-3.53181 5.378001,0.21926 0.260949,0.131334 3.74167,2.471972 3.888769,-3.943682 -2.524183,-3.706642 -0.134983,-0.259083 -0.294648,-5.3744 3.511368,-1.4833806 3.647777,3.9578676 0.09165,0.277389 0.897813,4.393703 5.53838,-0.03882 0.836122,-4.405864 0.08775,-0.278643 3.591931,-4.0086162 3.531811,1.4339992 -0.21926,5.378002 -0.131335,0.260948 -2.471971,3.74167 z"
id="path4605" />
<circle
r="7.4212298"
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5114-6"
cx="32"
cy="32" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB