network: improve NetworkManager support

This commit is contained in:
alfrix 2022-11-23 21:09:39 -03:00
parent db48bb8a1f
commit c630178f0c
5 changed files with 125 additions and 154 deletions

View File

@ -116,7 +116,7 @@ and reboot, that should make the touch work, if your screen is rotated 180 degre
KlipperScreen was never intended to be used with OctoPrint, and there is no support for it.
## WiFi networks not listed
## WiFi networks not listed (Using wpa_supplicant as backend)
This can be caused because of the user is not allowed to control the interface
@ -137,13 +137,42 @@ usermod -a -G netdev pi
Then reboot the machine:
```
```sh
systemctl reboot
```
!!! tip
It's possible to just restart KlipperScreen and networking
## WiFi networks not listed (Using NetworkManager as backend)
`[wifi_nm.py:rescan()] [...] NetworkManager.wifi.scan request failed: not authorized`
If you see the above permission error in the log you may need to use polkit or disable it:
```sh
mkdir -p /etc/NetworkManager/conf.d
sudo nano /etc/NetworkManager/conf.d/any-user.conf
```
in the editor paste this:
```conf
[main]
auth-polkit=false
```
Then restart the service:
```sh
systemctl restart NetworkManager.service
```
!!! tip
It's possible to just restart KlipperScreen and NetworkManager
## Other issues
If you found an issue not listed here, or can't make it work, please provide all the log files

View File

@ -16,31 +16,25 @@ class WifiManager:
networks_in_supplicant = []
connected = False
_stop_loop = False
thread = None
def __init__(self, interface, *args, **kwargs):
super().__init__(*args, **kwargs)
self.loop = None
self._poll_task = None
self._scanning = False
self._callbacks = {
"connected": [],
"connecting_status": [],
"scan_results": []
"scan_results": [],
"popup": [],
}
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.supplicant_networks = {}
self.queue = Queue()
self.tasks = []
self.timeout = None
self.scan_time = 0
ks_socket_file = "/tmp/.KS_wpa_supplicant"
if os.path.exists(ks_socket_file):
@ -115,7 +109,7 @@ class WifiManager:
return False
logging.info(f"Attempting to connect to wifi: {netid}")
self.connecting_info = [f"Attempting to connect to {ssid}"]
self.callback("connecting_status", f"Attempting to connect to {ssid}")
self.wpa_cli(f"SELECT_NETWORK {id}")
self.save_wpa_conf()
@ -195,7 +189,7 @@ class WifiManager:
for net in self.networks:
if mac == net['mac']:
return net
return None
return {}
def get_networks(self):
return list(self.networks)
@ -203,12 +197,6 @@ class WifiManager:
def get_supplicant_networks(self):
return self.supplicant_networks
def is_connected(self):
return self.connected
def is_initialized(self):
return self.initialized
def read_wpa_supplicant(self):
results = self.wpa_cli("LIST_NETWORKS").split('\n')
results.pop(0)
@ -222,13 +210,8 @@ class WifiManager:
}
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")
@ -340,9 +323,6 @@ class WpaSocket(Thread):
def skip_command(self):
self.skip_commands = self.skip_commands + 1
def stop(self):
self._stop_loop = True
class WifiChannels:
@staticmethod

View File

@ -1,38 +1,22 @@
# Network in KlipperScreen is a connection in NetworkManager
# Interface in KlipperScreen is a device in NetworkManager
# Todo:
# + Disable hotspot autoconnect when page is showing.
# - Use the security provided by the AP when adding a network
# + Consider removing hotspot from list of APs
# + Handle hidden networks or networks with no SSID better.
# - Fix responsiveness issue. Might be realated to DBusGMainLoop
# - The IP address for the ethernet is not right. It is from wifi.
# - Avahi does not announce the ethernet IP.
# - settings = con.GetSettings() sometimes fails
# - When adding and removing connections, make sure known_connections is updated as well.
import os
import contextlib
import logging
import re
import socket
import threading
from threading import Thread
import NetworkManager
from queue import Queue
import uuid
from dbus.mainloop.glib import DBusGMainLoop
import NetworkManager
import dbus
from dbus.mainloop.glib import DBusGMainLoop
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import GLib, Gdk
from gi.repository import GLib
from ks_includes.wifi import WifiChannels
class WifiManagerNM():
class WifiManager:
networks_in_supplicant = []
def __init__(self, interface_name, *args, **kwargs):
@ -41,14 +25,14 @@ class WifiManagerNM():
self._callbacks = {
"connected": [],
"connecting_status": [],
"scan_results": []
"scan_results": [],
"popup": [],
}
self.connected = False
self.connected_ssid = None
self.connecting_info = []
self.interface_name = interface_name
self.known_networks = {} # List of known connections
self.visible_networks = {} # List of visible access points
self.known_networks = {} # List of known connections
self.visible_networks = {} # List of visible access points
self.ssid_by_path = {}
self.path_by_ssid = {}
self.hidden_ssid_index = 0
@ -58,31 +42,26 @@ class WifiManagerNM():
self.wifi_dev.OnAccessPointRemoved(self._ap_removed)
self.wifi_dev.OnStateChanged(self._ap_state_changed)
for ap in self.wifi_dev.GetAccessPoints():
self._add_ap(ap)
self._update_known_connections()
self._set_autoconnect_on_hotspot(False)
self.initialized = True
def _update_known_connections(self):
self.known_networks = {}
for con in NetworkManager.Settings.ListConnections():
settings = con.GetSettings()
if "802-11-wireless" in settings and settings["802-11-wireless"]['ssid'] != "Recore":
if "802-11-wireless" in settings:
ssid = settings["802-11-wireless"]['ssid']
self.known_networks[ssid] = con
def _ap_added(self, nm, interface, signal, access_point):
try:
with contextlib.suppress(NetworkManager.ObjectVanished):
access_point.OnPropertiesChanged(self._ap_prop_changed)
ssid = self._add_ap(access_point)
for cb in self._callbacks['scan_results']:
Gdk.threads_add_idle(
GLib.PRIORITY_DEFAULT_IDLE,
cb, [ssid], [])
except NetworkManager.ObjectVanished:
pass
args = (cb, [ssid], [])
GLib.idle_add(*args)
def _ap_removed(self, dev, interface, signal, access_point):
path = access_point.object_path
@ -90,41 +69,52 @@ class WifiManagerNM():
ssid = self.ssid_by_path[path]
self._remove_ap(path)
for cb in self._callbacks['scan_results']:
Gdk.threads_add_idle(
GLib.PRIORITY_DEFAULT_IDLE,
cb, [], [ssid])
args = (cb, [ssid], [])
GLib.idle_add(*args)
def _ap_state_changed(self, nm, interface, signal, old_state, new_state, reason):
msg = ""
if new_state == NetworkManager.NM_DEVICE_STATE_UNKNOWN:
msg = "the device's state is unknown"
if new_state in (NetworkManager.NM_DEVICE_STATE_UNKNOWN, NetworkManager.NM_DEVICE_STATE_REASON_UNKNOWN):
msg = "State is unknown"
elif new_state == NetworkManager.NM_DEVICE_STATE_UNMANAGED:
msg = "the device is recognized, but not managed by NetworkManager"
msg = "Error: Not managed by NetworkManager"
elif new_state == NetworkManager.NM_DEVICE_STATE_UNAVAILABLE:
msg = "the device is managed by NetworkManager, but is not available for use. Reasons may include the wireless switched off, missing firmware, no ethernet carrier, missing supplicant or modem manager, etc."
msg = "Error: Not available for use:\nReasons may include the wireless switched off, missing firmware, etc."
elif new_state == NetworkManager.NM_DEVICE_STATE_DISCONNECTED:
msg = "the device can be activated, but is currently idle and not connected to a network."
msg = "Currently disconnected"
elif new_state == NetworkManager.NM_DEVICE_STATE_PREPARE:
msg = "the device is preparing the connection to the network."
msg = "Preparing the connection to the network"
elif new_state == NetworkManager.NM_DEVICE_STATE_CONFIG:
msg = "the device is connecting to the requested network."
msg = "Connecting to the requested network..."
elif new_state == NetworkManager.NM_DEVICE_STATE_NEED_AUTH:
msg = "the device requires more information to continue connecting to the requested network."
msg = "Authorizing"
elif new_state == NetworkManager.NM_DEVICE_STATE_IP_CONFIG:
msg = "the device is requesting IPv4 and/or IPv6 addresses and routing information from the network."
msg = "Requesting IP addresses and routing information"
elif new_state == NetworkManager.NM_DEVICE_STATE_IP_CHECK:
msg = "the device is checking whether further action is required for the requested network connection."
msg = "Checking whether further action is required for the requested network connection"
elif new_state == NetworkManager.NM_DEVICE_STATE_SECONDARIES:
msg = "Waiting for a secondary connection (like a VPN)"
elif new_state == NetworkManager.NM_DEVICE_STATE_ACTIVATED:
msg = "Connected"
elif new_state == NetworkManager.NM_DEVICE_STATE_DEACTIVATING:
msg = "A disconnection from the current network connection was requested"
elif new_state == NetworkManager.NM_DEVICE_STATE_FAILED:
msg = "Failed to connect to the requested network"
self.callback("popup", msg)
elif new_state == NetworkManager.NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
msg = "A dependency of the connection failed"
elif new_state == NetworkManager.NM_DEVICE_STATE_REASON_CARRIER:
msg = ""
else:
logging.info(f"State {new_state}")
if msg != "":
self.callback("connecting_status", msg)
if new_state == NetworkManager.NM_DEVICE_STATE_ACTIVATED:
self.connected = True
for cb in self._callbacks['connected']:
Gdk.threads_add_idle(
GLib.PRIORITY_DEFAULT_IDLE,
cb, self.get_connected_ssid(), None)
args = (cb, self.get_connected_ssid(), None)
GLib.idle_add(*args)
else:
self.connected = False
@ -134,7 +124,7 @@ class WifiManagerNM():
def _add_ap(self, ap):
ssid = ap.Ssid
if ssid == "":
ssid = f"(hidden-{self.hidden_ssid_index})"
ssid = _("Hidden") + f" {self.hidden_ssid_index}"
self.hidden_ssid_index += 1
self.ssid_by_path[ap.object_path] = ssid
self.path_by_ssid[ssid] = ap.object_path
@ -142,21 +132,17 @@ class WifiManagerNM():
return ssid
def _remove_ap(self, path):
ssid = self.ssid_by_path.pop(path, None)
self.ssid_by_path.pop(path, None)
self.visible_networks.pop(path, None)
def add_callback(self, name, callback):
if name in self._callbacks and callback not in self._callbacks[name]:
self._callbacks[name].append(callback)
def remove_callback(self, name, callback):
if name in self._callbacks and callback in self._callbacks[name]:
self._callbacks[name].remove(callback)
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 callback(self, cb_type, msg):
if cb_type in self._callbacks:
for cb in self._callbacks[cb_type]:
GLib.idle_add(cb, msg)
def add_network(self, ssid, psk):
aps = self._visible_networks_by_ssid()
@ -185,22 +171,23 @@ class WifiManagerNM():
'method': 'auto'
}
}
NetworkManager.Settings.AddConnection(new_connection)
try:
NetworkManager.Settings.AddConnection(new_connection)
except dbus.exceptions.DBusException as e:
msg = _("Invalid password") if "802-11-wireless-security.psk" in e else f"{e}"
self.callback("popup", msg)
logging.info(f"Error adding network {e}")
self._update_known_connections()
return True
def connect(self, ssid):
if ssid in self.known_networks:
conn = self.known_networks[ssid]
try:
logging.info("Attempting to connect to wifi: %s" % id)
with contextlib.suppress(NetworkManager.ObjectVanished):
msg = f"Connecting to: {ssid}"
logging.info(msg)
self.callback("connecting_status", msg)
NetworkManager.NetworkManager.ActivateConnection(conn, self.wifi_dev, "/")
except NetworkManager.ObjectVanished:
pass
def _get_known_connections_by_uuid(self):
connections = NetworkManager.Settings.ListConnections()
return dict([(x.GetSettings()['connection']['uuid'], x) for x in connections])
def delete_network(self, ssid):
for ssid in self.known_networks:
@ -221,30 +208,27 @@ class WifiManagerNM():
aps = self.wifi_dev.GetAccessPoints()
ret = {}
for ap in aps:
try:
with contextlib.suppress(NetworkManager.ObjectVanished):
ret[ap.Ssid] = ap
except NetworkManager.ObjectVanished:
pass
return ret
def get_network_info(self, ssid):
netinfo = {}
if ssid in self.known_networks:
con = self.known_networks[ssid]
try:
with contextlib.suppress(NetworkManager.ObjectVanished):
settings = con.GetSettings()
if settings and '802-11-wireless' in settings:
return {
netinfo.update({
"ssid": settings['802-11-wireless']['ssid'],
"connected": self.get_connected_ssid() == ssid
}
except NetworkManager.ObjectVanished:
pass
})
path = self.path_by_ssid[ssid]
aps = self.visible_networks
if path in aps:
ap = aps[path]
try:
return {
with contextlib.suppress(NetworkManager.ObjectVanished):
netinfo.update({
"mac": ap.HwAddress,
"channel": WifiChannels.lookup(str(ap.Frequency))[1],
"configured": ssid in self.known_networks,
@ -254,22 +238,22 @@ class WifiManagerNM():
"connected": self._get_connected_ap() == ap,
"encryption": self._get_encryption(ap.RsnFlags),
"signal_level_dBm": str(ap.Strength)
}
except NetworkManager.ObjectVanished:
pass
})
return netinfo
def _get_encryption(self, flags):
@staticmethod
def _get_encryption(flags):
encryption = ""
if (flags & NetworkManager.NM_802_11_AP_SEC_PAIR_WEP40 or
flags & NetworkManager.NM_802_11_AP_SEC_PAIR_WEP104 or
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_WEP40 or
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_WEP104):
flags & NetworkManager.NM_802_11_AP_SEC_PAIR_WEP104 or
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_WEP40 or
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_WEP104):
encryption += "WEP "
if (flags & NetworkManager.NM_802_11_AP_SEC_PAIR_TKIP or
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_TKIP):
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_TKIP):
encryption += "TKIP "
if (flags & NetworkManager.NM_802_11_AP_SEC_PAIR_CCMP or
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_CCMP):
flags & NetworkManager.NM_802_11_AP_SEC_GROUP_CCMP):
encryption += "AES "
if flags & NetworkManager.NM_802_11_AP_SEC_KEY_MGMT_PSK:
encryption += "WPA-PSK "
@ -281,26 +265,10 @@ class WifiManagerNM():
return list(set(list(self.known_networks.keys()) + list(self.ssid_by_path.values())))
def get_supplicant_networks(self):
return {ssid:{"ssid": ssid} for ssid in self.known_networks.keys()}
def is_connected(self):
return self.connected
def is_initialized(self):
return self.initialized
def _set_autoconnect_on_hotspot(self, value):
for con in NetworkManager.Settings.ListConnections():
settings = con.GetSettings()
if "802-11-wireless" in settings and settings["802-11-wireless"]['ssid'] == "Recore":
old_val = settings["connection"]["autoconnect"]
if old_val != value:
settings["connection"]["autoconnect"] = value
con.Update(settings)
return {ssid: {"ssid": ssid} for ssid in self.known_networks.keys()}
def rescan(self):
try:
self.wifi_dev.RequestScan({})
except dbus.exceptions.DBusException:
return False
return True
except dbus.exceptions.DBusException as e:
logging.error(f"Error during rescan {e}")

View File

@ -26,16 +26,16 @@ class NetworkPanel(ScreenPanel):
self.network_interfaces = netifaces.interfaces()
self.wireless_interfaces = [iface for iface in self.network_interfaces if iface.startswith('w')]
self.wifi = None
self.use_network_manager = os.system('systemctl is-active --quiet NetworkManager.service') == 0
if len(self.wireless_interfaces) > 0:
logging.info(f"Found wireless interfaces: {self.wireless_interfaces}")
if self.use_network_manager:
from ks_includes.wifi_nm import WifiManagerNM
self.wifi = WifiManagerNM(self.wireless_interfaces[0])
logging.info("Using NetworkManager")
from ks_includes.wifi_nm import WifiManager
else:
logging.info("Using wpa_cli")
from ks_includes.wifi import WifiManager
self.wifi = WifiManager(self.wireless_interfaces[0])
self.wifi = WifiManager(self.wireless_interfaces[0])
# Get IP Address
gws = netifaces.gateways()
@ -83,7 +83,7 @@ class NetworkPanel(ScreenPanel):
self.labels['networklist'] = Gtk.Grid()
if self.wifi is not None and self.wifi.is_initialized():
if self.wifi is not None and self.wifi.initialized:
box.pack_start(sbox, False, False, 5)
box.pack_start(scroll, True, True, 0)
@ -92,6 +92,7 @@ class NetworkPanel(ScreenPanel):
self.wifi.add_callback("connected", self.connected_callback)
self.wifi.add_callback("scan_results", self.scan_callback)
self.wifi.add_callback("popup", self.popup_callback)
if self.update_timeout is None:
self.update_timeout = GLib.timeout_add_seconds(5, self.update_all_networks)
else:
@ -108,13 +109,10 @@ class NetworkPanel(ScreenPanel):
def load_networks(self):
networks = self.wifi.get_networks()
if not networks:
return
for net in networks:
self.add_network(net, False)
self.update_all_networks()
self.content.show_all()
@ -124,7 +122,6 @@ class NetworkPanel(ScreenPanel):
return
ssid = ssid.strip()
if ssid in list(self.networks):
logging.info("SSID already listed")
return
configured_networks = self.wifi.get_supplicant_networks()
@ -256,6 +253,9 @@ class NetworkPanel(ScreenPanel):
del self.labels[i]
self.show_add = False
def popup_callback(self, msg):
self._screen.show_popup_message(msg)
def connected_callback(self, ssid, prev_ssid):
logging.info("Now connected to a new network")
if ssid is not None:
@ -284,12 +284,7 @@ class NetworkPanel(ScreenPanel):
{"name": _("Close"), "response": Gtk.ResponseType.CANCEL}
]
scroll = Gtk.ScrolledWindow()
scroll.set_property("overlay-scrolling", False)
scroll.set_hexpand(True)
scroll.set_vexpand(True)
scroll.add_events(Gdk.EventMask.TOUCH_MASK)
scroll.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
scroll = self._gtk.ScrolledWindow()
self.labels['connecting_info'] = Gtk.Label(_("Starting WiFi Association"))
self.labels['connecting_info'].set_halign(Gtk.Align.START)
self.labels['connecting_info'].set_valign(Gtk.Align.START)
@ -387,8 +382,6 @@ class NetworkPanel(ScreenPanel):
logging.info(f"Unknown SSID {ssid}")
return
netinfo = self.wifi.get_network_info(ssid)
if netinfo is None:
netinfo = {}
if "connected" in netinfo:
connected = netinfo['connected']
else:
@ -443,7 +436,7 @@ class NetworkPanel(ScreenPanel):
def reload_networks(self, widget=None):
self.networks = {}
self.labels['networklist'].remove_column(0)
if self.wifi is not None and self.wifi.is_initialized():
if self.wifi is not None and self.wifi.initialized:
self.wifi.rescan()
GLib.idle_add(self.load_networks)
@ -451,7 +444,7 @@ class NetworkPanel(ScreenPanel):
if self.initialized:
self.reload_networks()
if self.update_timeout is None:
if self.wifi is not None and self.wifi.is_initialized():
if self.wifi is not None and self.wifi.initialized:
self.update_timeout = GLib.timeout_add_seconds(5, self.update_all_networks)
else:
self.update_timeout = GLib.timeout_add_seconds(5, self.update_single_network_info)

View File

@ -4,3 +4,4 @@ requests==2.28.1
websocket-client==1.4.2
pycairo==1.21.0
PyGObject==3.42.2
python-networkmanager==2.2