From c945e342a45f61cc1c18c2ef437185f92178e57d Mon Sep 17 00:00:00 2001 From: Zach Glenwright Date: Sat, 15 Jan 2022 08:04:34 -0500 Subject: [PATCH 1/5] 1-15-22 - Rewrote Connect/Disconnect Re-wrote connect and disconnect calls to asynchronously attempt multiple connect(s) and disconnect(s) in parallel instead of doing them one at a time. --- NeewerLite-Python.py | 53 +++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/NeewerLite-Python.py b/NeewerLite-Python.py index 1e2e222..b6eb3c8 100644 --- a/NeewerLite-Python.py +++ b/NeewerLite-Python.py @@ -997,12 +997,6 @@ def closeEvent(self, event): threadAction = "quit" # make sure to tell the thread to quit again (if it missed it the first time) time.sleep(2) - loop = asyncio.get_event_loop() - - # THE THREAD HAS TERMINATED, NOW CONTINUE... - self.statusBar.showMessage("Quitting program - unlinking from lights...") - QApplication.processEvents() # force the status bar to update - # Keep in mind, this is broken into 2 separate "for" loops, so we save all the light params FIRST, then try to unlink from them if rememberLightsOnExit == True: printDebugString("You asked NeewerLite-Python to save the last used light parameters on exit, so we will do that now...") @@ -1011,15 +1005,13 @@ def closeEvent(self, event): printDebugString("Saving last used parameters for light #" + str(a + 1) + " (" + str(a + 1) + " of " + str(len(availableLights)) + ")") self.saveLightPrefs(a) + # THE THREAD HAS TERMINATED, NOW CONTINUE... printDebugString("We will now attempt to unlink from the lights...") + self.statusBar.showMessage("Quitting program - unlinking from lights...") + QApplication.processEvents() # force the status bar to update - # TRY TO DISCONNECT EACH LIGHT FROM BLUETOOTH BEFORE QUITTING THE PROGRAM COMPLETELY - for a in range (len(availableLights)): - printDebugString("Attempting to unlink from light #" + str(a + 1) + " (" + str(a + 1) + " of " + str(len(availableLights)) + " lights to unlink)") - self.statusBar.showMessage("Attempting to unlink from light #" + str(a + 1) + " (" + str(a + 1) + " of " + str(len(availableLights)) + " lights to unlink)...") - QApplication.processEvents() # force update to show statusbar progress - - loop.run_until_complete(disconnectFromLight(a)) # disconnect from each light, one at a time + loop = asyncio.get_event_loop() + loop.run_until_complete(parallelAction("disconnect")) # disconnect from all lights in parallel printDebugString("Closing the program NOW") @@ -1393,6 +1385,7 @@ async def disconnectFromLight(selectedLight, updateGUI=True): if not availableLights[selectedLight][1].is_connected: # if the current light is NOT connected, then we're good if updateGUI == False: returnValue = True # if we're in CLI mode, then return False if there is an error disconnecting + mainWindow.setTheTable(["", "", "NOT\nLINKED", "Light disconnected!"], selectedLight) # show the new status in the table printDebugString("Successfully unlinked from light " + str(selectedLight + 1) + " [" + availableLights[selectedLight][0].name + "] " + returnMACname() + " " + availableLights[selectedLight][0].address) except AttributeError: @@ -1553,19 +1546,43 @@ def workerThread(_loop): mainWindow.updateLights() # tell the GUI to update its list of available lights if autoConnectToLights == True: # if we're set to automatically link to the lights on startup, then do it here - for a in range(len(availableLights)): - if threadAction != "quit": # if we're not supposed to quit, then try to connect to the light(s) - threadAction = _loop.run_until_complete(connectToLight(a)) # connect to each light in turn + #for a in range(len(availableLights)): + if threadAction != "quit": # if we're not supposed to quit, then try to connect to the light(s) + _loop.run_until_complete(parallelAction("connect")) # connect to each available light in parallel + + threadAction = "" elif threadAction == "connect": selectedLights = mainWindow.selectedLights() # get the list of currently selected lights - for a in range(len(mainWindow.selectedLights())): # and try to link to each of those lights - threadAction = _loop.run_until_complete(connectToLight(selectedLights[a])) + if threadAction != "quit": # if we're not supposed to quit, then try to connect to the light(s) + _loop.run_until_complete(parallelAction("connect", selectedLights)) # connect to each *selected* light in parallel + + threadAction = "" elif threadAction == "send": threadAction = _loop.run_until_complete(writeToLight()) # write a value to the light(s) - the selectedLights() section is in the write loop itself for responsiveness time.sleep(0.25) +async def parallelAction(theAction, theLights = [-1]): + # SUBMIT A SERIES OF PARALLEL ASYNCIO FUNCTIONS TO RUN ALL IN PARALLEL + parallelFuncs = [] + + if theLights[0] == -1: # if we have no specific lights set, then operate on the entire availableLights range + theLights = [] # clear the selected light list + + for a in range(len(availableLights)): + theLights.append(a) # add all of availableLights to the list + + for a in range(len(theLights)): + if theAction == "connect": # connect to a series of lights + parallelFuncs.append(connectToLight(theLights[a])) + elif theAction == "disconnect": # disconnect from a series of lights + parallelFuncs.append(disconnectFromLight(theLights[a])) + elif theAction == "getInfo": # get the info for a series of lights + pass + + await asyncio.gather(*parallelFuncs) # run the functions in parallel + def processCommands(listToProcess=[]): inStartupMode = False # if we're in startup mode (so report that to the log), start as False initially to be set to True below From 792838a242e78d5add157b5293bb5606c1fe305e Mon Sep 17 00:00:00 2001 From: Zach Glenwright Date: Sun, 16 Jan 2022 06:07:41 -0500 Subject: [PATCH 2/5] 1-16-22 - Better error checking for power/channel info Changed check for power/channel status from light to make *sure* it's looking at the right kind of data to set the table up correctly by verifying the category of data its looking at is 100% correct instead of assuming it is. --- NeewerLite-Python.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NeewerLite-Python.py b/NeewerLite-Python.py index b6eb3c8..3272475 100644 --- a/NeewerLite-Python.py +++ b/NeewerLite-Python.py @@ -1305,7 +1305,7 @@ async def connectToLight(selectedLight, updateGUI=True): return returnValue # once the connection is over, then return either True or False (for CLI) or nothing (for GUI) -async def readNotifyCharacteristic(selectedLight, diagCommand): +async def readNotifyCharacteristic(selectedLight, diagCommand, typeOfData): # clear the global variable before asking the light for info global receivedData receivedData = "" @@ -1321,7 +1321,7 @@ async def readNotifyCharacteristic(selectedLight, diagCommand): except Exception as e: return "" # if there is an error checking the characteristic, just quit out of this routine - if receivedData != "": + if receivedData != "" and receivedData[1] == typeOfData: # if we've received data, and the data returned is the right *kind* of data, then return it break # we found data, so we can stop checking else: await asyncio.sleep(0.25) # wait a little bit of time before checking again @@ -1336,14 +1336,14 @@ async def getLightChannelandPower(selectedLight): global availableLights returnInfo = ["---", "---"] # the information to return to the light - powerInfo = await readNotifyCharacteristic(selectedLight, [120, 133, 0, 253]) + powerInfo = await readNotifyCharacteristic(selectedLight, [120, 133, 0, 253], 2) try: if powerInfo != "" and powerInfo[3] == 1: returnInfo[0] = "ON" # IF THE LIGHT IS ON, THEN ATTEMPT TO READ THE CURRENT CHANNEL - chanInfo = await readNotifyCharacteristic(selectedLight, [120, 132, 0, 252]) + chanInfo = await readNotifyCharacteristic(selectedLight, [120, 132, 0, 252], 1) if chanInfo != "": # if we got a result from the query try: From 02e31edc455ff2a90272613c43da13895b58c1a6 Mon Sep 17 00:00:00 2001 From: Zach Glenwright Date: Sun, 16 Jan 2022 17:52:18 -0500 Subject: [PATCH 3/5] 1-16-22 - More parallelization improvements Like the title says, more parallelization improvements! Connecting and disconnecting is handled all at the same time, instead of one at a time, and the HTTP server now uses those techniques as well as the GUI version. Also, as a solution to #20, NeewerLite-Python now creates a lockfile at launch, and deletes it at exit, to make sure only one copy of NEP is running at one time. This could be refined a bit, into a "do you want to launch a new copy anyway" prompt, but that's not part of it *yet*. --- NeewerLite-Python.py | 84 ++++++++++++++++++++++++++------------------ ui_NeewerLightUI.py | 2 +- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/NeewerLite-Python.py b/NeewerLite-Python.py index 3272475..05b6b0f 100644 --- a/NeewerLite-Python.py +++ b/NeewerLite-Python.py @@ -15,6 +15,8 @@ import os import sys +import tempfile + import argparse import platform # used to determine which OS we're using for MAC address/GUID listing @@ -98,8 +100,23 @@ customKeys = [] # custom keymappings for keyboard shortcuts, set on launch by the prefs file enableTabsOnLaunch = False # whether or not to enable tabs on startup (even with no lights connected) +lockFile = tempfile.gettempdir() + os.sep + "NeewerLite-Python.lock" globalPrefsFile = os.path.dirname(os.path.abspath(sys.argv[0])) + os.sep + "NeewerLite-Python.prefs" # the global preferences file for saving/loading +# FILE LOCKING FOR SINGLE INSTANCE +def singleInstanceLock(): + try: + lf = os.open(lockFile, os.O_WRONLY | os.O_CREAT | os.O_EXCL) + except IOError: + print("You're already running another copy of NeewerLite-Python. Please close that copy first before opening a new one.") + sys.exit(0) # quit out if we're already running an instance of NeewerLite-Python + with os.fdopen(lf, 'w') as lockfile: + lockfile.write(str(os.getpid())) # write the PID of the current running process to the temporary lockfile + +def singleInstanceUnlockandQuit(exitCode): + os.remove(lockFile) # delete the lockfile on exit + sys.exit(exitCode) # quit out, with the specified exitCode + try: # try to load the GUI class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): @@ -1011,7 +1028,7 @@ def closeEvent(self, event): QApplication.processEvents() # force the status bar to update loop = asyncio.get_event_loop() - loop.run_until_complete(parallelAction("disconnect")) # disconnect from all lights in parallel + loop.run_until_complete(parallelAction("disconnect", [-1])) # disconnect from all lights in parallel printDebugString("Closing the program NOW") @@ -1276,13 +1293,14 @@ async def connectToLight(selectedLight, updateGUI=True): isConnected = True # the light is already connected, so mark it as being connected except Exception as e: printDebugString("Error linking to light " + str(selectedLight + 1) + " [" + availableLights[selectedLight][0].name + "] " + returnMACname() + " " + availableLights[selectedLight][0].address) - + if updateGUI == True: mainWindow.setTheTable(["", "", "NOT\nLINKED", "There was an error connecting to the light, trying again (Attempt " + str(currentAttempt + 1) + " of " + str(maxNumOfAttempts) + ")..."], selectedLight) # there was an issue connecting this specific light to Bluetooh, so show that else: returnValue = False # if we're in CLI mode, and there is an error connecting to the light, return False currentAttempt = currentAttempt + 1 + await asyncio.sleep(3) # wait a few seconds before trying again else: return "quit" @@ -1293,8 +1311,7 @@ async def connectToLight(selectedLight, updateGUI=True): printDebugString("Successfully linked to light " + str(selectedLight + 1) + " [" + availableLights[selectedLight][0].name + "] " + returnMACname() + " " + availableLights[selectedLight][0].address) if updateGUI == True: - await getLightChannelandPower(selectedLight) - mainWindow.setTheTable(["", "", "LINKED\n" + availableLights[selectedLight][7][0] + " / ᴄʜ. " + str(availableLights[selectedLight][7][1]), "Waiting to send..."], selectedLight) # if it's successful, show that in the table + mainWindow.setTheTable(["", "", "LINKED\n --- / ᴄʜ. ---", "Waiting to send..."], selectedLight) # if it's successful, show that in the table else: returnValue = True # if we're in CLI mode, and there is no error connecting to the light, return True else: @@ -1339,9 +1356,12 @@ async def getLightChannelandPower(selectedLight): powerInfo = await readNotifyCharacteristic(selectedLight, [120, 133, 0, 253], 2) try: - if powerInfo != "" and powerInfo[3] == 1: - returnInfo[0] = "ON" - + if powerInfo != "": + if powerInfo[3] == 1: + returnInfo[0] = "ON" + elif powerInfo[3] == 2: + returnInfo[0] = "STBY" + # IF THE LIGHT IS ON, THEN ATTEMPT TO READ THE CURRENT CHANNEL chanInfo = await readNotifyCharacteristic(selectedLight, [120, 132, 0, 252], 1) @@ -1350,8 +1370,6 @@ async def getLightChannelandPower(selectedLight): returnInfo[1] = chanInfo[3] # set the current channel to the returned result except IndexError: pass # if we have an index error (the above value doesn't exist), then just return -1 - elif powerInfo != "" and powerInfo[3] == 2: - returnInfo[0] = "STBY" except IndexError: # if we have an IndexError (the information returned isn't blank, but also isn't enough to descipher the status) # then just error out, but print the information that *was* returned for debugging purposes @@ -1548,7 +1566,7 @@ def workerThread(_loop): if autoConnectToLights == True: # if we're set to automatically link to the lights on startup, then do it here #for a in range(len(availableLights)): if threadAction != "quit": # if we're not supposed to quit, then try to connect to the light(s) - _loop.run_until_complete(parallelAction("connect")) # connect to each available light in parallel + _loop.run_until_complete(parallelAction("connect", [-1])) # connect to each available light in parallel threadAction = "" elif threadAction == "connect": @@ -1563,7 +1581,7 @@ def workerThread(_loop): time.sleep(0.25) -async def parallelAction(theAction, theLights = [-1]): +async def parallelAction(theAction, theLights, updateGUI = True): # SUBMIT A SERIES OF PARALLEL ASYNCIO FUNCTIONS TO RUN ALL IN PARALLEL parallelFuncs = [] @@ -1575,12 +1593,10 @@ async def parallelAction(theAction, theLights = [-1]): for a in range(len(theLights)): if theAction == "connect": # connect to a series of lights - parallelFuncs.append(connectToLight(theLights[a])) + parallelFuncs.append(connectToLight(theLights[a], updateGUI)) elif theAction == "disconnect": # disconnect from a series of lights - parallelFuncs.append(disconnectFromLight(theLights[a])) - elif theAction == "getInfo": # get the info for a series of lights - pass - + parallelFuncs.append(disconnectFromLight(theLights[a], updateGUI)) + await asyncio.gather(*parallelFuncs) # run the functions in parallel def processCommands(listToProcess=[]): @@ -1733,14 +1749,13 @@ def processHTMLCommands(paramsList, loop): loop.run_until_complete(findDevices()) # find the lights available to control # try to connect to each light - for a in range(len(availableLights)): - loop.run_until_complete(connectToLight(a, False)) + if autoConnectToLights == True: + loop.run_until_complete(parallelAction("connect", [-1], False)) # try to connect to *all* lights in parallel elif paramsList[0] == "link": # we asked to connect to a specific light selectedLights = returnLightIndexesFromMacAddress(paramsList[1]) if len(selectedLights) > 0: - for a in range(len(selectedLights)): - loop.run_until_complete(connectToLight(selectedLights[a], False)) + loop.run_until_complete(parallelAction("connect", selectedLights, False)) # try to connect to all *selected* lights in parallel else: # we want to write a value to a specific light if paramsList[3] == "CCT": # calculate CCT bytestring calculateByteString(colorMode=paramsList[3], temp=paramsList[4], brightness=paramsList[5]) @@ -1964,7 +1979,7 @@ def writeHTMLSections(self, theSection, errorMsg = ""): footerLinks = footerLinks + "List Currently Available Lights" self.wfile.write(bytes("
" + footerLinks + "
", "utf-8")) - self.wfile.write(bytes("NeewerLite-Python 0.7 by Zach Glenwright
", "utf-8")) + self.wfile.write(bytes("NeewerLite-Python 0.8 by Zach Glenwright
", "utf-8")) self.wfile.write(bytes("", "utf-8")) def formatStringForConsole(theString, maxLength): @@ -2083,7 +2098,9 @@ def loadPrefsFile(globalPrefsFile = ""): enableTabsOnLaunch = bool(int(mainPrefs.enableTabsOnLaunch)) -if __name__ == '__main__': +if __name__ == '__main__': + singleInstanceLock() # make a lockfile if one doesn't exist yet, and quit out if one does + if os.path.exists(globalPrefsFile): loadPrefsFile(globalPrefsFile) # if a preferences file exists, process it and load the preferences else: @@ -2113,15 +2130,14 @@ def loadPrefsFile(globalPrefsFile = ""): webServer.server_close() # DISCONNECT FROM EACH LIGHT BEFORE FINISHING THE PROGRAM - for a in range (0, len(availableLights)): - printDebugString("Attempting to unlink from light #" + str(a + 1) + " (" + str(a + 1) + " of " + str(len(availableLights)) + " lights to unlink)") - loop.run_until_complete(disconnectFromLight(a)) # disconnect from each light, one at a time - + printDebugString("Attempting to unlink from lights...") + loop.run_until_complete(parallelAction("disconnect", [-1], False)) # disconnect from all lights in parallel + printDebugString("Closing the program NOW") - sys.exit(0) + singleInstanceUnlockandQuit(0) # delete the lock file and quit out if cmdReturn[0] == "LIST": - print("NeewerLite-Python 0.7 by Zach Glenwright") + print("NeewerLite-Python 0.8 by Zach Glenwright") print("Searching for nearby Neewer lights...") loop.run_until_complete(findDevices()) @@ -2163,7 +2179,7 @@ def loadPrefsFile(globalPrefsFile = ""): else: print("We did not find any Neewer lights on the last search.") - sys.exit(0) # show the list, then quit out to the command line + singleInstanceUnlockandQuit(0) # delete the lock file and quit out printDebugString(" > Launch GUI: " + str(cmdReturn[0])) printDebugString(" > Show Debug Strings on Console: " + str(cmdReturn[1])) @@ -2216,7 +2232,7 @@ def loadPrefsFile(globalPrefsFile = ""): workerThread.start() ret = app.exec_() - sys.exit( ret ) + singleInstanceUnlockandQuit(ret) # delete the lock file and quit out except NameError: pass # same as above - we could not load the GUI, but we have already sorted error messages else: @@ -2273,7 +2289,7 @@ def loadPrefsFile(globalPrefsFile = ""): numOfAttempts = numOfAttempts + 1 else: printDebugString("Error connecting to light " + str(maxNumOfAttempts) + " times - quitting out") - sys.exit(1) + singleInstanceUnlockandQuit(1) # delete the lock file and quit out isFinished = False numOfAttempts = 1 @@ -2288,7 +2304,7 @@ def loadPrefsFile(globalPrefsFile = ""): numOfAttempts = numOfAttempts + 1 else: printDebugString("Error writing to light " + str(maxNumOfAttempts) + " times - quitting out") - sys.exit(1) + singleInstanceUnlockandQuit(1) # delete the lock file and quit out isFinished = False numOfAttempts = 1 @@ -2303,9 +2319,9 @@ def loadPrefsFile(globalPrefsFile = ""): numOfAttempts = numOfAttempts + 1 else: printDebugString("Error disconnecting from light " + str(maxNumOfAttempts) + " times - quitting out") - sys.exit(1) + singleInstanceUnlockandQuit(1) # delete the lock file and quit out else: printDebugString("-------------------------------------------------------------------------------------") printDebugString(" > CLI >> Calculated bytestring:" + updateStatus()) - sys.exit(0) \ No newline at end of file + singleInstanceUnlockandQuit(0) # delete the lock file and quit out \ No newline at end of file diff --git a/ui_NeewerLightUI.py b/ui_NeewerLightUI.py index 194afdc..216b592 100644 --- a/ui_NeewerLightUI.py +++ b/ui_NeewerLightUI.py @@ -11,7 +11,7 @@ def setupUi(self, MainWindow): mainFont.setWeight(75) MainWindow.setFixedSize(590, 521) # the main window should be this size at launch, and no bigger - MainWindow.setWindowTitle("NeewerLite-Python 0.7 by Zach Glenwright") + MainWindow.setWindowTitle("NeewerLite-Python 0.8 by Zach Glenwright") self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") From 3d97a24f2ba7da1c2c37f65e136d911ae961b749 Mon Sep 17 00:00:00 2001 From: Zach Glenwright Date: Mon, 17 Jan 2022 15:48:28 -0500 Subject: [PATCH 4/5] 1-17-22 - Refined Lockfile handling Added --force_instance command to force NeewerLite-Python to launch even if a lockfile already exists (#20) - Added dialog to the GUI to ask the same question as above ("There is already an instance running, are you sure you want to force-open a new one") - Tweaked the channel/power return function slightly (#21) --- NeewerLite-Python.py | 65 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/NeewerLite-Python.py b/NeewerLite-Python.py index 05b6b0f..8cc0377 100644 --- a/NeewerLite-Python.py +++ b/NeewerLite-Python.py @@ -57,7 +57,7 @@ try: from PySide2.QtCore import Qt from PySide2.QtGui import QLinearGradient, QColor, QKeySequence - from PySide2.QtWidgets import QApplication, QMainWindow, QTableWidgetItem, QShortcut + from PySide2.QtWidgets import QApplication, QMainWindow, QTableWidgetItem, QShortcut, QMessageBox except Exception as e: importError = 1 # log that we can't find PySide2 @@ -101,22 +101,37 @@ enableTabsOnLaunch = False # whether or not to enable tabs on startup (even with no lights connected) lockFile = tempfile.gettempdir() + os.sep + "NeewerLite-Python.lock" +anotherInstance = False # whether or not we're using a new instance (for the Singleton check) globalPrefsFile = os.path.dirname(os.path.abspath(sys.argv[0])) + os.sep + "NeewerLite-Python.prefs" # the global preferences file for saving/loading # FILE LOCKING FOR SINGLE INSTANCE def singleInstanceLock(): + global anotherInstance + try: - lf = os.open(lockFile, os.O_WRONLY | os.O_CREAT | os.O_EXCL) - except IOError: - print("You're already running another copy of NeewerLite-Python. Please close that copy first before opening a new one.") - sys.exit(0) # quit out if we're already running an instance of NeewerLite-Python - with os.fdopen(lf, 'w') as lockfile: - lockfile.write(str(os.getpid())) # write the PID of the current running process to the temporary lockfile + lf = os.open(lockFile, os.O_WRONLY | os.O_CREAT | os.O_EXCL) # try to get a file spec to lock the "running" instance + with os.fdopen(lf, 'w') as lockfile: + lockfile.write(str(os.getpid())) # write the PID of the current running process to the temporary lockfile + except IOError: # if we had an error acquiring the file descriptor, the file most likely already exists. + anotherInstance = True + def singleInstanceUnlockandQuit(exitCode): - os.remove(lockFile) # delete the lockfile on exit + try: + os.remove(lockFile) # try to delete the lockfile on exit + except FileNotFoundError: # if another process deleted it, then just error out + printDebugString("Lockfile not found in temp directory, so we're going to skip deleting it!") + sys.exit(exitCode) # quit out, with the specified exitCode +def doAnotherInstanceCheck(): + if anotherInstance == True: # if we're running a 2nd instance, but we shouldn't be + print("You're already running another instance of NeewerLite-Python.") + print("Please close that copy first before opening a new one.") + print() + print("To force opening a new instance, add --force_instance to the command line.") + sys.exit(1) + try: # try to load the GUI class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): @@ -1338,7 +1353,7 @@ async def readNotifyCharacteristic(selectedLight, diagCommand, typeOfData): except Exception as e: return "" # if there is an error checking the characteristic, just quit out of this routine - if receivedData != "" and receivedData[1] == typeOfData: # if we've received data, and the data returned is the right *kind* of data, then return it + if receivedData != "" and len(receivedData) > 1 and receivedData[1] == typeOfData: # if we've received data, and the data returned is the right *kind* of data, then return it break # we found data, so we can stop checking else: await asyncio.sleep(0.25) # wait a little bit of time before checking again @@ -1623,7 +1638,7 @@ def processCommands(listToProcess=[]): # TO CLEAN UP THE ARGUMENT LIST AND ENSURE THE PARSER CAN STILL RUN WHEN INVALID ARGUMENTS ARE PRESENT if inStartupMode == True: acceptable_arguments = ["--http", "--cli", "--silent", "--light", "--mode", "--temp", "--hue", - "--sat", "--bri", "--intensity", "--scene", "--animation", "--help", "--off", "--on", "--list"] + "--sat", "--bri", "--intensity", "--scene", "--animation", "--help", "--off", "--on", "--list", "--force_instance"] else: # if we're doing HTTP processing, we don't need the http, cli, silent and help flags, so toss 'em acceptable_arguments = ["--light", "--mode", "--temp", "--hue", "--sat", "--bri", "--intensity", "--scene", "--animation", "--list", "--discover", "--link", "--off", "--on"] @@ -1668,6 +1683,7 @@ def processCommands(listToProcess=[]): parser.add_argument("--http", action="store_true", help="Use an HTTP server to send commands to Neewer lights using a web browser") parser.add_argument("--silent", action="store_false", help="Don't show any debug information in the console") parser.add_argument("--cli", action="store_false", help="Don't show the GUI at all, just send command to one light and quit") + parser.add_argument("--force_instance", action="store_false", help="Force a new instance of NeewerLite-Python if another one is already running") # HTML SERVER SPECIFIC PARAMETERS if inStartupMode == False: @@ -1686,6 +1702,10 @@ def processCommands(listToProcess=[]): parser.add_argument("--scene", "--animation", default="1", help="[DEFAULT: 1] (ANM or SCENE mode) The animation (1-9) to use in Scene mode") args = parser.parse_args(listToProcess) + if args.force_instance == False: # if this value is True, then don't do anything + global anotherInstance + anotherInstance = False # change the global to False to allow new instances + if args.silent == True: if inStartupMode == True: if args.list != True: # if we're not looking for lights using --list, then print line @@ -2113,8 +2133,13 @@ def loadPrefsFile(globalPrefsFile = ""): cmdReturn = processCommands() printDebug = cmdReturn[1] # if we use the --quiet option, then don't show debug strings in the console + if cmdReturn[0] == False: # if we're trying to load the CLI, make sure we aren't already running another version of it + doAnotherInstanceCheck() # check to see if another instance is running, and if it is, then error out and quit + # START HTTP SERVER HERE AND SIT IN THIS LOOP UNTIL THE END if cmdReturn[0] == "HTTP": + doAnotherInstanceCheck() # check to see if another instance is running, and if it is, then error out and quit + webServer = ThreadingHTTPServer(("", 8080), NLPythonServer) try: @@ -2137,6 +2162,8 @@ def loadPrefsFile(globalPrefsFile = ""): singleInstanceUnlockandQuit(0) # delete the lock file and quit out if cmdReturn[0] == "LIST": + doAnotherInstanceCheck() # check to see if another instance is running, and if it is, then error out and quit + print("NeewerLite-Python 0.8 by Zach Glenwright") print("Searching for nearby Neewer lights...") loop.run_until_complete(findDevices()) @@ -2179,7 +2206,7 @@ def loadPrefsFile(globalPrefsFile = ""): else: print("We did not find any Neewer lights on the last search.") - singleInstanceUnlockandQuit(0) # delete the lock file and quit out + sys.exit(0) # we shouldn't need to do anything with the lock file here, so just quit out printDebugString(" > Launch GUI: " + str(cmdReturn[0])) printDebugString(" > Show Debug Strings on Console: " + str(cmdReturn[1])) @@ -2214,6 +2241,22 @@ def loadPrefsFile(globalPrefsFile = ""): if importError == 0: try: # try to load the GUI app = QApplication(sys.argv) + + if anotherInstance == True: # different than the CLI handling, the GUI needs to show a dialog box asking to quit or launch + errDlg = QMessageBox() + errDlg.setWindowTitle("Another Instance Running!") + errDlg.setTextFormat(Qt.TextFormat.RichText) + errDlg.setText("There is another instance of NeewerLite-Python already running. Please close out of that instance first before trying to launch a new instance of the program.

