diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..700c750 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,143 @@ +name: release + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+a[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+b[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" + +env: + PACKAGE_NAME: "contact" + OWNER: "pdxlocations" + +jobs: + details: + runs-on: ubuntu-latest + outputs: + new_version: ${{ steps.release.outputs.new_version }} + suffix: ${{ steps.release.outputs.suffix }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: actions/checkout@v2 + + - name: Extract tag and Details + id: release + run: | + if [ "${{ github.ref_type }}" = "tag" ]; then + TAG_NAME=${GITHUB_REF#refs/tags/} + NEW_VERSION=$(echo $TAG_NAME | awk -F'-' '{print $1}') + SUFFIX=$(echo $TAG_NAME | grep -oP '[a-z]+[0-9]+' || echo "") + echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT" + echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" + echo "Version is $NEW_VERSION" + echo "Suffix is $SUFFIX" + echo "Tag name is $TAG_NAME" + else + echo "No tag found" + exit 1 + fi + + check_pypi: + needs: details + runs-on: ubuntu-latest + steps: + - name: Fetch information from PyPI + run: | + response=$(curl -s https://pypi.org/pypi/${{ env.PACKAGE_NAME }}/json || echo "{}") + latest_previous_version=$(echo $response | jq --raw-output "select(.releases != null) | .releases | keys_unsorted | last") + if [ -z "$latest_previous_version" ]; then + echo "Package not found on PyPI." + latest_previous_version="0.0.0" + fi + echo "Latest version on PyPI: $latest_previous_version" + echo "latest_previous_version=$latest_previous_version" >> $GITHUB_ENV + + - name: Compare versions and exit if not newer + run: | + NEW_VERSION=${{ needs.details.outputs.new_version }} + LATEST_VERSION=$latest_previous_version + if [ "$(printf '%s\n' "$LATEST_VERSION" "$NEW_VERSION" | sort -rV | head -n 1)" != "$NEW_VERSION" ] || [ "$NEW_VERSION" == "$LATEST_VERSION" ]; then + echo "The new version $NEW_VERSION is not greater than the latest version $LATEST_VERSION on PyPI." + exit 1 + else + echo "The new version $NEW_VERSION is greater than the latest version $LATEST_VERSION on PyPI." + fi + + setup_and_build: + needs: [details, check_pypi] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Set project version with Poetry + run: | + poetry version ${{ needs.details.outputs.new_version }} + + - name: Install dependencies + run: poetry install --sync --no-interaction + + - name: Build source and wheel distribution + run: | + poetry build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + pypi_publish: + name: Upload release to PyPI + needs: [setup_and_build, details] + runs-on: ubuntu-latest + environment: + name: release + permissions: + id-token: write + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + github_release: + name: Create GitHub Release + needs: [setup_and_build, details] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Create GitHub Release + id: create_release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create ${{ needs.details.outputs.tag_name }} dist/* --title ${{ needs.details.outputs.tag_name }} --generate-notes diff --git a/.gitignore b/.gitignore index 5bceb25..1d30efb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ client.db client.log settings.log config.json -default_config.log \ No newline at end of file +default_config.log +dist/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6d8c679 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "cwd": "${workspaceFolder}", + "module": "contact.main", + "args": [] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 0badf56..f4e2ce9 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ If no connection arguments are specified, the client will attempt a serial conne ### Example Usage ```sh -python main.py --port /dev/ttyUSB0 -python main.py --host 192.168.1.1 -python main.py --ble BlAddressOfDevice +contact --port /dev/ttyUSB0 +contact --host 192.168.1.1 +contact --ble BlAddressOfDevice ``` To quickly connect to localhost, use: ```sh -python main.py -t +contact -t ``` diff --git a/globals.py b/contact/globals.py similarity index 100% rename from globals.py rename to contact/globals.py diff --git a/localisations/en.ini b/contact/localisations/en.ini similarity index 100% rename from localisations/en.ini rename to contact/localisations/en.ini diff --git a/main.py b/contact/main.py similarity index 82% rename from main.py rename to contact/main.py index c0028c3..9b861bd 100644 --- a/main.py +++ b/contact/main.py @@ -18,18 +18,18 @@ import logging import traceback import threading -from utilities.db_handler import init_nodedb, load_messages_from_db -from message_handlers.rx_handler import on_receive -from settings import set_region -from ui.curses_ui import main_ui -from ui.colors import setup_colors -from ui.splash import draw_splash -import ui.default_config as config -from utilities.arg_parser import setup_parser -from utilities.interfaces import initialize_interface -from utilities.input_handlers import get_list_input -from utilities.utils import get_channels, get_node_list, get_nodeNum -import globals +from contact.utilities.db_handler import init_nodedb, load_messages_from_db +from contact.message_handlers.rx_handler import on_receive +from contact.settings import set_region +from contact.ui.curses_ui import main_ui +from contact.ui.colors import setup_colors +from contact.ui.splash import draw_splash +import contact.ui.default_config as config +from contact.utilities.arg_parser import setup_parser +from contact.utilities.interfaces import initialize_interface +from contact.utilities.input_handlers import get_list_input +from contact.utilities.utils import get_channels, get_node_list, get_nodeNum +import contact.globals as globals # Set ncurses compatibility settings os.environ["NCURSES_NO_UTF8_ACS"] = "1" @@ -87,7 +87,7 @@ def main(stdscr): logging.error("Console output before crash:\n%s", console_output) raise # Re-raise only unexpected errors -if __name__ == "__main__": +def start(): log_file = config.log_file_path log_f = open(log_file, "a", buffering=1) # Enable line-buffering for immediate log writes @@ -103,4 +103,7 @@ if __name__ == "__main__": except Exception as e: logging.error("Fatal error in curses wrapper: %s", e) logging.error("Traceback: %s", traceback.format_exc()) - sys.exit(1) # Exit with an error code \ No newline at end of file + sys.exit(1) # Exit with an error code + +if __name__ == "__main__": + start() diff --git a/message_handlers/rx_handler.py b/contact/message_handlers/rx_handler.py similarity index 91% rename from message_handlers/rx_handler.py rename to contact/message_handlers/rx_handler.py index c95f0c5..74cc5f9 100644 --- a/message_handlers/rx_handler.py +++ b/contact/message_handlers/rx_handler.py @@ -1,11 +1,11 @@ import logging import time -from utilities.utils import refresh_node_list +from contact.utilities.utils import refresh_node_list from datetime import datetime -from ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification -from utilities.db_handler import save_message_to_db, maybe_store_nodeinfo_in_db, get_name_from_database, update_node_info_in_db -import ui.default_config as config -import globals +from contact.ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification +from contact.utilities.db_handler import save_message_to_db, maybe_store_nodeinfo_in_db, get_name_from_database, update_node_info_in_db +import contact.ui.default_config as config +import contact.globals as globals from datetime import datetime diff --git a/message_handlers/tx_handler.py b/contact/message_handlers/tx_handler.py similarity index 95% rename from message_handlers/tx_handler.py rename to contact/message_handlers/tx_handler.py index 0b72fc9..45cfc13 100644 --- a/message_handlers/tx_handler.py +++ b/contact/message_handlers/tx_handler.py @@ -3,16 +3,16 @@ import google.protobuf.json_format from meshtastic import BROADCAST_NUM from meshtastic.protobuf import mesh_pb2, portnums_pb2 -from utilities.db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db -import ui.default_config as config -import globals +from contact.utilities.db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db +import contact.ui.default_config as config +import contact.globals as globals ack_naks = {} # Note "onAckNak" has special meaning to the API, thus the nonstandard naming convention # See https://github.com/meshtastic/python/blob/master/meshtastic/mesh_interface.py#L462 def onAckNak(packet): - from ui.curses_ui import draw_messages_window + from contact.ui.curses_ui import draw_messages_window request = packet['decoded']['requestId'] if(request not in ack_naks): return @@ -43,7 +43,7 @@ def onAckNak(packet): def on_response_traceroute(packet): """on response for trace route""" - from ui.curses_ui import draw_channel_list, draw_messages_window, add_notification + from contact.ui.curses_ui import draw_channel_list, draw_messages_window, add_notification refresh_channels = False refresh_messages = False diff --git a/settings.py b/contact/settings.py similarity index 83% rename from settings.py rename to contact/settings.py index 5be03b4..c0ee3ae 100644 --- a/settings.py +++ b/contact/settings.py @@ -5,13 +5,13 @@ import logging import sys import traceback -import ui.default_config as config -from utilities.input_handlers import get_list_input -from ui.colors import setup_colors -from ui.splash import draw_splash -from ui.control_ui import set_region, settings_menu -from utilities.arg_parser import setup_parser -from utilities.interfaces import initialize_interface +import contact.ui.default_config as config +from contact.utilities.input_handlers import get_list_input +from contact.ui.colors import setup_colors +from contact.ui.splash import draw_splash +from contact.ui.control_ui import set_region, settings_menu +from contact.utilities.arg_parser import setup_parser +from contact.utilities.interfaces import initialize_interface def main(stdscr): diff --git a/ui/colors.py b/contact/ui/colors.py similarity index 95% rename from ui/colors.py rename to contact/ui/colors.py index 8016542..cb8c064 100644 --- a/ui/colors.py +++ b/contact/ui/colors.py @@ -1,5 +1,5 @@ import curses -import ui.default_config as config +import contact.ui.default_config as config COLOR_MAP = { "black": curses.COLOR_BLACK, diff --git a/ui/control_ui.py b/contact/ui/control_ui.py similarity index 97% rename from ui/control_ui.py rename to contact/ui/control_ui.py index c89a1a6..b3c5b9e 100644 --- a/ui/control_ui.py +++ b/contact/ui/control_ui.py @@ -5,14 +5,16 @@ import os import re import sys -from utilities.save_to_radio import save_changes -from utilities.config_io import config_export, config_import -from utilities.input_handlers import get_repeated_input, get_text_input, get_fixed32_input, get_list_input, get_admin_key_input -from ui.menus import generate_menu_from_protobuf -from ui.colors import get_color -from ui.dialog import dialog -from utilities.control_utils import parse_ini_file, transform_menu_path -from ui.user_config import json_editor +from contact.utilities.save_to_radio import save_changes +from contact.utilities.config_io import config_export, config_import +from contact.utilities.input_handlers import get_repeated_input, get_text_input, get_fixed32_input, get_list_input, get_admin_key_input +from contact.ui.menus import generate_menu_from_protobuf +from contact.ui.colors import get_color +from contact.ui.dialog import dialog +from contact.utilities.control_utils import parse_ini_file, transform_menu_path +from contact.ui.user_config import json_editor + +import contact.localisations # Constants width = 80 @@ -27,8 +29,9 @@ parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir)) # Paths locals_dir = os.path.dirname(os.path.abspath(sys.argv[0])) # Current script directory -translation_file = os.path.join(locals_dir, "localisations", "en.ini") -config_folder = os.path.join(parent_dir, "node-configs") +translation_file = os.path.join(parent_dir, "localisations", "en.ini") + +config_folder = os.path.join(locals_dir, "node-configs") # Load translations field_mapping, help_text = parse_ini_file(translation_file) diff --git a/ui/curses_ui.py b/contact/ui/curses_ui.py similarity index 97% rename from ui/curses_ui.py rename to contact/ui/curses_ui.py index 7547cfd..c71a172 100644 --- a/ui/curses_ui.py +++ b/contact/ui/curses_ui.py @@ -2,14 +2,14 @@ import curses import textwrap import logging import traceback -from utilities.utils import get_channels, get_readable_duration, get_time_ago, refresh_node_list -from settings import settings_menu -from message_handlers.tx_handler import send_message, send_traceroute -from ui.colors import setup_colors, get_color -from utilities.db_handler import get_name_from_database, update_node_info_in_db, is_chat_archived -import ui.default_config as config -import ui.dialog -import globals +from contact.utilities.utils import get_channels, get_readable_duration, get_time_ago, refresh_node_list +from contact.settings import settings_menu +from contact.message_handlers.tx_handler import send_message, send_traceroute +from contact.ui.colors import setup_colors, get_color +from contact.utilities.db_handler import get_name_from_database, update_node_info_in_db, is_chat_archived +import contact.ui.default_config as config +import contact.ui.dialog +import contact.globals as globals def handle_resize(stdscr, firstrun): global messages_pad, messages_win, nodes_pad, nodes_win, channel_pad, channel_win, function_win, packetlog_win, entry_win @@ -212,7 +212,7 @@ def main_ui(stdscr): elif char == chr(20): send_traceroute() curses.curs_set(0) # Hide cursor - ui.dialog.dialog(stdscr, "Traceroute Sent", "Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.") + contact.ui.dialog.dialog(stdscr, "Traceroute Sent", "Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.") curses.curs_set(1) # Show cursor again handle_resize(stdscr, False) diff --git a/ui/default_config.py b/contact/ui/default_config.py similarity index 100% rename from ui/default_config.py rename to contact/ui/default_config.py diff --git a/ui/dialog.py b/contact/ui/dialog.py similarity index 96% rename from ui/dialog.py rename to contact/ui/dialog.py index 2d9ed67..ea39b43 100644 --- a/ui/dialog.py +++ b/contact/ui/dialog.py @@ -1,5 +1,5 @@ import curses -from ui.colors import get_color +from contact.ui.colors import get_color def dialog(stdscr, title, message): height, width = stdscr.getmaxyx() diff --git a/ui/menus.py b/contact/ui/menus.py similarity index 100% rename from ui/menus.py rename to contact/ui/menus.py diff --git a/ui/splash.py b/contact/ui/splash.py similarity index 93% rename from ui/splash.py rename to contact/ui/splash.py index 08fe2e5..403bc34 100644 --- a/ui/splash.py +++ b/contact/ui/splash.py @@ -1,5 +1,5 @@ import curses -from ui.colors import get_color +from contact.ui.colors import get_color def draw_splash(stdscr): curses.curs_set(0) diff --git a/ui/user_config.py b/contact/ui/user_config.py similarity index 96% rename from ui/user_config.py rename to contact/ui/user_config.py index 32aae7f..eb4e9b6 100644 --- a/ui/user_config.py +++ b/contact/ui/user_config.py @@ -1,9 +1,9 @@ import os import json import curses -from ui.colors import get_color, setup_colors, COLOR_MAP -from ui.default_config import format_json_single_line_arrays, loaded_config -from utilities.input_handlers import get_list_input +from contact.ui.colors import get_color, setup_colors, COLOR_MAP +from contact.ui.default_config import format_json_single_line_arrays, loaded_config +from contact.utilities.input_handlers import get_list_input width = 60 save_option_text = "Save Changes" @@ -196,7 +196,10 @@ def json_editor(stdscr): menu_path = ["App Settings"] selected_index = 0 # Track the selected option - file_path = "config.json" + script_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir)) + file_path = os.path.join(parent_dir, "config.json") + # file_path = "config.json" show_save_option = True # Always show the Save button # Ensure the file exists diff --git a/utilities/arg_parser.py b/contact/utilities/arg_parser.py similarity index 100% rename from utilities/arg_parser.py rename to contact/utilities/arg_parser.py diff --git a/utilities/config_io.py b/contact/utilities/config_io.py similarity index 100% rename from utilities/config_io.py rename to contact/utilities/config_io.py diff --git a/utilities/control_utils.py b/contact/utilities/control_utils.py similarity index 100% rename from utilities/control_utils.py rename to contact/utilities/control_utils.py diff --git a/utilities/db_handler.py b/contact/utilities/db_handler.py similarity index 99% rename from utilities/db_handler.py rename to contact/utilities/db_handler.py index 8368f38..b969ca9 100644 --- a/utilities/db_handler.py +++ b/contact/utilities/db_handler.py @@ -3,9 +3,9 @@ import time import logging from datetime import datetime -from utilities.utils import decimal_to_hex -import ui.default_config as config -import globals +from contact.utilities.utils import decimal_to_hex +import contact.ui.default_config as config +import contact.globals as globals def get_table_name(channel): # Construct the table name diff --git a/utilities/input_handlers.py b/contact/utilities/input_handlers.py similarity index 99% rename from utilities/input_handlers.py rename to contact/utilities/input_handlers.py index 34b41ae..847b9f0 100644 --- a/utilities/input_handlers.py +++ b/contact/utilities/input_handlers.py @@ -3,7 +3,7 @@ import binascii import curses import ipaddress import re -from ui.colors import get_color +from contact.ui.colors import get_color def wrap_text(text, wrap_width): """Wraps text while preserving spaces and breaking long words.""" diff --git a/utilities/interfaces.py b/contact/utilities/interfaces.py similarity index 96% rename from utilities/interfaces.py rename to contact/utilities/interfaces.py index 66c6e7b..6cb0918 100644 --- a/utilities/interfaces.py +++ b/contact/utilities/interfaces.py @@ -1,6 +1,6 @@ import logging import meshtastic.serial_interface, meshtastic.tcp_interface, meshtastic.ble_interface -import globals +import contact.globals as globals def initialize_interface(args): try: diff --git a/utilities/save_to_radio.py b/contact/utilities/save_to_radio.py similarity index 100% rename from utilities/save_to_radio.py rename to contact/utilities/save_to_radio.py diff --git a/utilities/utils.py b/contact/utilities/utils.py similarity index 98% rename from utilities/utils.py rename to contact/utilities/utils.py index ce8d567..6392899 100644 --- a/utilities/utils.py +++ b/contact/utilities/utils.py @@ -1,7 +1,7 @@ -import globals +import contact.globals as globals import datetime from meshtastic.protobuf import config_pb2 -import ui.default_config as config +import contact.ui.default_config as config def get_channels(): """Retrieve channels from the node and update globals.channel_list and globals.all_messages.""" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3798286 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "contact" +version = "1.3.0" +description = "This Python curses client for Meshtastic is a terminal-based client designed to manage device settings, enable mesh chat communication, and handle configuration backups and restores." +authors = [ + {name = "Ben Lipsey",email = "ben@pdxlocations.com"} +] +license = "GPL-3.0-only" +readme = "README.md" +requires-python = ">=3.9,<3.14" +dependencies = [ + "meshtastic (>=2.6.0,<3.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +contact = "contact.main:start"