diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index d1a4cd621..d104a1c79 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -1,4 +1,4 @@ -name: "Update protobufs" +name: 'Update protobufs' on: workflow_dispatch jobs: @@ -11,10 +11,9 @@ jobs: with: submodules: true - - name: Update Submodule + - name: Update submodule run: | - git pull --recurse-submodules - git submodule update --remote --recursive + git submodule update --remote protobufs - name: Download nanopb run: | @@ -26,11 +25,9 @@ jobs: run: | ./bin/regen-protobufs.sh - - name: Commit update - run: | - git config --global user.name 'github-actions' - git config --global user.email 'bot@noreply.github.com' - git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} - git add protobufs - git add meshtastic - git commit -m "Update protobuf submodule" && git push || echo "No changes to commit" + - name: Create pull request + uses: peter-evans/create-pull-request@v3 + with: + add-paths: | + protobufs + meshtastic diff --git a/.gitignore b/.gitignore index f2c823cb7..88b8fe16b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ venv/ __pycache__ examples/__pycache__ meshtastic.spec +.idea/ +wallet diff --git a/.gitmodules b/.gitmodules index d155a849a..5db3d3079 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "protobufs"] path = protobufs - url = http://github.com/meshtastic/protobufs + url = ../protobufs.git diff --git a/BlackLagerMessage_pb2.py b/BlackLagerMessage_pb2.py new file mode 100644 index 000000000..a00ad6f70 --- /dev/null +++ b/BlackLagerMessage_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: BlackLagerMessage.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x42lackLagerMessage.proto\"z\n\x11\x42lackLagerMessage\x12\x0c\n\x04type\x18\x01 \x01(\x05\x12\"\n\x04text\x18\x02 \x01(\x0b\x32\x0f.BlackLagerTextH\x00\x88\x01\x01\x12\x1e\n\x07Persona\x18\x03 \x01(\x0b\x32\x08.PersonaH\x01\x88\x01\x01\x42\x07\n\x05_textB\n\n\x08_Persona\"H\n\x06Wallet\x12\x1d\n\x0bmy_personas\x18\x01 \x01(\x0b\x32\x08.Persona\x12\x1f\n\rpeer_personas\x18\x02 \x03(\x0b\x32\x08.Persona\"W\n\x0e\x42lackLagerText\x12\x15\n\rsignedMessage\x18\x01 \x01(\x0c\x12\x15\n\rsenderNodeNum\x18\x02 \x01(\x05\x12\x17\n\x0freceiverNodeNum\x18\x03 \x01(\x05\"\x97\x01\n\x07Persona\x12\x12\n\nlocal_name\x18\x01 \x01(\t\x12\x13\n\x0bmac_address\x18\x02 \x01(\t\x12\x10\n\x08node_num\x18\x03 \x01(\x05\x12\x12\n\npublic_key\x18\x04 \x01(\x0c\x12\x13\n\x0bprivate_key\x18\x05 \x01(\x0c\x12\x0b\n\x03uid\x18\x06 \x01(\x0c\x12\x0c\n\x04mask\x18\x07 \x01(\x0c\x12\r\n\x05owned\x18\x08 \x01(\x08\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'BlackLagerMessage_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _BLACKLAGERMESSAGE._serialized_start=27 + _BLACKLAGERMESSAGE._serialized_end=149 + _WALLET._serialized_start=151 + _WALLET._serialized_end=223 + _BLACKLAGERTEXT._serialized_start=225 + _BLACKLAGERTEXT._serialized_end=312 + _PERSONA._serialized_start=315 + _PERSONA._serialized_end=466 +# @@protoc_insertion_point(module_scope) diff --git a/README.md b/README.md index 49be18933..4c2a2e435 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,59 @@ -# Meshtastic Python - -[![codecov](https://codecov.io/gh/meshtastic/python/branch/master/graph/badge.svg?token=TIWPJL73KV)](https://codecov.io/gh/meshtastic/python) -![PyPI - Downloads](https://img.shields.io/pypi/dm/meshtastic) -[![CI](https://img.shields.io/github/workflow/status/meshtastic/python/CI?label=actions&logo=github&color=yellow)](https://github.com/meshtastic/python/actions/workflows/ci.yml) -[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/python)](https://cla-assistant.io/meshtastic/python) -[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) -[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) +# Black Lager Python App ## Overview +A Python messaging client for use with radio devices flashed with the [Black Lager firmware](https://github.com/black-lager/firmware). +Provides an application that allows you to send and receive text messages with digital signatures. +The messages are verified and displayed in a text based interface created with Curses. + +## Supported hardware +* LILYGO Lora v2.1-1.6 +* LILYGO T-Beam + +Note: Other models listed at https://meshtastic.org/docs/supported-hardware may also work, but they have not been tested with the Black Lager firmware or application + +## Connect to device +Connect your computer to a radio device flashed with the Black Lager firmware via USB cable. +For more information on hardware and firmware, see: https://github.com/black-lager/firmware + +## Installation +### Clone the repo +`git clone git@github.com:meshtastic/python.git` + +### Install dependencies +`pip3 install -r requirements.txt` + +## Running the app +`python3 black_lager_app.py` + +## Commands +![Help is here!](https://github.com/datagod/meshtalk/blob/main/pics/Meshtalk%20help%20window%20send%20message.jpg?raw=true "Help") + +## Screen Layout +The screen has 5 areas of scrolling text. + +## Top Row +![Top Row](https://github.com/datagod/meshtalk/blob/main/pics/Meshtalk%20messages.jpg?raw=true "Top Row") + +The top row has basic information messages from the system, a debug area that shows the currently executing function, and a spot to show incoming messages. + + +## Decoded Packets +![Decoded Packets](https://github.com/datagod/meshtalk/blob/main/pics/Meshtalk%20packet.jpg?raw=true "Packet values") + +Each packet that is intercepted will be displayed here in, decoded. Some fields such as RAW are not supported yet. This type of window is using a wrap around function to display the new lines. + + +## Extended Info +![Just the Keys](https://github.com/datagod/meshtalk/blob/main/pics/Meshtalk%20extended%20info.jpg?raw=true "Extended Info") -A Python client for use with Meshtastic devices. -This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. -It also provides access to any of the operations/data available in the device user interface or the Android application. -Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in. +This is a curses text pad that scrolls upwards as new lines are entered. In this example I am displaying the connected nodes in the mesh. -**[Getting Started Guide](https://meshtastic.org/docs/software/python/python-installation)** +This is a curses text pad that scrolls upwards as new lines are entered. In this example I am displaying the connected nodes in the mesh. -**[Documentation/API Reference](https://python.meshtastic.org/)** +## Send Messages +Press S to send a signed message or U to send an unsigned message. Press control+g when you are finished typing the message. -## Stats +## Viewing Messages +![Messages](https://github.com/datagod/meshtalk/blob/main/pics/Meshtalk%20help%20window%20send%20message%202.jpg?raw=true "Messages") -![Alt](https://repobeats.axiom.co/api/embed/c71ee8fc4a79690402e5d2807a41eec5e96d9039.svg "Repobeats analytics image") +As both signed and unsigned messages are sent or received, they are displayed in the Messages text box. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index ed3c17783..000000000 --- a/TODO.md +++ /dev/null @@ -1,40 +0,0 @@ -# TODO - -Basic functionality is complete now. - -## Eventual tasks - -- Improve documentation on properties/fields -- change back to Bleak for BLE support - now that they fixed https://github.com/hbldh/bleak/issues/139#event-3499535304 -- include more examples: textchat.py, replymessage.py all as one little demo - -- possibly use tk to make a multiwindow test console: https://stackoverflow.com/questions/12351786/how-to-redirect-print-statements-to-tkinter-text-widget - -## MeshtasticShell todos - -- Possibly use multiple windows: https://stackoverflow.com/questions/12351786/how-to-redirect-print-statements-to-tkinter-text-widget -- make pingpong test - -## Bluetooth support - -(Pre-alpha level feature - you probably don't want this one yet) - -- This library supports connecting to Meshtastic devices over either USB (serial) or Bluetooth. Before connecting to the device you must [pair](https://docs.ubuntu.com/core/en/stacks/bluetooth/bluez/docs/reference/pairing/outbound.html) your PC with it. -- We use the pip3 install "pygatt[GATTTOOL]" -- ./bin/run.sh --debug --ble --device 24:62:AB:DD:DF:3A - -## Done - -- DONE use port enumeration to find ports https://pyserial.readthedocs.io/en/latest/shortintro.html -- DONE make serial debug output optional (by providing a null stream) -- DONE make pubsub work -- DONE make docs decent -- DONE keep everything in dicts -- DONE have device send a special packet at boot so the serial client can detect if it rebooted -- DONE add fromId and toId to received messages dictionaries -- make command line options for displaying/changing config -- update nodedb as nodes change -- localConfig - getter/setter syntax: https://www.python-course.eu/python3_properties.php -- let user change radio params via commandline options -- keep nodedb up-to-date based on received MeshPackets -- handle radio reboots and redownload db when that happens. Look for a special FromRadio.rebooted packet diff --git a/bin/regen-protosbuf.sh b/bin/regen-protobufs.sh similarity index 100% rename from bin/regen-protosbuf.sh rename to bin/regen-protobufs.sh diff --git a/black_lager_app.py b/black_lager_app.py new file mode 100644 index 000000000..0870181db --- /dev/null +++ b/black_lager_app.py @@ -0,0 +1,1164 @@ +#!python3 + +from persona_wallet import PersonaWallet +from textwindow import TextWindow +from textpad import TextPad +from client_utils import * +import meshtastic +import meshtastic.serial_interface +import meshtastic.tcp_interface +import time +from datetime import datetime +import traceback +from pubsub import pub +import argparse +import collections +import sys +import os +import pickle + +# to review logfiles +import subprocess + +# for calculating distance +import geopy.distance + +# For capturing key presses and drawing text boxes +import curses +from curses.textpad import Textbox + +# for capturing ctl-c +from signal import signal, SIGINT +from sys import exit + +# PyNaCl libsodium library +from crypto_utils import NaclSuite + +from nacl.encoding import HexEncoder +from nacl.signing import VerifyKey +from nacl.exceptions import BadSignatureError + +NAME = "BlackLager" +DESCRIPTION = "Send and receive signed and unsigned messages from a Meshtastic device." +DEBUG = False + +parser = argparse.ArgumentParser(description=DESCRIPTION) +parser.add_argument('-s', '--send', type=str, help="send a text message") +parser.add_argument('-t', '--time', type=int, + help="seconds to listen before exiting", default=36000) +ifparser = parser.add_mutually_exclusive_group(required=False) +ifparser.add_argument('-p', '--port', type=str, + help="port the Meshtastic device is connected to (e.g., /dev/ttyUSB0)") +ifparser.add_argument('-i', '--host', type=str, + help="hostname/ipaddr of the device to connect to over TCP") +args = parser.parse_args() + +# process arguments and assign values to local variables +if args.send: + SendMessage = True + TheMessage = args.send +else: + SendMessage = False + +TimeToSleep = args.time + + +global PrintSleep # controls how fast the screens scroll +global OldPrintSleep # controls how fast the screens scroll +global TitleWindow +global StatusWindow +global Window1 +global Window2 +global Window3 +global Window4 +global Window5 +global Window6 +global Pad1 +global InputMessageBox +global INputMessageWindow +global IPAddress +global Interface +global DeviceStatus +global DeviceName +global DevicePort +global PacketsReceived +global PacketsSent +global LastPacketType +global BaseLat +global BaseLon + +global MacAddress +global DeviceID + +global PauseOutput +global PriorityOutput + + +PrintSleep = 0.1 +OldPrintSleep = PrintSleep + +# Create Persona wallet +wallet = PersonaWallet() + + +# -------------------------------------- +# Initialize Text window / pads -- +# -------------------------------------- + +def create_text_windows(): + + global StatusWindow + global TitleWindow + global Window1 + global Window2 + global Window3 + global Window4 + global Window5 + global HelpWindow + global Pad1 + global SendMessageWindow + global InputMessageWindow + global InputMessageBox + + # Colors are numbered, and start_color() initializes 8 + # basic colors when it activates color mode. + # They are: 0:black, 1:red, 2:green, 3:yellow, 4:blue, 5:magenta, 6:cyan, and 7:white. + # The curses module defines named constants for each of these colors: curses.COLOR_BLACK, curses.COLOR_RED, and so forth. + # Future Note for pads: call noutrefresh() on a number of windows to update the data structure, and then call doupdate() to update the screen. + + # Text windows + + stdscr.nodelay(1) # doesn't keep waiting for a key press + curses.start_color() + curses.noecho() + + # We do a quick check to prevent the screen boxes from being erased. Weird, I know. Could not find + # a solution. Am happy with this work around. + c = str(stdscr.getch()) + + # Note: When making changes, be very careful. Each Window's position is relative to the other ones on the same + # horizontal level. Change one setting at a time and see how it looks on your screen + + # Window1 Coordinates (info window) + Window1Height = 12 + Window1Length = 40 + Window1x1 = 0 + Window1y1 = 1 + Window1x2 = Window1x1 + Window1Length + Window1y2 = Window1y1 + Window1Height + + # Window2 Coordinates (small debug window) + Window2Height = 12 + Window2Length = 46 + Window2x1 = Window1x2 + 1 + Window2y1 = 1 + Window2x2 = Window2x1 + Window2Length + Window2y2 = Window2y1 + Window2Height + + # Window3 Coordinates (Messages) + Window3Height = 12 + Window3Length = 104 + Window3x1 = Window2x2 + 1 + Window3y1 = 1 + Window3x2 = Window3x1 + Window3Length + Window3y2 = Window3y1 + Window3Height + + # Window4 Coordinates (packet data) + Window4Height = 45 + #Window4Length = Window1Length + Window2Length + Window3Length + 2 + Window4Length = 60 + Window4x1 = 0 + Window4y1 = Window1y2 + Window4x2 = Window4x1 + Window4Length + Window4y2 = Window4y1 + Window4Height + + # We are going to put a window here as a border, but have the pad + # displayed inside + # Window5 Coordinates (to the right of window4) + Window5Height = 45 + Window5Length = 95 + Window5x1 = Window4x2 + 1 + Window5y1 = Window4y1 + Window5x2 = Window5x1 + Window5Length + Window5y2 = Window5y1 + Window5Height + + # Coordinates (scrolling pad/window for showing keys being decoded) + Pad1Columns = Window5Length - 2 + Pad1Lines = Window5Height - 2 + Pad1x1 = Window5x1+1 + Pad1y1 = Window5y1+1 + Pad1x2 = Window5x2 - 1 + Pad1y2 = Window5y2 - 1 + + # Help Window + HelpWindowHeight = 13 + HelpWindowLength = 35 + HelpWindowx1 = Window5x2 + 1 + HelpWindowy1 = Window5y1 + HelpWindowx2 = HelpWindowx1 + HelpWindowLength + HelpWindowy2 = HelpWindowy1 + HelpWindowHeight + + # SendMessage Window + # This window will be used to display the border + # and title and will surround the input window + SendMessageWindowHeight = 6 + SendMessageWindowLength = 35 + SendMessageWindowx1 = Window5x2 + 1 + SendMessageWindowy1 = HelpWindowy1 + HelpWindowHeight + SendMessageWindowx2 = SendMessageWindowx1 + SendMessageWindowLength + SendMessageWindowy2 = SendMessageWindowy1 + SendMessageWindowHeight + + # InputMessage Window + # This window will be used get the text to be sent + InputMessageWindowHeight = SendMessageWindowHeight - 2 + InputMessageWindowLength = SendMessageWindowLength - 2 + InputMessageWindowx1 = Window5x2 + 2 + InputMessageWindowy1 = HelpWindowy1 + HelpWindowHeight + 1 + InputMessageWindowx2 = InputMessageWindowx1 + InputMessageWindowLength - 2 + InputMessageWindowy2 = InputMessageWindowy1 + InputMessageWindowHeight - 2 + + try: + + # stdscr.clear() + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) + curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) + curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) + curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) + curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) + curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) + + # -------------------------------------- + # Draw Screen -- + # -------------------------------------- + + # Create windows + # name, rows, columns, y1, x1, y2, x2,ShowBorder,BorderColor,TitleColor): + TitleWindow = TextWindow('TitleWindow', 1, 50, 0, 0, 0, 50, 'N', 0, 0, stdscr) + StatusWindow = TextWindow( + 'StatusWindow', 1, 50, 0, 51, 0, 100, 'N', 0, 0, stdscr) + StatusWindow2 = TextWindow( + 'StatusWindow2', 1, 30, 0, 101, 0, 130, 'N', 0, 0, stdscr) + Window1 = TextWindow('Window1', Window1Height, Window1Length, + Window1y1, Window1x1, Window1y2, Window1x2, 'Y', 2, 2, stdscr) + Window2 = TextWindow('Window2', Window2Height, Window2Length, + Window2y1, Window2x1, Window2y2, Window2x2, 'Y', 2, 2, stdscr) + Window3 = TextWindow('Window3', Window3Height, Window3Length, + Window3y1, Window3x1, Window3y2, Window3x2, 'Y', 3, 3, stdscr) + Window4 = TextWindow('Window4', Window4Height, Window4Length, + Window4y1, Window4x1, Window4y2, Window4x2, 'Y', 5, 5, stdscr) + Window5 = TextWindow('Window5', Window5Height, Window5Length, + Window5y1, Window5x1, Window5y2, Window5x2, 'Y', 6, 6, stdscr) + HelpWindow = TextWindow('HelpWindow', HelpWindowHeight, HelpWindowLength, + HelpWindowy1, HelpWindowx1, HelpWindowy2, HelpWindowx2, 'Y', 7, 7, stdscr) + SendMessageWindow = TextWindow('SendMessageWindow', SendMessageWindowHeight, SendMessageWindowLength, + SendMessageWindowy1, SendMessageWindowx1, SendMessageWindowy2, SendMessageWindowx2, 'Y', 7, 7, stdscr) + InputMessageWindow = TextWindow('InputMessageWindow', InputMessageWindowHeight, InputMessageWindowLength, + InputMessageWindowy1, InputMessageWindowx1, InputMessageWindowy2, InputMessageWindowx2, 'N', 7, 7, stdscr) + Pad1 = TextPad('Pad1', Pad1Lines, Pad1Columns, + Pad1y1, Pad1x1, Pad1y2, Pad1x2, 'N', 5, stdscr) + + # each title needs to be initialized or you get errors in scrollprint + TitleWindow.Title, TitleWindow.TitleColor = "--Black Lager--", 2 + StatusWindow.Title, StatusWindow.TitleColor = "", 2 + StatusWindow2.Title, StatusWindow2.TitleColor = "", 2 + Window1.Title, Window1.TitleColor = "Device Info", 2 + Window2.Title, Window2.TitleColor = "Debug", 2 + Window3.Title, Window3.TitleColor = "Messages", 3 + Window4.Title, Window4.TitleColor = "Data Packets", 5 + Window5.Title, Window5.TitleColor = "Extended Information", 6 + HelpWindow.Title, HelpWindow.TitleColor = "Help", 7 + SendMessageWindow.Title, SendMessageWindow.TitleColor = "Press S to send a message", 7 + + TitleWindow.window_print(0, 0, TitleWindow.Title) + Window1.display_title() + Window2.display_title() + Window3.display_title() + Window4.display_title() + Window5.display_title() + HelpWindow.display_title() + SendMessageWindow.display_title() + + display_help_info() + + # Prepare edit window for send message + InputMessageBox = Textbox(InputMessageWindow.TextWindow) + + except Exception as ErrorMessage: + TraceMessage = traceback.format_exc() + AdditionalInfo = "Creating text windows" + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, stdscr) + + +# -------------------------------------- +# Meshtastic functions -- +# -------------------------------------- + + +def decode_packet(PacketParent, Packet, Filler, FillerChar, PrintSleep=0): + global DeviceStatus + global DeviceName + global DevicePort + global PacketsReceived + global PacketsSent + global LastPacketType + global HardwareModel + global DeviceID + + # This is a recursive function that will decode a packet (get key/value pairs from a dictionary) + # if the value is itself a dictionary, recurse + Window2.scroll_print("DecodePacket", 2, TimeStamp=True) + + # used to indent packets + if (PacketParent.upper() != 'MAINPACKET'): + Filler = Filler + FillerChar + + #Window4.scroll_print("{}".format(PacketParent).upper(), 2) + update_status_window(NewLastPacketType=PacketParent) + + # adjust the input to slow down the output for that cool retro feel + if (PrintSleep > 0): + time.sleep(PrintSleep) + + if PriorityOutput == True: + time.sleep(5) + + # if the packet is a dictionary, decode it + if isinstance(Packet, collections.abc.Mapping): + + for Key in Packet.keys(): + Value = Packet.get(Key) + + if (PrintSleep > 0): + time.sleep(PrintSleep) + + # if the value paired with this key is another dictionary, keep digging + if isinstance(Value, collections.abc.Mapping): + + # Print the name/type of the packet + #Window4.scroll_print(" ", 2) + LastPacketType = Key.upper() + + decode_packet("{}/{}".format(PacketParent, Key).upper(), Value, Filler, FillerChar, PrintSleep=PrintSleep) + + # else: + # # Print KEY if not RAW (gotta decode those further, or ignore) + # if (Key == 'raw'): + # #Window4.scroll_print("{} RAW value not yet suported by DecodePacket function".format(Filler), 2) + # else: + # #Window4.scroll_print(" {}{}: {}".format(Filler, Key, Value), 2) + + else: + Window2.scroll_print("Warning: Not a packet!", 5, TimeStamp=True) + + +def on_receive(packet, interface): + """Called when a new packet arrives""" + global PacketsReceived + global PacketsSent + + PacketsReceived = PacketsReceived + 1 + + Window2.scroll_print("onReceive", 2, TimeStamp=True) + #Window4.scroll_print(" ", 2) + #Window4.scroll_print("==Packet RECEIVED======================================", 2) + + decoded = packet.get('decoded') + unsigned_message = decoded.get('text') + portnum = decoded.get('portnum') + + sender = packet.get('from') + + # Recursively decode all the packets of packets + decode_packet('MainPacket', packet, Filler='', FillerChar='', PrintSleep=PrintSleep) + + if unsigned_message: + Window3.scroll_print("UNSIGNED message from: {} - {}".format(sender, unsigned_message), 2, TimeStamp=True) + elif portnum == "BLACK_LAGER": + signed_message = decoded.get('payload') + # Split the concatenated byte string into the message and key + signed_b64 = signed_message[:-64] + verify_key_b64 = signed_message[-64:] + + # Create a VerifyKey object from a base64 serialized public key + verify_key = VerifyKey(verify_key_b64, encoder=HexEncoder) + + # Check the validity of a message's signature + try: + text_message_bytes = verify_key.verify(signed_b64, encoder=HexEncoder) + text_message = text_message_bytes.decode('utf-8') + Window3.scroll_print("VERIFIED SIGNED message from: {} - {}".format(sender, text_message), 2, TimeStamp=True) + except BadSignatureError: + Window3.scroll_print("Signature from {} was forged or corrupt".format(sender), 2, TimeStamp=True) + + #Window4.scroll_print("=======================================================", 2) + #Window4.scroll_print(" ", 2) + + +# called when we (re)connect to the radio +def on_connection_established(interface, topic=pub.AUTO_TOPIC): + global PriorityOutput + + if not PriorityOutput: + update_status_window(NewDeviceStatus="CONNECTED", Color=2) + + From = "BaseStation" + To = "All" + current_time = datetime.now().strftime("%H:%M:%S") + Message = "MeshWatch active, please respond. [{}]".format( + current_time) + Window3.scroll_print("From: {} - {}".format(From, + Message, To), 2, TimeStamp=True) + + try: + interface.sendText(Message, wantAck=True) + #Window4.scroll_print("", 2) + #Window4.scroll_print("==Packet SENT==========================================", 3) + #Window4.scroll_print("To: {}:".format(To), 3) + #Window4.scroll_print("From {}:".format(From), 3) + #Window4.scroll_print("Message {}:".format(Message), 3) + #Window4.scroll_print("=======================================================", 3) + #Window4.scroll_print("", 2) + + except Exception as ErrorMessage: + TraceMessage = traceback.format_exc() + AdditionalInfo = "Sending text message ({})".format(Message) + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, stdscr) + + +# called when we (re)connect to the radio +def on_connection_lost(interface, topic=pub.AUTO_TOPIC): + global PriorityOutput + if (PriorityOutput == False): + Window2.scroll_print('onConnectionLost', 2, TimeStamp=True) + update_status_window(NewDeviceStatus="DISCONNECTED", Color=1) + + +# called when we (re)connect to the radio +def on_node_update(interface, topic=pub.AUTO_TOPIC): + global PriorityOutput + if (PriorityOutput == False): + Window2.scroll_print('onNodeUpdated', 2, TimeStamp=True) + Window1.window_print(1, 4, 'UPDATE RECEIVED', 1, TimeStamp=True) + #Window4.scroll_print("", 2) + + +def sigint_handler(signal_received, frame): + # Handle any cleanup here + print('WARNING: Somethign bad happened. SIGINT detected.') + final_cleanup(stdscr) + print('** END OF LINE') + sys.exit('Meshwatch exiting...') + + +def poll_keyboard(): + global stdscr + global Window2 + global interface + + return_char = "" + + # curses.filter() + curses.noecho() + + try: + c = chr(stdscr.getch()) + except Exception as ErrorMessage: + c = "" + + # Look for digits (ascii 48-57 == digits 0-9) + if (c >= '0' and c <= '9'): + #print ("Digit detected") + #StatusWindow.ScrollPrint("Digit Detected",2) + return_char = c + + if (c != ""): + #print ("----------------") + #print ("Key Pressed: ",Key) + #print ("----------------") + OutputLine = "Key Pressed: " + c + # Window2.ScrollPrint(OutputLine,4) + process_keypress(c) + return return_char + + +def process_keypress(key): + # c = clear screen + # i = get node info + # l = show system LOGS (dmesg) + # n = show all nodes in mesh + # p = pause + # q = quit + # r = reboot + # u = Send unsigned message + # s = Send signed message + # t = test messages + # k = send keys + + global stdscr + global StatusWindow + global Window2 + global Window4 + global interface + global PauseOutput + global PriorityOutput + global PrintSleep + global OldPrintSleep + + output_line = "KEYPRESS: [" + str(key) + "]" + Window2.scroll_print(output_line, 5) + + if key == "p" or key == " ": + PauseOutput = not PauseOutput + if PauseOutput: + Window2.scroll_print("Pausing output", 2) + StatusWindow.window_print(0, 0, "** Output SLOW - press SPACE again to cancel **", 1) + PrintSleep = PrintSleep * 3 + else: + Window2.scroll_print("Resuming output", 2) + StatusWindow.window_print(0, 0, " ", 3) + PrintSleep = OldPrintSleep + + elif key == "i": + Window4.clear() + get_node_info(interface) + + elif key == "l": + Pad1.clear() + display_logs(0.01) + + elif key == "n": + Pad1.clear() + display_nodes(interface) + + elif key == "q": + wallet.write_wallet_to_file() + final_cleanup(stdscr) + exit() + + elif key == "c": + clear_all_windows() + + elif key == "r": + Window2.scroll_print('** REBOOTING **', 1) + + final_cleanup(stdscr) + os.execl(sys.executable, sys.executable, *sys.argv) + + elif key == "u": + send_unsigned_message(interface) + + elif key == "s": + send_signed_message(interface) + + elif key == "t": + test_mesh(interface, 5, 10) + + elif key == "k": + send_keys(interface) + + +def send_keys(interface): + node_list = [] + suite = NaclSuite() + + Window2.scroll_print("SendSignedMessagePacket", 2) + TheMessage='' + + + InputMessageWindow.TextWindow.move(0,0) + #Change color temporarily + SendMessageWindow.TextWindow.attron(curses.color_pair(2)) + SendMessageWindow.TextWindow.border() + SendMessageWindow.TitleColor = 2 + SendMessageWindow.Title = 'Press CTL-G to send' + SendMessageWindow.display_title() + + SendMessageWindow.TextWindow.attroff(curses.color_pair(2)) + + SendMessageWindow.TextWindow.refresh() + + #Show cursor + + curses.curs_set(True) + + InputMessageWindow.TextWindow.erase() + InputMessageBox.edit() + curses.curs_set(False) + + for node in interface.nodes.values(): + new_tuple = (node['user']['longName'], node['user']['macaddr'], node['num']) + node_list.append(new_tuple) + + + for node in node_list: + local_name = node[0] + mac_addr = node[1] + node_num = node[2] + public_key, private_key = suite.generate_key_pairs(local_name) + suite.add_person_to_book(local_name,mac_addr,node_num, bytes(public_key), bytes(private_key)) + interface.sendData(public_key, wantAck=True) + interface.sendData(private_key, wantAck=True) + + suite.write_all_secrets_to_file() + + Window4.scroll_print(" ", 2) + Window4.scroll_print("==Keys Sent SENT===================================", 3) + Window4.scroll_print("=======================================================", 3) + Window4.scroll_print(" ", 2) + + SendMessageWindow.clear() + SendMessageWindow.TitleColor = 2 + SendMessageWindow.Title = 'Press S to send a message' + SendMessageWindow.display_title() + + Window3.scroll_print("To: All - {}".format(TheMessage), 2, TimeStamp=True) + + +def send_unsigned_message(interface, Message=''): + Window2.scroll_print("SendUnsignedMessagePacket", 2) + TheMessage = '' + + InputMessageWindow.TextWindow.move(0, 0) + # Change color temporarily + SendMessageWindow.TextWindow.attron(curses.color_pair(2)) + SendMessageWindow.TextWindow.border() + SendMessageWindow.TitleColor = 2 + SendMessageWindow.Title = 'Press CTL-G to send' + SendMessageWindow.display_title() + + SendMessageWindow.TextWindow.attroff(curses.color_pair(2)) + + SendMessageWindow.TextWindow.refresh() + + # Show cursor + curses.curs_set(True) + + # Let the user edit until Ctrl-G is struck. + InputMessageWindow.TextWindow.erase() + InputMessageBox.edit() + curses.curs_set(False) + + # Get resulting contents + + TheMessage = InputMessageBox.gather().replace("\n", " ") + + # remove last character which seems to be interfering with line printing + TheMessage = TheMessage[0:-1] + + # Send the message to the device + interface.sendText(TheMessage, wantAck=True) + + #Window4.scroll_print(" ", 2) + #Window4.scroll_print("==Unsigned Packet SENT=================================", 3) + #Window4.scroll_print("To: All:", 3) + #Window4.scroll_print("From: BaseStation", 3) + #Window4.scroll_print("Message: {}".format(TheMessage), 3) + #Window4.scroll_print("=======================================================", 3) + #Window4.scroll_print(" ", 2) + + SendMessageWindow.clear() + SendMessageWindow.TitleColor = 2 + SendMessageWindow.Title = 'Press S to send a message' + SendMessageWindow.display_title() + + Window3.scroll_print( + "UNSIGNED message to: All - {}".format(TheMessage), 2, TimeStamp=True) + + +def send_signed_message(interface, Message=''): + #Window2.scroll_print("SendSignedMessagePacket", 2) + TheMessage = '' + + InputMessageWindow.TextWindow.move(0, 0) + # Change color temporarily + SendMessageWindow.TextWindow.attron(curses.color_pair(2)) + SendMessageWindow.TextWindow.border() + SendMessageWindow.TitleColor = 2 + SendMessageWindow.Title = 'Press CTL-G to send' + SendMessageWindow.display_title() + + SendMessageWindow.TextWindow.attroff(curses.color_pair(2)) + + SendMessageWindow.TextWindow.refresh() + + # Show cursor + curses.curs_set(True) + + # Let the user edit until Ctrl-G is struck. + InputMessageWindow.TextWindow.erase() + InputMessageBox.edit() + curses.curs_set(False) + + # Get resulting contents + TheMessage = InputMessageBox.gather().replace("\n", " ") + + # remove last character which seems to be interfering with line printing + TheMessage = TheMessage[0:-1] + + # Sign the message and send the signed message to the device + signing_key = pickle.loads(wallet.current_persona.private_key) + + # Convert the text message to bytes and sign it with the private key + text_message_bytes = TheMessage.encode('utf-8') + + # Sign a message with the signing key + signed_b64 = signing_key.sign(text_message_bytes, encoder=HexEncoder) + + # Obtain the verify key for a given signing key + verify_key = signing_key.verify_key + + # Serialize the verify key to send it to a third party + verify_key_b64 = verify_key.encode(encoder=HexEncoder) + + signed_message_bytes = signed_b64 + verify_key_b64 + + interface.sendSignedText(signed_message_bytes, wantAck=True) + + #Window4.scroll_print(" ", 2) + #Window4.scroll_print("==Signed Packet SENT===================================", 3) + #Window4.scroll_print("To: All:", 3) + #Window4.scroll_print("From: BaseStation", 3) + #Window4.scroll_print("Message: {}".format(TheMessage), 3) + #Window4.scroll_print("=======================================================", 3) + #Window4.scroll_print(" ", 2) + + SendMessageWindow.clear() + SendMessageWindow.TitleColor = 2 + SendMessageWindow.Title = 'Press S to send a message' + SendMessageWindow.display_title() + + Window3.scroll_print("SIGNED message to: All - {}".format(TheMessage), 2, TimeStamp=True) + + +def go_to_sleep(TimeToSleep): + Window2.scroll_print("GoToSleep({})".format(TimeToSleep), 2, TimeStamp=True) + for i in range(0, (TimeToSleep * 10)): + # Check for keyboard input + poll_keyboard() + time.sleep(0.1) + + +def clear_all_windows(): + Window1.clear() + Window2.clear() + Window3.clear() + Window4.clear() + Window5.clear() + Window2.scroll_print("**Clearing screens**", 2) + update_status_window() + + +def update_status_window(NewDeviceStatus='', + NewDeviceName='', + NewDevicePort='', + NewHardwareModel='', + NewMacAddress='', + NewDeviceID='', + NewBatteryLevel=-1, + NewLastPacketType='', + NewLat=0, + NewLon=0, + Color=2 + ): + # Window2.ScrollPrint("UpdateStatusWindow",2,TimeStamp=True) + + global DeviceStatus + global DeviceName + global DevicePort + global PacketsReceived + global PacketsSent + global LastPacketType + global HardwareModel + global MacAddress + global DeviceID + global BaseLat + global BaseLon + + BatteryLevel = -1 + + x1, y1 = 1, 1 # DeviceName + x2, y2 = 1, 2 # HardwareModel + x3, y3 = 1, 3 # DeviceStatus + x4, y4 = 1, 4 # MacAddress + x5, y5 = 1, 5 # DeviceID + x6, y6 = 1, 6 # PacketsDecoded + x7, y7 = 1, 7 # LastPacketType + x8, y8 = 1, 8 # BatteryLevel + x9, y9 = 1, 9 # BaseLat + x10, y10 = 1, 10 # BaseLon + + if (NewDeviceName != ''): + DeviceName = NewDeviceName + + if (NewDeviceStatus != ''): + DeviceStatus = NewDeviceStatus + + if (NewDevicePort != ''): + DevicePort = NewDevicePort + + if (NewLastPacketType != ''): + LastPacketType = NewLastPacketType + + if (NewHardwareModel != ''): + HardwareModel = NewHardwareModel + + if (NewMacAddress != ''): + MacAddress = NewMacAddress + + if (NewDeviceID != ''): + DeviceID = NewDeviceID + + if (NewBatteryLevel > -1): + BatteryLevel = NewBatteryLevel + + if (NewLat != 0): + BaseLat = NewLat + + if (NewLon != 0): + BaseLon = NewLon + + # DeviceName + Window1.window_print(y1, x1, "UserName: ", 2) + Window1.window_print(y1, x1 + 12, DeviceName, Color) + + # DeviceStatus + Window1.window_print(y2, x2, "Model: " + HardwareModel, 2) + Window1.window_print(y2, x2 + 12, HardwareModel, Color) + + # DeviceStatus + Window1.window_print(y3, x3, "Status: " + DeviceStatus, 2) + Window1.window_print(y3, x3 + 12, DeviceStatus, Color) + + # MacAddress + Window1.window_print(y4, x4, "MacAddress: ", 2) + Window1.window_print(y4, x4 + 12, MacAddress, Color) + + # DeviceID + Window1.window_print(y5, x5, "DeviceID: ", 2) + Window1.window_print(y5, x5 + 12, DeviceID, Color) + + # PacketsReceived + Window1.window_print(y6, x6, "Packets Decoded: ", 2) + Window1.window_print(y6, x6 + 17, "{}".format(PacketsReceived), Color) + + # LastPacketType + Window1.window_print(y7, x7, "LastPacketType: ", 2) + Window1.window_print(y7, x7 + 17, LastPacketType, Color) + + # BatteryLevel + Window1.window_print(y8, x8, "BatteryLevel: ", 2) + Window1.window_print(y8, x8 + 17, "{}".format(BatteryLevel), Color) + + # Base LAT + Window1.window_print(y9, x9, "Base LAT: ", 2) + Window1.window_print(y9, x9 + 17, "{}".format(BaseLat), Color) + + # Base LON + Window1.window_print(y10, x10, "Base LON: ", 2) + Window1.window_print(y10, x10 + 17, "{}".format(BaseLon), Color) + + +def display_help_info(): + HelpWindow.scroll_print("C - CLEAR Screen", 7) + HelpWindow.scroll_print("I - Request node INFO", 7) + HelpWindow.scroll_print("L - Show LOGS", 7) + HelpWindow.scroll_print("N - Show all NODES", 7) + HelpWindow.scroll_print("Q - QUIT program", 7) + HelpWindow.scroll_print("R - RESTART Black Lager", 7) + HelpWindow.scroll_print("U - SEND unsigned message", 7) + HelpWindow.scroll_print("S - SEND signed message", 7) + HelpWindow.scroll_print("T - TEST mesh network", 7) + HelpWindow.scroll_print("K - Assign KEYS", 7) + HelpWindow.scroll_print("SPACEBAR - Slow/Fast output", 7) + + +def get_node_info(interface): + # Get information about my own node + + Window4.scroll_print(" ", 2) + Window4.scroll_print("==MyNodeInfo===================================", 3) + TheNode = interface.getMyNodeInfo() + decode_packet('MYNODE', TheNode, '', '', PrintSleep=PrintSleep) + Window4.scroll_print("===============================================", 3) + Window4.scroll_print(" ", 2) + + if 'latitude' in TheNode['position'] and 'longitude' in TheNode['position']: + BaseLat = TheNode['position']['latitude'] + BaseLon = TheNode['position']['longitude'] + update_status_window(NewLon=BaseLon, NewLat=BaseLat, Color=2) + + if 'longName' in TheNode['user']: + update_status_window(NewDeviceName=TheNode['user']['longName'], Color=2) + + if 'hwModel' in TheNode['user']: + update_status_window( + NewHardwareModel=TheNode['user']['hwModel'], Color=2) + + if 'macaddr' in TheNode['user']: + update_status_window(NewMacAddress=TheNode['user']['macaddr'], Color=2) + + if 'id' in TheNode['user']: + update_status_window(NewDeviceID=TheNode['user']['id'], Color=2) + + if 'batteryLevel' in TheNode['position']: + update_status_window( + NewBatteryLevel=TheNode['position']['batteryLevel'], Color=2) + + +def display_nodes(interface): + Pad1.clear() + Pad1.pad_print("--NODES IN MESH------------", 3) + + if (PriorityOutput == True): + time.sleep(5) + + try: + # interface.nodes.values() will return a dictionary + for node in (interface.nodes.values()): + Pad1.pad_print("NAME: {}".format(node['user']['longName']), 3) + Pad1.pad_print("NODE: {}".format(node['num']), 3) + Pad1.pad_print("ID: {}".format(node['user']['id']), 3) + Pad1.pad_print("MAC: {}".format(node['user']['macaddr']), 3) + + if 'position' in node.keys(): + + # used to calculate XY for tile servers + if 'latitude' in node['position'] and 'longitude' in node['position']: + Lat = node['position']['latitude'] + Lon = node['position']['longitude'] + + xtile, ytile = deg2num(Lat, Lon, 10) + Pad1.pad_print("Tile: {}/{}".format(xtile, ytile), 3) + Pad1.pad_print("LAT: {}".format( + node['position']['latitude']), 3) + Pad1.pad_print("LONG: {}".format( + node['position']['longitude']), 3) + Distance = geopy.distance.geodesic( + (Lat, Lon), (BaseLat, BaseLon)).m + Pad1.pad_print("Distance: {:.3f} m".format(Distance), 3) + + if 'batteryLevel' in node['position']: + Battery = node['position']['batteryLevel'] + Pad1.pad_print("Battery: {}".format(Battery), 3) + + if 'lastHeard' in node.keys(): + LastHeardDatetime = time.strftime( + '%Y-%m-%d %H:%M:%S', time.localtime(node['lastHeard'])) + Pad1.pad_print("LastHeard: {}".format(LastHeardDatetime), 3) + + time.sleep(PrintSleep) + Pad1.pad_print("", 3) + + except Exception as ErrorMessage: + TraceMessage = traceback.format_exc() + AdditionalInfo = "Processing node info" + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, stdscr) + + Pad1.pad_print("---------------------------", 3) + + +def exec_process(cmdline, silent, input=None, **kwargs): + """Execute a subprocess and returns the returncode, stdout buffer and stderr buffer. + Optionally prints stdout and stderr while running.""" + try: + sub = subprocess.Popen(cmdline, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) + stdout, stderr = sub.communicate(input=input) + returncode = sub.returncode + if not silent: + sys.stdout.write(stdout) + sys.stderr.write(stderr) + except OSError as e: + if e.errno == 2: + raise RuntimeError( + '"%s" is not present on this system' % cmdline[0]) + else: + raise + if returncode != 0: + raise RuntimeError('Got return value %d while executing "%s", stderr output was:\n%s' % ( + returncode, " ".join(cmdline), stderr.rstrip("\n"))) + return stdout + + +def tail(f, n): + assert n >= 0 + pos, lines = n+1, [] + while len(lines) <= n: + try: + f.seek(-pos, 2) + except IOError: + f.seek(0) + break + finally: + lines = list(f) + pos *= 2 + return lines[-n:] + + +def display_logs(ScrollSleep): + global PriorityOutput + + # we want to stop all other output to prevent text being written to other windows + PriorityOutput = True + Window2.scroll_print("PriorityOutput: activated") + + try: + with open("/var/log/kern.log") as f: + + f = tail(f, 50) + + for line in f: + Pad1.pad_print(line, 3) + time.sleep(ScrollSleep) + poll_keyboard() + except IOError: + Pad1.pad_print("Could not open /var/log/kern.log.", 3) + + PriorityOutput = False + Window2.scroll_print("PriorityOutput: deactivated") + + +def test_mesh(interface, MessageCount=10, Sleep=10): + Window2.scroll_print("TestMesh", 2) + + for i in range(1, MessageCount+1): + + TheMessage = '' + current_time = datetime.now().strftime("%H:%M:%S") + TheMessage = "This is Base station. Message: {} Date: {}".format( + i, current_time) + + # Send the message to the device + interface.sendText(TheMessage, wantAck=True) + + Window4.scroll_print(" ", 2) + Window4.scroll_print("==Packet SENT==========================================", 3) + Window4.scroll_print("To: All:", 3) + Window4.scroll_print("From: BaseStation", 3) + Window4.scroll_print("Message: {}".format(TheMessage), 3) + Window4.scroll_print("=======================================================", 3) + Window4.scroll_print(" ", 2) + + SendMessageWindow.clear() + SendMessageWindow.TitleColor = 2 + SendMessageWindow.Title = 'Press S to send a message' + SendMessageWindow.display_title() + + Window3.scroll_print("To: All - {}".format(TheMessage), 2, TimeStamp=True) + + go_to_sleep(Sleep) + + +def main(stdscr): + global interface + global DeviceStatus + global DeviceName + global DevicePort + global PacketsSent + global PacketsReceived + global LastPacketType + global HardwareModel + global MacAddress + global DeviceID + global PauseOutput + global HardwareModel + global PriorityOutput + global BaseLat + global BaseLon + + try: + DeviceName = '??' + DeviceStatus = '??' + DevicePort = '??' + PacketsReceived = 0 + PacketsSent = 0 + LastPacketType = '' + HardwareModel = '' + MacAddress = '' + DeviceName = '' + DeviceID = '' + PauseOutput = False + HardwareModel = '??' + PriorityOutput = False, + BaseLat = 0 + BaseLon = 0 + + if curses.LINES < 57 or curses.COLS < 190: + ErrorMessage = "Display area too small. Increase window size or reduce font size." + TraceMessage = traceback.format_stack()[0] + AdditionalInfo = "57 lines and 190 columns required. Found {} lines and {} columns.".format( + curses.LINES, curses.COLS) + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, stdscr) + + create_text_windows() + Window4.scroll_print("System initiated", 2) + Window2.scroll_print("Priorityoutput: {}".format(PriorityOutput), 1) + + # Instantiate a meshtastic object + # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0 + if args.host: + Window4.scroll_print("Connecting to device on host {}".format(args.host), 2) + interface = meshtastic.tcp_interface.TCPInterface(args.host) + elif args.port: + Window4.scroll_print("Connecting to device at port {}".format(args.port), 2) + interface = meshtastic.serial_interface.SerialInterface(args.port) + else: + Window4.scroll_print("Finding Meshtastic device", 2) + interface = meshtastic.serial_interface.SerialInterface() + + # subscribe to connection and receive channels + Window4.scroll_print("Subscribe to publications", 2) + pub.subscribe(on_connection_established, "meshtastic.connection.established") + pub.subscribe(on_connection_lost, "meshtastic.connection.lost") + + time.sleep(2) + # Get node info for connected device + Window4.scroll_print("Requesting device info", 2) + get_node_info(interface) + + # Check for message to be sent (command line option) + if SendMessage: + interface.sendText(TheMessage, wantAck=True) + + # Go into listening mode + Window4.scroll_print("Listening for: {} seconds".format(TimeToSleep), 2) + Window4.scroll_print("Subscribing to interface channels...", 2) + pub.subscribe(on_receive, "meshtastic.receive") + + while True: + go_to_sleep(5) + + except Exception as ErrorMessage: + time.sleep(2) + TraceMessage = traceback.format_exc() + AdditionalInfo = "Main function " + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, stdscr) + + # if SIGINT or CTL-C detected, run SIGINT_handler to exit gracefully + signal(SIGINT, sigint_handler) + + +if __name__ == '__main__': + try: + # Initialize curses + stdscr = curses.initscr() + # Turn off echoing of keys, and enter cbreak mode, where no buffering is performed on keyboard input + curses.noecho() + curses.cbreak() + curses.curs_set(0) + + # In keypad mode, escape sequences for special keys (like the cursor keys) will be interpreted + # and a special value like curses.KEY_LEFT will be returned + stdscr.keypad(1) + # Enter the main loop + main(stdscr) + + # Set everything back to normal + final_cleanup(stdscr) + + except Exception as ErrorMessage: + # In event of error, restore terminal to sane state. + TraceMessage = traceback.format_exc() + AdditionalInfo = "Main pre-amble" + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, stdscr) diff --git a/client_utils.py b/client_utils.py new file mode 100644 index 000000000..699a34b88 --- /dev/null +++ b/client_utils.py @@ -0,0 +1,73 @@ +import inspect +import math +import time +import sys +import curses + + +def error_handler(ErrorMessage, TraceMessage, AdditionalInfo, stdscr): + calling_function = inspect.stack()[1][3] + final_cleanup(stdscr) + print("") + print("") + print("--------------------------------------------------------------") + print("ERROR - Function (", calling_function, ") has encountered an error. ") + print(ErrorMessage) + print("") + print("") + print("TRACE") + print(TraceMessage) + print("") + print("") + if (AdditionalInfo != ""): + print("Additonal info:", AdditionalInfo) + print("") + print("") + print("--------------------------------------------------------------") + print("") + print("") + time.sleep(1) + sys.exit('Meshwatch exiting...') + + +def final_cleanup(stdscr): + stdscr.keypad(0) + curses.echo() + curses.nocbreak() + curses.curs_set(1) + curses.endwin() + + +def deg2num(lat_deg, lon_deg, zoom): + lat_rad = math.radians(lat_deg) + n = 2.0 ** zoom + xtile = int((lon_deg + 180.0) / 360.0 * n) + ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + return xtile, ytile + + +def from_str(valstr): + """try to parse as int, float or bool (and fallback to a string as last resort) + Returns: an int, bool, float, str or byte array (for strings of hex digits) + Args: + valstr (string): A user provided string + """ + if (len(valstr) == 0): # Treat an empty string as an empty bytes + val = bytes() + elif (valstr.startswith('0x')): + # if needed convert to string with asBytes.decode('utf-8') + val = bytes.fromhex(valstr[2:]) + elif valstr == True: + val = True + elif valstr == False: + val = False + else: + try: + val = int(valstr) + except ValueError: + try: + val = float(valstr) + except ValueError: + val = valstr # Not a float or an int, assume string + + return val diff --git a/crypto_utils.py b/crypto_utils.py new file mode 100644 index 000000000..2b1ceb2ec --- /dev/null +++ b/crypto_utils.py @@ -0,0 +1,83 @@ +from meshtastic import persona_pb2 +from nacl.public import PrivateKey, Box +import os, sys + + +class NaclSuite: + # suite for PyNaCl + + def __init__(self): + # Secret vault stores collections of device name & corresponding keys + self.secret_vault = {} + self.wallet = persona_pb2.Wallet() + + def align_with_config(self): + people = self.read_from_config() + for person in people: + self.secret_vault[person.local_name] = (person.public_key, person.private_key) + + def get_config_path(self): + path_to_script = os.path.dirname(os.path.realpath(__file__)) + return path_to_script[:-13] + "/meshtastic_node/config.txt" + + def read_from_config(self): + filename = self.get_config_path() + temp = persona_pb2.Wallet() + f = open(filename, "rb") + temp.ParseFromString(f.read()) + print( type(temp.person)) + for person in temp.person: + print (person.local_name) + print (person.public_key) + print (person.private_key) + + # Public / private key generator using Curve25519 + # Uses device_name as key + def generate_key_pairs(self, device_name: str): + # Keys are 48 bytes + + newPrivateKey = PrivateKey.generate() + newPublicKey = newPrivateKey.public_key + try: + self.secret_vault[device_name] = [newPublicKey,newPrivateKey] + print("keys generation succeed") + except AttributeError: + print("attribute error, secret vault most likely not initialized") + return + + return newPublicKey, newPrivateKey + + # Remove a device key pairs using device_name + def remove_key_pairs(self, device_name: str): + try: + del self.secret_vault[device_name] + print("Successfully deleted key") + except Exception as e: + print(repr(e)) + + # Print out all device names and key pairs + def fbi_open_up(self): + for eachKey in self.secret_vault.keys(): + print("Device name : " + eachKey) + print("Public Key:") + print(self.secret_vault[eachKey][0]) + print("Private Key: ") + print(self.secret_vault[eachKey[1]]) + + def add_person_to_book(self, local_name, address, num, public_key=b'0', private_key=b'0'): + new_dude = persona_pb2.Persona() + new_dude.local_name = local_name + new_dude.mac_address = address + new_dude.node_num = num + new_dude.public_key = bytes(public_key) + new_dude.private_key = bytes(private_key) + self.wallet.person.append(new_dude) + + def write_all_secrets_to_file(self): + file_name = self.get_config_path() + f = open(file_name, 'ab') + f.write(self.wallet.SerializeToString()) + f.close() + + def print_book(self): + print(self.wallet) diff --git a/examples/signed_message_example.py b/examples/signed_message_example.py new file mode 100644 index 000000000..39339eac7 --- /dev/null +++ b/examples/signed_message_example.py @@ -0,0 +1,38 @@ +"""Simple program to demo how to send a signed message. + To run: python examples/signed_message_example.py +""" + +import sys +import meshtastic +import meshtastic.serial_interface +from nacl.signing import SigningKey +from nacl.encoding import HexEncoder + +# simple arg check +if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} message") + sys.exit(3) + +# By default will try to find a meshtastic device, +# otherwise provide a device path like /dev/ttyUSB0 +iface = meshtastic.serial_interface.SerialInterface() + +signing_key = SigningKey.generate() + +# Convert the text message to bytes and sign it with the private key +text_message_bytes = sys.argv[1].encode('utf-8') + +# Sign a message with the signing key +signed = signing_key.sign(text_message_bytes, encoder=HexEncoder) + +# Obtain the verify key for a given signing key +verify_key = signing_key.verify_key + +# Serialize the verify key to send it to a third party +verify_key_b64 = verify_key.encode(encoder=HexEncoder) + +signed_message_bytes = signed + verify_key_b64 + +iface.sendSignedText(signed_message_bytes, wantAck=True) + +iface.close() diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py index e4a172c8e..2140f5648 100644 --- a/meshtastic/__init__.py +++ b/meshtastic/__init__.py @@ -174,6 +174,12 @@ def _receiveInfoUpdate(iface, asDict): iface._getOrCreateByNum(asDict["from"])["hopLimit"] = asDict.get("hopLimit") +def _onSignedTextReceive(iface, asDict): + """Parsing for received signed text messages""" + logging.debug(f'in _onSignedTextReceive() asDict:{asDict}') + _receiveInfoUpdate(iface, asDict) + + """Well known message payloads can register decoders for automatic protobuf parsing""" protocols = { portnums_pb2.PortNum.TEXT_MESSAGE_APP: KnownProtocol("text", onReceive=_onTextReceive), @@ -182,6 +188,7 @@ def _receiveInfoUpdate(iface, asDict): portnums_pb2.PortNum.ADMIN_APP: KnownProtocol("admin", admin_pb2.AdminMessage), portnums_pb2.PortNum.ROUTING_APP: KnownProtocol("routing", mesh_pb2.Routing), portnums_pb2.PortNum.TELEMETRY_APP: KnownProtocol("telemetry", telemetry_pb2.Telemetry), - portnums_pb2.PortNum.REMOTE_HARDWARE_APP: KnownProtocol("remotehw", remote_hardware_pb2.HardwareMessage), - portnums_pb2.PortNum.SIMULATOR_APP: KnownProtocol("simulator", mesh_pb2.Compressed) + portnums_pb2.PortNum.REMOTE_HARDWARE_APP: KnownProtocol("remotehw", remote_hardware_pb2.HardwareMessage), + portnums_pb2.PortNum.SIMULATOR_APP: KnownProtocol("simulator", mesh_pb2.Compressed), + portnums_pb2.PortNum.BLACK_LAGER: KnownProtocol("signed-text", onReceive=_onSignedTextReceive) } diff --git a/meshtastic/config_pb2.py b/meshtastic/config_pb2.py index 299d0cd44..910af1a62 100644 --- a/meshtastic/config_pb2.py +++ b/meshtastic/config_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onfig.proto\"\xd3\x14\n\x06\x43onfig\x12&\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x14.Config.DeviceConfigH\x00\x12*\n\x08position\x18\x02 \x01(\x0b\x32\x16.Config.PositionConfigH\x00\x12$\n\x05power\x18\x03 \x01(\x0b\x32\x13.Config.PowerConfigH\x00\x12(\n\x07network\x18\x04 \x01(\x0b\x32\x15.Config.NetworkConfigH\x00\x12(\n\x07\x64isplay\x18\x05 \x01(\x0b\x32\x15.Config.DisplayConfigH\x00\x12\"\n\x04lora\x18\x06 \x01(\x0b\x32\x12.Config.LoRaConfigH\x00\x12,\n\tbluetooth\x18\x07 \x01(\x0b\x32\x17.Config.BluetoothConfigH\x00\x1a\xae\x01\n\x0c\x44\x65viceConfig\x12\'\n\x04role\x18\x01 \x01(\x0e\x32\x19.Config.DeviceConfig.Role\x12\x16\n\x0eserial_enabled\x18\x02 \x01(\x08\x12\x19\n\x11\x64\x65\x62ug_log_enabled\x18\x03 \x01(\x08\"B\n\x04Role\x12\n\n\x06\x43LIENT\x10\x00\x12\x0f\n\x0b\x43LIENT_MUTE\x10\x01\x12\n\n\x06ROUTER\x10\x02\x12\x11\n\rROUTER_CLIENT\x10\x03\x1a\x85\x03\n\x0ePositionConfig\x12\x1f\n\x17position_broadcast_secs\x18\x01 \x01(\r\x12(\n position_broadcast_smart_enabled\x18\x02 \x01(\x08\x12\x16\n\x0e\x66ixed_position\x18\x03 \x01(\x08\x12\x13\n\x0bgps_enabled\x18\x04 \x01(\x08\x12\x1b\n\x13gps_update_interval\x18\x05 \x01(\r\x12\x18\n\x10gps_attempt_time\x18\x06 \x01(\r\x12\x16\n\x0eposition_flags\x18\x07 \x01(\r\"\xab\x01\n\rPositionFlags\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x41LTITUDE\x10\x01\x12\x10\n\x0c\x41LTITUDE_MSL\x10\x02\x12\x16\n\x12GEOIDAL_SEPARATION\x10\x04\x12\x07\n\x03\x44OP\x10\x08\x12\t\n\x05HVDOP\x10\x10\x12\r\n\tSATINVIEW\x10 \x12\n\n\x06SEQ_NO\x10@\x12\x0e\n\tTIMESTAMP\x10\x80\x01\x12\x0c\n\x07HEADING\x10\x80\x02\x12\n\n\x05SPEED\x10\x80\x04\x1a\xe5\x01\n\x0bPowerConfig\x12\x17\n\x0fis_power_saving\x18\x01 \x01(\x08\x12&\n\x1eon_battery_shutdown_after_secs\x18\x02 \x01(\r\x12\x1f\n\x17\x61\x64\x63_multiplier_override\x18\x03 \x01(\x02\x12\x1b\n\x13wait_bluetooth_secs\x18\x04 \x01(\r\x12\x1d\n\x15mesh_sds_timeout_secs\x18\x05 \x01(\r\x12\x10\n\x08sds_secs\x18\x06 \x01(\r\x12\x0f\n\x07ls_secs\x18\x07 \x01(\r\x12\x15\n\rmin_wake_secs\x18\x08 \x01(\r\x1a\x87\x03\n\rNetworkConfig\x12\x14\n\x0cwifi_enabled\x18\x01 \x01(\x08\x12\x11\n\twifi_ssid\x18\x03 \x01(\t\x12\x10\n\x08wifi_psk\x18\x04 \x01(\t\x12\x12\n\nntp_server\x18\x05 \x01(\t\x12\x13\n\x0b\x65th_enabled\x18\x06 \x01(\x08\x12/\n\x08\x65th_mode\x18\x07 \x01(\x0e\x32\x1d.Config.NetworkConfig.EthMode\x12\x35\n\x0bipv4_config\x18\x08 \x01(\x0b\x32 .Config.NetworkConfig.IpV4Config\x1a\x46\n\nIpV4Config\x12\n\n\x02ip\x18\x01 \x01(\x07\x12\x0f\n\x07gateway\x18\x02 \x01(\x07\x12\x0e\n\x06subnet\x18\x03 \x01(\x07\x12\x0b\n\x03\x64ns\x18\x04 \x01(\x07\"A\n\x08WiFiMode\x12\n\n\x06\x43LIENT\x10\x00\x12\x10\n\x0c\x41\x43\x43\x45SS_POINT\x10\x01\x12\x17\n\x13\x41\x43\x43\x45SS_POINT_HIDDEN\x10\x02\"\x1f\n\x07\x45thMode\x12\x08\n\x04\x44HCP\x10\x00\x12\n\n\x06STATIC\x10\x01\x1a\xe5\x02\n\rDisplayConfig\x12\x16\n\x0escreen_on_secs\x18\x01 \x01(\r\x12=\n\ngps_format\x18\x02 \x01(\x0e\x32).Config.DisplayConfig.GpsCoordinateFormat\x12!\n\x19\x61uto_screen_carousel_secs\x18\x03 \x01(\r\x12\x19\n\x11\x63ompass_north_top\x18\x04 \x01(\x08\x12\x13\n\x0b\x66lip_screen\x18\x05 \x01(\x08\x12\x31\n\x05units\x18\x06 \x01(\x0e\x32\".Config.DisplayConfig.DisplayUnits\"M\n\x13GpsCoordinateFormat\x12\x07\n\x03\x44\x45\x43\x10\x00\x12\x07\n\x03\x44MS\x10\x01\x12\x07\n\x03UTM\x10\x02\x12\x08\n\x04MGRS\x10\x03\x12\x07\n\x03OLC\x10\x04\x12\x08\n\x04OSGR\x10\x05\"(\n\x0c\x44isplayUnits\x12\n\n\x06METRIC\x10\x00\x12\x0c\n\x08IMPERIAL\x10\x01\x1a\xdd\x04\n\nLoRaConfig\x12\x12\n\nuse_preset\x18\x01 \x01(\x08\x12\x34\n\x0cmodem_preset\x18\x02 \x01(\x0e\x32\x1e.Config.LoRaConfig.ModemPreset\x12\x11\n\tbandwidth\x18\x03 \x01(\r\x12\x15\n\rspread_factor\x18\x04 \x01(\r\x12\x13\n\x0b\x63oding_rate\x18\x05 \x01(\r\x12\x18\n\x10\x66requency_offset\x18\x06 \x01(\x02\x12-\n\x06region\x18\x07 \x01(\x0e\x32\x1d.Config.LoRaConfig.RegionCode\x12\x11\n\thop_limit\x18\x08 \x01(\r\x12\x12\n\ntx_enabled\x18\t \x01(\x08\x12\x10\n\x08tx_power\x18\n \x01(\x05\x12\x13\n\x0b\x63hannel_num\x18\x0b \x01(\r\x12\x17\n\x0fignore_incoming\x18g \x03(\r\"\x91\x01\n\nRegionCode\x12\t\n\x05UNSET\x10\x00\x12\x06\n\x02US\x10\x01\x12\n\n\x06\x45U_433\x10\x02\x12\n\n\x06\x45U_868\x10\x03\x12\x06\n\x02\x43N\x10\x04\x12\x06\n\x02JP\x10\x05\x12\x07\n\x03\x41NZ\x10\x06\x12\x06\n\x02KR\x10\x07\x12\x06\n\x02TW\x10\x08\x12\x06\n\x02RU\x10\t\x12\x06\n\x02IN\x10\n\x12\n\n\x06NZ_865\x10\x0b\x12\x06\n\x02TH\x10\x0c\x12\x0b\n\x07LORA_24\x10\r\"\x81\x01\n\x0bModemPreset\x12\r\n\tLONG_FAST\x10\x00\x12\r\n\tLONG_SLOW\x10\x01\x12\x12\n\x0eVERY_LONG_SLOW\x10\x02\x12\x0f\n\x0bMEDIUM_SLOW\x10\x03\x12\x0f\n\x0bMEDIUM_FAST\x10\x04\x12\x0e\n\nSHORT_SLOW\x10\x05\x12\x0e\n\nSHORT_FAST\x10\x06\x1a\xa2\x01\n\x0f\x42luetoothConfig\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x31\n\x04mode\x18\x02 \x01(\x0e\x32#.Config.BluetoothConfig.PairingMode\x12\x11\n\tfixed_pin\x18\x03 \x01(\r\"8\n\x0bPairingMode\x12\x0e\n\nRANDOM_PIN\x10\x00\x12\r\n\tFIXED_PIN\x10\x01\x12\n\n\x06NO_PIN\x10\x02\x42\x11\n\x0fpayload_variantBI\n\x13\x63om.geeksville.meshB\x0c\x43onfigProtosH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onfig.proto\"\xbf\x15\n\x06\x43onfig\x12&\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x14.Config.DeviceConfigH\x00\x12*\n\x08position\x18\x02 \x01(\x0b\x32\x16.Config.PositionConfigH\x00\x12$\n\x05power\x18\x03 \x01(\x0b\x32\x13.Config.PowerConfigH\x00\x12(\n\x07network\x18\x04 \x01(\x0b\x32\x15.Config.NetworkConfigH\x00\x12(\n\x07\x64isplay\x18\x05 \x01(\x0b\x32\x15.Config.DisplayConfigH\x00\x12\"\n\x04lora\x18\x06 \x01(\x0b\x32\x12.Config.LoRaConfigH\x00\x12,\n\tbluetooth\x18\x07 \x01(\x0b\x32\x17.Config.BluetoothConfigH\x00\x1a\xae\x01\n\x0c\x44\x65viceConfig\x12\'\n\x04role\x18\x01 \x01(\x0e\x32\x19.Config.DeviceConfig.Role\x12\x16\n\x0eserial_enabled\x18\x02 \x01(\x08\x12\x19\n\x11\x64\x65\x62ug_log_enabled\x18\x03 \x01(\x08\"B\n\x04Role\x12\n\n\x06\x43LIENT\x10\x00\x12\x0f\n\x0b\x43LIENT_MUTE\x10\x01\x12\n\n\x06ROUTER\x10\x02\x12\x11\n\rROUTER_CLIENT\x10\x03\x1a\x85\x03\n\x0ePositionConfig\x12\x1f\n\x17position_broadcast_secs\x18\x01 \x01(\r\x12(\n position_broadcast_smart_enabled\x18\x02 \x01(\x08\x12\x16\n\x0e\x66ixed_position\x18\x03 \x01(\x08\x12\x13\n\x0bgps_enabled\x18\x04 \x01(\x08\x12\x1b\n\x13gps_update_interval\x18\x05 \x01(\r\x12\x18\n\x10gps_attempt_time\x18\x06 \x01(\r\x12\x16\n\x0eposition_flags\x18\x07 \x01(\r\"\xab\x01\n\rPositionFlags\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x41LTITUDE\x10\x01\x12\x10\n\x0c\x41LTITUDE_MSL\x10\x02\x12\x16\n\x12GEOIDAL_SEPARATION\x10\x04\x12\x07\n\x03\x44OP\x10\x08\x12\t\n\x05HVDOP\x10\x10\x12\r\n\tSATINVIEW\x10 \x12\n\n\x06SEQ_NO\x10@\x12\x0e\n\tTIMESTAMP\x10\x80\x01\x12\x0c\n\x07HEADING\x10\x80\x02\x12\n\n\x05SPEED\x10\x80\x04\x1a\xe5\x01\n\x0bPowerConfig\x12\x17\n\x0fis_power_saving\x18\x01 \x01(\x08\x12&\n\x1eon_battery_shutdown_after_secs\x18\x02 \x01(\r\x12\x1f\n\x17\x61\x64\x63_multiplier_override\x18\x03 \x01(\x02\x12\x1b\n\x13wait_bluetooth_secs\x18\x04 \x01(\r\x12\x1d\n\x15mesh_sds_timeout_secs\x18\x05 \x01(\r\x12\x10\n\x08sds_secs\x18\x06 \x01(\r\x12\x0f\n\x07ls_secs\x18\x07 \x01(\r\x12\x15\n\rmin_wake_secs\x18\x08 \x01(\r\x1a\x87\x03\n\rNetworkConfig\x12\x14\n\x0cwifi_enabled\x18\x01 \x01(\x08\x12\x11\n\twifi_ssid\x18\x03 \x01(\t\x12\x10\n\x08wifi_psk\x18\x04 \x01(\t\x12\x12\n\nntp_server\x18\x05 \x01(\t\x12\x13\n\x0b\x65th_enabled\x18\x06 \x01(\x08\x12/\n\x08\x65th_mode\x18\x07 \x01(\x0e\x32\x1d.Config.NetworkConfig.EthMode\x12\x35\n\x0bipv4_config\x18\x08 \x01(\x0b\x32 .Config.NetworkConfig.IpV4Config\x1a\x46\n\nIpV4Config\x12\n\n\x02ip\x18\x01 \x01(\x07\x12\x0f\n\x07gateway\x18\x02 \x01(\x07\x12\x0e\n\x06subnet\x18\x03 \x01(\x07\x12\x0b\n\x03\x64ns\x18\x04 \x01(\x07\"A\n\x08WiFiMode\x12\n\n\x06\x43LIENT\x10\x00\x12\x10\n\x0c\x41\x43\x43\x45SS_POINT\x10\x01\x12\x17\n\x13\x41\x43\x43\x45SS_POINT_HIDDEN\x10\x02\"\x1f\n\x07\x45thMode\x12\x08\n\x04\x44HCP\x10\x00\x12\n\n\x06STATIC\x10\x01\x1a\xd1\x03\n\rDisplayConfig\x12\x16\n\x0escreen_on_secs\x18\x01 \x01(\r\x12=\n\ngps_format\x18\x02 \x01(\x0e\x32).Config.DisplayConfig.GpsCoordinateFormat\x12!\n\x19\x61uto_screen_carousel_secs\x18\x03 \x01(\r\x12\x19\n\x11\x63ompass_north_top\x18\x04 \x01(\x08\x12\x13\n\x0b\x66lip_screen\x18\x05 \x01(\x08\x12\x31\n\x05units\x18\x06 \x01(\x0e\x32\".Config.DisplayConfig.DisplayUnits\x12,\n\x04oled\x18\x07 \x01(\x0e\x32\x1e.Config.DisplayConfig.OledType\"M\n\x13GpsCoordinateFormat\x12\x07\n\x03\x44\x45\x43\x10\x00\x12\x07\n\x03\x44MS\x10\x01\x12\x07\n\x03UTM\x10\x02\x12\x08\n\x04MGRS\x10\x03\x12\x07\n\x03OLC\x10\x04\x12\x08\n\x04OSGR\x10\x05\"(\n\x0c\x44isplayUnits\x12\n\n\x06METRIC\x10\x00\x12\x0c\n\x08IMPERIAL\x10\x01\"<\n\x08OledType\x12\r\n\tOLED_AUTO\x10\x00\x12\x10\n\x0cOLED_SSD1306\x10\x01\x12\x0f\n\x0bOLED_SH1106\x10\x02\x1a\xdd\x04\n\nLoRaConfig\x12\x12\n\nuse_preset\x18\x01 \x01(\x08\x12\x34\n\x0cmodem_preset\x18\x02 \x01(\x0e\x32\x1e.Config.LoRaConfig.ModemPreset\x12\x11\n\tbandwidth\x18\x03 \x01(\r\x12\x15\n\rspread_factor\x18\x04 \x01(\r\x12\x13\n\x0b\x63oding_rate\x18\x05 \x01(\r\x12\x18\n\x10\x66requency_offset\x18\x06 \x01(\x02\x12-\n\x06region\x18\x07 \x01(\x0e\x32\x1d.Config.LoRaConfig.RegionCode\x12\x11\n\thop_limit\x18\x08 \x01(\r\x12\x12\n\ntx_enabled\x18\t \x01(\x08\x12\x10\n\x08tx_power\x18\n \x01(\x05\x12\x13\n\x0b\x63hannel_num\x18\x0b \x01(\r\x12\x17\n\x0fignore_incoming\x18g \x03(\r\"\x91\x01\n\nRegionCode\x12\t\n\x05UNSET\x10\x00\x12\x06\n\x02US\x10\x01\x12\n\n\x06\x45U_433\x10\x02\x12\n\n\x06\x45U_868\x10\x03\x12\x06\n\x02\x43N\x10\x04\x12\x06\n\x02JP\x10\x05\x12\x07\n\x03\x41NZ\x10\x06\x12\x06\n\x02KR\x10\x07\x12\x06\n\x02TW\x10\x08\x12\x06\n\x02RU\x10\t\x12\x06\n\x02IN\x10\n\x12\n\n\x06NZ_865\x10\x0b\x12\x06\n\x02TH\x10\x0c\x12\x0b\n\x07LORA_24\x10\r\"\x81\x01\n\x0bModemPreset\x12\r\n\tLONG_FAST\x10\x00\x12\r\n\tLONG_SLOW\x10\x01\x12\x12\n\x0eVERY_LONG_SLOW\x10\x02\x12\x0f\n\x0bMEDIUM_SLOW\x10\x03\x12\x0f\n\x0bMEDIUM_FAST\x10\x04\x12\x0e\n\nSHORT_SLOW\x10\x05\x12\x0e\n\nSHORT_FAST\x10\x06\x1a\xa2\x01\n\x0f\x42luetoothConfig\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x31\n\x04mode\x18\x02 \x01(\x0e\x32#.Config.BluetoothConfig.PairingMode\x12\x11\n\tfixed_pin\x18\x03 \x01(\r\"8\n\x0bPairingMode\x12\x0e\n\nRANDOM_PIN\x10\x00\x12\r\n\tFIXED_PIN\x10\x01\x12\n\n\x06NO_PIN\x10\x02\x42\x11\n\x0fpayload_variantBI\n\x13\x63om.geeksville.meshB\x0c\x43onfigProtosH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') @@ -33,6 +33,7 @@ _CONFIG_NETWORKCONFIG_ETHMODE = _CONFIG_NETWORKCONFIG.enum_types_by_name['EthMode'] _CONFIG_DISPLAYCONFIG_GPSCOORDINATEFORMAT = _CONFIG_DISPLAYCONFIG.enum_types_by_name['GpsCoordinateFormat'] _CONFIG_DISPLAYCONFIG_DISPLAYUNITS = _CONFIG_DISPLAYCONFIG.enum_types_by_name['DisplayUnits'] +_CONFIG_DISPLAYCONFIG_OLEDTYPE = _CONFIG_DISPLAYCONFIG.enum_types_by_name['OledType'] _CONFIG_LORACONFIG_REGIONCODE = _CONFIG_LORACONFIG.enum_types_by_name['RegionCode'] _CONFIG_LORACONFIG_MODEMPRESET = _CONFIG_LORACONFIG.enum_types_by_name['ModemPreset'] _CONFIG_BLUETOOTHCONFIG_PAIRINGMODE = _CONFIG_BLUETOOTHCONFIG.enum_types_by_name['PairingMode'] @@ -112,7 +113,7 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\014ConfigProtosH\003Z\"github.com/meshtastic/go/generated' _CONFIG._serialized_start=17 - _CONFIG._serialized_end=2660 + _CONFIG._serialized_end=2768 _CONFIG_DEVICECONFIG._serialized_start=316 _CONFIG_DEVICECONFIG._serialized_end=490 _CONFIG_DEVICECONFIG_ROLE._serialized_start=424 @@ -132,19 +133,21 @@ _CONFIG_NETWORKCONFIG_ETHMODE._serialized_start=1477 _CONFIG_NETWORKCONFIG_ETHMODE._serialized_end=1508 _CONFIG_DISPLAYCONFIG._serialized_start=1511 - _CONFIG_DISPLAYCONFIG._serialized_end=1868 - _CONFIG_DISPLAYCONFIG_GPSCOORDINATEFORMAT._serialized_start=1749 - _CONFIG_DISPLAYCONFIG_GPSCOORDINATEFORMAT._serialized_end=1826 - _CONFIG_DISPLAYCONFIG_DISPLAYUNITS._serialized_start=1828 - _CONFIG_DISPLAYCONFIG_DISPLAYUNITS._serialized_end=1868 - _CONFIG_LORACONFIG._serialized_start=1871 - _CONFIG_LORACONFIG._serialized_end=2476 - _CONFIG_LORACONFIG_REGIONCODE._serialized_start=2199 - _CONFIG_LORACONFIG_REGIONCODE._serialized_end=2344 - _CONFIG_LORACONFIG_MODEMPRESET._serialized_start=2347 - _CONFIG_LORACONFIG_MODEMPRESET._serialized_end=2476 - _CONFIG_BLUETOOTHCONFIG._serialized_start=2479 - _CONFIG_BLUETOOTHCONFIG._serialized_end=2641 - _CONFIG_BLUETOOTHCONFIG_PAIRINGMODE._serialized_start=2585 - _CONFIG_BLUETOOTHCONFIG_PAIRINGMODE._serialized_end=2641 + _CONFIG_DISPLAYCONFIG._serialized_end=1976 + _CONFIG_DISPLAYCONFIG_GPSCOORDINATEFORMAT._serialized_start=1795 + _CONFIG_DISPLAYCONFIG_GPSCOORDINATEFORMAT._serialized_end=1872 + _CONFIG_DISPLAYCONFIG_DISPLAYUNITS._serialized_start=1874 + _CONFIG_DISPLAYCONFIG_DISPLAYUNITS._serialized_end=1914 + _CONFIG_DISPLAYCONFIG_OLEDTYPE._serialized_start=1916 + _CONFIG_DISPLAYCONFIG_OLEDTYPE._serialized_end=1976 + _CONFIG_LORACONFIG._serialized_start=1979 + _CONFIG_LORACONFIG._serialized_end=2584 + _CONFIG_LORACONFIG_REGIONCODE._serialized_start=2307 + _CONFIG_LORACONFIG_REGIONCODE._serialized_end=2452 + _CONFIG_LORACONFIG_MODEMPRESET._serialized_start=2455 + _CONFIG_LORACONFIG_MODEMPRESET._serialized_end=2584 + _CONFIG_BLUETOOTHCONFIG._serialized_start=2587 + _CONFIG_BLUETOOTHCONFIG._serialized_end=2749 + _CONFIG_BLUETOOTHCONFIG_PAIRINGMODE._serialized_start=2693 + _CONFIG_BLUETOOTHCONFIG_PAIRINGMODE._serialized_end=2749 # @@protoc_insertion_point(module_scope) diff --git a/meshtastic/device_metadata_pb2.py b/meshtastic/device_metadata_pb2.py index afea49a3e..47865bdce 100644 --- a/meshtastic/device_metadata_pb2.py +++ b/meshtastic/device_metadata_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x64\x65vice_metadata.proto\"H\n\x0e\x44\x65viceMetadata\x12\x18\n\x10\x66irmware_version\x18\x01 \x01(\t\x12\x1c\n\x14\x64\x65vice_state_version\x18\x02 \x01(\rBQ\n\x13\x63om.geeksville.meshB\x14\x44\x65viceMetadataProtosH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x64\x65vice_metadata.proto\"\x99\x01\n\x0e\x44\x65viceMetadata\x12\x18\n\x10\x66irmware_version\x18\x01 \x01(\t\x12\x1c\n\x14\x64\x65vice_state_version\x18\x02 \x01(\r\x12\x13\n\x0b\x63\x61nShutdown\x18\x03 \x01(\x08\x12\x0f\n\x07hasWifi\x18\x04 \x01(\x08\x12\x14\n\x0chasBluetooth\x18\x05 \x01(\x08\x12\x13\n\x0bhasEthernet\x18\x06 \x01(\x08\x42Q\n\x13\x63om.geeksville.meshB\x14\x44\x65viceMetadataProtosH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') @@ -30,6 +30,6 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\024DeviceMetadataProtosH\003Z\"github.com/meshtastic/go/generated' - _DEVICEMETADATA._serialized_start=25 - _DEVICEMETADATA._serialized_end=97 + _DEVICEMETADATA._serialized_start=26 + _DEVICEMETADATA._serialized_end=179 # @@protoc_insertion_point(module_scope) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 96d170b32..37e8b7160 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -722,3 +722,27 @@ def _handlePacketFromRadio(self, meshPacket, hack=False): logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ") publishingThread.queueWork(lambda: pub.sendMessage( topic, packet=asDict, interface=self)) + +# TODO: take the clear text message and signing key as parameters here and do the signing in this function + def sendSignedText(self, signedMessage: bytes, + destinationId=BROADCAST_ADDR, + wantAck=False, + wantResponse=False, + hopLimit=None, + onResponse=None, + channelIndex=0): + """ + Send a signed text message to another node using the black lager module. + This function signs the text message with the users private key and sets + the payload of the packet to be the signed text message data. + """ + if hopLimit is None: + hopLimit = self.defaultHopLimit + + return self.sendData(signedMessage, destinationId, + portNum=portnums_pb2.PortNum.BLACK_LAGER, + wantAck=wantAck, + wantResponse=wantResponse, + hopLimit=hopLimit, + onResponse=onResponse, + channelIndex=channelIndex) diff --git a/meshtastic/mesh_pb2.py b/meshtastic/mesh_pb2.py index bb04df0a9..9c5a65d65 100644 --- a/meshtastic/mesh_pb2.py +++ b/meshtastic/mesh_pb2.py @@ -20,7 +20,7 @@ from . import telemetry_pb2 as telemetry__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nmesh.proto\x1a\rchannel.proto\x1a\x0c\x63onfig.proto\x1a\x13module_config.proto\x1a\x0eportnums.proto\x1a\x0ftelemetry.proto\"\xb7\x05\n\x08Position\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12,\n\x0flocation_source\x18\x05 \x01(\x0e\x32\x13.Position.LocSource\x12,\n\x0f\x61ltitude_source\x18\x06 \x01(\x0e\x32\x13.Position.AltSource\x12\x11\n\ttimestamp\x18\x07 \x01(\x07\x12\x1f\n\x17timestamp_millis_adjust\x18\x08 \x01(\x05\x12\x14\n\x0c\x61ltitude_hae\x18\t \x01(\x11\x12#\n\x1b\x61ltitude_geoidal_separation\x18\n \x01(\x11\x12\x0c\n\x04PDOP\x18\x0b \x01(\r\x12\x0c\n\x04HDOP\x18\x0c \x01(\r\x12\x0c\n\x04VDOP\x18\r \x01(\r\x12\x14\n\x0cgps_accuracy\x18\x0e \x01(\r\x12\x14\n\x0cground_speed\x18\x0f \x01(\r\x12\x14\n\x0cground_track\x18\x10 \x01(\r\x12\x13\n\x0b\x66ix_quality\x18\x11 \x01(\r\x12\x10\n\x08\x66ix_type\x18\x12 \x01(\r\x12\x14\n\x0csats_in_view\x18\x13 \x01(\r\x12\x11\n\tsensor_id\x18\x14 \x01(\r\x12\x13\n\x0bnext_update\x18\x15 \x01(\r\x12\x12\n\nseq_number\x18\x16 \x01(\r\"N\n\tLocSource\x12\r\n\tLOC_UNSET\x10\x00\x12\x0e\n\nLOC_MANUAL\x10\x01\x12\x10\n\x0cLOC_INTERNAL\x10\x02\x12\x10\n\x0cLOC_EXTERNAL\x10\x03\"b\n\tAltSource\x12\r\n\tALT_UNSET\x10\x00\x12\x0e\n\nALT_MANUAL\x10\x01\x12\x10\n\x0c\x41LT_INTERNAL\x10\x02\x12\x10\n\x0c\x41LT_EXTERNAL\x10\x03\x12\x12\n\x0e\x41LT_BAROMETRIC\x10\x04\"\x81\x01\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x0f\n\x07macaddr\x18\x04 \x01(\x0c\x12 \n\x08hw_model\x18\x05 \x01(\x0e\x32\x0e.HardwareModel\x12\x13\n\x0bis_licensed\x18\x06 \x01(\x08\"\x1f\n\x0eRouteDiscovery\x12\r\n\x05route\x18\x01 \x03(\x07\"\xc5\x02\n\x07Routing\x12(\n\rroute_request\x18\x01 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0broute_reply\x18\x02 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\x0e.Routing.ErrorH\x00\"\xb4\x01\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x12\x0f\n\x0bNO_RESPONSE\x10\x08\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10 \x12\x12\n\x0eNOT_AUTHORIZED\x10!B\t\n\x07variant\"\x9c\x01\n\x04\x44\x61ta\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\x12\x10\n\x08reply_id\x18\x07 \x01(\x07\x12\r\n\x05\x65moji\x18\x08 \x01(\x07\"\x82\x01\n\x08Waypoint\x12\n\n\x02id\x18\x01 \x01(\r\x12\x12\n\nlatitude_i\x18\x02 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x03 \x01(\x0f\x12\x0e\n\x06\x65xpire\x18\x04 \x01(\r\x12\x0e\n\x06locked\x18\x05 \x01(\x08\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\"\xcb\x03\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\r\x12\x18\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x05.DataH\x00\x12\x13\n\tencrypted\x18\x05 \x01(\x0cH\x00\x12\n\n\x02id\x18\x06 \x01(\x07\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x11\n\thop_limit\x18\t \x01(\r\x12\x10\n\x08want_ack\x18\n \x01(\x08\x12&\n\x08priority\x18\x0b \x01(\x0e\x32\x14.MeshPacket.Priority\x12\x0f\n\x07rx_rssi\x18\x0c \x01(\x05\x12$\n\x07\x64\x65layed\x18\r \x01(\x0e\x32\x13.MeshPacket.Delayed\"[\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\"B\n\x07\x44\x65layed\x12\x0c\n\x08NO_DELAY\x10\x00\x12\x15\n\x11\x44\x45LAYED_BROADCAST\x10\x01\x12\x12\n\x0e\x44\x45LAYED_DIRECT\x10\x02\x42\x11\n\x0fpayload_variant\"\x92\x01\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x13\n\x04user\x18\x02 \x01(\x0b\x32\x05.User\x12\x1b\n\x08position\x18\x03 \x01(\x0b\x32\t.Position\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12&\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\x0e.DeviceMetrics\"\x86\x03\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x0f\n\x07has_gps\x18\x02 \x01(\x08\x12\x14\n\x0cmax_channels\x18\x03 \x01(\r\x12\x18\n\x10\x66irmware_version\x18\x04 \x01(\t\x12&\n\nerror_code\x18\x05 \x01(\x0e\x32\x12.CriticalErrorCode\x12\x15\n\rerror_address\x18\x06 \x01(\r\x12\x13\n\x0b\x65rror_count\x18\x07 \x01(\r\x12\x14\n\x0creboot_count\x18\x08 \x01(\r\x12\x0f\n\x07\x62itrate\x18\t \x01(\x02\x12\x1c\n\x14message_timeout_msec\x18\n \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0b \x01(\r\x12\x15\n\rair_period_tx\x18\x0c \x03(\r\x12\x15\n\rair_period_rx\x18\r \x03(\r\x12\x10\n\x08has_wifi\x18\x0e \x01(\x08\x12\x1b\n\x13\x63hannel_utilization\x18\x0f \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x10 \x01(\x02\"\xb5\x01\n\tLogRecord\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x1f\n\x05level\x18\x04 \x01(\x0e\x32\x10.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"\xbc\x02\n\tFromRadio\x12\n\n\x02id\x18\x01 \x01(\r\x12\x1d\n\x06packet\x18\x02 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x1e\n\x07my_info\x18\x03 \x01(\x0b\x32\x0b.MyNodeInfoH\x00\x12\x1e\n\tnode_info\x18\x04 \x01(\x0b\x32\t.NodeInfoH\x00\x12\x19\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x07.ConfigH\x00\x12 \n\nlog_record\x18\x06 \x01(\x0b\x32\n.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x07 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\x08 \x01(\x08H\x00\x12%\n\x0cmoduleConfig\x18\t \x01(\x0b\x32\r.ModuleConfigH\x00\x12\x1b\n\x07\x63hannel\x18\n \x01(\x0b\x32\x08.ChannelH\x00\x42\x11\n\x0fpayload_variant\"k\n\x07ToRadio\x12\x1d\n\x06packet\x18\x01 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x03 \x01(\rH\x00\x12\x14\n\ndisconnect\x18\x04 \x01(\x08H\x00\x42\x11\n\x0fpayload_variant\"5\n\nCompressed\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c*\xc3\x03\n\rHardwareModel\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08TLORA_V2\x10\x01\x12\x0c\n\x08TLORA_V1\x10\x02\x12\x12\n\x0eTLORA_V2_1_1P6\x10\x03\x12\t\n\x05TBEAM\x10\x04\x12\x0f\n\x0bHELTEC_V2_0\x10\x05\x12\x0e\n\nTBEAM_V0P7\x10\x06\x12\n\n\x06T_ECHO\x10\x07\x12\x10\n\x0cTLORA_V1_1P3\x10\x08\x12\x0b\n\x07RAK4631\x10\t\x12\x0f\n\x0bHELTEC_V2_1\x10\n\x12\r\n\tHELTEC_V1\x10\x0b\x12\x18\n\x14LILYGO_TBEAM_S3_CORE\x10\x0c\x12\x0c\n\x08RAK11200\x10\r\x12\x0b\n\x07NANO_G1\x10\x0e\x12\x0e\n\nSTATION_G1\x10\x19\x12\x11\n\rLORA_RELAY_V1\x10 \x12\x0e\n\nNRF52840DK\x10!\x12\x07\n\x03PPR\x10\"\x12\x0f\n\x0bGENIEBLOCKS\x10#\x12\x11\n\rNRF52_UNKNOWN\x10$\x12\r\n\tPORTDUINO\x10%\x12\x0f\n\x0b\x41NDROID_SIM\x10&\x12\n\n\x06\x44IY_V1\x10\'\x12\x15\n\x11NRF52840_PCA10059\x10(\x12\n\n\x06\x44R_DEV\x10)\x12\x0b\n\x07M5STACK\x10*\x12\x0f\n\nPRIVATE_HW\x10\xff\x01*,\n\tConstants\x12\x08\n\x04ZERO\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xed\x01*\xee\x01\n\x11\x43riticalErrorCode\x12\x08\n\x04NONE\x10\x00\x12\x0f\n\x0bTX_WATCHDOG\x10\x01\x12\x14\n\x10SLEEP_ENTER_WAIT\x10\x02\x12\x0c\n\x08NO_RADIO\x10\x03\x12\x0f\n\x0bUNSPECIFIED\x10\x04\x12\x15\n\x11UBLOX_UNIT_FAILED\x10\x05\x12\r\n\tNO_AXP192\x10\x06\x12\x19\n\x15INVALID_RADIO_SETTING\x10\x07\x12\x13\n\x0fTRANSMIT_FAILED\x10\x08\x12\x0c\n\x08\x42ROWNOUT\x10\t\x12\x12\n\x0eSX1262_FAILURE\x10\n\x12\x11\n\rRADIO_SPI_BUG\x10\x0b\x42G\n\x13\x63om.geeksville.meshB\nMeshProtosH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nmesh.proto\x1a\rchannel.proto\x1a\x0c\x63onfig.proto\x1a\x13module_config.proto\x1a\x0eportnums.proto\x1a\x0ftelemetry.proto\"\xb7\x05\n\x08Position\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12,\n\x0flocation_source\x18\x05 \x01(\x0e\x32\x13.Position.LocSource\x12,\n\x0f\x61ltitude_source\x18\x06 \x01(\x0e\x32\x13.Position.AltSource\x12\x11\n\ttimestamp\x18\x07 \x01(\x07\x12\x1f\n\x17timestamp_millis_adjust\x18\x08 \x01(\x05\x12\x14\n\x0c\x61ltitude_hae\x18\t \x01(\x11\x12#\n\x1b\x61ltitude_geoidal_separation\x18\n \x01(\x11\x12\x0c\n\x04PDOP\x18\x0b \x01(\r\x12\x0c\n\x04HDOP\x18\x0c \x01(\r\x12\x0c\n\x04VDOP\x18\r \x01(\r\x12\x14\n\x0cgps_accuracy\x18\x0e \x01(\r\x12\x14\n\x0cground_speed\x18\x0f \x01(\r\x12\x14\n\x0cground_track\x18\x10 \x01(\r\x12\x13\n\x0b\x66ix_quality\x18\x11 \x01(\r\x12\x10\n\x08\x66ix_type\x18\x12 \x01(\r\x12\x14\n\x0csats_in_view\x18\x13 \x01(\r\x12\x11\n\tsensor_id\x18\x14 \x01(\r\x12\x13\n\x0bnext_update\x18\x15 \x01(\r\x12\x12\n\nseq_number\x18\x16 \x01(\r\"N\n\tLocSource\x12\r\n\tLOC_UNSET\x10\x00\x12\x0e\n\nLOC_MANUAL\x10\x01\x12\x10\n\x0cLOC_INTERNAL\x10\x02\x12\x10\n\x0cLOC_EXTERNAL\x10\x03\"b\n\tAltSource\x12\r\n\tALT_UNSET\x10\x00\x12\x0e\n\nALT_MANUAL\x10\x01\x12\x10\n\x0c\x41LT_INTERNAL\x10\x02\x12\x10\n\x0c\x41LT_EXTERNAL\x10\x03\x12\x12\n\x0e\x41LT_BAROMETRIC\x10\x04\"\x81\x01\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x0f\n\x07macaddr\x18\x04 \x01(\x0c\x12 \n\x08hw_model\x18\x05 \x01(\x0e\x32\x0e.HardwareModel\x12\x13\n\x0bis_licensed\x18\x06 \x01(\x08\"\x1f\n\x0eRouteDiscovery\x12\r\n\x05route\x18\x01 \x03(\x07\"\xc5\x02\n\x07Routing\x12(\n\rroute_request\x18\x01 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0broute_reply\x18\x02 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\x0e.Routing.ErrorH\x00\"\xb4\x01\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x12\x0f\n\x0bNO_RESPONSE\x10\x08\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10 \x12\x12\n\x0eNOT_AUTHORIZED\x10!B\t\n\x07variant\"\x9c\x01\n\x04\x44\x61ta\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\x12\x10\n\x08reply_id\x18\x07 \x01(\x07\x12\r\n\x05\x65moji\x18\x08 \x01(\x07\"\x82\x01\n\x08Waypoint\x12\n\n\x02id\x18\x01 \x01(\r\x12\x12\n\nlatitude_i\x18\x02 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x03 \x01(\x0f\x12\x0e\n\x06\x65xpire\x18\x04 \x01(\r\x12\x0e\n\x06locked\x18\x05 \x01(\x08\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\"\xcb\x03\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\r\x12\x18\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x05.DataH\x00\x12\x13\n\tencrypted\x18\x05 \x01(\x0cH\x00\x12\n\n\x02id\x18\x06 \x01(\x07\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x11\n\thop_limit\x18\t \x01(\r\x12\x10\n\x08want_ack\x18\n \x01(\x08\x12&\n\x08priority\x18\x0b \x01(\x0e\x32\x14.MeshPacket.Priority\x12\x0f\n\x07rx_rssi\x18\x0c \x01(\x05\x12$\n\x07\x64\x65layed\x18\r \x01(\x0e\x32\x13.MeshPacket.Delayed\"[\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\"B\n\x07\x44\x65layed\x12\x0c\n\x08NO_DELAY\x10\x00\x12\x15\n\x11\x44\x45LAYED_BROADCAST\x10\x01\x12\x12\n\x0e\x44\x45LAYED_DIRECT\x10\x02\x42\x11\n\x0fpayload_variant\"\x92\x01\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x13\n\x04user\x18\x02 \x01(\x0b\x32\x05.User\x12\x1b\n\x08position\x18\x03 \x01(\x0b\x32\t.Position\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12&\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\x0e.DeviceMetrics\"\x86\x03\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x0f\n\x07has_gps\x18\x02 \x01(\x08\x12\x14\n\x0cmax_channels\x18\x03 \x01(\r\x12\x18\n\x10\x66irmware_version\x18\x04 \x01(\t\x12&\n\nerror_code\x18\x05 \x01(\x0e\x32\x12.CriticalErrorCode\x12\x15\n\rerror_address\x18\x06 \x01(\r\x12\x13\n\x0b\x65rror_count\x18\x07 \x01(\r\x12\x14\n\x0creboot_count\x18\x08 \x01(\r\x12\x0f\n\x07\x62itrate\x18\t \x01(\x02\x12\x1c\n\x14message_timeout_msec\x18\n \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0b \x01(\r\x12\x15\n\rair_period_tx\x18\x0c \x03(\r\x12\x15\n\rair_period_rx\x18\r \x03(\r\x12\x10\n\x08has_wifi\x18\x0e \x01(\x08\x12\x1b\n\x13\x63hannel_utilization\x18\x0f \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x10 \x01(\x02\"\xb5\x01\n\tLogRecord\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x1f\n\x05level\x18\x04 \x01(\x0e\x32\x10.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"\xbc\x02\n\tFromRadio\x12\n\n\x02id\x18\x01 \x01(\r\x12\x1d\n\x06packet\x18\x02 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x1e\n\x07my_info\x18\x03 \x01(\x0b\x32\x0b.MyNodeInfoH\x00\x12\x1e\n\tnode_info\x18\x04 \x01(\x0b\x32\t.NodeInfoH\x00\x12\x19\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x07.ConfigH\x00\x12 \n\nlog_record\x18\x06 \x01(\x0b\x32\n.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x07 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\x08 \x01(\x08H\x00\x12%\n\x0cmoduleConfig\x18\t \x01(\x0b\x32\r.ModuleConfigH\x00\x12\x1b\n\x07\x63hannel\x18\n \x01(\x0b\x32\x08.ChannelH\x00\x42\x11\n\x0fpayload_variant\"k\n\x07ToRadio\x12\x1d\n\x06packet\x18\x01 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x03 \x01(\rH\x00\x12\x14\n\ndisconnect\x18\x04 \x01(\x08H\x00\x42\x11\n\x0fpayload_variant\"5\n\nCompressed\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c*\xd7\x03\n\rHardwareModel\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08TLORA_V2\x10\x01\x12\x0c\n\x08TLORA_V1\x10\x02\x12\x12\n\x0eTLORA_V2_1_1P6\x10\x03\x12\t\n\x05TBEAM\x10\x04\x12\x0f\n\x0bHELTEC_V2_0\x10\x05\x12\x0e\n\nTBEAM_V0P7\x10\x06\x12\n\n\x06T_ECHO\x10\x07\x12\x10\n\x0cTLORA_V1_1P3\x10\x08\x12\x0b\n\x07RAK4631\x10\t\x12\x0f\n\x0bHELTEC_V2_1\x10\n\x12\r\n\tHELTEC_V1\x10\x0b\x12\x18\n\x14LILYGO_TBEAM_S3_CORE\x10\x0c\x12\x0c\n\x08RAK11200\x10\r\x12\x0b\n\x07NANO_G1\x10\x0e\x12\x12\n\x0eTLORA_V2_1_1P8\x10\x0f\x12\x0e\n\nSTATION_G1\x10\x19\x12\x11\n\rLORA_RELAY_V1\x10 \x12\x0e\n\nNRF52840DK\x10!\x12\x07\n\x03PPR\x10\"\x12\x0f\n\x0bGENIEBLOCKS\x10#\x12\x11\n\rNRF52_UNKNOWN\x10$\x12\r\n\tPORTDUINO\x10%\x12\x0f\n\x0b\x41NDROID_SIM\x10&\x12\n\n\x06\x44IY_V1\x10\'\x12\x15\n\x11NRF52840_PCA10059\x10(\x12\n\n\x06\x44R_DEV\x10)\x12\x0b\n\x07M5STACK\x10*\x12\x0f\n\nPRIVATE_HW\x10\xff\x01*,\n\tConstants\x12\x08\n\x04ZERO\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xed\x01*\xee\x01\n\x11\x43riticalErrorCode\x12\x08\n\x04NONE\x10\x00\x12\x0f\n\x0bTX_WATCHDOG\x10\x01\x12\x14\n\x10SLEEP_ENTER_WAIT\x10\x02\x12\x0c\n\x08NO_RADIO\x10\x03\x12\x0f\n\x0bUNSPECIFIED\x10\x04\x12\x15\n\x11UBLOX_UNIT_FAILED\x10\x05\x12\r\n\tNO_AXP192\x10\x06\x12\x19\n\x15INVALID_RADIO_SETTING\x10\x07\x12\x13\n\x0fTRANSMIT_FAILED\x10\x08\x12\x0c\n\x08\x42ROWNOUT\x10\t\x12\x12\n\x0eSX1262_FAILURE\x10\n\x12\x11\n\rRADIO_SPI_BUG\x10\x0b\x42G\n\x13\x63om.geeksville.meshB\nMeshProtosH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') _HARDWAREMODEL = DESCRIPTOR.enum_types_by_name['HardwareModel'] HardwareModel = enum_type_wrapper.EnumTypeWrapper(_HARDWAREMODEL) @@ -43,6 +43,7 @@ LILYGO_TBEAM_S3_CORE = 12 RAK11200 = 13 NANO_G1 = 14 +TLORA_V2_1_1P8 = 15 STATION_G1 = 25 LORA_RELAY_V1 = 32 NRF52840DK = 33 @@ -187,11 +188,11 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nMeshProtosH\003Z\"github.com/meshtastic/go/generated' _HARDWAREMODEL._serialized_start=3252 - _HARDWAREMODEL._serialized_end=3703 - _CONSTANTS._serialized_start=3705 - _CONSTANTS._serialized_end=3749 - _CRITICALERRORCODE._serialized_start=3752 - _CRITICALERRORCODE._serialized_end=3990 + _HARDWAREMODEL._serialized_end=3723 + _CONSTANTS._serialized_start=3725 + _CONSTANTS._serialized_end=3769 + _CRITICALERRORCODE._serialized_start=3772 + _CRITICALERRORCODE._serialized_end=4010 _POSITION._serialized_start=98 _POSITION._serialized_end=793 _POSITION_LOCSOURCE._serialized_start=615 diff --git a/meshtastic/persona_pb2.py b/meshtastic/persona_pb2.py new file mode 100644 index 000000000..2b705fb5f --- /dev/null +++ b/meshtastic/persona_pb2.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: persona.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rpersona.proto\"\x97\x01\n\x07Persona\x12\x12\n\nlocal_name\x18\x01 \x01(\t\x12\x13\n\x0bmac_address\x18\x02 \x01(\t\x12\x10\n\x08node_num\x18\x03 \x01(\x05\x12\x12\n\npublic_key\x18\x04 \x01(\x0c\x12\x13\n\x0bprivate_key\x18\x05 \x01(\x0c\x12\x0b\n\x03uid\x18\x06 \x01(\x0c\x12\x0c\n\x04mask\x18\x07 \x01(\x0c\x12\r\n\x05owned\x18\x08 \x01(\x08\"H\n\x06Wallet\x12\x1d\n\x0bmy_personas\x18\x01 \x03(\x0b\x32\x08.Persona\x12\x1f\n\rpeer_personas\x18\x02 \x03(\x0b\x32\x08.Personab\x06proto3') + + + +_PERSONA = DESCRIPTOR.message_types_by_name['Persona'] +_WALLET = DESCRIPTOR.message_types_by_name['Wallet'] +Persona = _reflection.GeneratedProtocolMessageType('Persona', (_message.Message,), { + 'DESCRIPTOR' : _PERSONA, + '__module__' : 'persona_pb2' + # @@protoc_insertion_point(class_scope:Persona) + }) +_sym_db.RegisterMessage(Persona) + +Wallet = _reflection.GeneratedProtocolMessageType('Wallet', (_message.Message,), { + 'DESCRIPTOR' : _WALLET, + '__module__' : 'persona_pb2' + # @@protoc_insertion_point(class_scope:Wallet) + }) +_sym_db.RegisterMessage(Wallet) + +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _PERSONA._serialized_start=18 + _PERSONA._serialized_end=169 + _WALLET._serialized_start=171 + _WALLET._serialized_end=243 +# @@protoc_insertion_point(module_scope) diff --git a/meshtastic/portnums_pb2.py b/meshtastic/portnums_pb2.py index 24cccc13b..4c5c64caa 100644 --- a/meshtastic/portnums_pb2.py +++ b/meshtastic/portnums_pb2.py @@ -15,7 +15,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eportnums.proto*\x81\x03\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42\x45\n\x13\x63om.geeksville.meshB\x08PortnumsH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eportnums.proto*\x93\x03\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x10\n\x0b\x42LACK_LAGER\x10\xcd\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42\x45\n\x13\x63om.geeksville.meshB\x08PortnumsH\x03Z\"github.com/meshtastic/go/generatedb\x06proto3') _PORTNUM = DESCRIPTOR.enum_types_by_name['PortNum'] PortNum = enum_type_wrapper.EnumTypeWrapper(_PORTNUM) @@ -37,6 +37,7 @@ ZPS_APP = 68 SIMULATOR_APP = 69 PRIVATE_APP = 256 +BLACK_LAGER = 333 ATAK_FORWARDER = 257 MAX = 511 @@ -46,5 +47,5 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\010PortnumsH\003Z\"github.com/meshtastic/go/generated' _PORTNUM._serialized_start=19 - _PORTNUM._serialized_end=404 + _PORTNUM._serialized_end=422 # @@protoc_insertion_point(module_scope) diff --git a/persona_wallet.py b/persona_wallet.py new file mode 100644 index 000000000..c412eed60 --- /dev/null +++ b/persona_wallet.py @@ -0,0 +1,95 @@ +from meshtastic import persona_pb2 +from nacl.signing import SigningKey +import BlackLagerMessage_pb2 +import pickle +import sys, os + + +class PersonaWallet: + """A wrapper around the protobuf Wallet message. Read and write wallet from disk. Create new personas.""" + + def __init__(self): + """Initialize a Wallet object. Check if a command line argument is provided for the wallet file. + If there is one provided, then set it as the wallet path. Else set the default wallet path + """ + self.wallet_message = BlackLagerMessage_pb2.Wallet() + + if len(sys.argv) == 1: + self.wallet_path = self.get_config_path() + elif len(sys.argv) == 2: + self.wallet_path = sys.argv[1] + else: + print("Usage:", sys.argv[0], "WALLET_FILE") + sys.exit(-1) + + self.read_wallet_from_file() + + # TODO: make persona selection CLI more robust, maybe use Python Click library + if self.wallet_message.my_personas: + print("Your personas:") + for index, persona in enumerate(self.wallet_message.my_personas): + print(index, persona.local_name) + create_new_input = input("Would you like to use an existing persona? [y/n]: ") + create_new = create_new_input.lower() == "n" + else: + print("No existing personas found. Creating new persona.") + create_new = True + + if create_new: + self.current_persona = self.create_new_persona() + else: + persona_selection = -1 + while not persona_selection > -1 and persona_selection < len(self.wallet_message.my_personas): + persona_selection = int(input("Select a persona to use: ")) + + self.current_persona = self.wallet_message.my_personas[persona_selection] + + + def get_config_path(self): + path_to_script = os.path.dirname(os.path.realpath(__file__)) + return path_to_script[:-13] + "/secret/config.txt" + + def create_local_file(self): + if os.path.exists(self.wallet_path): + return + else: + #Create a new file + with open(self.wallet_path, 'w') as fp: + pass + + def read_wallet_from_file(self): + """Read wallet data from the wallet_path file and parse it into the wallet_message protobuf object""" + try: + f = open(self.wallet_path, "rb") + self.wallet_message.ParseFromString(f.read()) + f.close() + except IOError: + print("Creating new wallet file.") + + def create_new_persona(self): + """Prompt user for a name and add a new owned persona to the wallet.""" + new_persona = self.wallet_message.my_personas.add() + new_persona.owned = True + new_persona.local_name = input("Enter name: ") + signing_key = SigningKey.generate() + new_persona.private_key = pickle.dumps(signing_key) + new_persona.public_key = pickle.dumps(signing_key.verify_key) + return new_persona + + def save_peer_persona(self, node_num, public_key, friendly_name): + """Save a peer persona to wallet""" + self.configure_peer_persona(self.wallet_message.peer_personas.add(), node_num, public_key, friendly_name ) + self.write_wallet_to_file() + + def configure_peer_persona(self, persona, node_num, public_key, friendly_name): + persona.local_name = friendly_name + persona.public_key = public_key + persona.owned = False + persona.node_num = node_num + + def write_wallet_to_file(self): + """Writes wallet data out to a file on disk""" + # Note that the bytes are binary, not text; we only use the str type as a convenient container. + f = open(self.wallet_path, "wb") + f.write(self.wallet_message.SerializeToString()) + f.close() diff --git a/protobufs b/protobufs index ed9f2499d..7c8e9fd87 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit ed9f2499d692925461facd64c6af2d2a7672245a +Subproject commit 7c8e9fd872ee62b4afb89f872ecbb68990f3ec02 diff --git a/requirements.txt b/requirements.txt index 8c351a132..c474b1680 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,5 @@ pytap2 pdoc3 pypubsub pygatt; platform_system == "Linux" +geopy==2.3.0 +PyNaCl==1.5.0 diff --git a/setup.py b/setup.py index 829b47d89..60df769a3 100644 --- a/setup.py +++ b/setup.py @@ -11,14 +11,14 @@ # This call to setup() does all the work setup( - name="meshtastic", - version="2.0.2", - description="Python API & client shell for talking to Meshtastic devices", + name="BlackLager", + version="1.0.0", + description="Python API & client shell for sending encrypted messages to Meshtastic devices", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/meshtastic/python", - author="Meshtastic Developers", - author_email="contact@meshtastic.org", + url="https://github.com/black-lager/python", + author="Black Lager Developers", + author_email=["mkrizek@dons.usfca.edu", "akadd8@gmail.com"], license="GPL-3.0-only", classifiers=[ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", @@ -28,11 +28,11 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], - packages=["meshtastic"], + packages=["BlackLager"], include_package_data=True, install_requires=["pyserial>=3.4", "protobuf>=3.13.0", "pypubsub>=4.0.3", "dotmap>=1.3.14", "pexpect>=4.6.0", "pyqrcode>=1.2.1", - "tabulate>=0.8.9", "timeago>=1.0.15", "pyyaml", + "tabulate>=0.8.9", "timeago>=1.0.15", "pyyaml", "geopy>=2.3.0", "pygatt>=4.0.5 ; platform_system=='Linux'"], extras_require={ 'tunnel': ["pytap2>=2.0.0"] @@ -40,6 +40,7 @@ python_requires='>=3.7', entry_points={ "console_scripts": [ + "blacklager=black_lager_app.__main__:main", "meshtastic=meshtastic.__main__:main", "mesh-tunnel=meshtastic.__main__:tunnelMain [tunnel]" ] diff --git a/textpad.py b/textpad.py new file mode 100644 index 000000000..994f287c5 --- /dev/null +++ b/textpad.py @@ -0,0 +1,61 @@ +from client_utils import error_handler +import curses +from datetime import datetime +import time +import traceback + + +class TextPad(object): + # use this as a virtual notepad + # write a large amount of data to it, then display a section of it on the screen + # to have a border, use another window with a border + def __init__(self, name, rows, columns, y1, x1, y2, x2, ShowBorder, BorderColor, stdscr): + self.name = name + self.rows = rows + self.columns = columns + self.y1 = y1 # These are coordinates for the window corners on the screen + self.x1 = x1 # These are coordinates for the window corners on the screen + self.y2 = y2 # These are coordinates for the window corners on the screen + self.x2 = x2 # These are coordinates for the window corners on the screen + self.ShowBorder = ShowBorder + self.BorderColor = BorderColor # pre defined text colors 1-7 + self.TextPad = curses.newpad(self.rows, self.columns) + self.PreviousLineColor = 2 + self.stdscr = stdscr + + def pad_print(self, PrintLine, Color=2, TimeStamp=False): + # print to the pad + try: + self.TextPad.idlok(1) + self.TextPad.scrollok(1) + + current_time = datetime.now().strftime("%H:%M:%S") + if (TimeStamp): + PrintLine = current_time + ": " + PrintLine + + # expand tabs to X spaces, pad the string with space then truncate + PrintLine = PrintLine.expandtabs(4) + PrintLine = PrintLine.ljust(self.columns, ' ') + + self.TextPad.attron(curses.color_pair(Color)) + self.TextPad.addstr(PrintLine) + self.TextPad.attroff(curses.color_pair(Color)) + + # We will refresh after a series of calls instead of every update + self.TextPad.refresh(0, 0, self.y1, self.x1, + self.y1 + self.rows, self.x1 + self.columns) + + except Exception as ErrorMessage: + time.sleep(2) + TraceMessage = traceback.format_exc() + AdditionalInfo = "PrintLine: " + PrintLine + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, self.stdscr) + + def clear(self): + try: + self.TextPad.erase() + self.TextPad.refresh(0, 0, self.y1, self.x1, self.y1 + self.rows, self.x1 + self.columns) + except Exception as ErrorMessage: + TraceMessage = traceback.format_exc() + AdditionalInfo = "erasing textpad" + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, self.stdscr) diff --git a/textwindow.py b/textwindow.py new file mode 100644 index 000000000..d029e99eb --- /dev/null +++ b/textwindow.py @@ -0,0 +1,195 @@ +from client_utils import error_handler +import curses +from datetime import datetime +import traceback + + +class TextWindow(object): + def __init__(self, name, rows, columns, y1, x1, y2, x2, ShowBorder, BorderColor, TitleColor, stdscr): + self.name = name + self.rows = rows + self.columns = columns + self.y1 = y1 + self.x1 = x1 + self.y2 = y2 + self.x2 = x2 + self.ShowBorder = ShowBorder + self.BorderColor = BorderColor # pre defined text colors 1-7 + self.TextWindow = curses.newwin( + self.rows, self.columns, self.y1, self.x1) + self.CurrentRow = 1 + self.StartColumn = 1 + # we will modify this later, based on if we show borders or not + self.DisplayRows = self.rows + # we will modify this later, based on if we show borders or not + self.DisplayColumns = self.columns + self.PreviousLineText = "" + self.PreviousLineRow = 0 + self.PreviousLineColor = 2 + self.Title = "" + self.TitleColor = TitleColor + self.stdscr = stdscr + + # If we are showing border, we only print inside the lines + if (self.ShowBorder == 'Y'): + self.CurrentRow = 1 + self.StartColumn = 1 + self.DisplayRows = self.rows - 2 # we don't want to print over the border + # we don't want to print over the border + self.DisplayColumns = self.columns - 2 + self.TextWindow.attron(curses.color_pair(BorderColor)) + self.TextWindow.border() + self.TextWindow.attroff(curses.color_pair(BorderColor)) + self.TextWindow.refresh() + + else: + self.CurrentRow = 0 + self.StartColumn = 0 + + def scroll_print(self, PrintLine, Color=2, TimeStamp=False, BoldLine=True): + # print(PrintLine) + # for now the string is printed in the window and the current row is incremented + # when the counter reaches the end of the window, we will wrap around to the top + # we don't print on the window border + # make sure to pad the new string with spaces to overwrite any old text + + current_time = datetime.now().strftime("%H:%M:%S") + + if (TimeStamp): + PrintLine = current_time + ": {}".format(PrintLine) + + # expand tabs to X spaces, pad the string with space + PrintLine = PrintLine.expandtabs(4) + + # adjust strings + # Get a part of the big string that will fit in the window + PrintableString = PrintLine[0:self.DisplayColumns] + RemainingString = PrintLine[self.DisplayColumns+1:] + + try: + + while (len(PrintableString) > 0): + + # padd with spaces + PrintableString = PrintableString.ljust( + self.DisplayColumns, ' ') + + # if (self.rows == 1): + # #if you print on the last character of a window you get an error + # PrintableString = PrintableString[0:-2] + # self.TextWindow.addstr(0,0,PrintableString) + # else: + + # unbold Previous line + self.TextWindow.attron( + curses.color_pair(self.PreviousLineColor)) + self.TextWindow.addstr( + self.PreviousLineRow, self.StartColumn, self.PreviousLineText) + self.TextWindow.attroff( + curses.color_pair(self.PreviousLineColor)) + + if BoldLine: + # A_NORMAL Normal display (no highlight) + # A_STANDOUT Best highlighting mode of the terminal + # A_UNDERLINE Underlining + # A_REVERSE Reverse video + # A_BLINK Blinking + # A_DIM Half bright + # A_BOLD Extra bright or bold + # A_PROTECT Protected mode + # A_INVIS Invisible or blank mode + # A_ALTCHARSET Alternate character set + # A_CHARTEXT Bit-mask to extract a character + # COLOR_PAIR(n) Color-pair number n + + # print new line in bold + self.TextWindow.attron(curses.color_pair(Color)) + self.TextWindow.addstr( + self.CurrentRow, self.StartColumn, PrintableString, curses.A_BOLD) + self.TextWindow.attroff(curses.color_pair(Color)) + else: + # print new line in Regular + self.TextWindow.attron(curses.color_pair(Color)) + self.TextWindow.addstr( + self.CurrentRow, self.StartColumn, PrintableString) + self.TextWindow.attroff(curses.color_pair(Color)) + + self.PreviousLineText = PrintableString + self.PreviousLineColor = Color + self.PreviousLineRow = self.CurrentRow + self.CurrentRow = self.CurrentRow + 1 + + # Adjust strings + PrintableString = RemainingString[0:self.DisplayColumns] + RemainingString = RemainingString[self.DisplayColumns:] + + if (self.CurrentRow > (self.DisplayRows)): + if (self.ShowBorder == 'Y'): + self.CurrentRow = 1 + else: + self.CurrentRow = 0 + + # erase to end of line + # self.TextWindow.clrtoeol() + self.TextWindow.refresh() + + except Exception as ErrorMessage: + TraceMessage = traceback.format_exc() + AdditionalInfo = "PrintLine: {}".format(PrintLine) + + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, self.stdscr) + + def window_print(self, y, x, PrintLine, Color=2): + # print at a specific coordinate within the window + # try: + + # expand tabs to X spaces, pad the string with space then truncate + PrintLine = PrintLine.expandtabs(4) + + # pad the print line with spaces then truncate at the display length + PrintLine = PrintLine.ljust(self.DisplayColumns - 1) + PrintLine = PrintLine[0:self.DisplayColumns - x] + + self.TextWindow.attron(curses.color_pair(Color)) + self.TextWindow.addstr(y, x, PrintLine) + self.TextWindow.attroff(curses.color_pair(Color)) + + self.TextWindow.refresh() + + def display_title(self): + # display the window title + + title = '' + + try: + # expand tabs to X spaces, pad the string with space then truncate + title = self.Title[0:self.DisplayColumns-3] + + self.TextWindow.attron(curses.color_pair(self.TitleColor)) + if (self.rows > 2): + # print new line in bold + self.TextWindow.addstr(0, 2, title) + else: + print("ERROR - You cannot display title on a window smaller than 3 rows") + + self.TextWindow.attroff(curses.color_pair(self.TitleColor)) + self.TextWindow.refresh() + + except Exception as ErrorMessage: + TraceMessage = traceback.format_exc() + AdditionalInfo = "Title: " + title + error_handler(ErrorMessage, TraceMessage, AdditionalInfo, self.stdscr) + + def clear(self): + self.TextWindow.erase() + self.TextWindow.attron(curses.color_pair(self.BorderColor)) + self.TextWindow.border() + self.TextWindow.attroff(curses.color_pair(self.BorderColor)) + self.display_title() + + if self.ShowBorder == 'Y': + self.CurrentRow = 1 + self.StartColumn = 1 + else: + self.CurrentRow = 0 + self.StartColumn = 0