From ec1bd33b5f22608a545fe653f94146db902266b3 Mon Sep 17 00:00:00 2001 From: suut Date: Sat, 24 Jan 2026 01:28:04 +0100 Subject: [PATCH] Add TCP and Android (Termux) support --- CHANGELOG.md | 2 + software/script/chameleon_com.py | 114 +++++++++++++++++------ software/script/requirements.android.txt | 2 + software/src/CMakeLists.txt | 24 ++--- 4 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 software/script/requirements.android.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 444ca19..6387da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac ## [unreleased][unreleased] - Fix for static nested key recovery (@jekkos) - Fix LEDs being stuck on after battery check (@suut) + - Add TCP support for the CLI (@suut) + - Fix build on Android in Termux (@suut) ## [v2.1.0][2025-09-02] - Added UV, formatter and linter. Contribution guidelines. (@GameTec-live) diff --git a/software/script/chameleon_com.py b/software/script/chameleon_com.py index 33bd8d0..24ef149 100644 --- a/software/script/chameleon_com.py +++ b/software/script/chameleon_com.py @@ -2,8 +2,13 @@ import queue import struct import threading import time -import serial +import platform from typing import Union +from enum import Enum, auto +if platform.system() != 'Android': + import serial +import socket + from chameleon_utils import CR, CG, CC, CY, color_string from chameleon_enum import Command, Status @@ -13,6 +18,10 @@ THREAD_BLOCKING_TIMEOUT = 0.1 # TODO: client settings DEBUG = False +class TransportType(Enum): + NONE = auto() + SERIAL = auto() + SOCKET = auto() class NotOpenException(Exception): """ @@ -57,7 +66,8 @@ class ChameleonCom: """ Create a chameleon device instance """ - self.serial_instance: Union[serial.Serial, None] = None + self.transport: Union[serial.Serial, socket.socket, None] = None + self.transport_type = TransportType.NONE self.send_data_queue = queue.Queue() self.wait_response_map = {} self.event_closing = threading.Event() @@ -68,7 +78,7 @@ class ChameleonCom: :return: """ - return self.serial_instance is not None and self.serial_instance.is_open + return self.transport is not None and (self.transport_type is TransportType.SOCKET or self.transport.is_open) def open(self, port) -> "ChameleonCom": """ @@ -82,19 +92,35 @@ class ChameleonCom: error = None try: # open serial port - self.serial_instance = serial.Serial(port=port, baudrate=115200) + if port.startswith('tcp:'): + host, _, port = port[4:].partition(':') + if not host or not port: + sys.exit(color_string(CR, 'Usage: tcp:127.0.0.1:4321')) + self.transport = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + print('Connecting to', host, int(port)) + self.transport.connect((host, int(port))) + self.transport_type = TransportType.SOCKET + else: + if platform.system() == 'Android': + sys.exit(color_string(CR, 'COM port is not supported on Android, make a USB-serial to TCP communication bridge')) + self.transport = serial.Serial(port=port, baudrate=115200) + self.transport_type = TransportType.SERIAL except Exception as e: error = e finally: if error is not None: raise OpenFailException(error) - assert self.serial_instance is not None - try: - self.serial_instance.dtr = True # must make dtr enable - except Exception: - # not all serial support dtr, e.g. virtual serial over BLE - pass - self.serial_instance.timeout = THREAD_BLOCKING_TIMEOUT + assert self.transport is not None + assert self.transport_type is not TransportType.NONE + if self.transport_type is TransportType.SERIAL: + try: + self.transport.dtr = True # must make dtr enable + except Exception: + # not all serial support dtr, e.g. virtual serial over BLE + pass + self.transport.timeout = THREAD_BLOCKING_TIMEOUT + else: # SOCKET + self.transport.settimeout(THREAD_BLOCKING_TIMEOUT) # clear variable self.send_data_queue.queue.clear() self.wait_response_map.clear() @@ -136,12 +162,14 @@ class ChameleonCom: """ self.event_closing.set() try: - assert self.serial_instance is not None - self.serial_instance.close() + assert self.transport is not None + if self.transport_type is TransportType.SOCKET: + self.transport.shutdown() + self.transport.close() except Exception: pass finally: - self.serial_instance = None + self.transport = None self.wait_response_map.clear() self.send_data_queue.queue.clear() @@ -159,16 +187,29 @@ class ChameleonCom: while self.isOpen(): # receive - try: - assert self.serial_instance is not None - data_bytes = self.serial_instance.read() - except Exception as e: - if not self.event_closing.is_set(): - print(f"Serial Error {e}, thread for receiver exit.") - self.close() - break - if len(data_bytes) > 0: + assert self.transport_type is not TransportType.NONE + if self.transport_type is TransportType.SERIAL: + try: + assert self.transport is not None + data_bytes = bytearray(self.transport.read()) + except Exception as e: + if not self.event_closing.is_set(): + print(f"Serial Error {e}, thread for receiver exit.") + self.close() + break + else: # SOCKET + try: + data_bytes = bytearray(self.transport.recv(1024)) + except socket.timeout: + continue + except OSError: + print(color_string(CR, 'socket closed')) + self.transport = None + break + + while len(data_bytes) > 0: data_byte = data_bytes[0] + data_bytes = data_bytes[1:] data_buffer.append(data_byte) if data_position < struct.calcsize('!BB'): # start of frame + lrc1 if data_position == 0: @@ -267,14 +308,25 @@ class ChameleonCom: self.wait_response_map[task_cmd]['start_time'] = start_time self.wait_response_map[task_cmd]['end_time'] = start_time + task_timeout self.wait_response_map[task_cmd]['is_timeout'] = False - try: - assert self.serial_instance is not None - # send to device - self.serial_instance.write(task['frame']) - except Exception as e: - print(f"Serial Error {e}, thread for transfer exit.") - self.close() - break + assert self.transport_type is not TransportType.NONE + if self.transport_type == TransportType.SERIAL: + try: + assert self.transport is not None + # send to device + self.transport.write(task['frame']) + except Exception as e: + print(f"Serial Error {e}, thread for transfer exit.") + self.close() + break + else: # SOCKET + try: + assert self.transport is not None + self.transport.sendall(task['frame']) + except OSError as e: + self.transport = None + print(f'Socket error {e}, thread for transfer exit.') + self.close() + break # update queue status self.send_data_queue.task_done() # disconnect if DFU command has been sent diff --git a/software/script/requirements.android.txt b/software/script/requirements.android.txt new file mode 100644 index 0000000..6d08f94 --- /dev/null +++ b/software/script/requirements.android.txt @@ -0,0 +1,2 @@ +colorama==0.4.6 +prompt-toolkit==3.0.39 diff --git a/software/src/CMakeLists.txt b/software/src/CMakeLists.txt index 619d083..c4f25a0 100644 --- a/software/src/CMakeLists.txt +++ b/software/src/CMakeLists.txt @@ -77,7 +77,7 @@ endif() # --- Platform specific settings --- -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") MESSAGE(STATUS "Run on linux.") if (CMAKE_BUILD_TYPE STREQUAL "Release") set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3") @@ -126,7 +126,7 @@ endif() add_executable(nested ${COMMON_FILES} ${NESTED_UTIL} nested.c) target_include_directories(nested PRIVATE ${SRC_DIR}) target_link_libraries(nested PRIVATE ${LIBTHREAD}) # Link common thread lib -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(nested PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -138,7 +138,7 @@ endif() add_executable(staticnested ${COMMON_FILES} ${NESTED_UTIL} staticnested.c) target_include_directories(staticnested PRIVATE ${SRC_DIR}) target_link_libraries(staticnested PRIVATE ${LIBTHREAD}) # Link common thread lib -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(staticnested PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -150,7 +150,7 @@ endif() add_executable(darkside ${COMMON_FILES} ${MFKEY_UTIL} darkside.c) target_include_directories(darkside PRIVATE ${SRC_DIR}) # darkside doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(darkside PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -161,7 +161,7 @@ endif() add_executable(mfkey32 ${COMMON_FILES} mfkey32.c) target_include_directories(mfkey32 PRIVATE ${SRC_DIR}) # mfkey32 doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(mfkey32 PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -172,7 +172,7 @@ endif() add_executable(mfkey32v2 ${COMMON_FILES} mfkey32v2.c) target_include_directories(mfkey32v2 PRIVATE ${SRC_DIR}) # mfkey32v2 doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(mfkey32v2 PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -183,7 +183,7 @@ endif() add_executable(mfkey64 ${COMMON_FILES} mfkey64.c) target_include_directories(mfkey64 PRIVATE ${SRC_DIR}) # mfkey64 doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(mfkey64 PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -192,7 +192,7 @@ endif() add_executable(staticnested_1nt ${COMMON_FILES} staticnested_1nt.c) target_include_directories(staticnested_1nt PRIVATE ${SRC_DIR}) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(staticnested_1nt PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -201,7 +201,7 @@ endif() add_executable(staticnested_2x1nt_rf08s ${COMMON_FILES} staticnested_2x1nt_rf08s.c) target_include_directories(staticnested_2x1nt_rf08s PRIVATE ${SRC_DIR}) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(staticnested_2x1nt_rf08s PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -210,7 +210,7 @@ endif() add_executable(staticnested_2x1nt_rf08s_1key ${COMMON_FILES} staticnested_2x1nt_rf08s_1key.c) target_include_directories(staticnested_2x1nt_rf08s_1key PRIVATE ${SRC_DIR}) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(staticnested_2x1nt_rf08s_1key PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -222,7 +222,7 @@ add_executable(mfulc_des_brute mfulc_des_brute.c) target_include_directories(mfulc_des_brute PRIVATE ${SRC_DIR}) target_link_libraries(mfulc_des_brute PRIVATE ${LIBTHREAD} OpenSSL::Crypto) target_compile_options(mfulc_des_brute PRIVATE -Wno-deprecated-declarations) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(mfulc_des_brute PRIVATE _GNU_SOURCE) find_package(OpenSSL REQUIRED) endif() @@ -244,7 +244,7 @@ target_include_directories(hardnested PRIVATE ) target_compile_options(hardnested PRIVATE -Wall) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") target_compile_definitions(hardnested PRIVATE _GNU_SOURCE) endif()