The Phrozen Shuffle 2019 is a capable resin 3D printer, but like many machines of its era, it suffers from one annoying problem: noise. The stock stepper drivers produce that characteristic whine during Z-axis movements that can drive you crazy during long prints. I understand that you shouldn’t sleep in the same room as an SLA printer due to resin toxicity, but the thing is, you can hear this printer even from other rooms.
Spoiler: The hardware upgrade was successful and the printer is now whisper-quiet. The software side revealed some interesting limitations that would require custom firmware to fully overcome.
Hardware Overview
Main Board Orange Pi PC (Allwinner H3 SoC)
Motion Controller Custom GRBL board (GRBL 1.1g)
Display 4K Mono LCD (2560x1440) DMG85480C050_06WT
Touchscreen DWIN DGUS HMI display
Stepper A4988


Serial Ports
ttyS1 (GRBL), ttyS2 (HMI)
The Orange Pi runs a custom Python firmware (/root/python.sla), not a standard one, that coordinates between the touchscreen, motion controller, and UV LCD.
TMC2208 Stepper Driver Upgrade


The TMC2208 is a drop-in replacement for A4988 drivers that offers:
StealthChop - Nearly silent operation
256 microsteps - Smoother motion (interpolated from 16)
Better thermal performance - Runs cooler
Stall detection - Can detect skipped steps
The Phrozen's stepper driver board has a few quirks that required attention:
The stock driver board uses jumpers for microstepping configuration (M1, M2, M3). Fortunately, the UART pins were unpopulated, meaning I didn't need to cut any traces for UART communication, though I'm running the TMC2208 in standalone (legacy) mode anyway.
Here's where it gets interesting. The TMC2208 has an active-low ENABLE pin, meaning:
LOW (GND) = Driver enabled, motor powered
HIGH = Driver disabled, motor free
The stock board's ENABLE signal logic was inconsistent with the TMC2208's requirements. To ensure the driver is always active, I removed the ENABLE pin from the driver and shorted the board's pad directly to GND. By the way, never try this on a FDM printer as motors will never relax when enable pin is disabled. It could turn into a fire hazard easily.
This keeps the driver permanently enabled, which is fine for a Z-axis-only printer like the Shuffle. The motor will always be powered when the printer is on.


The TMC2208 has a small potentiometer for current adjustment. For the Shuffle's Z-axis motor, I set it to approximately 800mA RMS. Use this formula:
Vref = I_rms × 1.41 × R_sense × 2
The trick is, I’ve connected the motor driver with long female-male jumpers. Then I connected a probe to my screw driver, and other one to ground. I set the voltage between the potentiometer and ground to a safe level. 0.8v seems to be safe. I tried moving Z-axis from UI. I increased the voltage until it moves normally. Then I added a tiny bit of weight to build plate. I increased the voltage until build plate moves with the weight. The reason I did that is simple, I don’t want my stepper motor to run at its RMS current always. By giving it just enough current, we can increase the life of both stepper motor and motor driver.


