diff --git a/display.py b/display.py index cf110cc..9c63f0e 100755 --- a/display.py +++ b/display.py @@ -21,7 +21,7 @@ import random import sys import csv -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageFont from init_shared import shared_data from comment import Commentaireia from logger import Logger @@ -58,7 +58,7 @@ def __init__(self, shared_data): try: self.epd_helper = self.shared_data.epd_helper - # MAX7219 and other non-EPD displays set epd_helper to None; + # MAX7219, LCD1602 and other non-EPD displays set epd_helper to None; # skip EPD-specific init — their _run_* method handles setup. if self.epd_helper is not None: self.epd_helper.init_partial_update() @@ -1524,6 +1524,224 @@ def _info_line(slot, wifi_on, ssid, ap_on): time.sleep(TICK_SLEEP) + # ------------------------------------------------------------------ + # LCD1602 16x2 character LCD display loop + # ------------------------------------------------------------------ + + def _run_lcd1602(self): + """LCD1602 16×2 display loop with two independent rotation timers. + + Top row (15 s each): + Slot 0 — WiFi SSID e.g. "Tango Down 5G " + Slot 1 — IP address e.g. "192.168.1.100 " + Slot 2 — Ragnar status e.g. "NetworkScanner " + + Bottom row (5 s each, 3 slots): + Slot 0 — "Targets: 42 " + Slot 1 — "Vuln: 4 " + Slot 2 — "Credentials: 0 " + + Handles I2C errors gracefully — forces full re-init on next tick. + """ + import subprocess + from resources.waveshare_epd import lcd1602 as _lcd1602_mod + + TOP_INTERVAL = 15.0 # seconds per top-row slot + TOP_SLOTS = 3 + BOTTOM_INTERVAL = 5.0 # seconds per bottom-row slot + BOTTOM_SLOTS = 3 + TICK_SLEEP = 0.5 # polling interval (seconds) + + def _get_ssid(): + try: + res = subprocess.run( + ["iwgetid", "-r"], capture_output=True, text=True, timeout=2 + ) + ssid = res.stdout.strip() + if ssid: + return ssid + except Exception: + pass + if getattr(self.shared_data, "ap_enabled", False): + return "AP MODE" + return "No Network" + + def _get_ip(): + try: + res = subprocess.run( + ["hostname", "-I"], capture_output=True, text=True, timeout=2 + ) + ip = res.stdout.strip().split()[0] + if ip: + return ip + except Exception: + pass + return "No IP" + + def _get_status(): + status = getattr(self.shared_data, "ragnarstatustext", None) or "IDLE" + return str(status) + + def _render_lcd_preview(row0: str, row1: str): + """Write a simulated LCD1602 image to screen.png for the web preview tab.""" + try: + # Colours — classic green-on-dark LCD backlit look + BEZEL_COLOR = (20, 28, 20) + BG_COLOR = (10, 22, 10) + CHAR_COLOR = (80, 255, 100) + CURSOR_COLOR = (40, 120, 50) # darker fill for empty char cells + + BEZEL = 14 # px border around the LCD panel + PAD_X = 18 # horizontal padding inside the panel + PAD_Y = 12 # vertical padding inside the panel + ROW_GAP = 10 # gap between the two character rows + FONT_SIZE = 28 + + # Try common monospace fonts available on the Pi and Windows + font = None + for fp in ( + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf", + "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf", + "/usr/share/fonts/truetype/freefont/FreeMono.ttf", + "C:/Windows/Fonts/cour.ttf", + ): + try: + font = ImageFont.truetype(fp, size=FONT_SIZE) + break + except Exception: + pass + if font is None: + font = ImageFont.load_default() + + # Measure a single character to size the canvas + probe = Image.new("RGB", (1, 1)) + bb = ImageDraw.Draw(probe).textbbox((0, 0), "W", font=font) + char_w = bb[2] - bb[0] + char_h = bb[3] - bb[1] + + panel_w = PAD_X * 2 + char_w * 16 + panel_h = PAD_Y * 2 + char_h * 2 + ROW_GAP + img_w = panel_w + BEZEL * 2 + img_h = panel_h + BEZEL * 2 + + img = Image.new("RGB", (img_w, img_h), BEZEL_COLOR) + draw = ImageDraw.Draw(img) + + # LCD panel background + draw.rectangle( + [BEZEL, BEZEL, BEZEL + panel_w - 1, BEZEL + panel_h - 1], + fill=BG_COLOR, + ) + + # Draw each row of 16 characters + for row_idx, text in enumerate((row0, row1)): + text = (text or "").ljust(16)[:16] + x = BEZEL + PAD_X + y = BEZEL + PAD_Y + row_idx * (char_h + ROW_GAP) + draw.text((x, y), text, font=font, fill=CHAR_COLOR) + + webdir = getattr(self.shared_data, "webdir", None) + if webdir: + img.save(os.path.join(webdir, "screen.png")) + except Exception as exc: + logger.error(f"LCD1602 preview render error: {exc}") + + # ── Initialise display ────────────────────────────────────────── + _i2c_raw = self.config.get("lcd1602_i2c_address", "0x27") + i2c_addr = int(_i2c_raw, 16) if isinstance(_i2c_raw, str) else int(_i2c_raw) + i2c_bus = int(self.config.get("lcd1602_i2c_bus", 1)) + + epd = _lcd1602_mod.EPD(i2c_address=i2c_addr, i2c_bus=i2c_bus) + try: + epd.init() + except Exception as exc: + logger.error(f"LCD1602 init failed: {exc}") + + _last_line0 = None + _last_line1 = None + _top_slot = 0 + _bottom_slot = 0 + _top_start = time.monotonic() - TOP_INTERVAL # trigger write on first tick + _bottom_start = time.monotonic() - BOTTOM_INTERVAL # trigger write on first tick + + # ── Main loop ─────────────────────────────────────────────────── + _hw_line0 = None # tracks what is currently written to hardware + _hw_line1 = None + while not self.shared_data.display_should_exit: + # — compute current content (slot timers + data) — + try: + sd = self.shared_data + now = time.monotonic() + + # — advance top slot — + if now - _top_start >= TOP_INTERVAL: + _top_slot = (_top_slot + 1) % TOP_SLOTS + _top_start = now + + # — advance bottom slot — + if now - _bottom_start >= BOTTOM_INTERVAL: + _bottom_slot = (_bottom_slot + 1) % BOTTOM_SLOTS + _bottom_start = now + + # — gather data — + targets = getattr(sd, "total_targetnbr", 0) or 0 + vulns = getattr(sd, "vulnnbr", 0) or 0 + creds = getattr(sd, "crednbr", 0) or 0 + + # — build top line — + if _top_slot == 0: + line0 = _get_ssid() + elif _top_slot == 1: + line0 = _get_ip() + else: + line0 = _get_status() + line0 = line0.ljust(16)[:16] + + # — build bottom line — + if _bottom_slot == 0: + line1 = f"Targets: {targets}" + elif _bottom_slot == 1: + line1 = f"Vuln: {vulns}" + else: + line1 = f"Credentials: {creds}" + line1 = line1.ljust(16)[:16] + + # — update web preview whenever content changes — + if line0 != _last_line0 or line1 != _last_line1: + _render_lcd_preview(line0, line1) + _last_line0 = line0 + _last_line1 = line1 + + except Exception as exc: + logger.error(f"LCD1602 data error: {exc}") + + # — write to hardware (isolated so I2C errors don't block preview) — + try: + if not epd._initialized: + epd.init() + _hw_line0 = None # force full rewrite after reinit + _hw_line1 = None + if _last_line0 is not None and _last_line0 != _hw_line0: + epd.write_line(0, _last_line0) + _hw_line0 = _last_line0 + if _last_line1 is not None and _last_line1 != _hw_line1: + epd.write_line(1, _last_line1) + _hw_line1 = _last_line1 + except Exception as exc: + logger.error(f"LCD1602 hardware write error: {exc}") + epd._initialized = False + _hw_line0 = None + _hw_line1 = None + + time.sleep(TICK_SLEEP) + + # ── Cleanup on exit ───────────────────────────────────────────── + try: + epd.Clear() + epd.sleep() + except Exception as exc: + logger.error(f"LCD1602 shutdown error: {exc}") + # ------------------------------------------------------------------ # SSD1306 0.96" 128x64 monochrome OLED display loop # ------------------------------------------------------------------ @@ -1875,6 +2093,10 @@ def run(self): self._run_ssd1306() return + if self.config.get("epd_type") == "lcd1602": + self._run_lcd1602() + return + if self.config.get("epd_type") in ("max7219_4panel", "max7219_8panel"): self._run_max7219() return diff --git a/install_ragnar.sh b/install_ragnar.sh index 57ec48a..a3f7daf 100755 --- a/install_ragnar.sh +++ b/install_ragnar.sh @@ -806,6 +806,17 @@ print('SUCCESS: Set shared_config.json epd_type to $EPD_VERSION') # Enable I2C interface raspi-config nonint do_i2c 0 2>/dev/null || true log "INFO" "I2C interface enabled for SSD1306" + elif [ "$EPD_VERSION" = "lcd1602" ]; then + if [ -f "$ragnar_PATH/resources/waveshare_epd/lcd1602.py" ]; then + log "SUCCESS" "LCD1602 driver verified (resources/waveshare_epd/lcd1602.py)" + else + log "ERROR" "LCD1602 driver not found at $ragnar_PATH/resources/waveshare_epd/lcd1602.py" + fi + # Install smbus2 for I2C communication + pip3 install smbus2 --break-system-packages >/dev/null 2>&1 + # Enable I2C interface + raspi-config nonint do_i2c 0 2>/dev/null || true + log "INFO" "I2C interface enabled for LCD1602" else log "INFO" "Verifying Waveshare e-Paper library installation for $EPD_VERSION..." cd /home/$ragnar_USER/e-Paper/RaspberryPi_JetsonNano/python @@ -1530,19 +1541,21 @@ main() { echo -e "\n${BLUE}Select your TFT/OLED display:${NC}" echo "1. GC9A01 (1.28\" Round 240x240)" echo "2. SSD1306 (0.96\" OLED 128x64)" - echo "3. No display (headless install)" + echo "3. LCD1602 (16x2 I2C Character LCD)" + echo "4. No display (headless install)" while true; do - read -p "Enter your choice (1-3): " tft_choice + read -p "Enter your choice (1-4): " tft_choice case $tft_choice in 1) EPD_VERSION="gc9a01"; break;; 2) EPD_VERSION="ssd1306"; break;; - 3) + 3) EPD_VERSION="lcd1602"; break;; + 4) select_headless_variant EPD_VERSION="" break ;; - *) echo -e "${RED}Invalid choice. Please select 1-3.${NC}";; + *) echo -e "${RED}Invalid choice. Please select 1-4.${NC}";; esac done @@ -1667,14 +1680,17 @@ except: echo -e "${CYAN} OLED displays:${NC}" echo "11. SSD1306 (0.96\" OLED 128x64)" echo "" + echo -e "${CYAN} Character LCD:${NC}" + echo "12. LCD1602 (16x2 I2C Character LCD)" + echo "" echo -e "${CYAN} LED Matrix displays:${NC}" - echo "12. MAX7219 (8 panels 64×8 LED matrix)" - echo "13. MAX7219 (4 panels 32×8 LED matrix)" + echo "13. MAX7219 (8 panels 64×8 LED matrix)" + echo "14. MAX7219 (4 panels 32×8 LED matrix)" echo "" - echo "14. No display (headless install)" + echo "15. No display (headless install)" while true; do - read -p "Enter your choice (1-14): " epd_choice + read -p "Enter your choice (1-15): " epd_choice case $epd_choice in 1) EPD_VERSION="epd2in13"; break;; 2) EPD_VERSION="epd2in13_V2"; break;; @@ -1687,14 +1703,15 @@ except: 9) EPD_VERSION="epd4in26"; break;; 10) EPD_VERSION="gc9a01"; break;; 11) EPD_VERSION="ssd1306"; break;; - 12) EPD_VERSION="max7219_8panel"; break;; - 13) EPD_VERSION="max7219_4panel"; break;; - 14) + 12) EPD_VERSION="lcd1602"; break;; + 13) EPD_VERSION="max7219_8panel"; break;; + 14) EPD_VERSION="max7219_4panel"; break;; + 15) select_headless_variant EPD_VERSION="" break ;; - *) echo -e "${RED}Invalid choice. Please select 1-14.${NC}";; + *) echo -e "${RED}Invalid choice. Please select 1-15.${NC}";; esac done diff --git a/resources/waveshare_epd/lcd1602.py b/resources/waveshare_epd/lcd1602.py new file mode 100644 index 0000000..0e78e6a --- /dev/null +++ b/resources/waveshare_epd/lcd1602.py @@ -0,0 +1,273 @@ +# lcd1602.py +# Driver for the LCD1602 16x2 character LCD with PCF8574 I2C backpack. +# +# Exposes the same interface as other Ragnar display drivers so it integrates +# with the rest of the display system: +# width, height, init(), Clear(), write_line(row, text), sleep() +# +# The PCF8574 I2C expander maps to LCD pins as follows: +# Bit 7 (P7) = D7 Bit 6 (P6) = D6 Bit 5 (P5) = D5 Bit 4 (P4) = D4 +# Bit 3 (P3) = BL Bit 2 (P2) = EN Bit 1 (P1) = RW Bit 0 (P0) = RS +# +# Wiring (Raspberry Pi ↔ LCD1602 I2C backpack): +# VCC → Pin 2 (5V) +# GND → Pin 6 (GND) +# SDA → Pin 3 (GPIO 2 / I2C SDA) +# SCL → Pin 5 (GPIO 3 / I2C SCL) +# +# Default I2C address is 0x27 (A0/A1/A2 all HIGH on PCF8574). +# If the display is not found at 0x27, 0x3F is tried automatically. + +import logging +import time + +logger = logging.getLogger(__name__) + +try: + import smbus2 as smbus2_mod + _SMBUS2_AVAILABLE = True +except ImportError: + smbus2_mod = None + _SMBUS2_AVAILABLE = False + logger.warning( + "smbus2 not available — LCD1602 driver will not function. " + "Install with: pip3 install smbus2" + ) + +# PCF8574 pin bitmasks +_BL = 0x08 # P3 — backlight +_EN = 0x04 # P2 — enable (pulse to latch) +_RW = 0x02 # P1 — read/write (always 0 = write) +_RS = 0x01 # P0 — register select (0=cmd, 1=data) + +# LCD1602 commands +_CMD_CLEARDISPLAY = 0x01 +_CMD_RETURNHOME = 0x02 +_CMD_ENTRYMODESET = 0x04 +_CMD_DISPLAYCONTROL = 0x08 +_CMD_FUNCTIONSET = 0x20 + +_FLAG_ENTRY_INCREMENT = 0x02 +_FLAG_DISPLAY_ON = 0x04 +_FLAG_FUNCTION_4BIT = 0x00 +_FLAG_FUNCTION_2LINE = 0x08 +_FLAG_FUNCTION_5X8 = 0x00 + +# Row start addresses for each physical line +_ROW_OFFSETS = [0x00, 0x40] + +LCD_WIDTH = 16 +LCD_HEIGHT = 2 + +# Candidate I2C addresses tried during auto-detection (most common first) +_CANDIDATE_ADDRESSES = [0x27, 0x3F] + + +class EPD: + """LCD1602 16×2 character LCD driver (PCF8574 I2C backpack). + + Provides the same public interface as the Waveshare e-Paper drivers used + throughout Ragnar so it integrates transparently with the display system. + """ + + def __init__(self, i2c_address=0x27, i2c_bus=1): + self.width = LCD_WIDTH + self.height = LCD_HEIGHT + self._addr = i2c_address + self._bus_num = i2c_bus + self._bus = None + self._backlight = _BL # backlight ON by default + self._initialized = False + + # ------------------------------------------------------------------ + # Public interface + # ------------------------------------------------------------------ + + def init(self, *args): + """Initialise the I2C bus and run the LCD1602 startup sequence. + + Idempotent: returns immediately if already initialised. + Auto-detects I2C address if the configured one is unreachable. + """ + if self._initialized: + return + self._open_bus() + self._addr = self._detect_address() + self._init_sequence() + self._initialized = True + logger.info( + "LCD1602 initialised (%dx%d) at I2C 0x%02X bus %d", + self.width, self.height, self._addr, self._bus_num, + ) + + def Clear(self): + """Clear the display and return cursor to home.""" + if not self._initialized: + self.init() + self._send_cmd(_CMD_CLEARDISPLAY) + time.sleep(0.002) # clear takes up to 1.52 ms + logger.debug("LCD1602 cleared") + + def write_line(self, row: int, text: str): + """Write *text* to the given *row* (0 or 1), padded/truncated to 16 chars. + + Non-ASCII characters are replaced with '?' since the HD44780 ROM only + covers ASCII + Japanese kana glyphs. This is the primary method used + by the display loop. + """ + if not self._initialized: + self.init() + row = max(0, min(row, LCD_HEIGHT - 1)) + text = text.encode("ascii", errors="replace").decode("ascii") + text = text.ljust(LCD_WIDTH)[:LCD_WIDTH] + self._set_cursor(row, 0) + for ch in text: + self._send_data(ord(ch)) + + def backlight(self, on: bool = True): + """Turn the backlight on or off.""" + self._backlight = _BL if on else 0x00 + # Write a dummy byte to push the backlight state to the expander + try: + self._write_byte(self._backlight) + except Exception as exc: + logger.warning("LCD1602 backlight control failed: %s", exc) + + def sleep(self): + """Turn off the display and backlight to save power. + + Resets *_initialized* so a subsequent call to ``init()`` will re-run + the full HD44780 startup sequence. + """ + try: + if self._initialized: + self._send_cmd(_CMD_DISPLAYCONTROL) # display OFF (all flags cleared) + self.backlight(False) + self._initialized = False + logger.info("LCD1602 sleeping") + except Exception as exc: + logger.error("LCD1602 sleep error: %s", exc) + + # ------------------------------------------------------------------ + # Helpers — hardware / I2C + # ------------------------------------------------------------------ + + def _open_bus(self): + """Open the I2C bus (idempotent).""" + if self._bus is not None: + return + if not _SMBUS2_AVAILABLE: + raise ImportError( + "smbus2 is required for the LCD1602 driver. " + "Install with: pip3 install smbus2" + ) + try: + self._bus = smbus2_mod.SMBus(self._bus_num) + except Exception as exc: + logger.error("LCD1602: cannot open I2C bus %d: %s", self._bus_num, exc) + raise + + def _detect_address(self) -> int: + """Return the first responsive I2C address from _CANDIDATE_ADDRESSES. + + If the configured address responds, it is used immediately. + Otherwise the candidates are tried in order and a warning is logged. + Falls back to the configured address if none responds. + """ + # Try configured address first + if self._probe(self._addr): + return self._addr + + logger.warning( + "LCD1602: no response at I2C 0x%02X — probing alternative addresses", + self._addr, + ) + for addr in _CANDIDATE_ADDRESSES: + if addr != self._addr and self._probe(addr): + logger.info("LCD1602: found device at 0x%02X", addr) + return addr + + logger.warning( + "LCD1602: no device found; falling back to 0x%02X — check wiring", + self._addr, + ) + return self._addr + + def _probe(self, addr: int) -> bool: + """Return True if an I2C device responds at *addr*.""" + try: + self._bus.read_byte(addr) + return True + except Exception: + return False + + def _write_byte(self, data: int): + """Write a single byte to the PCF8574 expander.""" + try: + self._bus.write_byte(self._addr, data) + except Exception as exc: + logger.error("LCD1602: I2C write error at 0x%02X: %s", self._addr, exc) + raise + + def _pulse_enable(self, data: int): + """Pulse the EN pin high then low to latch a nibble.""" + self._write_byte(data | _EN) # EN high + time.sleep(0.0005) + self._write_byte(data & ~_EN) # EN low + time.sleep(0.0001) + + def _send_nibble(self, nibble: int, mode: int): + """Send a 4-bit nibble. *mode* is _RS for data or 0 for command.""" + high = (nibble & 0xF0) | self._backlight | mode + self._write_byte(high) + self._pulse_enable(high) + + def _send_cmd(self, cmd: int): + """Send an 8-bit command to the LCD (RS=0).""" + self._send_nibble(cmd & 0xF0, 0) + self._send_nibble((cmd << 4) & 0xF0, 0) + + def _send_data(self, data: int): + """Send an 8-bit data byte to the LCD (RS=1).""" + self._send_nibble(data & 0xF0, _RS) + self._send_nibble((data << 4) & 0xF0, _RS) + + def _set_cursor(self, row: int, col: int): + """Move cursor to (row, col).""" + addr = _ROW_OFFSETS[row] + col + self._send_cmd(0x80 | addr) + + # ------------------------------------------------------------------ + # Initialisation sequence (HD44780 4-bit mode) + # ------------------------------------------------------------------ + + def _init_sequence(self): + """Run the HD44780 initialisation sequence for 4-bit operation.""" + time.sleep(0.05) # wait >40 ms after VCC rises to 2.7 V + + # Three wake-up writes in 8-bit mode (before switching to 4-bit) + for _ in range(3): + self._send_nibble(0x30, 0) + time.sleep(0.005) + + # Switch to 4-bit mode + self._send_nibble(0x20, 0) + time.sleep(0.001) + + # Function set: 4-bit, 2 lines, 5×8 font + self._send_cmd(_CMD_FUNCTIONSET | _FLAG_FUNCTION_2LINE | _FLAG_FUNCTION_5X8) + time.sleep(0.001) + + # Display control: display ON, cursor OFF, blink OFF + self._send_cmd(_CMD_DISPLAYCONTROL | _FLAG_DISPLAY_ON) + time.sleep(0.001) + + # Clear display + self._send_cmd(_CMD_CLEARDISPLAY) + time.sleep(0.002) + + # Entry mode: increment cursor, no display shift + self._send_cmd(_CMD_ENTRYMODESET | _FLAG_ENTRY_INCREMENT) + time.sleep(0.001) + + logger.debug("LCD1602 init sequence complete") diff --git a/shared.py b/shared.py index f2cca7f..a755ed4 100755 --- a/shared.py +++ b/shared.py @@ -65,6 +65,7 @@ "4in26": "epd4in26", "1in28_tft": "gc9a01", "0in96_oled": "ssd1306", + "1602_lcd": "lcd1602", } def resolve_epd_type(size_key, current_epd_type=None): @@ -104,7 +105,10 @@ def resolve_epd_type(size_key, current_epd_type=None): # GC9A01 1.28" 240x240 round colour TFT LCD "gc9a01": {"ref_width": DESIGN_REF_WIDTH, "ref_height": DESIGN_REF_WIDTH, "default_flip": False}, # SSD1306 0.96" 128x64 monochrome OLED - "ssd1306": {"ref_width": 128, "ref_height": 64, "default_flip": False}, + "ssd1306": {"ref_width": 128, "ref_height": 64, "default_flip": False}, + # LCD1602 16x2 character LCD (I2C via PCF8574 backpack) + "lcd1602": {"ref_width": 16, "ref_height": 2, "default_flip": False}, + # MAX7219 LED matrix displays "max7219_4panel": {"ref_width": 32, "ref_height": 8, "default_flip": False}, "max7219_8panel": {"ref_width": 64, "ref_height": 8, "default_flip": False}, } @@ -561,6 +565,8 @@ def get_default_config(self): "screen_reversed": default_profile.get("default_flip", False), "gc9a01_mascot_color": "#96C8FF", "ssd1306_i2c_address": "0x3C", + "lcd1602_i2c_address": "0x27", + "lcd1602_i2c_bus": 1, "display_brightness": 8, "spi_clock_mhz": 2, "max7219_spi_port": 0, @@ -841,16 +847,22 @@ def initialize_epd_display(self): self.web_screen_reversed = self.screen_reversed return - # MAX7219 LED matrix is managed entirely by display.py — skip EPD init - _epd_type_check = self.config.get('epd_type', '') - if _epd_type_check in ("max7219_4panel", "max7219_8panel"): - logger.info(f"MAX7219 display configured ({_epd_type_check}) — skipping EPD init") + # Non-EPD displays (character LCDs and LED matrices) have no PIL buffer + # interface — skip EPDHelper init entirely for these types. + _NON_EPD_DISPLAYS = {"lcd1602", "max7219_4panel", "max7219_8panel"} + epd_type_cfg = self.config.get("epd_type", DEFAULT_EPD_TYPE) + if epd_type_cfg in _NON_EPD_DISPLAYS: + profile = DISPLAY_PROFILES.get(epd_type_cfg, {}) + self.width = profile.get("ref_width", 16) + self.height = profile.get("ref_height", 2) self.epd_helper = None - profile = DISPLAY_PROFILES.get(_epd_type_check, {"ref_width": 64, "ref_height": 8, "default_flip": False}) - self.width = profile['ref_width'] - self.height = profile['ref_height'] - self.screen_reversed = bool(self.config.get('screen_reversed', False)) + self.screen_reversed = bool(self.config.get("screen_reversed", False)) self.web_screen_reversed = self.screen_reversed + self.apply_display_profile(epd_type_cfg) + logger.info( + f"Non-EPD display '{epd_type_cfg}' configured: {self.width}x{self.height}" + " — skipping EPD buffer init" + ) return try: diff --git a/web/index_modern.html b/web/index_modern.html index 3bcbdb5..72d82c7 100644 --- a/web/index_modern.html +++ b/web/index_modern.html @@ -4004,6 +4004,7 @@
Displ + diff --git a/web/scripts/ragnar_modern.js b/web/scripts/ragnar_modern.js index 144eba4..84a543e 100644 --- a/web/scripts/ragnar_modern.js +++ b/web/scripts/ragnar_modern.js @@ -313,6 +313,10 @@ const configMetadata = { label: "GC9A01 Mascot Tint Color", description: "Tint color applied to the animated mascot on the GC9A01 1.28\" round TFT display. Only visible when GC9A01 is selected." }, + lcd1602_i2c_address: { + label: "LCD1602 I2C Address", + description: "I2C address of the PCF8574 backpack on the LCD1602 16×2 character display. Common values: 0x27 (most common) or 0x3F. Auto-detected if unreachable." + }, display_brightness: { label: "Display Brightness", description: "Brightness level for non-e-ink displays (SSD1306, GC9A01, MAX7219). Range 0–15. Default: 8." @@ -496,6 +500,7 @@ function epdTypeToSizeKey(epd_type) { if (epd_type.startsWith('epd4in26')) return '4in26'; if (epd_type === 'gc9a01') return '1in28_tft'; if (epd_type === 'ssd1306') return '0in96_oled'; + if (epd_type === 'lcd1602') return 'lcd1602'; return epd_type; // fallback: return as-is } @@ -509,6 +514,7 @@ const displaySelectOptions = { { value: '4in26', label: '4.26" e-Paper (800x480)' }, { value: '1in28_tft', label: '1.28" GC9A01 Round TFT (240x240)' }, { value: '0in96_oled', label: '0.96" SSD1306 OLED (128x64)' }, + { value: 'lcd1602', label: '16×2 LCD1602 Character LCD (I2C)' }, { value: 'max7219_8panel', label: 'MAX7219 8-panel LED Matrix (64×8)' }, { value: 'max7219_4panel', label: 'MAX7219 4-panel LED Matrix (32×8)' } ], @@ -9917,15 +9923,16 @@ function displayConfigForm(config) { 'General': ['manual_mode', 'debug_mode', 'scan_vuln_running', 'scan_vuln_no_ports', 'enable_attacks', 'blacklistcheck'], 'Network': ['network_max_failed_pings'], 'Timing': ['startup_delay', 'web_delay', 'screen_delay', 'scan_interval'], - 'Display': ['epd_type', 'screen_reversed', 'spi_clock_mhz', 'gc9a01_mascot_color', 'ssd1306_i2c_address', 'max7219_spi_port', 'max7219_spi_device', 'max7219_block_orientation', 'display_brightness'] + 'Display': ['epd_type', 'screen_reversed', 'spi_clock_mhz', 'gc9a01_mascot_color', 'ssd1306_i2c_address', 'lcd1602_i2c_address', 'max7219_spi_port', 'max7219_spi_device', 'max7219_block_orientation', 'display_brightness'] }; const knownBooleans = ['manual_mode', 'debug_mode', 'scan_vuln_running', 'scan_vuln_no_ports', 'enable_attacks', 'blacklistcheck', 'screen_reversed']; - const alwaysShowKeys = new Set(['network_max_failed_pings', 'gc9a01_mascot_color', 'ssd1306_i2c_address', 'spi_clock_mhz', 'max7219_spi_port', 'max7219_spi_device', 'max7219_block_orientation', 'display_brightness']); + const alwaysShowKeys = new Set(['network_max_failed_pings', 'gc9a01_mascot_color', 'ssd1306_i2c_address', 'lcd1602_i2c_address', 'spi_clock_mhz', 'max7219_spi_port', 'max7219_spi_device', 'max7219_block_orientation', 'display_brightness']); const fallbackValues = { network_max_failed_pings: 15, gc9a01_mascot_color: '#96C8FF', ssd1306_i2c_address: '0x3C', + lcd1602_i2c_address: '0x27', spi_clock_mhz: 2, max7219_spi_port: 0, max7219_spi_device: 0, @@ -10027,32 +10034,32 @@ function displayConfigForm(config) {

Most modules use 0x3C. Use 0x3D if display doesn't initialize.

`; - } else if (key === 'max7219_spi_port') { - const spiPort = (value !== undefined && value !== null) ? value : 0; + } else if (key === 'lcd1602_i2c_address') { + const currentVal = (value && typeof value === 'string') ? value : '0x27'; html += ` -
+
- -

SPI bus (0 = SPI0, 1 = SPI1). Default: 0.

+ value="${currentVal}" placeholder="0x27 or 0x3F"> +

Most PCF8574 backpacks use 0x27. Address is auto-detected if unreachable.

`; - } else if (key === 'max7219_spi_device') { - const spiDev = (value !== undefined && value !== null) ? value : 0; + } else if (key === 'max7219_spi_port') { + const spiPort = (value !== undefined && value !== null) ? value : 0; html += ` -
+
- -

CE pin (0 = CE0/Pin24, 1 = CE1/Pin26). Default: 0.

+ value="${spiPort}" min="0" max="1"> +

SPI bus (0 = SPI0, 1 = SPI1). Default: 0.

`; } else if (key === 'max7219_block_orientation') { @@ -10095,6 +10102,7 @@ function displayConfigForm(config) { class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-sm font-mono" value="${clockMhz}" min="0.5" max="4" step="0.5">

2 MHz recommended with PiSugar. Drop to 1 MHz if pixels are still corrupted. Takes effect after service restart.

+>>>>>>> upstream/main
`; } else { @@ -10133,6 +10141,7 @@ function displayConfigForm(config) { const epdSelect = document.querySelector('select[name="epd_type"]'); const colorRow = document.getElementById('cfg-gc9a01-color-row'); const addrRow = document.getElementById('cfg-ssd1306-addr-row'); + const lcdAddrRow = document.getElementById('cfg-lcd1602-addr-row'); const max7219SpiPortRow = document.getElementById('cfg-max7219-spi-port-row'); const max7219SpiDevRow = document.getElementById('cfg-max7219-spi-device-row'); const max7219BlockRow = document.getElementById('cfg-max7219-block-row'); @@ -10143,9 +10152,11 @@ function displayConfigForm(config) { const isMax7219 = val === 'max7219_8panel' || val === 'max7219_4panel'; const isSsd1306 = val === '0in96_oled'; const isGc9a01 = val === '1in28_tft'; - const isEpaper = !isMax7219 && !isSsd1306 && !isGc9a01; + const isLcd1602 = val === 'lcd1602'; + const isEpaper = !isMax7219 && !isSsd1306 && !isGc9a01 && !isLcd1602; if (colorRow) colorRow.style.display = isGc9a01 ? '' : 'none'; if (addrRow) addrRow.style.display = isSsd1306 ? '' : 'none'; + if (lcdAddrRow) lcdAddrRow.style.display = isLcd1602 ? '' : 'none'; if (max7219SpiPortRow) max7219SpiPortRow.style.display = isMax7219 ? '' : 'none'; if (max7219SpiDevRow) max7219SpiDevRow.style.display = isMax7219 ? '' : 'none'; if (max7219BlockRow) max7219BlockRow.style.display = isMax7219 ? '' : 'none';