├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml └── src └── meshcore_cli ├── __init__.py ├── __main__.py └── meshcore_cli.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 fdlamotte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meshcore-cli 2 | 3 | meshcore-cli : CLI interface to MeschCore companion app over BLE, TCP or Serial 4 | 5 | ## Install 6 | 7 | Meshcore-cli depends on the [python meshcore](https://github.com/fdlamotte/meshcore_py) package. You can install both via `pip` or `pipx` using the command : 8 | 9 |
 10 | $ pipx install meshcore-cli
 11 | 
12 | 13 | It will install you `meshcore-cli` and `meshcli`, which is an alias to the former. 14 | 15 | If you want meshcore-cli to remember last BLE device, you should have some `$HOME/.config/meshcore` where configuration for meschcore-cli will be stored (if not it will use first device it finds). 16 | 17 | ## Usage 18 | 19 |
 20 | $ meshcli <args> <commands>
 21 | 
22 | 23 | If using BLE, don't forget to pair your device first (using `bluetoothctl` for instance on Linux) or meshcli won't be able to communicate. There is a device selector for BLE, you'll just have to use `meshcli -S` to select your device, subsequent calls to meshcli will be send to that device. 24 | 25 | ### Configuration 26 | 27 | Configuration files are stored in ```$HOME/.config/meshcore``` 28 | 29 | If the directory exists, default ble address and history will be stored there. 30 | 31 | If there is an initialization script file called ```init```, it will be executed just before the commands provided on command line are executed (and after evaluation of the arguments). 32 | 33 | ### Arguments 34 | 35 | Arguments mostly deals with ble connection 36 | 37 |
 38 |     -h : prints this help
 39 |     -j : json output
 40 |     -D : print debug messages
 41 |     -S : BLE device selector
 42 |     -l : lists BLE devices
 43 |     -a <address>    : specifies device address (can be a name)
 44 |     -d <name>       : filter meshcore devices with name or address
 45 |     -t <hostname>   : connects via tcp/ip
 46 |     -p <port>       : specifies tcp port (default 5000)
 47 |     -s <port>       : use serial port <port>
 48 |     -b <baudrate>   : specify baudrate
 49 | 
50 | 51 | ### Available Commands 52 | 53 | Commands are given after arguments, they can be chained and some have shortcuts. Also prefixing a command with a dot ```.``` will force it to output json instead of synthetic result. 54 | 55 |
 56 |   General commands
 57 |     chat                   : enter the chat (interactive) mode
 58 |     chat_to <ct>           : enter chat with contact                to
 59 |     script <filename>      : execute commands in filename
 60 |     infos                  : print informations about the node      i
 61 |     card                   : export this node URI                   e
 62 |     ver                    : firmware version                       v
 63 |     reboot                 : reboots node
 64 |     sleep <secs>           : sleeps for a given amount of secs      s
 65 |     wait_key               : wait until user presses <Enter>        wk
 66 |   Messenging
 67 |     msg <name> <msg>       : send message to node by name           m  {
 68 |     wait_ack               : wait an ack                            wa }
 69 |     chan <nb> <msg>        : send message to channel number <nb>    ch
 70 |     public <msg>           : send message to public channel (0)     dch
 71 |     recv                   : reads next msg                         r
 72 |     wait_msg               : wait for a message and read it         wm
 73 |     sync_msgs              : gets all unread msgs from the node     sm
 74 |     msgs_subscribe         : display msgs as they arrive            ms
 75 |   Management
 76 |     advert                 : sends advert                           a
 77 |     floodadv               : flood advert
 78 |     get <param>            : gets a param, "get help" for more
 79 |     set <param> <value>    : sets a param, "set help" for more 
 80 |     time <epoch>           : sets time to given epoch
 81 |     clock                  : get current time
 82 |     clock sync             : sync device clock                      st
 83 |     cli                    : send a cmd to node's cli (if avail)    @
 84 |   Contacts
 85 |     contacts / list        : gets contact list                      lc
 86 |     share_contact <ct>     : share a contact with others            sc
 87 |     export_contact <ct>    : get a contact's URI                    ec
 88 |     import_contact <URI>   : import a contactt from its URI         ic
 89 |     remove_contact <ct>    : removes a contact from this node
 90 |     reset_path <ct>        : resets path to a contact to flood      rp
 91 |     change_path <ct> <pth> : change the path to a contact           cp
 92 |     change_flags <ct> <f>  : change contact flags (tel_l|tel_a|star)cf
 93 |     req_telemetry <ct>     : prints telemetry data as json          rt
 94 |   Repeaters
 95 |     login <name> <pwd>     : log into a node (rep) with given pwd   l
 96 |     logout <name>          : log out of a repeater
 97 |     cmd <name> <cmd>       : sends a command to a repeater (no ack) c  [
 98 |     wmt8                   : wait for a msg (reply) with a timeout     ]
 99 |     req_status <name>      : requests status from a node            rs
