Files
2026-07-17 15:29:53 -04:00

235 lines
10 KiB
Python

"""Shared constants and key mappings for the ATOMS3 MacroPad app."""
import os
APP_NAME = "ATOMS3 MacroPad"
APP_VERSION = "1.0.0"
DEVICE_ID = "ATOMS3-MACROPAD"
ESPRESSIF_VID = 0x303A
SCREEN_W = 128
SCREEN_H = 128
# Project-relative storage. utils/constants.py → repo root → config/
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
APPDATA_DIR = os.path.join(_PROJECT_ROOT, "config")
PROJECT_FILE = os.path.join(APPDATA_DIR, "project.json")
IMAGES_DIR = os.path.join(APPDATA_DIR, "images")
PROFILES_DIR = os.path.join(APPDATA_DIR, "profiles")
PROFILES_META_FILE = os.path.join(APPDATA_DIR, "profiles_meta.json")
BACKUPS_DIR = os.path.join(APPDATA_DIR, "backups")
DEFAULT_PROFILE_NAME = "Default"
DEFAULT_HOLD_MS = 500
DEFAULT_TYPE_DELAY = 15
DEFAULT_ORIENTATION = 0
DEFAULT_RESUME_DELAY = 0 # seconds, 0 = disabled
DEFAULT_COMBO_PRE_MS = 500 # ms delay before key combo is sent
DEFAULT_COMBO_POST_MS = 500 # ms delay after key combo is sent
DEFAULT_PROBE_TIMEOUT_MS = 300 # ms to wait for host LED response in PC Alive Check
DEFAULT_MEDIA_HOLD_MS = 100 # ms to hold media key before release
DEFAULT_TYPE_SHIFT_EXTRA_MS = 25 # extra ms per shifted char (uppercase, !@#$ etc.)
DEFAULT_TYPE_SETTLE_MS = 150 # ms after last char to let HID reports drain
# Pause-screen text-box margins (pixels at the device's 128x128 LCD).
# These control where the pause-text wrapping/truncation box sits inside
# the screen and are mirrored 1:1 into the firmware's drawWrapped() call.
DEFAULT_PAUSE_MARGIN_LEFT = 4
DEFAULT_PAUSE_MARGIN_RIGHT = 4
DEFAULT_PAUSE_MARGIN_TOP = 16
DEFAULT_PAUSE_MARGIN_BOTTOM = 12
# Device LCD dimensions — used by the pause preview to render at the
# same proportions as the device.
DEVICE_SCREEN_W = 128
DEVICE_SCREEN_H = 128
NODE_TYPES = {
"start": {"label": "Start", "color": "#2ECC71", "desc": "Routine starts here"},
"text": {"label": "Type Text", "color": "#4A90D9", "desc": "Type a string of text"},
"combo": {"label": "Key Combo", "color": "#D94A4A", "desc": "Press a key combination"},
"pause": {"label": "Pause", "color": "#D9A84A", "desc": "Wait for click or timer"},
"branch": {"label": "Branch", "color": "#9B59B6", "desc": "Choose between paths"},
"delay": {"label": "Delay", "color": "#7F8C8D", "desc": "Wait milliseconds"},
"repeat": {"label": "Loop", "color": "#27AE60", "desc": "Repeat nodes N times"},
"loop_selector": {"label": "Loop Selector", "color": "#F1C40F", "desc": "Prompt user for loop count"},
"iteration_branch": {"label": "Iteration Branch", "color": "#8E44AD", "desc": "Branch based on loop iteration number"},
"note": {"label": "Note", "color": "#555555", "desc": "On-canvas annotation (not sent to device)"},
"aggregator": {"label": "Aggregator", "color": "#5D6D7E", "desc": "Merge multiple paths into one output"},
"mouse": {"label": "Mouse Click", "color": "#E67E22", "desc": "Send mouse button"},
"media": {"label": "Media Key", "color": "#1ABC9C", "desc": "Media control key"},
"bluetooth": {"label": "Variables", "color": "#0077CC", "desc": "Read/write variables (BLE or local)"},
"rs232": {"label": "RS232 Send", "color": "#8B5CF6", "desc": "Send data via RS232 serial"},
"subroutine": {"label": "Sub-Routine", "color": "#FF6B9D", "desc": "Call a reusable sub-routine"},
"pc_alive_check": {"label": "PC Alive Check", "color": "#F59E0B", "desc": "Check if host PC is responding"},
"macro": {"label": "Macro", "color": "#14B8A6", "desc": "Replay an exact recorded key-press sequence"},
}
SUBROUTINES_PROFILE_NAME = "Sub-Routines"
RS232_BAUD_RATES = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
RS232_DATA_BITS = [5, 6, 7, 8]
RS232_STOP_BITS = ["1", "1.5", "2"]
RS232_PARITY = ["none", "even", "odd"]
RS232_LINE_ENDINGS = [("None", "none"), ("CR (\\r)", "cr"), ("LF (\\n)", "lf"), ("CRLF (\\r\\n)", "crlf")]
MODIFIER_KEYS = ["ctrl", "shift", "alt", "gui", "rctrl", "rshift", "ralt", "rgui"]
MODIFIER_LABELS = {
"ctrl": "Ctrl", "shift": "Shift", "alt": "Alt", "gui": "Win",
"rctrl": "Right Ctrl", "rshift": "Right Shift", "ralt": "AltGr", "rgui": "Right Win",
}
SPECIAL_KEYS = [
"enter", "esc", "backspace", "tab", "space", "delete", "insert",
"home", "end", "pageup", "pagedown",
"up", "down", "left", "right",
"capslock", "numlock", "scrolllock", "printscreen", "pause", "menu",
"f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12",
"f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24",
]
MOUSE_BUTTONS = ["left", "right", "middle"]
MOUSE_ACTIONS = ["click", "double", "press", "release"]
MEDIA_ACTIONS = [
("vol_up", "Volume Up"), ("vol_down", "Volume Down"), ("mute", "Mute"),
("play_pause", "Play/Pause"), ("next", "Next Track"), ("prev", "Previous Track"),
("stop", "Stop"), ("brightness_up", "Brightness Up"), ("brightness_down", "Brightness Down"),
]
ORIENTATION_OPTIONS = [
(0, "Normal (0\u00b0)"),
(1, "90\u00b0"),
(2, "180\u00b0"),
(3, "270\u00b0"),
]
# (name, GUI hex preview)
DISPLAY_COLORS = [
("white", "#FFFFFF"),
("red", "#FF4444"),
("green", "#44FF88"),
("blue", "#4488FF"),
("yellow", "#FFFF44"),
("cyan", "#44FFFF"),
("magenta", "#FF44FF"),
("orange", "#FFA040"),
]
# HID USB usage codes for the "macro" recording node.
# Macro events are stored as [t_ms, action, hid_code] tuples. Using raw HID
# usage codes (not ASCII) lets us press/release each physical key independently
# — essential when modifiers and letters overlap or when multiple keys are
# held at once. The firmware calls keyboard.pressRaw() / keyboard.releaseRaw()
# with these codes directly, so modifiers never get retyped out from under a
# chord.
#
# Map from Tkinter keysym → HID USB usage code (standard 104-key layout).
# Keysym ordering reference: https://www.tcl.tk/man/tcl/TkCmd/keysyms.html
TKKEYSYM_TO_HID = {
# --- Letters (both cases map to the same physical key) ---
**{chr(c): 0x04 + (c - ord('a')) for c in range(ord('a'), ord('z') + 1)},
**{chr(c): 0x04 + (c - ord('A')) for c in range(ord('A'), ord('Z') + 1)},
# --- Top-row digits (1..0) ---
"1": 0x1E, "2": 0x1F, "3": 0x20, "4": 0x21, "5": 0x22,
"6": 0x23, "7": 0x24, "8": 0x25, "9": 0x26, "0": 0x27,
# --- Whitespace / edit ---
"Return": 0x28, "KP_Enter": 0x58,
"Escape": 0x29, "BackSpace": 0x2A, "Tab": 0x2B, "space": 0x2C,
# --- Punctuation (US layout shift-pair maps to same key) ---
"minus": 0x2D, "underscore": 0x2D,
"equal": 0x2E, "plus": 0x2E,
"bracketleft": 0x2F, "braceleft": 0x2F,
"bracketright": 0x30, "braceright": 0x30,
"backslash": 0x31, "bar": 0x31,
"semicolon": 0x33, "colon": 0x33,
"apostrophe": 0x34, "quotedbl": 0x34,
"grave": 0x35, "asciitilde": 0x35,
"comma": 0x36, "less": 0x36,
"period": 0x37, "greater": 0x37,
"slash": 0x38, "question": 0x38,
# Shifted digits — map to the underlying digit key
"exclam": 0x1E, "at": 0x1F, "numbersign": 0x20, "dollar": 0x21,
"percent": 0x22, "asciicircum": 0x23, "ampersand": 0x24,
"asterisk": 0x25, "parenleft": 0x26, "parenright": 0x27,
# --- Locks / system ---
"Caps_Lock": 0x39,
"Num_Lock": 0x53,
"Scroll_Lock": 0x47,
"Print": 0x46,
"Pause": 0x48,
"Menu": 0x65,
# --- Navigation ---
"Insert": 0x49, "Home": 0x4A, "Prior": 0x4B,
"Delete": 0x4C, "End": 0x4D, "Next": 0x4E,
"Right": 0x4F, "Left": 0x50, "Down": 0x51, "Up": 0x52,
# --- Function keys ---
**{f"F{i}": 0x3A + (i - 1) for i in range(1, 13)}, # F1..F12 → 0x3A..0x45
**{f"F{i}": 0x68 + (i - 13) for i in range(13, 25)}, # F13..F24 → 0x68..0x73
# --- Modifiers (0xE0..0xE7) ---
"Control_L": 0xE0, "Shift_L": 0xE1, "Alt_L": 0xE2, "Super_L": 0xE3,
"Control_R": 0xE4, "Shift_R": 0xE5, "Alt_R": 0xE6, "Super_R": 0xE7,
# Numpad
"KP_Divide": 0x54, "KP_Multiply": 0x55, "KP_Subtract": 0x56,
"KP_Add": 0x57, "KP_Decimal": 0x63,
**{f"KP_{i}": 0x59 + (i - 1) for i in range(1, 10)}, # KP_1..KP_9 → 0x59..0x61
"KP_0": 0x62,
}
def hid_code_label(code: int) -> str:
"""Friendly short label for a HID usage code (for the macro event list)."""
# Prefer a pretty name over the raw keysym
PRETTY = {
0x28: "Enter", 0x29: "Esc", 0x2A: "Backspace", 0x2B: "Tab", 0x2C: "Space",
0x4C: "Delete", 0x49: "Insert", 0x4A: "Home", 0x4D: "End",
0x4B: "PageUp", 0x4E: "PageDown",
0x4F: "→", 0x50: "←", 0x51: "↓", 0x52: "↑",
0x39: "CapsLock", 0x53: "NumLock", 0x47: "ScrollLock",
0x46: "PrtSc", 0x48: "Pause", 0x65: "Menu",
0x58: "KP Enter", 0x54: "KP /", 0x55: "KP *", 0x56: "KP -",
0x57: "KP +", 0x63: "KP .",
0xE0: "LCtrl", 0xE1: "LShift", 0xE2: "LAlt", 0xE3: "LWin",
0xE4: "RCtrl", 0xE5: "RShift", 0xE6: "RAlt", 0xE7: "RWin",
}
if code in PRETTY:
return PRETTY[code]
if 0x04 <= code <= 0x1D:
return chr(ord('A') + (code - 0x04))
if 0x1E <= code <= 0x26:
return str(1 + (code - 0x1E))
if code == 0x27:
return "0"
if 0x3A <= code <= 0x45:
return f"F{1 + (code - 0x3A)}"
if 0x68 <= code <= 0x73:
return f"F{13 + (code - 0x68)}"
if 0x59 <= code <= 0x61:
return f"KP{1 + (code - 0x59)}"
if code == 0x62:
return "KP0"
PUNCT = {
0x2D: "-", 0x2E: "=", 0x2F: "[", 0x30: "]", 0x31: "\\",
0x33: ";", 0x34: "'", 0x35: "`", 0x36: ",", 0x37: ".", 0x38: "/",
}
return PUNCT.get(code, f"0x{code:02X}")
CANVAS_BG = "#1E1E2E"
NODE_HEADER_HEIGHT = 24
NODE_MIN_WIDTH = 160
NODE_PORT_RADIUS = 6
NODE_BODY_COLOR = "#2D2D3D"
NODE_SELECTED_BORDER = "#FFFFFF"
NODE_UPSTREAM_BORDER = "#22C55E" # green — matches input-port color (neighbor wires INTO our input)
NODE_DOWNSTREAM_BORDER = "#EF4444" # red — matches output-port color (neighbor is wired FROM our output)
# Backwards-compatible alias; older code may still import this name.
NODE_CONNECTED_BORDER = NODE_UPSTREAM_BORDER
NODE_TEXT_COLOR = "#FFFFFF"
NODE_PORT_IN_COLOR = "#4ADE80"
NODE_PORT_OUT_COLOR = "#F87171"
WIRE_COLOR = "#6B7280"
WIRE_SELECTED_COLOR = "#FBBF24"
GRID_SIZE = 20
GRID_COLOR = "#2A2A3A"