diff --git a/.github/workflows/system-monitor-windows.yml b/.github/workflows/system-monitor-windows.yml
index df2bb648..78131fd9 100644
--- a/.github/workflows/system-monitor-windows.yml
+++ b/.github/workflows/system-monitor-windows.yml
@@ -34,6 +34,9 @@ jobs:
echo "Using theme ${{ matrix.theme }}"
(Get-Content config.yaml) -replace "^ THEME.*$"," THEME: ${{ matrix.theme }}" | Set-Content config.yaml
+ # For tests do not use LibreHardwareMonitor as it needs admin rights
+ (Get-Content config.yaml) -replace "^ HW_SENSORS.*$"," HW_SENSORS: PYTHON" | Set-Content config.yaml
+
- name: Run system monitor for 20 seconds
run: |
Start-Process -NoNewWindow python3 main.py -RedirectStandardOutput output.log -RedirectStandardError error.log
diff --git a/.gitignore b/.gitignore
index bd66cae9..b9fa221a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -136,3 +136,6 @@ dmypy.json
# Simulated display capture
screencap.png
+tmp
+
+external/LibreHardwareMonitor/LibreHardwareMonitorLib.sys
diff --git a/config.yaml b/config.yaml
index 289c602a..5de91b60 100644
--- a/config.yaml
+++ b/config.yaml
@@ -2,35 +2,53 @@
config:
# Configuration values to set up basic communication
# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux...
+ # Use AUTO for COM port auto-discovery (may not work on every setup)
# COM_PORT: "/dev/ttyACM0"
# COM_PORT: "COM3"
COM_PORT: "AUTO"
# Theme to use (located in res/themes)
- # THEME: Terminal
- # THEME: Landscape6Grid
- # THEME: Cyberpunk
- # THEME: bash-dark-green
+ # Available themes:
+ # - 3.5inchTheme2
+ # - Terminal
+ # - Landscape6Grid
+ # - Cyberpunk
+ # - bash-dark-green
THEME: 3.5inchTheme2
- # Network metrics
- # Put the interface name or let it blank if the card does not exist
+ # Hardware sensors reading
+ # Choose the appropriate method for reading your hardware sensors:
+ # - PYTHON use Python libraries (psutils, GPUtil...) to read hardware sensors (supports all OS but not all HW)
+ # - LHM use LibreHardwareMonitor library to read hardware sensors (Windows only - NEEDS ADMIN RIGHTS)
+ # - STUB use fake random data instead of real hardware sensors
+ # - AUTO use the best method based on your OS: Windows OS will use LHM, other OS will use Python libraries
+ HW_SENSORS: AUTO
+
+ # Network interfaces
# Linux/MacOS interfaces are named "eth0", "wlan0", "wlp1s0", "enp2s0"...
# For Windows use the interfaces pretty name: "Ethernet 2", "Wi-Fi", ...
- ETH: "eth0" # Ethernet Card
- WLO: "wlan0" # Wi-Fi Card
+ # Leave the fields empty if the card does not exist on your setup
+ ETH: "" # Ethernet Card
+ WLO: "" # Wi-Fi Card
display:
- # Display resolution in portrait orientation
- # Do not use this setting to rotate display! Display orientation is managed by themes
- DISPLAY_WIDTH: 320
- DISPLAY_HEIGHT: 480
+ # Display revision: A or B (for "flagship" version, use B) or SIMU for simulated LCD (image written in screencap.png)
+ # To identify your revision: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions
+ REVISION: A
# Display Brightness
# Set this as the desired %, 0 being completely dark and 100 being max brightness
- # Warning: screen can get very hot at high brightness!
+ # Warning: revision A display can get hot at high brightness!
BRIGHTNESS: 20
- # Display revision: A or B (for "flagship" version, use B) or SIMU for simulated LCD (image written in screencap.png)
- # To identify your revision: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions
- REVISION: A
+ # Display reverse: true/false
+ # Set to true to reverse display orientation (landscape <-> reverse landscape, portrait <-> reverse portrait)
+ # Note: Display basic orientation (portrait or landscape) is defined by the theme you have selected
+ DISPLAY_REVERSE: false
+
+ # Display resolution in portrait orientation
+ # Do not use this setting to rotate display! Use DISPLAY_REVERSE
+ DISPLAY_WIDTH: 320 # Do not change unless you have a good reason
+ DISPLAY_HEIGHT: 480 # Do not change unless you have a good reason
+
+
diff --git a/external/LibreHardwareMonitor/HidSharp.dll b/external/LibreHardwareMonitor/HidSharp.dll
new file mode 100644
index 00000000..895c318a
Binary files /dev/null and b/external/LibreHardwareMonitor/HidSharp.dll differ
diff --git a/external/LibreHardwareMonitor/LICENSE b/external/LibreHardwareMonitor/LICENSE
new file mode 100644
index 00000000..a612ad98
--- /dev/null
+++ b/external/LibreHardwareMonitor/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/external/LibreHardwareMonitor/LibreHardwareMonitorLib.dll b/external/LibreHardwareMonitor/LibreHardwareMonitorLib.dll
new file mode 100644
index 00000000..e5a098be
Binary files /dev/null and b/external/LibreHardwareMonitor/LibreHardwareMonitorLib.dll differ
diff --git a/external/LibreHardwareMonitor/test_librehardwaremonitor.py b/external/LibreHardwareMonitor/test_librehardwaremonitor.py
new file mode 100644
index 00000000..3788b9d9
--- /dev/null
+++ b/external/LibreHardwareMonitor/test_librehardwaremonitor.py
@@ -0,0 +1,56 @@
+# Use this file to display all hardware & sensors available from LibreHardwareMonitor on your computer
+# Windows only - needs administrative rights
+import ctypes
+import os
+import sys
+
+import clr # Clr is from pythonnet package. Do not install clr package
+from win32api import *
+
+if ctypes.windll.shell32.IsUserAnAdmin() == 0:
+ print("Program is not running as administrator. Please run again with admin rights.")
+ try:
+ sys.exit(0)
+ except:
+ os._exit(0)
+
+# noinspection PyUnresolvedReferences
+clr.AddReference(os.getcwd() + '\\LibreHardwareMonitorLib.dll')
+# noinspection PyUnresolvedReferences
+clr.AddReference(os.getcwd() + '\\HidSharp.dll')
+# noinspection PyUnresolvedReferences
+from LibreHardwareMonitor import Hardware
+
+File_information = GetFileVersionInfo(os.getcwd() + '\\LibreHardwareMonitorLib.dll', "\\")
+ms_file_version = File_information['FileVersionMS']
+ls_file_version = File_information['FileVersionLS']
+print("Found LibreHardwareMonitorLib %s" % ".".join([str(HIWORD(ms_file_version)), str(LOWORD(ms_file_version)),
+ str(HIWORD(ls_file_version)),
+ str(LOWORD(ls_file_version))]))
+
+File_information = GetFileVersionInfo(os.getcwd() + '\\HidSharp.dll', "\\")
+ms_file_version = File_information['FileVersionMS']
+ls_file_version = File_information['FileVersionLS']
+print("Found HidSharp %s" % ".".join([str(HIWORD(ms_file_version)), str(LOWORD(ms_file_version)),
+ str(HIWORD(ls_file_version)),
+ str(LOWORD(ls_file_version))]))
+
+handle = Hardware.Computer()
+handle.IsCpuEnabled = True
+handle.IsGpuEnabled = True
+handle.IsMemoryEnabled = True
+handle.IsMotherboardEnabled = True
+handle.IsControllerEnabled = True
+handle.IsNetworkEnabled = True
+handle.IsStorageEnabled = True
+handle.Open()
+
+for hw in handle.Hardware:
+ print("%s | %s | %s" % (hw.HardwareType, hw.Name, hw.Identifier))
+ hw.Update()
+
+ for sensor in hw.Sensors:
+ print(" %s | %s | %s" % (sensor.SensorType, sensor.Name, sensor.Value))
+ print("----------------------------------------------------")
+
+handle.Close()
diff --git a/library/config.py b/library/config.py
index a97ac385..37d973df 100644
--- a/library/config.py
+++ b/library/config.py
@@ -6,7 +6,6 @@
from library.log import logger
-PNIC_BEFORE = ""
def load_yaml(configfile):
with open(configfile, "r") as stream:
diff --git a/library/display.py b/library/display.py
index 1cdf6be2..07d83352 100644
--- a/library/display.py
+++ b/library/display.py
@@ -1,8 +1,8 @@
from library import config
-from library.lcd_comm import Orientation
-from library.lcd_comm_rev_a import LcdCommRevA
-from library.lcd_comm_rev_b import LcdCommRevB
-from library.lcd_simulated import LcdSimulated
+from library.lcd.lcd_comm import Orientation
+from library.lcd.lcd_comm_rev_a import LcdCommRevA
+from library.lcd.lcd_comm_rev_b import LcdCommRevB
+from library.lcd.lcd_simulated import LcdSimulated
from library.log import logger
THEME_DATA = config.THEME_DATA
@@ -18,12 +18,22 @@ def _get_full_path(path, name):
def _get_theme_orientation() -> Orientation:
if THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'portrait':
- return Orientation.PORTRAIT
+ if CONFIG_DATA["display"].get("DISPLAY_REVERSE", False):
+ return Orientation.REVERSE_PORTRAIT
+ else:
+ return Orientation.PORTRAIT
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'landscape':
- return Orientation.LANDSCAPE
+ if CONFIG_DATA["display"].get("DISPLAY_REVERSE", False):
+ return Orientation.REVERSE_LANDSCAPE
+ else:
+ return Orientation.LANDSCAPE
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_portrait':
+ logger.warn("'reverse_portrait' is deprecated as DISPLAY_ORIENTATION value in the theme."
+ "Use 'portrait' instead, and use DISPLAY_REVERSE in config.yaml to reverse orientation.")
return Orientation.REVERSE_PORTRAIT
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_landscape':
+ logger.warn("'reverse_landscape' is deprecated as DISPLAY_ORIENTATION value in the theme."
+ "Use 'landscape' instead, and use DISPLAY_REVERSE in config.yaml to reverse orientation.")
return Orientation.REVERSE_LANDSCAPE
else:
logger.warning("Orientation '", THEME_DATA["display"]["DISPLAY_ORIENTATION"], "' unknown, using portrait")
diff --git a/library/lcd_comm.py b/library/lcd/lcd_comm.py
similarity index 100%
rename from library/lcd_comm.py
rename to library/lcd/lcd_comm.py
diff --git a/library/lcd_comm_rev_a.py b/library/lcd/lcd_comm_rev_a.py
similarity index 99%
rename from library/lcd_comm_rev_a.py
rename to library/lcd/lcd_comm_rev_a.py
index 47340741..e7fa5615 100644
--- a/library/lcd_comm_rev_a.py
+++ b/library/lcd/lcd_comm_rev_a.py
@@ -3,7 +3,7 @@
from serial.tools.list_ports import comports
-from library.lcd_comm import *
+from library.lcd.lcd_comm import *
from library.log import logger
diff --git a/library/lcd_comm_rev_b.py b/library/lcd/lcd_comm_rev_b.py
similarity index 99%
rename from library/lcd_comm_rev_b.py
rename to library/lcd/lcd_comm_rev_b.py
index c706122f..8bb6d621 100644
--- a/library/lcd_comm_rev_b.py
+++ b/library/lcd/lcd_comm_rev_b.py
@@ -2,7 +2,7 @@
from serial.tools.list_ports import comports
-from library.lcd_comm import *
+from library.lcd.lcd_comm import *
from library.log import logger
diff --git a/library/lcd_simulated.py b/library/lcd/lcd_simulated.py
similarity index 81%
rename from library/lcd_simulated.py
rename to library/lcd/lcd_simulated.py
index a1d5405a..13131236 100644
--- a/library/lcd_simulated.py
+++ b/library/lcd/lcd_simulated.py
@@ -2,9 +2,10 @@
import shutil
from http.server import BaseHTTPRequestHandler, HTTPServer
-from library.lcd_comm import *
+from library.lcd.lcd_comm import *
SCREENSHOT_FILE = "screencap.png"
+WEBSERVER_PORT = 5678
# This webserver offer a blank page displaying simulated screen with auto-refresh
@@ -17,14 +18,14 @@ def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
- self.wfile.write(bytes("
", "utf-8"))
+ self.wfile.write(bytes("
", "utf-8"))
self.wfile.write(bytes("", "utf-8"))
- elif self.path.startswith("/screencap.png"):
+ elif self.path.startswith("/" + SCREENSHOT_FILE):
imgfile = open(SCREENSHOT_FILE, 'rb').read()
mimetype = mimetypes.MimeTypes().guess_type(SCREENSHOT_FILE)[0]
self.send_response(200)
@@ -33,7 +34,7 @@ def do_GET(self):
self.wfile.write(imgfile)
-# Simulated display: write on a screencap.png file instead of serial port
+# Simulated display: write on a file instead of serial port
class LcdSimulated(LcdComm):
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480,
update_queue: queue.Queue = None):
@@ -43,10 +44,12 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_hei
shutil.copyfile("tmp", SCREENSHOT_FILE)
self.orientation = Orientation.PORTRAIT
- webServer = HTTPServer(("localhost", 5678), SimulatedLcdWebServer)
- logger.debug("To see your simulated screen, open http://%s:%s" % ("localhost", 5678))
-
- threading.Thread(target=webServer.serve_forever).start()
+ try:
+ webServer = HTTPServer(("localhost", WEBSERVER_PORT), SimulatedLcdWebServer)
+ logger.debug("To see your simulated screen, open http://%s:%d in a browser" % ("localhost", WEBSERVER_PORT))
+ threading.Thread(target=webServer.serve_forever).start()
+ except OSError:
+ logger.error("Error starting webserver! An instance might already be running on port %d." % WEBSERVER_PORT)
@staticmethod
def auto_detect_com_port():
diff --git a/library/scheduler.py b/library/scheduler.py
index 4c2a0925..ad668e78 100644
--- a/library/scheduler.py
+++ b/library/scheduler.py
@@ -95,18 +95,10 @@ def CPUTemperature():
@async_job("GPU_Stats")
@schedule(timedelta(seconds=THEME_DATA['STATS']['GPU'].get("INTERVAL", None)).total_seconds())
-def GpuNvidiaStats():
+def GpuStats():
""" Refresh the GPU Stats """
# logger.debug("Refresh GPU Stats")
- stats.GpuNvidia.stats()
-
-
-@async_job("GPU_Stats")
-@schedule(timedelta(seconds=THEME_DATA['STATS']['GPU'].get("INTERVAL", None)).total_seconds())
-def GpuAmdStats():
- """ Refresh the GPU Stats """
- # logger.debug("Refresh GPU Stats")
- stats.GpuAmd.stats()
+ stats.Gpu.stats()
@async_job("Memory_Stats")
diff --git a/library/sensors/sensors.py b/library/sensors/sensors.py
new file mode 100644
index 00000000..86a1f07c
--- /dev/null
+++ b/library/sensors/sensors.py
@@ -0,0 +1,91 @@
+# This file defines all supported hardware in virtual classes and their abstract methods to access sensors
+# To be overriden by child sensors classes
+
+from abc import ABC, abstractmethod
+from typing import Tuple
+
+
+class Cpu(ABC):
+ @staticmethod
+ @abstractmethod
+ def percentage(interval: float) -> float:
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def frequency() -> float:
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def is_temperature_available() -> bool:
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def temperature() -> float:
+ pass
+
+
+class Gpu(ABC):
+ @staticmethod
+ @abstractmethod
+ def stats() -> Tuple[float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / temp (°C)
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def is_available() -> bool:
+ pass
+
+
+class Memory(ABC):
+ @staticmethod
+ @abstractmethod
+ def swap_percent() -> float:
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def virtual_percent() -> float:
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def virtual_used() -> int: # In bytes
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def virtual_free() -> int: # In bytes
+ pass
+
+
+class Disk(ABC):
+ @staticmethod
+ @abstractmethod
+ def disk_usage_percent() -> float:
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def disk_used() -> int: # In bytes
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def disk_free() -> int: # In bytes
+ pass
+
+
+class Net(ABC):
+ @staticmethod
+ @abstractmethod
+ def stats(if_name, interval) -> Tuple[
+ int, int, int, int]: # up rate (B/s), uploaded (B), dl rate (B/s), downloaded (B)
+ pass
diff --git a/library/sensors/sensors_librehardwaremonitor.py b/library/sensors/sensors_librehardwaremonitor.py
new file mode 100644
index 00000000..c40d1cf8
--- /dev/null
+++ b/library/sensors/sensors_librehardwaremonitor.py
@@ -0,0 +1,298 @@
+# This file will use LibreHardwareMonitor.dll library to get hardware sensors
+# Some metrics are still fetched by psutil when not available on LibreHardwareMonitor
+# For Windows platforms only
+
+import ctypes
+import math
+import os
+import sys
+from statistics import mean
+from typing import Tuple
+
+import clr # Clr is from pythonnet package. Do not install clr package
+import psutil
+from win32api import *
+
+import library.sensors.sensors as sensors
+from library.log import logger
+
+# Import LibreHardwareMonitor dll to Python
+lhm_dll = os.getcwd() + '\\external\\LibreHardwareMonitor\\LibreHardwareMonitorLib.dll'
+# noinspection PyUnresolvedReferences
+clr.AddReference(lhm_dll)
+# noinspection PyUnresolvedReferences
+clr.AddReference(os.getcwd() + '\\external\\LibreHardwareMonitor\\HidSharp.dll')
+# noinspection PyUnresolvedReferences
+from LibreHardwareMonitor import Hardware
+
+File_information = GetFileVersionInfo(lhm_dll, "\\")
+
+ms_file_version = File_information['FileVersionMS']
+ls_file_version = File_information['FileVersionLS']
+
+logger.debug("Found LibreHardwareMonitorLib %s" % ".".join([str(HIWORD(ms_file_version)), str(LOWORD(ms_file_version)),
+ str(HIWORD(ls_file_version)),
+ str(LOWORD(ls_file_version))]))
+
+if ctypes.windll.shell32.IsUserAnAdmin() == 0:
+ logger.error(
+ "Program is not running as administrator. Please run with admin rights or choose another HW_SENSORS option in "
+ "config.yaml")
+ try:
+ sys.exit(0)
+ except:
+ os._exit(0)
+
+handle = Hardware.Computer()
+handle.IsCpuEnabled = True
+handle.IsGpuEnabled = True
+handle.IsMemoryEnabled = True
+handle.IsMotherboardEnabled = False
+handle.IsControllerEnabled = False
+handle.IsNetworkEnabled = True
+handle.IsStorageEnabled = True
+handle.Open()
+for hardware in handle.Hardware:
+ if hardware.HardwareType == Hardware.HardwareType.Cpu:
+ logger.info("Found CPU: %s" % hardware.Name)
+ elif hardware.HardwareType == Hardware.HardwareType.Memory:
+ logger.info("Found Memory: %s" % hardware.Name)
+ elif hardware.HardwareType == Hardware.HardwareType.GpuNvidia:
+ logger.info("Found Nvidia GPU: %s" % hardware.Name)
+ elif hardware.HardwareType == Hardware.HardwareType.GpuAmd:
+ logger.info("Found AMD GPU: %s" % hardware.Name)
+ elif hardware.HardwareType == Hardware.HardwareType.GpuIntel:
+ logger.info("Found Intel GPU: %s" % hardware.Name)
+ elif hardware.HardwareType == Hardware.HardwareType.Storage:
+ logger.info("Found Storage: %s" % hardware.Name)
+ elif hardware.HardwareType == Hardware.HardwareType.Network:
+ logger.info("Found Network interface: %s" % hardware.Name)
+
+
+def get_hw_and_update(hwtype: Hardware.HardwareType) -> Hardware.Hardware:
+ for hardware in handle.Hardware:
+ if hardware.HardwareType == hwtype:
+ hardware.Update()
+ return hardware
+ return None
+
+
+def get_net_interface_and_update(if_name: str) -> Hardware.Hardware:
+ for hardware in handle.Hardware:
+ if hardware.HardwareType == Hardware.HardwareType.Network and hardware.Name == if_name:
+ hardware.Update()
+ return hardware
+
+ logger.warning("Network interface '%s' not found. Check names in config.yaml." % if_name)
+ return None
+
+
+class Cpu(sensors.Cpu):
+ @staticmethod
+ def percentage(interval: float) -> float:
+ cpu = get_hw_and_update(Hardware.HardwareType.Cpu)
+ for sensor in cpu.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith("CPU Total"):
+ return float(sensor.Value)
+
+ logger.error("CPU load cannot be read")
+ return math.nan
+
+ @staticmethod
+ def frequency() -> float:
+ frequencies = []
+ cpu = get_hw_and_update(Hardware.HardwareType.Cpu)
+ for sensor in cpu.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Clock:
+ # Keep only real core clocks, ignore effective core clocks
+ if "Core #" in str(sensor.Name) and "Effective" not in str(sensor.Name):
+ frequencies.append(float(sensor.Value))
+ # Take mean of all core clock as "CPU clock"
+ return mean(frequencies)
+
+ @staticmethod
+ def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg:
+ # Get this data from psutil because it is not available from LibreHardwareMonitor
+ return psutil.getloadavg()
+
+ @staticmethod
+ def is_temperature_available() -> bool:
+ cpu = get_hw_and_update(Hardware.HardwareType.Cpu)
+ for sensor in cpu.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Temperature:
+ if str(sensor.Name).startswith("Core") or str(sensor.Name).startswith("CPU Package"):
+ return True
+
+ return False
+
+ @staticmethod
+ def temperature() -> float:
+ cpu = get_hw_and_update(Hardware.HardwareType.Cpu)
+ # By default, the average temperature of all CPU cores will be used
+ for sensor in cpu.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith("Core Average"):
+ return float(sensor.Value)
+ # If not available, the max core temperature will be used
+ for sensor in cpu.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith("Core Max"):
+ return float(sensor.Value)
+ # If not available, the CPU Package temperature (usually same as max core temperature) will be used
+ for sensor in cpu.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith("CPU Package"):
+ return float(sensor.Value)
+ # Otherwise any sensor named "Core..." will be used
+ for sensor in cpu.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith("Core"):
+ return float(sensor.Value)
+
+ return math.nan
+
+
+class Gpu(sensors.Gpu):
+ @staticmethod
+ def stats() -> Tuple[float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / temp (°C)
+ gpu_to_use = get_hw_and_update(Hardware.HardwareType.GpuAmd)
+ if gpu_to_use is None:
+ gpu_to_use = get_hw_and_update(Hardware.HardwareType.GpuNvidia)
+ if gpu_to_use is None:
+ gpu_to_use = get_hw_and_update(Hardware.HardwareType.GpuIntel)
+ if gpu_to_use is None:
+ # GPU not supported
+ return math.nan, math.nan, math.nan, math.nan
+
+ load = math.nan
+ used_mem = math.nan
+ total_mem = math.nan
+ temp = math.nan
+
+ for sensor in gpu_to_use.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith("GPU Core"):
+ load = float(sensor.Value)
+ elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith("GPU Memory Used"):
+ used_mem = float(sensor.Value)
+ elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith("GPU Memory Total"):
+ total_mem = float(sensor.Value)
+ elif sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith("GPU Core"):
+ temp = float(sensor.Value)
+
+ return load, (used_mem / total_mem * 100.0), used_mem, temp
+
+ @staticmethod
+ def is_available() -> bool:
+ found_amd = (get_hw_and_update(Hardware.HardwareType.GpuAmd) is not None)
+ found_nvidia = (get_hw_and_update(Hardware.HardwareType.GpuNvidia) is not None)
+ found_intel = (get_hw_and_update(Hardware.HardwareType.GpuIntel) is not None)
+
+ if found_amd and (found_nvidia or found_intel) or (found_nvidia and found_intel):
+ logger.warning(
+ "Found multiple GPUs on your system. Will use dedicated GPU (AMD/Nvidia) for stats if possible.")
+
+ return found_amd or found_nvidia or found_intel
+
+
+class Memory(sensors.Memory):
+ @staticmethod
+ def swap_percent() -> float:
+ memory = get_hw_and_update(Hardware.HardwareType.Memory)
+
+ virtual_mem_used = math.nan
+ mem_used = math.nan
+ virtual_mem_available = math.nan
+ mem_available = math.nan
+
+ # Get virtual / physical memory stats
+ for sensor in memory.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith("Virtual Memory Used"):
+ virtual_mem_used = int(sensor.Value)
+ elif sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith("Memory Used"):
+ mem_used = int(sensor.Value)
+ elif sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith(
+ "Virtual Memory Available"):
+ virtual_mem_available = int(sensor.Value)
+ elif sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith("Memory Available"):
+ mem_available = int(sensor.Value)
+
+ # Compute swap stats from virtual / physical memory stats
+ swap_used = virtual_mem_used - mem_used
+ swap_available = virtual_mem_available - mem_available
+ swap_total = swap_used + swap_available
+
+ return swap_used / swap_total * 100.0
+
+ @staticmethod
+ def virtual_percent() -> float:
+ memory = get_hw_and_update(Hardware.HardwareType.Memory)
+ for sensor in memory.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith("Memory"):
+ return float(sensor.Value)
+
+ return math.nan
+
+ @staticmethod
+ def virtual_used() -> int: # In bytes
+ memory = get_hw_and_update(Hardware.HardwareType.Memory)
+ for sensor in memory.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith("Memory Used"):
+ return int(sensor.Value * 1000000000.0)
+
+ return 0
+
+ @staticmethod
+ def virtual_free() -> int: # In bytes
+ memory = get_hw_and_update(Hardware.HardwareType.Memory)
+ for sensor in memory.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith("Memory Available"):
+ return int(sensor.Value * 1000000000.0)
+
+ return 0
+
+
+class Disk(sensors.Disk):
+ @staticmethod
+ def disk_usage_percent() -> float:
+ disk = get_hw_and_update(Hardware.HardwareType.Storage)
+ for sensor in disk.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith("Used Space"):
+ return float(sensor.Value)
+
+ # Get this data from psutil if it is not available from LibreHardwareMonitor
+ return psutil.disk_usage("/").percent
+
+ @staticmethod
+ def disk_used() -> int: # In bytes
+ # Get this data from psutil because it is not available from LibreHardwareMonitor
+ return psutil.disk_usage("/").used
+
+ @staticmethod
+ def disk_free() -> int: # In bytes
+ # Get this data from psutil because it is not available from LibreHardwareMonitor
+ return psutil.disk_usage("/").free
+
+
+class Net(sensors.Net):
+ @staticmethod
+ def stats(if_name, interval) -> Tuple[
+ int, int, int, int]: # up rate (B/s), uploaded (B), dl rate (B/s), downloaded (B)
+
+ upload_rate = 0
+ uploaded = 0
+ download_rate = 0
+ downloaded = 0
+
+ if if_name != "":
+ net_if = get_net_interface_and_update(if_name)
+ if net_if is not None:
+ for sensor in net_if.Sensors:
+ if sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith("Data Uploaded"):
+ uploaded = int(sensor.Value * 1000000000.0)
+ elif sensor.SensorType == Hardware.SensorType.Data and str(sensor.Name).startswith(
+ "Data Downloaded"):
+ downloaded = int(sensor.Value * 1000000000.0)
+ elif sensor.SensorType == Hardware.SensorType.Throughput and str(sensor.Name).startswith(
+ "Upload Speed"):
+ upload_rate = int(sensor.Value)
+ elif sensor.SensorType == Hardware.SensorType.Throughput and str(sensor.Name).startswith(
+ "Download Speed"):
+ download_rate = int(sensor.Value)
+
+ return upload_rate, uploaded, download_rate, downloaded
diff --git a/library/sensors/sensors_python.py b/library/sensors/sensors_python.py
new file mode 100644
index 00000000..478179e1
--- /dev/null
+++ b/library/sensors/sensors_python.py
@@ -0,0 +1,280 @@
+# This file will use Python libraries (psutil, GPUtil, etc.) to get hardware sensors
+# For all platforms (Linux, Windows, macOS) but not all HW is supported
+
+import math
+from typing import Tuple
+from enum import IntEnum, auto
+
+import library.sensors.sensors as sensors
+from library.log import logger
+
+# CPU & disk sensors
+import psutil
+
+# Nvidia GPU
+import GPUtil
+
+# AMD GPU on Linux
+try:
+ import pyamdgpuinfo
+except:
+ pyamdgpuinfo = None
+
+# AMD GPU on Windows
+try:
+ import pyadl
+except:
+ pyadl = None
+
+PNIC_BEFORE = {}
+
+
+class GpuType(IntEnum):
+ UNSUPPORTED = auto()
+ AMD = auto()
+ NVIDIA = auto()
+
+
+DETECTED_GPU = GpuType.UNSUPPORTED
+
+
+class Cpu(sensors.Cpu):
+ @staticmethod
+ def percentage(interval: float) -> float:
+ return psutil.cpu_percent(interval=interval)
+
+ @staticmethod
+ def frequency() -> float:
+ return psutil.cpu_freq().current
+
+ @staticmethod
+ def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg:
+ return psutil.getloadavg()
+
+ @staticmethod
+ def is_temperature_available() -> bool:
+ try:
+ sensors_temps = psutil.sensors_temperatures()
+ if 'coretemp' in sensors_temps or 'k10temp' in sensors_temps or 'cpu_thermal' in sensors_temps:
+ return True
+ else:
+ return False
+ except AttributeError:
+ # sensors_temperatures may not be available at all
+ return False
+
+ @staticmethod
+ def temperature() -> float:
+ cpu_temp = 0
+ sensors_temps = psutil.sensors_temperatures()
+ if 'coretemp' in sensors_temps:
+ # Intel CPU
+ cpu_temp = sensors_temps['coretemp'][0].current
+ elif 'k10temp' in sensors_temps:
+ # AMD CPU
+ cpu_temp = sensors_temps['k10temp'][0].current
+ elif 'cpu_thermal' in sensors_temps:
+ # ARM CPU
+ cpu_temp = sensors_temps['cpu_thermal'][0].current
+ return cpu_temp
+
+
+class Gpu(sensors.Gpu):
+ @staticmethod
+ def stats() -> Tuple[float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / temp (°C)
+ global DETECTED_GPU
+ if DETECTED_GPU == GpuType.AMD:
+ return GpuAmd.stats()
+ elif DETECTED_GPU == GpuType.NVIDIA:
+ return GpuNvidia.stats()
+ else:
+ return math.nan, math.nan, math.nan, math.nan
+
+ @staticmethod
+ def is_available() -> bool:
+ global DETECTED_GPU
+ if GpuAmd.is_available():
+ logger.info("Detected AMD GPU(s)")
+ DETECTED_GPU = GpuType.AMD
+ elif GpuNvidia.is_available():
+ logger.info("Detected Nvidia GPU(s)")
+ DETECTED_GPU = GpuType.NVIDIA
+ else:
+ logger.warning("Your GPU is not supported yet")
+ DETECTED_GPU = GpuType.UNSUPPORTED
+
+ return DETECTED_GPU != GpuType.UNSUPPORTED
+
+
+class GpuNvidia(sensors.Gpu):
+ @staticmethod
+ def stats() -> Tuple[float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / temp (°C)
+ # Unlike other sensors, Nvidia GPU with GPUtil pulls in all the stats at once
+ nvidia_gpus = GPUtil.getGPUs()
+
+ try:
+ memory_used_all = [item.memoryUsed for item in nvidia_gpus]
+ memory_used_mb = sum(memory_used_all) / len(memory_used_all)
+ except:
+ memory_used_mb = math.nan
+
+ try:
+ memory_total_all = [item.memoryTotal for item in nvidia_gpus]
+ memory_total_mb = sum(memory_total_all) / len(memory_total_all)
+ memory_percentage = (memory_used_mb / memory_total_mb) * 100
+ except:
+ memory_percentage = math.nan
+
+ try:
+ load_all = [item.load for item in nvidia_gpus]
+ load = (sum(load_all) / len(load_all)) * 100
+ except:
+ load = math.nan
+
+ try:
+ temperature_all = [item.temperature for item in nvidia_gpus]
+ temperature = sum(temperature_all) / len(temperature_all)
+ except:
+ temperature = math.nan
+
+ return load, memory_percentage, memory_used_mb, temperature
+
+ @staticmethod
+ def is_available() -> bool:
+ try:
+ return len(GPUtil.getGPUs()) > 0
+ except:
+ return False
+
+
+class GpuAmd(sensors.Gpu):
+ @staticmethod
+ def stats() -> Tuple[float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / temp (°C)
+ if pyamdgpuinfo:
+ # Unlike other sensors, AMD GPU with pyamdgpuinfo pulls in all the stats at once
+ i = 0
+ amd_gpus = []
+ while i < pyamdgpuinfo.detect_gpus():
+ amd_gpus.append(pyamdgpuinfo.get_gpu(i))
+ i = i + 1
+
+ try:
+ memory_used_all = [item.query_vram_usage() for item in amd_gpus]
+ memory_used_bytes = sum(memory_used_all) / len(memory_used_all)
+ memory_used = memory_used_bytes / 1000000
+ except:
+ memory_used_bytes = math.nan
+ memory_used = math.nan
+
+ try:
+ memory_total_all = [item.memory_info["vram_size"] for item in amd_gpus]
+ memory_total_bytes = sum(memory_total_all) / len(memory_total_all)
+ memory_percentage = (memory_used_bytes / memory_total_bytes) * 100
+ except:
+ memory_percentage = math.nan
+
+ try:
+ load_all = [item.query_load() for item in amd_gpus]
+ load = (sum(load_all) / len(load_all)) * 100
+ except:
+ load = math.nan
+
+ try:
+ temperature_all = [item.query_temperature() for item in amd_gpus]
+ temperature = sum(temperature_all) / len(temperature_all)
+ except:
+ temperature = math.nan
+
+ return load, memory_percentage, memory_used, temperature
+ elif pyadl:
+ amd_gpus = pyadl.ADLManager.getInstance().getDevices()
+
+ try:
+ load_all = [item.getCurrentUsage() for item in amd_gpus]
+ load = (sum(load_all) / len(load_all))
+ except:
+ load = math.nan
+
+ try:
+ temperature_all = [item.getCurrentTemperature() for item in amd_gpus]
+ temperature = sum(temperature_all) / len(temperature_all)
+ except:
+ temperature = math.nan
+
+ # Memory absolute (M) and relative (%) usage not supported by pyadl
+ return load, math.nan, math.nan, temperature
+
+ @staticmethod
+ def is_available() -> bool:
+ try:
+ if pyamdgpuinfo and pyamdgpuinfo.detect_gpus() > 0:
+ return True
+ elif pyadl and len(pyadl.ADLManager.getInstance().getDevices()) > 0:
+ return True
+ else:
+ return False
+ except:
+ return False
+
+
+class Memory(sensors.Memory):
+ @staticmethod
+ def swap_percent() -> float:
+ return psutil.swap_memory().percent
+
+ @staticmethod
+ def virtual_percent() -> float:
+ return psutil.virtual_memory().percent
+
+ @staticmethod
+ def virtual_used() -> int: # In bytes
+ return psutil.virtual_memory().used
+
+ @staticmethod
+ def virtual_free() -> int: # In bytes
+ return psutil.virtual_memory().free
+
+
+class Disk(sensors.Disk):
+ @staticmethod
+ def disk_usage_percent() -> float:
+ return psutil.disk_usage("/").percent
+
+ @staticmethod
+ def disk_used() -> int: # In bytes
+ return psutil.disk_usage("/").used
+
+ @staticmethod
+ def disk_free() -> int: # In bytes
+ return psutil.disk_usage("/").free
+
+
+class Net(sensors.Net):
+ @staticmethod
+ def stats(if_name, interval) -> Tuple[
+ int, int, int, int]: # up rate (B/s), uploaded (B), dl rate (B/s), downloaded (B)
+ global PNIC_BEFORE
+ # Get current counters
+ pnic_after = psutil.net_io_counters(pernic=True)
+
+ upload_rate = 0
+ uploaded = 0
+ download_rate = 0
+ downloaded = 0
+
+ if if_name != "":
+ if if_name in pnic_after:
+ try:
+ upload_rate = (pnic_after[if_name].bytes_sent - PNIC_BEFORE[if_name].bytes_sent) / interval
+ uploaded = pnic_after[if_name].bytes_sent
+ download_rate = (pnic_after[if_name].bytes_recv - PNIC_BEFORE[if_name].bytes_recv) / interval
+ downloaded = pnic_after[if_name].bytes_recv
+ except:
+ # Interface might not be in PNIC_BEFORE for now
+ pass
+
+ PNIC_BEFORE.update({if_name: pnic_after[if_name]})
+ else:
+ logger.warning("Network interface '%s' not found. Check names in config.yaml." % if_name)
+
+ return upload_rate, uploaded, download_rate, downloaded
diff --git a/library/sensors/sensors_stub.py b/library/sensors/sensors_stub.py
new file mode 100644
index 00000000..66a2b358
--- /dev/null
+++ b/library/sensors/sensors_stub.py
@@ -0,0 +1,79 @@
+# This file will use randomly generated data instead of real hardware sensors
+# For all platforms (Linux, Windows, macOS)
+
+import random
+from typing import Tuple
+
+import library.sensors.sensors as sensors
+
+
+class Cpu(sensors.Cpu):
+ @staticmethod
+ def percentage(interval: float) -> float:
+ return random.uniform(0, 100)
+
+ @staticmethod
+ def frequency() -> float:
+ return random.uniform(800, 3400)
+
+ @staticmethod
+ def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg:
+ return random.uniform(0, 100), random.uniform(0, 100), random.uniform(0, 100)
+
+ @staticmethod
+ def is_temperature_available() -> bool:
+ return True
+
+ @staticmethod
+ def temperature() -> float:
+ return random.uniform(30, 90)
+
+
+class Gpu(sensors.Gpu):
+ @staticmethod
+ def stats() -> Tuple[float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / temp (°C)
+ return random.uniform(0, 100), random.uniform(0, 100), random.uniform(300, 16000), random.uniform(30, 90)
+
+ @staticmethod
+ def is_available() -> bool:
+ return True
+
+
+class Memory(sensors.Memory):
+ @staticmethod
+ def swap_percent() -> float:
+ return random.uniform(0, 100)
+
+ @staticmethod
+ def virtual_percent() -> float:
+ return random.uniform(0, 100)
+
+ @staticmethod
+ def virtual_used() -> int: # In bytes
+ return random.randint(300000000, 16000000000)
+
+ @staticmethod
+ def virtual_free() -> int: # In bytes
+ return random.randint(300000000, 16000000000)
+
+
+class Disk(sensors.Disk):
+ @staticmethod
+ def disk_usage_percent() -> float:
+ return random.uniform(0, 100)
+
+ @staticmethod
+ def disk_used() -> int: # In bytes
+ return random.randint(1000000000, 2000000000000)
+
+ @staticmethod
+ def disk_free() -> int: # In bytes
+ return random.randint(1000000000, 2000000000000)
+
+
+class Net(sensors.Net):
+ @staticmethod
+ def stats(if_name, interval) -> Tuple[
+ int, int, int, int]: # up rate (B/s), uploaded (B), dl rate (B/s), downloaded (B)
+ return random.randint(1000000, 999000000), random.randint(1000000, 999000000), random.randint(
+ 1000000, 999000000), random.randint(1000000, 999000000)
diff --git a/library/stats.py b/library/stats.py
index 94ae0db7..95197da2 100644
--- a/library/stats.py
+++ b/library/stats.py
@@ -1,23 +1,11 @@
+import datetime
import math
+import os
+import platform
+import sys
-import GPUtil
-import psutil
from psutil._common import bytes2human
-import datetime
-
-# AMD GPU on Linux
-try:
- import pyamdgpuinfo
-except:
- pyamdgpuinfo = None
-
-# AMD GPU on Windows
-try:
- import pyadl
-except:
- pyadl = None
-
import library.config as config
from library.display import display
from library.log import logger
@@ -26,6 +14,26 @@
CONFIG_DATA = config.CONFIG_DATA
ETH_CARD = CONFIG_DATA["config"]["ETH"]
WLO_CARD = CONFIG_DATA["config"]["WLO"]
+HW_SENSORS = CONFIG_DATA["config"]["HW_SENSORS"]
+
+if HW_SENSORS == "PYTHON":
+ import library.sensors.sensors_python as sensors
+elif HW_SENSORS == "LHM":
+ import library.sensors.sensors_librehardwaremonitor as sensors
+elif HW_SENSORS == "STUB":
+ import library.sensors.sensors_stub as sensors
+elif HW_SENSORS == "AUTO":
+ if platform.system() == 'Windows':
+ import library.sensors.sensors_librehardwaremonitor as sensors
+ else:
+ import library.sensors.sensors_python as sensors
+else:
+ logger.error("Unsupported SENSORS value in config.yaml")
+ try:
+ sys.exit(0)
+ except:
+ os._exit(0)
+
def get_full_path(path, name):
if name:
@@ -37,7 +45,7 @@ def get_full_path(path, name):
class CPU:
@staticmethod
def percentage():
- cpu_percentage = psutil.cpu_percent(interval=THEME_DATA['STATS']['CPU']['PERCENTAGE'].get("INTERVAL", None))
+ cpu_percentage = sensors.Cpu.percentage(interval=THEME_DATA['STATS']['CPU']['PERCENTAGE'].get("INTERVAL", None))
# logger.debug(f"CPU Percentage: {cpu_percentage}")
if THEME_DATA['STATS']['CPU']['PERCENTAGE']['TEXT'].get("SHOW", False):
@@ -75,11 +83,11 @@ def percentage():
@staticmethod
def frequency():
- cpu_freq = psutil.cpu_freq()
+ cpu_freq = sensors.Cpu.frequency()
if THEME_DATA['STATS']['CPU']['FREQUENCY']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=str(f'{int(cpu_freq.current) / 1000:.2f}') + " GHz",
+ text=str(f'{int(cpu_freq) / 1000:.2f}') + " GHz",
x=THEME_DATA['STATS']['CPU']['FREQUENCY']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['CPU']['FREQUENCY']['TEXT'].get("Y", 0),
font=THEME_DATA['STATS']['CPU']['FREQUENCY']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
@@ -94,7 +102,7 @@ def frequency():
@staticmethod
def load():
- cpu_load = psutil.getloadavg()
+ cpu_load = sensors.Cpu.load()
# logger.debug(f"CPU Load: ({cpu_load[0]},{cpu_load[1]},{cpu_load[2]})")
if THEME_DATA['STATS']['CPU']['LOAD']['ONE']['TEXT'].get("SHOW", False):
@@ -147,29 +155,11 @@ def load():
@staticmethod
def is_temperature_available():
- try:
- sensors_temps = psutil.sensors_temperatures()
- if 'coretemp' in sensors_temps or 'k10temp' in sensors_temps or 'cpu_thermal' in sensors_temps:
- return True
- else:
- return False
- except AttributeError:
- # sensors_temperatures may not be available at all
- return False
+ return sensors.Cpu.is_temperature_available()
@staticmethod
def temperature():
- cpu_temp = 0
- sensors_temps = psutil.sensors_temperatures()
- if 'coretemp' in sensors_temps:
- # Intel CPU
- cpu_temp = sensors_temps['coretemp'][0].current
- elif 'k10temp' in sensors_temps:
- # AMD CPU
- cpu_temp = sensors_temps['k10temp'][0].current
- elif 'cpu_thermal' in sensors_temps:
- # ARM CPU
- cpu_temp = sensors_temps['cpu_thermal'][0].current
+ cpu_temp = sensors.Cpu.temperature()
if THEME_DATA['STATS']['CPU']['TEMPERATURE']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
@@ -186,7 +176,6 @@ def temperature():
THEME_DATA['STATS']['CPU']['TEMPERATURE']['TEXT'].get("BACKGROUND_IMAGE",
None))
)
- # TODO: Built in function for *nix in psutil, for Windows can use WMI or a third party library
def display_gpu_stats(load, memory_percentage, memory_used_mb, temperature):
@@ -296,115 +285,21 @@ def display_gpu_stats(load, memory_percentage, memory_used_mb, temperature):
pass
-class GpuNvidia:
+class Gpu:
@staticmethod
def stats():
- # Unlike the CPU, the GPU pulls in all the stats at once
- nvidia_gpus = GPUtil.getGPUs()
-
- try:
- memory_used_all = [item.memoryUsed for item in nvidia_gpus]
- memory_used_mb = sum(memory_used_all) / len(memory_used_all)
- except:
- memory_used_mb = math.nan
-
- try:
- memory_total_all = [item.memoryTotal for item in nvidia_gpus]
- memory_total_mb = sum(memory_total_all) / len(memory_total_all)
- memory_percentage = (memory_used_mb / memory_total_mb) * 100
- except:
- memory_percentage = math.nan
-
- try:
- load_all = [item.load for item in nvidia_gpus]
- load = (sum(load_all) / len(load_all)) * 100
- except:
- load = math.nan
-
- try:
- temperature_all = [item.temperature for item in nvidia_gpus]
- temperature = sum(temperature_all) / len(temperature_all)
- except:
- temperature = math.nan
-
+ load, memory_percentage, memory_used_mb, temperature = sensors.Gpu.stats()
display_gpu_stats(load, memory_percentage, memory_used_mb, temperature)
@staticmethod
def is_available():
- return len(GPUtil.getGPUs()) > 0
-
-
-class GpuAmd:
- @staticmethod
- def stats():
- # Unlike the CPU, the GPU pulls in all the stats at once
- if pyamdgpuinfo:
- i = 0
- amd_gpus = []
- while i < pyamdgpuinfo.detect_gpus():
- amd_gpus.append(pyamdgpuinfo.get_gpu(i))
- i = i + 1
-
- try:
- memory_used_all = [item.query_vram_usage() for item in amd_gpus]
- memory_used_bytes = sum(memory_used_all) / len(memory_used_all)
- memory_used = memory_used_bytes / 1000000
- except:
- memory_used_bytes = math.nan
- memory_used = math.nan
-
- try:
- memory_total_all = [item.memory_info["vram_size"] for item in amd_gpus]
- memory_total_bytes = sum(memory_total_all) / len(memory_total_all)
- memory_percentage = (memory_used_bytes / memory_total_bytes) * 100
- except:
- memory_percentage = math.nan
-
- try:
- load_all = [item.query_load() for item in amd_gpus]
- load = (sum(load_all) / len(load_all)) * 100
- except:
- load = math.nan
-
- try:
- temperature_all = [item.query_temperature() for item in amd_gpus]
- temperature = sum(temperature_all) / len(temperature_all)
- except:
- temperature = math.nan
-
- display_gpu_stats(load, memory_percentage, memory_used, temperature)
- elif pyadl:
- amd_gpus = pyadl.ADLManager.getInstance().getDevices()
-
- try:
- load_all = [item.getCurrentUsage() for item in amd_gpus]
- load = (sum(load_all) / len(load_all))
- except:
- load = math.nan
-
- try:
- temperature_all = [item.getCurrentTemperature() for item in amd_gpus]
- temperature = sum(temperature_all) / len(temperature_all)
- except:
- temperature = math.nan
-
- # Memory absolute (M) and relative (%) usage not supported by pyadl
- display_gpu_stats(load, math.nan, math.nan, temperature)
-
- @staticmethod
- def is_available():
- if pyamdgpuinfo and pyamdgpuinfo.detect_gpus() > 0:
- return True
- elif pyadl and len(pyadl.ADLManager.getInstance().getDevices()) > 0:
- return True
- else:
- return False
+ return sensors.Gpu.is_available()
class Memory:
@staticmethod
def stats():
- swap_percent = psutil.swap_memory().percent
+ swap_percent = sensors.Memory.swap_percent()
if THEME_DATA['STATS']['MEMORY']['SWAP']['GRAPH'].get("SHOW", False):
display.lcd.DisplayProgressBar(
@@ -424,7 +319,7 @@ def stats():
None))
)
- virtual_percent = psutil.virtual_memory().percent
+ virtual_percent = sensors.Memory.virtual_percent()
if THEME_DATA['STATS']['MEMORY']['VIRTUAL']['GRAPH'].get("SHOW", False):
display.lcd.DisplayProgressBar(
@@ -460,7 +355,7 @@ def stats():
"BACKGROUND_IMAGE", None))
)
- virtual_used = psutil.virtual_memory().used
+ virtual_used = sensors.Memory.virtual_used()
if THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get("SHOW", False):
display.lcd.DisplayText(
@@ -468,29 +363,29 @@ def stats():
x=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get("X", 0),
y=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get("Y", 0),
font=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get("FONT",
- "roboto-mono/RobotoMono-Regular.ttf"),
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get("FONT_COLOR", (0, 0, 0)),
background_color=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get("BACKGROUND_COLOR",
- (255, 255, 255)),
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
THEME_DATA['STATS']['MEMORY']['VIRTUAL']['USED'].get(
"BACKGROUND_IMAGE", None))
)
- virtual_used = psutil.virtual_memory().free
+ virtual_free = sensors.Memory.virtual_free()
if THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{int(virtual_used / 1000000):>5} M",
+ text=f"{int(virtual_free / 1000000):>5} M",
x=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get("X", 0),
y=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get("Y", 0),
font=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get("FONT",
- "roboto-mono/RobotoMono-Regular.ttf"),
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get("FONT_COLOR", (0, 0, 0)),
background_color=THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get("BACKGROUND_COLOR",
- (255, 255, 255)),
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
THEME_DATA['STATS']['MEMORY']['VIRTUAL']['FREE'].get(
"BACKGROUND_IMAGE", None))
@@ -500,15 +395,15 @@ def stats():
class Disk:
@staticmethod
def stats():
- disk_usage = psutil.disk_usage("/")
-
+ used = sensors.Disk.disk_used()
+ free = sensors.Disk.disk_free()
if THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("SHOW", False):
display.lcd.DisplayProgressBar(
x=THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("X", 0),
y=THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("Y", 0),
width=THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("WIDTH", 0),
height=THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("HEIGHT", 0),
- value=int(disk_usage.percent),
+ value=int(sensors.Disk.disk_usage_percent()),
min_value=THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("MIN_VALUE", 0),
max_value=THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("MAX_VALUE", 100),
bar_color=THEME_DATA['STATS']['DISK']['USED']['GRAPH'].get("BAR_COLOR", (0, 0, 0)),
@@ -521,7 +416,7 @@ def stats():
if THEME_DATA['STATS']['DISK']['USED']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{int(disk_usage.used / 1000000000):>5} G",
+ text=f"{int(used / 1000000000):>5} G",
x=THEME_DATA['STATS']['DISK']['USED']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['DISK']['USED']['TEXT'].get("Y", 0),
font=THEME_DATA['STATS']['DISK']['USED']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
@@ -535,21 +430,24 @@ def stats():
if THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{int(disk_usage.percent):>3}%",
+ text=f"{int(sensors.Disk.disk_usage_percent()):>3}%",
x=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("X", 0),
y=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['DISK']['USED']['PERCENT_TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
if THEME_DATA['STATS']['DISK']['TOTAL']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{int(disk_usage.total / 1000000000):>5} G",
+ text=f"{int((free + used) / 1000000000):>5} G",
x=THEME_DATA['STATS']['DISK']['TOTAL']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['DISK']['TOTAL']['TEXT'].get("Y", 0),
font=THEME_DATA['STATS']['DISK']['TOTAL']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
@@ -563,7 +461,7 @@ def stats():
if THEME_DATA['STATS']['DISK']['FREE']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{int(disk_usage.free / 1000000000):>5} G",
+ text=f"{int(free / 1000000000):>5} G",
x=THEME_DATA['STATS']['DISK']['FREE']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['DISK']['FREE']['TEXT'].get("Y", 0),
font=THEME_DATA['STATS']['DISK']['FREE']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
@@ -579,165 +477,165 @@ def stats():
class Net:
@staticmethod
def stats():
- pnic_after = psutil.net_io_counters(pernic=True)
- if config.PNIC_BEFORE:
- pnic_before = config.PNIC_BEFORE
- else:
- pnic_before = pnic_after
-
- if WLO_CARD in pnic_after:
- upload_wlo = pnic_after[WLO_CARD].bytes_sent - pnic_before[WLO_CARD].bytes_sent
- upload_wlo_text = f"{bytes2human(upload_wlo)}/s"
- uploaded_wlo = pnic_after[WLO_CARD].bytes_sent
- uploaded_wlo_text = f"{bytes2human(uploaded_wlo)}"
- download_wlo = pnic_after[WLO_CARD].bytes_recv - pnic_before[WLO_CARD].bytes_recv
- download_wlo_text = f"{bytes2human(download_wlo)}/s"
- downloaded_wlo = pnic_after[WLO_CARD].bytes_recv
- downloaded_wlo_text = f"{bytes2human(downloaded_wlo)}"
- else:
- upload_wlo_text = f"N/A"
- uploaded_wlo_text = f"N/A"
- download_wlo_text = f"N/A"
- downloaded_wlo_text = f"N/A"
-
- if ETH_CARD in pnic_after:
- upload_eth = pnic_after[ETH_CARD].bytes_sent - pnic_before[ETH_CARD].bytes_sent
- upload_eth_text = f"{bytes2human(upload_eth)}/s"
- uploaded_eth = pnic_after[ETH_CARD].bytes_sent
- uploaded_eth_text = f"{bytes2human(uploaded_eth)}"
- download_eth = pnic_after[ETH_CARD].bytes_recv - pnic_before[ETH_CARD].bytes_recv
- download_eth_text = f"{bytes2human(download_eth)}/s"
- downloaded_eth = pnic_after[ETH_CARD].bytes_recv
- downloaded_eth_text = f"{bytes2human(downloaded_eth)}"
- else:
- upload_eth_text = f"N/A"
- uploaded_eth_text = f"N/A"
- download_eth_text = f"N/A"
- downloaded_eth_text = f"N/A"
+ interval = THEME_DATA['STATS']['CPU']['PERCENTAGE'].get("INTERVAL", None)
+ upload_wlo, uploaded_wlo, download_wlo, downloaded_wlo = sensors.Net.stats(WLO_CARD, interval)
+
+ upload_wlo_text = f"{bytes2human(upload_wlo)}/s"
+ uploaded_wlo_text = f"{bytes2human(uploaded_wlo)}"
+ download_wlo_text = f"{bytes2human(download_wlo)}/s"
+ downloaded_wlo_text = f"{bytes2human(downloaded_wlo)}"
if THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{upload_wlo_text:<8}",
+ text=f"{upload_wlo_text:>9}",
x=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['WLO']['UPLOAD']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
if THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{uploaded_wlo_text:<6}",
+ text=f"{uploaded_wlo_text:>6}",
x=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['WLO']['UPLOADED']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
if THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{download_wlo_text:<8}",
+ text=f"{download_wlo_text:>9}",
x=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['WLO']['DOWNLOAD']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
if THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{downloaded_wlo_text:<6}",
+ text=f"{downloaded_wlo_text:>6}",
x=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['WLO']['DOWNLOADED']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
+ upload_eth, uploaded_eth, download_eth, downloaded_eth = sensors.Net.stats(ETH_CARD, interval)
+
+ upload_eth_text = f"{bytes2human(upload_eth)}/s"
+ uploaded_eth_text = f"{bytes2human(uploaded_eth)}"
+ download_eth_text = f"{bytes2human(download_eth)}/s"
+ downloaded_eth_text = f"{bytes2human(downloaded_eth)}"
+
if THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{upload_eth_text:<8}",
+ text=f"{upload_eth_text:>9}",
x=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['ETH']['UPLOAD']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
if THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{uploaded_eth_text:<6}",
+ text=f"{uploaded_eth_text:>6}",
x=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['ETH']['UPLOADED']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
if THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{download_eth_text:<8}",
+ text=f"{download_eth_text:>9}",
x=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['ETH']['DOWNLOAD']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
if THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{downloaded_eth_text:<6}",
+ text=f"{downloaded_eth_text:>6}",
x=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("Y", 0),
- font=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
+ font=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("FONT",
+ "roboto-mono/RobotoMono-Regular.ttf"),
font_size=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("FONT_SIZE", 10),
font_color=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("FONT_COLOR", (0, 0, 0)),
- background_color=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
+ background_color=THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("BACKGROUND_COLOR",
+ (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
- THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ THEME_DATA['STATS']['NET']['ETH']['DOWNLOADED']['TEXT'].get(
+ "BACKGROUND_IMAGE",
+ None))
)
-
- config.PNIC_BEFORE = psutil.net_io_counters(pernic=True)
class Date:
@staticmethod
def stats():
date_now = datetime.datetime.now()
-
if THEME_DATA['STATS']['DATE']['DAY']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{date_now.strftime('%d-%m-%Y')}",
+ text=f"{date_now.strftime('%x')}",
x=THEME_DATA['STATS']['DATE']['DAY']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['DATE']['DAY']['TEXT'].get("Y", 0),
font=THEME_DATA['STATS']['DATE']['DAY']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
@@ -746,12 +644,12 @@ def stats():
background_color=THEME_DATA['STATS']['DATE']['DAY']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
THEME_DATA['STATS']['DATE']['DAY']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ None))
)
if THEME_DATA['STATS']['DATE']['HOUR']['TEXT'].get("SHOW", False):
display.lcd.DisplayText(
- text=f"{date_now.strftime('%H:%M:%S')}",
+ text=f"{date_now.strftime('%X')}",
x=THEME_DATA['STATS']['DATE']['HOUR']['TEXT'].get("X", 0),
y=THEME_DATA['STATS']['DATE']['HOUR']['TEXT'].get("Y", 0),
font=THEME_DATA['STATS']['DATE']['HOUR']['TEXT'].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
@@ -760,5 +658,5 @@ def stats():
background_color=THEME_DATA['STATS']['DATE']['HOUR']['TEXT'].get("BACKGROUND_COLOR", (255, 255, 255)),
background_image=get_full_path(THEME_DATA['PATH'],
THEME_DATA['STATS']['DATE']['HOUR']['TEXT'].get("BACKGROUND_IMAGE",
- None))
+ None))
)
diff --git a/main.py b/main.py
index 989bce58..b454e5fd 100644
--- a/main.py
+++ b/main.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# A system monitor in Python for "Turing Smart Screen" 3.5" IPS USB-C display
# https://github.com/mathoudebine/turing-smart-screen-python
-
+import locale
import os
import signal
import sys
@@ -21,6 +21,9 @@
if __name__ == "__main__":
+ # Apply system locale to this program
+ locale.setlocale(locale.LC_ALL, '')
+
def sighandler(signum, frame):
logger.info(" Caught signal %d, exiting" % signum)
@@ -71,14 +74,8 @@ def sighandler(signum, frame):
scheduler.CPUTemperature()
else:
logger.warning("Your CPU temperature is not supported yet")
- if stats.GpuNvidia.is_available():
- logger.info("Detected Nvidia GPU(s)")
- scheduler.GpuNvidiaStats()
- elif stats.GpuAmd.is_available():
- logger.info("Detected AMD GPU(s)")
- scheduler.GpuAmdStats()
- else:
- logger.warning("Your GPU is not supported yet")
+ if stats.Gpu.is_available():
+ scheduler.GpuStats()
scheduler.MemoryStats()
scheduler.DiskStats()
scheduler.NetStats()
diff --git a/requirements.txt b/requirements.txt
index 4b8f9c6e..40781e99 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
# Python packages requirements
-Pillow~=9.2.0 # Image generation
+Pillow~=9.3.0 # Image generation
pyserial~=3.5 # Serial linl to communicate with the display
PyYAML~=6.0 # For themes files
-psutil~=5.9.2 # CPU / disk / network metrics
+psutil~=5.9.4 # CPU / disk / network metrics
GPUtil~=1.4.0 # Nvidia GPU
# Following packages are for AMD GPU on Linux
@@ -12,3 +12,7 @@ pyamdgpuinfo~=2.1.3; sys_platform=="linux"
# Following packages are for AMD GPU on Windows
pyadl~=0.1; sys_platform=="win32"
+
+# Following packages are for LibreHardwareMonitor integration on Windows
+pythonnet~=3.0.1; sys_platform=="win32"
+pywin32>=305; sys_platform=="win32"
diff --git a/res/themes/3.5inchTheme2/theme.yaml b/res/themes/3.5inchTheme2/theme.yaml
index 1911809d..7ab3b673 100644
--- a/res/themes/3.5inchTheme2/theme.yaml
+++ b/res/themes/3.5inchTheme2/theme.yaml
@@ -1,6 +1,6 @@
---
display:
- # Specify the display orientation for this theme: portrait, landscape, reverse_portrait, reverse_landscape
+ # Specify the display orientation for this theme: portrait or landscape
DISPLAY_ORIENTATION: portrait
# Backplate RGB LED color (for HW revision 'flagship' devices only)
diff --git a/res/themes/Cyberpunk/theme.yaml b/res/themes/Cyberpunk/theme.yaml
index a5aaa0b9..373a8d59 100644
--- a/res/themes/Cyberpunk/theme.yaml
+++ b/res/themes/Cyberpunk/theme.yaml
@@ -1,6 +1,6 @@
---
display:
- # Specify the display orientation for this theme: portrait, landscape, reverse_portrait, reverse_landscape
+ # Specify the display orientation for this theme: portrait or landscape
DISPLAY_ORIENTATION: portrait
# Backplate RGB LED color (for HW revision 'flagship' devices only)
diff --git a/res/themes/Landscape6Grid/theme.yaml b/res/themes/Landscape6Grid/theme.yaml
index f9e11ec7..00afa26d 100644
--- a/res/themes/Landscape6Grid/theme.yaml
+++ b/res/themes/Landscape6Grid/theme.yaml
@@ -1,6 +1,6 @@
---
display:
- # Specify the display orientation for this theme: portrait, landscape, reverse_portrait, reverse_landscape
+ # Specify the display orientation for this theme: portrait or landscape
DISPLAY_ORIENTATION: landscape
# Backplate RGB LED color (for HW revision 'flagship' devices only)
diff --git a/res/themes/Terminal/theme.yaml b/res/themes/Terminal/theme.yaml
index 345d73a3..b9181896 100644
--- a/res/themes/Terminal/theme.yaml
+++ b/res/themes/Terminal/theme.yaml
@@ -1,6 +1,6 @@
---
display:
- # Specify the display orientation for this theme: portrait, landscape, reverse_portrait, reverse_landscape
+ # Specify the display orientation for this theme: portrait or landscape
DISPLAY_ORIENTATION: portrait
# Backplate RGB LED color (for HW revision 'flagship' devices only)
diff --git a/res/themes/bash-dark-green/theme.yaml b/res/themes/bash-dark-green/theme.yaml
index b90eaabd..a48d9600 100644
--- a/res/themes/bash-dark-green/theme.yaml
+++ b/res/themes/bash-dark-green/theme.yaml
@@ -1,6 +1,6 @@
---
display:
- # Specify the display orientation for this theme: portrait, landscape, reverse_portrait, reverse_landscape
+ # Specify the display orientation for this theme: portrait or landscape
DISPLAY_ORIENTATION: portrait
# Backplate RGB LED color (for HW revision 'flagship' devices only)
diff --git a/simple-program.py b/simple-program.py
index 23e114b0..9cdbbe4f 100644
--- a/simple-program.py
+++ b/simple-program.py
@@ -8,9 +8,9 @@
from datetime import datetime
# Import only the modules for LCD communication
-from library.lcd_comm_rev_a import LcdCommRevA, Orientation
-from library.lcd_comm_rev_b import LcdCommRevB
-from library.lcd_simulated import LcdSimulated
+from library.lcd.lcd_comm_rev_a import LcdCommRevA, Orientation
+from library.lcd.lcd_comm_rev_b import LcdCommRevB
+from library.lcd.lcd_simulated import LcdSimulated
from library.log import logger
# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux, etc. or "AUTO" for auto-discovery
@@ -42,12 +42,12 @@ def sighandler(signum, frame):
lcd_comm = None
if REVISION == "A":
logger.info("Selected Hardware Revision A (Turing Smart Screen)")
- lcd_comm = LcdCommRevA(com_port="AUTO",
+ lcd_comm = LcdCommRevA(com_port=COM_PORT,
display_width=320,
display_height=480)
elif REVISION == "B":
print("Selected Hardware Revision B (XuanFang screen version B / flagship)")
- lcd_comm = LcdCommRevB(com_port="AUTO",
+ lcd_comm = LcdCommRevB(com_port=COM_PORT,
display_width=320,
display_height=480)
elif REVISION == "SIMU":
@@ -67,8 +67,8 @@ def sighandler(signum, frame):
# Send initialization commands
lcd_comm.InitializeComm()
- # Set brightness in % (warning: screen can get very hot at high brightness!)
- lcd_comm.SetBrightness(level=25)
+ # Set brightness in % (warning: revision A display can get hot at high brightness!)
+ lcd_comm.SetBrightness(level=10)
# Set backplate RGB LED color (for supported HW only)
lcd_comm.SetBackplateLedColor(led_color=(255, 255, 255))