If you are positive that you don't have any other instances running and you want to launch a new one anyway, click Launch New Instance below. Otherwise click Quit to quit out.") + errDlg.addButton("Launch New Instance", QMessageBox.ButtonRole.YesRole) + errDlg.addButton("Quit", QMessageBox.ButtonRole.NoRole) + errDlg.setDefaultButton(QMessageBox.No) + errDlg.setIcon(QMessageBox.Warning) + + button = errDlg.exec_() + + if button == 1: # if we clicked the Quit button, then quit out + sys.exit(1) + mainWindow = MainWindow() # SET UP GUI BASED ON COMMAND LINE ARGUMENTS From f71d75f9d5b3cdc60491056aff643bbfa05f1413 Mon Sep 17 00:00:00 2001 From: Zach Glenwright Date: Tue, 18 Jan 2022 10:17:46 -0500 Subject: [PATCH 5/5] 1-18-22 - Refined power/channel info - Refined channel/power check (from workerThread) by implementing a list (currently only NEEWER-RGB176) that don't feed that data back - so we don't need to check them - Refined setTheTable as well, implementing a check to not update the table if the data being submitted is the same as the data already in the table - Added another second of delay before trying to re-link a light for the 2nd time --- NeewerLite-Python.py | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/NeewerLite-Python.py b/NeewerLite-Python.py index 8cc0377..9ed21f7 100644 --- a/NeewerLite-Python.py +++ b/NeewerLite-Python.py @@ -819,15 +819,20 @@ def setTheTable(self, infoArray, rowToChange = -1): else: currentRow = rowToChange # change data for the specified row + # THIS SECTION BELOW LIMITS UPDATING THE TABLE **ONLY** IF THE DATA SUPPLIED IS DIFFERENT THAN IT WAS ORIGINALLY if infoArray[0] != "": # the name of the light - self.lightTable.setItem(currentRow, 0, QTableWidgetItem(infoArray[0])) + if rowToChange == -1 or (rowToChange != -1 and infoArray[0] != self.returnTableInfo(rowToChange, 0)): + self.lightTable.setItem(currentRow, 0, QTableWidgetItem(infoArray[0])) if infoArray[1] != "": # the MAC address of the light - self.lightTable.setItem(currentRow, 1, QTableWidgetItem(infoArray[1])) + if rowToChange == -1 or (rowToChange != -1 and infoArray[1] != self.returnTableInfo(rowToChange, 1)): + self.lightTable.setItem(currentRow, 1, QTableWidgetItem(infoArray[1])) if infoArray[2] != "": # the Linked status of the light - self.lightTable.setItem(currentRow, 2, QTableWidgetItem(infoArray[2])) - self.lightTable.item(currentRow, 2).setTextAlignment(Qt.AlignCenter) # align the light status info to be center-justified + if rowToChange == -1 or (rowToChange != -1 and infoArray[2] != self.returnTableInfo(rowToChange, 2)): + self.lightTable.setItem(currentRow, 2, QTableWidgetItem(infoArray[2])) + self.lightTable.item(currentRow, 2).setTextAlignment(Qt.AlignCenter) # align the light status info to be center-justified if infoArray[3] != "": # the current status message of the light - self.lightTable.setItem(currentRow, 3, QTableWidgetItem(infoArray[3])) + if rowToChange == -1 or (rowToChange != -1 and infoArray[2] != self.returnTableInfo(rowToChange, 3)): + self.lightTable.setItem(currentRow, 3, QTableWidgetItem(infoArray[3])) self.lightTable.resizeRowsToContents() @@ -1299,10 +1304,9 @@ async def connectToLight(selectedLight, updateGUI=True): while isConnected == False and currentAttempt <= maxNumOfAttempts: if threadAction != "quit": - printDebugString("Attempting to link to light " + str(selectedLight + 1) + " [" + availableLights[selectedLight][0].name + "] " + returnMACname() + " " + availableLights[selectedLight][0].address + " (Attempt " + str(currentAttempt) + " of " + str(maxNumOfAttempts) + ")") - try: if not availableLights[selectedLight][1].is_connected: # if the current device isn't linked to Bluetooth + printDebugString("Attempting to link to light " + str(selectedLight + 1) + " [" + availableLights[selectedLight][0].name + "] " + returnMACname() + " " + availableLights[selectedLight][0].address + " (Attempt " + str(currentAttempt) + " of " + str(maxNumOfAttempts) + ")") isConnected = await availableLights[selectedLight][1].connect() # try connecting it (and return the connection status) else: isConnected = True # the light is already connected, so mark it as being connected @@ -1315,7 +1319,7 @@ async def connectToLight(selectedLight, updateGUI=True): returnValue = False # if we're in CLI mode, and there is an error connecting to the light, return False currentAttempt = currentAttempt + 1 - await asyncio.sleep(3) # wait a few seconds before trying again + await asyncio.sleep(4) # wait a few seconds before trying to link to the light again else: return "quit" @@ -1323,10 +1327,10 @@ async def connectToLight(selectedLight, updateGUI=True): return "quit" else: if isConnected == True: - printDebugString("Successfully linked to light " + str(selectedLight + 1) + " [" + availableLights[selectedLight][0].name + "] " + returnMACname() + " " + availableLights[selectedLight][0].address) + printDebugString("Successful link on light " + str(selectedLight + 1) + " [" + availableLights[selectedLight][0].name + "] " + returnMACname() + " " + availableLights[selectedLight][0].address) if updateGUI == True: - mainWindow.setTheTable(["", "", "LINKED\n --- / ᴄʜ. ---", "Waiting to send..."], selectedLight) # if it's successful, show that in the table + mainWindow.setTheTable(["", "", "LINKED", "Waiting to send..."], selectedLight) # if it's successful, show that in the table else: returnValue = True # if we're in CLI mode, and there is no error connecting to the light, return True else: @@ -1353,8 +1357,13 @@ async def readNotifyCharacteristic(selectedLight, diagCommand, typeOfData): except Exception as e: return "" # if there is an error checking the characteristic, just quit out of this routine - if receivedData != "" and len(receivedData) > 1 and receivedData[1] == typeOfData: # if we've received data, and the data returned is the right *kind* of data, then return it - break # we found data, so we can stop checking + if receivedData != "": # if the recieved data is populated + if len(receivedData) > 1: # if we have enough elements to get a status from + if receivedData[1] == typeOfData: # if the data returned is the correct *kind* of data + break # stop scanning for data + else: # if we have a list, but it doesn't have a payload in it (the light didn't supply enough data) + receivedData = "---" # then just re-set recievedData to the default string + break # stop scanning for data else: await asyncio.sleep(0.25) # wait a little bit of time before checking again try: @@ -1545,6 +1554,9 @@ async def connectToOneLight(MACAddress): def workerThread(_loop): global threadAction + # A LIST OF LIGHTS THAT DON'T SEND POWER/CHANNEL STATUS + lightsToNotCheckPower = ["NEEWER-RGB176"] + if findLightsOnStartup == True: # if we're set to find lights at startup, then automatically set the thread to discovery mode threadAction = "discover" @@ -1565,8 +1577,11 @@ def workerThread(_loop): mainWindow.setTheTable(["", "", "NOT\nLINKED", "Light disconnected!"], a) # show the new status in the table availableLights[a][1] = "" # clear the Bleak object else: - _loop.run_until_complete(getLightChannelandPower(a)) - mainWindow.setTheTable(["", "", "LINKED\n" + availableLights[a][7][0] + " / ᴄʜ. " + str(availableLights[a][7][1]), ""], a) + if not availableLights[a][0].name in lightsToNotCheckPower: # if the name of the current light is not in the list to skip checking + _loop.run_until_complete(getLightChannelandPower(a)) # then check the power and light status of that light + mainWindow.setTheTable(["", "", "LINKED\n" + availableLights[a][7][0] + " / ᴄʜ. " + str(availableLights[a][7][1]), ""], a) + else: # if the light we're scanning doesn't supply power or channel status, then just show "LINKED" + mainWindow.setTheTable(["", "", "LINKED", ""], a) if threadAction == "quit": printDebugString("Stopping the background thread")