diff options
author | m0sia <m0sia@m0sia.ru> | 2016-02-18 15:04:42 -0600 |
---|---|---|
committer | m0sia <m0sia@m0sia.ru> | 2016-02-18 15:04:42 -0600 |
commit | 52c1c1f9d06631b94c3b8ce8eaf816c5a36842b3 (patch) | |
tree | 020f823cf692d1ce6898649a15c7633c831dd972 | |
parent | f57d9a8d4ebc30effbec71382cce7e0a37b697e3 (diff) | |
parent | 92e47683fc783a2dbd3d9737af40eb0b3372af61 (diff) | |
download | pyParrotZikTCP-52c1c1f9d06631b94c3b8ce8eaf816c5a36842b3.tar.xz pyParrotZikTCP-52c1c1f9d06631b94c3b8ce8eaf816c5a36842b3.zip |
Merge pull request #12 from serathius/master
Rework and Zik 2.0 implementation.
Diffstat (limited to '')
27 files changed, 1474 insertions, 577 deletions
diff --git a/BluetoothPairedDevices.py b/BluetoothPairedDevices.py deleted file mode 100644 index 6f9f673..0000000 --- a/BluetoothPairedDevices.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/python - -import sys -import re -import os - - -def ParrotZikMac(): - p = re.compile('90:03:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}') - if sys.platform == "linux2": - out = os.popen("bluez-test-device list").read() - res = p.findall(out) - if len(res)>0: - return res[0] - - elif sys.platform == "darwin": - fd = open("/Library/Preferences/com.apple.Bluetooth.plist", "rb") - plist = binplist.BinaryPlist(file_obj=fd) - parsed_plist = plist.Parse() - try : - for mac in parsed_plist['PairedDevices']: - if p.match(mac.replace("-",":")): - return mac.replace("-",":") - except: - pass - - elif sys.platform == "win32": - aReg = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE) - aKey = _winreg.OpenKey(aReg, 'SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Devices') - for i in range(10): - try: - asubkey_name=_winreg.EnumKey(aKey,i) - mac =':'.join(asubkey_name[i:i+2] for i in range(0,12,2)) - res = p.findall(mac) - if len(res)>0: - return res[0] - - except EnvironmentError: - pass - -if sys.platform == "darwin": - from binplist import binplist -elif sys.platform == "win32": - import _winreg
\ No newline at end of file diff --git a/ParrotProtocol.py b/ParrotProtocol.py deleted file mode 100644 index 9324b8b..0000000 --- a/ParrotProtocol.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python - -def generateRequest(requestString): - message=bytearray() - message.extend(generateHeader(requestString)) - message.extend(bytearray(requestString)) - return message - -def generateHeader(requestString): - header = bytearray([0]) - header.append(len(requestString)+3) - header.append("\x80") - return header - -def getRequest(apiString): - return generateRequest("GET "+apiString) - -def setRequest(apiString,args): - return generateRequest("SET "+apiString+"?arg="+args)
\ No newline at end of file diff --git a/ParrotZik.py b/ParrotZik.py deleted file mode 100644 index 8a2fca6..0000000 --- a/ParrotZik.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python - -import sys -if sys.platform == "darwin": - import lightblue -else: - import bluetooth - -import ParrotProtocol -import struct -from BeautifulSoup import BeautifulSoup - -class ParrotZik(object): - def __init__(self,addr=None): - uuid = "0ef0f502-f0ee-46c9-986c-54ed027807fb" - - - if sys.platform == "darwin": - service_matches = lightblue.findservices( name = "Parrot RFcomm service", addr = addr ) - else: - service_matches = bluetooth.find_service( uuid = uuid, address = addr ) - - - if len(service_matches) == 0: - print "Failed to find Parrot Zik RFCOMM service" - self.sock ="" - return - - if sys.platform == "darwin": - first_match = service_matches[0] - port = first_match[1] - name = first_match[2] - host = first_match[0] - else: - first_match = service_matches[0] - port = first_match["port"] - name = first_match["name"] - host = first_match["host"] - - print "Connecting to \"%s\" on %s" % (name, host) - - if sys.platform == "darwin": - self.sock=lightblue.socket() - else: - self.sock=bluetooth.BluetoothSocket( bluetooth.RFCOMM ) - - self.sock.connect((host, port)) - - self.sock.send('\x00\x03\x00') - data = self.sock.recv(1024) - - self.BatteryLevel = 100 - self.BatteryCharging = False - print "Connected" - - def getBatteryState(self): - data = self.sendGetMessage("/api/system/battery/get") - return data.answer.system.battery["state"] - - def getBatteryLevel(self): - data = self.sendGetMessage("/api/system/battery/get") - try: - if data.answer.system.battery["level"] <> '': - self.BatteryLevel = data.answer.system.battery["level"] - if data.answer.system.battery["state"] == 'charging': - self.BatteryCharging = True - else: - self.BatteryCharging = False - except: - pass - - try: - print "notification received" + data.notify["path"] - except: - pass - - return self.BatteryLevel - - def getVersion(self): - data = self.sendGetMessage("/api/software/version/get") - return data.answer.software["version"] - - def getFriendlyName(self): - data = self.sendGetMessage("/api/bluetooth/friendlyname/get") - return data.answer.bluetooth["friendlyname"] - - def getAutoConnection(self): - data = self.sendGetMessage("/api/system/auto_connection/enabled/get") - return data.answer.system.auto_connection["enabled"] - - def setAutoConnection(self,arg): - data = self.sendSetMessage("/api/system/auto_connection/enabled/set",arg) - return data - - def getAncPhoneMode(self): - data = self.sendGetMessage("/api/system/anc_phone_mode/enabled/get") - return data.answer.system.anc_phone_mode["enabled"] - - def getNoiseCancel(self): - data = self.sendGetMessage("/api/audio/noise_cancellation/enabled/get") - return data.answer.audio.noise_cancellation["enabled"] - - def setNoiseCancel(self,arg): - data = self.sendSetMessage("/api/audio/noise_cancellation/enabled/set",arg) - return data - - def getLouReedMode(self): - data = self.sendGetMessage("/api/audio/specific_mode/enabled/get") - return data.answer.audio.specific_mode["enabled"] - - def setLouReedMode(self,arg): - data = self.sendSetMessage("/api/audio/specific_mode/enabled/set",arg) - return data - - def getParrotConcertHall(self): - data = self.sendGetMessage("/api/audio/sound_effect/enabled/get") - return data.answer.audio.sound_effect["enabled"] - - def setParrotConcertHall(self,arg): - data = self.sendSetMessage("/api/audio/sound_effect/enabled/set",arg) - return data - - def sendGetMessage(self,message): - message = ParrotProtocol.getRequest(message) - return self.sendMessage(message) - - def sendSetMessage(self,message,arg): - message = ParrotProtocol.setRequest(message,arg) - return self.sendMessage(message) - - def sendMessage(self,message): - try: - self.sock.send(str(message)) - except: - self.sock ="" - return - if sys.platform == "darwin": - data = self.sock.recv(30) - else: - data = self.sock.recv(7) - data = self.sock.recv(1024) - data=BeautifulSoup(data) - return data - - def Close(self): - self.sock.close()
\ No newline at end of file diff --git a/ParrotZikTray b/ParrotZikTray deleted file mode 100755 index d9bdbc1..0000000 --- a/ParrotZikTray +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python - -import sys -import gtk -import re -import os -import ParrotZik -import BluetoothPairedDevices -from SysIndicator import * - -UPDATE_FREQUENCY = 30 # seconds - -class ParrotZikIndicator(SysIndicator): - def __init__(self): - - self.menu = UniversalMenu() - - self.info_item = MenuItem("Parrot Zik Not connected..",None,sensitive = False) - self.menu.append(self.info_item) - - self.battery_level = MenuItem("Battery Level:",None,sensitive = False) - self.menu.append(self.battery_level) - - self.battery_state = MenuItem("Battery State:",None,sensitive = False) - self.menu.append(self.battery_state) - - self.firmware_version = MenuItem("Firmware Version:",None,sensitive = False) - self.menu.append(self.firmware_version) - - self.check = MenuItem("Noise Cancellation",self.toggleANC,sensitive = False, checkitem = True) - self.menu.append(self.check) - - self.check2 = MenuItem("Auto Connection",self.toggleAuto,sensitive = False, checkitem = True) - self.menu.append(self.check2) - - self.check3 = MenuItem("Lou Reed Mode",self.toggleLouReedMode,sensitive = False, checkitem = True) - self.menu.append(self.check3) - - self.check4 = MenuItem("Concert Hall Mode",self.toggleParrotConcertHall,sensitive = False, checkitem = True) - self.menu.append(self.check4) - - self.quit = MenuItem("Quit",sys.exit,sensitive = True, checkitem = True) - self.menu.append(self.quit) - - SysIndicator.__init__(self,icon = "zik-audio-headset",menu = self.menu) - - self.connected=False - self.p = re.compile('90:03:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}') - - def EstablishConnection(self): - if self.connected: - if not self.parrot.sock: - print "Lost connection" - self.connected = False - else: - print "Connection already established" - else: - mac=BluetoothPairedDevices.ParrotZikMac() - if mac: - self.parrot = ParrotZik.ParrotZik(mac) - if not self.parrot.sock: - print "Failed to connect to Parrot Zik %s" % mac - return False - - self.connected = True - self.name = self.parrot.getFriendlyName() - self.version = self.parrot.getVersion() - - self.check.set_sensitive(True) - self.check2.set_sensitive(True) - self.check3.set_sensitive(True) - self.check4.set_sensitive(True) - - if self.parrot.getNoiseCancel() == "true": - self.check.set_active(True) - else: - self.check.set_active(False) - - if self.parrot.getAutoConnection() == "true": - self.check2.set_active(True) - else: - self.check2.set_active(False) - - if self.parrot.getLouReedMode() == "true": - self.check3.set_active(True) - else: - self.check3.set_active(False) - - if self.parrot.getParrotConcertHall() == "true": - self.check4.set_active(True) - else: - self.check4.set_active(False) - - self.CheckBattery() - return True - - def toggleANC(self,widget): - if self.connected: - if self.check.get_active(): - self.parrot.setNoiseCancel("true") - else: - self.parrot.setNoiseCancel("false") - - def toggleAuto(self,widget): - if self.connected: - if self.check2.get_active(): - self.parrot.setAutoConnection("true") - else: - self.parrot.setAutoConnection("false") - - def toggleLouReedMode(self,widget): - if self.connected: - if self.check3.get_active(): - self.parrot.setLouReedMode("true") - self.check4.set_sensitive(False) - else: - self.parrot.setLouReedMode("false") - self.check4.set_sensitive(True) - - def toggleParrotConcertHall(self,widget): - if self.connected: - if self.check4.get_active(): - self.parrot.setParrotConcertHall("true") - else: - self.parrot.setParrotConcertHall("false") - - def CheckBattery(self): - if self.connected: - print "Updating battery" - self.batteryLevel = int(self.parrot.getBatteryLevel()) - - if self.parrot.BatteryCharging: - self.batteryLevel = "Charging" - self.setIcon("zik-battery-charging") - self.batteryLevel="Unknown" - self.batteryState="Charging" - elif self.batteryLevel>80: - self.setIcon("zik-battery-100") - self.batteryState="In Use" - elif self.batteryLevel>60: - self.setIcon("zik-battery-080") - self.batteryState="In Use" - elif self.batteryLevel>40: - self.setIcon("zik-battery-060") - self.batteryState="In Use" - elif self.batteryLevel>20: - self.setIcon("zik-battery-040") - self.batteryState="In Use" - else: - self.setIcon("zik-battery-low") - self.batteryState="In Use" - - self.info_item.set_label("Connected to: "+self.name) - self.firmware_version.set_label("Firmware version: "+self.version) - self.battery_state.set_label("State: "+self.batteryState) - self.battery_level.set_label("Battery Level: "+str(self.batteryLevel)) - else: - self.setIcon("zik-audio-headset") - self.info_item.set_label("Parrot Zik Not connected..") - self.check.set_sensitive(False) - self.check2.set_sensitive(False) - self.check3.set_sensitive(False) - self.check4.set_sensitive(False) - return True - - def main(self): - self.EstablishConnection() - gtk.timeout_add(UPDATE_FREQUENCY * 1000, self.EstablishConnection) - gtk.timeout_add(UPDATE_FREQUENCY * 1000, self.CheckBattery) - SysIndicator.main(self) - -if __name__ == "__main__": - indicator = ParrotZikIndicator() - indicator.main()
\ No newline at end of file diff --git a/SysIndicator.py b/SysIndicator.py deleted file mode 100644 index 3c79286..0000000 --- a/SysIndicator.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python - -import sys -import re -import os -import tempfile - -if sys.platform=="linux2" or sys.platform=="win32": - import gtk -elif sys.platform=="darwin": - import objc - from Foundation import * - from AppKit import * - from PyObjCTools import AppHelper - from StatusAppMac import StatusApp - - -class SysIndicator: - def __init__(self, icon,menu): - if sys.platform=="linux2": - self.menu = menu.gtk_menu - import appindicator - self.icon_directory = os.path.sep + 'usr' + os.path.sep+ 'share' + os.path.sep+'icons' + os.path.sep+'zik'+ os.path.sep - if not os.path.isdir(self.icon_directory): - self.icon_directory = os.path.dirname(sys.argv[0]) + os.path.sep + 'share' + os.path.sep+'icons' + os.path.sep+'zik'+ os.path.sep - self.statusicon = appindicator.Indicator("new-parrotzik-indicator", - "indicator-messages", - appindicator.CATEGORY_APPLICATION_STATUS) - self.statusicon.set_status(appindicator.STATUS_ACTIVE) - self.statusicon.set_icon_theme_path(self.icon_directory) - self.statusicon.set_menu(self.menu) - - elif sys.platform=="win32": - self.menu = menu.gtk_menu - self.icon_directory = os.path.dirname(os.path.realpath(sys.argv[0])) + os.path.sep+ 'share' + os.path.sep+'icons' + os.path.sep+'zik'+ os.path.sep - self.statusicon = gtk.StatusIcon() - self.statusicon.connect("popup-menu", self.gtk_right_click_event) - self.statusicon.set_tooltip("Parrot Zik") - self.menu_shown=False - sys.stdout = open(tempfile.gettempdir()+os.path.sep+"zik_tray_stdout.log", "w") - sys.stderr = open(tempfile.gettempdir()+os.path.sep+"zik_tray_stderr.log", "w") - - elif sys.platform=="darwin": - self.icon_directory = os.path.dirname(os.path.realpath(sys.argv[0])) + os.path.sep+ 'share' + os.path.sep+'icons' + os.path.sep+'zik'+ os.path.sep - self.statusicon = StatusApp.sharedApplication() - self.statusicon.initMenu(menu) - - self.setIcon(icon) - - def setIcon(self, name): - if sys.platform=="linux2": - self.statusicon.set_icon(name) - elif sys.platform=="win32": - self.statusicon.set_from_file(self.icon_directory+name+'.png') - elif sys.platform=="darwin": - self.statusicon.setIcon(name,self.icon_directory) - - def gtk_right_click_event(self, icon, button, time): - if not self.menu_shown: - self.menu_shown=True - self.menu.popup(None, None, gtk.status_icon_position_menu, button, time, self.statusicon) - else: - self.menu_shown=False - self.menu.popdown() - - def main(self): - if sys.platform=="linux2" or sys.platform=="win32": - gtk.main() - elif sys.platform=="darwin": - #self.statusicon.run() - AppHelper.runEventLoop() - - def show_about_dialog(self, widget): - if sys.platform=="linux2" or sys.platform=="win32": - about_dialog = gtk.AboutDialog() - about_dialog.set_destroy_with_parent(True) - about_dialog.set_name("Parrot Zik Tray") - about_dialog.set_version("0.3") - about_dialog.set_authors(["Dmitry Moiseev m0sia@m0sia.ru"]) - about_dialog.run() - about_dialog.destroy() - - -class UniversalMenu: - def __init__(self): - if sys.platform=="linux2" or sys.platform=="win32": - self.gtk_menu = gtk.Menu() - elif sys.platform=="darwin": - self.actions = {} - self.menubarMenu = NSMenu.alloc().init() - self.menubarMenu.setAutoenablesItems_(False) - - - def append(self,MenuItem): - if sys.platform=="linux2" or sys.platform=="win32": - self.gtk_menu.append(MenuItem.gtk_item) - elif sys.platform=="darwin": - self.actions[MenuItem.title] = MenuItem.action - self.menubarMenu.addItem_(MenuItem.nsmenu_item) - -class MenuItem: - def __init__(self,name,action,sensitive = True, checkitem = False): - if sys.platform=="linux2" or sys.platform=="win32": - if checkitem: - self.gtk_item=gtk.CheckMenuItem(name) - else: - self.gtk_item=gtk.MenuItem(name) - self.gtk_item.show() - if action: - self.gtk_item.connect("activate", action) - - elif sys.platform=="darwin": - self.title = name - self.action = action - self.nsmenu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(name, 'clicked:', '') - - self.set_sensitive(sensitive) - - def set_sensitive(self,option): - if sys.platform=="linux2" or sys.platform=="win32": - return self.gtk_item.set_sensitive(option) - elif sys.platform=="darwin": - self.nsmenu_item.setEnabled_(option) - - def set_active(self,option): - if sys.platform=="linux2" or sys.platform=="win32": - return self.gtk_item.set_active(option) - elif sys.platform=="darwin": - self.nsmenu_item.setState_(option) - - def get_active(self): - if sys.platform=="linux2" or sys.platform=="win32": - return self.gtk_item.get_active() - elif sys.platform=="darwin": - print self.nsmenu_item.state - return self.nsmenu_item.state - - - def set_label(self,option): - if sys.platform=="linux2" or sys.platform=="win32": - return self.gtk_item.set_label(option) - elif sys.platform=="darwin": - self.title = option - self.nsmenu_item.setTitle_(option) - #self.rumps_item.title=option - -if __name__ == "__main__": - - quit_item = MenuItem("Quit",sys.exit,True) - - menu = UniversalMenu() - menu.append(quit_item) - - indicator = SysIndicator(icon = "zik-audio-headset",menu = menu) - indicator.main()
\ No newline at end of file diff --git a/parrot_zik/__init__.py b/parrot_zik/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parrot_zik/__init__.py diff --git a/parrot_zik/bluetooth_paired_devices.py b/parrot_zik/bluetooth_paired_devices.py new file mode 100644 index 0000000..905819f --- /dev/null +++ b/parrot_zik/bluetooth_paired_devices.py @@ -0,0 +1,171 @@ +import dbus +import sys +import re +from subprocess import Popen, PIPE, STDOUT + +from .resource_manager import GenericResourceManager + +if sys.platform == "darwin": + from binplist import binplist + import lightblue +else: + import bluetooth + if sys.platform == "win32": + import _winreg + + +p = re.compile('90:03:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}|' + 'A0:14:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}') + + +class BluetoothDeviceManager(object): + def is_bluetooth_on(self): + raise NotImplementedError + + def get_mac(self): + raise NotImplementedError + + +class BluezBluetoothDeviceManager(BluetoothDeviceManager): + def is_bluetooth_on(self): + pipe = Popen(['bluez-test-adapter', 'powered'], stdout=PIPE, stdin=PIPE, + stderr=STDOUT) + try: + stdout, stderr = pipe.communicate() + except dbus.exceptions.DBusException: + pass + else: + return bool(stdout.strip()) + + def get_mac(self): + pipe = Popen(['bluez-test-device', 'list'], stdout=PIPE, stdin=PIPE, + stderr=STDOUT) + try: + stdout, stderr = pipe.communicate() + except dbus.exceptions.DBusException: + pass + else: + res = p.findall(stdout) + if len(res) > 0: + return res[0] + else: + raise DeviceNotConnected + + +class BluetoothCmdDeviceManager(BluetoothDeviceManager): + def is_bluetooth_on(self): + return True + + def get_mac(self): + pipe = Popen(['bluetoothctl'], stdout=PIPE, stdin=PIPE, stderr=STDOUT) + res = pipe.communicate("exit") + if len(res) > 0 and res[0]: + match = p.search(res[0]) + if match: + return match.group(0) + raise DeviceNotConnected + + +def get_parrot_zik_mac_linux(): + bluez_manager = BluezBluetoothDeviceManager() + try: + bluez_manager.is_bluetooth_on() + return bluez_manager.get_mac() + except OSError as e: + if e.errno == 2: + bluetoothcmd_manager = BluetoothCmdDeviceManager() + return bluetoothcmd_manager.get_mac() + + +def get_parrot_zik_mac_darwin(): + fd = open("/Library/Preferences/com.apple.Bluetooth.plist", "rb") + plist = binplist.BinaryPlist(file_obj=fd) + parsed_plist = plist.Parse() + try: + for mac in parsed_plist['PairedDevices']: + if p.match(mac.replace("-", ":")): + return mac.replace("-", ":") + else: + raise DeviceNotConnected + except Exception: + pass + + +def get_parrot_zik_mac_windows(): + aReg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) + aKey = _winreg.OpenKey( + aReg, 'SYSTEM\CurrentControlSet\Services\ + BTHPORT\Parameters\Devices') + for i in range(10): + try: + asubkey_name = _winreg.EnumKey(aKey, i) + mac = ':'.join(asubkey_name[i:i+2] for i in range(0, 12, 2)) + res = p.findall(mac) + if len(res) > 0: + return res[0] + else: + raise DeviceNotConnected + except EnvironmentError: + pass + + +if sys.platform in ['linux', 'linux2']: + get_parrot_zik_mac = get_parrot_zik_mac_linux +elif sys.platform == 'darwin': + get_parrot_zik_mac = get_parrot_zik_mac_darwin +elif sys.platform == 'win32': + get_parrot_zik_mac = get_parrot_zik_mac_windows +else: + raise AssertionError('Platform not supported') + + +def connect(): + mac = get_parrot_zik_mac() + if sys.platform == "darwin": + service_matches = lightblue.findservices( + name="Parrot RFcomm service", addr=mac) + else: + uuids = ["0ef0f502-f0ee-46c9-986c-54ed027807fb", + "8B6814D3-6CE7-4498-9700-9312C1711F63"] + service_matches = [] + for uuid in uuids: + try: + service_matches = bluetooth.find_service(uuid=uuid, address=mac) + except bluetooth.btcommon.BluetoothError: + pass + if service_matches: + break + + if len(service_matches) == 0: + raise ConnectionFailure + first_match = service_matches[0] + + if sys.platform == "darwin": + host = first_match[0] + port = first_match[1] + sock = lightblue.socket() + else: + port = first_match["port"] + host = first_match["host"] + sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM) + + try: + sock.connect((host, port)) + except bluetooth.btcommon.BluetoothError: + raise ConnectionFailure + + sock.send('\x00\x03\x00') + sock.recv(1024) + return GenericResourceManager(sock) + + +class DeviceNotConnected(Exception): + pass + + +class ConnectionFailure(Exception): + pass + + +class BluetoothIsNotOn(Exception): + pass diff --git a/parrot_zik/indicator/__init__.py b/parrot_zik/indicator/__init__.py new file mode 100644 index 0000000..156f7de --- /dev/null +++ b/parrot_zik/indicator/__init__.py @@ -0,0 +1,25 @@ +__all__ = ('SysIndicator', 'Menu', 'MenuItem') + +import sys + +if sys.platform in ['linux', 'linux2']: + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--gtk", action="store_true") + args = parser.parse_args() + if args.gtk: + from parrot_zik.indicator.linux import LinuxGtkIndicator as SysIndicator + else: + from parrot_zik.indicator.linux import LinuxAppIndicator as SysIndicator + from parrot_zik.indicator.gtk_wrapping import GTKMenuItem as MenuItem + from parrot_zik.indicator.gtk_wrapping import GTKMenu as Menu +elif sys.platform in ['win32']: + from parrot_zik.indicator.windows import WindowsIndicator as SysIndicator + from parrot_zik.indicator.gtk_wrapping import GTKMenuItem as MenuItem + from parrot_zik.indicator.gtk_wrapping import GTKMenu as Menu +elif sys.platform == 'darwin': + from parrot_zik.indicator.mac import DarwinIndicator as SysIndicator + from parrot_zik.indicator.mac import NSMenuItem as MenuItem + from parrot_zik.indicator.mac import NSMenu as Menu +else: + raise Exception('Platform not supported') diff --git a/parrot_zik/indicator/base.py b/parrot_zik/indicator/base.py new file mode 100644 index 0000000..b29368e --- /dev/null +++ b/parrot_zik/indicator/base.py @@ -0,0 +1,48 @@ +class BaseIndicator(object): + def __init__(self, icon, menu, statusicon): + self.menu = menu + self.statusicon = statusicon + self.setIcon(icon) + + def setIcon(self, name): + raise NotImplementedError + + @classmethod + def main(cls): + raise NotImplementedError + + def show_about_dialog(self, widget): + raise NotImplementedError + + def quit(self, _): + raise NotImplementedError + +class MenuItemBase(object): + def __init__(self, base_item, sensitive, visible): + self.base_item = base_item + self.set_sensitive(sensitive) + if visible: + self.show() + else: + self.hide() + + def set_sensitive(self, option): + raise NotImplementedError + + def set_active(self, option): + raise NotImplementedError + + def get_active(self): + raise NotImplementedError + + def set_label(self, option): + raise NotImplementedError + + def show(self): + self.base_item.show() + + def hide(self): + self.base_item.hide() + + def set_submenu(self, menu): + raise NotImplementedError diff --git a/parrot_zik/indicator/gtk_wrapping.py b/parrot_zik/indicator/gtk_wrapping.py new file mode 100644 index 0000000..6d4a5a8 --- /dev/null +++ b/parrot_zik/indicator/gtk_wrapping.py @@ -0,0 +1,46 @@ +import gtk + +from parrot_zik.indicator.base import MenuItemBase + + +class GTKMenu(object): + def __init__(self): + self.gtk_menu = gtk.Menu() + + def append(self, menu_item): + self.gtk_menu.append(menu_item.base_item) + + def reposition(self): + self.gtk_menu.reposition() + + def popup(self, *args, **kwargs): + self.gtk_menu.popup(*args, **kwargs) + + def poVpdown(self, *args, **kwargs): + pass + + +class GTKMenuItem(MenuItemBase): + def __init__(self, name, action, sensitive=True, checkitem=False, visible=True): + if checkitem: + gtk_item = gtk.CheckMenuItem(name) + else: + gtk_item = gtk.MenuItem(name) + if action: + gtk_item.connect("activate", action) + super(GTKMenuItem, self).__init__(gtk_item, sensitive, visible) + + def set_sensitive(self, option): + return self.base_item.set_sensitive(option) + + def set_active(self, option): + return self.base_item.set_active(option) + + def get_active(self): + return self.base_item.get_active() + + def set_label(self, option): + return self.base_item.set_label(option) + + def set_submenu(self, menu): + self.base_item.set_submenu(menu.gtk_menu) diff --git a/parrot_zik/indicator/linux.py b/parrot_zik/indicator/linux.py new file mode 100644 index 0000000..f6f9cfc --- /dev/null +++ b/parrot_zik/indicator/linux.py @@ -0,0 +1,69 @@ +import os + +import gtk + +from parrot_zik.indicator.base import BaseIndicator + + +class LinuxIndicator(BaseIndicator): + def __init__(self, icon, menu, statusicon): + super(LinuxIndicator, self).__init__(icon, menu, statusicon) + + def gtk_right_click_event(self, icon, button, time): + if not self.menu_shown: + self.menu_shown = True + self.menu.popup(None, None, gtk.status_icon_position_menu, + button, time, self.statusicon) + else: + self.menu_shown = False + self.menu.poVpdown() + + @classmethod + def main(cls): + gtk.main() + + def quit(self, _): + gtk.main_quit() + + def show_about_dialog(self, widget): + about_dialog = gtk.AboutDialog() + about_dialog.set_destroy_with_parent(True) + about_dialog.set_name("Parrot Zik Tray") + about_dialog.set_version("0.3") + about_dialog.set_authors(["Dmitry Moiseev m0sia@m0sia.ru"]) + about_dialog.run() + about_dialog.destroy() + + +class LinuxAppIndicator(LinuxIndicator): + def __init__(self, icon, menu): + import appindicator + self.icon_directory = os.path.join('/', 'usr', 'share', 'icons', 'zik') + if not os.path.isdir(self.icon_directory): + self.icon_directory = os.path.join('share', 'icons', 'zik') + statusicon = appindicator.Indicator( + "new-parrotzik-indicator", "indicator-messages", + appindicator.CATEGORY_APPLICATION_STATUS) + statusicon.set_status(appindicator.STATUS_ACTIVE) + statusicon.set_icon_theme_path(self.icon_directory) + statusicon.set_menu(menu.gtk_menu) + super(LinuxIndicator, self).__init__(icon, menu, statusicon) + + def setIcon(self, name): + self.statusicon.set_icon(name) + + + +class LinuxGtkIndicator(LinuxIndicator): + def __init__(self, icon, menu): + self.icon_directory = os.path.join( + '/usr', 'share', 'icons/') + self.menu_shown = False + statusicon = gtk.StatusIcon() + statusicon.connect("popup-menu", self.gtk_right_click_event) + statusicon.set_tooltip("Parrot Zik") + super(LinuxIndicator, self).__init__(icon, menu, statusicon) + + def setIcon(self, name): + self.statusicon.set_from_file(self.icon_directory + name + '.png') + diff --git a/parrot_zik/indicator/mac.py b/parrot_zik/indicator/mac.py new file mode 100644 index 0000000..ceeacb0 --- /dev/null +++ b/parrot_zik/indicator/mac.py @@ -0,0 +1,70 @@ +import os +import sys + +from Foundation import * +from AppKit import * +from PyObjCTools import AppHelper + +from parrot_zik.indicator.base import BaseIndicator +from parrot_zik.indicator.base import MenuItemBase +from parrot_zik.status_app_mac import StatusApp + + +class DarwinIndicator(BaseIndicator): + def __init__(self, icon, menu): + self.icon_directory = os.path.join( + os.path.dirname(os.path.realpath(sys.argv[0])), 'share', 'icons', 'zik') + statusicon = StatusApp.sharedApplication() + statusicon.initMenu(menu) + super(DarwinIndicator, self).__init__(icon, menu, statusicon) + + def setIcon(self, name): + self.statusicon.setIcon(name, self.icon_directory) + + @classmethod + def main(cls): + AppHelper.runEventLoop() + + def show_about_dialog(self, widget): + pass + + def quit(self, _): + pass + + +class NSMenu(object): + def __init__(self): + self.actions = {} + self.menubarMenu = NSMenu.alloc().init() + self.menubarMenu.setAutoenablesItems_(False) + + def append(self, menu_item): + self.actions[menu_item.title] = menu_item.action + self.menubarMenu.addItem_(menu_item.nsmenu_item) + + def reposition(self): + # TODO + pass + + +class NSMenuItem(MenuItemBase): + def __init__(self, name, action, sensitive=True, checkitem=False, visible=True): + self.title = name + self.action = action + nsmenu_item = ( + NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( + name, 'clicked:', '')) + super(NSMenuItem, self).__init__(nsmenu_item, sensitive, visible) + + def set_sensitive(self, option): + self.base_item.setEnabled_(option) + + def set_active(self, option): + self.base_item.setState_(option) + + def get_active(self): + return self.base_item.state + + def set_label(self, option): + self.title = option + self.base_item.setTitle_(option) diff --git a/parrot_zik/indicator/windows.py b/parrot_zik/indicator/windows.py new file mode 100644 index 0000000..f10c91a --- /dev/null +++ b/parrot_zik/indicator/windows.py @@ -0,0 +1,47 @@ +import sys +import tempfile +import gtk +import os + +from parrot_zik.indicator.base import BaseIndicator + + +class WindowsIndicator(BaseIndicator): + def __init__(self, icon, menu): + self.icon_directory = os.path.join( + os.path.dirname(os.path.realpath(sys.argv[0])), 'share', 'icons', 'zik') + self.menu_shown = False + sys.stdout = open(os.path.join(tempfile.gettempdir(), "zik_tray_stdout.log", "w")) + sys.stderr = open(os.path.join(tempfile.gettempdir(), "zik_tray_stderr.log", "w")) + statusicon = gtk.StatusIcon() + statusicon.connect("popup-menu", self.gtk_right_click_event) + statusicon.set_tooltip("Parrot Zik") + super(WindowsIndicator, self).__init__(icon, menu, statusicon) + + def gtk_right_click_event(self, icon, button, time): + if not self.menu_shown: + self.menu_shown = True + self.menu.popup(None, None, gtk.status_icon_position_menu, + button, time, self.statusicon) + else: + self.menu_shown = False + self.menu.poVpdown() + + def setIcon(self, name): + self.statusicon.set_from_file(self.icon_directory + name + '.png') + + @classmethod + def main(cls): + gtk.main() + + def quit(self, _): + gtk.main_quit() + + def show_about_dialog(self, widget): + about_dialog = gtk.AboutDialog() + about_dialog.set_destroy_with_parent(True) + about_dialog.set_name("Parrot Zik Tray") + about_dialog.set_version("0.3") + about_dialog.set_authors(["Dmitry Moiseev m0sia@m0sia.ru"]) + about_dialog.run() + about_dialog.destroy() diff --git a/parrot_zik/interface/__init__.py b/parrot_zik/interface/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parrot_zik/interface/__init__.py diff --git a/parrot_zik/interface/base.py b/parrot_zik/interface/base.py new file mode 100644 index 0000000..90caa02 --- /dev/null +++ b/parrot_zik/interface/base.py @@ -0,0 +1,95 @@ +from parrot_zik import resource_manager +from parrot_zik.indicator import Menu +from parrot_zik.indicator import MenuItem +from parrot_zik.model.base import BatteryStates + +RECONNECT_FREQUENCY = 5000 + + +class ParrotZikBaseInterface(object): + def __init__(self, indicator): + self.indicator = indicator + self.parrot = None + self.battery_level = MenuItem("Battery Level:", None, sensitive=False, + visible=False) + self.battery_state = MenuItem("Battery State:", None, sensitive=False, + visible=False) + self.firmware_version = MenuItem("Firmware Version:", None, + sensitive=False, visible=False) + self.settings = MenuItem("Settings", None, visible=False) + self.settings_submenu = Menu() + self.settings.set_submenu(self.settings_submenu) + + self.auto_connection = MenuItem("Auto Connection", self.toggle_auto_connection, + checkitem=True) + self.settings_submenu.append(self.auto_connection) + + self.indicator.menu.append(self.battery_level) + self.indicator.menu.append(self.battery_state) + self.indicator.menu.append(self.firmware_version) + self.indicator.menu.append(self.settings) + + def activate(self, manager): + self.parrot = self.parrot_class(manager) + self.read_battery() + self.indicator.info("Connected to: " + self.parrot.friendly_name) + self.firmware_version.set_label( + "Firmware version: " + self.parrot.version) + self.auto_connection.set_active(self.parrot.auto_connect) + self.battery_level.show() + self.battery_state.show() + self.firmware_version.show() + self.settings.show() + self.indicator.active_interface = self + + @property + def parrot_class(self): + raise NotImplementedError + + def deactivate(self): + self.parrot = None + self.battery_level.hide() + self.battery_state.hide() + self.firmware_version.hide() + self.settings.hide() + self.indicator.menu.reposition() + self.indicator.active_interface = None + self.indicator.setIcon("zik-audio-headset") + self.indicator.info('Lost Connection') + self.indicator.reconnect.start(self.indicator, RECONNECT_FREQUENCY) + + def toggle_auto_connection(self, widget): + try: + self.parrot.auto_connect = self.auto_connection.get_active() + self.auto_connection.set_active(self.parrot.auto_connect) + except resource_manager.DeviceDisconnected: + self.deactivate() + + def refresh(self): + self.read_battery() + + def read_battery(self): + try: + self.parrot.refresh_battery() + battery_level = self.parrot.battery_level + battery_state = self.parrot.battery_state + except resource_manager.DeviceDisconnected: + self.deactivate() + else: + if battery_state == BatteryStates.CHARGING: + self.indicator.setIcon("zik-battery-charging") + elif battery_level > 80: + self.indicator.setIcon("zik-battery-100") + elif battery_level > 60: + self.indicator.setIcon("zik-battery-080") + elif battery_level > 40: + self.indicator.setIcon("zik-battery-060") + elif battery_level > 20: + self.indicator.setIcon("zik-battery-040") + else: + self.indicator.setIcon("zik-battery-low") + + self.battery_state.set_label( + "State: " + BatteryStates.representation[battery_state]) + self.battery_level.set_label( + "Battery Level: " + str(battery_level)) diff --git a/parrot_zik/interface/version1.py b/parrot_zik/interface/version1.py new file mode 100644 index 0000000..d0a1b2b --- /dev/null +++ b/parrot_zik/interface/version1.py @@ -0,0 +1,63 @@ +from parrot_zik import resource_manager +from parrot_zik.indicator import MenuItem +from parrot_zik.interface.base import ParrotZikBaseInterface +from parrot_zik.model.version1 import ParrotZikVersion1 + + +class ParrotZikVersion1Interface(ParrotZikBaseInterface): + parrot_class = ParrotZikVersion1 + + def __init__(self, indicator): + super(ParrotZikVersion1Interface, self).__init__(indicator) + self.noise_cancelation = MenuItem( + "Noise Cancellation", self.toggle_noise_cancelation, + checkitem=True, visible=False) + self.lou_reed_mode = MenuItem("Lou Reed Mode", self.toggle_lou_reed_mode, + checkitem=True, visible=False) + self.concert_hall_mode = MenuItem( + "Concert Hall Mode", self.toggle_parrot_concert_hall, + checkitem=True, visible=False) + self.indicator.menu.append(self.noise_cancelation) + self.indicator.menu.append(self.lou_reed_mode) + self.indicator.menu.append(self.concert_hall_mode) + + def activate(self, manager): + super(ParrotZikVersion1Interface, self).activate(manager) + self.noise_cancelation.set_active(self.parrot.cancel_noise) + self.lou_reed_mode.set_active(self.parrot.lou_reed_mode) + self.concert_hall_mode.set_active(self.parrot.concert_hall) + + self.noise_cancelation.show() + self.lou_reed_mode.show() + self.concert_hall_mode.show() + self.indicator.menu.reposition() + + def deactivate(self): + self.noise_cancelation.hide() + self.lou_reed_mode.hide() + self.concert_hall_mode.hide() + super(ParrotZikVersion1Interface, self).deactivate() + + def toggle_noise_cancelation(self, widget): + try: + self.parrot.cancel_noise = self.noise_cancelation.get_active() + self.noise_cancelation.set_active(self.parrot.cancel_noise) + except resource_manager.DeviceDisconnected: + self.deactivate() + + def toggle_lou_reed_mode(self, widget): + try: + self.parrot.lou_reed_mode = self.lou_reed_mode.get_active() + self.lou_reed_mode.set_active(self.parrot.lou_reed_mode) + self.concert_hall_mode.set_active(self.parrot.concert_hall) + self.concert_hall_mode.set_sensitive( + not self.lou_reed_mode.get_active()) + except resource_manager.DeviceDisconnected: + self.deactivate() + + def toggle_parrot_concert_hall(self, widget): + try: + self.parrot.concert_hall = self.concert_hall_mode.get_active() + self.concert_hall_mode.set_active(self.parrot.concert_hall) + except resource_manager.DeviceDisconnected: + self.deactivate() diff --git a/parrot_zik/interface/version2.py b/parrot_zik/interface/version2.py new file mode 100644 index 0000000..e6076ed --- /dev/null +++ b/parrot_zik/interface/version2.py @@ -0,0 +1,239 @@ +import functools + +from parrot_zik import resource_manager +from parrot_zik.indicator import MenuItem, Menu +from parrot_zik.interface.base import ParrotZikBaseInterface +from parrot_zik.model.version2 import ParrotZikVersion2 +from parrot_zik.model.version2 import NoiseControlTypes +from parrot_zik.model.version2 import Rooms + + +class ParrotZikVersion2Interface(ParrotZikBaseInterface): + parrot_class = ParrotZikVersion2 + + def __init__(self, indicator): + self.room_dirty = False + self.angle_dirty = False + self.noise_cancelation_dirty = False + super(ParrotZikVersion2Interface, self).__init__(indicator) + self.noise_cancelation = MenuItem("Noise Control", None, visible=False) + self.noise_cancelation_submenu = Menu() + self.noise_cancelation.set_submenu(self.noise_cancelation_submenu) + + self.noise_control_cancelation_max = MenuItem( + "Max Calcelation", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.NOISE_CONTROL_MAX), checkitem=True) + self.noise_control_cancelation_on = MenuItem( + "Normal Cancelation", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.NOISE_CONTROL_ON), checkitem=True) + self.noise_control_off = MenuItem( + "Off", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.NOISE_CONTROL_OFF), checkitem=True) + self.noise_control_street_mode = MenuItem( + "Street Mode", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.STREET_MODE), checkitem=True) + self.noise_control_street_mode_max = MenuItem( + "Street Mode Max", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.STREET_MODE_MAX), checkitem=True) + self.noise_cancelation_submenu.append(self.noise_control_cancelation_max) + self.noise_cancelation_submenu.append(self.noise_control_cancelation_on) + self.noise_cancelation_submenu.append(self.noise_control_off) + self.noise_cancelation_submenu.append(self.noise_control_street_mode) + self.noise_cancelation_submenu.append(self.noise_control_street_mode_max) + + self.room_sound_effect = MenuItem( + "Room Sound Effect", None, visible=False) + self.room_sound_effect_submenu = Menu() + self.room_sound_effect.set_submenu(self.room_sound_effect_submenu) + + self.room_sound_effect_enabled = MenuItem( + "Enabled", self.toggle_room_sound_effect, checkitem=True) + self.rooms = MenuItem("Rooms", None, checkitem=False) + self.angle = MenuItem("Angle", None, checkitem=False) + self.room_sound_effect_submenu.append(self.room_sound_effect_enabled) + self.room_sound_effect_submenu.append(self.rooms) + self.room_sound_effect_submenu.append(self.angle) + + self.rooms_submenu = Menu() + self.rooms.set_submenu(self.rooms_submenu) + + self.concert_hall_mode = MenuItem( + "Concert Hall", functools.partial(self.toggle_room, Rooms.CONCERT_HALL), checkitem=True) + self.jazz_mode = MenuItem( + "Jazz Club", functools.partial(self.toggle_room, Rooms.JAZZ_CLUB), checkitem=True) + self.living_mode = MenuItem( + "Living Room", functools.partial(self.toggle_room, Rooms.LIVING_ROOM), checkitem=True) + self.silent_mode = MenuItem( + "Silent Room", functools.partial(self.toggle_room, Rooms.SILENT_ROOM), checkitem=True) + self.rooms_submenu.append(self.concert_hall_mode) + self.rooms_submenu.append(self.jazz_mode) + self.rooms_submenu.append(self.living_mode) + self.rooms_submenu.append(self.silent_mode) + + self.angle_submenu = Menu() + self.angle.set_submenu(self.angle_submenu) + self.angle_30 = MenuItem( + "30", functools.partial(self.toggle_angle, 30), checkitem=True) + self.angle_60 = MenuItem( + "60", functools.partial(self.toggle_angle, 60), checkitem=True) + self.angle_90 = MenuItem( + "90", functools.partial(self.toggle_angle, 90), checkitem=True) + self.angle_120 = MenuItem( + "120", functools.partial(self.toggle_angle, 120), checkitem=True) + self.angle_150 = MenuItem( + "150", functools.partial(self.toggle_angle, 150), checkitem=True) + self.angle_180 = MenuItem( + "180", functools.partial(self.toggle_angle, 180), checkitem=True) + self.angle_submenu.append(self.angle_30) + self.angle_submenu.append(self.angle_60) + self.angle_submenu.append(self.angle_90) + self.angle_submenu.append(self.angle_120) + self.angle_submenu.append(self.angle_150) + self.angle_submenu.append(self.angle_180) + + self.flight_mode = MenuItem("Flight Mode", self.toggle_flight_mode, + checkitem=True, visible=False) + + self.head_detection = MenuItem("Head Detection", self.toggle_head_detection, checkitem=True) + self.settings_submenu.append(self.head_detection) + + self.indicator.menu.append(self.room_sound_effect) + self.indicator.menu.append(self.noise_cancelation) + self.indicator.menu.append(self.flight_mode) + + def activate(self, manager): + super(ParrotZikVersion2Interface, self).activate(manager) + self._read_noise_cancelation() + self.flight_mode.set_active(self.parrot.flight_mode) + self._read_sound_effect_room() + self._read_sound_effect_angle() + self.head_detection.set_active(self.parrot.head_detection) + + sound_effect = self.parrot.sound_effect + + self.room_sound_effect_enabled.set_active(sound_effect) + self.concert_hall_mode.set_sensitive(sound_effect) + self.jazz_mode.set_sensitive(sound_effect) + self.living_mode.set_sensitive(sound_effect) + self.silent_mode.set_sensitive(sound_effect) + + self.angle_30.set_sensitive(sound_effect) + self.angle_60.set_sensitive(sound_effect) + self.angle_90.set_sensitive(sound_effect) + self.angle_120.set_sensitive(sound_effect) + self.angle_150.set_sensitive(sound_effect) + self.angle_180.set_sensitive(sound_effect) + + self.noise_cancelation.show() + self.flight_mode.show() + self.room_sound_effect.show() + self.indicator.menu.reposition() + + def deactivate(self): + self.noise_cancelation.hide() + self.flight_mode.hide() + self.room_sound_effect.hide() + super(ParrotZikVersion2Interface, self).deactivate() + + def toggle_flight_mode(self, widget): + try: + self.parrot.flight_mode = self.flight_mode.get_active() + self.flight_mode.set_active(self.parrot.flight_mode) + except resource_manager.DeviceDisconnected: + self.deactivate() + + def toggle_room(self, room, widget): + try: + if not self.room_dirty: + self.parrot.room = room + self.room_dirty = True + self._read_sound_effect_room() + self.room_dirty = False + except resource_manager.DeviceDisconnected: + self.deactivate() + + def _read_sound_effect_room(self): + active_room = self.parrot.room + room_to_menuitem_map = ( + (Rooms.CONCERT_HALL, self.concert_hall_mode), + (Rooms.JAZZ_CLUB, self.jazz_mode), + (Rooms.LIVING_ROOM, self.living_mode), + (Rooms.SILENT_ROOM, self.silent_mode), + ) + for room, menu_item in room_to_menuitem_map: + menu_item.set_active(room == active_room) + + def toggle_room_sound_effect(self, widget): + try: + self.parrot.sound_effect = self.room_sound_effect_enabled.get_active() + sound_effect = self.parrot.sound_effect + self.room_sound_effect_enabled.set_active(sound_effect) + self.concert_hall_mode.set_sensitive(sound_effect) + self.jazz_mode.set_sensitive(sound_effect) + self.living_mode.set_sensitive(sound_effect) + self.silent_mode.set_sensitive(sound_effect) + self.angle_30.set_sensitive(sound_effect) + self.angle_60.set_sensitive(sound_effect) + self.angle_90.set_sensitive(sound_effect) + self.angle_120.set_sensitive(sound_effect) + self.angle_150.set_sensitive(sound_effect) + self.angle_180.set_sensitive(sound_effect) + except resource_manager.DeviceDisconnected: + self.deactivate() + + def toggle_angle(self, angle, widget): + try: + if not self.angle_dirty: + self.parrot.angle = angle + self.angle_dirty = True + self._read_sound_effect_angle() + self.angle_dirty = False + except resource_manager.DeviceDisconnected: + self.deactivate() + + def _read_sound_effect_angle(self): + active_angle = self.parrot.angle + angle_to_menuitem_map = ( + (30, self.angle_30), + (60, self.angle_60), + (90, self.angle_90), + (120, self.angle_120), + (150, self.angle_150), + (180, self.angle_180), + ) + for angle, menu_item in angle_to_menuitem_map: + menu_item.set_active(angle == active_angle) + + def toggle_noise_cancelation(self, noise_calcelation, widget): + try: + if not self.noise_cancelation_dirty: + self.parrot.noise_control = noise_calcelation + self.noise_cancelation_dirty = True + self._read_noise_cancelation() + self.noise_cancelation_dirty = False + except resource_manager.DeviceDisconnected: + self.deactivate() + + def _read_noise_cancelation(self): + active_noise_control = self.parrot.noise_control + noise_control_to_menuitem_map = ( + (NoiseControlTypes.NOISE_CONTROL_MAX, self.noise_control_cancelation_max), + (NoiseControlTypes.NOISE_CONTROL_ON, self.noise_control_cancelation_on), + (NoiseControlTypes.NOISE_CONTROL_OFF, self.noise_control_off), + (NoiseControlTypes.STREET_MODE, self.noise_control_street_mode), + (NoiseControlTypes.STREET_MODE_MAX, self.noise_control_street_mode_max), + ) + for noise_control, menu_item in noise_control_to_menuitem_map: + menu_item.set_active(active_noise_control == noise_control) + + def toggle_head_detection(self, widget): + try: + self.parrot.head_detection = self.head_detection.get_active() + self.head_detection.set_active(self.parrot.head_detection) + except resource_manager.DeviceDisconnected: + self.deactivate() diff --git a/parrot_zik/message.py b/parrot_zik/message.py new file mode 100644 index 0000000..214b6f9 --- /dev/null +++ b/parrot_zik/message.py @@ -0,0 +1,30 @@ +class Message: + def __init__(self, resource, method, arg=None): + self.method = method + self.resource = resource + self.arg = arg + + def __str__(self): + return str(self.request) + + @property + def request(self): + message = bytearray() + message.extend(self.header) + message.extend(bytearray(self.request_string)) + return message + + @property + def header(self): + header = bytearray([0]) + header.append(len(self.request_string) + 3) + header.append("\x80") + return header + + @property + def request_string(self): + if self.method == 'set': + return 'SET {}/{}?arg={}'.format(self.resource, self.method, + str(self.arg).lower()) + else: + return 'GET {}/{}'.format(self.resource, self.method) diff --git a/parrot_zik/model/__init__.py b/parrot_zik/model/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parrot_zik/model/__init__.py diff --git a/parrot_zik/model/base.py b/parrot_zik/model/base.py new file mode 100644 index 0000000..fdacde0 --- /dev/null +++ b/parrot_zik/model/base.py @@ -0,0 +1,60 @@ +class ParrotZikBase(object): + def __init__(self, resource_manager): + self.resource_manager = resource_manager + + @property + def version(self): + return self.resource_manager.api_version + + def refresh_battery(self): + self.resource_manager.fetch('/api/system/battery') + + @property + def battery_state(self): + answer = self.resource_manager.get("/api/system/battery") + return answer.system.battery["state"] + + def get_battery_level(self, field_name): + answer = self.resource_manager.get("/api/system/battery") + return int(answer.system.battery[field_name] or 0) + + @property + def friendly_name(self): + answer = self.resource_manager.get("/api/bluetooth/friendlyname") + return answer.bluetooth["friendlyname"] + + @property + def auto_connect(self): + answer = self.resource_manager.get("/api/system/auto_connection/enabled") + return self._result_to_bool( + answer.system.auto_connection["enabled"]) + + @auto_connect.setter + def auto_connect(self, arg): + self.resource_manager.set("/api/system/auto_connection/enabled", arg) + + @property + def anc_phone_mode(self): + answer = self.resource_manager.get("/api/system/anc_phone_mode/enabled") + return self._result_to_bool( + answer.system.anc_phone_mode["enabled"]) + + def _result_to_bool(self, result): + if result == "true": + return True + elif result == "false": + return False + elif result == "invalid_on": + return True + else: + raise AssertionError(result) + +class BatteryStates: + CHARGED = 'charged' + IN_USE = 'in_use' + CHARGING = 'charging' + representation = { + CHARGED: 'Charged', + IN_USE: 'In Use', + CHARGING: 'Charging', + } diff --git a/parrot_zik/model/version1.py b/parrot_zik/model/version1.py new file mode 100644 index 0000000..324cc9f --- /dev/null +++ b/parrot_zik/model/version1.py @@ -0,0 +1,48 @@ +from parrot_zik.model.base import ParrotZikBase +from parrot_zik.resource_manager import Version1ResourceManager + + +class ParrotZikVersion1(ParrotZikBase): + def __init__(self, resource_manager): + super(ParrotZikVersion1, self).__init__( + resource_manager.get_resource_manager( + Version1ResourceManager)) + + @property + def version(self): + answer = self.resource_manager.get('/api/software/version') + return answer.software['version'] + + @property + def battery_level(self): + return int(self.get_battery_level('level')) + + @property + def lou_reed_mode(self): + answer = self.resource_manager.get("/api/audio/specific_mode/enabled") + return self._result_to_bool( + answer.audio.specific_mode["enabled"]) + + @lou_reed_mode.setter + def lou_reed_mode(self, arg): + self.resource_manager.set("/api/audio/specific_mode/enabled", arg) + + @property + def concert_hall(self): + answer = self.resource_manager.get("/api/audio/sound_effect/enabled") + return self._result_to_bool( + answer.audio.sound_effect["enabled"]) + + @concert_hall.setter + def concert_hall(self, arg): + self.resource_manager.set("/api/audio/sound_effect/enabled", arg) + + @property + def cancel_noise(self): + answer = self.resource_manager.get("/api/audio/noise_cancellation/enabled") + return self._result_to_bool( + answer.audio.noise_cancellation["enabled"]) + + @cancel_noise.setter + def cancel_noise(self, arg): + self.resource_manager.set("/api/audio/noise_cancellation/enabled", arg) diff --git a/parrot_zik/model/version2.py b/parrot_zik/model/version2.py new file mode 100644 index 0000000..0cbdc1a --- /dev/null +++ b/parrot_zik/model/version2.py @@ -0,0 +1,131 @@ +from parrot_zik.model.base import ParrotZikBase +from parrot_zik.resource_manager import Version2ResourceManager + + +class ParrotZikVersion2(ParrotZikBase): + def __init__(self, resource_manager): + super(ParrotZikVersion2, self).__init__( + resource_manager.get_resource_manager( + Version2ResourceManager)) + + @property + def version(self): + answer = self.resource_manager.get('/api/software/version') + return answer.software['sip6'] + + @property + def battery_level(self): + return self.get_battery_level('percent') + + @property + def flight_mode(self): + answer = self.resource_manager.get('/api/flight_mode') + return self._result_to_bool(answer.flight_mode['enabled']) + + @flight_mode.setter + def flight_mode(self, arg): + if arg: + self.resource_manager.toggle_on('/api/flight_mode') + else: + self.resource_manager.toggle_off('/api/flight_mode') + + @property + def sound_effect(self): + answer = self.resource_manager.get('/api/audio/sound_effect/enabled') + return self._result_to_bool(answer.audio.sound_effect['enabled']) + + @sound_effect.setter + def sound_effect(self, arg): + self.resource_manager.set('/api/audio/sound_effect/enabled', arg) + + @property + def room(self): + answer = self.resource_manager.get('/api/audio/sound_effect/room_size') + return answer.audio.sound_effect['room_size'] + + @room.setter + def room(self, arg): + self.resource_manager.set('/api/audio/sound_effect/room_size', arg) + + @property + def external_noise(self): + answer = self.resource_manager.get('/api/audio/noise') + return int(answer.audio.noise['external']) + + @property + def internal_noise(self): + answer = self.resource_manager.get('/api/audio/noise') + return int(answer.audio.noise['internal']) + + @property + def angle(self): + answer = self.resource_manager.get('/api/audio/sound_effect/angle') + return int(answer.audio.sound_effect['angle']) + + @angle.setter + def angle(self, arg): + self.resource_manager.set('/api/audio/sound_effect/angle', arg) + + @property + def noise_control(self): + answer = self.resource_manager.get('/api/audio/noise_control') + return NoiseControl.from_noise_control(answer.audio.noise_control) + + @noise_control.setter + def noise_control(self, arg): + self.resource_manager.set('/api/audio/noise_control', arg) + + @property + def noise_control_enabled(self): + answer = self.resource_manager.get('/api/audio/noise_control/enabled') + return self._result_to_bool(answer.audio.noise_control['enabled']) + + @property + def head_detection(self): + answer = self.resource_manager.get('/api/system/head_detection/enabled') + return self._result_to_bool(answer.system.head_detection['enabled']) + + @head_detection.setter + def head_detection(self, arg): + self.resource_manager.set('/api/system/head_detection/enabled', arg) + + +class NoiseControl(object): + def __init__(self, type, value): + self.type = type + self.value = value + + @classmethod + def from_noise_control(cls, noise_control): + return cls(noise_control['type'], int(noise_control['value'])) + + def __eq__(self, other): + return self.type == other.type and self.value == other.value + + def __str__(self): + return '{}&value={}'.format(self.type, self.value) + + +class NoiseControlTypes: + NOISE_CONTROL_MAX = NoiseControl('anc', 2) + NOISE_CONTROL_ON = NoiseControl('anc', 1) + NOISE_CONTROL_OFF = NoiseControl('off', 1) + STREET_MODE = NoiseControl('aoc', 1) + STREET_MODE_MAX = NoiseControl('aoc', 2) + + +class Rooms: + CONCERT_HALL = 'concert' + JAZZ_CLUB = 'jazz' + LIVING_ROOM = 'living' + SILENT_ROOM = 'silent' + representation = { + CONCERT_HALL: 'Concert Hall', + JAZZ_CLUB: 'Jazz Club', + LIVING_ROOM: 'Living Room', + SILENT_ROOM: 'Silent Room', + } + +class SoundSource: + LINE_IN = 'line-in' + A2DP = 'a2dp' diff --git a/parrot_zik/parrot_zik_tray.py b/parrot_zik/parrot_zik_tray.py new file mode 100755 index 0000000..35931d7 --- /dev/null +++ b/parrot_zik/parrot_zik_tray.py @@ -0,0 +1,80 @@ +from parrot_zik.interface.version1 import ParrotZikVersion1Interface +from parrot_zik.interface.version2 import ParrotZikVersion2Interface +from parrot_zik import resource_manager +from parrot_zik import bluetooth_paired_devices +from parrot_zik.indicator import MenuItem +from parrot_zik.indicator import Menu +from parrot_zik.indicator import SysIndicator +from parrot_zik.utils import repeat + +REFRESH_FREQUENCY = 30000 +RECONNECT_FREQUENCY = 5000 + + + +class ParrotZikIndicator(SysIndicator): + def __init__(self): + + self.menu = Menu() + + self.info_item = MenuItem("Parrot Zik Not connected", + None, sensitive=False) + self.menu.append(self.info_item) + + self.version_1_interface = ParrotZikVersion1Interface(self) + self.version_2_interface = ParrotZikVersion2Interface(self) + self.quit_item = MenuItem("Quit", self.quit, checkitem=True) + self.menu.append(self.quit_item) + + SysIndicator.__init__(self, icon="zik-audio-headset", menu=self.menu) + + self.active_interface = None + + @repeat + def reconnect(self): + if self.active_interface: + self.reconnect.stop() + else: + self.info("Trying to connect") + try: + manager = bluetooth_paired_devices.connect() + except bluetooth_paired_devices.BluetoothIsNotOn: + self.info("Bluetooth is turned off") + except bluetooth_paired_devices.DeviceNotConnected: + self.info("Parrot Zik Not connected") + except bluetooth_paired_devices.ConnectionFailure: + self.info("Failed to connect") + else: + if manager.api_version.startswith('1'): + interface = self.version_1_interface + else: + interface = self.version_2_interface + try: + interface.activate(manager) + except resource_manager.DeviceDisconnected: + interface.deactivate() + else: + self.autorefresh(self) + self.autorefresh.start(self, REFRESH_FREQUENCY) + self.reconnect.stop() + + def info(self, message): + self.info_item.set_label(message) + print(message) + + @repeat + def autorefresh(self): + if self.active_interface: + self.active_interface.refresh() + else: + self.reconnect.start(self, RECONNECT_FREQUENCY) + self.autorefresh.stop() + + @classmethod + def main(cls): + try: + indicator = cls() + cls.reconnect.start(indicator, RECONNECT_FREQUENCY) + super(ParrotZikIndicator, cls).main() + except KeyboardInterrupt: + pass diff --git a/parrot_zik/resource_manager.py b/parrot_zik/resource_manager.py new file mode 100644 index 0000000..aa141cb --- /dev/null +++ b/parrot_zik/resource_manager.py @@ -0,0 +1,177 @@ +import bluetooth +from operator import itemgetter +import sys + +from bs4 import BeautifulSoup + +from .message import Message + + +class ResourceManagerBase(object): + resources = [ + ] + + def __init__(self, socket, resource_values=None): + self.sock = socket + self.resource_values = resource_values or {} + + def get(self, resource): + try: + return self.resource_values[resource] + except KeyError: + return self.fetch(resource) + + def fetch(self, resource): + result = self.send_message(self._create_message(resource, 'get')) + self.resource_values[resource] = result + return result + + def toggle_on(self, resource): + self.send_message(self._create_message(resource, 'enable')) + self.fetch(resource) + + def toggle_off(self, resource): + self.send_message(self._create_message(resource, 'disable')) + self.fetch(resource) + + def set(self, resource, arg): + self.send_message(self._create_message(resource, 'set', arg)) + self.fetch(resource) + + def _create_message(self, resource, method, arg=None): + assert resource in self.resources, 'Unknown resource {}'.format(resource) + assert method in self.resources[resource], 'Unhandled method {} for {}'.format(method, resource) + return Message(resource, method, arg) + + def send_message(self, message): + try: + self.sock.send(str(message)) + return self.get_answer(message) + except bluetooth.btcommon.BluetoothError: + raise DeviceDisconnected + + def get_answer(self, message): + data = self.receive_message() + notifications = [] + while not data.answer: + if data.notify: + notifications.append(data.notify) + else: + raise AssertionError('Unknown response "{}" for {}'.format( + data, message.request_string)) + data = self.receive_message() + self.handle_notifications(notifications, message.resource) + return data.answer + + def handle_notifications(self, notifications, resource): + paths = map(itemgetter('path'), notifications) + clean_paths = set(map(self._clean_path, paths)) + for path in clean_paths: + if resource != path: + self.fetch(path) + + def _clean_path(self, path): + return path.rsplit('/', 1)[0].encode('utf-8') + + def receive_message(self): + if sys.platform == "darwin": + self.sock.recv(30) + else: + self.sock.recv(7) + return BeautifulSoup(self.sock.recv(1024)) + + def close(self): + self.sock.close() + + +class GenericResourceManager(ResourceManagerBase): + resources = { + '/api/software/version': ['get'], + } + + def __init__(self, sock): + super(GenericResourceManager, self).__init__(sock) + self.notifications = [] + + def handle_notification(self, notification): + self.notifications.append(notification) + + def get_resource_manager(self, resource_manager_class): + resource_manager = resource_manager_class(self.sock, self.resource_values) + resource_manager.handle_notifications(self.notifications, '/api/software/version') + return resource_manager + + @property + def api_version(self): + answer = self.get("/api/software/version") + try: + return answer.software["version"] + except KeyError: + return answer.software['sip6'] + + +class Version1ResourceManager(ResourceManagerBase): + resources = { + '/api/software/version': ['get'], + '/api/system/battery': ['get'], + '/api/bluetooth/friendlyname': ['get'], + '/api/system/auto_connection/enabled': ['get', 'set'], + '/api/system/anc_phone_mode/enabled': ['get', 'set'], + '/api/audio/specific_mode/enabled': ['get', 'set'], + '/api/audio/sound_effect/enabled': ['get', 'set'], + '/api/audio/noise_cancellation/enabled': ['get', 'set'], + } + +class Version2ResourceManager(ResourceManagerBase): + resources = { + '/api/account/username': ['get', 'set'], + '/api/appli_version': ['set'], + '/api/audio/counter': ['get'], + '/api/audio/equalizer/enabled': ['get', 'set'], + '/api/audio/equalizer/preset_id': ['set'], + '/api/audio/equalizer/preset_value': ['set'], + '/api/audio/noise_cancellation/enabled': ['get', 'set'], + '/api/audio/noise_control/enabled': ['get', 'set'], + '/api/audio/noise_control': ['get', 'set'], + '/api/audio/noise_control/phone_mode': ['get', 'set'], + '/api/audio/noise': ['get'], + '/api/audio/param_equalizer/value': ['set'], + '/api/audio/preset/bypass': ['get', 'set'], + '/api/audio/preset/': ['clear_all'], + '/api/audio/preset/counter': ['get'], + '/api/audio/preset/current': ['get'], + '/api/audio/preset': ['download', 'activate', 'save', 'remove', 'cancel_producer'], + '/api/audio/preset/synchro': ['start', 'stop'], + '/api/audio/smart_audio_tune': ['get', 'set'], + '/api/audio/sound_effect/angle': ['get', 'set'], + '/api/audio/sound_effect/enabled': ['get', 'set'], + '/api/audio/sound_effect': ['get'], + '/api/audio/sound_effect/room_size': ['get', 'set'], + '/api/audio/source': ['get'], + '/api/audio/specific_mode/enabled': ['get', 'set'], + '/api/audio/thumb_equalizer/value': ['get', 'set'], + '/api/audio/track/metadata': ['get', 'force'], + '/api/bluetooth/friendlyname': ['get', 'set'], + '/api/flight_mode': ['get', 'enable', 'disable'], + '/api/software/download_check_state': ['get'], + '/api/software/download_size': ['set'], + '/api/software/tts': ['get', 'enable', 'disable'], + '/api/software/version_checking': ['get'], + '/api/software/version': ['get'], + '/api/system/anc_phone_mode/enabled': ['get', 'set'], + '/api/system/auto_connection/enabled': ['get', 'set'], + '/api/system/auto_power_off': ['get', 'set'], + '/api/system/auto_power_off/presets_list': ['get'], + '/api/system/battery/forecast': ['get'], + '/api/system/battery': ['get'], + '/api/system/bt_address': ['get'], + '/api/system': ['calibrate'], + '/api/system/color': ['get'], + '/api/system/device_type': ['get'], + '/api/system/': ['factory_reset'], + '/api/system/head_detection/enabled': ['get', 'set'], + '/api/system/pi': ['get'], + } + +class DeviceDisconnected(Exception): + pass diff --git a/StatusAppMac.py b/parrot_zik/status_app_mac.py index fc0bc64..dccffe4 100644 --- a/StatusAppMac.py +++ b/parrot_zik/status_app_mac.py @@ -1,13 +1,12 @@ -import objc from Foundation import * from AppKit import * -from PyObjCTools import AppHelper class StatusApp(NSApplication): - def initMenu(self,menu): + def initMenu(self, menu): statusbar = NSStatusBar.systemStatusBar() - self.statusitem = statusbar.statusItemWithLength_(NSVariableStatusItemLength) + self.statusitem = statusbar.statusItemWithLength_( + NSVariableStatusItemLength) self.mymenu = menu #add menu to statusitem @@ -15,11 +14,12 @@ class StatusApp(NSApplication): self.statusitem.setToolTip_('Parrot Zik Indicator') def setIcon(self,icon,icon_directory): - self.icon = NSImage.alloc().initByReferencingFile_(icon_directory+icon+'.png') + self.icon = NSImage.alloc().initByReferencingFile_( + icon_directory + icon + '.png') self.icon.setScalesWhenResized_(True) self.icon.setSize_((20, 20)) self.statusitem.setImage_(self.icon) def clicked_(self, notification): self.mymenu.actions[notification._.title]() - NSLog('clicked!')
\ No newline at end of file + NSLog('clicked!') diff --git a/parrot_zik/utils.py b/parrot_zik/utils.py new file mode 100644 index 0000000..86f52cd --- /dev/null +++ b/parrot_zik/utils.py @@ -0,0 +1,32 @@ +import functools +from threading import Lock + +import gtk + + +class repeat(object): + def __init__(self, f): + self.f = f + self.id = None + self.lock = Lock() + + def __call__(self, cls): + self.f(cls) + + def start(self, cls, frequency): + self.lock.acquire() + if not self.id: + @functools.wraps(self.f) + def run(): + self.f(cls) + return True + self.id = gtk.timeout_add(frequency, run) + self.lock.release() + + def stop(self): + self.lock.acquire() + if self.id: + gtk.timeout_remove(self.id) + self.id = None + self.lock.release() + @@ -1,49 +1,53 @@ -from setuptools import setup
import glob
import sys
-if sys.platform=="win32":
- import py2exe
- from distutils.core import setup
+if sys.platform == "win32":
+ from distutils.core import setup
+else:
+ from setuptools import setup
setup(
- name = 'parrotziktray',
- description = 'Parrot Zik Tray Indicator',
+ name='parrotziktray',
+ description='Parrot Zik Tray Indicator',
author="Dmitry Moiseev",
author_email="m0sia@m0sia.ru",
maintainer_email="m0sia@m0sia.ru",
url="https://github.com/m0sia/pyParrotZik",
license="'GPLv2+'",
- version = '0.3',
-
- windows = [
- {
- 'script': 'ParrotZikTray',
- 'icon_resources': [(1, "./share/icons/zik/Headphone.ico")],
- }
- ],
-
- options = {
- 'py2exe': {
- #'packages':'encodings',
- # Optionally omit gio, gtk.keysyms, and/or rsvg if you're not using them
- 'includes': 'cairo, pango, pangocairo, atk, gobject, gio, gtk.keysyms, _winreg',
- }
- },
+ version='0.3',
+
+ windows=[
+ {
+ 'script': 'parrot_zik/parrot_zik_tray',
+ 'icon_resources': [(1, "./share/icons/zik/Headphone.ico")],
+ }
+ ],
+
+ options={
+ 'py2exe': {
+ #'packages':'encodings',
+ # Optionally omit gio, gtk.keysyms, and/or rsvg if you're not using them
+ 'includes': 'cairo, pango, pangocairo, atk, gobject, gio, gtk.keysyms, _winreg',
+ }
+ },
data_files=[
- ("share/icons/zik", glob.glob("share/icons/zik/*.png"))
- # If using GTK+'s built in SVG support, uncomment these
- #os.path.join(gtk_base_path, '..', 'runtime', 'bin', 'gdk-pixbuf-query-loaders.exe'),
- #os.path.join(gtk_base_path, '..', 'runtime', 'bin', 'libxml2-2.dll'),
- ],
+ ("share/icons/zik", glob.glob("share/icons/zik/*.png"))
+ # If using GTK+'s built in SVG support, uncomment these
+ #os.path.join(gtk_base_path, '..', 'runtime', 'bin', 'gdk-pixbuf-query-loaders.exe'),
+ #os.path.join(gtk_base_path, '..', 'runtime', 'bin', 'libxml2-2.dll'),
+ ],
install_requires=[
- 'BeautifulSoup','bluetooth'
- ],
-
- py_modules=['ParrotZik','ParrotProtocol'],
-
- scripts=["ParrotZikTray"]
+ 'beautifulsoup4', 'pybluez'
+ ],
+
+ packages=['parrot_zik', 'parrot_zik.interface', 'parrot_zik.indicator', 'parrot_zik.model'],
+ entry_points={
+ 'console_scripts': [
+ 'parrot_zik_tray=parrot_zik.parrot_zik_tray:ParrotZikIndicator.main',
+ ]
+ },
+ include_package_data=True,
)
|