100 | 
101 | 102 | ### Interactive Mode 103 | 104 | aka Instant Message or chat mode ... 105 | 106 | Chat mode lets you interactively interact with your node or remote nodes. It is automatically triggered when no option is given on the command line. 107 | 108 | You'll get a prompt with the name of your node. From here you can type meshcore-cli commands. The prompt has history and a basic completion (pressing tab will display possible command or argument options). 109 | 110 | The `to` command is specific to chat mode, it lets you enter the recipient for next command. By default you're on your node but you can enter other nodes or public rooms. Here are some examples : 111 | * `to ` : will enter nodename 112 | * `to /`, `to ~` : will go to the root (your node) 113 | * `to ..` : will go to the last node (it will switch between the two last nodes, this is just a 1-depth history) 114 | * `to !` : will switch to the node you received last message from 115 | 116 | When you are connected to a node, the behaviour will depend on the node type, if you're on a chat node, it will send messages by default and you can chat. On a repeater or a room server, it will send commands (autocompletioin has been set to comply with the CommonCli class of meshcore). To send a message through a room you'll have to prefix the message with a quote or use the send command. 117 | 118 | You can alse set a channel as recipient, `to public` will switch to the public channel, and `to ch1` to channel 1. 119 | 120 | ## Examples 121 | 122 |
123 | # gets info from first ble MC device it finds (was -s but now used for serial port)
124 | $ meshcore-cli -d "" infos
125 | INFO:meshcore:Scanning for devices
126 | INFO:meshcore:Found device : C2:2B:A1:D5:3E:B6: MeshCore-t114_fdl
127 | INFO:meshcore:BLE Connection started
128 | {
129 |     "adv_type": 1,
130 |     "tx_power": 22,
131 |     "max_tx_power": 22,
132 |     "public_key": "993acd42fc779962c68c627829b32b111fa27a67d86b75c17460ff48c3102db4",
133 |     "adv_lat": 47.794,
134 |     "adv_lon": -3.428,
135 |     "radio_freq": 869.525,
136 |     "radio_bw": 250.0,
137 |     "radio_sf": 11,
138 |     "radio_cr": 5,
139 |     "name": "t114_fdl"
140 | }
141 | 
142 | # getting time
143 | $ meshcli -a C2:2B:A1:D5:3E:B6 clock
144 | INFO:meshcore:BLE Connection started
145 | Current time : 2025-04-18 08:19:26 (1744957166)
146 | 
147 | # If you're familiar with meshcli, you should have noted that 
148 | # now output is not json only, to get json output, use -j 
149 | # or prefix your commands with a dot
150 | $ meshcli -a C2:2B:A1:D5:3E:B6 .clock
151 | INFO:meshcore:BLE Connection started
152 | {
153 |     "time": 1744957249
154 | }
155 | 
156 | # Using -j, meshcli will return replies in json format ...
157 | $ meshcli -j -a C2:2B:A1:D5:3E:B6 clock
158 | {
159 |     "time": 1744957261
160 | }
161 | 
162 | # So if I reboot the node, and want to set time, I can chain the commands
163 | # and get that kind of output (even better by feeding it to jq)
164 | $ meshcli reboot
165 | INFO:meshcore:BLE Connection started
166 | $ meshcli -j clock clock sync clock | jq -c
167 | { "time": 1715770371 }
168 | { "ok": "time synced" }
169 | { "time": 1745996105 }
170 | 
171 | # Now check if time is ok with human output (I don't read epoch time yet)
172 | $ meshcli clock
173 | INFO:meshcore:BLE Connection started
174 | Current time : 2025-04-30 08:56:27 (1745996187)
175 | 
176 | # Now you'll probably want to send some messages ... 
177 | # For that, there is the msg command, wait_ack
178 | $ meshcli msg Techo_fdl "Hello T-Echo" wa
179 | INFO:meshcore:BLE Connection started
180 | Msg acked
181 | 
182 | # I can check the message on the techo
183 | $ meshcli -d Techo sm
184 | INFO:meshcore:Scanning for devices
185 | INFO:meshcore:Found device : DE:B6:D0:68:D5:62: MeshCore-Techo_fdl
186 | INFO:meshcore:BLE Connection started
187 | t114_fdl(0): Hello T-Echo
188 | 
189 | # And reply using json output for more verbosity
190 | # here I've used jq with -cs to get a compact array
191 | $ meshcli msg t114_fdl hello wa | jq -cs
192 | [{"type":0,"expected_ack":"4802ed93","suggested_timeout":2970},{"code":"4802ed93"}]
193 | 
194 | # But this could have been done interactively using the chat mode
195 | # Here from the techo side. Note that un-acked messages will be
196 | # signaled with an ! at the start of the prompt (or red color in color mode)
197 | $ meshcli chat
198 | INFO:meshcore:BLE Connection started
199 | Interactive mode, most commands from terminal chat should work.
200 | Use "to" to selects contact, "list" to list contacts, "send" to send a message ...
201 | Line starting with "$" or "." will issue a meshcli command.
202 | "quit" or "q" will end interactive mode
203 | t114_fdl(D): Hello T-Echo
204 | EnsibsRoom> Hi
205 | !EnsibsRoom> to t114_fdl
206 | t114_fdl> Hi
207 | t114_fdl(D): It took you long to reply ...
208 | t114_fdl> I forgot to set the recipient with the to command
209 | t114_fdl(D): It happens ...
210 | t114_fdl> 
211 | 
212 | # Loging into repeaters and sending commands is also possible
213 | # directly from the chat, because we can use meshcli commands ;)
214 | $ meshcli chat (pending msgs are shown at connexion ...)
215 | INFO:meshcore:BLE Connection started
216 | Interactive mode, most commands from terminal chat should work.
217 | Use "to" to selects contact, "list" to list contacts, "send" to send a message ...
218 | Line starting with "$" or "." will issue a meshcli command.
219 | "quit" or "q" will end interactive mode
220 | Techo_fdl(0): Cool to receive some msgs from you
221 | Techo_fdl(D): Hi
222 | Techo_fdl(D): I forgot to set the recipient with the to command
223 | FdlRoom> login password
224 | Login success
225 | FdlRoom> clock
226 | FdlRoom(0): 06:40 - 18/4/2025 UTC
227 | FdlRoom>
228 | 
229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "meshcore-cli" 7 | version = "1.0.1" 8 | authors = [ 9 | { name="Florent de Lamotte", email="florent@frizoncorrea.fr" }, 10 | ] 11 | description = "Command line interface to meshcore companion radios" 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Operating System :: OS Independent", 17 | ] 18 | license = "MIT" 19 | license-files = ["LICEN[CS]E*"] 20 | dependencies = [ "meshcore >= 1.9.10", "prompt_toolkit >= 3.0.50", "requests >= 2.28.0" ] 21 | 22 | [project.urls] 23 | Homepage = "https://github.com/fdlamotte/meshcore-cli" 24 | Issues = "https://github.com/fdlamotte/meshcore-cli/issues" 25 | 26 | [project.scripts] 27 | meshcli = "meshcore_cli.meshcore_cli:cli" 28 | meshcore-cli = "meshcore_cli.meshcore_cli:cli" 29 | -------------------------------------------------------------------------------- /src/meshcore_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdlamotte/meshcore-cli/c26bfbf1b227ee84e4caff392bc5fbf012bbd5eb/src/meshcore_cli/__init__.py -------------------------------------------------------------------------------- /src/meshcore_cli/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | from meshcore_cli.meshcore_cli import cli 3 | cli() 4 | -------------------------------------------------------------------------------- /src/meshcore_cli/meshcore_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | mccli.py : CLI interface to MeschCore BLE companion app 4 | """ 5 | import asyncio 6 | import os, sys 7 | import time, datetime 8 | import getopt, json, shlex, re 9 | import logging 10 | import requests 11 | from bleak import BleakScanner 12 | from pathlib import Path 13 | from prompt_toolkit.shortcuts import PromptSession 14 | from prompt_toolkit.shortcuts import CompleteStyle 15 | from prompt_toolkit.completion import NestedCompleter 16 | from prompt_toolkit.history import FileHistory 17 | from prompt_toolkit.formatted_text import ANSI 18 | from prompt_toolkit.key_binding import KeyBindings 19 | from prompt_toolkit.shortcuts import radiolist_dialog 20 | 21 | from meshcore import TCPConnection, BLEConnection, SerialConnection 22 | from meshcore import MeshCore, EventType, logger 23 | 24 | # Version 25 | VERSION = "v1.0.1" 26 | 27 | # default ble address is stored in a config file 28 | MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/" 29 | MCCLI_ADDRESS = MCCLI_CONFIG_DIR + "default_address" 30 | MCCLI_HISTORY_FILE = MCCLI_CONFIG_DIR + "history" 31 | MCCLI_INIT_SCRIPT = MCCLI_CONFIG_DIR + "init" 32 | 33 | # Fallback address if config file not found 34 | # if None or "" then a scan is performed 35 | ADDRESS = "" 36 | JSON = False 37 | 38 | PS = None 39 | CS = None 40 | 41 | # Ansi colors 42 | ANSI_END = "\033[0m" 43 | ANSI_INVERT = "\033[7m" 44 | ANSI_NORMAL = "\033[27m" 45 | ANSI_GREEN = "\033[0;32m" 46 | ANSI_BGREEN = "\033[1;32m" 47 | ANSI_BLUE = "\033[0;34m" 48 | ANSI_BBLUE = "\033[1;34m" 49 | ANSI_YELLOW = "\033[0;33m" 50 | ANSI_BYELLOW = "\033[1;33m" 51 | ANSI_RED = "\033[0;31m" 52 | ANSI_BRED = "\033[1;31m" 53 | ANSI_MAGENTA = "\033[0;35m" 54 | ANSI_BMAGENTA = "\033[1;35m" 55 | ANSI_CYAN = "\033[0;36m" 56 | ANSI_BCYAN = "\033[1;36m" 57 | ANSI_LIGHT_BLUE = "\033[0;94m" 58 | ANSI_LIGHT_GREEN = "\033[0;92m" 59 | ANSI_LIGHT_YELLOW = "\033[0;93m" 60 | ANSI_LIGHT_GRAY="\033[0;38;5;247m" 61 | ANSI_BGRAY="\033[1;38;5;247m" 62 | 63 | def escape_ansi(line): 64 | ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') 65 | return ansi_escape.sub('', line) 66 | 67 | def print_above(str): 68 | """ prints a string above current line """ 69 | width = os.get_terminal_size().columns 70 | stringlen = len(escape_ansi(str))-1 71 | lines = divmod(stringlen, width)[0] + 1 72 | print("\u001B[s", end="") # Save current cursor position 73 | print("\u001B[A", end="") # Move cursor up one line 74 | print("\u001B[999D", end="") # Move cursor to beginning of line 75 | for _ in range(lines): 76 | print("\u001B[S", end="") # Scroll up/pan window down 1 line 77 | print("\u001B[L", end="") # Insert new line 78 | for _ in range(lines - 1): 79 | print("\u001B[A", end="") # Move cursor up one line 80 | print(str, end="") # Print output status msg 81 | print("\u001B[u", end="", flush=True) # Jump back to saved cursor position 82 | 83 | async def process_event_message(mc, ev, json_output, end="\n", above=False): 84 | """ display incoming message """ 85 | if ev is None : 86 | logger.error("Event does not contain message.") 87 | elif ev.type == EventType.NO_MORE_MSGS: 88 | logger.debug("No more messages") 89 | return False 90 | elif ev.type == EventType.ERROR: 91 | logger.error(f"Error retrieving messages: {ev.payload}") 92 | return False 93 | elif json_output : 94 | if above : 95 | print_above(json.dumps(ev.payload)) 96 | else: 97 | print(json.dumps(ev.payload), end=end, flush=True) 98 | else : 99 | await mc.ensure_contacts() 100 | data = ev.payload 101 | 102 | if data['path_len'] == 255 : 103 | path_str = "D" 104 | else : 105 | path_str = f"{data['path_len']}" 106 | if "SNR" in data and process_event_message.print_snr: 107 | path_str = path_str + f",{data['SNR']}dB" 108 | 109 | if (data['type'] == "PRIV") : 110 | ct = mc.get_contact_by_key_prefix(data['pubkey_prefix']) 111 | if ct is None: 112 | logger.debug(f"Unknown contact with pubkey prefix: {data['pubkey_prefix']}") 113 | name = data["pubkey_prefix"] 114 | else: 115 | name = ct["adv_name"] 116 | process_event_message.last_node=ct 117 | 118 | if ct is None: # Unknown 119 | disp = f"{ANSI_RED}" 120 | elif ct["type"] == 3 : # room 121 | disp = f"{ANSI_CYAN}" 122 | elif ct["type"] == 2 : # repeater 123 | disp = f"{ANSI_MAGENTA}" 124 | else: 125 | disp = f"{ANSI_BLUE}" 126 | disp = disp + f"{name}" 127 | if 'signature' in data: 128 | sender = mc.get_contact_by_key_prefix(data['signature']) 129 | if sender is None: 130 | disp = disp + f"/{ANSI_RED}{data['signature']}" 131 | else: 132 | disp = disp + f"/{ANSI_BLUE}{sender['adv_name']}" 133 | disp = disp + f" {ANSI_YELLOW}({path_str})" 134 | if data["txt_type"] == 1: 135 | disp = disp + f"{ANSI_LIGHT_GRAY}" 136 | else: 137 | disp = disp + f"{ANSI_END}" 138 | disp = disp + f": {data['text']}" 139 | 140 | if not process_event_message.color: 141 | disp = escape_ansi(disp) 142 | 143 | if above: 144 | print_above(disp) 145 | else: 146 | print(disp, flush=True) 147 | 148 | elif (data['type'] == "CHAN") : 149 | path_str = f"{ANSI_YELLOW}({path_str}){ANSI_END}" 150 | if data["channel_idx"] == 0: #public 151 | disp = f"{ANSI_GREEN}public {path_str}" 152 | process_event_message.last_node = {"adv_name" : "public", "type" : 0, "chan_nb" : 0} 153 | else : 154 | disp = f"{ANSI_GREEN}ch{data['channel_idx']} {path_str}" 155 | process_event_message.last_node = {"adv_name" : f"ch{data['channel_idx']}", "type" : 0, "chan_nb" : data['channel_idx']} 156 | disp = disp + f"{ANSI_END}" 157 | disp = disp + f": {data['text']}" 158 | 159 | if not process_event_message.color: 160 | disp = escape_ansi(disp) 161 | 162 | if above: 163 | print_above(disp) 164 | else: 165 | print(disp) 166 | else: 167 | print(json.dumps(ev.payload)) 168 | return True 169 | process_event_message.print_snr=False 170 | process_event_message.color=True 171 | process_event_message.last_node=None 172 | 173 | async def handle_message(event): 174 | """ Process incoming message events """ 175 | await process_event_message(handle_message.mc, event, 176 | above=handle_message.above, 177 | json_output=handle_message.json_output) 178 | handle_message.json_output=False 179 | handle_message.mc=None 180 | handle_message.above=False 181 | 182 | async def subscribe_to_msgs(mc, json_output=False, above=False): 183 | """ Subscribe to incoming messages """ 184 | global PS, CS 185 | await mc.ensure_contacts() 186 | handle_message.json_output = json_output 187 | handle_message.above = above 188 | # Subscribe to private messages 189 | if PS is None : 190 | PS = mc.subscribe(EventType.CONTACT_MSG_RECV, handle_message) 191 | # Subscribe to channel messages 192 | if CS is None : 193 | CS = mc.subscribe(EventType.CHANNEL_MSG_RECV, handle_message) 194 | await mc.start_auto_message_fetching() 195 | 196 | def make_completion_dict(contacts, to=None): 197 | contact_list = {} 198 | to_list = {} 199 | 200 | to_list["~"] = None 201 | to_list["/"] = None 202 | if not process_event_message.last_node is None: 203 | to_list["!"] = None 204 | to_list[".."] = None 205 | to_list["public"] = None 206 | 207 | it = iter(contacts.items()) 208 | for c in it : 209 | contact_list[c[1]['adv_name']] = None 210 | 211 | to_list.update(contact_list) 212 | 213 | to_list["ch"] = None 214 | to_list["ch0"] = None 215 | 216 | completion_list = { 217 | "to" : to_list, 218 | "public" : None, 219 | "chan" : None, 220 | } 221 | 222 | if to is None : 223 | completion_list.update({ 224 | "ver" : None, 225 | "infos" : None, 226 | "advert" : None, 227 | "floodadv" : None, 228 | "msg" : contact_list, 229 | "wait_ack" : None, 230 | "time" : None, 231 | "clock" : {"sync" : None}, 232 | "reboot" : None, 233 | "card" : None, 234 | "upload_card" : None, 235 | "contact_info": contact_list, 236 | "export_contact" : contact_list, 237 | "upload_contact" : contact_list, 238 | "path": contact_list, 239 | "reset_path" : contact_list, 240 | "change_path" : contact_list, 241 | "change_flags" : contact_list, 242 | "remove_contact" : contact_list, 243 | "import_contact" : {"meshcore://":None}, 244 | "login" : contact_list, 245 | "cmd" : contact_list, 246 | "req_status" : contact_list, 247 | "logout" : contact_list, 248 | "req_telemetry" : contact_list, 249 | "self_telemetry" : None, 250 | "get_channel": None, 251 | "set_channel": None, 252 | "set" : { 253 | "name" : None, 254 | "pin" : None, 255 | "radio" : {",,,":None, "f,bw,sf,cr":None}, 256 | "tx" : None, 257 | "tuning" : {",", "af,tx_d"}, 258 | "lat" : None, 259 | "lon" : None, 260 | "coords" : None, 261 | "print_snr" : {"on":None, "off": None}, 262 | "json_msgs" : {"on":None, "off": None}, 263 | "color" : {"on":None, "off":None}, 264 | "print_name" : {"on":None, "off":None}, 265 | "classic_prompt" : {"on" : None, "off":None}, 266 | "manual_add_contacts" : {"on" : None, "off":None}, 267 | "telemetry_mode_base" : {"always" : None, "device":None, "never":None}, 268 | "telemetry_mode_loc" : {"always" : None, "device":None, "never":None}, 269 | "telemetry_mode_env" : {"always" : None, "device":None, "never":None}, 270 | }, 271 | "get" : {"name":None, 272 | "bat":None, 273 | "radio":None, 274 | "tx":None, 275 | "coords":None, 276 | "lat":None, 277 | "lon":None, 278 | "print_snr":None, 279 | "json_msgs":None, 280 | "color":None, 281 | "print_name":None, 282 | "classic_prompt":None, 283 | "manual_add_contacts":None, 284 | "telemetry_mode_base":None, 285 | "telemetry_mode_loc":None, 286 | "telemetry_mode_env":None, 287 | "custom":None 288 | }, 289 | }) 290 | completion_list["set"].update(make_completion_dict.custom_vars) 291 | completion_list["get"].update(make_completion_dict.custom_vars) 292 | else : 293 | completion_list.update({ 294 | "send" : None, 295 | }) 296 | 297 | if to['type'] > 0: # contact 298 | completion_list.update({ 299 | "contact_info": None, 300 | "path": None, 301 | "export_contact" : None, 302 | "upload_contact" : None, 303 | "reset_path" : None, 304 | "change_path" : None, 305 | "change_flags" : None, 306 | "req_telemetry" : None, 307 | }) 308 | 309 | if to['type'] > 1 : # repeaters and room servers 310 | completion_list.update({ 311 | "login" : None, 312 | "logout" : None, 313 | "req_status" : None, 314 | "cmd" : None, 315 | "ver" : None, 316 | "advert" : None, 317 | "time" : None, 318 | "clock" : {"sync" : None}, 319 | "reboot" : None, 320 | "start ota" : None, 321 | "password" : None, 322 | "neighbors" : None, 323 | "get" : {"name" : None, 324 | "role":None, 325 | "radio" : None, 326 | "freq":None, 327 | "tx":None, 328 | "af" : None, 329 | "repeat" : None, 330 | "allow.read.only" : None, 331 | "flood.advert.interval" : None, 332 | "flood.max":None, 333 | "advert.interval" : None, 334 | "guest.password" : None, 335 | "rxdelay": None, 336 | "txdelay": None, 337 | "direct.tx_delay":None, 338 | "public.key":None, 339 | "lat" : None, 340 | "lon" : None, 341 | }, 342 | "set" : {"name" : None, 343 | "radio" : {",,,":None, "f,bw,sf,cr": None}, 344 | "freq" : None, 345 | "tx" : None, 346 | "af": None, 347 | "repeat" : {"on": None, "off": None}, 348 | "flood.advert.interval" : None, 349 | "flood.max" : None, 350 | "advert.interval" : None, 351 | "guest.password" : None, 352 | "allow.read.only" : {"on": None, "off": None}, 353 | "rxdelay" : None, 354 | "txdelay": None, 355 | "direct.txdelay" : None, 356 | "lat" : None, 357 | "lon" : None, 358 | }, 359 | "erase": None, 360 | "log" : {"start" : None, "stop" : None, "erase" : None} 361 | }) 362 | 363 | completion_list.update({ 364 | "cli" : None, 365 | "script" : None, 366 | "quit" : None 367 | }) 368 | 369 | return completion_list 370 | make_completion_dict.custom_vars = {} 371 | 372 | async def interactive_loop(mc, to=None) : 373 | print("""Interactive mode, most commands from terminal chat should work. 374 | Use \"to\" to select recipient, use Tab to complete name ... 375 | Line starting with \"$\" or \".\" will issue a meshcli command. 376 | \"quit\", \"q\", CTRL+D will end interactive mode""") 377 | 378 | contact = to 379 | prev_contact = None 380 | 381 | await mc.ensure_contacts() 382 | await subscribe_to_msgs(mc, above=True) 383 | 384 | try: 385 | while True: # purge msgs 386 | res = await mc.commands.get_msg() 387 | if res.type == EventType.NO_MORE_MSGS: 388 | break 389 | 390 | if os.path.isdir(MCCLI_CONFIG_DIR) : 391 | our_history = FileHistory(MCCLI_HISTORY_FILE) 392 | else: 393 | our_history = None 394 | 395 | # beware, mouse support breaks mouse scroll ... 396 | session = PromptSession(history=our_history, 397 | wrap_lines=False, 398 | mouse_support=False, 399 | complete_style=CompleteStyle.MULTI_COLUMN) 400 | 401 | bindings = KeyBindings() 402 | 403 | res = await mc.commands.get_custom_vars() 404 | cv = [] 405 | if res.type != EventType.ERROR : 406 | cv = list(res.payload.keys()) 407 | make_completion_dict.custom_vars = {k:None for k in cv} 408 | 409 | # Add our own key binding. 410 | @bindings.add("escape") 411 | def _(event): 412 | event.app.current_buffer.cancel_completion() 413 | 414 | last_ack = True 415 | while True: 416 | color = process_event_message.color 417 | classic = interactive_loop.classic or not color 418 | print_name = interactive_loop.print_name 419 | 420 | if classic: 421 | prompt = "" 422 | else: 423 | prompt = f"{ANSI_INVERT}" 424 | 425 | # some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋 426 | if print_name or contact is None : 427 | prompt = prompt + f"{ANSI_BGRAY}" 428 | prompt = prompt + f"{mc.self_info['name']}" 429 | if classic : 430 | prompt = prompt + " > " 431 | else : 432 | prompt = prompt + "🭨" 433 | 434 | if not contact is None : 435 | if not last_ack: 436 | prompt = prompt + f"{ANSI_BRED}" 437 | if classic : 438 | prompt = prompt + "!" 439 | elif contact["type"] == 3 : # room server 440 | prompt = prompt + f"{ANSI_BCYAN}" 441 | elif contact["type"] == 2 : 442 | prompt = prompt + f"{ANSI_BMAGENTA}" 443 | elif contact["type"] == 0 : # public channel 444 | prompt = prompt + f"{ANSI_BGREEN}" 445 | else : 446 | prompt = prompt + f"{ANSI_BBLUE}" 447 | if not classic: 448 | prompt = prompt + f"{ANSI_INVERT}" 449 | 450 | if print_name and not classic : 451 | prompt = prompt + "🭬" 452 | 453 | prompt = prompt + f"{contact['adv_name']}" 454 | if classic : 455 | prompt = prompt + f"{ANSI_NORMAL} > " 456 | else: 457 | prompt = prompt + f"{ANSI_NORMAL}🭬" 458 | 459 | prompt = prompt + f"{ANSI_END}" 460 | 461 | if not color : 462 | prompt=escape_ansi(prompt) 463 | 464 | session.app.ttimeoutlen = 0.2 465 | session.app.timeoutlen = 0.2 466 | 467 | completer = NestedCompleter.from_nested_dict( 468 | make_completion_dict(mc.contacts, to=contact)) 469 | 470 | line = await session.prompt_async(ANSI(prompt), 471 | complete_while_typing=False, 472 | completer=completer, 473 | key_bindings=bindings) 474 | 475 | if line == "" : # blank line 476 | pass 477 | 478 | # raw meshcli command as on command line 479 | elif line.startswith("$") : 480 | args = shlex.split(line[1:]) 481 | await process_cmds(mc, args) 482 | 483 | elif line.startswith("@") : # send a cli command that won't need quotes ! 484 | args=["cli", line[1:]] 485 | await process_cmds(mc, args) 486 | 487 | elif line.startswith("to ") : # dest 488 | dest = line[3:] 489 | if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote 490 | dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes 491 | nc = mc.get_contact_by_name(dest) 492 | if nc is None: 493 | if dest == "public" : 494 | nc = {"adv_name" : "public", "type" : 0, "chan_nb" : 0} 495 | elif dest.startswith("ch"): 496 | dest = int(dest[2:]) 497 | nc = {"adv_name" : "chan" + str(dest), "type" : 0, "chan_nb" : dest} 498 | elif dest == ".." : # previous recipient 499 | nc = prev_contact 500 | elif dest == "~" or dest == "/" or dest == mc.self_info['name']: 501 | nc = None 502 | elif dest == "!" : 503 | nc = process_event_message.last_node 504 | else : 505 | print(f"Contact '{dest}' not found in contacts.") 506 | nc = contact 507 | if nc != contact : 508 | last_ack = True 509 | prev_contact = contact 510 | contact = nc 511 | 512 | elif line == "to" : 513 | if contact is None : 514 | print(mc.self_info['name']) 515 | else: 516 | print(contact["adv_name"]) 517 | 518 | elif line == "quit" or line == "q" : 519 | break 520 | 521 | # commands that take one parameter (don't need quotes) 522 | elif line.startswith("public ") or line.startswith("cli ") : 523 | cmds = line.split(" ", 1) 524 | args = [cmds[0], cmds[1]] 525 | await process_cmds(mc, args) 526 | 527 | # lines starting with ! are sent as reply to last received msg 528 | elif line.startswith("!"): 529 | ln = process_event_message.last_node 530 | if ln is None : 531 | print("No received msg yet !") 532 | elif ln["type"] == 0 : 533 | await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] ) 534 | else : 535 | last_ack = await msg_ack(mc, ln, line[1:]) 536 | if last_ack == False : 537 | contact = ln 538 | 539 | # commands are passed through if at root 540 | elif contact is None or line.startswith(".") : 541 | args = shlex.split(line) 542 | await process_cmds(mc, args) 543 | 544 | # commands that take contact as second arg will be sent to recipient 545 | elif contact["type"] > 0 and (line == "sc" or line == "share_contact" or\ 546 | line == "ec" or line == "export_contact" or\ 547 | line == "uc" or line == "upload_contact" or\ 548 | line == "rp" or line == "reset_path" or\ 549 | line == "contact_info" or line == "ci" or\ 550 | line == "req_status" or line == "rs" or\ 551 | line == "req_telemetry" or line == "rt" or\ 552 | line == "path" or\ 553 | line == "logout" ) : 554 | args = [line, contact['adv_name']] 555 | await process_cmds(mc, args) 556 | 557 | # same but for commands with a parameter 558 | elif contact["type"] > 0 and (line.startswith("cmd ") or\ 559 | line.startswith("cp ") or line.startswith("change_path ") or\ 560 | line.startswith("cf ") or line.startswith("change_flags ") or\ 561 | line.startswith("login ")) : 562 | cmds = line.split(" ", 1) 563 | args = [cmds[0], contact['adv_name'], cmds[1]] 564 | await process_cmds(mc, args) 565 | 566 | elif line.startswith(":") : # : will send a command to current recipient 567 | args=["cmd", contact['adv_name'], line[1:]] 568 | await process_cmds(mc, args) 569 | 570 | elif line == "reset path" : # reset path for compat with terminal chat 571 | args = ["reset_path", contact['adv_name']] 572 | await process_cmds(mc, args) 573 | 574 | elif line == "list" : # list command from chat displays contacts on a line 575 | it = iter(mc.contacts.items()) 576 | first = True 577 | for c in it : 578 | if not first: 579 | print(", ", end="") 580 | first = False 581 | print(f"{c[1]['adv_name']}", end="") 582 | print("") 583 | 584 | elif line.startswith("send") or line.startswith("\"") : 585 | if line.startswith("send") : 586 | line = line[5:] 587 | if line.startswith("\"") : 588 | line = line[1:] 589 | last_ack = await msg_ack(mc, contact, line) 590 | 591 | elif contact["type"] == 0 : # channel, send msg to channel 592 | await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] ) 593 | 594 | elif contact["type"] == 1 : # chat, send to recipient and wait ack 595 | last_ack = await msg_ack(mc, contact, line) 596 | 597 | elif contact["type"] == 2 or contact["type"] == 3 : # repeater, send cmd 598 | await process_cmds(mc, ["cmd", contact["adv_name"], line]) 599 | 600 | except (EOFError, KeyboardInterrupt): 601 | print("Exiting cli") 602 | except asyncio.CancelledError: 603 | # Handle task cancellation from KeyboardInterrupt in asyncio.run() 604 | print("Exiting cli") 605 | interactive_loop.classic = False 606 | interactive_loop.print_name = True 607 | 608 | async def msg_ack (mc, contact, msg) : 609 | result = await mc.commands.send_msg(contact, msg) 610 | if result.type == EventType.ERROR: 611 | print(f"⚠️ Failed to send message: {result.payload}") 612 | return False 613 | 614 | exp_ack = result.payload["expected_ack"].hex() 615 | res = await mc.wait_for_event(EventType.ACK, attribute_filters={"code": exp_ack}, timeout=5) 616 | if res is None : 617 | return False 618 | 619 | return True 620 | 621 | async def next_cmd(mc, cmds, json_output=False): 622 | """ process next command """ 623 | try : 624 | argnum = 0 625 | if cmds[0].startswith(".") : # override json_output 626 | json_output = True 627 | cmd = cmds[0][1:] 628 | else: 629 | cmd = cmds[0] 630 | match cmd : 631 | case "help" : 632 | command_help() 633 | 634 | case "ver" | "query" | "v" | "q": 635 | res = await mc.commands.send_device_query() 636 | logger.debug(res) 637 | if res.type == EventType.ERROR : 638 | print(f"ERROR: {res}") 639 | elif json_output : 640 | print(json.dumps(res.payload, indent=4)) 641 | else : 642 | print("Devince info :") 643 | if res.payload["fw ver"] >= 3: 644 | print(f" Model: {res.payload['model']}") 645 | print(f" Version: {res.payload['ver']}") 646 | print(f" Build date: {res.payload['fw_build']}") 647 | else : 648 | print(f" Firmware version : {res.payload['fw ver']}") 649 | 650 | case "clock" : 651 | if len(cmds) > 1 and cmds[1] == "sync" : 652 | argnum=1 653 | res = await mc.commands.set_time(int(time.time())) 654 | logger.debug(res) 655 | if res.type == EventType.ERROR: 656 | if json_output : 657 | print(json.dumps({"error" : "Error syncing time"})) 658 | else: 659 | print(f"Error setting time: {res}") 660 | elif json_output : 661 | res.payload["ok"] = "time synced" 662 | print(json.dumps(res.payload, indent=4)) 663 | else : 664 | print("Time synced") 665 | else: 666 | res = await mc.commands.get_time() 667 | timestamp = res.payload["time"] 668 | if res.type == EventType.ERROR: 669 | print(f"Error getting time: {res}") 670 | elif json_output : 671 | print(json.dumps(res.payload, indent=4)) 672 | else : 673 | print('Current time :' 674 | f' {datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")}' 675 | f' ({timestamp})') 676 | 677 | case "sync_time"|"clock sync"|"st": # keep if for the st shortcut 678 | res = await mc.commands.set_time(int(time.time())) 679 | logger.debug(res) 680 | if res.type == EventType.ERROR: 681 | if json_output : 682 | print(json.dumps({"error" : "Error syncing time"})) 683 | else: 684 | print(f"Error syncing time: {res}") 685 | elif json_output : 686 | res.payload["ok"] = "time synced" 687 | print(json.dumps(res.payload, indent=4)) 688 | else: 689 | print("Time synced") 690 | 691 | case "time" : 692 | argnum = 1 693 | res = await mc.commands.set_time(cmds[1]) 694 | logger.debug(res) 695 | if res.type == EventType.ERROR: 696 | if json_output : 697 | print(json.dumps({"error" : "Error setting time"})) 698 | else: 699 | print (f"Error setting time: {res}") 700 | elif json_output : 701 | print(json.dumps(res.payload, indent=4)) 702 | else: 703 | print("Time set") 704 | 705 | case "set": 706 | argnum = 2 707 | match cmds[1]: 708 | case "help" : 709 | argnum = 1 710 | print("""Available parameters : 711 | pin : ble pin 712 | radio : radio params 713 | tuning : tuning params 714 | tx : tx power 715 | name : node name 716 | lat : latitude 717 | lon : longitude 718 | coords : coordinates 719 | print_snr : toggle snr display in messages""") 720 | case "print_name": 721 | interactive_loop.print_name = (cmds[2] == "on") 722 | if json_output : 723 | print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]})) 724 | case "classic_prompt": 725 | interactive_loop.classic = (cmds[2] == "on") 726 | if json_output : 727 | print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]})) 728 | case "color" : 729 | process_event_message.color = (cmds[2] == "on") 730 | if json_output : 731 | print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]})) 732 | case "print_snr" : 733 | process_event_message.print_snr = (cmds[2] == "on") 734 | if json_output : 735 | print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]})) 736 | case "json_msgs" : 737 | handle_message.json_output = (cmds[2] == "on") 738 | if json_output : 739 | print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]})) 740 | case "pin": 741 | res = await mc.commands.set_devicepin(cmds[2]) 742 | logger.debug(res) 743 | if res.type == EventType.ERROR: 744 | print(f"Error: {res}") 745 | elif json_output : 746 | print(json.dumps(res.payload, indent=4)) 747 | else: 748 | print("ok") 749 | case "radio": 750 | params=cmds[2].split(",") 751 | res=await mc.commands.set_radio(params[0], params[1], params[2], params[3]) 752 | logger.debug(res) 753 | if res.type == EventType.ERROR: 754 | print(f"Error: {res}") 755 | elif json_output : 756 | print(json.dumps(res.payload, indent=4)) 757 | else: 758 | print("ok") 759 | case "name": 760 | res = await mc.commands.set_name(cmds[2]) 761 | logger.debug(res) 762 | if res.type == EventType.ERROR: 763 | print(f"Error: {res}") 764 | elif json_output : 765 | print(json.dumps(res.payload, indent=4)) 766 | else: 767 | print("ok") 768 | case "tx": 769 | res = await mc.commands.set_tx_power(cmds[2]) 770 | logger.debug(res) 771 | if res.type == EventType.ERROR: 772 | print(f"Error: {res}") 773 | elif json_output : 774 | print(json.dumps(res.payload, indent=4)) 775 | else: 776 | print("ok") 777 | case "lat": 778 | if "adv_lon" in mc.self_info : 779 | lon = mc.self_info['adv_lon'] 780 | else: 781 | lon = 0 782 | lat = float(cmds[2]) 783 | res = await mc.commands.set_coords(lat, lon) 784 | logger.debug(res) 785 | if res.type == EventType.ERROR: 786 | print(f"Error: {res}") 787 | elif json_output : 788 | print(json.dumps(res.payload, indent=4)) 789 | else: 790 | print("ok") 791 | case "lon": 792 | if "adv_lat" in mc.self_info : 793 | lat = mc.self_info['adv_lat'] 794 | else: 795 | lat = 0 796 | lon = float(cmds[2]) 797 | res = await mc.commands.set_coords(lat, lon) 798 | logger.debug(res) 799 | if res.type == EventType.ERROR: 800 | print(f"Error: {res}") 801 | elif json_output : 802 | print(json.dumps(res.payload, indent=4)) 803 | else: 804 | print("ok") 805 | case "coords": 806 | params=cmds[2].split(",") 807 | res = await mc.commands.set_coords(\ 808 | float(params[0]),\ 809 | float(params[1])) 810 | logger.debug(res) 811 | if res.type == EventType.ERROR: 812 | print(f"Error: {res}") 813 | elif json_output : 814 | print(json.dumps(res.payload, indent=4)) 815 | else: 816 | print("ok") 817 | case "tuning": 818 | params=cmds[2].commands.split(",") 819 | res = await mc.commands.set_tuning( 820 | int(params[0]), int(params[1])) 821 | logger.debug(res) 822 | if res.type == EventType.ERROR: 823 | print(f"Error: {res}") 824 | elif json_output : 825 | print(json.dumps(res.payload, indent=4)) 826 | else: 827 | print("ok") 828 | case "manual_add_contacts": 829 | mac = (cmds[2] == "on") or (cmds[2] == "true") or (cmds[2] == "yes") or (cmds[2] == "1") 830 | res = await mc.commands.set_manual_add_contacts(mac) 831 | if res.type == EventType.ERROR: 832 | print(f"Error : {res}") 833 | else : 834 | print(f"manual add contact: {mac}") 835 | case "telemetry_mode_base": 836 | if (cmds[2] == "2") or (cmds[2] == "all") or (cmds[2] == "yes") or (cmds[2] == "on") : 837 | mode = 2 838 | elif (cmds[2] == "1") or (cmds[2] == "selected") or (cmds[2] == "dev") : 839 | mode = 1 840 | else : 841 | mode = 0 842 | res = await mc.commands.set_telemetry_mode_base(mode) 843 | if res.type == EventType.ERROR: 844 | print(f"Error : {res}") 845 | else: 846 | print(f"telemetry mode: {mode}") 847 | case "telemetry_mode_loc": 848 | if (cmds[2] == "2") or (cmds[2].startswith("al")) or (cmds[2] == "yes") or (cmds[2] == "on") : 849 | mode = 2 850 | elif (cmds[2] == "1") or (cmds[2] == "selected") or (cmds[2].startswith("dev")) : 851 | mode = 1 852 | else : 853 | mode = 0 854 | res = await mc.commands.set_telemetry_mode_loc(mode) 855 | if res.type == EventType.ERROR: 856 | print(f"Error : {res}") 857 | else: 858 | print(f"telemetry mode for location: {mode}") 859 | case "telemetry_mode_env": 860 | if (cmds[2] == "2") or (cmds[2].startswith("al")) or (cmds[2] == "yes") or (cmds[2] == "on") : 861 | mode = 2 862 | elif (cmds[2] == "1") or (cmds[2] == "selected") or (cmds[2].startswith("dev")) : 863 | mode = 1 864 | else : 865 | mode = 0 866 | res = await mc.commands.set_telemetry_mode_env(mode) 867 | if res.type == EventType.ERROR: 868 | print(f"Error : {res}") 869 | else: 870 | print(f"telemetry mode for env: {mode}") 871 | case _: # custom var 872 | if cmds[1].startswith("_") : 873 | vname = cmds[1][1:] 874 | else: 875 | vname = cmds[1] 876 | res = await mc.commands.set_custom_var(vname, cmds[2]) 877 | if res.type == EventType.ERROR: 878 | print(f"Error : {res}") 879 | elif json_output : 880 | print(json.dumps({"result" : "set", "var" : vname, "value" : cmds[2]})) 881 | else : 882 | print(f"Var {vname} set to {cmds[2]}") 883 | 884 | case "get" : 885 | argnum = 1 886 | match cmds[1]: 887 | case "help": 888 | print("""Gets parameters from node 889 | name : node name 890 | bat : battery level in mV 891 | coords : adv coordinates 892 | lat : latitude 893 | lon : longitude 894 | radio : radio parameters 895 | tx : tx power 896 | print_snr : snr display in messages 897 | custom : all custom variables in json format 898 | each custom var can also be get/set directly""") 899 | case "print_name": 900 | if json_output : 901 | print(json.dumps({"print_name" : interactive_loop.print_name})) 902 | else: 903 | print(f"{'on' if interactive_loop.print_name else 'off'}") 904 | case "classic_prompt": 905 | if json_output : 906 | print(json.dumps({"classic_prompt" : interactive_loop.classic})) 907 | else: 908 | print(f"{'on' if interactive_loop.classic else 'off'}") 909 | case "json_msgs": 910 | if json_output : 911 | print(json.dumps({"json_msgs" : handle_message.json_output})) 912 | else: 913 | print(f"{'on' if handle_message.json_output else 'off'}") 914 | case "color": 915 | if json_output : 916 | print(json.dumps({"color" : process_event_message.color})) 917 | else: 918 | print(f"{'on' if process_event_message.color else 'off'}") 919 | case "print_snr": 920 | if json_output : 921 | print(json.dumps({"print_snr" : process_event_message.print_snr})) 922 | else: 923 | print(f"{'on' if process_event_message.print_snr else 'off'}") 924 | case "name": 925 | await mc.commands.send_appstart() 926 | if json_output : 927 | print(json.dumps(mc.self_info["name"])) 928 | else: 929 | print(mc.self_info["name"]) 930 | case "tx": 931 | await mc.commands.send_appstart() 932 | if json_output : 933 | print(json.dumps(mc.self_info["tx_power"])) 934 | else: 935 | print(mc.self_info["tx_power"]) 936 | case "coords": 937 | await mc.commands.send_appstart() 938 | if json_output : 939 | print(json.dumps({"lat": mc.self_info["adv_lat"], "lon":mc.self_info["adv_lon"]})) 940 | else: 941 | print(f"{mc.self_info['adv_lat']},{mc.self_info['adv_lon']}") 942 | case "lat": 943 | await mc.commands.send_appstart() 944 | if json_output : 945 | print(json.dumps({"lat": mc.self_info["adv_lat"]})) 946 | else: 947 | print(f"{mc.self_info['adv_lat']}") 948 | case "lon": 949 | await mc.commands.send_appstart() 950 | if json_output : 951 | print(json.dumps({"lon": mc.self_info["adv_lon"]})) 952 | else: 953 | print(f"{mc.self_info['adv_lon']}") 954 | case "radio": 955 | await mc.commands.send_appstart() 956 | if json_output : 957 | print(json.dumps( 958 | {"radio_freq": mc.self_info["radio_freq"], 959 | "radio_bw": mc.self_info["radio_bw"], 960 | "radio_sf": mc.self_info["radio_sf"], 961 | "radio_cr": mc.self_info["radio_cr"]})) 962 | else: 963 | print(f"{mc.self_info['radio_freq']},{mc.self_info['radio_bw']},{mc.self_info['radio_sf']},{mc.self_info['radio_cr']}") 964 | case "bat" : 965 | res = await mc.commands.get_bat() 966 | logger.debug(res) 967 | if res.type == EventType.ERROR: 968 | print(f"Error getting bat {res}") 969 | elif json_output : 970 | print(json.dumps(res.payload, indent=4)) 971 | else: 972 | print(f"Battery level : {res.payload['level']}") 973 | case "manual_add_contacts" : 974 | await mc.commands.send_appstart() 975 | if json_output : 976 | print(json.dumps({"manual_add_contacts" : mc.self_info["manual_add_contacts"]})) 977 | else : 978 | print(f"manual_add_contacts: {mc.self_info['manual_add_contacts']}") 979 | case "telemetry_mode_base" : 980 | await mc.commands.send_appstart() 981 | if json_output : 982 | print(json.dumps({"telemetry_mode_base" : mc.self_info["telemetry_mode_base"]})) 983 | else : 984 | print(f"telemetry_mode_base: {mc.self_info['telemetry_mode_base']}") 985 | case "telemetry_mode_loc" : 986 | await mc.commands.send_appstart() 987 | if json_output : 988 | print(json.dumps({"telemetry_mode_loc" : mc.self_info["telemetry_mode_loc"]})) 989 | else : 990 | print(f"telemetry_mode_loc: {mc.self_info['telemetry_mode_loc']}") 991 | case "telemetry_mode_env" : 992 | await mc.commands.send_appstart() 993 | if json_output : 994 | print(json.dumps({"telemetry_mode_env" : mc.self_info["telemetry_mode_env"]})) 995 | else : 996 | print(f"telemetry_mode_env: {mc.self_info['telemetry_mode_env']}") 997 | case "custom" : 998 | res = await mc.commands.get_custom_vars() 999 | logger.debug(res) 1000 | if res.type == EventType.ERROR : 1001 | if json_output : 1002 | print(json.dumps(res)) 1003 | else : 1004 | logger.error("Couldn't get custom variables") 1005 | else : 1006 | print(json.dumps(res.payload, indent=4)) 1007 | case _ : 1008 | res = await mc.commands.get_custom_vars() 1009 | logger.debug(res) 1010 | if res.type == EventType.ERROR : 1011 | if json_output : 1012 | print(json.dumps(res)) 1013 | else : 1014 | logger.error(f"Couldn't get custom variables") 1015 | else : 1016 | try: 1017 | if cmds[1].startswith("_"): 1018 | vname = cmds[1][1:] 1019 | else: 1020 | vname = cmds[1] 1021 | val = res.payload[vname] 1022 | except KeyError: 1023 | if json_output : 1024 | print(json.dumps({"error" : "Unknown var", "var" : cmds[1]})) 1025 | else : 1026 | print(f"Unknown var {cmds[1]}") 1027 | else: 1028 | if json_output : 1029 | print(json.dumps({"var" : vname, "value" : val})) 1030 | else: 1031 | print(val) 1032 | 1033 | case "self_telemetry" | "t": 1034 | res = await mc.commands.get_self_telemetry() 1035 | logger.debug(res) 1036 | if res.type == EventType.ERROR: 1037 | print(f"Error while requesting telemetry") 1038 | elif res is None: 1039 | if json_output : 1040 | print(json.dumps({"error" : "Timeout waiting telemetry"})) 1041 | else: 1042 | print("Timeout waiting telemetry") 1043 | else : 1044 | print(json.dumps(res.payload, indent=4)) 1045 | 1046 | case "get_channel": 1047 | argnum = 1 1048 | res = await mc.commands.get_channel(int(cmds[1])) 1049 | logger.debug(res) 1050 | if res.type == EventType.ERROR: 1051 | print(f"Error while requesting channel info") 1052 | else: 1053 | info = res.payload 1054 | info["channel_secret"] = info["channel_secret"].hex() 1055 | print(json.dumps(info)) 1056 | 1057 | case "set_channel": 1058 | argnum = 3 1059 | res = await mc.commands.set_channel(int(cmds[1]), cmds[2], bytes.fromhex(cmds[3])) 1060 | logger.debug(res) 1061 | if res.type == EventType.ERROR: 1062 | print(f"Error while setting channel") 1063 | 1064 | case "reboot" : 1065 | res = await mc.commands.reboot() 1066 | logger.debug(res) 1067 | if json_output : 1068 | print(json.dumps(res.payload, indent=4)) 1069 | 1070 | case "msg" | "m" | "{" : # sends to a contact from name 1071 | argnum = 2 1072 | await mc.ensure_contacts() 1073 | contact = mc.get_contact_by_name(cmds[1]) 1074 | if contact is None: 1075 | if json_output : 1076 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1077 | else: 1078 | print(f"Unknown contact {cmds[1]}") 1079 | else: 1080 | res = await mc.commands.send_msg(contact, cmds[2]) 1081 | logger.debug(res) 1082 | if res.type == EventType.ERROR: 1083 | print(f"Error sending message: {res}") 1084 | elif json_output : 1085 | res.payload["expected_ack"] = res.payload["expected_ack"].hex() 1086 | print(json.dumps(res.payload, indent=4)) 1087 | 1088 | case "chan"|"ch" : 1089 | argnum = 2 1090 | res = await mc.commands.send_chan_msg(int(cmds[1]), cmds[2]) 1091 | logger.debug(res) 1092 | if res.type == EventType.ERROR: 1093 | print(f"Error sending message: {res}") 1094 | elif json_output : 1095 | print(json.dumps(res.payload, indent=4)) 1096 | 1097 | case "public" | "dch" : # default chan 1098 | argnum = 1 1099 | res = await mc.commands.send_chan_msg(0, cmds[1]) 1100 | logger.debug(res) 1101 | if res.type == EventType.ERROR: 1102 | print(f"Error sending message: {res}") 1103 | elif json_output : 1104 | print(json.dumps(res.payload, indent=4)) 1105 | 1106 | case "cmd" | "c" | "[" : 1107 | argnum = 2 1108 | await mc.ensure_contacts() 1109 | contact = mc.get_contact_by_name(cmds[1]) 1110 | if contact is None: 1111 | if json_output : 1112 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1113 | else: 1114 | print(f"Unknown contact {cmds[1]}") 1115 | else: 1116 | res = await mc.commands.send_cmd(contact, cmds[2]) 1117 | logger.debug(res) 1118 | if res.type == EventType.ERROR: 1119 | print(f"Error sending cmd: {res}") 1120 | elif json_output : 1121 | res.payload["expected_ack"] = res.payload["expected_ack"].hex() 1122 | print(json.dumps(res.payload, indent=4)) 1123 | 1124 | case "login" | "l" : 1125 | argnum = 2 1126 | await mc.ensure_contacts() 1127 | contact = mc.get_contact_by_name(cmds[1]) 1128 | if contact is None: 1129 | if json_output : 1130 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1131 | else: 1132 | print(f"Unknown contact {cmds[1]}") 1133 | else: 1134 | res = await mc.commands.send_login(contact, cmds[2]) 1135 | logger.debug(res) 1136 | if res.type == EventType.ERROR: 1137 | if json_output : 1138 | print(json.dumps({"error" : "Error while login"})) 1139 | else: 1140 | print(f"Error while loging: {res}") 1141 | else: # should probably wait for the good ack 1142 | res = await mc.wait_for_event(EventType.LOGIN_SUCCESS) 1143 | logger.debug(res) 1144 | if res is None: 1145 | print("Login failed : Timeout waiting response") 1146 | elif json_output : 1147 | if res.type == EventType.LOGIN_SUCCESS: 1148 | print(json.dumps({"login_success" : True}, indent=4)) 1149 | else: 1150 | print(json.dumps({"login_success" : False, "error" : "login failed"}, indent=4)) 1151 | else: 1152 | if res.type == EventType.LOGIN_SUCCESS: 1153 | print("Login success") 1154 | else: 1155 | print("Login failed") 1156 | 1157 | case "logout" : 1158 | argnum = 1 1159 | await mc.ensure_contacts() 1160 | contact = mc.get_contact_by_name(cmds[1]) 1161 | res = await mc.commands.send_logout(contact) 1162 | logger.debug(res) 1163 | if res.type == EventType.ERROR: 1164 | print(f"Error while logout: {res}") 1165 | elif json_output : 1166 | print(json.dumps(res.payload)) 1167 | else: 1168 | print("Logout ok") 1169 | 1170 | case "req_status" | "rs" : 1171 | argnum = 1 1172 | await mc.ensure_contacts() 1173 | contact = mc.get_contact_by_name(cmds[1]) 1174 | res = await mc.commands.send_statusreq(contact) 1175 | logger.debug(res) 1176 | if res.type == EventType.ERROR: 1177 | print(f"Error while requesting status: {res}") 1178 | else : 1179 | res = await mc.wait_for_event(EventType.STATUS_RESPONSE) 1180 | logger.debug(res) 1181 | if res is None: 1182 | if json_output : 1183 | print(json.dumps({"error" : "Timeout waiting status"})) 1184 | else: 1185 | print("Timeout waiting status") 1186 | else : 1187 | print(json.dumps(res.payload, indent=4)) 1188 | 1189 | case "req_telemetry" | "rt" : 1190 | argnum = 1 1191 | await mc.ensure_contacts() 1192 | contact = mc.get_contact_by_name(cmds[1]) 1193 | res = await mc.commands.send_telemetry_req(contact) 1194 | logger.debug(res) 1195 | if res.type == EventType.ERROR: 1196 | print(f"Error while requesting telemetry") 1197 | else: 1198 | res = await mc.wait_for_event(EventType.TELEMETRY_RESPONSE) 1199 | logger.debug(res) 1200 | if res is None: 1201 | if json_output : 1202 | print(json.dumps({"error" : "Timeout waiting telemetry"})) 1203 | else: 1204 | print("Timeout waiting telemetry") 1205 | else : 1206 | print(json.dumps(res.payload, indent=4)) 1207 | 1208 | case "contacts" | "list" | "lc": 1209 | res = await mc.commands.get_contacts() 1210 | logger.debug(json.dumps(res.payload,indent=4)) 1211 | if res.type == EventType.ERROR: 1212 | print(f"Error asking for contacts: {res}") 1213 | elif json_output : 1214 | print(json.dumps(res.payload, indent=4)) 1215 | else : 1216 | for c in res.payload.items(): 1217 | print(c[1]["adv_name"]) 1218 | 1219 | case "path": 1220 | argnum = 1 1221 | res = await mc.ensure_contacts() 1222 | contact = mc.get_contact_by_name(cmds[1]) 1223 | if contact is None: 1224 | if json_output : 1225 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1226 | else: 1227 | print(f"Unknown contact {cmds[1]}") 1228 | else: 1229 | res = contact["out_path"] 1230 | if json_output : 1231 | print(json.dumps({"adv_name" : contact["adv_name"], 1232 | "out_path" : res})) 1233 | else: 1234 | if (res == "") : 1235 | print("0 hop") 1236 | else: 1237 | print(res) 1238 | 1239 | case "contact_info" | "ci": 1240 | argnum = 1 1241 | res = await mc.ensure_contacts() 1242 | contact = mc.get_contact_by_name(cmds[1]) 1243 | if contact is None: 1244 | if json_output : 1245 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1246 | else: 1247 | print(f"Unknown contact {cmds[1]}") 1248 | else: 1249 | print(json.dumps(contact, indent=4)) 1250 | 1251 | case "change_path" | "cp": 1252 | argnum = 2 1253 | await mc.ensure_contacts() 1254 | contact = mc.get_contact_by_name(cmds[1]) 1255 | if contact is None: 1256 | if json_output : 1257 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1258 | else: 1259 | print(f"Unknown contact {cmds[1]}") 1260 | else: 1261 | res = await mc.commands.change_contact_path(contact, cmds[2]) 1262 | logger.debug(res) 1263 | if res.type == EventType.ERROR: 1264 | print(f"Error setting path: {res}") 1265 | elif json_output : 1266 | print(json.dumps(res.payload, indent=4)) 1267 | await mc.commands.get_contacts() 1268 | 1269 | case "change_flags" | "cf": 1270 | argnum = 2 1271 | await mc.ensure_contacts() 1272 | contact = mc.get_contact_by_name(cmds[1]) 1273 | if contact is None: 1274 | if json_output : 1275 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1276 | else: 1277 | print(f"Unknown contact {cmds[1]}") 1278 | else: 1279 | res = await mc.commands.change_contact_flags(contact, int(cmds[2])) 1280 | logger.debug(res) 1281 | if res.type == EventType.ERROR: 1282 | print(f"Error setting path: {res}") 1283 | elif json_output : 1284 | print(json.dumps(res.payload, indent=4)) 1285 | await mc.commands.get_contacts() 1286 | 1287 | case "reset_path" | "rp" : 1288 | argnum = 1 1289 | await mc.ensure_contacts() 1290 | contact = mc.get_contact_by_name(cmds[1]) 1291 | if contact is None: 1292 | if json_output : 1293 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1294 | else: 1295 | print(f"Unknown contact {cmds[1]}") 1296 | else: 1297 | res = await mc.commands.reset_path(contact) 1298 | logger.debug(res) 1299 | if res.type == EventType.ERROR: 1300 | print(f"Error resetting path: {res}") 1301 | elif json_output : 1302 | print(json.dumps(res.payload, indent=4)) 1303 | await mc.commands.get_contacts() 1304 | 1305 | case "share_contact" | "sc": 1306 | argnum = 1 1307 | await mc.ensure_contacts() 1308 | contact = mc.get_contact_by_name(cmds[1]) 1309 | if contact is None: 1310 | if json_output : 1311 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1312 | else: 1313 | print(f"Unknown contact {cmds[1]}") 1314 | else: 1315 | res = await mc.commands.share_contact(contact) 1316 | logger.debug(res) 1317 | if res.type == EventType.ERROR: 1318 | print(f"Error while sharing contact: {res}") 1319 | elif json_output : 1320 | print(json.dumps(res.payload, indent=4)) 1321 | 1322 | case "export_contact"|"ec": 1323 | argnum = 1 1324 | await mc.ensure_contacts() 1325 | contact = mc.get_contact_by_name(cmds[1]) 1326 | if contact is None: 1327 | if json_output : 1328 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1329 | else: 1330 | print(f"Unknown contact {cmds[1]}") 1331 | else: 1332 | res = await mc.commands.export_contact(contact) 1333 | logger.debug(res) 1334 | if res.type == EventType.ERROR: 1335 | print(f"Error exporting contact: {res}") 1336 | elif json_output : 1337 | print(json.dumps(res.payload)) 1338 | else : 1339 | print(res.payload['uri']) 1340 | 1341 | case "import_contact"|"ic": 1342 | argnum = 1 1343 | if cmds[1].startswith("meshcore://") : 1344 | res = await mc.commands.import_contact(bytes.fromhex(cmds[1][11:])) 1345 | logger.debug(res) 1346 | if res.type == EventType.ERROR: 1347 | print(f"Error while importing contact: {res}") 1348 | else: 1349 | logger.info("Contact successfully added, refresh with lc") 1350 | 1351 | case "upload_contact" | "uc" : 1352 | argnum = 1 1353 | await mc.ensure_contacts() 1354 | contact = mc.get_contact_by_name(cmds[1]) 1355 | if contact is None: 1356 | if json_output : 1357 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1358 | else: 1359 | print(f"Unknown contact {cmds[1]}") 1360 | else: 1361 | res = await mc.commands.export_contact(contact) 1362 | logger.debug(res) 1363 | if res.type == EventType.ERROR: 1364 | print(f"Error exporting contact: {res}") 1365 | else : 1366 | resp = requests.post("https://map.meshcore.dev/api/v1/nodes", 1367 | json = {"links": [res.payload['uri']]}) 1368 | if json_output : 1369 | print(json.dumps({"response", str(resp)})) 1370 | else : 1371 | print(resp) 1372 | 1373 | case "card" : 1374 | res = await mc.commands.export_contact() 1375 | logger.debug(res) 1376 | if res.type == EventType.ERROR: 1377 | print(f"Error exporting contact: {res}") 1378 | elif json_output : 1379 | print(json.dumps(res.payload)) 1380 | else : 1381 | print(res.payload['uri']) 1382 | 1383 | case "upload_card" : 1384 | res = await mc.commands.export_contact() 1385 | logger.debug(res) 1386 | if res.type == EventType.ERROR: 1387 | print(f"Error exporting contact: {res}") 1388 | else : 1389 | resp = requests.post("https://map.meshcore.dev/api/v1/nodes", 1390 | json = {"links": [res.payload['uri']]}) 1391 | if json_output : 1392 | print(json.dumps({"response", str(resp)})) 1393 | else : 1394 | print(resp) 1395 | 1396 | case "remove_contact" : 1397 | argnum = 1 1398 | await mc.ensure_contacts() 1399 | contact = mc.get_contact_by_name(cmds[1]) 1400 | if contact is None: 1401 | if json_output : 1402 | print(json.dumps({"error" : "contact unknown", "name" : cmds[1]})) 1403 | else: 1404 | print(f"Unknown contact {cmds[1]}") 1405 | else: 1406 | res = await mc.commands.remove_contact(contact) 1407 | logger.debug(res) 1408 | if res.type == EventType.ERROR: 1409 | print(f"Error removing contact: {res}") 1410 | elif json_output : 1411 | print(json.dumps(res.payload, indent=4)) 1412 | 1413 | case "recv" | "r" : 1414 | res = await mc.commands.get_msg() 1415 | logger.debug(res) 1416 | await process_event_message(mc, res, json_output) 1417 | 1418 | case "sync_msgs" | "sm": 1419 | ret = True 1420 | first = True 1421 | if json_output : 1422 | print("[", end="", flush=True) 1423 | end="" 1424 | else: 1425 | end="\n" 1426 | while ret: 1427 | res = await mc.commands.get_msg() 1428 | logger.debug(res) 1429 | if res.type != EventType.NO_MORE_MSGS: 1430 | if not first and json_output : 1431 | print(",") 1432 | ret = await process_event_message(mc, res, json_output,end=end) 1433 | first = False 1434 | if json_output : 1435 | print("]") 1436 | 1437 | case "infos" | "i" : 1438 | await mc.commands.send_appstart() 1439 | print(json.dumps(mc.self_info,indent=4)) 1440 | 1441 | case "advert" | "a": 1442 | res = await mc.commands.send_advert() 1443 | logger.debug(res) 1444 | if res.type == EventType.ERROR: 1445 | print(f"Error sending advert: {res}") 1446 | elif json_output : 1447 | print(json.dumps(res.payload, indent=4)) 1448 | else: 1449 | print("Advert sent") 1450 | 1451 | case "flood_advert" | "floodadv": 1452 | res = await mc.commands.send_advert(flood=True) 1453 | logger.debug(res) 1454 | if res.type == EventType.ERROR: 1455 | print(f"Error sending advert: {res}") 1456 | elif json_output : 1457 | print(json.dumps(res.payload, indent=4)) 1458 | else: 1459 | print("Advert sent") 1460 | 1461 | case "sleep" | "s" : 1462 | argnum = 1 1463 | await asyncio.sleep(int(cmds[1])) 1464 | 1465 | case "wait_key" | "wk" : 1466 | try : 1467 | ps = PromptSession() 1468 | if json_output: 1469 | await ps.prompt_async() 1470 | else: 1471 | await ps.prompt_async("Press Enter to continue ...") 1472 | except (EOFError, KeyboardInterrupt, asyncio.CancelledError): 1473 | pass 1474 | 1475 | case "wait_msg" | "wm" : 1476 | ev = await mc.wait_for_event(EventType.MESSAGES_WAITING) 1477 | if ev is None: 1478 | print("Timeout waiting msg") 1479 | else: 1480 | res = await mc.commands.get_msg() 1481 | logger.debug(res) 1482 | await process_event_message(mc, res, json_output) 1483 | 1484 | case "trywait_msg" | "wmt" : 1485 | argnum = 1 1486 | if await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=int(cmds[1])) : 1487 | res = await mc.commands.get_msg() 1488 | logger.debug(res) 1489 | await process_event_message(mc, res, json_output) 1490 | 1491 | case "wmt8"|"]": 1492 | if await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=8) : 1493 | res = await mc.commands.get_msg() 1494 | logger.debug(res) 1495 | await process_event_message(mc, res, json_output) 1496 | 1497 | case "wait_ack" | "wa" | "}": 1498 | res = await mc.wait_for_event(EventType.ACK, timeout = 5) 1499 | logger.debug(res) 1500 | if res is None: 1501 | if json_output : 1502 | print(json.dumps({"error" : "Timeout waiting ack"})) 1503 | else: 1504 | print("Timeout waiting ack") 1505 | elif json_output : 1506 | print(json.dumps(res.payload, indent=4)) 1507 | else : 1508 | print("Msg acked") 1509 | 1510 | case "msgs_subscribe" | "ms" : 1511 | await subscribe_to_msgs(mc, json_output=json_output) 1512 | 1513 | case "interactive" | "im" | "chat" : 1514 | await interactive_loop(mc) 1515 | 1516 | case "chat_to" | "imto" | "to" : 1517 | argnum = 1 1518 | await mc.ensure_contacts() 1519 | contact = mc.get_contact_by_name(cmds[1]) 1520 | await interactive_loop(mc, to=contact) 1521 | 1522 | case "script" : 1523 | argnum = 1 1524 | await process_script(mc, cmds[1], json_output=json_output) 1525 | 1526 | case "cli" | "@" : 1527 | argnum = 1 1528 | res = await mc.commands.send_cli(cmds[1]) 1529 | logger.debug(res) 1530 | if res.type == EventType.ERROR: 1531 | print(f"Error sending cli cmd: {res}") 1532 | elif json_output : 1533 | print(json.dumps(res.payload, indent=4)) 1534 | else: 1535 | print(f"{res.payload['response']}") 1536 | 1537 | case _ : 1538 | if cmd[0] == "@" : 1539 | res = await mc.commands.send_cli(cmd[1:]) 1540 | logger.debug(res) 1541 | if res.type == EventType.ERROR: 1542 | print(f"Error sending cli cmd: {res}") 1543 | elif json_output : 1544 | print(json.dumps(res.payload, indent=4)) 1545 | else: 1546 | print(f"{res.payload['response']}") 1547 | 1548 | else : 1549 | await mc.ensure_contacts() 1550 | contact = mc.get_contact_by_name(cmds[0]) 1551 | if contact is None: 1552 | logger.error(f"Unknown command : {cmd}, will exit ...") 1553 | return None 1554 | 1555 | await interactive_loop(mc, to=contact) 1556 | 1557 | logger.debug(f"cmd {cmds[0:argnum+1]} processed ...") 1558 | return cmds[argnum+1:] 1559 | 1560 | except IndexError: 1561 | logger.error("Error in parameters, returning") 1562 | return None 1563 | 1564 | async def process_cmds (mc, args, json_output=False) : 1565 | cmds = args 1566 | while cmds and len(cmds) > 0 and cmds[0][0] != '#' : 1567 | cmds = await next_cmd(mc, cmds, json_output) 1568 | 1569 | async def process_script(mc, file, json_output=False): 1570 | if not os.path.exists(file) : 1571 | logger.info(f"file {file} not found") 1572 | if json_output : 1573 | print(json.dumps({"error" : f"file {file} not found"})) 1574 | return 1575 | 1576 | with open(file, "r") as f : 1577 | lines=f.readlines() 1578 | 1579 | for line in lines: 1580 | logger.debug(f"processing {line}") 1581 | cmds = shlex.split(line[:-1]) 1582 | await process_cmds(mc, cmds, json_output) 1583 | 1584 | def version(): 1585 | print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}") 1586 | 1587 | def command_help(): 1588 | print(""" General commands 1589 | chat : enter the chat (interactive) mode 1590 | chat_to : enter chat with contact to 1591 | script : execute commands in filename 1592 | infos : print informations about the node i 1593 | self_telemetry : print own telemtry t 1594 | card : export this node URI e 1595 | ver : firmware version v 1596 | reboot : reboots node 1597 | sleep : sleeps for a given amount of secs s 1598 | wait_key : wait until user presses wk 1599 | Messenging 1600 | msg : send message to node by name m { 1601 | wait_ack : wait an ack wa } 1602 | chan : send message to channel number ch 1603 | public : send message to public channel (0) dch 1604 | recv : reads next msg r 1605 | wait_msg : wait for a message and read it wm 1606 | sync_msgs : gets all unread msgs from the node sm 1607 | msgs_subscribe : display msgs as they arrive ms 1608 | get_channel : get info for channel n 1609 | set_channel n nm k : set channel info (nb, name, key) 1610 | Management 1611 | advert : sends advert a 1612 | floodadv : flood advert 1613 | get : gets a param, \"get help\" for more 1614 | set : sets a param, \"set help\" for more 1615 | time : sets time to given epoch 1616 | clock : get current time 1617 | clock sync : sync device clock st 1618 | cli : send a cmd to node's cli (if avail) @ 1619 | Contacts 1620 | contacts / list : gets contact list lc 1621 | share_contact : share a contact with others sc 1622 | export_contact : get a contact's URI ec 1623 | import_contact : import a contactt from its URI ic 1624 | remove_contact : removes a contact from this node 1625 | reset_path : resets path to a contact to flood rp 1626 | change_path : change the path to a contact cp 1627 | change_flags : change contact flags (tel_l|tel_a|star)cf 1628 | req_telemetry : prints telemetry data as json rt 1629 | Repeaters 1630 | login : log into a node (rep) with given pwd l 1631 | logout : log out of a repeater 1632 | cmd : sends a command to a repeater (no ack) c [ 1633 | wmt8 : wait for a msg (reply) with a timeout ] 1634 | req_status : requests status from a node rs""") 1635 | 1636 | def usage () : 1637 | """ Prints some help """ 1638 | version() 1639 | print(""" 1640 | Usage : meshcore-cli 1641 | 1642 | Arguments : 1643 | -h : prints this help 1644 | -v : prints version 1645 | -j : json output (disables init file) 1646 | -D : debug 1647 | -S : performs a ble scan and ask for device 1648 | -l : list available ble devices and exit 1649 | -T : timeout for the ble scan (-S and -l) default 2s 1650 | -a
: specifies device address (can be a name) 1651 | -d : filter meshcore devices with name or address 1652 | -t : connects via tcp/ip 1653 | -p : specifies tcp port (default 5000) 1654 | -s : use serial port 1655 | -b : specify baudrate 1656 | 1657 | Available Commands and shorcuts (can be chained) :""") 1658 | command_help() 1659 | 1660 | async def main(argv): 1661 | """ Do the job """ 1662 | json_output = JSON 1663 | debug = False 1664 | address = ADDRESS 1665 | port = 5000 1666 | hostname = None 1667 | serial_port = None 1668 | baudrate = 115200 1669 | timeout = 2 1670 | # If there is an address in config file, use it by default 1671 | # unless an arg is explicitely given 1672 | if os.path.exists(MCCLI_ADDRESS) : 1673 | with open(MCCLI_ADDRESS, encoding="utf-8") as f : 1674 | address = f.readline().strip() 1675 | 1676 | opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:jDhvSlT:") 1677 | for opt, arg in opts : 1678 | match opt: 1679 | case "-d" : # name specified on cmdline 1680 | address = arg 1681 | case "-a" : # address specified on cmdline 1682 | address = arg 1683 | case "-s" : # serial port 1684 | serial_port = arg 1685 | case "-b" : 1686 | baudrate = int(arg) 1687 | case "-t" : 1688 | hostname = arg 1689 | case "-p" : 1690 | port = int(arg) 1691 | case "-j" : 1692 | json_output=True 1693 | handle_message.json_output=True 1694 | case "-D" : 1695 | debug=True 1696 | case "-h" : 1697 | usage() 1698 | return 1699 | case "-T" : 1700 | timeout = float(arg) 1701 | case "-v": 1702 | version() 1703 | return 1704 | case "-l" : 1705 | devices = await BleakScanner.discover(timeout=timeout) 1706 | if len(devices) == 0: 1707 | logger.error("No ble device found") 1708 | for d in devices : 1709 | if not d.name is None and d.name.startswith("MeshCore-"): 1710 | print(f"{d.address} {d.name}") 1711 | return 1712 | case "-S" : 1713 | devices = await BleakScanner.discover(timeout=timeout) 1714 | choices = [] 1715 | for d in devices: 1716 | if not d.name is None and d.name.startswith("MeshCore-"): 1717 | choices.append((d.address, f"{d.address} {d.name}")) 1718 | if len(choices) == 0: 1719 | logger.error("No BLE device found, exiting") 1720 | return 1721 | 1722 | result = await radiolist_dialog( 1723 | title="MeshCore-cli BLE device selector", 1724 | text="Chose the device to connect to :", 1725 | values=choices 1726 | ).run_async() 1727 | 1728 | if result is None: 1729 | logger.info("No choice made, exiting") 1730 | return 1731 | 1732 | address = result 1733 | 1734 | if (debug==True): 1735 | logger.setLevel(logging.DEBUG) 1736 | elif (json_output) : 1737 | logger.setLevel(logging.ERROR) 1738 | 1739 | con = None 1740 | if not hostname is None : # connect via tcp 1741 | con = TCPConnection(hostname, port) 1742 | await con.connect() 1743 | elif not serial_port is None : # connect via serial port 1744 | con = SerialConnection(serial_port, baudrate) 1745 | await con.connect() 1746 | await asyncio.sleep(0.2) 1747 | else : #connect via ble 1748 | con = BLEConnection(address) 1749 | address = await con.connect() 1750 | if address is None or address == "" : # no device, no action 1751 | logger.error("No device found, exiting ...") 1752 | return 1753 | 1754 | # Store device address in configuration 1755 | if os.path.isdir(MCCLI_CONFIG_DIR) : 1756 | with open(MCCLI_ADDRESS, "w", encoding="utf-8") as f : 1757 | f.write(address) 1758 | 1759 | mc = MeshCore(con, debug=debug) 1760 | await mc.connect() 1761 | handle_message.mc = mc # connect meshcore to handle_message 1762 | 1763 | res = await mc.commands.send_device_query() 1764 | if res.type == EventType.ERROR : 1765 | logger.error(f"Error while querying device: {res}") 1766 | return 1767 | 1768 | if (json_output) : 1769 | logger.setLevel(logging.ERROR) 1770 | else : 1771 | if res.payload["fw ver"] > 2 : 1772 | logger.info(f"Connected to {mc.self_info['name']} running on a {res.payload['ver']} fw.") 1773 | else : 1774 | logger.info(f"Connected to {mc.self_info['name']}.") 1775 | 1776 | if os.path.exists(MCCLI_INIT_SCRIPT) and not json_output : 1777 | logger.debug(f"Executing init script : {MCCLI_INIT_SCRIPT}") 1778 | await process_script(mc, MCCLI_INIT_SCRIPT, json_output) 1779 | 1780 | if len(args) == 0 : # no args, run in chat mode 1781 | await process_cmds(mc, ["chat"], json_output) 1782 | else: 1783 | await process_cmds(mc, args, json_output) 1784 | 1785 | def cli(): 1786 | try: 1787 | asyncio.run(main(sys.argv[1:])) 1788 | except KeyboardInterrupt: 1789 | # This prevents the KeyboardInterrupt traceback from being shown 1790 | print("\nExited cleanly") 1791 | except Exception as e: 1792 | print(f"Error: {e}") 1793 | 1794 | if __name__ == '__main__': 1795 | cli() 1796 | --------------------------------------------------------------------------------