From d512fba6a2a098ffdad4c11651bc7e0cf12383eb Mon Sep 17 00:00:00 2001 From: Josh Pinto Date: Wed, 16 Jul 2025 22:44:27 -0400 Subject: [PATCH] :sparkles: Add product id request, rebrand to ltamp --- .github/workflows/python-publish.yml | 2 +- README.md | 36 +++++++++++++++------------- lt25.py | 7 ------ lt25async.py => ltAmpAsync.py | 20 ++++++++++++---- lt25base.py => ltAmpBase.py | 11 ++++++--- lt25sync.py => ltAmpSync.py | 29 ++++++++++++++++++---- ltamp.py | 7 ++++++ protocol/__init__.py | 12 +++++++--- pyproject.toml | 10 ++++---- 9 files changed, 88 insertions(+), 46 deletions(-) delete mode 100644 lt25.py rename lt25async.py => ltAmpAsync.py (88%) rename lt25base.py => ltAmpBase.py (92%) rename lt25sync.py => ltAmpSync.py (81%) create mode 100644 ltamp.py diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 512f214..e028c36 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -51,7 +51,7 @@ jobs: environment: name: pypi # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: - url: https://pypi.org/p/lt25 + url: https://pypi.org/p/ltamp # # ALTERNATIVE: if your GitHub Release name is the PyPI project version string # ALTERNATIVE: exactly, uncomment the following line instead: diff --git a/README.md b/README.md index ade5d16..68c9162 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# lt25.py +# LTAmp.py -[![PyPI Latest Release](https://img.shields.io/pypi/v/lt25.svg)](https://pypi.org/project/lt25/) +[![PyPI Latest Release](https://img.shields.io/pypi/v/ltamp.svg)](https://pypi.org/project/ltamp/) [![wakatime](https://wakatime.com/badge/user/7482ea9d-3085-4e9b-95ad-1ca78a14d948/project/08632cd5-4928-49fb-8d0d-2d8f2bebbdad.svg)](https://wakatime.com/badge/user/7482ea9d-3085-4e9b-95ad-1ca78a14d948/project/08632cd5-4928-49fb-8d0d-2d8f2bebbdad) -![MIT License](https://img.shields.io/github/license/bendertools/lt25.py) +![MIT License](https://img.shields.io/github/license/bendertools/ltamp.py) -🎸 A cross-platform Python module for interacting with the LT25 amplifier from a certain guitar company that rhymes with "bender" and names products after horses. +🎸 A cross-platform Python module for interacting with the LT amplifiers from a certain guitar company that rhymes with "bender" and names products after horses. ## 👋 Introduction @@ -12,12 +12,12 @@ I made this libray because I was disappointed by the lack of certain features in ### ⚙️ Specifications -This library requires Python 3.8+ to be installed on your system (due to demands of the protobuf 5 module). Additionally, the library only supports HID USB operations (no MIDI). +This package requires Python 3.8+ to be installed on your system (due to demands of the protobuf 5 module). Additionally, the library only supports HID USB operations (no MIDI). ## 🚀 Quickstart ``` -pip install lt25 +pip install ltamp ``` > [!IMPORTANT] @@ -25,19 +25,18 @@ pip install lt25 ## 💾 Usage -For full documentation of the LT25 and LT25Async classes, refer to the [wiki](/wiki). +For full documentation of the LtAmp and LtAmpAsync classes, refer to the [wiki](/wiki). **Demonstration loop:** > [!WARNING] > On some linux distros, `sudo`-mode is required for running python files which interact with USB devices. - ```python import time -import lt25 +import ltamp -amp = lt25.LT25() +amp = ltamp.LtAmp() amp.connect() amp.send_sync_begin() amp.send_sync_end() @@ -58,36 +57,39 @@ Compatibility with LtAmp protocol: - [ ] Audio Status - [x] Audition Preset (status, start, exit) - [ ] Clear/rename/shift/swap/save (to/as) preset -- [ ] Connection Status +- [x] Connection Status - [x] Current Preset (request/load) - [ ] DSP Unit Parameter - [x] Firmware Version - [x] Heartbeat -- [ ] ⭐ LT4 Footswitch Mode +- [x] LT4 Footswitch Mode - [ ] Loopback - [x] Memory usage +- [x] Product Identification - [x] Processor Utilization - [x] QA Slots - [x] Sync (Modal Status) - [x] USB Gain Other TODOs: + - [x] Publish to Pypi - [x] Continuous Deployment -- [x] LT25Async/Base Classes +- [x] LtAmpAsync/Base Classes - [ ] Unit Testing +- [ ] Customize timeout - [ ] Complete Protocol -- [ ] Other Amps +- [x] Other LT Amps +- [ ] Multiple Amps ## 🛠️ Contributing > [!NOTE] > If you need to contribute in a way which updates protocol classes, you can find the original .proto files in Brent Maxwell's [repository](https://github.com/brentmaxwell/LtAmp/). I unfortunately cannot include these files in this module due to copyleft licensing restrictions. -lt25.py is licensed under the permissive MIT license, which means that you may fork, modify, adapt, and redistribute with few restrictions. If you wish to contribute your changes back to the base module, please open a [pull request](/pulls). To report bugs, request features, or discuss the project, open an [issue](/issues) or [discussion](/discussions). +LTAmp.py is licensed under the permissive MIT license, which means that you may fork, modify, adapt, and redistribute with few restrictions. If you wish to contribute your changes back to the base module, please open a [pull request](/pulls). To report bugs, request features, or discuss the project, open an [issue](/issues) or [discussion](/discussions). -## 🙌 Acknowledgements +## 🙌 Acknowledgements - Brent Maxwell ([@brentmaxwell](https://github.com/brentmaxwell)) and his LtAmp .NET libray: his published reverse engineering docs, schemas, proto files, etc. were instrumental in the creation of this Python module. - Additionally, some of the goals of his work greatly inspired those of this project. - diff --git a/lt25.py b/lt25.py deleted file mode 100644 index 4f3f484..0000000 --- a/lt25.py +++ /dev/null @@ -1,7 +0,0 @@ -from .hidwrapper import HIDWrapper, HIDDevice, HIDBackendNotFound -from .lt25sync import LT25 -from .lt25async import LT25Async - -__all__ = [ - 'LT25', 'LT25Async', 'HIDBackendNotFound', 'HIDWrapper', 'HIDDevice' -] diff --git a/lt25async.py b/ltAmpAsync.py similarity index 88% rename from lt25async.py rename to ltAmpAsync.py index 2dac867..dfd9721 100644 --- a/lt25async.py +++ b/ltAmpAsync.py @@ -2,7 +2,7 @@ import os import sys import asyncio -from .lt25base import LT25Base +from .ltAmpBase import LtAmpBase # protobuf imports protocol_path = os.path.join(os.path.dirname(__file__), 'protocol') @@ -11,12 +11,12 @@ if protocol_path not in sys.path: from .protocol import * -class LT25Async(LT25Base): +class LtAmpAsync(LtAmpBase): """ - async version of LT25 controller using asyncio + async version of LtAmp controller using asyncio Methods: -- connect() connects to the amp (finds first matching device) +- connect() connects to the amp (first matching device) - disconnect() disconnect and clean up - send_sync_begin() send SYNC_BEGIN (start handshake) - send_sync_end() send SYNC_END (end handshake) @@ -32,9 +32,10 @@ class LT25Async(LT25Base): - request_audition_state() get current audition state (status event) - request_memory_usage() get memory usage - request_processor_utilization() get processor utilization -- request_footswitch_mode() get current footswitch mode +- request_footswitch_mode() get current footswitch mode (lt4 only!) - set_usb_gain(gain) set USB gain (0-100) - request_usb_gain() get current USB gain setting + request_product_id() get product ID - - Data: - last_message Last parsed message @@ -143,3 +144,12 @@ class LT25Async(LT25Base): except asyncio.TimeoutError: raise TimeoutError("No usb gain state response received.") + async def request_product_id(self): + self._last_product_id = None + self._pid_event.clear() + request_product_id(self.device) + try: + await asyncio.wait_for(self._pid_event.wait(), timeout=2.0) + return self._last_product_id + except asyncio.TimeoutError: + raise TimeoutError("No product ID response received.") diff --git a/lt25base.py b/ltAmpBase.py similarity index 92% rename from lt25base.py rename to ltAmpBase.py index d98f895..c176dc6 100644 --- a/lt25base.py +++ b/ltAmpBase.py @@ -16,9 +16,9 @@ from .protocol import * VENDOR_ID = 0x1ed8 PRODUCT_ID = 0x0037 -class LT25Base: +class LtAmpBase: """ - base class for Fender Mustang LT25 communication + base class for LT amplifier communication """ def __init__(self): self.hid_wrapper = HIDWrapper() @@ -36,6 +36,7 @@ class LT25Base: self._pu_event = threading.Event() # processor self._ftsw_event = threading.Event() # footswitch self._gain_event = threading.Event() # usb gain + self._pid_event = threading.Event() # product ID def _set_event(self, event): if hasattr(self, 'loop') and self.loop is not None: @@ -52,7 +53,7 @@ class LT25Base: def connect(self): amp_info = self.find_amp() if not amp_info: - raise RuntimeError("Fender LT25 not found") + raise RuntimeError("LT amp not found") self.device = self.hid_wrapper.open_device(amp_info) self.device.set_input_callback(self._process_input_data) if self.hid_wrapper.backend == "hidapi": @@ -142,6 +143,10 @@ class LT25Base: "maxPercent": utilization.maxPercent } self._set_event(self._pu_event) + elif msg.HasField("productIdentificationStatus"): + pid = msg.productIdentificationStatus.id + self._last_product_id = pid + self._set_event(self._pid_event) except Exception: pass self.msg_buffer = bytearray() diff --git a/lt25sync.py b/ltAmpSync.py similarity index 81% rename from lt25sync.py rename to ltAmpSync.py index f4b14f2..e475288 100644 --- a/lt25sync.py +++ b/ltAmpSync.py @@ -1,7 +1,7 @@ import os import sys -from .lt25base import LT25Base +from .ltAmpBase import LtAmpBase # protobuf imports protocol_path = os.path.join(os.path.dirname(__file__), 'protocol') @@ -10,12 +10,12 @@ if protocol_path not in sys.path: from .protocol import * -class LT25(LT25Base): +class LtAmp(LtAmpBase): """ - synchronous version of LT25 controller + synchronous version of LT amp controller Methods: -- connect() connects to the amp (finds first matching device) +- connect() connects to the amp (first matching device) - disconnect() disconnect and clean up - send_sync_begin() send SYNC_BEGIN (start handshake) - send_sync_end() send SYNC_END (end handshake) @@ -31,9 +31,10 @@ class LT25(LT25Base): - request_audition_state() get current audition state (status event) - request_memory_usage() get memory usage - request_processor_utilization() get processor utilization -- request_footswitch_mode() get current footswitch mode +- request_footswitch_mode() get current footswitch mode (lt4 only!) - set_usb_gain(gain) set USB gain (0-100) - request_usb_gain() get current USB gain setting +- request_product_id() get product ID - - Data: - last_message Last parsed message @@ -112,3 +113,21 @@ class LT25(LT25Base): return self._last_gain_state else: raise TimeoutError("No usb gain state response received.") + + def request_footswitch_mode(self): + self._last_ftsw_state = None + self._ftsw_event.clear() + request_footswitch_mode(self.device) + if self._ftsw_event.wait(timeout=2.0): + return self._last_ftsw_state + else: + raise TimeoutError("No footswitch mode response received. (Maybe not on an LT4?)") + + def request_product_id(self): + self._last_product_id = None + self._pid_event.clear() + request_product_id(self.device) + if self._pid_event.wait(timeout=2.0): + return self._last_product_id + else: + raise TimeoutError("No product ID response received.") diff --git a/ltamp.py b/ltamp.py new file mode 100644 index 0000000..dbb23cb --- /dev/null +++ b/ltamp.py @@ -0,0 +1,7 @@ +from .hidwrapper import HIDWrapper, HIDDevice, HIDBackendNotFound +from .ltAmpSync import LtAmp +from .ltAmpAsync import LtAmpAsync + +__all__ = [ + 'LtAmp', 'LtAmpAsync', 'HIDBackendNotFound', 'HIDWrapper', 'HIDDevice' +] diff --git a/protocol/__init__.py b/protocol/__init__.py index 19858eb..1173bff 100644 --- a/protocol/__init__.py +++ b/protocol/__init__.py @@ -1,5 +1,5 @@ """ -protocol actions for LT25 USB control. +protocol actions for LT amp USB control. covers QA, firmware, preset status, auditioning, memory, footswitch status, USB gain. @@ -25,6 +25,8 @@ from .ProcessorUtilizationRequest_pb2 import ProcessorUtilizationRequest from .LT4FootswitchModeRequest_pb2 import LT4FootswitchModeRequest from .UsbGainRequest_pb2 import UsbGainRequest from .ConnectionStatusRequest_pb2 import ConnectionStatusRequest +from .LT4FootswitchModeRequest_pb2 import LT4FootswitchModeRequest +from .ProductIdentificationRequest_pb2 import ProductIdentificationRequest def send_message(device, msg): """ @@ -62,7 +64,7 @@ def send_message(device, msg): def _msg(**kwargs): - """construct a FenderMessageLT with given fields""" + """construct a message with given fields""" msg = FenderMessageLT() msg.responseType = ResponseType.UNSOLICITED for k, v in kwargs.items(): @@ -77,7 +79,6 @@ def _msg(**kwargs): def request_qa_slots(device): """get current footswitch assignments""" - print('send msg') send_message(device, _msg(qASlotsRequest= QASlotsRequest(request=True))) def set_qa_slots(device, slots): @@ -147,3 +148,8 @@ def send_sync_end(device): def request_connection_status(device): send_message(device, _msg(connectionStatusRequest=ConnectionStatusRequest(request=True))) + +# --- product id --- + +def request_product_id(device): + send_message(device, _msg(productIdentificationRequest=ProductIdentificationRequest(request=True))) diff --git a/pyproject.toml b/pyproject.toml index ad95997..0dd2e37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "lt25" -version = "0.1.2" -description = "🎸 Cross-platform python interface for LT25 guitar amplifier" +name = "ltamp" +version = "0.2.0" +description = "🎸 Cross-platform python interface for LT guitar/bass amplifiers" readme = "README.md" license = {text = "MIT"} requires-python = ">=3.8" @@ -31,5 +31,5 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project.urls] -Homepage = "https://github.com/bendertools/lt25.py" -Repository = "https://github.com/bendertools/lt25.py" +Homepage = "https://github.com/bendertools/LTAmp.py" +Repository = "https://github.com/bendertools/LTAmp.py" -- 2.51.2