Now my 3D Printer is nearly silent, it is time to NanoDLP Upgrade.
NanoDLP Upgrade
The Phrozen Shuffle runs Armbian on its Orange Pi, but SSH isn't enabled by default and the root password is unknown. Here's how to gain access:
Power off the printer and remove the SD card
Mount the SD card on a Linux computer
Set a known root password:
ENCRYPTED_PW=$(openssl passwd -6 orange)
echo $ENCRYPTED_PW
sudo sed -i "s|^root:[^:]*|root:$ENCRYPTED_PW|" etc/shadowPassword is now “orange”. Just like normal OrangePIs. You can set it to antything you want.
Enable SSH root login:
# Force PermitRootLogin to yes
sudo sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' etc/ssh/sshd_config
# Ensure PasswordAuthentication is enabled
sudo sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication yes/' etc/ssh/sshd_config
# Verify the change
grep "PermitRootLogin" etc/ssh/sshd_configSync and unmount:
syncNow you can ssh into printer.
ssh root@<printer-ip>
# Password: orangeBefore attempting NanoDLP integration, I needed to understand how the stock Phrozen firmware worked. This involved mounting the SD card and exploring the filesystem.
The first clue was /etc/rc.local:
#!/bin/sh -e
clear
setterm -foreground black -background black -store
setterm -cursor off
clear
setterm -blank
/root/python.sla/runall.sh
exit 0
Then /root/python.sla/runall.sh revealed the multi-process architecture:
#!/bin/bash
cat /dev/zero > /dev/fb0
sleep 2
cd /root/python.sla
sudo python3 /root/python.sla/1_gcode.py > /dev/null 2>&1 &
sudo python3 /root/python.sla/2_png.py > /dev/null 2>&1 &
sudo python3 /root/python.sla/3_prnsrv.py > /dev/null 2>&1 &
sudo python3 /root/python.sla/printer_v7.py runserver --port 80 --host 0.0.0.0 --processes 10 > /dev/null 2>&1 &
sleep 2
sudo python3 /root/python.sla/6_hmi.py > /dev/null 2>&1 &/root/python.sla/ => this is where original firmware lies. Also there are some interesting stuff in bash history. You can check it yourself if you are curious.
The Python files turned out to be single-line stubs importing compiled Cython modules:
# 1_gcode.py
import gbrl_gcode_mqtt
# 2_png.py
import png_mqtt_v2
# 3_prnsrv.py
import print_chitu_mqtt_v4
# 6_hmi.py
import hmi_main_4k_v2I've extracted strings from compiled Python binaries:
# Find baud rate
strings *.so | grep -E "9600|115200|250000"
# Result: __pyx_int_115200 (multiple times)
# Find serial port (revealed macOS development origin!)
strings gbrlClass.cpython-36m-arm-linux-gnueabihf.so | grep -i "tty"
# Result: /dev/tty.usbserial-AC00HEB6
# Find G-code commands
strings gbrl_gcode_mqtt.cpython-36m-arm-linux-gnueabihf.so | grep -iE "G[0-9]|M[0-9]"
# Results: G28, G90, G91, G1, M106, M107, M114The firmware uses Mosquitto MQTT broker for inter-process communication:
# From printer_v7.py (the readable Flask app)
grep -r "printer/" /root/python.sla/*.pyThis revealed the MQTT topics:
Topic Purpose
printer/gcode G-code commands to motion controller
printer/printchitu Start print, it has chitu, quite interesting it can't connect ChituBox
jobprinter/resume R=resume, P=pause
printer/hmicontrol rebootshutdown
printer/pngLayer image display
No MCU firmware on SD card - The motion controller was pre-programmed at factory. No avrdude, no .hex files, no way to update it from Linux.
Developed on macOS - The /dev/tty.usbserial-AC00HEB6 path in compiled modules revealed development environment.
Standard GRBL - Despite being compiled, the motion controller speaks standard GRBL with custom extensions ($HZ for Z-only homing).
Simple update mechanism - phupdate.sh just extracts a tarball from USB to /root, no MCU firmware flashing.
Understanding the architecture made NanoDLP integration seem straightforward:
Motion is standard GRBL on /dev/ttyS1 at 115200 baud
Display is standard Linux framebuffer
UV LED uses standard M106/M107 G-code
Only the HMI touchscreen uses proprietary DWIN DGUS protocol
From my understanding, while not fully open source, NanoDLP is essentially the Klipper or Mainsail of the SLA world; offering better slicer compatibility and control than the obscure stock firmware.
# Download NanoDLP
cd /root
mkdir printer && cd printer
wget https://www.nanodlp.com/download/nanodlp.linux.arm.stable.tar.gz
tar -xzf nanodlp.linux.arm.stable.tar.gz
# Disable Phrozen firmware
mv /root/python.sla /root/python.sla.disabled
# Create startup script
cat > /root/start_nanodlp.sh << 'EOF'
#!/bin/bash
cd /root/printer
./nanodlp &
EOF
chmod +x /root/start_nanodlp.sh
# Edit rc.local to run NanoDLP on boot
cat > /etc/rc.local << 'EOF'
#!/bin/bash
/root/start_nanodlp.sh
exit 0
EOF
chmod +x /etc/rc.local
# Reboot to test
rebootI started doing configurations. NanoDLP stores configuration in /root/printer/db/config.json:
{
"ProjectorWidth": 1440,
"ProjectorHeight": 2560,
"XResolution": 1440,
"YResolution": 2560,
"SerialPort": "/dev/ttyS1",
"SerialBaud": 115200,
"SerialProtocol": "grbl",
"DisplayDriver": "fb",
"FramebufferDevice": "/dev/fb0",
"ZAxisStepsPerMM": 800,
"MaxHeight": 200,
"ShutterType": "gcode",
"ShutterOpenGcode": "M106 S255",
"ShutterCloseGcode": "M107",
"HomingGcode": "G28\nG90",
"Path": "/root/printer/db/",
"TempPath": "/tmp/",
"SupportedFileTypes": ["sl1", "photon", "cbddlp", "photons", "zip", "phz"],
"PixelSize": 0.05,
"BlankTime": 500,
"Port": 8080
}while these might be valid, we have to configure these further in Nanodlp UI. Also I've found out that the fans are controlled with GPIO pins. I can't turn on them via GRBL. So I wrote a script to control them.
#!/usr/bin/env python3
import sys
sys.path.insert(0, '/root/python.sla.disabled')
import class_gpio
GPIO = class_gpio.GPIO()
GPIO.mode("1", "out")
if len(sys.argv) > 1:
if sys.argv[1] == "on":
GPIO.write("1", "1")
elif sys.argv[1] == "off":
GPIO.write("1", "0")
else:
print("Usage: fan_control.py [on|off]")Save it to " /root/fan_control.py" we will use it later.
I edited the startup script:
#!/bin/bash
# Clear framebuffer (black screen)
cat /dev/zero > /dev/fb0 2>/dev/null
# Wait for system to settle
sleep 3
# Start NanoDLP
cd /root/printer
./nanodlp &
# Log startup
echo "NanoDLP started at $(date)" >> /var/log/nanodlp.logFirst you have to access to NanoDLP at http://<printer-ip>:80
Then it will ask you for the brand of your printer. I picked Phrozen. Then NanoDLP url is set to http://<printer-ip>:8080.
I started setting the NanoDLP from webUI.


Here take a look at exec command. It calls our custom python code to turn on fan before showing anything on screen, to keep our leds cool. Also one mistake here is we should set shield axis direction to Zero at Bottom.
One of the problems, and the biggest one is while setting up GCode's, we can't use [[WaitForDoneMessage]] like the standard ones. Because GRBL returns "ok" message after each command. Not "Z_move_comp" as NanoDLP excepts. NanoDLP Docs
We have to use
[[ResponseCheck]]
G1 X1.1
[[ResponseCheck 1 1]]
However, I found out that the homing command, "$HZ" is not returning when completed. We had to delay a safe amount for homing progress.
$HZ
[[Delay 45]]
[[PositionSet 0]]So our start printing command would be:
[[Exec /root/fan_control.py on]]
$X
[[Delay 1.0]]
$HZ
[[Delay 45]]
[[PositionSet 0]]Here the "$X" unlocks printer. I added this also to the boot gcode sequence, so I can run diagnostics before printing.
These are my findings until that point. Then I tried printing but never got it work because that "ok" or "z_move_comp" issue. The printer doesn't home correctly, also it doesn't wait for commands. Maybe the official firmware is using a GPIO pin to see when a command is completed. But hunting for GPIO pins is hard.
For NanoDLP to work properly with the Phrozen Shuffle, you would need either:
Custom GRBL firmware patched to send NanoDLP-compatible responses
Replace the GRBL board with a RAMPS board connected to the Orange Pi's UART
Serial translator that converts GRBL responses to RAMPS format
I ultimately decided to revert to the stock Phrozen firmware, which works reliably with the TMC2208 upgrade. As only single benefit of NanoDLP for me to sync Resin profiles from my slicer is not working too. The slicers don't think my printer has NanoDLP and community Resin profiles seems to be empty as everyone seems to be using. For a future project I might replace the GRBL board with Ramps. But at that point I would be building the printer from scratch.
For anyone interested running this thing with NanoDLP, I also did a research on how we can get touchscreen of it to work. It is a DWIN HMI screen. They use a custom protocol.
This is a custom Python script that tests the HMI screen that I made Claude write over my findings:
#!/usr/bin/env python3
import serial
import struct
import time
import threading
class PhrozenHMI:
HEADER = bytes([0x5A, 0xA5])
CMD_WRITE = 0x82
CMD_READ = 0x83
# Page IDs (VERIFIED from capture)
PAGE_HOME = 0x04 # Home/plates list
PAGE_FILE_DETAIL = 0x08 # File preview with thumbnail
PAGE_PRINTING = 0x0C # Active print
PAGE_WIFI_SCAN = 0x38 # WiFi scanning animation
PAGE_WIFI_LIST = 0x1E # WiFi network list
PAGE_WIFI_PASS = 0x32 # WiFi password entry
PAGE_PAUSE = 0x2A # Paused / print finished
PAGE_CONFIRM = 0x34 # Confirmation dialogs
# Addresses (VERIFIED - note: 0x1100 not 0x0011!)
ADDR_INIT = 0x0082 # Init register
ADDR_PAGE = 0x0084 # Page switch
ADDR_STATUS_BAR = 0x3800 # "(hostname)IP"
ADDR_PAGE_IND = 0x2C00 # "P:x/y"
ADDR_DIALOG = 0x3900 # Dialog text
ADDR_PAUSE_MSG = 0x2F00 # Pause/WiFi status message
ADDR_CTRL1 = 0x4890 # Control register 1
ADDR_CTRL2 = 0x4820 # Control register 2 (brightness?)
# File list (6 slots)
ADDR_FILE = [0x1100, 0x1200, 0x1300, 0x1400, 0x1500, 0x1600]
# Resin list
ADDR_RESIN = [0x1E00, 0x1F00, 0x2000, 0x2100, 0x2200]
# WiFi list (5 slots)
ADDR_WIFI = [0x2700, 0x2800, 0x2900, 0x2A00, 0x2B00]
ADDR_WIFI_SSID = 0x3600 # Selected SSID for connection
ADDR_WIFI_PASS = 0x3700 # Password input
ADDR_WIFI_STATUS = 0x2F00 # "connect wi-fi success !" or "connect wi-fi Fail !"
# Print screen
ADDR_PRINT_FILE = 0x1700 # Filename
ADDR_PRINT_LAYER = 0x1800 # "x/y"
ADDR_PRINT_RESIN = 0x1900 # Resin name
ADDR_PRINT_TIME = 0x1A00 # "X Hr : Y Min"
def __init__(self, port='/dev/ttyS2', baud=115200):
self.port = port
self.baud = baud
self.ser = None
self.listener_thread = None
self.running = False
self.on_button = None # Callback for button events
# Button addresses (from display -> Orange Pi)
self.BTN_WIFI = 0x4810 # WiFi/Settings button
self.BTN_SELECT = 0x4240 # Select item in list (WiFi network, file, etc)
self.BTN_CONNECT = 0x4780 # Connect button (triggers password read)
self.BTN_HOME = 0x40B0 # Home button
self.BTN_BACK = 0x42A0 # Back button
self.BTN_CONFIRM = 0x43A0 # Confirm/OK button
# WiFi password storage
self.wifi_password = "" # Password to send when display asks
def connect(self):
self.ser = serial.Serial(self.port, self.baud, timeout=0.1)
print("[OK] Connected to %s" % self.port)
return True
def start_listener(self):
"""Start background thread to listen for button events"""
self.running = True
self.listener_thread = threading.Thread(target=self._listen_loop, daemon=True)
self.listener_thread.start()
print("[OK] Button listener started")
def _listen_loop(self):
"""Background loop for button events"""
buffer = b''
while self.running:
try:
if self.ser and self.ser.in_waiting:
data = self.ser.read(self.ser.in_waiting)
buffer += data
# Look for button event frames: 5A A5 06 83 XX XX 01 00 00
while len(buffer) >= 9:
idx = buffer.find(self.HEADER)
if idx == -1:
buffer = b''
break
if idx > 0:
buffer = buffer[idx:]
if len(buffer) < 3:
break
frame_len = buffer[2]
total_len = 3 + frame_len
if len(buffer) < total_len:
break
frame = buffer[:total_len]
buffer = buffer[total_len:]
# Skip OK responses
if b'OK' in frame:
continue
# Check command type
if len(frame) >= 6 and frame[3] == 0x83:
addr = struct.unpack('>H', frame[4:6])[0]
# Check if it's a read request (short frame with length byte)
# Format: 5A A5 04 83 [addr_hi] [addr_lo] [length]
if len(frame) == 7:
req_len = frame[6]
self._handle_read_request(addr, req_len)
else:
# Button press
self._handle_button(addr, frame)
else:
time.sleep(0.05)
except:
time.sleep(0.1)
def _handle_button(self, addr, frame):
"""Handle button press from display"""
btn_name = {
self.BTN_WIFI: "WIFI",
self.BTN_SELECT: "SELECT",
self.BTN_CONNECT: "CONNECT",
self.BTN_HOME: "HOME",
self.BTN_BACK: "BACK",
self.BTN_CONFIRM: "CONFIRM",
}.get(addr, None)
if btn_name:
print("\n[BTN] %s (0x%04X)" % (btn_name, addr))
if self.on_button:
self.on_button(btn_name, addr)
def _handle_read_request(self, addr, length):
"""Handle read request from display (e.g., password read)"""
if addr == 0x3700: # Password read
print("\n[READ] Password requested, sending: %s" % self.wifi_password)
self._send_read_response(addr, length, self.wifi_password)
def _send_read_response(self, addr, length, text):
"""Send response to read request"""
# Format: 5A A5 [len] 83 [addr_hi] [addr_lo] [req_len] [data...]
data = text.encode('utf-8')[:length]
# Pad with 0xFF then spaces
if len(data) < length:
data = data + b'\xff\xff'
data = data.ljust(length + 5, b' ') # Extra padding like Phrozen
payload = bytes([0x83]) + struct.pack('>H', addr) + bytes([length]) + data
frame = self.HEADER + bytes([len(payload)]) + payload
print(" TX (read response): %s" % ' '.join('%02X' % b for b in frame))
self.ser.write(frame)
def disconnect(self):
self.running = False
if self.listener_thread:
self.listener_thread.join(timeout=1)
if self.ser:
self.ser.close()
print("[OK] Disconnected")
def send(self, addr, data):
"""Send frame to HMI"""
payload = bytes([self.CMD_WRITE]) + struct.pack('>H', addr) + data
frame = self.HEADER + bytes([len(payload)]) + payload
print(" TX: %s" % ' '.join('%02X' % b for b in frame))
self.ser.write(frame)
time.sleep(0.03)
resp = self.ser.read(10)
if resp:
print(" RX: %s" % ' '.join('%02X' % b for b in resp))
return b'OK' in resp
def write_text(self, addr, text, length=40):
"""Write text to address"""
data = text.encode('utf-8')[:length]
data = data.ljust(length, b' ') # Pad with spaces, not nulls
return self.send(addr, data)
def switch_page(self, page):
"""Switch to page - CORRECT format from capture"""
# Format: 5A 01 00 [page]
data = bytes([0x5A, 0x01, 0x00, page])
return self.send(self.ADDR_PAGE, data)
def init_display(self):
"""Send init sequence like Phrozen does at boot"""
# Init register
self.send(self.ADDR_INIT, bytes([0x64, 0x10, 0xEA, 0x60]))
# Control register
self.send(self.ADDR_CTRL1, bytes([0x00, 0x00]))
def show_home(self, files=None, ip="192.168.1.153", hostname="NanoDLP"):
"""Show home screen with file list"""
print("\n[HOME] Showing home screen...")
# Init
self.init_display()
# Status bar
status = "(%s)%s" % (hostname, ip)
self.write_text(self.ADDR_STATUS_BAR, status, 80)
# Switch to home page
self.switch_page(self.PAGE_HOME)
# Clear file slots first (with spaces like Phrozen does)
for addr in self.ADDR_FILE:
self.write_text(addr, "", 40)
# Populate files
if files:
for i, fname in enumerate(files[:6]):
self.write_text(self.ADDR_FILE[i], fname, 40)
# Page indicator
self.write_text(self.ADDR_PAGE_IND, "P:1/1", 10)
print("[OK] Home screen shown")
def show_file_detail(self, filename, layers, resin):
"""Show file detail screen"""
print("\n[FILE] Showing file detail: %s" % filename)
self.switch_page(self.PAGE_FILE_DETAIL)
self.write_text(self.ADDR_PRINT_FILE, filename, 44)
self.write_text(self.ADDR_PRINT_LAYER, str(layers), 20)
self.write_text(self.ADDR_PRINT_RESIN, resin, 40)
print("[OK] File detail shown")
def show_confirm(self, message):
"""Show confirmation dialog"""
print("\n[CONFIRM] %s" % message)
self.switch_page(self.PAGE_CONFIRM)
self.write_text(self.ADDR_DIALOG, message, 40)
def show_printing(self, filename, layer, total, resin, time_str):
"""Show print status screen"""
print("\n[PRINT] Layer %d/%d" % (layer, total))
self.switch_page(self.PAGE_PRINTING)
self.write_text(self.ADDR_PRINT_FILE, filename, 44)
self.write_text(self.ADDR_PRINT_LAYER, "%d/%d" % (layer, total), 20)
self.write_text(self.ADDR_PRINT_RESIN, resin, 40)
self.write_text(self.ADDR_PRINT_TIME, time_str, 20)
def show_pause(self, message="Printer pause , press OK to resume !"):
"""Show pause screen"""
print("\n[PAUSE] %s" % message)
self.switch_page(self.PAGE_PAUSE)
self.write_text(self.ADDR_PAUSE_MSG, message, 44)
def show_wifi_scan(self):
"""Show WiFi scanning animation"""
print("\n[WIFI] Scanning...")
self.switch_page(self.PAGE_WIFI_SCAN)
def show_wifi_list(self, networks=None):
"""Show WiFi network list"""
print("\n[WIFI] Showing network list...")
# Show scanning animation first
self.switch_page(self.PAGE_WIFI_SCAN)
self.send(self.ADDR_CTRL2, bytes([0x00, 0xFF]))
self.write_text(self.ADDR_STATUS_BAR, "(NanoDLP)%s" % self.get_current_ip(), 80)
# Clear WiFi slots
for addr in self.ADDR_WIFI:
self.write_text(addr, "", 40)
# Populate networks
if networks:
for i, ssid in enumerate(networks[:5]):
self.write_text(self.ADDR_WIFI[i], ssid, 40)
# Switch to WiFi list page
self.switch_page(self.PAGE_WIFI_LIST)
# Page indicator
total = len(networks) if networks else 0
self.write_text(self.ADDR_PAGE_IND, "P:1/%d" % max(1, (total + 4) // 5), 10)
print("[OK] WiFi list shown")
def show_wifi_password(self, ssid):
"""Show WiFi password entry screen"""
print("\n[WIFI] Password entry for: %s" % ssid)
self.switch_page(self.PAGE_WIFI_PASS)
self.write_text(self.ADDR_WIFI_SSID, ssid, 40)
self.write_text(self.ADDR_WIFI_PASS, "", 40) # Clear password
def wifi_connect_status(self, success, ssid=None):
"""Show WiFi connection status"""
if success:
msg = "connect wi-fi success !"
print("\n[WIFI] Connected!")
else:
msg = "connect wi-fi Fail !"
print("\n[WIFI] Connection failed!")
self.write_text(self.ADDR_WIFI_STATUS, msg, 44)
def scan_wifi_networks(self):
"""Scan for WiFi networks using nmcli"""
import subprocess
networks = []
try:
result = subprocess.run(
['nmcli', '-t', '-f', 'SSID', 'dev', 'wifi', 'list'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=10
)
for line in result.stdout.decode().strip().split('\n'):
ssid = line.strip()
if ssid and ssid not in networks:
networks.append(ssid)
except Exception as e:
print("Error scanning: %s" % e)
return networks[:5]
def connect_wifi(self, ssid, password=""):
"""Connect to WiFi network using nmcli"""
import subprocess
try:
if password:
cmd = ['nmcli', 'dev', 'wifi', 'connect', ssid, 'password', password]
else:
cmd = ['nmcli', 'dev', 'wifi', 'connect', ssid]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
return result.returncode == 0
except Exception as e:
print("Error connecting: %s" % e)
return False
def get_current_ip(self):
"""Get current IP address"""
import subprocess
try:
result = subprocess.run(
['hostname', '-I'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=5
)
ip = result.stdout.decode().strip().split()[0]
return ip
except:
return "0.0.0.0"
def get_current_ssid(self):
"""Get current WiFi SSID"""
import subprocess
try:
result = subprocess.run(
['nmcli', '-t', '-f', 'ACTIVE,SSID', 'dev', 'wifi'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=5
)
for line in result.stdout.decode().strip().split('\n'):
if line.startswith('yes:'):
return line.split(':')[1]
except:
pass
return ""
def main():
hmi = PhrozenHMI()
if not hmi.connect():
return
# Start button listener
hmi.start_listener()
# Track state for WiFi flow
state = {'selected_ssid': None, 'networks': []}
def on_button(name, addr):
"""Handle button presses from touchscreen"""
if name == "SELECT":
# User tapped a WiFi network - show password screen
# We don't know which one they tapped, but display will tell us
print(" -> Network selected, password screen should appear")
elif name == "CONNECT":
# Connect button tapped - display will read password
print(" -> Connect tapped, password will be sent")
hmi.on_button = on_button
files = [
"calibration_cube.zip",
"mini_figure.zip",
"gear_set.zip",
"phone_stand.zip",
"dice_tower.zip",
]
print("\n" + "="*50)
print("Phrozen HMI Controller - With WiFi Support")
print("="*50)
print("Commands:")
print(" 1 - Home screen")
print(" 2 - File detail")
print(" 3 - Print screen")
print(" 4 - Pause screen")
print(" 5 - Confirm dialog")
print(" 6 - WiFi scan & list")
print(" 7 - Set WiFi password (7 <password>)")
print(" 8 - WiFi connect via nmcli (8 <ssid> [password])")
print(" p - Test page (p 04, p 08, etc)")
print(" 0 - Exit")
print("")
print("Touchscreen buttons will be detected!")
print("When you tap a WiFi network, it reads password from here.")
print("="*50)
try:
while True:
cmd = input("\n> ").strip()
if cmd == "0":
break
elif cmd == "1":
hmi.show_home(files)
elif cmd == "2":
hmi.show_file_detail("test_model.zip", 100, "Anycubic Clear")
elif cmd == "3":
hmi.show_printing("test_model.zip", 25, 100, "Anycubic Clear", "1 Hr : 30 Min")
elif cmd == "4":
hmi.show_pause()
elif cmd == "5":
hmi.show_confirm("Do you want to start printing ?")
elif cmd == "6":
# Scan and show WiFi networks
hmi.show_wifi_scan()
print("Scanning networks...")
networks = hmi.scan_wifi_networks()
print("Found: %s" % networks)
hmi.show_wifi_list(networks)
elif cmd.startswith("7 "):
# Set WiFi password for when display asks
password = cmd[2:].strip()
hmi.wifi_password = password
print("[OK] WiFi password set to: %s" % password)
print(" Now tap Connect on the touchscreen!")
elif cmd.startswith("8 "):
# Connect to WiFi via nmcli: 8 <ssid> [password]
parts = cmd.split(maxsplit=2)
ssid = parts[1] if len(parts) > 1 else ""
password = parts[2] if len(parts) > 2 else ""
if ssid:
hmi.show_wifi_password(ssid)
print("Connecting to %s..." % ssid)
success = hmi.connect_wifi(ssid, password)
hmi.wifi_connect_status(success, ssid)
if success:
# Update status bar with new connection
ip = hmi.get_current_ip()
hmi.write_text(hmi.ADDR_STATUS_BAR,
"(NanoDLP)%s (%s)" % (ip, ssid), 80)
time.sleep(2)
hmi.show_home(files)
else:
print("Usage: 8 <ssid> [password]")
elif cmd.startswith("p "):
try:
page = int(cmd[2:], 16)
print("Switching to page 0x%02X..." % page)
hmi.switch_page(page)
except:
print("Usage: p 04")
else:
print("Unknown command")
except KeyboardInterrupt:
pass
finally:
hmi.disconnect()
if __name__ == "__main__":
main()DWIN DGUS protocol is strictly Little-Endian for data values, but the addresses are Big-Endian.
The plan were connect it to NanoDLP API but as I ditched and now happy with official Phrozen firmware, that would not be beneficial for me. To capture HMI communication you can use this method:
# Install interceptty or use socat
apt-get install socat
# Create a sniffer between Orange Pi and HMI
# First, rename original port
mv /dev/ttyS2 /dev/ttyS2_real
# Create interceptor that logs traffic
socat -v /dev/ttyS2_real,raw,echo=0,b115200 PTY,link=/dev/ttyS2,raw,echo=0 2>&1 | tee /tmp/hmi_traffic.log &
# Now start Phrozen firmware
cd /root/python.sla.disabled
./runall.sh &
# Press buttons on touchscreen, watch the log
cat /tmp/hmi_traffic.logThe TMC2208 upgrade was a complete success - the printer is now whisper-quiet during operation. The NanoDLP attempt, while unsuccessful due to GRBL compatibility issues, led to a deep understanding of the printer's architecture and a fully documented HMI protocol.
For anyone looking to silence their Phrozen Shuffle, the stepper driver upgrade is highly recommended. For those wanting NanoDLP, be prepared to either modify the GRBL firmware or replace the motion controller entirely. Or you just need to do GPIO hunting.
I also heard that NanoDLP could be configured to poll "?" (status command) constantly to check if priner is in "Idle" or "Run" state. But I was not able to see this in UI. Maybe a script that injects itself to serial communication and it does that "?" polling and returns "Z_move_comp" properly. But as I said I won't be the getting benefits I wanted from NanoDLP to put more effort to it.
If anyone can get NanoDLP run with original hardware please let me know.