From 37ce50b3131d3799e3e330898baa60625ca47a37 Mon Sep 17 00:00:00 2001 From: Ninoh-FOX Date: Wed, 25 Sep 2024 15:07:25 +0200 Subject: [PATCH] added autoconnect wifi fuction and update system fuction --- base/.tmp_update/updater | 70 +- base/App/Wifi/autoconnect_state.txt | 1 + base/App/Wifi/wifi.py | 144 +- base/App/pico/.lexaloffle/pico-8/config.txt | 6 +- base/Koriki/bin/pico.sh | 2 +- base/Koriki/version.txt | 2 +- src/Wifi/autoconnect_state.txt | 1 + src/Wifi/data/Inconsolata.otf | Bin 0 -> 58560 bytes src/Wifi/data/closed.png | Bin 0 -> 394 bytes src/Wifi/data/gcwzero.ttf | Bin 0 -> 23920 bytes src/Wifi/data/open.png | Bin 0 -> 407 bytes src/Wifi/data/transparent.png | Bin 0 -> 68 bytes src/Wifi/data/unknown.png | Bin 0 -> 306 bytes src/Wifi/data/wifi-0.png | Bin 0 -> 316 bytes src/Wifi/data/wifi-1.png | Bin 0 -> 320 bytes src/Wifi/data/wifi-2.png | Bin 0 -> 342 bytes src/Wifi/data/wifi-3.png | Bin 0 -> 419 bytes src/Wifi/data/wifi-connecting.png | Bin 0 -> 411 bytes src/Wifi/dnsmasq.conf | 6 + src/Wifi/hostapd.conf | 15 + src/Wifi/hosts | 2 + src/Wifi/networks/wifi_saves.txt | 0 src/Wifi/udhcpd.conf | 2 + src/Wifi/wifi.py | 1852 +++++++++++++++++++ 24 files changed, 2067 insertions(+), 36 deletions(-) create mode 100644 base/App/Wifi/autoconnect_state.txt create mode 100644 src/Wifi/autoconnect_state.txt create mode 100644 src/Wifi/data/Inconsolata.otf create mode 100644 src/Wifi/data/closed.png create mode 100644 src/Wifi/data/gcwzero.ttf create mode 100644 src/Wifi/data/open.png create mode 100644 src/Wifi/data/transparent.png create mode 100644 src/Wifi/data/unknown.png create mode 100644 src/Wifi/data/wifi-0.png create mode 100644 src/Wifi/data/wifi-1.png create mode 100644 src/Wifi/data/wifi-2.png create mode 100644 src/Wifi/data/wifi-3.png create mode 100644 src/Wifi/data/wifi-connecting.png create mode 100644 src/Wifi/dnsmasq.conf create mode 100644 src/Wifi/hostapd.conf create mode 100644 src/Wifi/hosts create mode 100644 src/Wifi/networks/wifi_saves.txt create mode 100644 src/Wifi/udhcpd.conf create mode 100644 src/Wifi/wifi.py diff --git a/base/.tmp_update/updater b/base/.tmp_update/updater index ae7cce2..5b5380c 100644 --- a/base/.tmp_update/updater +++ b/base/.tmp_update/updater @@ -134,6 +134,70 @@ else fi } +update() { + + echo "Checking for updater Pico-FOX package" + + if [ -f "${SDCARD_PATH}"/.deletes ]; then + while IFS= read -r file_to_delete; do + rm -rf "${file_to_delete}" + done < "${SDCARD_PATH}"/.deletes + rm "${SDCARD_PATH}"/.deletes + fi + + if [ -f "${SDCARD_PATH}/"update_pico-fox_*.zip ]; then + + echo "update Pico-FOX package found" + + for file in `ls "${SDCARD_PATH}"/update_pico-fox_*.zip`; do + unzip -q -o "${file}" ".update_splash.png" -d "${SDCARD_PATH}" + sync + + show "${SDCARD_PATH}"/.update_splash.png + + unzip -q -o "${file}" ".deletes" -d "${SDCARD_PATH}" + + if [ -f "${SDCARD_PATH}"/.deletes ]; then + while IFS= read -r file_to_delete; do + if [ -f "${file_to_delete}" ]; then + rm "${file_to_delete}" + elif [ -d "${file_to_delete}" ]; then + rm -rf "${file_to_delete}" + fi + done < "${SDCARD_PATH}"/.deletes + fi + + unzip -q -o "${file}" -d "${SDCARD_PATH}" + + rm "${file}" + + if [ -f "${SDCARD_PATH}"/.deletes ]; then + rm "${SDCARD_PATH}"/.deletes + fi + + if [ -f "${SDCARD_PATH}"/.update_splash.png ]; then + rm "${SDCARD_PATH}"/.update_splash.png + fi + + sleep 5s + done + + sync + sleep 5s + + if [ "$MODEL" == "MMP" ]; then + poweroff + else + reboot + fi + + sleep 10s + fi + + echo "update Pico-FOX package not found" + +} + killprocess() { pid=`ps | grep $1 | grep -v grep | cut -d' ' -f3` kill -9 $pid @@ -218,6 +282,9 @@ resize # Charging screen "${SYSTEM_PATH}"/bin/charging +# Update opportunity +update + # check swap size if [ -f "${SWAPFILE}" ]; then SWAPSIZE=`stat -c %s "${SWAPFILE}"` @@ -420,7 +487,8 @@ while [ 1 ]; do fi if ls "$BBS"* 1> /dev/null 2>&1; then - cp "$BBS"* "$SPLORE" + rm "$BBS"temp-* + cp "$BBS"*.p8.png "$SPLORE" else echo "No BBS files found to copy." fi diff --git a/base/App/Wifi/autoconnect_state.txt b/base/App/Wifi/autoconnect_state.txt new file mode 100644 index 0000000..7e86436 --- /dev/null +++ b/base/App/Wifi/autoconnect_state.txt @@ -0,0 +1 @@ +autoconnect_enabled=False diff --git a/base/App/Wifi/wifi.py b/base/App/Wifi/wifi.py index 3755dcf..e614b46 100644 --- a/base/App/Wifi/wifi.py +++ b/base/App/Wifi/wifi.py @@ -44,6 +44,7 @@ netconfdir = confdir+"networks/" sysconfdir = "/appconfigs/" datadir = "/mnt/SDCARD/App/Wifi/data/" +autoconnect_enabled = False surface = pygame.display.set_mode((320,240)) selected_key = '' @@ -144,6 +145,30 @@ def enableiface(iface): mac_addresses[iface] = getmac(iface) return True +def autoconnect(iface): + check = checkinterfacestatus(iface) + if check: + return False + + modal("Autoconnecting...") + drawinterfacestatus() + pygame.display.update() + + SU.Popen(['/bin/sed', '-i', "s/\"wifi\":\s*[01]/\"wifi\": 1/", '/appconfigs/system.json'], close_fds=True).wait() + SU.Popen(['/customer/app/axp_test', 'wifion'], close_fds=True).wait() + SU.Popen(['sleep', '2'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'wpa_supplicant'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'udhcpc'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'hostapd'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'dnsmasq'], close_fds=True).wait() + while True: + if SU.Popen(['/sbin/ifconfig', iface, 'up'], close_fds=True).wait() == 0: + break + time.sleep(0.1); + SU.Popen(['/mnt/SDCARD/Koriki/bin/wpa_supplicant', '-B', '-D', 'nl80211', '-i', iface, '-c', '/appconfigs/wpa_supplicant.conf'], close_fds=True).wait() + mac_addresses[iface] = getmac(iface) + return True + def disableiface(iface): SU.Popen(['pkill', '-9', 'wpa_supplicant'], close_fds=True).wait() SU.Popen(['pkill', '-9', 'udhcpc'], close_fds=True).wait() @@ -455,33 +480,38 @@ def drawstatusbar(): # Set up the status bar wlan_text.topleft = (2, 225) surface.blit(wlantext, wlan_text) -def drawinterfacestatus(): # Interface status badge - global colors - wlanstatus = checkinterfacestatus(wlan) - if not wlanstatus: - wlanstatus = wlan+" is off." - else: - wlanstatus = getcurrentssid(wlan) - - wlantext = font_mono_small.render(wlanstatus, True, colors['white'], colors['lightbg']) - wlan_text = wlantext.get_rect() - wlan_text.topleft = (2, 225) - surface.blit(wlantext, wlan_text) - - # Note that the leading space here is intentional, to more cleanly overdraw any overly-long - # strings written to the screen beneath it (i.e. a very long ESSID) - if checkinterfacestatus(wlan): - text = font_mono_small.render(" "+getip(wlan), True, colors['white'], colors['lightbg']) - interfacestatus_text = text.get_rect() - interfacestatus_text.topright = (317, 225) - surface.blit(text, interfacestatus_text) - else: - mac = mac_addresses.get(wlan) # grabbed by enableiface() - if mac is not None: - text = font_mono_small.render(" "+mac, True, colors['white'], colors['lightbg']) - interfacestatus_text = text.get_rect() - interfacestatus_text.topright = (317, 225) - surface.blit(text, interfacestatus_text) +def drawinterfacestatus(): # Interface status badge + global colors + wlanstatus = checkinterfacestatus(wlan) + if not wlanstatus: + wlanstatus = wlan+" is off." + else: + wlanstatus = getcurrentssid(wlan) + + wlantext = font_mono_small.render(wlanstatus, True, colors['white'], colors['lightbg']) + wlan_text = wlantext.get_rect() + wlan_text.topleft = (2, 225) + surface.blit(wlantext, wlan_text) + + # Note that the leading space here is intentional, to more cleanly overdraw any overly-long + # strings written to the screen beneath it (i.e. a very long ESSID) + if checkinterfacestatus(wlan): + ip_address = getip(wlan) + if ip_address is None: # Handle case where no IP is assigned + ip_address = " " # Display alternative message if no IP is available + text = font_mono_small.render(" " + ip_address, True, colors['white'], colors['lightbg']) + interfacestatus_text = text.get_rect() + interfacestatus_text.topright = (317, 225) + surface.blit(text, interfacestatus_text) + else: + mac = mac_addresses.get(wlan) # grabbed by enableiface() + if mac is not None: + text = font_mono_small.render(" " + mac, True, colors['white'], colors['lightbg']) + else: + text = font_mono_small.render(" ", True, colors['white'], colors['lightbg']) # Handle no MAC case + interfacestatus_text = text.get_rect() + interfacestatus_text.topright = (317, 225) + surface.blit(text, interfacestatus_text) def redraw(): global colors @@ -596,6 +626,44 @@ def writeconfig(): # Write wireless configuration to disk f2.write('}\n') f2.close() +def save_autoconnect_state(): + config_file = "/mnt/SDCARD/App/Wifi/autoconnect_state.txt" + with open(config_file, 'w') as f: + f.write("autoconnect_enabled={}\n".format(autoconnect_enabled)) + +def load_autoconnect_state(): + global autoconnect_enabled + config_file = "/mnt/SDCARD/App/Wifi/autoconnect_state.txt" + if os.path.exists(config_file): + with open(config_file, 'r') as f: + for line in f: + key, value = line.strip().split("=") + if key == "autoconnect_enabled": + autoconnect_enabled = value == "True" + +def toggle_autoconnect(): + global autoconnect_enabled + if autoconnect_enabled: + confirm = modal("Disable Autoconnect?", query=True) + if confirm: + autoconnect_enabled = False + modal("Autoconnect is OFF", wait=True) + redraw() + else: + active_menu = to_menu("main") + redraw() + else: + confirm = modal("Enable Autoconnect?", query=True) + if confirm: + autoconnect_enabled = True + modal("Autoconnect is ON", wait=True) + redraw() + else: + active_menu = to_menu("main") + redraw() + + save_autoconnect_state() + ## HostAP def startap(): global wlan @@ -1320,11 +1388,11 @@ def mainmenu(): if mac == ap: elems = ['AP info'] + elems except: - elems = ['Create ADHOC'] + elems + pass - elems = ["Saved Networks", 'Scan for APs', "Manual Setup"] + elems + elems = ["Saved Networks", 'Scan for APs', "Manual Setup", 'Autoconnect'] + elems - if checkinterfacestatus(wlan): + if checkinterfacestatus(wlan) and getcurrentssid(wlan) is not None: elems = ['Disconnect'] + elems menu.init(elems, surface) @@ -1478,6 +1546,19 @@ def convert_file_names(): logoBar = LogoBar() redraw() + + load_autoconnect_state() + + if autoconnect_enabled: + modal("Autoconnecting...") + autoconnect(wlan) + if not udhcpc_timeout(wlan, 30): + modal('Autoconnect failed!', wait=True) + else: + modal('Autoconnect successful!') + time.sleep(2) + sys.exit() + while True: time.sleep(0.01) for event in pygame.event.get(): @@ -1648,6 +1729,9 @@ def convert_file_names(): elif menu.get_selected() == 'Create ADHOC': startap() + + elif menu.get_selected() == 'Autoconnect': + toggle_autoconnect() elif menu.get_selected() == 'AP info': apinfo() diff --git a/base/App/pico/.lexaloffle/pico-8/config.txt b/base/App/pico/.lexaloffle/pico-8/config.txt index 75b2fce..57e3f1b 100644 --- a/base/App/pico/.lexaloffle/pico-8/config.txt +++ b/base/App/pico/.lexaloffle/pico-8/config.txt @@ -9,8 +9,8 @@ // :: Video Settings -window_size 640 480 // window width, height -screen_size 640 480 // screen width, height (stretched to window) +window_size 320 240 // window width, height +screen_size 320 240 // screen width, height (stretched to window) show_fps 0 // Draw frames per second in the corner @@ -30,7 +30,7 @@ foreground_sleep_ms 1 // number of milliseconds to sleep each frame. Try 10 to c background_sleep_ms 10 // number of milliseconds to sleep each frame when running in the background -sessions 19 // number of times program has been run +sessions 59 // number of times program has been run // (scancode) hold this key down and left-click to simulate right-click rmb_key 0 // 0 for none 226 for LALT diff --git a/base/Koriki/bin/pico.sh b/base/Koriki/bin/pico.sh index b73a7f5..0bc98f0 100644 --- a/base/Koriki/bin/pico.sh +++ b/base/Koriki/bin/pico.sh @@ -160,7 +160,7 @@ set_snd_level "${volume}" & echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor -pico8_dyn -splore -width 640 -height 480 -root_path "/mnt/SDCARD/Roms/PICO/" +pico8_dyn -splore -width 320 -height 240 -root_path "/mnt/SDCARD/Roms/PICO/" sync diff --git a/base/Koriki/version.txt b/base/Koriki/version.txt index 8af85be..94fe62c 100644 --- a/base/Koriki/version.txt +++ b/base/Koriki/version.txt @@ -1 +1 @@ -1.5.3 +1.5.4 diff --git a/src/Wifi/autoconnect_state.txt b/src/Wifi/autoconnect_state.txt new file mode 100644 index 0000000..7e86436 --- /dev/null +++ b/src/Wifi/autoconnect_state.txt @@ -0,0 +1 @@ +autoconnect_enabled=False diff --git a/src/Wifi/data/Inconsolata.otf b/src/Wifi/data/Inconsolata.otf new file mode 100644 index 0000000000000000000000000000000000000000..e7e1fa0cd74847ceaa92fd3c9b4bfc75f329155e GIT binary patch literal 58560 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 literal 0 HcmV?d00001 diff --git a/src/Wifi/data/gcwzero.ttf b/src/Wifi/data/gcwzero.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6bca8acaddc5cfd285fd5f9b68f9da527fa9c641 GIT binary patch literal 23920 zcmeHv36x!Bo#*|&yYI`p_rCqrTdH1Fw&JDg)vK6<5E4jOlMo<48jt`9M8YCOKxJGJ z*+kU#sJP(**lN2lt+uqsqkU|3Yxc#^H>$fp2EMu)z}gKv-eTlYaH9- zS6_Sh){DO@eG1#(#J>ZFuD@d6{r}-MY%A}^@y}no@75cn4@!^Y*lE~qUbpYs{XaYL z^M5NzdmTyQ+itl2ro+aRah4=qO!N7kM03TzP1gABFZDP6R-}%q^xi4%M608}i^QJ2(jzom{Lz(6?At6&i#>{>v)@Q6 z4&BW`LFwLX$$luUWjLElT6QI!OygYq+jZd7E2XbUP3cu~b77sd7h?kjvUB*I{4T6F zCB_a(ExLrAjeA{)<;$>qMv`g7$_|OiS4#@ck@;qc(LdVH_eflvmoMddb}q;4=^AC1 z#PLrS>pSxGv#=lQxGuZ)laj*EmpI2#CeF|`bWVOAwiTRD_Yn7@>#)w0ZDO0N*NNlV zK01ev9mtp9oVX7A)Zby>&g|KA4$T4M&6dsoKhMqiLVTiswS4AgybmF zgmjX0iS$zGRnP(A(WG=Tw)QP7Ec|rgCksDb_|d`-^E=>h_QybZx)uGAR4huM*h4$< zXhqd@LozMfaXmi>qe7gd#ZtLat<@W2&6d>ej8AkYr&g?7wR+9;3B9#5v;Dcj{JNK{ z->~t-O&HVWlecVzW^X_3^fS&p>+Bsncb&8Q-1E-gbHRldjh^zay%aV?TA6?Dl>Ui5 z&OareBHt=MuB=nL>YeJB)MvDD?Q-oqx}!g6^o&oMTg=C;DeDLJ4fcO_`p(0yzicAGFfep|;W9+wnSoJ>DDt`otL%Pj+wY{-?>! zlV6*9`-;m~{AlH+ARH$>WGndtkP2Oz{w$MD*!ZZfNKemvR#Ttc_^8aWDt%U_-74*V zRM*(=H$Dn>KRq)Ww!+ETu+w7Z&%f~v{=ks~EmCHZ1c@2^Ua2i@k)-*-?0Pmklj=2A zXR)s7+A7wb>`r#)HnJ00e||7OD9p?kaYCHP&pJL=5XUR3Uw%_5445YCru#+L&=rMo zUG|g9%MnlBl!&1P-XmSy2W+A+vldHvZojee6=wAmZvW{$WC%)OH(j* znQh|t@wZ^?r$|y^Fx$(9p6Fs|onA*1WA6{rwpFgNJV_XkCM(j?YBM#rbwWH*@G#MDh-8(mopaQZwLnHE#2 zm}2H#nzSBczovU~x3iMtqG@&l>w>-bcue>BO4iZ3*b6?-4JMTq9V=>hYCf8yPdi+T z5Ap4Xyb|Xn@2cVc1I2Mxj=%nnu*?`M`ET9g#Z2EjckYp&o;x^SVe(d`q+PatOyOhQ zlVn?Gm+r2eUpt%CH*uGx%{TRUsbGz*8QbU-#wuyzq-)na<&HUb+@k1{6WE+8ZHw0q zs%epxhkr5LSDL_6ST%V$yZ?ru0@l>R^+&d^pF3A&{0$n5y!8VY>}wS_zbR?q#IST} z$&cxp#Cx*b->k6h{Wja)o<8-O`7NwBF??d)GBh@KdX-hT4SzQ;t2%pat-k&x!;kMM z&-jL|Fea<2HM#0ZTd{9GsOX)BscM`Xk-N6me`2ogaNa7--VLc7U=`T-3_ciM=A*|qInzUw$U{!(J z@luuS4<`F_EpQI{i4QnOLOwI!pKEv8lS1^NxIM8K4^-QFmoBH9&cbrD9?r#naDB4d zX?Nm>tj>d6*DNS1q?WOlR2@sv48vE7w|&`hwfpqmWTRO3Os-YJW^e1Kn3ijaAw$xK z-;)!$V3{&!mYwYX$=O|h<(A%9P}O;0`%XN`qylG5S0=K}L^>8W^G%bn z)oU0lI6-;bcHZ2sgsy2uAA-PWiRZ_L+!+22TAq5o6>>faUMXOh%-cXnnlQ-u2! zYA?o56PfH{V)%M2k(Ue~5}pr#_{{N!Z^-{I!(aAI|Au`N19$IczXJR?JwqL{xnG*e z{1REc8A4NWQp~_qy8~whc^sK90))9f5@?td0IQ}1=EmHIkUcvkCNL%3M6hLv8GA{R z;X(ne?qz3r9Dt4oJ}>Z%L7>F*!m^)tjD0_A1P(X5ilrjlMZ~hOo2k-=$PO(c*It~1 zyH!lMz_~^Aw}?!igYL*!rb{854GanhrvQY~bGUSb{fgZubw&v77{W9c&Hhm;G}->@ z;?(QI2b#ys_vjR#XHIzFv*&btG`={eCB!A7zJU2Bx&e58k>7(jQeulPN=u*ZqQKMK zMX~=XTuwlK^%$ok0P>>Kfd_vP_rYP)7l4WGlBROW>$QZvE0Vn?`NF3?{v11Br2TP1 z@hCp9`Q$j~`rSD~KHW~c`FhLbUz={%3L!I`YGpDl-!$ys)Tx)#7#7~Q3KPEz$+82^ z7$r6cRe=$h;yY1i_<#7FjKyKKGPZO050k}0xlk|x{BoOrBCDKLO+&Z(P-f{r8x%bU zs6}?1aN^ENs>D|A7lGy3hY?Gy0$qDa0JsJXfm@pI2hfqVeGIhI1!jk?L0!n!1Nzk& z`!K7?!Tt;V0&`;^sV!02v>JbYsJ7bF*khI9pLJSlHR!UH6D+C#W(VO;J27qMS;@PE zKh9`+w{#Vr+n(&T+C;{rmpQ=MRH4Td1$HD?LWoh&Sp#sP18|Zpj0F^(ADqYrMA31K z6N`C5D8)ewrhpF8;oNP>+pO^;6Gsu^E$nk=9tC$N{BpB^i(|#wB3$H8@N^L$rU!@T zU$X@Ltz!2)3oSyHkl3<|=Xi$6$){?u=O+eMZPSaIY3=1>=0rJk3KXmX_FWJV9?CGC z9RP#^{Q2bx+qt7%i5$xeNsn{cyApKjc8;afOmSu@otl8lgx5gD3^Dfr(;EW70f>M; zg2p70@ZLfu5h{(mdw7&cog>{^2BQNxy_J3bbWaBYmetg-hK~-vThttvrlgvB!f$sh zSC_e|o2vDiVfO^6o1!5|mQBNZ8;>uUY_!XbAdalSjwd0qimco59+&9Dxt97~g0mSn zOgT6yF%`~qb1ZO89gu``t|%7w?Mmb?wGWxnTfI{o z1vm#?Gr4DdA+{`8#Z(+b%)Q_CQr{v7r5nB+nfFdGjfIxVWIg2?^WHWY+XiE<=Xk?M zjxgWpSPvLZFu&R}bxfVWYWygSwyGn4U@4*Y%BIZ&!@@Dwu=R6|Vbd`R%rOEU)UK=9 zfvQk0HPfGaq=Olr%>pvgA2LRITUin~VSd>Zi3~aL2S|Hv7&d-Pbk$l6$nrGG`~cw= ze{zWe4bOZY1zH+!9hq1fU>#zgIJi`ShOc-Y1zHk6kp^X<&$HvGW5cGSQPFe4CvqD? zhkn8DM8@ShNty*{oyes=69q6BAB2>!d*gby4Y(lUQ;JSusB_yBCm|$6m>w7(x}pzL zBPc8U5pGNL6?q(7GWx#7w*Ec)`lhiYZiWRIXr|c}FNvnc*&1*7gvUz!T2D_bg_&m9 zxTE1((5=Yy;!^3W5mFDb9JyLaHOCEQmHf~JzG52&ldU2jlUF{xg2~J~2eX$AJ1sRG zuXM1MgmuqQDFp=CD(HUo=4NOjM6yF}56@G^<*AxyLrXux0xLp{0HJ2GZ5M2#2q#PW zv;cj&M;b^{t1WZ^ijWxxP-jvEGmG*t+an{K)1(*|IYs;OcNL{_cO@_#%`7O+`NLCh z#oc9+bj`Fq{!L}IGT9z0m3)`9M%7e3VDE`+U+9o#soqY6&j`6WJfG(m)0z?wzw!Ri zu(JFF`QkgJ?UYN#7#BG?-=FUZ*Gfho83e&p(%e?=IdbQcyO|Lu;h6^;IBOE`5**Vt zH4E4TI4lleAq88>u;K_M9x~%1&yM-!Vy9&5!q#iGI%sY>)#`+G!tis$zgX=~RI84` z)J9!nFpzesQ!IPsSH1r$YlrV)=PRM9!`OM+@DrW}Q-JkQDQfIQC9+Lfky&VK!-rkX z4mh?<8$lbmTymT!k-dE_ty8U!Yss}~j69RDBYH8d#_6F%AFo$CTI(`jP9n!C*$9Xk z`w^2pEA&hU5(p4rdZFbh>?h3DEzhw-xE?Ntw&Pj4P5J^t{{l8u=nK3~8;(Vg2EGM4 zkU8K6DF+;CQ!j$KpgP5fL%CSW-KKAOy zA{M7y*Kv=r-;3n)x?_TnlO2jZ{`+x{kRJud5trzQ7erqA4~R?_PZ>p9M}w?9q*%oH z&xu_eB`CkKLJ7O2A;2gGI9^oq^z+Q%Xk4X8eSR9>C!Gl+Hh@qf;V)8;B2`Se6__W8 zZ1(L5lSSDX;pjWlJaYikl-$ZPT$tY`#wJw1@#!8=nkj4JZ)w%I%5>G_iB`JMY56Uk z+c02?<$S%b8x|%YtGqIQaNd%UQf$0?MSwjH&<(!~0&7_|P{Y`*^%7ILp{p7*d921~ zS~b6>G0U-0`hc!#ct*K@?W~i!Dy!eu#XXQ7gKZ{RdoyCXqq0WuZHXKqxBgsNd(m?s zYc00FG7>hoK77y7$d9D$1&;oU<&7bCISYQhRPYPNfLI|63o(cbV?pL3|Dp_l59NUI zB^L3FPn%$kg-I61zTIV$s$c&M_+=2g+BFbb%h+{+(1GHjbp2by`b1j;|FUoEy_t{ zg3)fa6;)2B%&A7j1*}z^xYeA}Cw@Ql^Z-NQ%x4=z%SYFR?Zg|twV@DNHbeOsStY2! zU5u-xxLk-8XZZSt9)~p_pkCG@#p5k6Ngpwtg6f}nvuF8$Slo7_@waw7+tJYIL9qwb zvz`Xukn0e96En)Cz1`7-4a|#y%@zvGB*Dcj1t&5i9`PBU*_rVSwZrFA%tkyFCR9+O zJDFA1=La9N#`*Ut)d3HS@Va=5h-XU*Ewa!;zwU+mWX(>5fshqrec%I0Z^~$ck>odpv`xfm}!FE(TUdTEkTQ%}943zO}{z zBr~uRbt~1Bk?jaf)kgn;s#3aRg1vpbiZetmXcU{F$Lv`82(d-XFJO9}{K+x`!dm3g zKt7^Nw8%9fkM2YjHppsIpSBGC>#_^a2Y_uSHxEy{%(G35Mz>He;;(Zoiz>-7456hK zE*!482m&d?-i7U(S}~FqVNjdqcf-k=W_5GO93AS(bludQ(1OLKNgA5J*+S)yK)hi@ zT3mYhSb*S4bGXlTv6FUSrsfOXok0x(R>y=`ZpjAY~;#qF% z8i8qZI}Q@OV5VxRW7IRpNVDz20MLa0fUFq*&5Nb6VjpHpH=@Ejr##@^7tM>2gFJ3B z%Q0N@{bm9kA&Ec|t$S}=Wak!NBtJ&-0lMh0fyiAKX6AbULpl;*l*1;4KU&(J6S-`n zLRRs~`(pHCyL}+Gh%icrh7TP&CVtw&>{A%<;nfXM^1*XQxwrLrK0Q|$9cot*4;Bw^ z_1bI6>{8&DXBr1EO5}p)DK5r9_yW2XTtmd?npq0RI%}-4BhzC@*N{)(fFQ4BkFP5@ z0IjA`dqU(PBom>Q)>sU+kfIThR_j$RVRzdhpAFa`zkLR1sNA}sqj7kO@iNKUC7za52}jtWhB9n*g+O-FZ;qROS51{)*%Zv)moAT z6Aada;TLutn+h{venlz_28L2$?4~P3DhzT6BcVF5kFOzru%0g_;!+Vp!64=)ZnPw* zm$9!HXRUBYZKBAD&!IS#S=CW54mk_~ld=#@ALS|_*ib5=PV<9T$hH>AYtyt=ZksDE zdM$!3ZhMAf7$4eIRJ&#vgq~M#QO{hp9{47jn@po%PE4&RY6&nE@d90IpT563S&TyO z_Q%l5MyN>BbaXC;+rqMrCLIN(fFKTBf*Fcj2%F~k+1H~`%L%B+#y#6&6}Z*3(Gr95 z11c`niTj2hD>`I)Wz$64(&~pyHwHtLcfY1ccef9J7@kcw3%hQdj!U{O;5l5m>735C z?rByd+f@mPXMX4o^h8O8^$eb6k4UkkY(FV>y3&Uy4uDE3Mz1OxL zW#8B|N~wZpc~X^X5Klf|R#j9XjwV%6gb+zp6d{y{_AD(ztav0VLXd}FT82o2y042e z#IuVK$f-``MF``=EU?3+MF_n_UVS77J>>Y8AYI|NfPDzk3!?B&7pN{%rbRswWT8{C zNFb-k0M$GKKVoy1@DN>wGy)ajA1j63det_WV;S~}lh0{gKRmd-U9X5bJhd$S8YTfo z$8qELDS;zAEpyFaJ}HP&D8fIdF#JkLfL|{atA*t64+fh9ch+%Yz+BYiLoiCz$Hqf}tu_X{elXH@LX?Ke$Ohzt=z zOqE1vl5EQ~qZV7T9rOVd&Tt1cO;QAa3(zW^K<03R%+An>qP&0RL~q}JW9C_7rHPFQ zLrA-=l9#rp8Y{Pd+~5cc%>S8az_u``Y|T?AqiVsCQ!<&oK2j3swGz9#hTm(NI)OsV zC^!!}0e8@?T8Q|qAv|um==gY&Jqzy|#zdOc2s zJfUf7v1UvnA=(f{{CHG4GWj4Og2MFr^I7YPD2`_BARR-OZ>x*~XgQwyTQ5}634oCx zZc>8qy`gI$O@u^(;=ca9scoU=eGdxfR&st86f*>d_DXsBnJtM$p|t50PW#zu5sT%J zdH_r#puUm3S7l^FcL+Z`g0dr2MtQ8+foKV##ucOj^ZIP&ghQGoJ|yy8q!KdqAQS@~ z84unQl+nY0s75mst@MNPnHvEVGyy~8(68KELGg@$iW(JK|N%sXX>^{acpiN`t^p}R~XG=$)~PaJqU`m?lgP#bmAE# z$0DC|C*F}Ei$`-2&;gn%>K3HpqBOBd^t%h-H98CD2y+1Mk$v+lmX=L%vWF$1bmC$k z{o%>mq57t(gw31Uj%JvF(|lFOR*{QR!{B`|MBrGt_9r!~E9<+Gb*%#TqaZ~!(cao% z{e9Z0J6VPC*BY{nu->`Iu158%&{GcT*tKh#R#I@!vwUi9Fr0`vta+eOiOW07W?R;_ z71-KFzzG8&scvno^6B%n6SuKaBY90)Abkdp)px<>-8-U#Fm7b}IkGu~u<{R)ry3>_x_3EJeWM{5)MU~;Mm1DRP#@Hrz~b{_Hsi)SG$%1~}k*ZkeS zX9M#fRRde4%KE*#-#qUZ3P}=L<~-|i!>FMPn({@a9%-qu-w1(jNoqXPE5&xoDxIm` zSPByu1H5t5U-iLQGrW*HQx8sQ9jlf)_2z~Zap-taWP4r_DI!x3H1e)UB>~X6jo~*N z)HThtH=K3btcPq2UfM8chd<}KUey$&vt8>#o1s!Ha5eH;mcQ=t+2A(6S%I@f4;bM( z)PUV0jlf;`zJdm@#_aKTOlWPhY^h)klhLzn6*@u8F1)?uAeABVbD6EZTe^?z2Q}AF zG?9Hl>&(*LIg8W0w8EaH;I^Mx`BG>j}oca&|Vy*W2<4F0T3sybr2$W!OF z{fu|y)CYIJ4z2R2zfo%hOD`*%vY~0ln!ObScwVHoe@Y20)Pzp!7jGDIIbPU_5&QFW zR}}3j`7&Y%O}1A(-1SkI<8VR#EiI&}kb%2&4uLAO(AmzFbmEcKA~EKCVQo$4ND(P` zf5yI1O>b$r93AauaW};#GI)R}HomB2fQl>6E1{r=`Vv!A%Z9t(xTf&Zij4uHVi?Kr zfYB+we+JS|h)Stcj}|EJ`UW7e%F8QMU8trK$EA2i1#ujI#9)K zM6-fFnSH!lI$!9HzzU1h7L=pH8NGEw%n^`tQS28(S;X{Y8lg&LE#{VJ$^0Ps(S#Za z=g&)*m0jJmDwnubD&nGN#aQ=u$c7rSne0yD9r1LZjHPTCF8?lN0qrT}f_C}FF$a59 z0!lP3i>DVA@ivN8yud9}Fb*o=FcnITaC;bT2`5)8%0i@oZbTdOOO!4Ml9#2h9&P!I zda|#K=_7qS!U1ffh$P@e;cr|*DM9I4t7cB$5m6T6f7}g2MFj)y5Erqnq zb2wv248J>ck)VLpWY)RE&z{gJv(L&7-gnZx&(KRK8XxXNJ0ZI1UbL|{w`q&CqetZtf_T~=!TS!R z_Y(gSDkgXbw>8&F_?M2Ha^;mG@3&l6U9PL9%XQV|y6SRWb-Av(TvuJLt1j17Mew^^ zS2d~iak;L#TvuJLt7eI%<+|!0>HDk8b=Ad}yO!&!%XQV|y6V5e`=!fuRZ%uuuB$HB zRjFFCq++yK$rA#C>g#e{b-Av(TvuJLtNu~cRq2-eE! z>2LCNS!%N=UsoiZotm$!Qoydy*EQ_FpWg0MsKxJU{L9uEp7p_eo#QuizmTuXcnAO6 z`MM%`Y%X6{r6zvcIJ;KE{?|!c@s8p?#9a5Vz>vTSJ3MTXYarA;483vp%JggsE@r_6Ju{mbJ7~TE4ctQ*I zn;bK=_09Z|&GpT>HEY+*^w#2>V{deHRC_S08!_>Nf*eiUl5R>Qqfb+k_Uym$ri0gC z*X;GyPWO5}>^a`8sIOW4ExZw9d%nND*hyWl^m~pfsXDR3bbz7$BI6iEds zmJ$iSk1Cai>q|hd4bn#GL`nSqF@8h$6tHC*W^fwbB|HOfWS<4L>;Ri~N#}rF=St_Hqht@* zcOl-y+AIBubTM_Q(WvoT&gj+^>!d9y*6>b(m@(}g9TCfK7jL|R&iwXotl!cSzcYUE F{{^#YN4x+4 literal 0 HcmV?d00001 diff --git a/src/Wifi/data/open.png b/src/Wifi/data/open.png new file mode 100644 index 0000000000000000000000000000000000000000..a92164f49047e0d6decc04fd64452299f6697779 GIT binary patch literal 407 zcmV;I0cie-P)0OK{WIh8X8)w_Yedv(&(7b5E&6P6evv* zRD@ps>rl_bJnw$LPtTL;CLC+(gG&aDHLlJNP40hReO>j3x>++aVA}PuS1%8Wh>h0M zN0&9In{`$KoH?&@cwr^(YJ=PFQON)2UVvki{gRY@(g;;l+AX)2` z0OX>~UHy?>03vjY9ss!%UH|kGAcdo{1R$4ryy92^pi~ZI0CKmpnl4PoA%IF6-Rc04 zjHMf?QT7E$sZ#CtSNsG>V5pW(rBPk2IH0Eh;6JXLmXOAZci7-5DwYoALkhum>3xuj>b#J Q0fiYnUHx3vIVCg!08)ty+5i9m literal 0 HcmV?d00001 diff --git a/src/Wifi/data/unknown.png b/src/Wifi/data/unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..728f3ce5ceb3a76399e9beed7f28465e988f72e1 GIT binary patch literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}LV!<*>whp%Q&ao$@2qGdP zAPflUW#iz;6=AD+NsK;ZbVJiX@NSm^@cP~(FqNj^vh=gS6L35^N2OgFK zQ%fu-9K8AG|MYbao-|3uy!ch;U_8s_d`{t}){Y}#p|9$Gg}F}xu$`f>*?y}vd$@? F2>_c(WIF%= literal 0 HcmV?d00001 diff --git a/src/Wifi/data/wifi-0.png b/src/Wifi/data/wifi-0.png new file mode 100644 index 0000000000000000000000000000000000000000..8191852c20b6b74d1eb4c5e3b2e95b4b8a88ea35 GIT binary patch literal 316 zcmV-C0mJ@@P)Eb}1@6W~#jq5fesKLY@^BTzCVoEZfG O0000grD#WpB1nqR>OkW%%Pb`mLViUE{+oU&WwF?9Rrw< zR25=i3S@u(~A#Nc8W8g00P~WgIuK@rXk5rpn SPoc~J0000&CM*a2p30g)^L1I4ji;((@;96?-wA zVpyUYSgnoHyZRg0${53pbdqlmJJ`Zz*oxMxc!$Yo6-ghI*v%r#4xklpxp%KXAICUC z=N~8AN$)ZG1=cdgX2rOI3*6!%+(cWicsG)`JKEhYDw|HvZ!wEj|iYaDz z3A1SPcqVlM@9~tzXI$YjTt%D4b6+R$JiEDDa7dlhUgE9w^csK5RetYTBY}r_4~IR literal 0 HcmV?d00001 diff --git a/src/Wifi/data/wifi-3.png b/src/Wifi/data/wifi-3.png new file mode 100644 index 0000000000000000000000000000000000000000..1e77262fe3d6092873e43a323b9754a58900dd81 GIT binary patch literal 419 zcmV;U0bKrxP)!9}=5_C4oceT6=kdS)bMF88^7(vCHJh(Yz(GVXjxMxmJcB#zV;iSMJ1w9? z2ywh3hb%I9kc`H1S`*ivzZkz6HenSLc*6_oP%F;%EnjFv19mWroCWNmvkrUa6v*Ck zj9r`|t6(kYM+n1qWeRKwB{6LQH|Pv0vwCb{2`QKR@5VeP@u5sl?A$S=aE)leoq7?~ zUW*a43|omjg+tssGO(kEF^W!&*%v;-9^M^mJ5&*y<=z^`IUbQlQqmf`(wd^ZrRs17 zi+IKp{9^4>n)M+Tu;y0SE9g@YpN@E|xn7K@3M}v?<|-Aoi6DVx1WL>YG?&m?*CYSS zzg*bH^gM7~>7mYoxRIdSdcTM4+Hv9NS8QGa+W$P@=vUryvYCn literal 0 HcmV?d00001 diff --git a/src/Wifi/data/wifi-connecting.png b/src/Wifi/data/wifi-connecting.png new file mode 100644 index 0000000000000000000000000000000000000000..63085ae804d2199c49b7f25d8e308c3807a883ed GIT binary patch literal 411 zcmV;M0c8G(P)3}!L7c6u6zQ)g$oxV2ns5;sTb6$ zRTKXsUttKQR?WbVBqx)TGn1LPNs>5uHZLRIiy;i41HR&Q)DWSJyYz!@s=`4`qFdKl z<-0mQ;24#3LR0Cis`~95xT^LL?&6vMG*``_@0)+8P+kaH?3g{0hTeUs#Y|TS1hTrSn<{LIxK7c%mdGgy(o5}>ubdv^*L7o z)$bPerWSM;cJyUqRoa;6Z6z>YcUR|+*vMMgj&{cwZZ#G4)P1vSiU@J4UE?%M_^(!I zv0o^;Bs7VpB>d~&#%_VN^@2PSM%Lda`teVZ_ec2@U;v$1o@*lypilq+002ovPDHLk FV1gSZz7_xg literal 0 HcmV?d00001 diff --git a/src/Wifi/dnsmasq.conf b/src/Wifi/dnsmasq.conf new file mode 100644 index 0000000..f139e64 --- /dev/null +++ b/src/Wifi/dnsmasq.conf @@ -0,0 +1,6 @@ +interface=wlan0 +dhcp-range=192.168.4.101,192.168.4.120,255.255.255.0,12h +dhcp-option=3,192.168.4.100 +dhcp-leasefile=/appconfigs/dhcp.leases +no-resolv +user=root diff --git a/src/Wifi/hostapd.conf b/src/Wifi/hostapd.conf new file mode 100644 index 0000000..e8c7c9b --- /dev/null +++ b/src/Wifi/hostapd.conf @@ -0,0 +1,15 @@ +interface=wlan0 +ctrl_interface=/var/run/hostapd +ctrl_interface_group=0 +driver=nl80211 +ssid=MiyooMini +channel=4 +hw_mode=g +macaddr_acl=0 +ignore_broadcast_ssid=0 +auth_algs=1 +wpa=3 +wpa_passphrase=12345678 +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +rsn_pairwise=CCMP \ No newline at end of file diff --git a/src/Wifi/hosts b/src/Wifi/hosts new file mode 100644 index 0000000..ef44abf --- /dev/null +++ b/src/Wifi/hosts @@ -0,0 +1,2 @@ +127.0.0.1 localhost +127.0.1.1 Miyoomini \ No newline at end of file diff --git a/src/Wifi/networks/wifi_saves.txt b/src/Wifi/networks/wifi_saves.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Wifi/udhcpd.conf b/src/Wifi/udhcpd.conf new file mode 100644 index 0000000..6a44fbe --- /dev/null +++ b/src/Wifi/udhcpd.conf @@ -0,0 +1,2 @@ +interface wlan0 +nohook wpa_supplicant diff --git a/src/Wifi/wifi.py b/src/Wifi/wifi.py new file mode 100644 index 0000000..e614b46 --- /dev/null +++ b/src/Wifi/wifi.py @@ -0,0 +1,1852 @@ +#!/usr/bin/env python + +# wificonfig.py +# +# Requires: pygame +# +# Copyright (c) 2013 Hans Kokx +# +# Licensed under the GNU General Public License, Version 3.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.gnu.org/copyleft/gpl.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +''' + +TODO: +* Add option to cancel connecting to a network +* Clean up host ap info display. It's ugly. + +''' + + +import subprocess as SU +import sys, time, os, shutil, signal +import pygame +from pygame.locals import * +import pygame.gfxdraw +from os import listdir +from urllib import quote_plus, unquote_plus + +# What is our wireless interface? +wlan = "wlan0" + +## That's it for options. Everything else below shouldn't be edited. +confdir = "/mnt/SDCARD/App/Wifi/" +netconfdir = confdir+"networks/" +sysconfdir = "/appconfigs/" +datadir = "/mnt/SDCARD/App/Wifi/data/" +autoconnect_enabled = False + +surface = pygame.display.set_mode((320,240)) +selected_key = '' +passphrase = '' +active_menu = '' +encryptiontypes = ("WEP-40","WEP-128","WPA", "WPA2") +encryptionLabels = ('None', 'WEP', 'WPA', 'WPA2') +colors = { + "darkbg": (41, 41, 41), + "lightbg": (84, 84, 84), + "activeselbg": (160, 24, 24), + "inactiveselbg": (84, 84, 84), + "activetext": (255, 255, 255), + "inactivetext": (128, 128, 128), + "lightgrey": (200,200,200), + 'logogcw': (255, 255, 255), + 'logoconnect': (216, 32, 32), + "color": (255,255,255), + "yellow": (128, 128, 0), + "blue": (0, 0, 128), + "red": (128, 0, 0), + "green": (0, 128, 0), + "black": (0, 0, 0), + "white": (255, 255, 255), + } + +mac_addresses = {} + + +## Initialize the display, for pygame +if not pygame.display.get_init(): + pygame.display.init() +if not pygame.font.get_init(): + pygame.font.init() + +surface.fill(colors["darkbg"]) +pygame.mouse.set_visible(False) +pygame.key.set_repeat(199,69) #(delay,interval) + +## Fonts +font_path = '/mnt/SDCARD/Koriki/fonts/DejaVuSans.ttf' +font_tiny = pygame.font.Font(font_path, 8) +font_small = pygame.font.Font(font_path, 10) +font_medium = pygame.font.Font(font_path, 12) +font_large = pygame.font.Font(font_path, 16) +font_huge = pygame.font.Font(font_path, 48) +gcw_font = pygame.font.Font(os.path.join(datadir, 'gcwzero.ttf'), 23) +font_mono_small = pygame.font.Font(os.path.join(datadir, 'Inconsolata.otf'), 11) + +## File management +def createpaths(): # Create paths, if necessary + if not os.path.exists(confdir): + os.makedirs(confdir) + if not os.path.exists(netconfdir): + os.makedirs(netconfdir) + if not os.path.exists(sysconfdir): + os.makedirs(sysconfdir) + +## Interface management +def ifdown(iface): + #SU.Popen(['ifdown', iface], close_fds=True).wait() + SU.Popen(['/sbin/ifconfig', iface, 'down'], close_fds=True).wait() + SU.Popen(['sleep', '2'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'wpa_supplicant'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'udhcpc'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'hostapd'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'dnsmasq'], close_fds=True).wait() + #SU.Popen(['ap', '--stop'], close_fds=True).wait() + #SU.Popen(['/config/wifi/ssw01bClose.sh'], close_fds=True).wait() + SU.Popen(['/customer/app/axp_test', 'wifioff'], close_fds=True).wait() + SU.Popen(['/bin/sed', '-i', "s/\"wifi\":\s*[01]/\"wifi\": 0/", '/appconfigs/system.json'], close_fds=True).wait() + +def ifup(iface): + return SU.Popen(['ifup', iface], close_fds=True).wait() == 0 + +# Returns False if the interface was previously enabled +def enableiface(iface): + check = checkinterfacestatus(iface) + if check: + return False + + modal("Enabling WiFi...") + drawinterfacestatus() + pygame.display.update() + + SU.Popen(['/bin/sed', '-i', "s/\"wifi\":\s*[01]/\"wifi\": 1/", '/appconfigs/system.json'], close_fds=True).wait() + SU.Popen(['/customer/app/axp_test', 'wifion'], close_fds=True).wait() + SU.Popen(['sleep', '2'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'wpa_supplicant'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'udhcpc'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'hostapd'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'dnsmasq'], close_fds=True).wait() + while True: + if SU.Popen(['/sbin/ifconfig', iface, 'up'], close_fds=True).wait() == 0: + break + time.sleep(0.1); + SU.Popen(['/mnt/SDCARD/Koriki/bin/wpa_supplicant', '-B', '-D', 'nl80211', '-i', iface, '-c', '/appconfigs/wpa_supplicant.conf'], close_fds=True).wait() + mac_addresses[iface] = getmac(iface) + return True + +def autoconnect(iface): + check = checkinterfacestatus(iface) + if check: + return False + + modal("Autoconnecting...") + drawinterfacestatus() + pygame.display.update() + + SU.Popen(['/bin/sed', '-i', "s/\"wifi\":\s*[01]/\"wifi\": 1/", '/appconfigs/system.json'], close_fds=True).wait() + SU.Popen(['/customer/app/axp_test', 'wifion'], close_fds=True).wait() + SU.Popen(['sleep', '2'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'wpa_supplicant'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'udhcpc'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'hostapd'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'dnsmasq'], close_fds=True).wait() + while True: + if SU.Popen(['/sbin/ifconfig', iface, 'up'], close_fds=True).wait() == 0: + break + time.sleep(0.1); + SU.Popen(['/mnt/SDCARD/Koriki/bin/wpa_supplicant', '-B', '-D', 'nl80211', '-i', iface, '-c', '/appconfigs/wpa_supplicant.conf'], close_fds=True).wait() + mac_addresses[iface] = getmac(iface) + return True + +def disableiface(iface): + SU.Popen(['pkill', '-9', 'wpa_supplicant'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'udhcpc'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'hostapd'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'dnsmasq'], close_fds=True).wait() + SU.Popen(['/customer/app/axp_test', 'wifioff'], close_fds=True).wait() + SU.Popen(['/bin/sed', '-i', "s/\"wifi\":\s*[01]/\"wifi\": 0/", '/appconfigs/system.json'], close_fds=True).wait() + +def udhcpc_timeout(iface, timeout_seconds): + udhcpc_cmd = ['udhcpc', '-i', iface, '-s', '/etc/init.d/udhcpc.script'] + + try: + udhcpc_process = SU.Popen(udhcpc_cmd, stdout=SU.PIPE, stderr=SU.PIPE) + + start_time = time.time() + while True: + if time.time() - start_time > timeout_seconds: + os.kill(udhcpc_process.pid, signal.SIGTERM) + return False + + return_code = udhcpc_process.poll() + if return_code is not None: + break + + time.sleep(1) + + stdout, stderr = udhcpc_process.communicate() + + if return_code == 0: + print("udhcpc exito:", stdout.decode()) + return True + else: + print("udhcpc error:", stderr.decode()) + return False + + except Exception as e: + print("Error:", e) + return False + +def getip(iface): + with open(os.devnull, "w") as fnull: + output = SU.Popen(['/sbin/ifconfig', iface], + stderr=fnull, stdout=SU.PIPE, close_fds=True).stdout.readlines() + + for line in output: + if line.strip().startswith("inet addr"): + return str.strip( + line[line.find('inet addr')+len('inet addr"') : + line.find('Bcast')+len('Bcast')].rstrip('Bcast')) + +def getmac(iface): + try: + with open("/sys/class/net/" + iface + "/address", "rb") as mac_file: + return mac_file.readline(17) + except IOError: + return None # WiFi is disabled + +def getcurrentssid(iface): # What network are we connected to? + ssid = None + if not checkinterfacestatus(iface): + return None + + with open(os.devnull, "w") as fnull: + output = SU.Popen(['/mnt/SDCARD/Koriki/bin/iwconfig', iface], + stdout=SU.PIPE, stderr=fnull, close_fds=True).stdout.readlines() + for line in output: + if line.strip().startswith(iface): + ssid = str.strip(line[line.find('ESSID')+len('ESSID:"'):line.find('Nickname:')+len('Nickname:')].rstrip(' Nickname:').rstrip('"')) + return ssid + +def checkinterfacestatus(iface): + return getip(iface) != None + +def sync_system_time(): + SU.Popen(['ntpdate', '-u', 'pool.ntp.org'], close_fds=True).wait() + +def connect(iface): # Connect to a network + saved_file = netconfdir + quote_plus(ssid) + ".conf" + if os.path.exists(saved_file): + shutil.copy2(saved_file, sysconfdir+"config-"+iface+".conf") + + saved_file2 = netconfdir + quote_plus(ssid) + "_wpa.conf" + if os.path.exists(saved_file2): + shutil.copy2(saved_file2, sysconfdir+"wpa_supplicant.conf") + + if checkinterfacestatus(iface): + disconnect(iface) + + disconnect(iface) + enableiface(iface) + modal("Connecting...") + + if not udhcpc_timeout(wlan, 30): + modal('Connection failed!', wait=True) + disableiface(iface) + return False + + sync_system_time() + modal('Connected!', timeout=True) + pygame.display.update() + drawstatusbar() + drawinterfacestatus() + return True + +def disconnect(iface): + if checkinterfacestatus(iface): + modal("Disconnecting...") + ifdown(iface) + +def getnetworks(iface): # Run iwlist to get a list of networks in range + wasnotenabled = enableiface(iface) + modal("Scanning...") + + with open(os.devnull, "w") as fnull: + output = SU.Popen(['/mnt/SDCARD/Koriki/bin/iwlist', iface, 'scan'], + stdout=SU.PIPE, stderr=fnull, close_fds=True).stdout.readlines() + for item in output: + if item.strip().startswith('Cell'): + # network is the current list corresponding to a MAC address {MAC:[]} + network = networks.setdefault(parsemac(item), dict()) + + elif item.strip().startswith('ESSID:'): + network["ESSID"] = (parseessid(item)) + + elif item.strip().startswith('IE:') and not item.strip().startswith('IE: Unknown') or item.strip().startswith('Encryption key:'): + network["Encryption"] = (parseencryption(item)) + + elif item.strip().startswith('Quality='): + network["Quality"] = (parsequality(item)) + # Now the loop is over, we will probably find a MAC address and a new "network" will be created. + redraw() + + if wasnotenabled: + disableiface(iface) + return networks + +def listuniqssids(): + menuposition = 0 + uniqssid = {} + uniqssids = {} + + for network, detail in networks.iteritems(): + if detail['ESSID'] not in uniqssids and detail['ESSID']: + uniqssid = uniqssids.setdefault(detail['ESSID'], detail) + uniqssid["menu"] = menuposition + uniqssid["Encryption"] = detail['Encryption'] + menuposition += 1 + return uniqssids + +## Parsing iwlist output for various components +def parsemac(macin): + mac = str.strip(macin[macin.find("Address:")+len("Address: "):macin.find("\n")+len("\n")]) + return mac + +def parseessid(essid): + essid = str.strip(essid[essid.find('ESSID:"')+len('ESSID:"'):essid.find('"\n')+len('"\n')].rstrip('"\n')) + return essid + +def parsequality(quality): + quality = quality[quality.find("Quality=")+len("Quality="):quality.find(" S")+len(" S")].rstrip(" S") + if len(quality) < 1: + quality = '0/100' + return quality + +def parseencryption(encryption): + encryption = str.strip(encryption) + + if encryption.startswith('Encryption key:off'): + encryption = "none" + elif encryption.startswith('Encryption key:on'): + encryption = "WEP-40" + elif encryption.startswith("IE: WPA"): + encryption = "WPA" + elif encryption.startswith("IE: IEEE 802.11i/WPA2"): + encryption = "WPA2" + else: + encryption = "Encrypted (unknown)" + return encryption + +def aafilledcircle(surface, color, center, radius): + '''Helper function to draw anti-aliased circles using an interface similar + to pygame.draw.circle. + ''' + x, y = center + pygame.gfxdraw.aacircle(surface, x, y, radius, color) + pygame.gfxdraw.filled_circle(surface, x, y, radius, color) + return Rect(x - radius, y - radius, radius * 2 + 1, radius * 2 + 1) + +## Draw interface elements +class hint: + global colors + def __init__(self, button, text, x, y, bg=colors["darkbg"]): + self.button = button + self.text = text + self.x = x + self.y = y + self.bg = bg + self.drawhint() + + def drawhint(self): + if self.button == 'l' or self.button == 'r': + if self.button == 'l': + aafilledcircle(surface, colors["black"], (self.x, self.y+5), 5) + pygame.draw.rect(surface, colors["black"], (self.x-5, self.y+6, 10, 5)) + + + if self.button == 'r': + aafilledcircle(surface, colors["black"], (self.x+15, self.y+5), 5) + pygame.draw.rect(surface, colors["black"], (self.x+11, self.y+6, 10, 5)) + + button = pygame.draw.rect(surface, colors["black"], (self.x, self.y, 15, 11)) + text = font_tiny.render(self.button.upper(), True, colors["white"], colors["black"]) + buttontext = text.get_rect() + buttontext.center = button.center + surface.blit(text, buttontext) + + if self.button == "select" or self.button == "start": + lbox = aafilledcircle(surface, colors["black"], (self.x+5, self.y+5), 6) + rbox = aafilledcircle(surface, colors["black"], (self.x+29, self.y+5), 6) + straightbox = lbox.union(rbox) + buttoncenter = straightbox.center + if self.button == 'select': + straightbox.y = lbox.center[1] + straightbox.height = (straightbox.height + 1) / 2 + pygame.draw.rect(surface, colors["black"], straightbox) + + roundedbox = Rect(lbox.midtop, (rbox.midtop[0] - lbox.midtop[0], lbox.height - straightbox.height)) + if self.button == 'start': + roundedbox.bottomleft = lbox.midbottom + pygame.draw.rect(surface, colors["black"], roundedbox) + text = font_tiny.render(self.button.upper(), True, colors["white"], colors["black"]) + buttontext = text.get_rect() + buttontext.center = buttoncenter + buttontext.move_ip(0, 1) + surface.blit(text, buttontext) + + labelblock = pygame.draw.rect(surface, self.bg, (self.x+40,self.y,25,14)) + labeltext = font_tiny.render(self.text, True, colors["white"], self.bg) + surface.blit(labeltext, labelblock) + + elif self.button in ('a', 'b', 'y', 'x'): + if self.button == "a": + color = colors["red"] + elif self.button == "b": + color = colors["yellow"] + elif self.button == "y": + color = colors["green"] + elif self.button == "x": + color = colors["blue"] + + labelblock = pygame.draw.rect(surface, self.bg, (self.x+10,self.y,35,14)) + labeltext = font_tiny.render(self.text, True, colors["white"], self.bg) + surface.blit(labeltext, labelblock) + + button = aafilledcircle(surface, color, (self.x,self.y+5), 6) # (x, y) + text = font_tiny.render(self.button.upper(), True, colors["white"], color) + buttontext = text.get_rect() + buttontext.center = button.center + surface.blit(text, buttontext) + + elif self.button in ('left', 'right', 'up', 'down'): + + # Vertical + pygame.draw.rect(surface, colors["black"], (self.x+5, self.y-1, 4, 12)) + pygame.draw.rect(surface, colors["black"], (self.x+6, self.y-2, 2, 14)) + + # Horizontal + pygame.draw.rect(surface, colors["black"], (self.x+1, self.y+3, 12, 4)) + pygame.draw.rect(surface, colors["black"], (self.x, self.y+4, 14, 2)) + + if self.button == "left": + pygame.draw.rect(surface, colors["white"], (self.x+2, self.y+4, 3, 2)) + elif self.button == "right": + pygame.draw.rect(surface, colors["white"], (self.x+9, self.y+4, 3, 2)) + elif self.button == "up": + pygame.draw.rect(surface, colors["white"], (self.x+6, self.y+1, 2, 3)) + elif self.button == "down": + pygame.draw.rect(surface, colors["white"], (self.x+6, self.y+7, 2, 3)) + + labelblock = pygame.draw.rect(surface, self.bg, (self.x+20,self.y,35,14)) + labeltext = font_tiny.render(self.text, True, (255, 255, 255), self.bg) + surface.blit(labeltext, labelblock) + +class LogoBar(object): + '''The logo area at the top of the screen.''' + + def __init__(self): + self.text1 = gcw_font.render('KORIKI', True, colors['logogcw'], colors['lightbg']) + self.text2 = gcw_font.render('CONNECT', True, colors['logoconnect'], colors['lightbg']) + + def draw(self): + pygame.draw.rect(surface, colors['lightbg'], (0,0,320,34)) + pygame.draw.line(surface, colors['white'], (0, 34), (320, 34)) + + rect1 = self.text1.get_rect() + rect1.topleft = (8 + 5 + 1, 5) + surface.blit(self.text1, rect1) + + rect2 = self.text2.get_rect() + rect2.topleft = rect1.topright + surface.blit(self.text2, rect2) + +def drawstatusbar(): # Set up the status bar + global colors + pygame.draw.rect(surface, colors['lightbg'], (0,224,320,16)) + pygame.draw.line(surface, colors['white'], (0, 223), (320, 223)) + wlantext = font_mono_small.render("...", True, colors['white'], colors['lightbg']) + wlan_text = wlantext.get_rect() + wlan_text.topleft = (2, 225) + surface.blit(wlantext, wlan_text) + +def drawinterfacestatus(): # Interface status badge + global colors + wlanstatus = checkinterfacestatus(wlan) + if not wlanstatus: + wlanstatus = wlan+" is off." + else: + wlanstatus = getcurrentssid(wlan) + + wlantext = font_mono_small.render(wlanstatus, True, colors['white'], colors['lightbg']) + wlan_text = wlantext.get_rect() + wlan_text.topleft = (2, 225) + surface.blit(wlantext, wlan_text) + + # Note that the leading space here is intentional, to more cleanly overdraw any overly-long + # strings written to the screen beneath it (i.e. a very long ESSID) + if checkinterfacestatus(wlan): + ip_address = getip(wlan) + if ip_address is None: # Handle case where no IP is assigned + ip_address = " " # Display alternative message if no IP is available + text = font_mono_small.render(" " + ip_address, True, colors['white'], colors['lightbg']) + interfacestatus_text = text.get_rect() + interfacestatus_text.topright = (317, 225) + surface.blit(text, interfacestatus_text) + else: + mac = mac_addresses.get(wlan) # grabbed by enableiface() + if mac is not None: + text = font_mono_small.render(" " + mac, True, colors['white'], colors['lightbg']) + else: + text = font_mono_small.render(" ", True, colors['white'], colors['lightbg']) # Handle no MAC case + interfacestatus_text = text.get_rect() + interfacestatus_text.topright = (317, 225) + surface.blit(text, interfacestatus_text) + +def redraw(): + global colors + surface.fill(colors['darkbg']) + logoBar.draw() + mainmenu() + if wirelessmenu is not None: + wirelessmenu.draw() + pygame.draw.rect(surface, colors['darkbg'], (0, 208, 320, 16)) + hint("select", "Edit", 4, 210) + hint("a", "Connect", 75, 210) + hint("b", "/", 130, 210) + hint("left", "Back", 145, 210) + if active_menu == "main": + pygame.draw.rect(surface, colors['darkbg'], (0, 208, 320, 16)) + hint("a", "Select", 8, 210) + if active_menu == "saved": + hint("y", "Forget", 195, 210) + + drawstatusbar() + drawinterfacestatus() + pygame.display.update() + +def modal(text, wait=False, timeout=False, query=False): + global colors + dialog = pygame.draw.rect(surface, colors['lightbg'], (64,88,192,72)) + pygame.draw.rect(surface, colors['white'], (62,86,194,74), 2) + + text = font_medium.render(text, True, colors['white'], colors['lightbg']) + modal_text = text.get_rect() + modal_text.center = dialog.center + + surface.blit(text, modal_text) + pygame.display.update() + + if wait: + abutton = hint("a", "Continue", 205, 145, colors['lightbg']) + pygame.display.update() + elif timeout: + time.sleep(2.5) + redraw() + elif query: + abutton = hint("a", "Confirm", 150, 145, colors['lightbg']) + bbutton = hint("b", "Cancel", 205, 145, colors['lightbg']) + pygame.display.update() + while True: + for event in pygame.event.get(): + if event.type == KEYDOWN: + if event.key == K_SPACE: + return True + elif event.key == K_LCTRL: + return + + if not wait: + return + + while True: + for event in pygame.event.get(): + if event.type == KEYDOWN and event.key == K_SPACE: + redraw() + return + +## Connect to a network +def writeconfig(): # Write wireless configuration to disk + global passphrase + global encryption + try: + encryption + except NameError: + encryption = uniq[ssid]['Encryption'] + + if passphrase: + if passphrase == "none": + passphrase = "" + + conf = netconfdir + quote_plus(ssid) + ".conf" + + f = open(conf, "w") + f.write('WLAN_ESSID="'+ssid+'"\n') + + if encryption == "WEP-128": + encryption = "wep" + f.write('WLAN_PASSPHRASE="s:'+passphrase+'"\n') + else: + f.write('WLAN_PASSPHRASE="'+passphrase+'"\n') + if encryption == "WEP-40": + encryption = "wep" + elif encryption == "WPA": + encryption = "wpa" + elif encryption == "WPA2": + encryption = "wpa2" + + + f.write('WLAN_ENCRYPTION="'+encryption+'"\n') + f.write('WLAN_DHCP_RETRIES=20\n') + f.close() + + conf2 = netconfdir + quote_plus(ssid) + "_wpa.conf" + + f2 = open(conf2, "w") + f2.write('ctrl_interface=/var/run/wpa_supplicant\n') + f2.write('update_config=1\n') + f2.write('\n') + f2.write('network={\n') + f2.write('scan_ssid=1\n') + f2.write('ssid="'+ssid+'"\n') + if encryption == "WEP-128": + encryption = "wep" + f2.write('psk="s:'+passphrase+'"\n') + else: + f2.write('psk="'+passphrase+'"\n') + f2.write('}\n') + f2.close() + +def save_autoconnect_state(): + config_file = "/mnt/SDCARD/App/Wifi/autoconnect_state.txt" + with open(config_file, 'w') as f: + f.write("autoconnect_enabled={}\n".format(autoconnect_enabled)) + +def load_autoconnect_state(): + global autoconnect_enabled + config_file = "/mnt/SDCARD/App/Wifi/autoconnect_state.txt" + if os.path.exists(config_file): + with open(config_file, 'r') as f: + for line in f: + key, value = line.strip().split("=") + if key == "autoconnect_enabled": + autoconnect_enabled = value == "True" + +def toggle_autoconnect(): + global autoconnect_enabled + if autoconnect_enabled: + confirm = modal("Disable Autoconnect?", query=True) + if confirm: + autoconnect_enabled = False + modal("Autoconnect is OFF", wait=True) + redraw() + else: + active_menu = to_menu("main") + redraw() + else: + confirm = modal("Enable Autoconnect?", query=True) + if confirm: + autoconnect_enabled = True + modal("Autoconnect is ON", wait=True) + redraw() + else: + active_menu = to_menu("main") + redraw() + + save_autoconnect_state() + +## HostAP +def startap(): + global wlan + if checkinterfacestatus(wlan): + disconnect(wlan) + + modal("Creating ADHOC...") + SU.Popen(['pkill', '-9', 'wpa_supplicant'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'udhcpc'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'hostapd'], close_fds=True).wait() + SU.Popen(['pkill', '-9', 'dnsmasq'], close_fds=True).wait() + SU.Popen(['/bin/sed', '-i', "s/\"wifi\":\s*[01]/\"wifi\": 1/", '/appconfigs/system.json'], close_fds=True).wait() + SU.Popen(['/customer/app/axp_test', 'wifion'], close_fds=True).wait() + SU.Popen(['sleep', '2'], close_fds=True).wait() + #SU.Popen(['/config/wifi/ssw01bInit.sh'], close_fds=True).wait() + while True: + if SU.Popen(['/sbin/ifconfig', 'wlan0'], close_fds=True).wait() == 0: + break + time.sleep(0.1); + else: + #SU.Popen(['/config/wifi/ssw01bClose.sh'], close_fds=True).wait() + modal('Failed to create ADHOC...', wait=True) + redraw() + return False + + SU.Popen(['/sbin/ifconfig', 'wlan0', 'up'], close_fds=True).wait() + SU.Popen(['/mnt/SDCARD/Koriki/bin/iwconfig', 'wlan0', 'mode', 'master'], close_fds=True).wait() + SU.Popen(['/mnt/SDCARD/Koriki/bin/iw', 'dev', 'wlan0', 'set', 'type', '__ap'], close_fds=True).wait() + SU.Popen(['/mnt/SDCARD/Koriki/bin/hostapd', '-P' ,'/var/run/hostapd', '-B', '-i', 'wlan0', '/mnt/SDCARD/App/Wifi/hostapd.conf'], close_fds=True).wait() + time.sleep(0.5) + SU.Popen(['/sbin/ifconfig', 'wlan0', '192.168.4.100', 'netmask', '255.255.255.0', 'up'], close_fds=True).wait() + SU.Popen(['/mnt/SDCARD/Koriki/bin/dnsmasq', '-i', 'wlan0', '-C', '/mnt/SDCARD/App/Wifi/dnsmasq.conf'], close_fds=True) + time.sleep(2.0) + #SU.Popen(['ip', 'route', 'add', 'default', 'via', '192.168.4.100'], close_fds=True).wait() + #SU.Popen(['/mnt/SDCARD/Koriki/bin/dhcpcd', '-f', '/mnt/SDCARD/App/Wifi/udhcpd.conf'], close_fds=True) + #SU.Popen(['sysctl', '-w', 'net.ipv4.ip_forward=1'], close_fds=True).wait() + #SU.Popen(['/mnt/SDCARD/Koriki/bin/openport', '55435'], close_fds=True) + modal('ADHOC created!', timeout=True) + modal('AP MiyooMini Pass 12345678', wait=True) + return True +## Input methods + +keyLayouts = { + 'qwertyNormal': ( + ('`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='), + ('q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'), + ('a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\''), + ('z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/'), + ), + 'qwertyShift': ( + ('~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+'), + ('Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'), + ('A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"'), + ('Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?'), + ), + 'wep': ( + ('1', '2', '3', '4'), + ('5', '6', '7', '8'), + ('9', '0', 'A', 'B'), + ('C', 'D', 'E', 'F'), + ), + } +keyboardCycleOrder = ('wep', 'qwertyNormal', 'qwertyShift') +def nextKeyboard(board): + return keyboardCycleOrder[ + (keyboardCycleOrder.index(board) + 1) % len(keyboardCycleOrder) + ] + +class key: + global colors + def __init__(self): + self.key = [] + self.selection_color = colors['activeselbg'] + self.text_color = colors['activetext'] + self.selection_position = (0,0) + self.selected_item = 0 + + def init(self, key, row, column): + self.key = key + self.row = row + self.column = column + self.drawkey() + + def drawkey(self): + key_width = 16 + key_height = 16 + + top = 136 + self.row * 20 + left = 32 + self.column * 20 + + if len(self.key) > 1: + key_width = 36 + keybox = pygame.draw.rect(surface, colors['lightbg'], (left,top,key_width,key_height)) + text = font_medium.render(self.key, True, colors['white'], colors['lightbg']) + label = text.get_rect() + label.center = keybox.center + label.y -= 1 + surface.blit(text, label) + +class radio: + global colors + def __init__(self): + self.key = [] + self.selection_color = colors['activeselbg'] + self.text_color = colors['activetext'] + self.selection_position = (0,0) + self.selected_item = 0 + + def init(self, key, row, column): + self.key = key + self.row = row + self.column = column + self.drawkey() + + def drawkey(self): + key_width = 64 + key_height = 16 + + top = 136 + self.row * 20 + left = 32 + self.column * 64 + + if len(self.key) > 1: + key_width = 64 + radiobutton = aafilledcircle(surface, colors['white'], (left, top), 8) + aafilledcircle(surface, colors['darkbg'], (left, top), 6) + text = font_medium.render(self.key, True, (255, 255, 255), colors['darkbg']) + label = text.get_rect() + label.left = radiobutton.right + 8 + label.top = radiobutton.top + 4 + surface.blit(text, label) + +def getSSID(): + global passphrase + displayinputlabel("ssid") + drawkeyboard("qwertyNormal") + getinput("qwertyNormal", "ssid") + ssid = passphrase + passphrase = '' + return ssid + +def drawEncryptionType(): + global colors + # Draw top background + pygame.draw.rect(surface, colors['darkbg'], (0,40,320,200)) + + # Draw footer + pygame.draw.rect(surface, colors['lightbg'], (0,224,320,16)) + pygame.draw.line(surface, colors['white'], (0, 223), (320, 223)) + hint("select", "Cancel", 4, 227, colors['lightbg']) + hint("a", "Enter", 285, 227, colors['lightbg']) + + # Draw the keys + z = radio() + for i, label in enumerate(encryptionLabels): + z.init(label, 0, i) + + pygame.display.update() + +def displayencryptionhint(): + global colors + global encryption + + try: + if encryption: + if encryption == "wep": + encryption = "WEP-40" + except: + pass + + try: + if encryption: + pygame.draw.rect(surface, colors['darkbg'], (0,100,320,34)) + hint("l", "L", 16, 113) + hint("r", "R", 289, 113) + + pos = 1 + for enc in encryptiontypes: + x = (pos * 60) - 20 + labelblock = pygame.Rect(x,111,55,14) + if enc == encryption: + # Draw a selection rectangle for the active encryption method + pygame.draw.rect(surface, colors['activeselbg'], labelblock) + labeltext = font_small.render(enc.center(10, ' '), True, colors["white"]) + surface.blit(labeltext, labelblock) + pos += 1 + pygame.display.update() + except NameError: + pass + +def chooseencryption(direction): + global selected_key + + encryption = '' + + if direction == "left": + selected_key[0] = (selected_key[0] - 1) % len(encryptionLabels) + + elif direction == "right": + selected_key[0] = (selected_key[0] + 1) % len(encryptionLabels) + + elif direction == "select": + encryption = encryptionLabels[selected_key[0]] + if encryption == "WEP": + encryption = "WEP-40" + + elif direction == "init": + selected_key = [0,0] + + drawEncryptionType() + pos = (32 + selected_key[0] * 64, 136) + aafilledcircle(surface, colors['activeselbg'], pos, 6) + pygame.display.update() + + return encryption + +def prevEncryption(): + global encryption + + for i, s in enumerate(encryptiontypes): + if encryption in s: + x = encryptiontypes.index(s)-1 + try: + encryption = encryptiontypes[x] + return + except IndexError: + encryption = encryptiontypes[:-1] + return + +def nextEncryption(): + global encryption + + for i, s in enumerate(encryptiontypes): + if encryption in s: + x = encryptiontypes.index(s)+1 + try: + encryption = encryptiontypes[x] + return + except IndexError: + encryption = encryptiontypes[0] + return + +def getEncryptionType(): + chooseencryption("init") + while True: + for event in pygame.event.get(): + if event.type == KEYDOWN: + if event.key == K_LEFT: # Move cursor left + chooseencryption("left") + if event.key == K_RIGHT: # Move cursor right + chooseencryption("right") + if event.key == K_SPACE: # A button + return chooseencryption("select") + if event.key == K_RCTRL: # Select key + return 'cancel' + +def drawkeyboard(board): + global colors + + # Draw keyboard background + pygame.draw.rect(surface, colors['darkbg'], (0,134,320,106)) + + # Draw bottom background + pygame.draw.rect(surface, colors['lightbg'], (0,224,320,16)) + pygame.draw.line(surface, colors['white'], (0, 223), (320, 223)) + + hint("select", "Cancel", 4, 227, colors['lightbg']) + hint("start", "Finish", 75, 227, colors['lightbg']) + hint("y", "Delete", 155, 227, colors['lightbg']) + if not board == "wep": + hint("x", "Shift", 200, 227, colors['lightbg']) + hint("b", "Space", 240, 227, colors['lightbg']) + + else: + hint("x", "Full KB", 200, 227, colors['lightbg']) + + hint("a", "Enter", 285, 227, colors['lightbg']) + + # Draw the keys + z = key() + for row, rowData in enumerate(keyLayouts[board]): + for column, label in enumerate(rowData): + z.init(label, row, column) + + pygame.display.update() + +def getinput(board, kind, ssid=""): + selectkey(board, kind) + return softkeyinput(board, kind, ssid) + +def softkeyinput(keyboard, kind, ssid): + global passphrase + global encryption + global securitykey + def update(): + displayinputlabel("key") + displayencryptionhint() + + while True: + event = pygame.event.wait() + + if event.type == KEYDOWN: + if event.key == K_RETURN: # finish input + selectkey(keyboard, kind, "enter") + redraw() + if ssid == '': + return False + writeconfig() + connect(wlan) + return True + + if event.key == K_UP: # Move cursor up + selectkey(keyboard, kind, "up") + if event.key == K_DOWN: # Move cursor down + selectkey(keyboard, kind, "down") + if event.key == K_LEFT: # Move cursor left + selectkey(keyboard, kind, "left") + if event.key == K_RIGHT: # Move cursor right + selectkey(keyboard, kind, "right") + if event.key == K_SPACE: # A button + selectkey(keyboard, kind, "select") + if event.key == K_LCTRL: # B button + if encryption != "WEP-40": + selectkey(keyboard, kind, "space") + if event.key == K_LSHIFT: # X button (swap keyboards) + keyboard = nextKeyboard(keyboard) + drawkeyboard(keyboard) + selectkey(keyboard, kind, "swap") + if event.key == K_LALT: # Y button + selectkey(keyboard, kind, "delete") + if event.key == K_RCTRL: # Select key + passphrase = '' + try: + encryption + except NameError: + pass + else: + del encryption + + try: + securitykey + except NameError: + pass + else: + del securitykey + redraw() + return False + if kind == "key": + if event.key == K_e: # L shoulder button + prevEncryption() + update() + if event.key == K_t: # R shoulder button + nextEncryption() + update() + +def displayinputlabel(kind, size=24): # Display passphrase on screen + global colors + global encryption + + def update(): + displayencryptionhint() + + if kind == "ssid": + # Draw SSID and encryption type labels + pygame.draw.rect(surface, colors['darkbg'], (0,100,320,34)) + labelblock = pygame.draw.rect(surface, colors['white'], (0,35,320,20)) + labeltext = font_large.render("Enter new SSID", True, colors['lightbg'], colors['white']) + label = labeltext.get_rect() + label.center = labelblock.center + surface.blit(labeltext, label) + + elif kind == "key": + displayencryptionhint() + # Draw SSID and encryption type labels + labelblock = pygame.draw.rect(surface, colors['white'], (0,35,320,20)) + labeltext = font_large.render("Enter "+encryption+" key", True, colors['lightbg'], colors['white']) + label = labeltext.get_rect() + label.center = labelblock.center + surface.blit(labeltext, label) + update() + + # Input area + bg = pygame.draw.rect(surface, colors['white'], (0, 55, 320, 45)) + text = "[ " + text += passphrase + text += " ]" + pw = font_mono_small.render(text, True, (0, 0, 0), colors['white']) + pwtext = pw.get_rect() + pwtext.center = bg.center + surface.blit(pw, pwtext) + pygame.display.update() + +def selectkey(keyboard, kind, direction=""): + def highlightkey(keyboard, pos='[0,0]'): + drawkeyboard(keyboard) + pygame.display.update() + + left_margin = 32 + top_margin = 136 + + if pos[0] > left_margin: + x = left_margin + (16 * (pos[0])) + else: + x = left_margin + (16 * pos[0]) + (pos[0] * 4) + + + if pos[1] > top_margin: + y = top_margin + (16 * (pos[1])) + else: + y = top_margin + (16 * pos[1]) + (pos[1] * 4) + + pointlist = [ + (x, y), + (x + 16, y), + (x + 16, y + 16), + (x, y + 16), + (x, y) + ] + lines = pygame.draw.lines(surface, (255,255,255), True, pointlist, 1) + pygame.display.update() + + global selected_key + global passphrase + + if not selected_key: + selected_key = [0,0] + + def clampRow(): + selected_key[1] = min(selected_key[1], len(layout) - 1) + def clampColumn(): + selected_key[0] = min(selected_key[0], len(layout[selected_key[1]]) - 1) + + layout = keyLayouts[keyboard] + if direction == "swap": + # Clamp row first since each row can have a different number of columns. + clampRow() + clampColumn() + elif direction == "up": + selected_key[1] = (selected_key[1] - 1) % len(layout) + clampColumn() + elif direction == "down": + selected_key[1] = (selected_key[1] + 1) % len(layout) + clampColumn() + elif direction == "left": + selected_key[0] = (selected_key[0] - 1) % len(layout[selected_key[1]]) + elif direction == "right": + selected_key[0] = (selected_key[0] + 1) % len(layout[selected_key[1]]) + elif direction == "select": + passphrase += layout[selected_key[1]][selected_key[0]] + if len(passphrase) > 20: + logoBar.draw() + displayinputlabel(kind, 12) + else: + displayinputlabel(kind) + elif direction == "space": + passphrase += ' ' + if len(passphrase) > 20: + logoBar.draw() + displayinputlabel(kind, 12) + else: + displayinputlabel(kind) + elif direction == "delete": + if len(passphrase) > 0: + passphrase = passphrase[:-1] + logoBar.draw() + if len(passphrase) > 20: + displayinputlabel(kind, 12) + else: + displayinputlabel(kind) + + highlightkey(keyboard, selected_key) + +class Menu: + font = font_medium + dest_surface = surface + canvas_color = colors["darkbg"] + + elements = [] + + def __init__(self): + self.set_elements([]) + self.selected_item = 0 + self.origin = (0,0) + self.menu_width = 0 + self.menu_height = 0 + self.selection_color = colors["activeselbg"] + self.text_color = colors["activetext"] + self.font = font_medium + + def move_menu(self, top, left): + self.origin = (top, left) + + def set_colors(self, text, selection, background): + self.text_color = text + self.selection_color = selection + + def set_elements(self, elements): + self.elements = elements + + def get_position(self): + return self.selected_item + + def get_selected(self): + return self.elements[self.selected_item] + + def init(self, elements, dest_surface): + self.set_elements(elements) + self.dest_surface = dest_surface + + def draw(self,move=0): + # Clear any old text (like from apinfo()), but don't overwrite button hint area above statusbar + pygame.draw.rect(surface, colors['darkbg'], (0,35,320,173)) + + if len(self.elements) == 0: + return + + self.selected_item = (self.selected_item + move) % len(self.elements) + + # Which items are to be shown? + if self.selected_item <= 2: # We're at the top + visible_elements = self.elements[0:6] + selected_within_visible = self.selected_item + elif self.selected_item >= len(self.elements) - 3: # We're at the bottom + visible_elements = self.elements[-6:] + selected_within_visible = self.selected_item - (len(self.elements) - len(visible_elements)) + else: # The list is larger than 5 elements, and we're in the middle + visible_elements = self.elements[self.selected_item - 2:self.selected_item + 3] + selected_within_visible = 2 + + # What width does everything have? + max_width = max([self.get_item_width(visible_element) for visible_element in visible_elements]) + # And now the height + heights = [self.get_item_height(visible_element) for visible_element in visible_elements] + total_height = sum(heights) + + # Background + menu_surface = pygame.Surface((max_width, total_height)) + menu_surface.fill(self.canvas_color) + + # Selection + left = 0 + top = sum(heights[0:selected_within_visible]) + width = max_width + height = heights[selected_within_visible] + selection_rect = (left, top, width, height) + pygame.draw.rect(menu_surface,self.selection_color,selection_rect) + + # Clear any error elements + error_rect = (left+width+8, 35, 192, 172) + pygame.draw.rect(surface,colors['darkbg'],error_rect) + + # Elements + top = 0 + for i in xrange(len(visible_elements)): + self.render_element(menu_surface, visible_elements[i], 0, top) + top += heights[i] + self.dest_surface.blit(menu_surface,self.origin) + return self.selected_item + + def get_item_height(self, element): + render = self.font.render(element, 1, self.text_color) + spacing = 5 + return render.get_rect().height + spacing * 2 + + def get_item_width(self, element): + render = self.font.render(element, 1, self.text_color) + spacing = 5 + return render.get_rect().width + spacing * 2 + + def render_element(self, menu_surface, element, left, top): + render = self.font.render(element, 1, self.text_color) + spacing = 5 + menu_surface.blit(render, (left + spacing, top + spacing, render.get_rect().width, render.get_rect().height)) + +class NetworksMenu(Menu): + def set_elements(self, elements): + self.elements = elements + + def get_item_width(self, element): + the_ssid = element[0] + render = self.font.render(the_ssid, 1, self.text_color) + spacing = 15 + return render.get_rect().width + spacing * 2 + + def get_item_height(self, element): + render = self.font.render(element[0], 1, self.text_color) + spacing = 6 + return (render.get_rect().height + spacing * 2) + 5 + + def render_element(self, menu_surface, element, left, top): + the_ssid = element[0] + + def qualityPercent(x): + percent = (float(x.split("/")[0]) / float(x.split("/")[1])) * 100 + if percent > 100: + percent = 100 + return int(percent) + ## Wifi signal icons + percent = qualityPercent(element[1]) + + if percent >= 6 and percent <= 24: + signal_icon = 'wifi-0.png' + elif percent >= 25 and percent <= 49: + signal_icon = 'wifi-1.png' + elif percent >= 50 and percent <= 74: + signal_icon = 'wifi-2.png' + elif percent >= 75: + signal_icon = 'wifi-3.png' + else: + signal_icon = 'transparent.png' + + ## Encryption information + enc_type = element[2] + if enc_type == "NONE" or enc_type == '': + enc_icon = "open.png" + enc_type = "Open" + elif enc_type == "WPA" or enc_type == "wpa": + enc_icon = "closed.png" + elif enc_type == "WPA2" or enc_type == "wpa2": + enc_icon = "closed.png" + elif enc_type == "WEP-40" or enc_type == "WEP-128" or enc_type == "wep" or enc_type == "WEP": + enc_icon = "closed.png" + enc_type = "WEP" + else: + enc_icon = "unknown.png" + enc_type = "(Unknown)" + + + qual_img = pygame.image.load((os.path.join(datadir, signal_icon))).convert_alpha() + enc_img = pygame.image.load((os.path.join(datadir, enc_icon))).convert_alpha() + transparent_qual = qual_img.copy() + transparent_qual.fill((255, 255, 255, 100), special_flags=pygame.BLEND_RGBA_MULT) + transparent_enc = enc_img.copy() + transparent_enc.fill((255, 255, 255, 100), special_flags=pygame.BLEND_RGBA_MULT) + + ssid = font_mono_small.render(the_ssid, 1, self.text_color) + enc = font_small.render(enc_type, 1, colors["lightgrey"]) + #strength = font_small.render(str(str(percent) + "%").rjust(4), 1, colors["lightgrey"]) + #qual = font_small.render(element[1], 1, colors["lightgrey"]) + spacing = 2 + + menu_surface.blit(ssid, (left + spacing, top)) + menu_surface.blit(enc, (left + enc_img.get_rect().width + 12, top + 18)) + menu_surface.blit(enc_img, (left + 8, (top + 24) - (enc_img.get_rect().height / 2))) + # menu_surface.blit(strength, (left + 137, top + 18, strength.get_rect().width, strength.get_rect().height)) + qual_x = left + 200 - qual_img.get_rect().width - 3 + qual_y = top + 7 + 6 + menu_surface.blit(qual_img, (qual_x, qual_y)) + pygame.display.update() + + def draw(self,move=0): + if len(self.elements) == 0: + return + + if move != 0: + self.selected_item += move + if self.selected_item < 0: + self.selected_item = 0 + elif self.selected_item >= len(self.elements): + self.selected_item = len(self.elements) - 1 + + # Which items are to be shown? + if self.selected_item <= 2: # We're at the top + visible_elements = self.elements[0:5] + selected_within_visible = self.selected_item + elif self.selected_item >= len(self.elements) - 3: # We're at the bottom + visible_elements = self.elements[-5:] + selected_within_visible = self.selected_item - (len(self.elements) - len(visible_elements)) + else: # The list is larger than 5 elements, and we're in the middle + visible_elements = self.elements[self.selected_item - 2:self.selected_item + 3] + selected_within_visible = 2 + + max_width = 320 - self.origin[0] - 3 + + # And now the height + heights = [self.get_item_height(visible_element) for visible_element in visible_elements] + total_height = sum(heights) + + # Background + menu_surface = pygame.Surface((max_width, total_height)) + menu_surface.fill(self.canvas_color) + + # Selection + left = 0 + top = sum(heights[0:selected_within_visible]) + width = max_width + height = heights[selected_within_visible] + selection_rect = (left, top, width, height) + pygame.draw.rect(menu_surface,self.selection_color,selection_rect) + + # Elements + top = 0 + for i in xrange(len(visible_elements)): + self.render_element(menu_surface, visible_elements[i], 0, top) + top += heights[i] + self.dest_surface.blit(menu_surface,self.origin) + return self.selected_item + +def to_menu(new_menu): + global colors + if new_menu == "main": + menu.set_colors(colors['activetext'], colors['activeselbg'], colors['darkbg']) + if wirelessmenu is not None: + wirelessmenu.set_colors(colors['inactivetext'], colors['inactiveselbg'], colors['darkbg']) + elif new_menu == "ssid" or new_menu == "saved": + menu.set_colors(colors['inactivetext'], colors['inactiveselbg'], colors['darkbg']) + wirelessmenu.set_colors(colors['activetext'], colors['activeselbg'], colors['darkbg']) + return new_menu + +wirelessmenu = None +menu = Menu() +menu.move_menu(3, 41) + +def mainmenu(): + global wlan + elems = ['Quit'] + + try: + ap = getcurrentssid(wlan).split("-")[1] + file = open('/sys/class/net/wlan0/address', 'r') + mac = file.read().strip('\n').replace(":", "") + file.close() + if mac == ap: + elems = ['AP info'] + elems + except: + pass + + elems = ["Saved Networks", 'Scan for APs', "Manual Setup", 'Autoconnect'] + elems + + if checkinterfacestatus(wlan) and getcurrentssid(wlan) is not None: + elems = ['Disconnect'] + elems + + menu.init(elems, surface) + menu.draw() + +def apinfo(): + global wlan + + try: + ap = getcurrentssid(wlan).split("-")[1] + file = open('/sys/class/net/wlan0/address', 'r') + mac = file.read().strip('\n').replace(":", "") + file.close() + if mac == ap: + ssidlabel = "SSID" + renderedssidlabel = font_huge.render(ssidlabel, True, colors["lightbg"], colors["darkbg"]) + ssidlabelelement = renderedssidlabel.get_rect() + ssidlabelelement.right = 318 + ssidlabelelement.top = 34 + surface.blit(renderedssidlabel, ssidlabelelement) + + ssid = getcurrentssid(wlan) + renderedssid = font_mono_small.render(ssid, True, colors["white"], colors["darkbg"]) + ssidelement = renderedssid.get_rect() + ssidelement.right = 315 + ssidelement.top = 96 + surface.blit(renderedssid, ssidelement) + + enclabel = "Key" + renderedenclabel = font_huge.render(enclabel, True, colors["lightbg"], colors["darkbg"]) + enclabelelement = renderedenclabel.get_rect() + enclabelelement.right = 314 # Drawn a bit leftwards versus "SSID" text, so both right-align pixel-perfectly + enclabelelement.top = 114 + surface.blit(renderedenclabel, enclabelelement) + + renderedencp = font_mono_small.render(mac, True, colors["white"], colors["darkbg"]) + encpelement = renderedencp.get_rect() + encpelement.right = 315 + encpelement.top = 180 + surface.blit(renderedencp, encpelement) + + pygame.display.update() + except: + text = ":(" + renderedtext = font_huge.render(text, True, colors["lightbg"], colors["darkbg"]) + textelement = renderedtext.get_rect() + textelement.left = 192 + textelement.top = 96 + surface.blit(renderedtext, textelement) + pygame.display.update() + +def create_wireless_menu(): + global wirelessmenu + wirelessmenu = NetworksMenu() + wirelessmenu.move_menu(116,40) + +def destroy_wireless_menu(): + global wirelessmenu + wirelessmenu = None + +def create_saved_networks_menu(): + global uniq + + uniqssids = {} + menu = 1 + for confName in sorted(listdir(netconfdir)): + if not confName.endswith('.conf'): + continue + ssid = unquote_plus(confName[:-5]) + + detail = { + 'ESSID': ssid, + 'Encryption': '', + 'Key': '', + 'Quality': '0/1', + 'menu': menu, + } + try: + with open(netconfdir + confName) as f: + for line in f: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + if len(value) >= 2 and value[0] == '"' and value[-1] == '"': + value = value[1:-1] + + if key == 'WLAN_ESSID': + detail['ESSID'] = value + elif key == 'WLAN_ENCRYPTION': + detail['Encryption'] = value + elif key == 'WLAN_PASSPHRASE': + # TODO: fix for 128-bit wep + detail['Key'] = value + except IOError as ex: + print 'Error reading conf:', ex + except ValueError as ex: + print 'Error parsing conf line:', line.strip() + else: + uniqssids[ssid] = detail + menu += 1 + uniq = uniqssids + + if uniq: + l = [] + for item in sorted(uniq.iterkeys(), key=lambda x: uniq[x]['menu']): + detail = uniq[item] + l.append([ detail['ESSID'], detail['Quality'], detail['Encryption'].upper()]) + create_wireless_menu() + wirelessmenu.init(l, surface) + wirelessmenu.draw() + else: + text = 'empty' + renderedtext = font_huge.render(text, True, colors["lightbg"], colors["darkbg"]) + textelement = renderedtext.get_rect() + textelement.left = 152 + textelement.top = 96 + surface.blit(renderedtext, textelement) + pygame.display.update() + +def convert_file_names(): + """In the directory containing WiFi network configuration files, removes + backslashes from file names created by older versions of GCW Connect.""" + try: + confNames = listdir(netconfdir) + except IOError as ex: + print "Failed to list files in '%s': %s" (netconfdir, ex) + else: + for confName in confNames: + if not confName.endswith('.conf'): + continue + if '\\' in confName: + old, new = confName, quote_plus(confName.replace('\\', '')) + try: + os.rename(os.path.join(netconfdir, old), os.path.join(netconfdir, new)) + except IOError as ex: + print "Failed to rename old-style network configuration file '%s' to '%s': %s" % (os.path.join(netconfdir, old), new, ex) + +if __name__ == "__main__": + # Persistent variables + networks = {} + uniqssids = {} + active_menu = "main" + + try: + createpaths() + except: + pass ## Can't create directories. Great for debugging on a pc. + else: + convert_file_names() + + logoBar = LogoBar() + + redraw() + + load_autoconnect_state() + + if autoconnect_enabled: + modal("Autoconnecting...") + autoconnect(wlan) + if not udhcpc_timeout(wlan, 30): + modal('Autoconnect failed!', wait=True) + else: + modal('Autoconnect successful!') + time.sleep(2) + sys.exit() + + while True: + time.sleep(0.01) + for event in pygame.event.get(): + ## Miyoo mini keycodes: + # A = K_SPACE + # B = K_LCTRL + # Y = K_LALT + # X = K_LSHIFT + # L = K_e + # R = K_t + # L2 = K_TAB + # R2 = K_BACKSPACE + # start = K_RETURN + # select = K_RCTRL + # menu = K_ESCAPE + # power down = K_POWER + + if event.type == QUIT: + pygame.display.quit() + sys.exit() + + elif event.type == KEYDOWN: + if event.key == K_POWER: # Power down + pass + elif event.key == K_e: # Left shoulder button + pass + elif event.key == K_t: # Right shoulder button + pass + elif event.key == K_ESCAPE: # menu + pygame.display.quit() + sys.exit() + pass + elif event.key == K_UP: # Arrow up the menu + if active_menu == "main": + menu.draw(-1) + elif active_menu == "ssid" or active_menu == "saved": + wirelessmenu.draw(-1) + elif event.key == K_DOWN: # Arrow down the menu + if active_menu == "main": + menu.draw(1) + elif active_menu == "ssid" or active_menu == "saved": + wirelessmenu.draw(1) + elif event.key == K_RIGHT: + if wirelessmenu is not None and active_menu == "main": + active_menu = to_menu("ssid") + redraw() + elif event.key == K_LCTRL or event.key == K_LEFT: + if active_menu == "ssid" or active_menu == "saved": + destroy_wireless_menu() + active_menu = to_menu("main") + del uniq + redraw() + elif event.key == K_LCTRL: + pygame.display.quit() + sys.exit() + elif event.key == K_LALT: + if active_menu == "saved": + confirm = modal("Forget AP configuration?", query=True) + if confirm: + os.remove(netconfdir+quote_plus(str(wirelessmenu.get_selected()[0]))+".conf") + os.remove(netconfdir+quote_plus(str(wirelessmenu.get_selected()[0]))+"_wpa.conf") + create_saved_networks_menu() + redraw() + if len(uniq) < 1: + destroy_wireless_menu() + active_menu = to_menu("main") + redraw() + elif event.key == K_SPACE or event.key == K_RETURN: + # Main menu + if active_menu == "main": + if menu.get_selected() == 'Disconnect': + disconnect(wlan) + redraw() + pygame.display.update() + elif menu.get_selected() == 'Scan for APs': + try: + getnetworks(wlan) + uniq = listuniqssids() + except: + uniq = {} + text = ":(" + renderedtext = font_huge.render(text, True, colors["lightbg"], colors["darkbg"]) + textelement = renderedtext.get_rect() + textelement.left = 192 + textelement.top = 96 + surface.blit(renderedtext, textelement) + pygame.display.update() + + l = [] + if len(uniq) < 1: + text = ":(" + renderedtext = font_huge.render(text, True, colors["lightbg"], colors["darkbg"]) + textelement = renderedtext.get_rect() + textelement.left = 192 + textelement.top = 96 + surface.blit(renderedtext, textelement) + pygame.display.update() + else: + for item in sorted(uniq.iterkeys(), key=lambda x: uniq[x]['menu']): + for network, detail in uniq.iteritems(): + if network == item: + try: + detail['Quality'] + except KeyError: + detail['Quality'] = "0/1" + try: + detail['Encryption'] + except KeyError: + detail['Encryption'] = "" + + menuitem = [ detail['ESSID'], detail['Quality'], detail['Encryption']] + l.append(menuitem) + + create_wireless_menu() + wirelessmenu.init(l, surface) + wirelessmenu.draw() + + active_menu = to_menu("ssid") + redraw() + elif menu.get_selected() == 'Manual Setup': + ssid = '' + encryption = '' + passphrase = '' + selected_key = '' + securitykey = '' + + # Get SSID from the user + ssid = getSSID() + if ssid == '': + pass + else: + drawEncryptionType() + encryption = getEncryptionType() + displayinputlabel("key") + displayencryptionhint() + + # Get key from the user + if not encryption == 'None': + if encryption == "WPA": + drawkeyboard("qwertyNormal") + securitykey = getinput("qwertyNormal", "key", ssid) + elif encryption == "WPA2": + drawkeyboard("qwertyNormal") + securitykey = getinput("qwertyNormal", "key", ssid) + elif encryption == "WEP-40": + drawkeyboard("wep") + securitykey = getinput("wep", "key", ssid) + elif encryption == 'cancel': + del encryption, ssid, securitykey + redraw() + else: + encryption = "none" + redraw() + writeconfig() + connect(wlan) + try: + encryption + except NameError: + pass + + elif menu.get_selected() == 'Saved Networks': + create_saved_networks_menu() + try: + active_menu = to_menu("saved") + redraw() + except: + active_menu = to_menu("main") + + elif menu.get_selected() == 'Create ADHOC': + startap() + + elif menu.get_selected() == 'Autoconnect': + toggle_autoconnect() + + elif menu.get_selected() == 'AP info': + apinfo() + + elif menu.get_selected() == 'Quit': + pygame.display.quit() + sys.exit() + + # SSID menu + elif active_menu == "ssid": + ssid = "" + for network, detail in uniq.iteritems(): + position = str(wirelessmenu.get_position()) + if str(detail['menu']) == position: + if detail['ESSID'].split("-")[0] == "gcwzero": + ssid = detail['ESSID'] + conf = netconfdir + quote_plus(ssid) + ".conf" + encryption = "WPA2" + passphrase = ssid.split("-")[1] + connect(wlan) + else: + ssid = detail['ESSID'] + conf = netconfdir + quote_plus(ssid) + ".conf" + encryption = detail['Encryption'] + if not os.path.exists(conf): + if encryption == "none": + passphrase = "none" + encryption = "none" + writeconfig() + connect(wlan) + elif encryption == "WEP-40" or encryption == "WEP-128": + passphrase = '' + selected_key = '' + securitykey = '' + displayinputlabel("key") + drawkeyboard("wep") + encryption = "wep" + passphrase = getinput("wep", "key", ssid) + else: + passphrase = '' + selected_key = '' + securitykey = '' + displayinputlabel("key") + drawkeyboard("qwertyNormal") + passphrase = getinput("qwertyNormal", "key", ssid) + else: + connect(wlan) + break + + # Saved Networks menu + elif active_menu == "saved": + ssid = '' + for network, detail in uniq.iteritems(): + position = str(wirelessmenu.get_position()+1) + if str(detail['menu']) == position: + encryption = detail['Encryption'] + ssid = str(detail['ESSID']) + shutil.copy2(netconfdir + quote_plus(ssid) + ".conf", sysconfdir+"config-"+wlan+".conf") + shutil.copy2(netconfdir + quote_plus(ssid) + "_wpa.conf", sysconfdir+"wpa_supplicant.conf") + passphrase = detail['Key'] + #enableiface(wlan) + connect(wlan) + break + + elif event.key == K_RCTRL: + if active_menu == "ssid": # Allow us to edit the existing key + ssid = "" + for network, detail in uniq.iteritems(): + position = str(wirelessmenu.get_position()) + if str(detail['menu']) == position: + ssid = network + encryption = detail['Encryption'] + if detail['Encryption'] == "none": + pass + elif detail['Encryption'] == "wep": + passphrase = '' + selected_key = '' + securitykey = '' + displayinputlabel("key") + drawkeyboard("wep") + getinput("wep", "key", ssid) + else: + passphrase = '' + selected_key = '' + securitykey = '' + displayinputlabel("key") + drawkeyboard("qwertyNormal") + getinput("qwertyNormal", "key", ssid) + + if active_menu == "saved": # Allow us to edit the existing key + ssid = '' + + for network, detail in uniq.iteritems(): + position = str(wirelessmenu.get_position()+1) + if str(detail['menu']) == position: + ssid = network + passphrase = uniq[network]['Key'] + encryption = uniq[network]['Encryption'].upper() + if uniq[network]['Encryption'] == "none": + pass + elif uniq[network]['Encryption'] == "wep": + passphrase = '' + selected_key = '' + securitykey = '' + encryption = "WEP-40" + displayinputlabel("key") + drawkeyboard("wep") + getinput("wep", "key", ssid) + else: + passphrase = '' + selected_key = '' + securitykey = '' + displayinputlabel("key") + drawkeyboard("qwertyNormal") + getinput("qwertyNormal", "key", ssid) + + + pygame.display.update()