mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
Reshapes the embedded PrettyGCode viewer (landed in #963) into a focused archive-preview tool, matching Bambuddy's data model instead of the OctoPrint-style "connected-printer + library file picker" flow it shipped with. Reached only from the Archives page 3D-preview button; URL /gcode-viewer?archive=<id>[&plate=<N>]. Backend: - /archives/{id}/gcode accepts ?plate=N and resolves the filename by parsing the suffix as int, so zero-padded names like plate_01.gcode are found when the plates endpoint reports index 1. - /archives/{id}/plates gains top-level has_gcode: bool. Source-only 3MFs (PNG/JSON fallback path) surface the flag so the frontend can skip the picker instead of sending the user into a dead viewer. - printer_state_to_dict injects name + model into every WS snapshot so consumers render proper labels on the initial tick without racing a separate /printers fetch. - /gcode-viewer (no trailing slash) dropped from the backend so reloads fall through to the SPA catch-all and keep the layout shell; only /gcode-viewer/ (trailing slash) and /gcode-viewer/<path> remain for the iframe + static assets. Frontend: - PlatePickerModal shown only for multi-plate archives with sliced gcode, grid layout with thumbnails matching the Re-print modal. - Source-only archives show a noGcode toast instead of the empty viewer. - ArchivesPage navigate path swapped to /gcode-viewer?archive=<id> with no trailing slash; GCodeViewerPage iframe forwards window.location.search so the archive reference survives both the initial navigate and a full-page reload. - Viewer iframe's auth path: fetch intercept injects Bearer; a 401 redirects to / so the SPA handles login. Viewer adapter: - Stripped the printer selector, WebSocket subscription, library file picker, tryAutoLoadPrintingFile, BAMBU_BED_SIZES, and updatePrinter- Selector. The viewer no longer observes live printer state. - Bed size derived from /archives/{id}/capabilities.build_volume (extracted from the 3MF's printable_area/printable_height), so H2D, H-family, and any future printer render on the correct bed without a hardcoded map. - loadArchiveById accepts a plate param; fetch intercept rewrites __bambuddy_archive_<id>[_plate<N>] to /archives/<id>/gcode[?plate=N]. Nav + locale cleanup: - Sidebar "GCode Viewer" nav entry removed (viewer is archive-scoped now, not a destination page). - 32 orphaned gcodeViewer locale keys deleted across all 8 locales. - platePicker.{title, hint, plateLabel, objectCount, noGcode} keys added in all 8 locales. ArchivesPage: the now-unreachable ModelViewerModal render paths + its showViewer state removed. ModelViewerModal itself stays — File Manager still uses it for library file previews (plate picker + .3mf 3D model). pre-commit: - gcode_viewer/ excluded from trailing-whitespace + end-of-file-fixer so vendored third-party JS libs don't drift away from upstream. Incidental sweeps picked up by pre-commit and kept (unrelated but benign): - NotificationsPage.tsx: single trailing-whitespace line removed. - spoolbuddy/scripts/pn5180_diag.py: dead `import gpiod` dropped — the pn5180 driver module imported at line 27 does its own `import gpiod` and `gpiod.Chip()` calls, so the diag script's top-level import was never referenced. Tests: - 6 new cases in test_gcode_viewer.py for the backend plate / has_gcode behaviour (plate=N resolution, zero-padded filenames, missing-plate 404, no-plate fallback, plate=0 rejection, has_gcode true/false). - 3 new cases in test_printer_manager.py for name/model WS injection. - PlatePickerModal.test.tsx — 6 frontend cases covering render, plate-name composition, onSelect payload, backdrop close, and thumbnail fallback.
245 lines
7.7 KiB
Python
245 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""PN5180 NFC reader diagnostic script.
|
|
|
|
Connects to a PN5180 over SPI on a Raspberry Pi and reads
|
|
hardware status, version info, and register state.
|
|
|
|
Wiring (from spoolbuddy/README.md):
|
|
PN5180 VCC -> Pi Pin 1 (3.3V)
|
|
PN5180 GND -> Pi Pin 20 (GND)
|
|
PN5180 SCK -> Pi Pin 23 (GPIO11)
|
|
PN5180 MISO -> Pi Pin 21 (GPIO9)
|
|
PN5180 MOSI -> Pi Pin 19 (GPIO10)
|
|
PN5180 NSS -> Pi Pin 16 (GPIO23, manual CS)
|
|
PN5180 BUSY -> Pi Pin 22 (GPIO25)
|
|
PN5180 RST -> Pi Pin 18 (GPIO24)
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "daemon")))
|
|
|
|
|
|
from pn5180 import ( # noqa: E402
|
|
NSS_PIN as DRIVER_NSS_PIN,
|
|
PN5180,
|
|
RST_PIN as DRIVER_RST_PIN,
|
|
SPI_BUS as DRIVER_SPI_BUS,
|
|
SPI_DEVICE as DRIVER_SPI_DEVICE,
|
|
)
|
|
|
|
REG_SYSTEM_CONFIG = 0x00
|
|
REG_IRQ_ENABLE = 0x01
|
|
REG_IRQ_STATUS = 0x02
|
|
REG_IRQ_CLEAR = 0x03
|
|
REG_TRANSCEIVE_CONTROL = 0x04
|
|
REG_TIMER1_RELOAD = 0x0C
|
|
REG_TIMER1_CONFIG = 0x0F
|
|
REG_RX_WAIT_CONFIG = 0x11
|
|
REG_CRC_RX_CONFIG = 0x12
|
|
REG_RX_STATUS = 0x13
|
|
REG_CRC_TX_CONFIG = 0x19
|
|
REG_RF_STATUS = 0x1D
|
|
REG_SYSTEM_STATUS = 0x24
|
|
REG_SIGPRO_CONFIG = 0x1A # Signal Processing Configuration
|
|
REG_TEMP_CONTROL = 0x25
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EEPROM addresses
|
|
# ---------------------------------------------------------------------------
|
|
EEPROM_DIE_IDENTIFIER = 0x00 # 16 bytes
|
|
EEPROM_PRODUCT_VERSION = 0x10 # 2 bytes
|
|
EEPROM_FIRMWARE_VERSION = 0x12 # 2 bytes
|
|
EEPROM_EEPROM_VERSION = 0x14 # 2 bytes
|
|
EEPROM_IRQ_PIN_CONFIG = 0x1A # 1 byte
|
|
|
|
|
|
def _check_spi_device_access() -> str:
|
|
"""Check that the configured spidev exists and can be opened."""
|
|
spi_path = f"/dev/spidev{DRIVER_SPI_BUS}.{DRIVER_SPI_DEVICE}"
|
|
if not os.path.exists(spi_path):
|
|
raise FileNotFoundError(f"SPI device not found: {spi_path}")
|
|
|
|
fd = os.open(spi_path, os.O_RDWR)
|
|
os.close(fd)
|
|
return spi_path
|
|
|
|
|
|
def _self_test_control_pins(nfc: PN5180):
|
|
"""Toggle NSS and RST pins and print observed line state.
|
|
Uses public set_pin/get_pin methods to avoid direct access to driver internals.
|
|
"""
|
|
for pin_name, pin_num in (("NSS", DRIVER_NSS_PIN), ("RST", DRIVER_RST_PIN)):
|
|
nfc.set_pin(pin_num, True)
|
|
time.sleep(0.005)
|
|
active_state = nfc.get_pin(pin_num)
|
|
|
|
nfc.set_pin(pin_num, False)
|
|
time.sleep(0.005)
|
|
inactive_state = nfc.get_pin(pin_num)
|
|
|
|
# Restore idle-high level used by this driver.
|
|
nfc.set_pin(pin_num, True)
|
|
|
|
print(
|
|
f" {pin_name} pin {pin_num}: "
|
|
f"ACTIVE->{'ACTIVE' if active_state else 'INACTIVE'}, "
|
|
f"INACTIVE->{'ACTIVE' if inactive_state else 'INACTIVE'}"
|
|
)
|
|
|
|
|
|
def run_diagnostics():
|
|
print("=" * 60)
|
|
print("PN5180 NFC Reader Diagnostics")
|
|
print("=" * 60)
|
|
|
|
nfc = None
|
|
try:
|
|
print("\n[1] SPI device check...")
|
|
spi_path = _check_spi_device_access()
|
|
print(f" SPI device OK: {spi_path}")
|
|
|
|
nfc = PN5180()
|
|
|
|
print("\n[2] Control pin self-test (NSS/RST)...")
|
|
_self_test_control_pins(nfc)
|
|
|
|
# Reset
|
|
print("\n[3] Hardware reset...")
|
|
nfc.reset()
|
|
print(" Reset OK")
|
|
|
|
# Version info
|
|
print("\n[4] Version info (EEPROM)")
|
|
product = nfc.read_eeprom(EEPROM_PRODUCT_VERSION, 2)
|
|
firmware = nfc.read_eeprom(EEPROM_FIRMWARE_VERSION, 2)
|
|
eeprom = nfc.read_eeprom(EEPROM_EEPROM_VERSION, 2)
|
|
die_id = nfc.read_eeprom(EEPROM_DIE_IDENTIFIER, 16)
|
|
|
|
print(f" Product version : {product[1]}.{product[0]}")
|
|
print(f" Firmware version : {firmware[1]}.{firmware[0]}")
|
|
print(f" EEPROM version : {eeprom[1]}.{eeprom[0]}")
|
|
print(f" Die identifier : {die_id.hex()}")
|
|
|
|
# Register dump
|
|
print("\n[5] Register dump")
|
|
# Use register names from the script (not in pn5180.py)
|
|
REGISTER_NAMES_DUMP = {
|
|
0x00: "SYSTEM_CONFIG",
|
|
0x01: "IRQ_ENABLE",
|
|
0x02: "IRQ_STATUS",
|
|
0x03: "IRQ_CLEAR",
|
|
0x04: "TRANSCEIVE_CONTROL",
|
|
0x0C: "TIMER1_RELOAD",
|
|
0x0F: "TIMER1_CONFIG",
|
|
0x11: "RX_WAIT_CONFIG",
|
|
0x12: "CRC_RX_CONFIG",
|
|
0x13: "RX_STATUS",
|
|
0x19: "CRC_TX_CONFIG",
|
|
0x1A: "SIGPRO_CONFIG",
|
|
0x1D: "RF_STATUS",
|
|
0x24: "SYSTEM_STATUS",
|
|
0x25: "TEMP_CONTROL",
|
|
}
|
|
for addr, name in sorted(REGISTER_NAMES_DUMP.items()):
|
|
val = nfc.read_reg(addr)
|
|
print(f" 0x{addr:02X} {name:<24s} = 0x{val:08X}")
|
|
|
|
# SIGPRO_CONFIG ISO/IEC14443 mode check
|
|
sigpro_val = nfc.read_reg(REG_SIGPRO_CONFIG)
|
|
sigpro_mode = (sigpro_val >> 0) & 0b111
|
|
baudrate_map = {
|
|
0b100: "106 kBd (ISO/IEC14443 type A/B)",
|
|
0b101: "212 kBd (FeliCa 212 kBd)",
|
|
0b110: "424 kBd (FeliCa 424 kBd)",
|
|
0b111: "848 kBd",
|
|
}
|
|
baudrate_str = baudrate_map.get(sigpro_mode, "Unknown or reserved")
|
|
print(f"\n[5b] SIGPRO_CONFIG (0x1A) bits 2:0 = 0b{sigpro_mode:03b} ({baudrate_str})")
|
|
|
|
# IRQ status breakdown
|
|
irq = nfc.read_reg(REG_IRQ_STATUS)
|
|
print(f"\n[6] IRQ status flags (0x{irq:08X})")
|
|
irq_flags = [
|
|
(0, "RX_IRQ"),
|
|
(1, "TX_IRQ"),
|
|
(2, "IDLE_IRQ"),
|
|
(3, "MODE_DETECTED_IRQ"),
|
|
(4, "CARD_ACTIVATED_IRQ"),
|
|
(5, "STATE_CHANGE_IRQ"),
|
|
(6, "RFOFF_DET_IRQ"),
|
|
(7, "RFON_DET_IRQ"),
|
|
(8, "TX_RFOFF_IRQ"),
|
|
(9, "TX_RFON_IRQ"),
|
|
(10, "RF_ACTIVE_ERROR_IRQ"),
|
|
(14, "LPCD_IRQ"),
|
|
]
|
|
for bit, name in irq_flags:
|
|
state = "SET" if irq & (1 << bit) else "---"
|
|
print(f" bit {bit:2d}: {name:<28s} [{state}]")
|
|
|
|
# RF status
|
|
rf = nfc.read_reg(REG_RF_STATUS)
|
|
print(f"\n[7] RF status (0x{rf:08X})")
|
|
tx_rf_on = bool(rf & (1 << 0))
|
|
rx_en = bool(rf & (1 << 1))
|
|
print(f" TX RF active : {tx_rf_on}")
|
|
print(f" RX enabled : {rx_en}")
|
|
|
|
# System status
|
|
sys_stat = nfc.read_reg(REG_SYSTEM_STATUS)
|
|
print(f"\n[8] System status (0x{sys_stat:08X})")
|
|
|
|
# System status bit breakdown
|
|
sys_stat_bits = [
|
|
(9, "LDO_TVDD_OK"),
|
|
(8, "PARAMETER_ERROR"),
|
|
(7, "SYNTAX_ERROR"),
|
|
(6, "SEMANTIC_ERROR"),
|
|
(5, "STBY_PREVENT_RFLD"),
|
|
(4, "BOOT_TEMP"),
|
|
(3, "BOOT_SOFT_RESET"),
|
|
(2, "BOOT_WUC"),
|
|
(1, "BOOT_RFLD"),
|
|
(0, "BOOT_POR"),
|
|
]
|
|
for bit, symbol in sys_stat_bits:
|
|
state = "SET" if sys_stat & (1 << bit) else "---"
|
|
print(f" bit {bit:2d}: {symbol:<18s} [{state}]")
|
|
|
|
# Temperature
|
|
temp_ctrl = nfc.read_reg(REG_TEMP_CONTROL)
|
|
print(f"\n[9] Temp control register (0x{temp_ctrl:08X})")
|
|
|
|
# TEMP_DELTA bits 1:0
|
|
temp_delta = (temp_ctrl >> 0) & 0b11
|
|
temp_delta_map = {
|
|
0b00: "85°C",
|
|
0b01: "115°C",
|
|
0b10: "125°C",
|
|
0b11: "135°C",
|
|
}
|
|
temp_delta_str = temp_delta_map.get(temp_delta, "Unknown")
|
|
print(f" bits 1:0 TEMP_DELTA = 0b{temp_delta:02b} ({temp_delta_str})")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Diagnostics complete - PN5180 is responding over SPI.")
|
|
print("=" * 60)
|
|
|
|
except TimeoutError as e:
|
|
print(f"\nERROR: {e}")
|
|
print("Check wiring and ensure SPI is enabled (dtparam=spi=on in /boot/firmware/config.txt)")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"\nERROR: {e}")
|
|
sys.exit(1)
|
|
finally:
|
|
if nfc is not None:
|
|
nfc.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_diagnostics()
|