diff --git a/README.md b/README.md
index d8a8891d6..2a19fc300 100644
--- a/README.md
+++ b/README.md
@@ -26,9 +26,9 @@ Supported operating systems : macOS, Windows, Linux (incl. Raspberry Pi), basica
|
|
|
|
| revision B & flagship (with backplate & RGB LEDs) | Unknown manufacturer, visually similar to Turing 3.5" / 5". Original software is `UsbPCMonitor.exe` | Front panel has an engraved inscription "奇叶智显" Qiye Zhixian (Qiye Smart Display) |
-| ✅ WeAct Studio Display FS V1 0.96" | ✅ WeAct Studio Display FS V1 3.5" |
-|---------------------------------------------------------------|--------------------------------------------------------------|
-|
|
|
+| ✅ WeAct Studio Display FS V1 0.96" | ✅ WeAct Studio Display FS V1 3.5" | ✅ Solarmax / HL-VMAX (Artinchip) |
+|---------------------------------------------------------------|--------------------------------------------------------------|---------------------------------------------------------------|
+|
|
| Ultra-wide PC case & secondary displays (320x1480, 462x1920) |
diff --git a/config.yaml b/config.yaml
index 8aef493e4..fd101ad87 100644
--- a/config.yaml
+++ b/config.yaml
@@ -10,7 +10,7 @@ config:
# Theme to use (located in res/themes)
# Use the name of the folder as value
# Choose a theme made for your screen size (see DISPLAY_SIZE inside theme.yaml)
- THEME: 3.5inchTheme2
+ THEME: "Cyberpunk 2077 Vertical"
# Hardware sensors reading
# Choose the appropriate method for reading your hardware sensors:
@@ -18,7 +18,7 @@ config:
# - LHM use LibreHardwareMonitor library to read hardware sensors (Windows only - NEEDS ADMIN RIGHTS)
# - STUB / STATIC use random/static 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
+ HW_SENSORS: PYTHON
# Network interfaces
# Linux/MacOS interfaces are named "eth0", "wlan0", "wlp1s0", "enp2s0"...
@@ -59,14 +59,15 @@ display:
# - TUR_USB for Turing HW revisions 1.x: 4.6"/5.2"/8.0"/8.8"/9.2"
# - WEACT_A for WeAct Studio Display FS V1 3.5"
# - WEACT_B for WeAct Studio Display FS V1 0.96"
+ # - VMAX for Solarmax / HL-VMAX (Artinchip VID 0x33C3 PID 0xF101)
# - SIMU for simulated display (image written in screencap.png). Width & height will be detected from the theme
# To identify your smart screen: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions
- REVISION: A
+ REVISION: VMAX
# Display Brightness
# Set this as the desired %, 0 being completely dark and 100 being max brightness
# Warning: revision A display can get hot at high brightness!
- BRIGHTNESS: 20
+ BRIGHTNESS: 80
# Display reverse: true/false
# Set to true to reverse display orientation (landscape <-> reverse landscape, portrait <-> reverse portrait)
@@ -77,4 +78,4 @@ display:
# By default, this program resets the display on startup, in case it is in a degraded state from previous runs.
# This behavior causes the display to disconnect/reconnect, and the COM port to change.
# On some displays (like rev. A) it can be an issue, in this case change the value to "false".
- RESET_ON_STARTUP: true
+ RESET_ON_STARTUP: false
diff --git a/configure.py b/configure.py
old mode 100755
new mode 100644
index f4689f723..1032ff972
--- a/configure.py
+++ b/configure.py
@@ -63,6 +63,7 @@
XUANFANG_MODEL = "XuanFang rev. B & flagship"
KIPYE_MODEL = "Kipye Qiye Smart Display"
WEACT_MODEL = "WeAct Studio Display FS V1"
+SOLARMAX_MODEL = "Solarmax / HL-VMAX (Artinchip)"
SIMULATED_MODEL = "Simulated screen"
_SIZE_2_1_INCH = "2.1\"" # Only for retro compatibility
@@ -77,6 +78,7 @@
SIZE_8_INCH = "8\""
SIZE_8_8_INCH = "8.8\""
SIZE_8_8_INCH_NEWREV = "8.8\" / 9.2\" (V1.X new HW rev.)"
+SIZE_11_3_INCH = "11.3\" / 9.2\" (320x1480 / 462x1920)"
SIZE_12_3_INCH = "12.3\""
SIZE_2_8_INCH_NEWREV = "2.8\" round (V1.X new HW rev.)"
@@ -92,6 +94,7 @@
SIZE_8_INCH,
SIZE_8_8_INCH,
SIZE_8_8_INCH_NEWREV,
+ SIZE_11_3_INCH,
SIZE_12_3_INCH,
)
@@ -114,6 +117,9 @@
('TUR_USB', SIZE_2_8_INCH_NEWREV): TURING_MODEL,
('WEACT_A', SIZE_3_5_INCH): WEACT_MODEL,
('WEACT_B', SIZE_0_96_INCH): WEACT_MODEL,
+ ('VMAX', SIZE_11_3_INCH): SOLARMAX_MODEL,
+ ('VMAX', SIZE_8_8_INCH): SOLARMAX_MODEL,
+ ('VMAX', SIZE_8_8_INCH_NEWREV): SOLARMAX_MODEL,
('SIMU', SIZE_0_96_INCH): SIMULATED_MODEL,
('SIMU', SIZE_2_x_INCH): SIMULATED_MODEL,
@@ -123,10 +129,14 @@
('SIMU', SIZE_5_2_INCH): SIMULATED_MODEL,
('SIMU', SIZE_8_INCH): SIMULATED_MODEL,
('SIMU', SIZE_8_8_INCH): SIMULATED_MODEL,
+ ('SIMU', SIZE_11_3_INCH): SIMULATED_MODEL,
}
# This map is used to write the correct config.yaml "REVISION" from selected smart screen model and size
model_and_size_to_revision_map = {
(KIPYE_MODEL, SIZE_3_5_INCH): 'D',
+ (SOLARMAX_MODEL, SIZE_11_3_INCH): 'VMAX',
+ (SOLARMAX_MODEL, SIZE_8_8_INCH): 'VMAX',
+ (SOLARMAX_MODEL, SIZE_8_8_INCH_NEWREV): 'VMAX',
(TURING_MODEL, SIZE_2_x_INCH): 'C',
(TURING_MODEL, SIZE_3_5_INCH): 'A',
(TURING_MODEL, SIZE_4_6_INCH): 'TUR_USB',
@@ -151,6 +161,7 @@
(SIMULATED_MODEL, SIZE_5_2_INCH): 'SIMU',
(SIMULATED_MODEL, SIZE_8_INCH): 'SIMU',
(SIMULATED_MODEL, SIZE_8_8_INCH): 'SIMU',
+ (SIMULATED_MODEL, SIZE_11_3_INCH): 'SIMU',
}
hw_lib_map = {"AUTO": "Automatic", "LHM": "LibreHardwareMonitor (admin.)", "PYTHON": "Python libraries",
"STUB": "Fake random data", "STATIC": "Fake static data"}
@@ -606,7 +617,7 @@ def on_theme_editor_click(self):
# Load binary (for releases) or Python file with system interpreter
theme_editor = next(MAIN_DIRECTORY.glob("theme-editor*"))
if platform.system() == "Windows":
- subprocess.Popen([str(theme_editor), self.theme_cb.get()], shell=True)
+ subprocess.Popen([str(theme_editor), self.theme_cb.get()], creationflags=0x08000000)
else:
subprocess.Popen([str(theme_editor), self.theme_cb.get()])
@@ -624,7 +635,7 @@ def on_saverun_click(self):
# Load binary (for releases) or Python file with system interpreter
main_file = next(MAIN_DIRECTORY.glob("main*"))
if platform.system() == "Windows":
- subprocess.Popen([str(main_file)], shell=True)
+ subprocess.Popen([str(main_file)], creationflags=0x08000000)
else:
subprocess.Popen([str(main_file)])
diff --git a/library/display.py b/library/display.py
index dabbca52c..bb315ab87 100644
--- a/library/display.py
+++ b/library/display.py
@@ -27,6 +27,7 @@
from library.lcd.lcd_comm_rev_d import LcdCommRevD
from library.lcd.lcd_comm_weact_a import LcdCommWeActA
from library.lcd.lcd_comm_weact_b import LcdCommWeActB
+from library.lcd.lcd_comm_vmax import LcdCommVmax
from library.lcd.lcd_simulated import LcdSimulated
from library.log import logger
@@ -76,6 +77,8 @@ def _get_theme_size() -> tuple[int, int]:
return 480, 1920
elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '9.2"':
return 480, 1920 # 9.2" displays are 1920x462 but using 1920x480 to be compatible with 8.8" themes
+ elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '11.3"':
+ return 320, 1480
elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '12.3"':
return 720, 1920
else:
@@ -109,6 +112,9 @@ def __init__(self):
elif config.CONFIG_DATA["display"]["REVISION"] == "WEACT_B":
self.lcd = LcdCommWeActB(com_port=config.CONFIG_DATA['config']['COM_PORT'],
update_queue=config.update_queue)
+ elif config.CONFIG_DATA["display"]["REVISION"] in ("VMAX", "SOLARMAX"):
+ self.lcd = LcdCommVmax(com_port=config.CONFIG_DATA['config']['COM_PORT'],
+ update_queue=config.update_queue, display_width=width, display_height=height)
elif config.CONFIG_DATA["display"]["REVISION"] == "SIMU":
# Simulated display: always set width/height from theme
self.lcd = LcdSimulated(display_width=width, display_height=height)
diff --git a/library/lcd/lcd_comm_vmax.py b/library/lcd/lcd_comm_vmax.py
new file mode 100644
index 000000000..0aebae65b
--- /dev/null
+++ b/library/lcd/lcd_comm_vmax.py
@@ -0,0 +1,247 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# turing-smart-screen-python - a Python system monitor and library for USB-C displays like Turing Smart Screen or XuanFang
+# https://github.com/mathoudebine/turing-smart-screen-python/
+#
+# Driver for HL-VMAX / Solarmax USB LCD Displays (Artinchip VID 0x33C3, PID 0xF101)
+# Supports 462x1920, 320x1480, 480x1920 resolutions.
+#
+
+import io
+import re
+import time
+import threading
+from typing import Optional, Tuple
+from enum import IntEnum
+import serial
+from serial.tools.list_ports import comports
+from PIL import Image
+
+from library.lcd.lcd_comm import LcdComm, Orientation, queue
+from library.log import logger
+
+VID = 0x33C3
+PID = 0xF101
+HANDSHAKE = bytes([0xF0, 0xA5, 0x5A, 0x0F])
+SN_LEN = 26
+DEFAULT_WIDTH = 462
+DEFAULT_HEIGHT = 1920
+
+
+class LcdCommVmax(LcdComm):
+ def __init__(self, com_port: str = "AUTO", display_width: int = DEFAULT_WIDTH,
+ display_height: int = DEFAULT_HEIGHT, update_queue: Optional[queue.Queue] = None):
+ logger.debug("HW revision: VMAX / Solarmax (Artinchip)")
+ super().__init__(com_port, display_width, display_height, update_queue)
+ self.serial_number = None
+ self.brightness = 100
+
+ # Canvas for full frame composition
+ self._canvas_lock = threading.Lock()
+ self._canvas = Image.new("RGB", (self.display_width, self.display_height), (0, 0, 0))
+ self._frame_pending = False
+ self._serial_lock = threading.Lock()
+
+ self.openSerial()
+
+ def __del__(self):
+ self.closeSerial()
+
+ @staticmethod
+ def auto_detect_com_port() -> Optional[str]:
+ ports = comports()
+ for p in ports:
+ if p.vid == VID and p.pid == PID:
+ return p.device
+ if p.serial_number and "VMAX" in p.serial_number.upper():
+ return p.device
+ return None
+
+ def openSerial(self):
+ with self._serial_lock:
+ for attempt in range(1, 11):
+ com_port = self.com_port
+ if com_port == 'AUTO':
+ com_port = self.auto_detect_com_port()
+ if not com_port:
+ logger.warning(f"Cannot find HL-VMAX COM port automatically, retrying ({attempt}/10)")
+ time.sleep(1)
+ continue
+ logger.debug(f"Auto detected COM port: {com_port}")
+ else:
+ logger.debug(f"Static COM port: {com_port}")
+
+ try:
+ self.lcd_serial = serial.Serial(
+ com_port,
+ 115200,
+ timeout=1.0,
+ write_timeout=1.0,
+ rtscts=False,
+ dsrdtr=False
+ )
+ time.sleep(0.05)
+ try:
+ self.lcd_serial.reset_input_buffer()
+ self.lcd_serial.reset_output_buffer()
+ except Exception:
+ pass
+ return
+ except Exception as e:
+ logger.warning(f"Cannot open COM port {com_port}: {e} - retrying ({attempt}/10)")
+ time.sleep(1)
+
+ def InitializeComm(self):
+ with self._serial_lock:
+ logger.debug("Initializing HL-VMAX communication...")
+ try:
+ if self.lcd_serial and self.lcd_serial.is_open:
+ try:
+ self.lcd_serial.reset_input_buffer()
+ except Exception:
+ pass
+ self.lcd_serial.write(HANDSHAKE)
+ raw = self.lcd_serial.read(SN_LEN)
+ if len(raw) == SN_LEN:
+ self.serial_number = raw.decode("ascii", errors="replace").strip()
+ logger.info(f"Connected to HL-VMAX screen, Serial Number: {self.serial_number}")
+ self._parse_geometry(self.serial_number)
+ else:
+ logger.debug(f"Handshake response length: {len(raw)} bytes")
+ except Exception as e:
+ logger.warning(f"HL-VMAX handshake info: {e}")
+
+ def _parse_geometry(self, sn: str):
+ # Format example: VMAXD160462*1920S262001750 or VMAXA170320*1480S261001155
+ m = re.search(r"(\d{3,4})\s*\*\s*(\d{2,5})", sn)
+ if m:
+ w, h = int(m.group(1)), int(m.group(2))
+ if w >= 100 and h >= 100:
+ logger.info(f"Detected hardware panel geometry: {w}x{h}")
+ self.display_width = w
+ self.display_height = h
+ with self._canvas_lock:
+ target_w = self.get_width()
+ target_h = self.get_height()
+ self._canvas = Image.new("RGB", (target_w, target_h), (0, 0, 0))
+
+ def Reset(self):
+ with self._serial_lock:
+ if self.lcd_serial and self.lcd_serial.is_open:
+ try:
+ self.lcd_serial.reset_input_buffer()
+ except Exception:
+ pass
+ self.Clear()
+
+ def Clear(self):
+ with self._canvas_lock:
+ target_w = self.get_width()
+ target_h = self.get_height()
+ self._canvas = Image.new("RGB", (target_w, target_h), (0, 0, 0))
+ self._enqueue_render()
+
+ def ScreenOff(self):
+ self.SetBrightness(0)
+
+ def ScreenOn(self):
+ self.SetBrightness(self.brightness if self.brightness > 0 else 100)
+
+ def SetBrightness(self, level: int):
+ self.brightness = max(0, min(100, int(level)))
+ cmd = bytes([0xAA, 0xBB, self.brightness, 0xCC, 0xDD])
+ if self.update_queue:
+ with self.update_queue_mutex:
+ self.update_queue.put((self._send_brightness_direct, [cmd]))
+ else:
+ self._send_brightness_direct(cmd)
+
+ def _send_brightness_direct(self, cmd: bytes):
+ with self._serial_lock:
+ try:
+ if self.lcd_serial and self.lcd_serial.is_open:
+ self.lcd_serial.write(cmd)
+ except Exception as e:
+ logger.warning(f"Failed to send brightness command: {e}")
+
+ def SetOrientation(self, orientation: Orientation):
+ self.orientation = orientation
+ with self._canvas_lock:
+ target_w = self.get_width()
+ target_h = self.get_height()
+ self._canvas = Image.new("RGB", (target_w, target_h), (0, 0, 0))
+
+ def DisplayPILImage(
+ self,
+ image: Image.Image,
+ x: int = 0, y: int = 0,
+ image_width: int = 0,
+ image_height: int = 0
+ ):
+ if image_width != 0 and image_height != 0 and (image.size[0] != image_width or image.size[1] != image_height):
+ image = image.resize((image_width, image_height))
+
+ target_w = self.get_width()
+ target_h = self.get_height()
+
+ with self._canvas_lock:
+ if self._canvas.size != (target_w, target_h):
+ self._canvas = Image.new("RGB", (target_w, target_h), (0, 0, 0))
+
+ if x == 0 and y == 0 and image.size == (target_w, target_h):
+ self._canvas.paste(image, (0, 0))
+ else:
+ self._canvas.paste(image, (x, y))
+
+ self._enqueue_render()
+
+ def _enqueue_render(self):
+ if self.update_queue:
+ with self.update_queue_mutex:
+ if not self._frame_pending:
+ self._frame_pending = True
+ self.update_queue.put((self._render_and_send_frame, []))
+ else:
+ self._render_and_send_frame()
+
+ def _render_and_send_frame(self):
+ with self.update_queue_mutex:
+ self._frame_pending = False
+
+ with self._canvas_lock:
+ frame_to_send = self._orient_image(self._canvas.copy())
+
+ if frame_to_send.mode != "RGB":
+ frame_to_send = frame_to_send.convert("RGB")
+
+ buf = io.BytesIO()
+ frame_to_send.save(buf, format="JPEG", quality=88)
+ jpeg_bytes = buf.getvalue()
+
+ with self._serial_lock:
+ try:
+ if self.lcd_serial and self.lcd_serial.is_open:
+ self.lcd_serial.write(jpeg_bytes)
+ self.lcd_serial.flush()
+ except Exception as e:
+ logger.warning(f"Failed to send frame to HL-VMAX: {e}")
+
+ def _orient_image(self, img: Image.Image) -> Image.Image:
+ """Transforms logical image to match physical portrait panel (width x height)."""
+ if self.orientation == Orientation.PORTRAIT:
+ if img.size != (self.display_width, self.display_height):
+ img = img.resize((self.display_width, self.display_height))
+ return img
+ elif self.orientation == Orientation.REVERSE_PORTRAIT:
+ if img.size != (self.display_width, self.display_height):
+ img = img.resize((self.display_width, self.display_height))
+ return img.rotate(180)
+ elif self.orientation == Orientation.LANDSCAPE:
+ if img.size != (self.display_height, self.display_width):
+ img = img.resize((self.display_height, self.display_width))
+ return img.rotate(270, expand=True)
+ elif self.orientation == Orientation.REVERSE_LANDSCAPE:
+ if img.size != (self.display_height, self.display_width):
+ img = img.resize((self.display_height, self.display_width))
+ return img.rotate(90, expand=True)
+ return img
diff --git a/library/sensors/sensors_librehardwaremonitor.py b/library/sensors/sensors_librehardwaremonitor.py
index 473059c75..7d347576a 100644
--- a/library/sensors/sensors_librehardwaremonitor.py
+++ b/library/sensors/sensors_librehardwaremonitor.py
@@ -36,13 +36,21 @@
import library.sensors.sensors as sensors
from library.log import logger
+from pathlib import Path
# Import LibreHardwareMonitor dll to Python
-lhm_dll = os.getcwd() + '\\external\\LibreHardwareMonitor\\LibreHardwareMonitorLib.dll'
+base_dir = Path(getattr(sys, '_MEIPASS', Path(__file__).resolve().parents[2]))
+lhm_dir = base_dir / "external" / "LibreHardwareMonitor"
+if not (lhm_dir / "LibreHardwareMonitorLib.dll").exists():
+ lhm_dir = Path(os.getcwd()) / "external" / "LibreHardwareMonitor"
+
+lhm_dll = str(lhm_dir / "LibreHardwareMonitorLib.dll")
+hidsharp_dll = str(lhm_dir / "HidSharp.dll")
+
# noinspection PyUnresolvedReferences
clr.AddReference(lhm_dll)
# noinspection PyUnresolvedReferences
-clr.AddReference(os.getcwd() + '\\external\\LibreHardwareMonitor\\HidSharp.dll')
+clr.AddReference(hidsharp_dll)
# noinspection PyUnresolvedReferences
from LibreHardwareMonitor import Hardware
@@ -55,25 +63,33 @@
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)
+is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
+if not is_admin:
+ logger.warning(
+ "Program is not running as administrator. Motherboard fan/temp sensors may be limited. "
+ "Run as administrator for full hardware sensor access.")
handle = Hardware.Computer()
handle.IsCpuEnabled = True
handle.IsGpuEnabled = True
handle.IsMemoryEnabled = True
-handle.IsMotherboardEnabled = True # For CPU Fan Speed
-handle.IsControllerEnabled = True # For CPU Fan Speed
+handle.IsMotherboardEnabled = is_admin # Motherboard ring0 driver needs admin rights
+handle.IsControllerEnabled = is_admin # Fan controller needs admin rights
handle.IsNetworkEnabled = True
handle.IsStorageEnabled = True
handle.IsPsuEnabled = False
-handle.Open()
+try:
+ handle.Open()
+except Exception as e:
+ logger.warning(f"Error opening LHM hardware computer: {e}")
+ # Retry without motherboard/controller if failed
+ if handle.IsMotherboardEnabled or handle.IsControllerEnabled:
+ handle.IsMotherboardEnabled = False
+ handle.IsControllerEnabled = False
+ try:
+ handle.Open()
+ except Exception:
+ pass
for hardware in handle.Hardware:
if hardware.HardwareType == Hardware.HardwareType.Cpu:
logger.info("Found CPU: %s" % hardware.Name)
diff --git a/library/sensors/sensors_python.py b/library/sensors/sensors_python.py
index fc76af1bd..2a8203ed2 100644
--- a/library/sensors/sensors_python.py
+++ b/library/sensors/sensors_python.py
@@ -30,6 +30,60 @@
# Nvidia GPU
import GPUtil
+
+if platform.system() == "Windows":
+ import subprocess
+ import shutil
+ import os
+
+ def _no_window_getGPUs():
+ nvidia_smi = shutil.which('nvidia-smi')
+ if nvidia_smi is None:
+ nvidia_smi = "%s\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe" % os.environ.get('systemdrive', 'C:')
+
+ creationflags = 0x08000000 # CREATE_NO_WINDOW
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ startupinfo.wShowWindow = subprocess.SW_HIDE
+
+ try:
+ p = subprocess.Popen(
+ [nvidia_smi, "--query-gpu=index,uuid,utilization.gpu,memory.total,memory.used,memory.free,driver_version,name,gpu_serial,display_active,display_mode,temperature.gpu", "--format=csv,noheader,nounits"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ creationflags=creationflags,
+ startupinfo=startupinfo
+ )
+ stdout, _ = p.communicate()
+ output = stdout.decode('UTF-8', errors='ignore')
+ lines = [l for l in output.splitlines() if l.strip()]
+ GPUs = []
+ for line in lines:
+ vals = [v.strip() for v in line.split(',')]
+ if len(vals) >= 12:
+ def _cast(s):
+ try:
+ return float(s)
+ except:
+ return float('nan')
+ deviceIds = int(vals[0]) if vals[0].isdigit() else 0
+ uuid = vals[1]
+ gpuUtil = _cast(vals[2]) / 100
+ memTotal = _cast(vals[3])
+ memUsed = _cast(vals[4])
+ memFree = _cast(vals[5])
+ driver = vals[6]
+ gpu_name = vals[7]
+ serial = vals[8]
+ display_active = vals[9]
+ display_mode = vals[10]
+ temp_gpu = _cast(vals[11])
+ GPUs.append(GPUtil.GPU(deviceIds, uuid, gpuUtil, memTotal, memUsed, memFree, driver, gpu_name, serial, display_mode, display_active, temp_gpu))
+ return GPUs
+ except Exception:
+ return []
+
+ GPUtil.getGPUs = _no_window_getGPUs
# CPU & disk sensors
import psutil
diff --git a/main.py b/main.py
old mode 100755
new mode 100644
index dad5251f6..bf7448d74
--- a/main.py
+++ b/main.py
@@ -126,7 +126,7 @@ def on_configure_tray(tray_icon, item):
# Load binary (for releases) or Python file with system interpreter
configure_file = next(MAIN_DIRECTORY.glob("configure*"))
if platform.system() == "Windows":
- subprocess.Popen([str(configure_file)], shell=True)
+ subprocess.Popen([str(configure_file)], creationflags=0x08000000)
else:
subprocess.Popen([str(configure_file)])
diff --git a/simple-program.py b/simple-program.py
old mode 100755
new mode 100644
index 501cf9314..2e1c08a40
--- a/simple-program.py
+++ b/simple-program.py
@@ -38,6 +38,7 @@
from library.lcd.lcd_comm_turing_usb import LcdCommTuringUSB
from library.lcd.lcd_comm_weact_a import LcdCommWeActA
from library.lcd.lcd_comm_weact_b import LcdCommWeActB
+from library.lcd.lcd_comm_vmax import LcdCommVmax
from library.lcd.lcd_simulated import LcdSimulated
from library.log import logger
@@ -49,11 +50,11 @@
# Display revision: see config.yaml comments for values
# To identify your smart screen: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions
-REVISION = "A"
+REVISION = "VMAX"
# Display width & height in pixels for portrait orientation
# /!\ Do not switch width/height here for landscape, use lcd_comm.SetOrientation below
-WIDTH, HEIGHT = 320, 480
+WIDTH, HEIGHT = 462, 1920
assert WIDTH <= HEIGHT, "Indicate display width/height for PORTRAIT orientation: width <= height"
@@ -96,6 +97,9 @@ def sighandler(signum, frame):
elif REVISION == "WEACT_B":
logger.info("Selected Hardware WeAct Studio Display FS V1 0.96\"")
lcd_comm = LcdCommWeActB(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT)
+ elif REVISION in ("VMAX", "SOLARMAX"):
+ logger.info("Selected Hardware Solarmax / HL-VMAX (Artinchip)")
+ lcd_comm = LcdCommVmax(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT)
elif REVISION == "SIMU":
logger.info("Selected Simulated LCD")
lcd_comm = LcdSimulated(display_width=WIDTH, display_height=HEIGHT)
diff --git a/tests/library/lcd/test_lcd_comm_vmax.py b/tests/library/lcd/test_lcd_comm_vmax.py
new file mode 100644
index 000000000..3f5bb7efb
--- /dev/null
+++ b/tests/library/lcd/test_lcd_comm_vmax.py
@@ -0,0 +1,40 @@
+import unittest
+from PIL import Image
+
+from library.lcd.lcd_comm_vmax import LcdCommVmax, HANDSHAKE
+from library.lcd.lcd_comm import Orientation
+from .serial_mock import new_testing_serial
+from .sample_image import generate_sample_image
+
+
+class MockedLcdCommVmax(LcdCommVmax):
+ def openSerial(self):
+ self.lcd_serial = new_testing_serial()
+
+
+class TestLcdCommVmax(unittest.TestCase):
+ def test_brightness(self):
+ lcd = MockedLcdCommVmax(display_width=462, display_height=1920)
+ lcd.SetBrightness(80)
+ writes = [args[0] for name, args, _ in lcd.lcd_serial.mock_calls if name == "write"]
+ self.assertIn(bytes([0xAA, 0xBB, 80, 0xCC, 0xDD]), writes)
+
+ def test_display_pil_image_portrait(self):
+ lcd = MockedLcdCommVmax(display_width=100, display_height=200)
+ lcd.SetOrientation(Orientation.PORTRAIT)
+ img = Image.new("RGB", (100, 200), (255, 0, 0))
+ lcd.DisplayPILImage(img)
+ writes = [args[0] for name, args, _ in lcd.lcd_serial.mock_calls if name == "write"]
+ self.assertTrue(len(writes) > 0)
+ self.assertTrue(writes[0].startswith(b"\xff\xd8\xff"))
+ self.assertTrue(writes[0].endswith(b"\xff\xd9"))
+
+ def test_display_pil_image_landscape(self):
+ lcd = MockedLcdCommVmax(display_width=100, display_height=200)
+ lcd.SetOrientation(Orientation.LANDSCAPE)
+ img = Image.new("RGB", (200, 100), (0, 255, 0))
+ lcd.DisplayPILImage(img)
+ writes = [args[0] for name, args, _ in lcd.lcd_serial.mock_calls if name == "write"]
+ self.assertTrue(len(writes) > 0)
+ self.assertTrue(writes[0].startswith(b"\xff\xd8\xff"))
+ self.assertTrue(writes[0].endswith(b"\xff\xd9"))
diff --git a/tools/test-vmax-screen.py b/tools/test-vmax-screen.py
new file mode 100644
index 000000000..7aa570c2e
--- /dev/null
+++ b/tools/test-vmax-screen.py
@@ -0,0 +1,98 @@
+import sys
+import os
+import time
+from pathlib import Path
+from PIL import Image, ImageDraw, ImageFont
+
+# Add turing-smart-screen-python to sys.path
+BASE_DIR = Path(__file__).resolve().parent
+if str(BASE_DIR) not in sys.path:
+ sys.path.insert(0, str(BASE_DIR))
+
+from library.lcd.lcd_comm_vmax import LcdCommVmax
+from library.lcd.lcd_comm import Orientation
+from library.log import logger
+
+print("==================================================")
+print("Testing HL-VMAX / Solarmax in turing-smart-screen")
+print("==================================================")
+
+try:
+ print("[1] Initializing LcdCommVmax (auto-detecting COM port)...")
+ lcd = LcdCommVmax(com_port="AUTO")
+ print(f" Opened port: {lcd.lcd_serial.port if lcd.lcd_serial else 'None'}")
+
+ print("[2] Running InitializeComm & Handshake...")
+ lcd.InitializeComm()
+ print(f" Serial number: {lcd.serial_number}")
+ print(f" Detected size: {lcd.display_width} x {lcd.display_height}")
+
+ print("[3] Setting brightness to 75%...")
+ lcd.SetBrightness(75)
+
+ print("[4] Setting orientation to PORTRAIT...")
+ lcd.SetOrientation(Orientation.PORTRAIT)
+
+ w = lcd.get_width()
+ h = lcd.get_height()
+ print(f"[5] Creating test dashboard image ({w}x{h})...")
+
+ img = Image.new("RGB", (w, h), color=(15, 20, 30))
+ draw = ImageDraw.Draw(img)
+
+ # Decorative header
+ draw.rectangle([0, 0, w, 80], fill=(24, 75, 140))
+ draw.text((20, 25), "TURING SMART SCREEN PYTHON", fill=(255, 255, 255))
+
+ # Sub-header
+ draw.rectangle([0, 85, w, 140], fill=(30, 40, 60))
+ draw.text((20, 100), f"HL-VMAX Driver | {w}x{h} Native", fill=(0, 230, 180))
+
+ # System Status Card
+ draw.rounded_rectangle([20, 160, w - 20, 380], radius=15, fill=(25, 35, 50), outline=(50, 70, 100), width=2)
+ draw.text((40, 180), "HARDWARE INFO", fill=(200, 200, 200))
+ draw.text((40, 220), f"Serial: {lcd.serial_number}", fill=(255, 255, 255))
+ draw.text((40, 260), f"Port: {lcd.lcd_serial.port}", fill=(255, 255, 255))
+ draw.text((40, 300), f"Resolution: {w} x {h}", fill=(0, 255, 200))
+ draw.text((40, 340), "Driver Status: ONLINE & WORKING", fill=(50, 255, 50))
+
+ # CPU Load simulation bar
+ draw.rounded_rectangle([20, 410, w - 20, 560], radius=15, fill=(25, 35, 50), outline=(50, 70, 100), width=2)
+ draw.text((40, 430), "CPU USAGE (SIMULATED)", fill=(200, 200, 200))
+ draw.text((40, 465), "Load: 42%", fill=(255, 255, 255))
+ draw.rounded_rectangle([40, 505, w - 40, 535], radius=6, fill=(40, 50, 70))
+ bar_w = int((w - 80) * 0.42)
+ draw.rounded_rectangle([40, 505, 40 + bar_w, 535], radius=6, fill=(0, 180, 255))
+
+ # RAM Load simulation bar
+ draw.rounded_rectangle([20, 590, w - 20, 740], radius=15, fill=(25, 35, 50), outline=(50, 70, 100), width=2)
+ draw.text((40, 610), "RAM USAGE (SIMULATED)", fill=(200, 200, 200))
+ draw.text((40, 645), "Used: 68% (21.7 / 32 GB)", fill=(255, 255, 255))
+ draw.rounded_rectangle([40, 685, w - 40, 715], radius=6, fill=(40, 50, 70))
+ bar_ram = int((w - 80) * 0.68)
+ draw.rounded_rectangle([40, 685, 40 + bar_ram, 715], radius=6, fill=(180, 80, 255))
+
+ # GPU Status card
+ draw.rounded_rectangle([20, 770, w - 20, 920], radius=15, fill=(25, 35, 50), outline=(50, 70, 100), width=2)
+ draw.text((40, 790), "GPU STATUS (SIMULATED)", fill=(200, 200, 200))
+ draw.text((40, 825), "Load: 25% | Temp: 48°C", fill=(255, 255, 255))
+ draw.rounded_rectangle([40, 865, w - 40, 895], radius=6, fill=(40, 50, 70))
+ bar_gpu = int((w - 80) * 0.25)
+ draw.rounded_rectangle([40, 865, 40 + bar_gpu, 895], radius=6, fill=(0, 230, 120))
+
+ # Footer
+ draw.rectangle([0, h - 60, w, h], fill=(20, 25, 35))
+ draw.text((20, h - 42), "Turing Smart Screen Python • HL-VMAX Driver", fill=(120, 140, 170))
+
+ print("[6] Sending frame via DisplayPILImage...")
+ lcd.DisplayPILImage(img, 0, 0, w, h)
+ print("[7] Frame sent successfully to the screen!")
+
+ time.sleep(1)
+ lcd.closeSerial()
+ print("Test finished successfully!")
+
+except Exception as e:
+ import traceback
+ print("Error during test:")
+ traceback.print_exc()
diff --git a/turing-system-monitor.spec b/turing-system-monitor.spec
index 1f1c3c11a..7f7245a91 100644
--- a/turing-system-monitor.spec
+++ b/turing-system-monitor.spec
@@ -7,7 +7,7 @@ configure_a = Analysis(
pathex=[],
binaries=[],
datas=[('res', 'res'), ('config.yaml', '.'), ('external', 'external')],
- hiddenimports=['PIL', 'PIL._imagingtk', 'PIL._tkinter_finder'],
+ hiddenimports=['PIL', 'PIL._imagingtk', 'PIL._tkinter_finder', 'clr', 'pythonnet', 'clr_loader'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
@@ -45,7 +45,7 @@ main_a = Analysis(
pathex=[],
binaries=[],
datas=[('res', 'res'), ('config.yaml', '.'), ('external', 'external')],
- hiddenimports=['PIL', 'PIL._imagingtk', 'PIL._tkinter_finder'],
+ hiddenimports=['PIL', 'PIL._imagingtk', 'PIL._tkinter_finder', 'clr', 'pythonnet', 'clr_loader'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
@@ -83,7 +83,7 @@ editor_a = Analysis(
pathex=[],
binaries=[],
datas=[('res', 'res'), ('config.yaml', '.'), ('external', 'external')],
- hiddenimports=['PIL', 'PIL._imagingtk', 'PIL._tkinter_finder'],
+ hiddenimports=['PIL', 'PIL._imagingtk', 'PIL._tkinter_finder', 'clr', 'pythonnet', 'clr_loader'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],