Add .gitignore and eink-terminal.service; enhance virtual terminal with preview toggle and quadrant zoom
This commit is contained in:
parent
9005e49d82
commit
8880aa6b32
3 changed files with 270 additions and 21 deletions
134
.gitignore
vendored
Normal file
134
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
20
eink-terminal.service
Normal file
20
eink-terminal.service
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[Unit]
|
||||
Description=E-Ink Virtual Terminal
|
||||
After=bluetooth.target multi-user.target
|
||||
Wants=bluetooth.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=kacper
|
||||
WorkingDirectory=/home/kacper/Documents/eink
|
||||
ExecStart=/usr/bin/python3 /home/kacper/Documents/eink/virtual_terminal_eink.py --no-preview
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Allow access to GPIO/SPI and input devices
|
||||
SupplementaryGroups=gpio spi input
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -68,7 +68,7 @@ DISPLAY_CONFIGS = {
|
|||
}
|
||||
|
||||
class VirtualTerminalEInk:
|
||||
def __init__(self, display_type='epd2in13_V4', use_display=True, zoom=1.0):
|
||||
def __init__(self, display_type='epd2in13_V4', use_display=True, zoom=1.0, show_preview=True):
|
||||
"""
|
||||
Initialize the virtual terminal for e-ink display
|
||||
|
||||
|
|
@ -76,10 +76,12 @@ class VirtualTerminalEInk:
|
|||
display_type: Type of e-ink display (see DISPLAY_CONFIGS)
|
||||
use_display: Whether to actually use the e-ink display (False for testing)
|
||||
zoom: Zoom multiplier for rendering (1.0 = normal, 2.0 = 2x larger text, etc.)
|
||||
show_preview: Whether to show ASCII preview in console output
|
||||
"""
|
||||
self.display_type = display_type
|
||||
self.use_display = use_display
|
||||
self.zoom = max(1.0, float(zoom)) # Ensure zoom is at least 1.0
|
||||
self.show_preview = show_preview
|
||||
self.config = DISPLAY_CONFIGS.get(display_type)
|
||||
|
||||
# Serialize console output (preview redraw + status messages) so it doesn't
|
||||
|
|
@ -118,6 +120,10 @@ class VirtualTerminalEInk:
|
|||
self._update_timer = None
|
||||
self._update_lock = threading.Lock()
|
||||
|
||||
# Quadrant zoom feature (F5-F8 keys)
|
||||
self._quadrant_view = None # None, 'top_left', 'top_right', 'bottom_left', 'bottom_right'
|
||||
self._quadrant_changed = False # Flag to force refresh when quadrant changes
|
||||
|
||||
# Initialize e-ink display
|
||||
self.epd = None
|
||||
if self.use_display:
|
||||
|
|
@ -464,8 +470,9 @@ class VirtualTerminalEInk:
|
|||
def _do_update_display(self):
|
||||
"""Internal: Actually perform the display update (called after debounce)."""
|
||||
with self._console_lock:
|
||||
# Display preview in console
|
||||
self.display_preview()
|
||||
# Display preview in console (if enabled)
|
||||
if self.show_preview:
|
||||
self.display_preview()
|
||||
|
||||
# Update e-ink display (silence driver prints to avoid tearing the preview)
|
||||
if self.use_display and self.epd:
|
||||
|
|
@ -479,6 +486,12 @@ class VirtualTerminalEInk:
|
|||
curr_cursor,
|
||||
)
|
||||
|
||||
# Force update if quadrant view changed (even if screen didn't change)
|
||||
if self._quadrant_changed:
|
||||
self._safe_print(f"[DEBUG] Forcing display update due to quadrant view change")
|
||||
dirty_cells = (0, 0, self.cols - 1, self.rows - 1) # Force full refresh
|
||||
self._quadrant_changed = False # Reset flag
|
||||
|
||||
# Nothing changed -> skip panel update.
|
||||
if dirty_cells is None:
|
||||
self._last_snapshot = curr_snapshot
|
||||
|
|
@ -493,7 +506,27 @@ class VirtualTerminalEInk:
|
|||
|
||||
image = self.render_to_image()
|
||||
|
||||
# Apply zoom if needed
|
||||
# Apply quadrant view if active (before zoom)
|
||||
if self._quadrant_view is not None:
|
||||
self._safe_print(f"[DEBUG] Applying quadrant view: {self._quadrant_view}")
|
||||
# Crop to the selected quadrant
|
||||
half_width = image.width // 2
|
||||
half_height = image.height // 2
|
||||
|
||||
if self._quadrant_view == 'top_left':
|
||||
image = image.crop((0, 0, half_width, half_height))
|
||||
elif self._quadrant_view == 'top_right':
|
||||
image = image.crop((half_width, 0, image.width, half_height))
|
||||
elif self._quadrant_view == 'bottom_left':
|
||||
image = image.crop((0, half_height, half_width, image.height))
|
||||
elif self._quadrant_view == 'bottom_right':
|
||||
image = image.crop((half_width, half_height, image.width, image.height))
|
||||
|
||||
self._safe_print(f"[DEBUG] Cropped to size: {image.size}, scaling to display: {self.config['width']}x{self.config['height']}")
|
||||
# Scale the quadrant to full display size
|
||||
image = image.resize((self.config['width'], self.config['height']), Image.NEAREST)
|
||||
|
||||
# Apply zoom if needed (independent of quadrant view)
|
||||
if self.zoom > 1.0:
|
||||
new_width = int(image.width * self.zoom)
|
||||
new_height = int(image.height * self.zoom)
|
||||
|
|
@ -586,7 +619,7 @@ class VirtualTerminalEInk:
|
|||
# Proper QWERTY keyboard layout mapping based on actual evdev keycodes
|
||||
key_map = {
|
||||
# Special keys
|
||||
ecodes.KEY_ENTER: '\n',
|
||||
ecodes.KEY_ENTER: '\r', # Send carriage return (terminal driver handles conversion)
|
||||
ecodes.KEY_TAB: '\t',
|
||||
ecodes.KEY_SPACE: ' ',
|
||||
ecodes.KEY_BACKSPACE: '\x7f',
|
||||
|
|
@ -603,6 +636,18 @@ class VirtualTerminalEInk:
|
|||
ecodes.KEY_DOT: '.',
|
||||
ecodes.KEY_SLASH: '/',
|
||||
|
||||
# Arrow keys (ANSI escape sequences for terminal navigation)
|
||||
ecodes.KEY_UP: '\x1b[A',
|
||||
ecodes.KEY_DOWN: '\x1b[B',
|
||||
ecodes.KEY_RIGHT: '\x1b[C',
|
||||
ecodes.KEY_LEFT: '\x1b[D',
|
||||
|
||||
# Function keys (for quadrant zoom)
|
||||
ecodes.KEY_F5: '<F5>',
|
||||
ecodes.KEY_F6: '<F6>',
|
||||
ecodes.KEY_F7: '<F7>',
|
||||
ecodes.KEY_F8: '<F8>',
|
||||
|
||||
# Number row (unshifted)
|
||||
ecodes.KEY_1: '1', ecodes.KEY_2: '2', ecodes.KEY_3: '3',
|
||||
ecodes.KEY_4: '4', ecodes.KEY_5: '5', ecodes.KEY_6: '6',
|
||||
|
|
@ -662,6 +707,11 @@ class VirtualTerminalEInk:
|
|||
if event.type == ecodes.EV_KEY:
|
||||
key_event = categorize(event)
|
||||
|
||||
# Debug: show all key events
|
||||
if event.value == 1: # Key press
|
||||
key_name = ecodes.KEY[event.code] if event.code in ecodes.KEY else f"UNKNOWN({event.code})"
|
||||
self._safe_print(f"[DEBUG] Key pressed: {key_name} (code: {event.code})")
|
||||
|
||||
# Track modifier keys
|
||||
if event.code in (ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT):
|
||||
shift_pressed = (event.value == 1)
|
||||
|
|
@ -674,32 +724,73 @@ class VirtualTerminalEInk:
|
|||
if event.value != 1:
|
||||
continue
|
||||
|
||||
# Handle Ctrl+D to exit
|
||||
if ctrl_pressed and event.code == ecodes.KEY_D:
|
||||
self.stream.feed("\r\n[Exiting...]\r\n")
|
||||
self.update_display()
|
||||
self.running = False
|
||||
break
|
||||
# Handle Ctrl+letter combinations
|
||||
if ctrl_pressed:
|
||||
# Map letter keys to Ctrl codes (Ctrl+A = 1, Ctrl+B = 2, ..., Ctrl+Z = 26)
|
||||
ctrl_letter_map = {
|
||||
ecodes.KEY_A: 1, ecodes.KEY_B: 2, ecodes.KEY_C: 3, ecodes.KEY_D: 4,
|
||||
ecodes.KEY_E: 5, ecodes.KEY_F: 6, ecodes.KEY_G: 7, ecodes.KEY_H: 8,
|
||||
ecodes.KEY_I: 9, ecodes.KEY_J: 10, ecodes.KEY_K: 11, ecodes.KEY_L: 12,
|
||||
ecodes.KEY_M: 13, ecodes.KEY_N: 14, ecodes.KEY_O: 15, ecodes.KEY_P: 16,
|
||||
ecodes.KEY_Q: 17, ecodes.KEY_R: 18, ecodes.KEY_S: 19, ecodes.KEY_T: 20,
|
||||
ecodes.KEY_U: 21, ecodes.KEY_V: 22, ecodes.KEY_W: 23, ecodes.KEY_X: 24,
|
||||
ecodes.KEY_Y: 25, ecodes.KEY_Z: 26,
|
||||
}
|
||||
|
||||
# Handle Ctrl+C
|
||||
if ctrl_pressed and event.code == ecodes.KEY_C:
|
||||
os.write(self.master_fd, b'\x03')
|
||||
if event.code in ctrl_letter_map:
|
||||
ctrl_char = chr(ctrl_letter_map[event.code])
|
||||
os.write(self.master_fd, ctrl_char.encode())
|
||||
|
||||
# Ctrl+D exits the terminal
|
||||
if event.code == ecodes.KEY_D:
|
||||
self.stream.feed("\r\n[Exiting...]\r\n")
|
||||
self.update_display()
|
||||
self.running = False
|
||||
break
|
||||
continue
|
||||
|
||||
# Handle F5-F8 for quadrant zoom
|
||||
if event.code == ecodes.KEY_F5:
|
||||
self._safe_print(f"[DEBUG] F5 pressed - zooming to top-left quadrant")
|
||||
self._quadrant_view = 'top_left'
|
||||
self._quadrant_changed = True
|
||||
self.update_display()
|
||||
continue
|
||||
elif event.code == ecodes.KEY_F6:
|
||||
self._safe_print(f"[DEBUG] F6 pressed - zooming to top-right quadrant")
|
||||
self._quadrant_view = 'top_right'
|
||||
self._quadrant_changed = True
|
||||
self.update_display()
|
||||
continue
|
||||
elif event.code == ecodes.KEY_F7:
|
||||
self._safe_print(f"[DEBUG] F7 pressed - zooming to bottom-left quadrant")
|
||||
self._quadrant_view = 'bottom_left'
|
||||
self._quadrant_changed = True
|
||||
self.update_display()
|
||||
continue
|
||||
elif event.code == ecodes.KEY_F8:
|
||||
self._safe_print(f"[DEBUG] F8 pressed - zooming to bottom-right quadrant")
|
||||
self._quadrant_view = 'bottom_right'
|
||||
self._quadrant_changed = True
|
||||
self.update_display()
|
||||
continue
|
||||
|
||||
# Map key to character
|
||||
char = key_map.get(event.code)
|
||||
if char:
|
||||
# Apply shift modifier
|
||||
if shift_pressed:
|
||||
# Reset quadrant view on any normal keypress
|
||||
if self._quadrant_view is not None:
|
||||
self._safe_print(f"[DEBUG] Resetting quadrant view to full screen")
|
||||
self._quadrant_view = None
|
||||
self._quadrant_changed = True
|
||||
self.update_display()
|
||||
# Apply shift modifier (ctrl is handled separately above)
|
||||
if shift_pressed and not ctrl_pressed:
|
||||
if char.isalpha():
|
||||
char = char.upper()
|
||||
elif char in shift_map:
|
||||
char = shift_map[char]
|
||||
|
||||
# Apply ctrl modifier (for letters only)
|
||||
if ctrl_pressed and char.isalpha():
|
||||
char = chr(ord(char.upper()) - ord('A') + 1)
|
||||
|
||||
# Send to PTY
|
||||
os.write(self.master_fd, char.encode())
|
||||
|
||||
|
|
@ -833,6 +924,9 @@ def main():
|
|||
type=float,
|
||||
default=1.0,
|
||||
help='Zoom multiplier for text size (e.g., 2.0 for 2x larger text)')
|
||||
parser.add_argument('-p', '--no-preview',
|
||||
action='store_true',
|
||||
help='Disable ASCII preview in console output (useful for service mode)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
@ -840,7 +934,8 @@ def main():
|
|||
vt = VirtualTerminalEInk(
|
||||
display_type=args.display,
|
||||
use_display=not args.no_display,
|
||||
zoom=args.zoom
|
||||
zoom=args.zoom,
|
||||
show_preview=not args.no_preview
|
||||
)
|
||||
|
||||
if args.command:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue