diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29b9a40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/* +lastConnect.json \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..a157ad7 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pybluez = {git = "git+https://github.com/pybluez/pybluez.git#egg=pybluez"} +black = "*" +isort = "*" +rich = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..b909e75 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,136 @@ +{ + "_meta": { + "hash": { + "sha256": "2b172f5021684e8ebe146edb118804ed1e0b8a947af42c4ebb2f1b091311d5af" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "black": { + "hashes": [ + "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", + "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1", + "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", + "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8", + "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", + "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", + "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", + "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", + "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94", + "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", + "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c", + "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7", + "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", + "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", + "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7", + "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", + "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", + "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741", + "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", + "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", + "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", + "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==24.4.2" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "markdown-it-py": { + "hashes": [ + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.2" + }, + "pybluez": { + "git": "git+https://github.com/pybluez/pybluez.git#egg=pybluez", + "ref": "82cbba8a1ebd4c1e3442dfafd8581d58c50fa39e" + }, + "pygments": { + "hashes": [ + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" + ], + "markers": "python_version >= '3.8'", + "version": "==2.18.0" + }, + "rich": { + "hashes": [ + "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", + "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" + ], + "index": "pypi", + "markers": "python_full_version >= '3.7.0'", + "version": "==13.7.1" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 2a7f382..96e5ebc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is an **unofficial** API for the Anker SoundCore Life Headphones. I am not partnered with Soundcore or Anker in any way. ## Supported Devices -The Anker Soundcore Life Q30 is the only device I currently own, making me only able to test this one. However, other devices such as the Q35 should still work properly despite this. If not, feel free to report an Issue or send a Pull Request. +The Anker Soundcore Life Q30 is the only device I currently own, making me only able to test this one. However, other devices such as the Q35 and A2 should still work properly despite this. If not, feel free to report an Issue or send a Pull Request. ## Requirements - Python 3 (both the API and the application) diff --git a/SoundcoreAPI.py b/SoundcoreAPI.py index deee593..cf1c260 100644 --- a/SoundcoreAPI.py +++ b/SoundcoreAPI.py @@ -1,47 +1,54 @@ import socket import threading -from bluetooth import lookup_name +from bluetooth import BluetoothSocket, find_service -class Soundcore(): - def __init__(self, macaddress: str, onEvent = None): +class Soundcore: + def __init__(self, macaddress: str, onEvent=None): self.macaddress = macaddress - self.Port = self.getPort() - self.client = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) + self.service = find_service(address=self.macaddress) + if len(self.service): + self.Port = find_service(address=self.macaddress)[0]["port"] + else: + raise ConnectionError(f"Cannot connect to {self.macaddress}") + self.client = socket.socket( + socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM + ) self.__thread = threading.Thread(target=self.__ParseReceiveData) self._running = True - self.callback = onEvent + self.callback = onEvent self.data = "" + self.bluesock = BluetoothSocket(proto=3) self.presetEQprofile = { - "SoundCore Signature": "08ee00000002811400000078787878787878784d", - "Acoustic": "08ee000000028114000100a0828c8ca0a0a08c34", - "Bass Booster": "08ee000000028114000200a0968278787878789f", - "Bass Reducer": "08ee000000028114000300505a6e787878787800", - "Classical": "08ee00000002811400040096966464788c96a0bf", - "Podcast": "08ee0000000281140005005a8ca0a0968c7864b6", - "Dance": "08ee0000000281140006008c5a6e828c8c825a5d", - "Deep": "08ee0000000281140007008c8296968c64504654", - "Electronic": "08ee000000028114000800968c648c828c9696e1", - "Flat": "08ee00000002811400090064646e7878786464fc", - "Hip-Hop": "08ee000000028114000a008c966e6e8c6e8c96b1", - "Jazz": "08ee000000028114000b008c8c6464788c96a0b2", - "Latin": "08ee000000028114000c0078786464647896aa6d", - "Lounge": "08ee000000028114000d006e8ca09678648c82b4", - "Piano": "08ee000000028114000e007896968ca0aa96a04b", - "Pop": "08ee000000028114000f006e829696826e645a66", - "R&B": "08ee000000028114001000b48c64648c9696a0fd", - "Rock": "08ee000000028114001100968c6e6e82969696e0", - "Small Speaker(s)": "08ee000000028114001200a0968278645a50502d", - "Spoken Word": "08ee0000000281140013005a64828c8c82785a4c", - "Treble Booster": "08ee0000000281140014006464646e828c8ca075", - "Treble Reducer": "08ee000000028114001500787878645a50503ca4" + "SoundCore Signature": "08ee00000002811400000078787878787878784d", + "Acoustic": "08ee000000028114000100a0828c8ca0a0a08c34", + "Bass Booster": "08ee000000028114000200a0968278787878789f", + "Bass Reducer": "08ee000000028114000300505a6e787878787800", + "Classical": "08ee00000002811400040096966464788c96a0bf", + "Podcast": "08ee0000000281140005005a8ca0a0968c7864b6", + "Dance": "08ee0000000281140006008c5a6e828c8c825a5d", + "Deep": "08ee0000000281140007008c8296968c64504654", + "Electronic": "08ee000000028114000800968c648c828c9696e1", + "Flat": "08ee00000002811400090064646e7878786464fc", + "Hip-Hop": "08ee000000028114000a008c966e6e8c6e8c96b1", + "Jazz": "08ee000000028114000b008c8c6464788c96a0b2", + "Latin": "08ee000000028114000c0078786464647896aa6d", + "Lounge": "08ee000000028114000d006e8ca09678648c82b4", + "Piano": "08ee000000028114000e007896968ca0aa96a04b", + "Pop": "08ee000000028114000f006e829696826e645a66", + "R&B": "08ee000000028114001000b48c64648c9696a0fd", + "Rock": "08ee000000028114001100968c6e6e82969696e0", + "Small Speaker(s)": "08ee000000028114001200a0968278645a50502d", + "Spoken Word": "08ee0000000281140013005a64828c8c82785a4c", + "Treble Booster": "08ee0000000281140014006464646e828c8ca075", + "Treble Reducer": "08ee000000028114001500787878645a50503ca4", } def __recvData(self): if not (data := self.client.recv(1024)) == self.data: self.data = data if self.callback != None: - threading.Thread(target = self.callback, args=(self.data,)).start() + threading.Thread(target=self.callback, args=(self.data,)).start() def __ParseReceiveData(self): try: @@ -49,74 +56,58 @@ def __ParseReceiveData(self): self.__recvData() self.__ParseReceiveData() except Exception as e: - if 'timed out' or "[WinError 10054]" in list(str(e)): + if "timed out" or "[WinError 10054]" in list(str(e)): print(e) return else: pass - #print(e) - - def getPort(self): - """ - This function trys to look for Device ports range can be changed with `times` args - Range port is Only 1-30 - https://people.csail.mit.edu/albert/bluez-intro/x148.html - """ - print(self.macaddress) - if "Soundcore" in str(lookup_name(self.macaddress)).split(): - for x in range(1,31): - try: - s = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) - s.connect((self.macaddress, x)) - s.close() - return x - except OSError: - pass - return None - - # def GetNearby_devices(self): - # #return bluetooth.discover_devices(lookup_names=True) - # return bluetooth.find_service(address=self.macaddress) - def send(self, data): from time import sleep - #print(data) + + # print(data) self.client.send(bytearray.fromhex(data)) sleep(0.1) - def parseInfo(self): """ This returns a dict with SN, battery status, Modes, etc... """ + def getData(): self.send("08ee00000001010a0002") - return {"EQ": self.data.hex()[22:42], "battery": self.data[9]*10, "ANC": self.data[44:47], "SN": self.data[53:68], "firmware": self.data[48:52]} + return { + "EQ": self.data.hex()[22:42], + "battery": self.data[9] * 10, + "ANC": self.data[44:47], + "SN": self.data[53:68], + "firmware": self.data[48:52], + } + return getData() def ANC(self, modes: str): - ''' + """ "Transparency": Set your headphone to Transparency mode "Normal": Set your headphone to Normal mode "ANC Indoor": Set your headphone to ANC Indoor mode "ANC Outdoor": Set your headphone to ANC Outdoor mode "ANC Transport": Set your headphone to ANC Transport mode - ''' + """ base = "08ee00000006810e000" if modes == "Transparency": - self.send(base+"10101008e") + self.send(base + "10101008e") elif modes == "Normal": - self.send(base+"20101008f") + self.send(base + "20101008f") elif modes == "ANC Indoor": - self.send(base+"00201008e") + self.send(base + "00201008e") elif modes == "ANC Outdoor": - self.send(base+"00101008d") + self.send(base + "00101008d") elif modes == "ANC Transport": - self.send(base+"00001008c") + self.send(base + "00001008c") def preEQ(self, premadeEQ: str): - ''' + """ Custom EQ settings made by Soundcore Here is list of them all - SoundCore Signature @@ -141,19 +132,18 @@ def preEQ(self, premadeEQ: str): - Spoken Word - Treble Booster - Treble Reducer - ''' - #print(premadeEQ in self.presetEQprofile.keys()) + """ + # print(premadeEQ in self.presetEQprofile.keys()) if premadeEQ in list(self.presetEQprofile.keys()): self.send(self.presetEQprofile[premadeEQ]) else: - raise TypeError(premadeEQ+" is Invaild") + raise TypeError(premadeEQ + " is Invaild") def findCurrentEQ(self): currentEQ = self.parseInfo()["EQ"] for eqName, eqValue in self.presetEQprofile.items(): if currentEQ in eqValue: return eqName - return "Unkown" @@ -175,14 +165,15 @@ def EQGain(self, values: list): gainValues.append(value) else: gainValues.append(120) + def getCrc(packet): crc = 0 for b in packet: crc += b - return (crc & 0xFF) + return crc & 0xFF + gainValues.append(getCrc(gainValues)) self.send(gainValues) - def connect(self): """ @@ -200,6 +191,6 @@ def close(self): This stops the bluetooth connection with headphone, If you dont do this your Mobile app may not able to connect. """ self._running = False - self.client.close() + self.bluesock.close() self.__thread.join() - print("Disconnected") \ No newline at end of file + print("Disconnected") diff --git a/SoundcoreDesktop.py b/SoundcoreDesktop.py index 20b076f..45dfaae 100644 --- a/SoundcoreDesktop.py +++ b/SoundcoreDesktop.py @@ -4,15 +4,21 @@ import json from tkinter.constants import END, X from SoundcoreAPI import Soundcore +from rich.console import Console import bluetooth +console = Console() + + def passFunc(): pass + isConnected = False Headphone = None parsedata = None + def ANCState(ANC): if ANC == b"\x02\x00\x01": return "Normal" @@ -27,227 +33,324 @@ def ANCState(ANC): else: return None -window = tkinter.Tk() -window.title("Anker Soundcore Desktop") -window.geometry("340x500") - -checkMark = tkinter.Label(window, text="✓", font=("Arial", 13, "bold")) -def onHeadphoneEvent(data): - deviceInfo = Headphone.parseInfo() - #ANCValue = ANCState(deviceInfo['ANC']) - print(deviceInfo) - # checkMarkPos = { - # "ANC": (10+5, 90+5, "#714fb0"), - # "Trans": (119+5, 90+5, "#4f75b0"), - # "Normal": (227+5, 90+5, "#525a6b") - # } - # print(ANCValue) - # if "ANC" in ANCValue: - # print("ANC mode") - # checkMark.configure(background=checkMarkPos["ANC"][2]) - # elif "Normal" == ANCValue: - # print("Normal mode") - # checkMark.configure(background=checkMarkPos["Normal"][2]) - # elif "Transparency" == ANCValue: - # print("Trans mode") - # checkMark.configure(background=checkMarkPos["Trans"][2]) - # window.update() - -if not os.path.exists(os.path.join(os.getcwd(), "lastConnect.json")): - listBT = bluetooth.discover_devices(lookup_names=True) - print(listBT) - for device in listBT: - if "Soundcore" in device[1]: - macaddress = device[0] - Headphone = Soundcore(macaddress, onHeadphoneEvent) - with open(os.path.join(os.getcwd(), "lastConnect.json"), "w") as lastConnect: - lastConnect.write(json.dumps({"macAddress": macaddress})) -else: - with open(os.path.join(os.getcwd(), "lastConnect.json"), "r") as lastConnect: - lastConnect = json.loads(lastConnect.readlines()[0]) - Headphone = Soundcore(lastConnect["macAddress"], onHeadphoneEvent) - -Headphone.connect() -parsedata = Headphone.parseInfo() -if parsedata: - isConnected = True - -EQChoices = ["SoundCore Signature", "Acoustic", "Bass Booster", "Bass Reducer", "Classical", "Podcast", - "Dance", "Deep", "Electronic", "Flat", "Hip-Hop", "Jazz", "Latin", "Lounge", "Piano", "Pop", "R&B", "Rock", - "Small Speaker(s)", "Spoken Word", "Treble Booster", "Treble Reducer"] - -EQlabel = tkinter.Label(text="EQ") -EQlabel.configure(font=("Arial", 13, "bold")) -EQlabel.place(x=2, y=5) - -menu = tkinter.StringVar() -if parsedata != None: - menu.set(Headphone.findCurrentEQ()) - -def onChangeEQ(*arg): - if Headphone and menu.get() != "Change EQ": - Headphone.preEQ(menu.get()) - -menu.trace("w", onChangeEQ) - -drop= tkinter.OptionMenu(window, menu, *EQChoices) -drop.place(x=34, y=3) - -inputtxt = tkinter.Entry(window, width=16) -if Headphone != None: - inputtxt.delete(0, END) - inputtxt.insert(0, Headphone.macaddress) -inputtxt.place(x=190 , y=9) - -def tryConnect(): - global Headphone - Headphone = Soundcore(inputtxt.get()) - Headphone.connect() - data = Headphone.parseInfo() - if data: - with open(os.path.join(os.getcwd(), "lastConnect.json"), "w") as lastConnect: - lastConnect.write(json.dumps({"macAddress": inputtxt.get()})) - -connectButton = tkinter.Button(window, width=6, height=0, text="connect", command=tryConnect) -connectButton.place(x=290 , y=4) - -ttk.Separator(window, orient='horizontal').place(y=40, relwidth=1, relheight=1) - - -transImage = tkinter.PhotoImage(file="Images//Ambient Sound//a3029_img_trans.png") -transImage = transImage.subsample(3,3) -label = tkinter.Label(window, image=transImage) -label.place(x=0, y=210) - -Modeinfo = tkinter.Label(window, text="Stay aware of your sourrounding by allowing\n ambient sound in.") -Modeinfo.configure(font=("Arial", 10)) -Modeinfo.place(x=45, y=200) - -def removeImg(): - global label - label.destroy() - -text = { - "ANC Outdoor": "Reduce ambient sound on-the-go for quieter city spaces", - "ANC Trans": "Targets low-end frequencies like engine and road noise for serene journeys and commutes.", - "ANC INDOOR": "Eliminate voices and mid-frequency noise from coffee shaps and other inside spaces." -} - -def forgetANCWegets(): - global TransportButton, IndoorButton, OutdoorButton, ANCModetxt, window - try: - TransportButton.destroy() - IndoorButton.destroy() - OutdoorButton.destroy() - ANCModetxt.destroy() - window.update() - except NameError: - print("error") - -def ANC(): - global Modeinfo, label, window - global TransportButton, IndoorButton, OutdoorButton, ANCModetxt - var = tkinter.IntVar() - var.set(1) - - ANCModetxt = tkinter.Label(window, text="Modes") - ANCModetxt.configure(font=("Arial", 12, "bold")) - ANCModetxt.place(x=30, y=196) - - buttons = [ - ("Transport", 1, 30), - ("Indoor", 2, 120), - ("Outdoor", 3, 210) - ] - def getAction(data=None): - global image - data = data if data != None else var.get() - if data == 1: - Modeinfo.configure(text="Targets low-end frequencies like engine and road\n noise for serene journeys and commutes.") - image = tkinter.PhotoImage(file="Images//ANC//a3951_img_transport.png") - image = image.subsample(3,3) - label.configure(image=image) - Headphone.ANC("ANC Transport") - elif data == 2: - Modeinfo.configure(text="Eliminate voices and mid-frequency noise from\n coffee shaps and other inside spaces.") - image = tkinter.PhotoImage(file="Images//ANC//a3951_img_indoor.png") - image = image.subsample(3,3) - label.configure(image=image) - Headphone.ANC("ANC Indoor") - elif data == 3: - Modeinfo.configure(text="Reduce ambient sound on-the-go for quieter city\n spaces") - image = tkinter.PhotoImage(file="Images//ANC//a3951_img_outdoor.png") - image = image.subsample(3,3) - label.configure(image=image) - Headphone.ANC("ANC Outdoor") - Modeinfo.place(x=25, y=450) - - getAction() - - TransportButton = tkinter.Radiobutton(window, text=buttons[0][0], variable=var, command=getAction, value=buttons[0][1], indicatoron = 0, width=10, height=2) - TransportButton.place(x=buttons[0][2], y=230) - - IndoorButton = tkinter.Radiobutton(window, text=buttons[1][0], variable=var, command=getAction, value=buttons[1][1], indicatoron = 0, width=10, height=2) - IndoorButton.place(x=buttons[1][2], y=230) - - OutdoorButton = tkinter.Radiobutton(window, text=buttons[2][0], variable=var, command=getAction, value=buttons[2][1], indicatoron = 0, width=10, height=2) - OutdoorButton.place(x=buttons[2][2], y=230) - -def trans(): - global Modeinfo, label, window - forgetANCWegets() - Modeinfo.configure(text="Stay aware of your sourrounding by allowing\n ambient sound in.") - Modeinfo.place(x=45, y=200) - #Modeinfo.configure(x=45, y=200) - image = tkinter.PhotoImage(file="Images//Ambient Sound//a3029_img_trans.png") - image = image.subsample(3,3) - label.configure(image=image) - label.image = image - Headphone.ANC("Transparency") - text = "Reduce ambient sound on-the-go for quieter city spaces" - -def normal(): - global Modeinfo, label, window - forgetANCWegets() - Modeinfo.configure(text="Turn off noise cancellation and transparency") - Modeinfo.place(x=45, y=200) - image = tkinter.PhotoImage(file="Images//Ambient Sound//a3029_img_normal.png") - image = image.subsample(3,3) - label.configure(image=image) - label.image = image - Headphone.ANC("Normal") - -ANCImg = tkinter.PhotoImage(file = "Images//Modes//a3029_ambient_icon_anc.png") -ANCImg = ANCImg.subsample(2,2) -page1btn = tkinter.Button(window, text="Noise Cancellation", image=ANCImg, compound = "top", background = "#714fb0", command=ANC, height=96, width=96, fg = "white", relief="groove") -# page1btn.bind("", lambda _: "break", add=True) +try: + window = tkinter.Tk() + window.title("Anker Soundcore Desktop") + window.geometry("340x500") + + checkMark = tkinter.Label(window, text="✓", font=("Arial", 13, "bold")) + + def onHeadphoneEvent(data): + deviceInfo = Headphone.parseInfo() + # ANCValue = ANCState(deviceInfo['ANC']) + print(deviceInfo) + return deviceInfo + # checkMarkPos = { + # "ANC": (10+5, 90+5, "#714fb0"), + # "Trans": (119+5, 90+5, "#4f75b0"), + # "Normal": (227+5, 90+5, "#525a6b") + # } + # print(ANCValue) + # if "ANC" in ANCValue: + # print("ANC mode") + # checkMark.configure(background=checkMarkPos["ANC"][2]) + # elif "Normal" == ANCValue: + # print("Normal mode") + # checkMark.configure(background=checkMarkPos["Normal"][2]) + # elif "Transparency" == ANCValue: + # print("Trans mode") + # checkMark.configure(background=checkMarkPos["Trans"][2]) + # window.update() + + if not os.path.exists(os.path.join(os.getcwd(), "lastConnect.json")): + listBT = bluetooth.discover_devices(lookup_names=True) + print(listBT) + for device in listBT: + if "oundcore" in device[1]: + macaddress = device[0] + Headphone = Soundcore(macaddress, onHeadphoneEvent) + with open(os.path.join(os.getcwd(), "lastConnect.json"), "w") as lastConnect: + lastConnect.write(json.dumps({"macAddress": macaddress})) + else: + with open(os.path.join(os.getcwd(), "lastConnect.json"), "r") as lastConnect: + lastConnect = json.loads(lastConnect.readlines()[0]) + Headphone = Soundcore(lastConnect["macAddress"], onHeadphoneEvent) -TransparcyImg = tkinter.PhotoImage(file = "Images//Modes//a3029_ambient_icon_trans.png") -TransparcyImg = TransparcyImg.subsample(2,2) -page2btn = tkinter.Button(window, text="Transparency", image=TransparcyImg, compound = "top", background = "#4f75b0", command=trans, height=96, width=96, fg = "white", relief="groove") -# page2btn.bind("", lambda _: "break", add=True) + Headphone.connect() + parsedata = Headphone.parseInfo() + if parsedata: + isConnected = True + + EQChoices = [ + "SoundCore Signature", + "Acoustic", + "Bass Booster", + "Bass Reducer", + "Classical", + "Podcast", + "Dance", + "Deep", + "Electronic", + "Flat", + "Hip-Hop", + "Jazz", + "Latin", + "Lounge", + "Piano", + "Pop", + "R&B", + "Rock", + "Small Speaker(s)", + "Spoken Word", + "Treble Booster", + "Treble Reducer", + ] -NormalImg = tkinter.PhotoImage(file = "Images//Modes//a3029_ambient_icon_off.png") -NormalImg = NormalImg.subsample(2,2) -page3btn = tkinter.Button(window, text="Normal", image=NormalImg, compound = "top", background = "#525a6b", command=normal, height=96, width=96, fg = "white", relief="groove") -# page3btn.bind("", lambda _: "break", add=True) + EQlabel = tkinter.Label(text="EQ") + EQlabel.configure(font=("Arial", 13, "bold")) + EQlabel.place(x=2, y=5) + menu = tkinter.StringVar() + if parsedata != None: + menu.set(Headphone.findCurrentEQ()) -NavLabel = tkinter.Label(text="Ambient Sound") -NavLabel.configure(font=("Arial", 20, "bold")) -NavLabel.place(x=8, y=45) + def onChangeEQ(*arg): + if Headphone and menu.get() != "Change EQ": + Headphone.preEQ(menu.get()) -page1btn.place(x=5, y=90) -page2btn.place(x=23+96, y=90) -page3btn.place(x=232, y=90) + menu.trace("w", onChangeEQ) -window.resizable(0,0) + drop = tkinter.OptionMenu(window, menu, *EQChoices) + drop.place(x=34, y=3) -def on_closing(): + inputtxt = tkinter.Entry(window, width=16) if Headphone != None: - Headphone.close() - window.destroy() + inputtxt.delete(0, END) + inputtxt.insert(0, Headphone.macaddress) + inputtxt.place(x=190, y=9) + + def tryConnect(): + global Headphone + Headphone = Soundcore(inputtxt.get()) + Headphone.connect() + data = Headphone.parseInfo() + if data: + with open( + os.path.join(os.getcwd(), "lastConnect.json"), "w" + ) as lastConnect: + lastConnect.write(json.dumps({"macAddress": inputtxt.get()})) + + connectButton = tkinter.Button( + window, width=6, height=0, text="connect", command=tryConnect + ) + connectButton.place(x=290, y=4) + + ttk.Separator(window, orient="horizontal").place(y=40, relwidth=1, relheight=1) + + transImage = tkinter.PhotoImage(file="Images//Ambient Sound//a3029_img_trans.png") + transImage = transImage.subsample(3, 3) + label = tkinter.Label(window, image=transImage) + label.place(x=0, y=210) + + Modeinfo = tkinter.Label( + window, text="Stay aware of your sourrounding by allowing\n ambient sound in." + ) + Modeinfo.configure(font=("Arial", 10)) + Modeinfo.place(x=45, y=200) -window.protocol("WM_DELETE_WINDOW", on_closing) -window.mainloop() \ No newline at end of file + def removeImg(): + global label + label.destroy() + + text = { + "ANC Outdoor": "Reduce ambient sound on-the-go for quieter city spaces", + "ANC Trans": "Targets low-end frequencies like engine and road noise for serene journeys and commutes.", + "ANC INDOOR": "Eliminate voices and mid-frequency noise from coffee shops and other inside spaces.", + } + + def forgetANCWegets(): + global TransportButton, IndoorButton, OutdoorButton, ANCModetxt, window + try: + TransportButton.destroy() + IndoorButton.destroy() + OutdoorButton.destroy() + ANCModetxt.destroy() + window.update() + except NameError: + print("error") + + def ANC(): + global Modeinfo, label, window + global TransportButton, IndoorButton, OutdoorButton, ANCModetxt + var = tkinter.IntVar() + var.set(1) + + ANCModetxt = tkinter.Label(window, text="Modes") + ANCModetxt.configure(font=("Arial", 12, "bold")) + ANCModetxt.place(x=30, y=196) + + buttons = [("Transport", 1, 30), ("Indoor", 2, 120), ("Outdoor", 3, 210)] + + def getAction(data=None): + global image + data = data if data != None else var.get() + if data == 1: + Modeinfo.configure( + text="Targets low-end frequencies like engine and road\n noise for serene journeys and commutes." + ) + image = tkinter.PhotoImage(file="Images//ANC//a3951_img_transport.png") + image = image.subsample(3, 3) + label.configure(image=image) + Headphone.ANC("ANC Transport") + elif data == 2: + Modeinfo.configure( + text="Eliminate voices and mid-frequency noise from\n coffee shops and other inside spaces." + ) + image = tkinter.PhotoImage(file="Images//ANC//a3951_img_indoor.png") + image = image.subsample(3, 3) + label.configure(image=image) + Headphone.ANC("ANC Indoor") + elif data == 3: + Modeinfo.configure( + text="Reduce ambient sound on-the-go for quieter city\n spaces" + ) + image = tkinter.PhotoImage(file="Images//ANC//a3951_img_outdoor.png") + image = image.subsample(3, 3) + label.configure(image=image) + Headphone.ANC("ANC Outdoor") + Modeinfo.place(x=25, y=450) + + getAction() + + TransportButton = tkinter.Radiobutton( + window, + text=buttons[0][0], + variable=var, + command=getAction, + value=buttons[0][1], + indicatoron=0, + width=10, + height=2, + ) + TransportButton.place(x=buttons[0][2], y=230) + + IndoorButton = tkinter.Radiobutton( + window, + text=buttons[1][0], + variable=var, + command=getAction, + value=buttons[1][1], + indicatoron=0, + width=10, + height=2, + ) + IndoorButton.place(x=buttons[1][2], y=230) + + OutdoorButton = tkinter.Radiobutton( + window, + text=buttons[2][0], + variable=var, + command=getAction, + value=buttons[2][1], + indicatoron=0, + width=10, + height=2, + ) + OutdoorButton.place(x=buttons[2][2], y=230) + + def trans(): + global Modeinfo, label, window + forgetANCWegets() + Modeinfo.configure( + text="Stay aware of your sourrounding by allowing\n ambient sound in." + ) + Modeinfo.place(x=45, y=200) + # Modeinfo.configure(x=45, y=200) + image = tkinter.PhotoImage(file="Images//Ambient Sound//a3029_img_trans.png") + image = image.subsample(3, 3) + label.configure(image=image) + label.image = image + Headphone.ANC("Transparency") + text = "Reduce ambient sound on-the-go for quieter city spaces" + + def normal(): + global Modeinfo, label, window + forgetANCWegets() + Modeinfo.configure(text="Turn off noise cancellation and transparency") + Modeinfo.place(x=45, y=200) + image = tkinter.PhotoImage(file="Images//Ambient Sound//a3029_img_normal.png") + image = image.subsample(3, 3) + label.configure(image=image) + label.image = image + Headphone.ANC("Normal") + + ANCImg = tkinter.PhotoImage(file="Images//Modes//a3029_ambient_icon_anc.png") + ANCImg = ANCImg.subsample(2, 2) + page1btn = tkinter.Button( + window, + text="Noise Cancellation", + image=ANCImg, + compound="top", + background="#714fb0", + command=ANC, + height=96, + width=96, + fg="white", + relief="groove", + ) + # page1btn.bind("", lambda _: "break", add=True) + + TransparcyImg = tkinter.PhotoImage( + file="Images//Modes//a3029_ambient_icon_trans.png" + ) + TransparcyImg = TransparcyImg.subsample(2, 2) + page2btn = tkinter.Button( + window, + text="Transparency", + image=TransparcyImg, + compound="top", + background="#4f75b0", + command=trans, + height=96, + width=96, + fg="white", + relief="groove", + ) + # page2btn.bind("", lambda _: "break", add=True) + + NormalImg = tkinter.PhotoImage(file="Images//Modes//a3029_ambient_icon_off.png") + NormalImg = NormalImg.subsample(2, 2) + page3btn = tkinter.Button( + window, + text="Normal", + image=NormalImg, + compound="top", + background="#525a6b", + command=normal, + height=96, + width=96, + fg="white", + relief="groove", + ) + # page3btn.bind("", lambda _: "break", add=True) + + NavLabel = tkinter.Label(text="Ambient Sound") + NavLabel.configure(font=("Arial", 20, "bold")) + NavLabel.place(x=8, y=45) + + page1btn.place(x=5, y=90) + page2btn.place(x=23 + 96, y=90) + page3btn.place(x=232, y=90) + + window.resizable(0, 0) + + def on_closing(): + if Headphone != None: + Headphone.close() + window.destroy() + + window.protocol("WM_DELETE_WINDOW", on_closing) + window.mainloop() +except Exception: + console.print_exception(show_locals=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4ce7bd9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +-i https://pypi.org/simple +black==24.4.2; python_version >= '3.8' +click==8.1.7; python_version >= '3.7' +isort==5.13.2; python_full_version >= '3.8.0' +mypy-extensions==1.0.0; python_version >= '3.5' +packaging==24.0; python_version >= '3.7' +pathspec==0.12.1; python_version >= '3.8' +platformdirs==4.2.2; python_version >= '3.8' +pybluez@ git+https://github.com/pybluez/pybluez.git#egg=pybluez@82cbba8a1ebd4c1e3442dfafd8581d58c50fa39e