00001 import radio
00002 import time
00003 import state_publisher
00004 import config
00005 from twisted.internet import reactor
00006 import mac_addr
00007 import event
00008 import multiset
00009 from twisted.internet.defer import inlineCallbacks
00010 import async_helpers
00011 import random
00012 import logging_config
00013
00014 scan_periods_log = logging_config.get_logger_stream_for_file('scan_periods')
00015 known_bsses_log = logging_config.get_logger_stream_for_file('known_bsses')
00016 radio_manager_decisions = logging_config.get_logger_stream_for_file('console.radio_manager_decisions')
00017
00018 def check_band(freq, bands):
00019 if freq < 3000 and bands & 1:
00020 return True
00021 if freq > 4000 and bands & 2:
00022 return True
00023 return False
00024
00025 class Frequency:
00026 def __init__(self, freq, period):
00027 self.frequency = freq
00028 self.period = period
00029 self.next_scan_time_by_iface = {}
00030 self.next_scan_time = 0
00031 self.do_not_retry_before = {}
00032 self.interfaces = multiset.Multiset()
00033 self.last_hit = 0
00034
00035 def update_next_scan_time(self, iface, when):
00036 self.next_scan_time = when
00037 self.next_scan_time_by_iface[iface] = when
00038
00039 def set_period(self, period):
00040 when = time.time() + period
00041 self.period = period
00042 self.next_scan_time = min(self.next_scan_time, when)
00043 for iface in self.next_scan_time_by_iface:
00044 self.next_scan_time_by_iface[iface] = min(self.next_scan_time_by_iface[iface], when)
00045
00046 class NoFrequenciesReady(Exception):
00047 def __init__(self, next_time):
00048 Exception.__init__(self)
00049 self.next_time = next_time
00050
00051 class FrequencyList:
00052 def __init__(self):
00053 self.frequencies = {}
00054 self.bands = 0
00055 self.scan_period_cold = config.get_parameter('scan_period_cold', 10)
00056 self.scan_reschedule_delay = config.get_parameter('scan_reschedule_delay', 2)
00057 self.scan_period_randomness = config.get_parameter('scan_period_randomness', 0)
00058 self.min_scan_interval = config.get_parameter('min_scan_interval', 1)
00059
00060 def add(self, freq, iface):
00061 if freq not in self.frequencies:
00062 self.frequencies[freq] = Frequency(freq, self.scan_period_cold)
00063 self.frequencies[freq].interfaces.add(iface)
00064
00065 def remove(self, freq, iface):
00066 self.frequencies[freq].interfaces.remove(iface)
00067
00068 def next_scan_freq(self, iface, now = None, allow_early = False):
00069 if now == None:
00070 now = time.time()
00071
00072
00073 active_freqs = filter(lambda f: iface in f.interfaces and check_band(f.frequency, self.bands), self.frequencies.itervalues())
00074
00075
00076 try:
00077 freq = min(active_freqs, key = lambda f: f.next_scan_time)
00078 except ValueError:
00079 raise NoFrequenciesReady(None)
00080
00081
00082 if freq.next_scan_time > now:
00083
00084 freq = min(active_freqs, key = lambda f: f.next_scan_time_by_iface.get(iface,0))
00085
00086 earliest_scan_time = freq.do_not_retry_before.get(iface, 0)
00087 if not allow_early:
00088 earliest_scan_time = max(earliest_scan_time, freq.next_scan_time)
00089 if earliest_scan_time > now:
00090 raise NoFrequenciesReady(earliest_scan_time)
00091 rand_period = freq.period * (0.5 + self.scan_period_randomness * random.random())
00092 freq.update_next_scan_time(iface, now + rand_period)
00093 freq.do_not_retry_before[iface] = now + self.min_scan_interval
00094 return freq.frequency
00095
00096 def next_scan_freqs(self, iface, count, allow_early = False):
00097 now = time.time()
00098 freqs = []
00099 for i in range(count):
00100 try:
00101 f = self.next_scan_freq(iface, now, allow_early)
00102 except NoFrequenciesReady, nfr:
00103 if freqs:
00104 break
00105 raise
00106 freqs.append(f)
00107 return freqs
00108
00109 def set_freq_period(self, freq, period):
00110 f = self.frequencies[freq]
00111 f.set_period(period)
00112
00113 def reschedule(self, iface, freqs):
00114 when = time.time() + self.scan_reschedule_delay
00115 for f in freqs:
00116 self.frequencies[f].update_next_scan_time(iface, when)
00117
00118 def hit(self, freq, stamp):
00119 """Called each time a bss is seen."""
00120 try:
00121 self.frequencies[freq].last_hit = max(self.frequencies[freq].last_hit, stamp)
00122 except KeyError:
00123 print "Got a scan on an unexpected frequency."
00124 pass
00125
00126 def make_id(bss):
00127 return (bss.ssid, bss.bssid)
00128
00129 class Bss:
00130 def __init__(self, bss):
00131 self.id = make_id(bss)
00132 self.ssid = bss.ssid
00133 self.bssid = bss.bssid
00134 self.by_iface = {}
00135 self.frequency = bss.frequency
00136
00137 def update(self, bss, iface):
00138 assert bss.ssid == self.ssid
00139 assert bss.bssid == self.bssid
00140 if self.frequency != bss.frequency:
00141 print "Frequency for bss %s, %s has changed from %i to %i MHz."%(mac_addr.pretty(bss.bssid), bss.ssid, self.frequency, bss.frequency)
00142 self.frequency = bss.frequency
00143 self.by_iface[iface] = bss
00144
00145 def last_seen(self, iface = None):
00146 if iface is None:
00147 return max(bss.stamp.to_sec() for bss in self.by_iface.itervalues())
00148 if iface in self.by_iface:
00149 return self.by_iface[iface].stamp.to_sec()
00150 else:
00151 return 0
00152
00153 class BssList:
00154 def __init__(self, freq_list):
00155 self.bsses = {}
00156 self.freq_list = freq_list
00157 pass
00158
00159 def update(self, bsses, iface):
00160 for bss in bsses:
00161 id = (bss.ssid, bss.bssid)
00162 if id not in self.bsses:
00163 self.bsses[id] = Bss(bss)
00164 self.bsses[id].update(bss, iface)
00165 self.freq_list.hit(bss.frequency, bss.stamp.to_sec())
00166
00167 if True:
00168 all = self.bsses.items()
00169 all.sort(key=lambda (x,y):(y.frequency, x))
00170 now = time.time()
00171 print >> known_bsses_log, "\033[2J\033[0;0H"
00172 print >> known_bsses_log, "Known BSSes bsses:"
00173 for _, bss in all:
00174 print >> known_bsses_log, mac_addr.pretty(bss.bssid), "%20.20s"%bss.ssid, bss.frequency,
00175 ifaces = bss.by_iface.keys()
00176 ifaces.sort()
00177 min_stamp = now - max(bss.by_iface.itervalues(), key = lambda bss: bss.stamp).stamp.to_sec()
00178 max_level = max(bss.by_iface.itervalues(), key = lambda bss: bss.level).level
00179 print >> known_bsses_log, "%5.1f/%3i"%(min_stamp,max_level),
00180 for iface in ifaces:
00181 print >> known_bsses_log, iface.iface, "%5.1f/%3i"%(now - bss.by_iface[iface].stamp.to_sec(), bss.by_iface[iface].level),
00182 print >> known_bsses_log
00183 print >> known_bsses_log
00184
00185 fl = self.freq_list.frequencies.keys()
00186 fl.sort()
00187 for f in fl:
00188 fc = self.freq_list.frequencies[f]
00189 if fc.last_hit:
00190 print >> known_bsses_log, f, "%5.1f"%(now - fc.last_hit),
00191 else:
00192 print >> known_bsses_log, f, "never",
00193 print >> known_bsses_log, "%5.1f"%(fc.next_scan_time - now),
00194 for iface in fc.next_scan_time_by_iface:
00195 print >> known_bsses_log, "%s %5.1f"%(iface.iface, now - fc.next_scan_time_by_iface[iface]),
00196 print >> known_bsses_log
00197 print >> known_bsses_log
00198
00199 class ScanManager:
00200 def __init__(self):
00201 self.failed_scan_delay = config.get_parameter('failed_scan_delay', 1)
00202 self.scan_results = {}
00203 self.frequencies = FrequencyList()
00204 self.num_scan_frequencies = config.get_parameter('num_scan_frequencies', 4)
00205 self.scheduled_scan = {}
00206 self.bss_list = BssList(self.frequencies)
00207 self.new_scan_data = event.Event()
00208
00209 def add_iface(self, iface):
00210
00211 iface.radio_sm.scanning.subscribe(self._scanning_state_cb, iface)
00212 iface.radio_sm.associated.subscribe(self._scanning_state_cb, iface)
00213 iface.radio_sm.associated.subscribe(self._log_associations, iface)
00214 iface.radio_sm.scanning_enabled.subscribe(self._scanning_state_cb, iface)
00215
00216
00217 iface.radio_sm.scan_results_event.subscribe_repeating(self._scan_event_cb, iface)
00218
00219 iface.radio_sm.frequency_list.subscribe(self._frequency_list_cb, iface)
00220
00221 def _scanning_state_cb(self, iface, old_state, new_state):
00222 """A state change that might cause us to start scanning has occurred."""
00223 self._trigger_scan(iface)
00224
00225 def _log_associations(self, iface, old_state, new_state):
00226 if new_state:
00227 print >> radio_manager_decisions, "--> Associated to %s on %s."%(mac_addr.pretty(new_state.bssid), iface.iface)
00228 elif new_state == radio.Associating:
00229 print >> radio_manager_decisions, "--> Associating on %s."%iface.iface
00230 elif new_state == radio.Unassociated:
00231 print >> radio_manager_decisions, "--> Unassociated on %s."%iface.iface
00232 else:
00233 print >> radio_manager_decisions, "--> ERROR!! Unknown association state %s on %s."%(new_state, iface.iface)
00234
00235 @inlineCallbacks
00236 def _trigger_scan(self, iface):
00237
00238
00239
00240 if iface in self.scheduled_scan:
00241 if self.scheduled_scan[iface].active():
00242 self.scheduled_scan[iface].cancel()
00243 del self.scheduled_scan[iface]
00244 if not iface.radio_sm.scanning_enabled.get() or iface.radio_sm.scanning.get():
00245
00246 return
00247 try:
00248
00249
00250
00251 if iface.radio_sm.associated.get():
00252 freqs = self.frequencies.next_scan_freqs(iface, 1, False)
00253 else:
00254 freqs = self.frequencies.next_scan_freqs(iface, self.num_scan_frequencies, True)
00255 except NoFrequenciesReady, nfr:
00256 if nfr.next_time:
00257 self.scheduled_scan[iface] = reactor.callLater(max(0.1, nfr.next_time - time.time()), self._trigger_scan, iface)
00258
00259
00260
00261 else:
00262 print "Triggering scan", iface.iface, freqs
00263 rslt = yield iface.radio_sm.scan(freqs)
00264 if not rslt:
00265 print "Scan failed", iface.iface, freqs
00266 yield async_helpers.async_sleep(self.failed_scan_delay)
00267 self.frequencies.reschedule(iface, freqs)
00268
00269 def _scan_event_cb(self, iface, bsses):
00270
00271 self.bss_list.update(bsses, iface)
00272 self.new_scan_data.trigger()
00273
00274 def _frequency_list_cb(self, iface, old_state, new_state):
00275 now = time.time()
00276 for f in new_state:
00277 self.frequencies.add(f, iface)
00278
00279 if old_state != state_publisher.JustSubscribed:
00280 for f in old_state:
00281 self.frequencies.remove(f, iface)
00282
00283 self._trigger_scan(iface)
00284
00285 class RadioManager:
00286 def __init__(self):
00287 self.scan_manager = ScanManager()
00288 self.best_active = None
00289 self.iface_associations = {}
00290 self.initial_inhibit_end = time.time() + config.get_parameter('initial_assoc_inhibit', 5)
00291 self.bss_expiry_time = config.get_parameter('bss_expiry_time', 5)
00292 self.activate_hysteresis = config.get_parameter('activate_hysteresis', 5)
00293 self.max_hot_frequencies = config.get_parameter('max_hot_frequencies', 3)
00294 self.scan_period_warm = config.get_parameter('scan_period_warm', 10)
00295 self.scan_period_hot = config.get_parameter('scan_period_hot', 4)
00296 self.reassociate_hysteresis = config.get_parameter('reassociate_hysteresis', 5)
00297 self.same_bss_penalty = config.get_parameter('same_bss_penalty', 20)
00298 self.interfaces = set()
00299
00300 self.set_mode()
00301
00302 self.scan_manager.new_scan_data.subscribe_repeating(self._new_scan_data)
00303 self.hot_bss_expiry_time = config.get_parameter('hot_bss_expiry_time', 5)
00304 self.warm_bss_expiry_time = config.get_parameter('warm_bss_expiry_time', 3 * self.scan_period_warm)
00305
00306 def add_iface(self, iface):
00307 self.scan_manager.add_iface(iface)
00308 self.interfaces.add(iface)
00309 iface.radio_sm.associated.subscribe(self._associated_cb, iface)
00310 iface.dhcpdata.error_event.subscribe_repeating(self._dhcp_fail, iface)
00311
00312 def set_mode(self, ssid="", bssid="", band=0, scan_only=False, iface = ""):
00313 self.scan_only = scan_only
00314 self.forced_ssid = ssid
00315 self.forced_bssid = bssid
00316 self.forced_band = band
00317 self.forced_iface = iface
00318 self.scan_manager.frequencies.bands = band
00319 for iface in self.interfaces:
00320 self._check_cur_bss(iface)
00321 self.scan_manager._trigger_scan(iface)
00322
00323 def _associated_cb(self, iface, old_state, new_state):
00324 if new_state == radio.Unassociated:
00325 if iface in self.iface_associations:
00326 del self.iface_associations[iface]
00327 self._check_cur_bss(iface)
00328
00329 def _check_cur_bss(self, iface):
00330 """Makes sure that the current bss satisfies forcing constraints."""
00331
00332 cur_bss = iface.radio_sm.associated.get()
00333 if not cur_bss:
00334 return
00335 if not self.check_bss_matches_forcing(cur_bss):
00336 print iface.iface, "associated to", cur_bss.ssid, mac_addr.pretty(cur_bss.bssid), cur_bss.frequency
00337 print "but want", self.forced_ssid, self.forced_ssid, self.forced_band
00338 print "Unassociating because bss does not match requirements."
00339 iface.radio_sm.unassociate()
00340 return
00341 if not self.check_iface_matches_forcing(iface):
00342 print "Unassociating %s because interface forced to %s"%(iface, self.forced_iface)
00343 iface.radio_sm.unassociate()
00344 return
00345
00346 def check_bss_matches_forcing(self, bss):
00347 """Checks that the bss matches the forced bssid, ssid and band."""
00348 if self.scan_only:
00349 return False
00350 if not check_band(bss.frequency, self.forced_band):
00351 return False
00352 if self.forced_bssid and self.forced_bssid != bss.bssid:
00353 return False
00354 if self.forced_ssid and self.forced_ssid != bss.ssid:
00355 return False
00356 return True
00357
00358 def check_iface_matches_forcing(self, iface):
00359 if self.forced_iface and self.forced_iface != iface.iface:
00360 return False
00361 return True
00362
00363 def _new_scan_data(self):
00364 print "_new_scan_data"
00365
00366
00367 now = time.time()
00368 if now < self.initial_inhibit_end:
00369 print "inhibited"
00370 return
00371 for iface in self.interfaces:
00372 if not self.check_iface_matches_forcing(iface):
00373 continue
00374
00375 cur_assoc = iface.radio_sm.associated.get()
00376
00377 if cur_assoc and iface == self.best_active:
00378 continue
00379
00380 if iface.radio_sm.scanning_enabled.get() and cur_assoc != radio.Associating:
00381
00382 candidate_bsses = filter(self.check_bss_matches_forcing,
00383 self.scan_manager.bss_list.bsses.itervalues())
00384
00385 if candidate_bsses:
00386 expiry_time = now - self.bss_expiry_time
00387 best_bss = max(candidate_bsses, key = lambda bss: self.desirability(bss, expiry_time, iface))
00388 if iface not in best_bss.by_iface or best_bss.by_iface[iface].stamp.to_sec() < expiry_time:
00389 print "Best bss is expired.", mac_addr.pretty(best_bss.bssid)
00390 best_bss = None
00391 else:
00392 print "No candidate bsses."
00393 best_bss = None
00394 if best_bss is None:
00395 print "No candidate bsses or expired."
00396
00397
00398 continue
00399
00400 best_desirability = self.desirability(best_bss, expiry_time, iface)
00401 print "Best bss:", mac_addr.pretty(best_bss.bssid), best_desirability
00402
00403 if cur_assoc:
00404
00405 if make_id(cur_assoc) == best_bss.id:
00406 print "Already associated to best bss."
00407 continue
00408
00409
00410 cur_id = make_id(cur_assoc)
00411 if cur_id in self.scan_manager.bss_list.bsses:
00412 cur_bss = self.scan_manager.bss_list.bsses[make_id(cur_assoc)]
00413 cur_desirability = self.desirability(cur_bss, expiry_time, iface)
00414 else:
00415 cur_desirability = -1e1000
00416
00417
00418 if cur_desirability + self.reassociate_hysteresis > best_desirability:
00419 print "Best bss not over hysteresis threshold: %s %f > %s %f"%(mac_addr.pretty(cur_assoc.bssid), cur_desirability, mac_addr.pretty(best_bss.bssid), self.desirability(best_bss))
00420 continue
00421
00422
00423 print >> radio_manager_decisions, "Associating to %s (%f) on %s"%(mac_addr.pretty(best_bss.bssid), best_desirability, iface.iface),
00424 if cur_assoc:
00425 print >> radio_manager_decisions, "from %s (%f)"%(mac_addr.pretty(cur_assoc.bssid), cur_desirability)
00426 else:
00427 print >> radio_manager_decisions, "from unassociated"
00428 iface.radio_sm.associate_request.trigger(best_bss.id)
00429 self.iface_associations[iface] = best_bss.id
00430
00431 def _dhcp_fail(self, iface):
00432 print >> radio_manager_decisions, "DHCP failed, taking down", iface.iface
00433 iface.interface_upper.restart()
00434
00435 def _ping_fail():
00436 pass
00437
00438 def update(self):
00439 """Called in the main update cycle after all the scores have been
00440 computed. Decides which interfaces should be activated."""
00441
00442 def iface_score(iface):
00443 """Used to decide what to activate and disactivate."""
00444 return iface.prescore
00445
00446 now = time.time()
00447
00448 active = set()
00449 verified = set()
00450 for iface in self.interfaces:
00451 iface.active = iface.radio_sm.is_active.get()
00452 if iface.active:
00453 active.add(iface)
00454 if iface.ping_monitor.is_verified.get():
00455 verified.add(iface)
00456 inactive_verified = verified - active
00457
00458
00459 if active:
00460 self.best_active = max(active, key = iface_score)
00461 else:
00462 self.best_active = None
00463 if len(active) > 1:
00464 for iface in active:
00465 if iface != self.best_active:
00466 iface.radio_sm.activate_request.set(False)
00467 print >> radio_manager_decisions, "XXX Disactivating %s because %s is active and better."%(iface.iface, self.best_active.iface)
00468
00469
00470
00471 if inactive_verified:
00472
00473
00474
00475 best_inactive_verified = max(inactive_verified, key = iface_score)
00476 if not self.best_active or iface_score(best_inactive_verified) > iface_score(self.best_active) + self.activate_hysteresis:
00477 if not self.best_active:
00478 print >> radio_manager_decisions, "XXX Activating %s because no current best active."%best_inactive_verified.iface
00479 else:
00480 print >> radio_manager_decisions, "XXX Activating %s because it is better than %s."%(best_inactive_verified.iface, self.best_active.iface)
00481 best_inactive_verified.radio_sm.activate_request.set(True)
00482
00483
00484
00485
00486 candidate_bsses = filter(self.check_bss_matches_forcing,
00487 self.scan_manager.bss_list.bsses.itervalues())
00488 expiry_time = now - self.hot_bss_expiry_time
00489 candidate_bsses.sort(key = lambda bss: self.desirability(bss, expiry_time), reverse = True)
00490
00491 periods = dict((f, self.scan_manager.frequencies.scan_period_cold)
00492 for f in self.scan_manager.frequencies.frequencies)
00493 hot_frequencies = 0
00494 print >> scan_periods_log, "\033[2J\033[0;0H"
00495 print >> scan_periods_log, "Candidate bsses:"
00496 for bss in candidate_bsses:
00497 print >> scan_periods_log, bss.ssid, mac_addr.pretty(bss.bssid), bss.frequency, self.desirability(bss, expiry_time), now - bss.last_seen()
00498 if hot_frequencies < self.max_hot_frequencies and periods[bss.frequency] != self.scan_period_hot:
00499 hot_frequencies += 1
00500 p = self.scan_period_hot
00501 elif bss.last_seen(iface) > now - self.warm_bss_expiry_time:
00502 p = self.scan_period_warm
00503 periods[bss.frequency] = min(periods[bss.frequency], p)
00504 print >> scan_periods_log, "Frequencies"
00505
00506 freqs = [ f for f in self.scan_manager.frequencies.frequencies if check_band(f, self.forced_band) ]
00507 freqs.sort()
00508 for f in freqs:
00509 print >> scan_periods_log, f, periods[f], self.scan_manager.frequencies.frequencies[f].next_scan_time - now
00510 self.scan_manager.frequencies.set_freq_period(f, periods[f])
00511
00512 def desirability(self, target_bss, expiry_time = 0, iface = None):
00513
00514 desirabilities = [bss.level for bss_iface, bss in target_bss.by_iface.iteritems() if bss.stamp.to_sec() > expiry_time and (iface is None or bss_iface == iface)]
00515
00516 if desirabilities:
00517
00518
00519
00520 penalty = 0
00521 if iface:
00522 target_id = make_id(target_bss)
00523 for i, id in self.iface_associations.iteritems():
00524 if i != iface and id == target_id:
00525 penalty = self.same_bss_penalty
00526
00527
00528 return max(desirabilities) - penalty
00529 else:
00530
00531 return -1e1000
00532