diff --git a/INSTALL.md b/INSTALL.md index 225552f..f2bc6e7 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -311,7 +311,54 @@ chmod +x /home/bjorn/Bjorn/kill_port_8000.sh ``` -##### 7.3: USB Gadget Configuration +##### 7.3: Bluetooth Pairing Access + +Create the Bluetooth pairing service: + +```bash +sudo vi /etc/systemd/system/bjorn-bluetooth.service +``` + +Add: + +```ini +[Unit] +Description=Bjorn Bluetooth Pairing Service +After=bluetooth.service local-fs.target +Requires=bluetooth.service + +[Service] +ExecStart=/usr/bin/python3 /home/bjorn/Bjorn/bluetooth_manager.py +WorkingDirectory=/home/bjorn/Bjorn +StandardOutput=inherit +StandardError=inherit +Restart=always +User=root + +[Install] +WantedBy=multi-user.target +``` + +Enable pairing mode in `config/shared_config.json`: + +```json +{ + "bluetooth_pairing_enabled": true, + "bluetooth_ssh_user": "bjorn" +} +``` + +Reload and start the services: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable bjorn.service bjorn-bluetooth.service +sudo systemctl restart bjorn.service bjorn-bluetooth.service +``` + +When pairing mode is enabled, Bjorn shows the pairing prompt or code on the e-paper display and also shows the SSH user and best available host/IP. + +##### 7.4: USB Gadget Configuration Modify `/boot/firmware/cmdline.txt`: diff --git a/README.md b/README.md index e8aa67c..28eddc1 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ The e-Paper HAT display and web interface make it easy to monitor and interact w - **System Attacks**: Conducts brute-force attacks on various services (FTP, SSH, SMB, RDP, Telnet, SQL). - **File Stealing**: Extracts data from vulnerable services. - **User Interface**: Real-time display on the e-Paper HAT and web interface for monitoring and interaction. +- **Headless Bluetooth Pairing**: Optional Bluetooth pairing mode shows the pairing code and SSH target directly on the e-Paper display. ![Bjorn Display](https://github.com/infinition/Bjorn/assets/37984399/bcad830d-77d6-4f3e-833d-473eadd33921) @@ -91,6 +92,8 @@ sudo chmod +x install_bjorn.sh && sudo ./install_bjorn.sh For **detailed information** about **installation** process go to [Install Guide](INSTALL.md) +To enable the optional Bluetooth pairing screen after installation, set `"bluetooth_pairing_enabled": true` in `config/shared_config.json`, adjust `"bluetooth_ssh_user"` if needed, then restart `bjorn.service` and `bjorn-bluetooth.service`. + ## ⚡ Quick Start **Need help ? You struggle to find Bjorn's IP after the installation ?** diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 5fe0241..2ad6531 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -30,9 +30,11 @@ journalctl -fu bjorn.service # Check service status sudo systemctl status bjorn.service +sudo systemctl status bjorn-bluetooth.service # View detailed logs sudo journalctl -u bjorn.service -f +sudo journalctl -u bjorn-bluetooth.service -f or diff --git a/bluetooth_manager.py b/bluetooth_manager.py new file mode 100644 index 0000000..c369ad8 --- /dev/null +++ b/bluetooth_manager.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python3 + +import json +import logging +import os +import queue +import random +import re +import shutil +import signal +import socket +import subprocess +import threading +import time +from datetime import datetime, timezone + +from logger import Logger + + +logger = Logger(name="bluetooth_manager.py", level=logging.DEBUG) + +ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") +CONTROL_CHARS_RE = re.compile(r"[\x00-\x08\x0B-\x1F\x7F]") +DEVICE_LINE_RE = re.compile(r"Device ([0-9A-F:]{17}) (.+)") +PIN_RE = re.compile(r"(\d{4,6})") + + +class BluetoothManager: + def __init__(self): + self.base_dir = os.path.dirname(os.path.abspath(__file__)) + self.config_path = os.path.join(self.base_dir, "config", "shared_config.json") + self.state_path = os.path.join(self.base_dir, "config", "bluetooth_state.json") + self.output_queue = queue.Queue() + self.stop_event = threading.Event() + self.proc = None + self.reader_thread = None + self.settings = self.load_settings() + self.last_applied_settings = {} + self.current_device = {"mac": None, "name": None} + self.current_pin = None + self.pin_updated_at = 0.0 + self.last_pairing_success_at = 0.0 + self.last_error = "" + self.last_error_at = 0.0 + self.last_event = "" + self.adapter_state = { + "powered": False, + "pairable": False, + "discoverable": False, + "alias": self.settings["bluetooth_alias"], + } + self.paired_devices = [] + self.connected_devices = [] + + def run(self): + try: + while not self.stop_event.is_set(): + self.settings = self.load_settings() + self.ensure_session() + self.apply_adapter_settings() + self.consume_output(timeout=1.0) + self.refresh_state() + self.write_state() + time.sleep(1) + finally: + self.cleanup() + + def load_settings(self): + hostname = socket.gethostname().split(".")[0] + defaults = { + "bluetooth_pairing_enabled": False, + "bluetooth_discoverable_timeout": 0, + "bluetooth_agent_capability": "DisplayYesNo", + "bluetooth_ssh_user": "bjorn", + "bluetooth_pairing_pin": "", + "bluetooth_alias": hostname, + } + + try: + with open(self.config_path, "r", encoding="utf-8") as handle: + config = json.load(handle) + except FileNotFoundError: + return defaults + except json.JSONDecodeError as exc: + logger.error(f"Unable to read Bluetooth settings from {self.config_path}: {exc}") + return defaults + + for key in defaults: + if key in config: + defaults[key] = config[key] + + defaults["bluetooth_discoverable_timeout"] = self.safe_int( + defaults["bluetooth_discoverable_timeout"], + 0, + ) + defaults["bluetooth_alias"] = str(defaults["bluetooth_alias"]).strip() or hostname + defaults["bluetooth_ssh_user"] = str(defaults["bluetooth_ssh_user"]).strip() or "bjorn" + defaults["bluetooth_pairing_pin"] = str(defaults["bluetooth_pairing_pin"]).strip() + defaults["bluetooth_agent_capability"] = ( + str(defaults["bluetooth_agent_capability"]).strip() or "DisplayYesNo" + ) + defaults["bluetooth_pairing_enabled"] = bool(defaults["bluetooth_pairing_enabled"]) + return defaults + + def safe_int(self, value, fallback): + try: + return int(value) + except (TypeError, ValueError): + return fallback + + def ensure_session(self): + capability = self.settings["bluetooth_agent_capability"] + + if self.proc and self.proc.poll() is None: + return + + logger.info("Starting bluetoothctl agent session") + self.proc = subprocess.Popen( + ["bluetoothctl", "--agent", capability], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + + self.reader_thread = threading.Thread(target=self.enqueue_output, daemon=True) + self.reader_thread.start() + self.last_applied_settings = {} + time.sleep(1) + + self.send_command("power on") + self.send_command(f"agent {capability}") + self.send_command("default-agent") + self.send_command(f"system-alias {self.settings['bluetooth_alias']}") + + def enqueue_output(self): + if not self.proc or not self.proc.stdout: + return + + for line in self.proc.stdout: + self.output_queue.put(self.clean_line(line)) + + self.output_queue.put(None) + + def clean_line(self, line): + line = ANSI_ESCAPE_RE.sub("", line) + return CONTROL_CHARS_RE.sub("", line).strip() + + def send_command(self, command): + if not self.proc or self.proc.poll() is not None or not self.proc.stdin: + return + + logger.debug(f"bluetoothctl <= {command}") + self.proc.stdin.write(f"{command}\n") + self.proc.stdin.flush() + + def consume_output(self, timeout): + while not self.stop_event.is_set(): + try: + line = self.output_queue.get(timeout=timeout) + except queue.Empty: + break + + if line is None: + raise RuntimeError("bluetoothctl session ended unexpectedly") + + if line: + self.handle_output_line(line) + + timeout = 0 + + def handle_output_line(self, line): + lower_line = line.lower() + logger.debug(f"bluetoothctl => {line}") + self.last_event = line + + device_match = DEVICE_LINE_RE.search(line) + if device_match: + self.current_device["mac"] = device_match.group(1) + self.current_device["name"] = device_match.group(2).strip() + + if "confirm passkey" in lower_line or "request confirmation" in lower_line: + self.current_pin = self.extract_pin(line) + self.pin_updated_at = time.time() + self.last_error = "" + self.send_command("yes") + return + + if "enter pin code" in lower_line or "enter passkey" in lower_line: + pin = self.settings["bluetooth_pairing_pin"] or self.generate_pin() + self.current_pin = pin + self.pin_updated_at = time.time() + self.last_error = "" + self.send_command(pin) + return + + if ( + "authorize service" in lower_line + or "request authorization" in lower_line + or "accept pairing" in lower_line + ): + self.send_command("yes") + return + + if "pairing successful" in lower_line: + self.current_pin = None + self.last_pairing_success_at = time.time() + self.last_error = "" + if self.current_device["mac"]: + self.trust_device(self.current_device["mac"]) + return + + if "failed to pair" in lower_line or "authenticationfailed" in lower_line: + self.current_pin = None + self.last_error = line + self.last_error_at = time.time() + return + + def extract_pin(self, line): + pin_match = PIN_RE.search(line) + if pin_match: + return pin_match.group(1) + return None + + def generate_pin(self): + return f"{random.randint(0, 999999):06d}" + + def apply_adapter_settings(self): + if not self.last_applied_settings.get("power_on"): + self.send_command("power on") + self.last_applied_settings["power_on"] = True + + alias = self.settings["bluetooth_alias"] + if self.last_applied_settings.get("alias") != alias: + self.send_command(f"system-alias {alias}") + self.last_applied_settings["alias"] = alias + + timeout = self.settings["bluetooth_discoverable_timeout"] + if self.last_applied_settings.get("discoverable_timeout") != timeout: + self.send_command(f"discoverable-timeout {timeout}") + self.last_applied_settings["discoverable_timeout"] = timeout + + enabled = self.settings["bluetooth_pairing_enabled"] + if self.last_applied_settings.get("pairing_enabled") == enabled: + return + + if enabled: + self.send_command("pairable on") + self.send_command("discoverable on") + else: + self.send_command("discoverable off") + self.send_command("pairable off") + + self.last_applied_settings["pairing_enabled"] = enabled + + def refresh_state(self): + self.refresh_adapter_state() + self.refresh_devices() + self.prune_temporary_state() + + def refresh_adapter_state(self): + output = self.run_command(["bluetoothctl", "show"]) + self.adapter_state["powered"] = "Powered: yes" in output + self.adapter_state["pairable"] = "Pairable: yes" in output + self.adapter_state["discoverable"] = "Discoverable: yes" in output + + alias = self.parse_info_field(output, "Alias") + if alias: + self.adapter_state["alias"] = alias + + def refresh_devices(self): + output = self.run_command(["bluetoothctl", "devices", "Paired"]) + paired_devices = [] + + for raw_line in output.splitlines(): + line = raw_line.strip() + match = DEVICE_LINE_RE.match(line) + if not match: + continue + + mac_address = match.group(1) + device_name = match.group(2).strip() + info_output = self.run_command(["bluetoothctl", "info", mac_address]) + trusted = "Trusted: yes" in info_output + connected = "Connected: yes" in info_output + info_name = self.parse_info_field(info_output, "Name") or device_name + entry = { + "mac": mac_address, + "name": info_name, + "trusted": trusted, + "connected": connected, + } + paired_devices.append(entry) + + if not trusted: + self.trust_device(mac_address) + + self.paired_devices = paired_devices + self.connected_devices = [device for device in paired_devices if device["connected"]] + + def trust_device(self, mac_address): + if not mac_address: + return + + output = self.run_command(["bluetoothctl", "trust", mac_address]) + if "trust succeeded" in output.lower(): + logger.info(f"Trusted Bluetooth device {mac_address}") + + def prune_temporary_state(self): + now = time.time() + + if self.current_pin and now - self.pin_updated_at > 90: + self.current_pin = None + + if self.last_error and now - self.last_error_at > 30: + self.last_error = "" + + def parse_info_field(self, output, key): + prefix = f"{key}:" + for line in output.splitlines(): + stripped = line.strip() + if stripped.startswith(prefix): + return stripped.split(":", 1)[1].strip() + return "" + + def run_command(self, command): + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + return "" + + return (result.stdout or "") + (result.stderr or "") + + def build_ssh_targets(self): + targets = [] + seen_values = set() + + tailscale_dns = self.get_tailscale_dns_name() + tailscale_ip = self.get_tailscale_ip() + ipv4_addresses = self.get_ipv4_addresses() + + candidate_targets = [] + if tailscale_ip: + candidate_targets.append(("Tailscale", tailscale_ip)) + if tailscale_dns: + candidate_targets.append(("Tailnet", tailscale_dns)) + + preferred_interfaces = ("wlan0", "eth0", "usb0") + for interface_name in preferred_interfaces: + if interface_name in ipv4_addresses: + candidate_targets.append((interface_name, ipv4_addresses[interface_name])) + + for interface_name, address in ipv4_addresses.items(): + if interface_name not in preferred_interfaces and interface_name != "tailscale0": + candidate_targets.append((interface_name, address)) + + for label, value in candidate_targets: + if not value or value in seen_values: + continue + + seen_values.add(value) + targets.append({"label": label, "value": value}) + + return targets + + def get_ipv4_addresses(self): + output = self.run_command(["ip", "-o", "-4", "addr", "show", "up", "scope", "global"]) + addresses = {} + + for line in output.splitlines(): + parts = line.split() + if len(parts) < 4: + continue + + interface_name = parts[1] + if interface_name == "lo": + continue + + address = parts[3].split("/", 1)[0] + addresses[interface_name] = address + + return addresses + + def get_tailscale_dns_name(self): + if not shutil.which("tailscale"): + return "" + + output = self.run_command(["tailscale", "status", "--json"]) + try: + data = json.loads(output) + except json.JSONDecodeError: + return "" + + dns_name = str(data.get("Self", {}).get("DNSName", "")).strip() + return dns_name.rstrip(".") + + def get_tailscale_ip(self): + if not shutil.which("tailscale"): + return "" + + output = self.run_command(["tailscale", "ip", "-4"]) + for line in output.splitlines(): + candidate = line.strip() + if candidate: + return candidate + return "" + + def build_state(self): + ssh_targets = self.build_ssh_targets() + ssh_user = self.settings["bluetooth_ssh_user"] + primary_host = ssh_targets[0]["value"] if ssh_targets else "" + display_host = self.select_display_host(ssh_targets) + overlay_lines = self.build_display_lines(display_host, ssh_user) + + return { + "enabled": self.settings["bluetooth_pairing_enabled"], + "powered": self.adapter_state["powered"], + "pairable": self.adapter_state["pairable"], + "discoverable": self.adapter_state["discoverable"], + "status": self.get_status(), + "alias": self.adapter_state["alias"], + "device_name": self.current_device["name"], + "device_mac": self.current_device["mac"], + "pin": self.current_pin, + "last_error": self.last_error, + "last_event": self.last_event, + "paired_devices": self.paired_devices, + "connected_devices": self.connected_devices, + "ssh_user": ssh_user, + "ssh_host": primary_host, + "ssh_command": f"ssh {ssh_user}@{primary_host}" if primary_host else "", + "ssh_targets": ssh_targets, + "display_overlay": self.should_show_overlay(), + "display_lines": overlay_lines, + "updated_at": datetime.now(timezone.utc).isoformat(), + } + + def select_display_host(self, ssh_targets): + if not ssh_targets: + return "unavailable" + + for target in ssh_targets: + if re.match(r"^\d+\.\d+\.\d+\.\d+$", target["value"]): + return target["value"] + + return self.truncate_text(ssh_targets[0]["value"], 18) + + def truncate_text(self, text, limit): + if len(text) <= limit: + return text + return f"{text[: limit - 3]}..." + + def get_status(self): + if not self.settings["bluetooth_pairing_enabled"]: + return "inactive" + if self.last_error: + return "error" + if self.current_pin: + return "confirm" + if self.connected_devices: + return "connected" + if self.last_pairing_success_at and time.time() - self.last_pairing_success_at < 180: + return "paired" + if self.adapter_state["powered"]: + return "ready" + return "starting" + + def should_show_overlay(self): + status = self.get_status() + if status in {"confirm", "paired", "connected", "error"}: + return True + if status == "ready" and not self.paired_devices: + return True + return False + + def build_display_lines(self, display_host, ssh_user): + status = self.get_status() + device_name = self.current_device["name"] + if not device_name and self.connected_devices: + device_name = self.connected_devices[0]["name"] + if not device_name and self.paired_devices: + device_name = self.paired_devices[0]["name"] + + if status == "confirm": + lines = ["PAIR CODE", self.current_pin or "CHECK PHONE"] + elif status == "paired": + lines = ["BT PAIRED", self.truncate_text(device_name or "Ready", 18)] + elif status == "connected": + lines = ["BT CONNECTED", self.truncate_text(device_name or "Phone linked", 18)] + elif status == "error": + lines = ["BT ERROR", self.truncate_text(self.last_error, 18)] + elif status == "ready": + lines = ["BT READY", "PAIR FROM PHONE"] + else: + lines = ["BT STARTING"] + + lines.append(f"USER {self.truncate_text(ssh_user, 13)}") + lines.append(f"SSH {self.truncate_text(display_host, 14)}") + return lines + + def write_state(self): + state = self.build_state() + os.makedirs(os.path.dirname(self.state_path), exist_ok=True) + temp_path = f"{self.state_path}.tmp" + + with open(temp_path, "w", encoding="utf-8") as handle: + json.dump(state, handle, indent=2) + + os.replace(temp_path, self.state_path) + + def cleanup(self): + try: + if self.proc and self.proc.poll() is None: + self.send_command("discoverable off") + self.send_command("pairable off") + self.send_command("quit") + self.proc.terminate() + self.proc.wait(timeout=5) + except Exception as exc: + logger.error(f"Bluetooth cleanup failed: {exc}") + + +def main(): + manager = BluetoothManager() + + def stop_handler(signum, frame): + del signum, frame + manager.stop_event.set() + + signal.signal(signal.SIGINT, stop_handler) + signal.signal(signal.SIGTERM, stop_handler) + + try: + manager.run() + except Exception as exc: + logger.error(f"Bluetooth manager crashed: {exc}") + manager.last_error = str(exc) + manager.last_error_at = time.time() + try: + manager.write_state() + except Exception: + pass + raise + + +if __name__ == "__main__": + main() diff --git a/config/shared_config.json b/config/shared_config.json index 9278659..5c6537c 100644 --- a/config/shared_config.json +++ b/config/shared_config.json @@ -29,6 +29,12 @@ "ref_width": 122, "ref_height": 250, "epd_type": "epd2in13_V4", + "bluetooth_pairing_enabled": false, + "bluetooth_discoverable_timeout": 0, + "bluetooth_agent_capability": "DisplayYesNo", + "bluetooth_ssh_user": "bjorn", + "bluetooth_pairing_pin": "", + "bluetooth_alias": "bjorn", "__title_lists__": "List Settings", "portlist": [ 20, diff --git a/display.py b/display.py index 2f78a85..d6e4bf3 100644 --- a/display.py +++ b/display.py @@ -15,6 +15,7 @@ import threading import time import os +import json import pandas as pd import signal import glob @@ -199,6 +200,11 @@ def update_shared_data(self): self.manual_mode_txt = "M" else: self.manual_mode_txt = "A" + bluetooth_state = self.load_bluetooth_state() + self.shared_data.bluetooth_active = bool( + bluetooth_state.get("enabled") and bluetooth_state.get("powered") + ) + self.shared_data.pan_connected = bool(bluetooth_state.get("connected_devices")) self.shared_data.wifi_connected = self.is_wifi_connected() self.shared_data.usb_active = self.is_usb_connected() self.get_open_files() @@ -217,6 +223,32 @@ def display_comment(self, status): else: pass + def load_bluetooth_state(self): + """Load the current Bluetooth manager state from disk.""" + try: + with open(self.shared_data.bluetooth_state_file, "r", encoding="utf-8") as handle: + return json.load(handle) + except FileNotFoundError: + return {} + except json.JSONDecodeError as e: + logger.error(f"Error decoding Bluetooth state: {e}") + return {} + except Exception as e: + logger.error(f"Error loading Bluetooth state: {e}") + return {} + + def get_bluetooth_overlay_lines(self): + """Return Bluetooth overlay lines when pairing UX should take over the speech area.""" + bluetooth_state = self.load_bluetooth_state() + self.shared_data.bluetooth_active = bool( + bluetooth_state.get("enabled") and bluetooth_state.get("powered") + ) + self.shared_data.pan_connected = bool(bluetooth_state.get("connected_devices")) + + if bluetooth_state.get("display_overlay"): + return bluetooth_state.get("display_lines", []) + return [] + # # # def is_bluetooth_connected(self): # # # """ # # # Check if any device is connected to the Bluetooth (pan0) interface by checking the output of 'ip neigh show dev pan0'. @@ -290,8 +322,8 @@ def run(self): if self.shared_data.wifi_connected: image.paste(self.shared_data.wifi, (int(3 * self.scale_factor_x), int(3 * self.scale_factor_y))) - # # # if self.shared_data.bluetooth_active: - # # # image.paste(self.shared_data.bluetooth, (int(23 * self.scale_factor_x), int(4 * self.scale_factor_y))) + if self.shared_data.bluetooth_active: + image.paste(self.shared_data.bluetooth, (int(23 * self.scale_factor_x), int(4 * self.scale_factor_y))) if self.shared_data.pan_connected: image.paste(self.shared_data.connected, (int(104 * self.scale_factor_x), int(3 * self.scale_factor_y))) if self.shared_data.usb_active: @@ -328,7 +360,15 @@ def run(self): draw.line((1, 59, self.shared_data.width - 1, 59), fill=0) draw.line((1, 87, self.shared_data.width - 1, 87), fill=0) - lines = self.shared_data.wrap_text(self.shared_data.bjornsay, self.shared_data.font_arialbold, self.shared_data.width - 4) + overlay_lines = self.get_bluetooth_overlay_lines() + if overlay_lines: + lines = overlay_lines + else: + lines = self.shared_data.wrap_text( + self.shared_data.bjornsay, + self.shared_data.font_arialbold, + self.shared_data.width - 4, + ) y_text = int(90 * self.scale_factor_y) if self.main_image is not None: @@ -388,4 +428,4 @@ def handle_exit_display(signum, frame, display_thread): except Exception as e: logger.error(f"An exception occurred during program execution: {e}") handle_exit_display(signal.SIGINT, None, display_thread) - sys.exit(1) \ No newline at end of file + sys.exit(1) diff --git a/install_bjorn.sh b/install_bjorn.sh index 92d6675..0561782 100644 --- a/install_bjorn.sh +++ b/install_bjorn.sh @@ -370,6 +370,24 @@ User=root # Check open files and restart if it reached the limit (ulimit -n buffer of 1000) ExecStartPost=/bin/bash -c 'FILE_LIMIT=\$(ulimit -n); THRESHOLD=\$(( FILE_LIMIT - 1000 )); while :; do TOTAL_OPEN_FILES=\$(lsof | wc -l); if [ "\$TOTAL_OPEN_FILES" -ge "\$THRESHOLD" ]; then echo "File descriptor threshold reached: \$TOTAL_OPEN_FILES (threshold: \$THRESHOLD). Restarting service."; systemctl restart bjorn.service; exit 0; fi; sleep 10; done &' +[Install] +WantedBy=multi-user.target +EOF + + cat > /etc/systemd/system/bjorn-bluetooth.service << EOF +[Unit] +Description=Bjorn Bluetooth Pairing Service +After=bluetooth.service local-fs.target +Requires=bluetooth.service + +[Service] +ExecStart=/usr/bin/python3 /home/${BJORN_USER}/Bjorn/bluetooth_manager.py +WorkingDirectory=/home/${BJORN_USER}/Bjorn +StandardOutput=inherit +StandardError=inherit +Restart=always +User=root + [Install] WantedBy=multi-user.target EOF @@ -381,6 +399,7 @@ EOF # Enable and start services systemctl daemon-reload systemctl enable bjorn.service + systemctl enable bjorn-bluetooth.service check_success "Services setup completed" } @@ -633,4 +652,3 @@ main - diff --git a/shared.py b/shared.py index 3ebe912..a056c2d 100644 --- a/shared.py +++ b/shared.py @@ -85,6 +85,7 @@ def initialize_paths(self): """Files paths""" # Files directly under configdir self.shared_config_json = os.path.join(self.configdir, 'shared_config.json') + self.bluetooth_state_file = os.path.join(self.configdir, 'bluetooth_state.json') self.actions_file = os.path.join(self.configdir, 'actions.json') # Files directly under resourcesdir self.commentsfile = os.path.join(self.commentsdir, 'comments.json') @@ -143,6 +144,12 @@ def get_default_config(self): "ref_width" :122 , "ref_height" : 250, "epd_type": "epd2in13_V4", + "bluetooth_pairing_enabled": False, + "bluetooth_discoverable_timeout": 0, + "bluetooth_agent_capability": "DisplayYesNo", + "bluetooth_ssh_user": "bjorn", + "bluetooth_pairing_pin": "", + "bluetooth_alias": "bjorn", "__title_lists__": "List Settings", diff --git a/uninstall_bjorn.sh b/uninstall_bjorn.sh index 0ee9111..6e08bc1 100644 --- a/uninstall_bjorn.sh +++ b/uninstall_bjorn.sh @@ -60,6 +60,12 @@ stop_services() { systemctl disable bjorn fi + if systemctl is-active --quiet "bjorn-bluetooth"; then + log "INFO" "Stopping bjorn-bluetooth service..." + systemctl stop bjorn-bluetooth + systemctl disable bjorn-bluetooth + fi + # Stop and disable usb-gadget service if systemctl is-active --quiet "usb-gadget"; then log "INFO" "Stopping usb-gadget service..." @@ -80,6 +86,7 @@ stop_services() { remove_service_files() { log "INFO" "Removing service files..." rm -f /etc/systemd/system/bjorn.service + rm -f /etc/systemd/system/bjorn-bluetooth.service rm -f /etc/systemd/system/usb-gadget.service rm -f /usr/local/bin/usb-gadget.sh systemctl daemon-reload @@ -194,4 +201,4 @@ else fi log "SUCCESS" "Script completed" -echo -e "${BLUE}Log file available at: $LOG_FILE${NC}" \ No newline at end of file +echo -e "${BLUE}Log file available at: $LOG_FILE${NC}"