#!/usr/bin/env python import errno import ConfigParser import logging import logging.config import os import pair import re import select import shutil import socket import struct import sys import time sys.path.append("/usr/bin") import clock import ifconfig import iw import runlevel import udhcpc import wpa_control import wpa_supplicant import rc_lock sololink_conf = "/etc/sololink.conf" wpa_supplicant_conf = "/etc/wpa_supplicant.conf" wpa_supplicant_back = "/etc/wpa_supplicant.back" sololink_version_file = "/VERSION" firmware_version_file = "/PIX_VERSION" controller_sololink_version_file = "/tmp/PEER_SL_VERSION" controller_firmware_version_file = "/tmp/PEER_FW_VERSION" # defaults for items normally read from config file controller_link_port = 5501 wifi_connect_timeout = 5.0 connect_request_interval = 1.0 connect_ack_timeout = 0.5 button_filename = "/dev/input/event0" solo_ip = "10.1.1.10" check_versions = True # It is not critical that the PIN be secret. The reason for using WPS PIN is # not the PIN's security (it is not), but that we can cause hostapd to ask us # for the PIN, at which time get confirmation from the user. secret_pin = 74015887 button_error = False button_file = None runlevel_ready = False # Log a version mismatch once per minute version_mismatch_log_time_us = 0 version_mismatch_log_interval_us = 60 * 1000000 controller_sololink_version = None controller_firmware_version = None ifname = "wlan0" wpa = wpa_control.WpaControl(ifname) # conditions: # paired/not_paired: whether network exists in wpa_supplicant.conf def pair_button(): """get status of pairing button Returns True if pairing button has been pushed since the last call to this function, or False otherwise. """ global button_error, button_file if not button_error and button_file is None: # open on first call try: button_file = open(button_filename) except: button_error = True logger.error("can't open %s for reading", button_filename) pushed = False if not button_error: # read all events, looking for pushes and ignoring others while True: r, w, x = select.select([button_file], [], [], 0) if len(r) == 0: # no more events break # button event event = r[0].read(16) if len(event) != 16: logger.error("event not 16 bytes: len=%d, event=%s", len(event), str([hex(ord(x)) for x in event])) # event is: # struct input_event { # struct timeval time; # unsigned short type; # unsigned short code; # unsigned int value; # }; # time: 8 bytes, not used here # type: EV_SYN=0x0000, EV_KEY=0x0001 # code: KEY_WPS_BUTTON=0x0211 # value: 1 on push, 0 on release # Fields are little endian. # # button push: # xx xx xx xx xx xx xx xx 01 00 11 02 01 00 00 00 # type=0x0001 code=0x0211 value=0x00000001 # xx xx xx xx xx xx xx xx 00 00 00 00 00 00 00 00 # type=0x0000 code=0x0000 value=0x00000000 # # button release: # xx xx xx xx xx xx xx xx 01 00 11 02 00 00 00 00 # type=0x0001 code=0x0211 value=0x00000000 # xx xx xx xx xx xx xx xx 00 00 00 00 00 00 00 00 # type=0x0000 code=0x0000 value=0x00000000 try: s, t, c, v = struct.unpack("@QHHi", event) if t == 0x0001 and c == 0x0211 and v == 1: pushed = True # keep reading events to flush out others except: logger.error("error unpacking input event: %s", str([hex(ord(x)) for x in event])) ### end while True ### end if not button_error return pushed # Returns: # First network name if there is at least one # None if there are no networks def wpa_supplicant_network_get(): """get network from wpa_supplicant.conf Retrieve and return ssid from network={} section in wpa_supplicant.conf. The first network found is returned. """ try: d = wpa_supplicant.read(wpa_supplicant_conf) except: logger.error("can't open %s for reading", wpa_supplicant_conf) return None if "network" in d: for net in d["network"]: # net is a dictionary of net parameters if "ssid" in net: name = net["ssid"][0] # strip leading and trailing quotes if name[0] == "\"" and name[-1] == "\"": name = name[1:-1] return name # no network in wpa_supplicant.conf return None # Returns: # True - Solo is paired # False - Solo is not paired def is_paired(): # Solo is "paired" if there is a network in wpa_supplicant.conf ssid = wpa_supplicant_network_get() return (ssid is not None) # Returns True or False def pin_pair(): logger.info("pin pair...") wpa.pin_pair(secret_pin) state = None last_state = None start_us = clock.gettime_us(clock.CLOCK_MONOTONIC) while True: time.sleep(0.1) stat = wpa.get_status() now_us = clock.gettime_us(clock.CLOCK_MONOTONIC) if "wpa_state" in stat: state = stat["wpa_state"] else: state = None if state != last_state: if "ssid" in stat: ssid = stat["ssid"] else: ssid = "" if "bssid" in stat: bssid = stat["bssid"] else: bssid = "" logger.debug("%0.3f %s %s %s", (now_us - start_us) / 1000000.0, state, ssid, bssid) last_state = state if state == "COMPLETED": break if state == "INACTIVE": break # end while if state == "COMPLETED": wpa.set("update_config", "1") wpa.save() wpa.set("update_config", "0") os.system("md5sum %s > %s.md5" % \ (wpa_supplicant_conf, wpa_supplicant_conf)) os.system("sync") logger.info("pin pair successful") else: logger.info("pin pair failed") return (state == "COMPLETED") # Returns: # True associated # False not associated, timeout def associate(timeout): logger.debug("associate") wpa.reconfigure() return wpa.network_connect(timeout) def disassociate(): logger.debug("disassociate") wpa.network_disconnect() # Returns: # True got IP # False error getting IP def get_ip(): logger.debug("get_ip") udhcpc.start(ifname, hostname="solo") ip_mask = ifconfig.ip_mask(ifname) if ip_mask is None: logger.info("pairing failed: error getting IP address") udhcpc.stop() return False elif ip_mask[0] != solo_ip: # This happens if this Solo and another Solo both know the wifi # password, and: # 1. The controller is paired to and already connected to the other # Solo. This Solo will not get the correct IP unless the controller # restarts. # 2. The controller is paired to this Solo, but the other Solo got on # the wifi network first and got the Solo IP. In that case, the # other Solo will get booted off, and this Solo will get the # correct IP when it retries. logger.info("pairing failed: not at the fixed solo IP address") udhcpc.stop() return False else: logger.info("ip address %s netmask %s", ip_mask[0], ip_mask[1]) return True def release_ip(): logger.debug("release_ip") udhcpc.stop() # Check network link status. If it is good (associated, have Solo IP address), # return True. Otherwise, make sure everything is down (disassociated, udhcpc # stopped, no IP) and return False. # # Returns: # True associated, udhcpc running, at solo IP # False not associated, udhcpc not running, no IP def check_link(): stat = wpa.get_status() if ("wpa_state" in stat) and (stat["wpa_state"] == "COMPLETED"): # wifi is associated ip_mask = ifconfig.ip_mask(ifname) if ip_mask and (ip_mask[0] == solo_ip): return True # something is not right; tear everything down network_down() return False # Returns: # True network is up # False button was pushed (network is not up) def network_up(): logger.info("bringing up network...") if check_link(): logger.info("network already up") return True # check_link either confirms the network is up, or makes sure it is # completely down while True: if associate(wifi_connect_timeout): if get_ip(): logger.info("network is up") return True disassociate() # don't do network_remove_all here if wait_button(2): logger.info("network is down (button detected)") return False def network_down(): release_ip() disassociate() wpa.network_remove_all() # connected to controller, advance runlevel def go_ready(): global runlevel_ready if not runlevel_ready: logger.info("switching to runlevel.READY") runlevel.set(runlevel.READY) runlevel_ready = True def set_controller_versions(pkt): global controller_sololink_version global controller_firmware_version sololink = pkt[4:36].strip('\r\n\t\0 ') firmware = pkt[36:68].strip('\r\n\t\0 ') if controller_sololink_version == sololink and \ controller_firmware_version == firmware: return; controller_sololink_version = sololink controller_firmware_version = firmware try: f = open(controller_sololink_version_file, 'w') f.write("%s\n" % (controller_sololink_version, )) f.close() except: logger.error("error writing controller sololink version to %s", controller_sololink_version_file) try: f = open(controller_firmware_version_file, 'w') f.write("%s\n" % (controller_firmware_version, )) f.close() except: logger.error("error writing controller firmware version to %s", controller_firmware_version_file) logger.info("controller version \"%s\"; firmware \"%s\"", controller_sololink_version, controller_firmware_version) # Returns # Only if button pushed before any response from controller def run_connected(): global version_mismatch_log_time_us ack_received = None logger.info("establishing connection...") controller_adrs = (controller_ip, controller_link_port) confirmed = False pending = False pair_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) pair_sock.bind(("", 0)) # any port pair_sock.settimeout(connect_ack_timeout) send_error_logged = False recv_error_logged = False while True: need_sleep = False if rc_lock.locked(): locked = 1 else: locked = 0 logger.debug("locked=%d", locked) conn_req = struct.pack(" version_mismatch_log_time_us: logger.info("version mismatch: solo=\"%s\", controller=\"%s\"", solo_sololink_version, controller_sololink_version) version_mismatch_log_time_us = now_us + version_mismatch_log_interval_us # Change runlevel even if locked or versions incompatible. # Apps look better if there is telemetry flowing and shotmgr # is running go_ready() elif ord(pkt[2]) == pair.PEND: if not pending: logger.info("connection pending") pending = True else: # pair.NO if not confirmed: # Controller says no. This Solo knows the wifi password # from a previous pairing, but the controller has since # been re-paired to a different Solo. logger.info("connection rejected") network_down() return else: # Controller said yes to a previous connect request, but # is now saying no. We are already in runlevel 4; # something is really messed up. Ignore the nack. logger.error("connection was up, now rejected") else: # mystery packet! logger.error("mystery response received: %s", str([ord(c) for c in pkt])) time.sleep(connect_request_interval) # Wait for pairing button # # Timeout = None means wait forever, else timeout in seconds # # Returns: # True button pushed within timeout # False timeout def wait_button(timeout=None): if timeout is None: end_us = None else: end_us = clock.gettime_us(clock.CLOCK_MONOTONIC) + \ int(timeout * 1000000) while True: if pair_button(): return True if end_us is not None and \ clock.gettime_us(clock.CLOCK_MONOTONIC) >= end_us: return False time.sleep(0.1) def pair_solo(): logger.info("pairing using %s", ifname) # XXX update_config=0 should be default in wpa_supplicant.conf wpa.set("update_config", "0") while True: if is_paired(): if network_up(): run_connected() else: network_down() logger.info("waiting for pairing button") wait_button() pin_pair() if __name__ == "__main__": logging.config.fileConfig(sololink_conf) logger = logging.getLogger("pair") logger.info("pair_solo.py starting") config = ConfigParser.SafeConfigParser() # if the config file is not found, an empty list is returned and the "get" # operations below fail config.read(sololink_conf) # read configuration items try: controller_link_port = config.getint("pairing", "controller_link_port") wifi_connect_timeout = \ config.getfloat("pairing", "wifi_connect_timeout") connect_request_interval = \ config.getfloat("pairing", "connect_request_interval") connect_ack_timeout = config.getfloat("pairing", "connect_ack_timeout") button_filename = config.get("pairing", "button_filename") solo_ip = config.get("solo", "soloIp") controller_ip = config.get("solo", "artooIp") except: logger.error("error reading config from %s", sololink_conf) sys.exit(1) try: check_versions = sololink_config.getboolean("solo", "pairCheckVersions") except: check_versions = True # default logger.info("using default check_versions=%s", str(check_versions)) # read sololink version try: f = open(sololink_version_file, 'r') solo_sololink_version = f.readline() # still has \n solo_sololink_version = solo_sololink_version.strip('\r\n\t\0 ') except: logger.error("error reading version from %s", sololink_version_file) sys.exit(1) logger.info("sololink version \"%s\"", solo_sololink_version) # read firmware version try: f = open(firmware_version_file, 'r') solo_firmware_version = f.readline() # still has \n solo_firmware_version = solo_firmware_version.strip('\r\n\t\0 ') except: logger.error("error reading version from %s", firmware_version_file) solo_firmware_version = "unknown" logger.info("firmware version \"%s\"", solo_firmware_version) # If /etc/.rc_lock exists, delete it (SOLO-709) if os.path.isfile("/etc/.rc_lock"): logger.info("deleting /etc/.rc_lock") os.unlink("/etc/.rc_lock") pair_solo() # pair_solo never returns