From 947a9f7d10bc3c54939cfa72dab27b2e64034318 Mon Sep 17 00:00:00 2001 From: Marek Siarkowicz Date: Mon, 15 Jun 2015 11:33:33 +0200 Subject: Create packet. --- parrot_zik/__init__.py | 0 parrot_zik/bluetooth_paired_devices.py | 107 ++++++++ parrot_zik/indicator.py | 254 ++++++++++++++++++ parrot_zik/message.py | 30 +++ parrot_zik/parrot_zik_model.py | 222 ++++++++++++++++ parrot_zik/parrot_zik_tray | 472 +++++++++++++++++++++++++++++++++ parrot_zik/resource_manager.py | 176 ++++++++++++ parrot_zik/status_app_mac.py | 25 ++ 8 files changed, 1286 insertions(+) create mode 100644 parrot_zik/__init__.py create mode 100644 parrot_zik/bluetooth_paired_devices.py create mode 100644 parrot_zik/indicator.py create mode 100644 parrot_zik/message.py create mode 100644 parrot_zik/parrot_zik_model.py create mode 100755 parrot_zik/parrot_zik_tray create mode 100644 parrot_zik/resource_manager.py create mode 100644 parrot_zik/status_app_mac.py (limited to 'parrot_zik') diff --git a/parrot_zik/__init__.py b/parrot_zik/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/parrot_zik/bluetooth_paired_devices.py b/parrot_zik/bluetooth_paired_devices.py new file mode 100644 index 0000000..eb1ec09 --- /dev/null +++ b/parrot_zik/bluetooth_paired_devices.py @@ -0,0 +1,107 @@ +import sys +import re +import os + +from parrot_zik.resource_manager import GenericResourceManager + +if sys.platform == "darwin": + from binplist import binplist + import lightblue +else: + import bluetooth + if sys.platform == "win32": + import _winreg + + +def get_parrot_zik_mac(): + 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}') + if sys.platform == "linux2": + bluetooth_on = int(os.popen('bluez-test-adapter powered').read()) + if bluetooth_on == 1: + out = os.popen("bluez-test-device list").read() + res = p.findall(out) + if len(res) > 0: + return res[0] + else: + raise DeviceNotConnected + else: + raise BluetoothIsNotOn + 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("-", ":") + else: + raise DeviceNotConnected + except Exception: + 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] + else: + raise DeviceNotConnected + except EnvironmentError: + pass + + +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) + + sock.connect((host, port)) + + 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.py b/parrot_zik/indicator.py new file mode 100644 index 0000000..2691113 --- /dev/null +++ b/parrot_zik/indicator.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python + +import sys +import os +import tempfile + +if sys.platform == "linux2" or sys.platform == "win32": + import gtk +elif sys.platform == "darwin": + from Foundation import * + from AppKit import * + from PyObjCTools import AppHelper + from status_app_mac import StatusApp + + +class BaseIndicator(object): + def __init__(self, icon, menu, statusicon): + self.menu = menu + self.statusicon = statusicon + self.setIcon(icon) + + 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 setIcon(self, name): + raise NotImplementedError + + def main(self): + raise NotImplementedError + + def show_about_dialog(self, widget): + raise NotImplementedError + + +class WindowsIndicator(BaseIndicator): + def __init__(self, icon, menu): + self.icon_directory = ( + os.path.dirname(os.path.realpath(sys.argv[0])) + os.path.sep + '..' + + os.path.sep + 'share' + os.path.sep + 'icons' + + os.path.sep +'zik' + os.path.sep) + 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") + 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 setIcon(self, name): + self.statusicon.set_from_file(self.icon_directory + name + '.png') + + def main(self): + gtk.main() + + 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 LinuxIndicator(BaseIndicator): + def __init__(self, icon, 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 + '..' + + os.path.sep + 'share' + os.path.sep + + 'icons' + os.path.sep+'zik' + + os.path.sep) + 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) + + def main(self): + gtk.main() + + 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 DarwinIndicator(BaseIndicator): + def __init__(self, icon, menu): + self.icon_directory = ( + os.path.dirname(os.path.realpath(sys.argv[0])) + os.path.sep + '..' + os.path.sep + + 'share' + os.path.sep + 'icons' + os.path.sep + 'zik' + + os.path.sep) + statusicon = StatusApp.sharedApplication() + statusicon.initMenu(menu) + super(DarwinIndicator, self).__init__(icon, menu, statusicon) + + def setIcon(self, name): + self.statusicon.setIcon(name, self.icon_directory) + + def main(self): + AppHelper.runEventLoop() + + def show_about_dialog(self, widget): + 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 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() + + +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 + +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) + + +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) + +if sys.platform == 'linux2': + SysIndicator = LinuxIndicator + Menu = GTKMenu + MenuItem = GTKMenuItem +elif sys.platform == 'win32': + SysIndicator = WindowsIndicator + Menu = GTKMenu + MenuItem = GTKMenuItem +elif sys.platform == 'darwin': + SysIndicator = DarwinIndicator + Menu = NSMenu + MenuItem = NSMenuItem +else: + raise Exception('Platform not supported') + +if __name__ == "__main__": + + quit_item = MenuItem("Quit", sys.exit, True) + + menu = Menu() + menu.append(quit_item) + + indicator = SysIndicator(icon="zik-audio-headset", menu=menu) + indicator.main() 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/parrot_zik_model.py b/parrot_zik/parrot_zik_model.py new file mode 100644 index 0000000..e15200f --- /dev/null +++ b/parrot_zik/parrot_zik_model.py @@ -0,0 +1,222 @@ +from parrot_zik.resource_manager import Version1ResourceManager +from parrot_zik.resource_manager import Version2ResourceManager + + +class BatteryStates: + CHARGED = 'charged' + IN_USE = 'in_use' + CHARGING = 'charging' + representation = { + CHARGED: 'Charged', + IN_USE: 'In Use', + CHARGING: 'Charging', + } + +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 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 '{}++{}'.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 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]) + + @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 + else: + raise AssertionError(result) + + +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.get("/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.get("/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) + + +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): + pass + + @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']) diff --git a/parrot_zik/parrot_zik_tray b/parrot_zik/parrot_zik_tray new file mode 100755 index 0000000..dcdacd0 --- /dev/null +++ b/parrot_zik/parrot_zik_tray @@ -0,0 +1,472 @@ +#!/usr/bin/env python +import functools +from threading import Lock +import gtk + +from parrot_zik import resource_manager +from parrot_zik import bluetooth_paired_devices +from parrot_zik.parrot_zik_model import BatteryStates +from parrot_zik.parrot_zik_model import ParrotZikVersion1 +from parrot_zik.parrot_zik_model import ParrotZikVersion2 +from parrot_zik.parrot_zik_model import NoiseControlTypes +from parrot_zik.parrot_zik_model import Rooms +from parrot_zik.indicator import MenuItem +from parrot_zik.indicator import Menu +from parrot_zik.indicator import SysIndicator + +REFRESH_FREQUENCY = 30000 +RECONNECT_FREQUENCY = 5000 + + +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: + 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() + + +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 = MenuItem("Quit", gtk.main_quit, checkitem=True) + self.menu.append(self.quit) + + 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() + + def main(self): + self.reconnect.start(self, RECONNECT_FREQUENCY) + SysIndicator.main(self) + +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.auto_connection = MenuItem("Auto Connection", self.toggle_auto_connection, + checkitem=True, visible=False) + 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.auto_connection) + + 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.auto_connection.show() + self.indicator.active_interface = self + self.indicator.menu.reposition() + + @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.auto_connection.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)) + + +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): + self.noise_cancelation.show() + self.lou_reed_mode.show() + self.concert_hall_mode.show() + 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) + + 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() + + +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, sensitive=False) + self.noise_control_cancelation_on = MenuItem( + "Normal Cancelation", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.NOISE_CONTROL_ON), checkitem=True, sensitive=False) + self.noise_control_off = MenuItem( + "Off", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.NOISE_CONTROL_OFF), checkitem=True, sensitive=False) + self.noise_control_street_mode = MenuItem( + "Street Mode", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.STREET_MODE), checkitem=True, sensitive=False) + self.noise_control_street_mode_max = MenuItem( + "Street Mode Max", functools.partial( + self.toggle_noise_cancelation, + NoiseControlTypes.STREET_MODE_MAX), checkitem=True, sensitive=False) + 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.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): + self.noise_cancelation.show() + self.flight_mode.show() + self.room_sound_effect.show() + 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() + 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) + + 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) + + +if __name__ == "__main__": + try: + indicator = ParrotZikIndicator() + indicator.main() + except KeyboardInterrupt: + pass diff --git a/parrot_zik/resource_manager.py b/parrot_zik/resource_manager.py new file mode 100644 index 0000000..8b63e8c --- /dev/null +++ b/parrot_zik/resource_manager.py @@ -0,0 +1,176 @@ +import bluetooth +from operator import itemgetter +import sys + +from BeautifulSoup import BeautifulSoup + +from parrot_zik.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') + 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'], + '/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/parrot_zik/status_app_mac.py b/parrot_zik/status_app_mac.py new file mode 100644 index 0000000..dccffe4 --- /dev/null +++ b/parrot_zik/status_app_mac.py @@ -0,0 +1,25 @@ +from Foundation import * +from AppKit import * + +class StatusApp(NSApplication): + + def initMenu(self, menu): + statusbar = NSStatusBar.systemStatusBar() + self.statusitem = statusbar.statusItemWithLength_( + NSVariableStatusItemLength) + + self.mymenu = menu + #add menu to statusitem + self.statusitem.setMenu_(menu.menubarMenu) + self.statusitem.setToolTip_('Parrot Zik Indicator') + + def setIcon(self,icon,icon_directory): + 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!') -- cgit v1.2.1