Skip to main content

Meshtastic live logger with an interactive command prompt

Project description

mushtastic

Meshtastic live logger with an interactive command prompt.

mushtastic (installed as the mush console command) connects to a Meshtastic radio (over USB/serial, TCP, or BLE), archives every packet it sees into a local SQLite database, and drops you into an interactive REPL where you can inspect nodes, browse logged packets, send messages, manage channels, and traceroute other nodes while the logger keeps running in the background.

Features

  • Live packet logging to SQLite (peewee ORM) - nothing is lost between runs.
  • Interactive REPL (built directly on prompt-toolkit) with command history, /command autocompletion, and a live status toolbar.
  • Chat-style messaging: select a node or channel as the current target with /target, then anything typed without a leading / is sent to it as a text message. Prefix a single line with @ENTITY to send just that message elsewhere without changing the current target.
  • Node database: track short/long names, hardware info, position, favorites, device/environment telemetry, and last-heard times.
  • Packet database: filter, replay/reprocess, import, and inspect raw packets already logged.
  • Channel management: list, add, remove, and move channels directly from the REPL.
  • Send text messages, traceroutes, and position requests to nodes by short name or node ID.

Requirements

  • Python >= 3.10 (the checked-in .envrc uses pyenv with Python 3.13.5 via direnv).
  • A Meshtastic-capable radio, reachable via USB serial, TCP (e.g. Wi-Fi/ESP32 nodes), or BLE.

Install

pip install -e .

This installs the mush console script (see [project.scripts] in pyproject.toml) backed by the mushtastic package in src/mushtastic.

If you use direnv, entering the project directory will automatically activate the pyenv-managed virtualenv defined in .envrc.

Usage

Show top-level help:

mush --help

Global options (before the subcommand)

Option Description
-d, --debug Enable device debug logs from startup.
--database PATH Explicit path to the SQLite database file.
--pos-fmt FORMAT Position display format: latlon (default), google, or osm link.

Without --database, the DB defaults to mush.db in the current working directory.

Starting a live session

mush live                 # auto-detect a USB-connected radio
mush live --dev /dev/ttyUSB0
mush live --tcp 192.168.1.50:4403
mush live --ble AA:BB:CC:DD:EE:FF

Only one of --ble, --tcp, --dev may be given; without any of them mush tries to auto-connect over serial. Once connected, mush drops into a REPL (prompt shows ๐Ÿฐ<hops> <shortname>@<target>>, where <hops> is the radio's currently configured default hop limit - see /hops - <target> is colored magenta for a channel or yellow for a node) and starts archiving every received packet to the database. Press Ctrl+D to exit the REPL (closes the radio interface cleanly).

Inside the REPL, commands start with / (e.g. /node ls); typing / and pressing Tab shows a completion menu of available commands. A line typed without a leading / is sent as a text message to the currently selected target instead of being treated as a command - select a target (a node or a channel) with /target:

/target offers a popup completion menu for matching known channels and nodes. Type the beginning of a channel name, node short name, channel index, or node ID and press Tab to select it.

you@primary> /target !deadbeef  # target a node by ID
you@primary> /target JOHN       # target a node by short name
you@primary> /target #1         # target channel 1
you@primary> /target admin      # target a channel by name
you@JOHN> Hey, are you there?   # sent to JOHN, no leading '/' needed

If ENTITY (the argument to /target) starts with ! it's treated as a node ID; if it starts with # it's a channel number (0-7); otherwise it's looked up by name - a channel name takes priority over a node short name if both match. Without ever running /target, the target defaults to the primary channel, matching the previous behavior.

To send a single one-off message somewhere else without changing the current /target target, start the line with @ENTITY instead (ENTITY is resolved exactly like the /target argument):

you@JOHN> @LongFast hello everyone!   # sent to the "LongFast" channel
you@JOHN> @!deadbeef who are you?     # sent to a node by ID
you@JOHN> @#1 need assistance         # sent to channel 1
you@JOHN> @admin sup bro!             # sent to a channel or node by name
you@JOHN> Still talking to JOHN here  # /target (JOHN) is unaffected

Either message form (a bare line, or one starting with @ENTITY) can override the hop count for that single message with #N (N from 0 to 7), and it doesn't matter whether #N comes before or after @ENTITY:

you@JOHN> #3 hello there        # sent to JOHN with hop limit 3
you@JOHN> @admin #2 need help   # sent to "admin" with hop limit 2
you@JOHN> #2 @admin need help   # same as above - order doesn't matter

Without a #N prefix, messages use the radio's own configured hop limit, which can be read or changed with /hops:

you@primary> /hops     # print the radio's currently configured hop limit
you@primary> /hops 3   # set it to 3

With --ble, the underlying meshtastic library can occasionally deadlock while disconnecting (meshtastic/python#792, still open as of meshtastic 2.7.10). mush bounds this with a 5-second timeout, so a Ctrl+D exit will print a warning and continue exiting instead of hanging forever if that happens.

Command reference

The shell CLI and interactive REPL have separate command trees. The shell tree starts sessions and queries local data; radio operations are available after mush live in the REPL. Use mush <command> --help or /<command> --help for the authoritative syntax.

REPL commands

Inside live, prefix commands with /.

  • help - print Click's --help for the whole app inside the REPL.
  • debug (d) - toggle verbose device debug logging.
  • target <ENTITY> - select a node or channel as the current target (see above); subsequent lines typed without a leading / are sent to it. (Not a /-prefixed line, but also worth noting here: a line starting with @ENTITY sends just that one message elsewhere without changing the current target, and either message form can start its message text with #N to override the hop count for that message - see above.)
  • hops [COUNT] - get (no argument) or set (COUNT, 0-7) the radio's default hop limit, used for any message that doesn't override it with a #N prefix.
  • trace <NODE_ID|SHORT_NAME> [HOP_LIMIT] (t, traceroute) - send a traceroute request; doesn't block, the response (if any) is logged like any other received packet.
  • location <NODE_ID|SHORT_NAME> (loc) (REPL only) - request the current position from a node; doesn't block, the response (if any) is logged like any other received packet, same as with /trace - since the response is only useful to see live, there's no shell equivalent.
  • conf (config) - inspect and edit writable radio configuration: conf list, conf get OPTION, conf set OPTION VALUE, and conf apply. set validates and stages changes locally; apply writes all staged changes to the radio. Repeated values must be JSON arrays, such as [1, 2].
  • node (n, nodes) - manage the known-nodes table:
    • node (no args) - show info about our own node.
    • node ls (l, list) - list all known nodes.
    • node show <NAME> (s, i, info) - show details for one node.
    • node rm <ID> - delete a node from the local DB.
    • node fav <NAME> - toggle favorite status (synced to the device).
    • node dls - dump the radio's live in-memory NodeDB as JSON.
    • node pull - copy the radio's live NodeDB into the local database.
    • node count (c) - print the total number of nodes in the local DB.
  • channel (c, chan) - manage Meshtastic channels:
    • channel / channel ls (l, list) - list channels.
    • channel add <INDEX> <NAME> [PSK] [--random-psk] - add/overwrite a channel; --random-psk generates and displays a random 32-byte PSK. (PSK defaults to the default channel key; use base64:... for a custom key).
    • channel rm <INDEX> - delete a channel (not the primary one).
    • channel mv <INDEX1> <INDEX2> - exchange two secondary channel slots.
  • filter controls which packets are shown live and returned by REPL packet commands. filter-out hides matching packets. Each provides clear, from [NODE...], to [NODE...], channel [CHANNEL...], and portnum [PORTNUM...]. No values on a field command clears that field and prints its prior value. Within a field, values are alternatives; across fields, inclusion filters are combined. An exclusion is applied only when every active exclusion field matches. Filters never stop packet persistence.
  • packet (p, pkt, packets) - query the local packet log:
    • packet ls [N] (l, list) - print the filtered packets (in the REPL, N defaults to 100 if omitted).
    • packet show [ID] [--raw] (s) - show one packet in detail (or the filtered set); --raw prints the raw JSON blob.
    • packet process - reprocess the filtered packets (re-derive node state such as last-heard, telemetry, position from stored packets).
    • packet reprocess - reprocess every stored packet from its raw data, ignoring active filters, with a progress bar.
    • packet import [--merge=skip|overwrite|append] - read a JSON packet blob from stdin and insert or merge it into the database.
    • packet portnums (p) - show a count of stored packets per portnum.
    • packet count (c) - print the count of packets in the filtered set.

Shell commands

  • mush ble-scan discovers nearby Meshtastic BLE devices without connecting.
  • mush node ls [LIMIT], show NAME, rm ID, and count query the local known-node database.
  • mush packet accepts one-shot filters such as --portnum, --from-id, --to-id, packet-ID ranges, and receive-time ranges. Its list, show, process, import, portnums, and count subcommands operate on that result. For example: mush packet --portnum POSITION_APP list 20.

Example:

mush live --dev /dev/ttyUSB0
you@primary> /node ls
you@primary> /target BASE
you@BASE> Hello from the field!
you@BASE> /filter portnum POSITION_APP
you@BASE> /packet ls 20
you@BASE> /target #1
you@admin> /help

Data storage

  • SQLite database: ./mush.db in the current working directory by default, or the path passed to --database. Contains three tables: node, packet, and replhistory.
  • REPL command history: timestamped rows in the active SQLite database.
  • Startup creates missing tables and adds the packet.channel column to databases created before channel logging was added.

The database is ignored by git (see .gitignore, patterns *.mush and *.db), so it is local/runtime state, not project files. It can contain node identities, packet contents, telemetry, and locations. Treat it as sensitive data, and copy the SQLite file while mush is not running if you need a backup or wish to move the history to another machine.

Development

Project layout

mushtastic/
โ”œโ”€โ”€ pyproject.toml            # packaging metadata, deps, entry point (mush = mushtastic.app:cli)
โ”œโ”€โ”€ .envrc                     # direnv/pyenv Python version pin
โ””โ”€โ”€ src/
    โ””โ”€โ”€ mushtastic/
        โ”œโ”€โ”€ __init__.py        # re-exports `cli` from app.py
        โ”œโ”€โ”€ __main__.py        # allows `python -m mushtastic`
        โ”œโ”€โ”€ app.py             # assembles the full app (imports cli.py + commands/ + repl_commands/)
        โ”œโ”€โ”€ cli.py             # shell root Click group + top-level commands (live, ble-scan, help)
        โ”œโ”€โ”€ commands/                    # SHELL (`mush <command>`) command groups
        โ”‚   โ”œโ”€โ”€ __init__.py    # imports node/packet for their registration side-effect
        โ”‚   โ”œโ”€โ”€ node.py        # `node` command group
        โ”‚   โ””โ”€โ”€ packet.py      # `packet` command group
        โ”œโ”€โ”€ repl_root.py        # root click group for the REPL-only ('/command') tree
        โ”œโ”€โ”€ repl_commands/               # REPL (`/command`) command groups - independent of commands/
        โ”‚   โ”œโ”€โ”€ __init__.py    # imports top/node/channel/packet for their registration side-effect
        โ”‚   โ”œโ”€โ”€ top.py          # REPL versions of help/debug/trace/location/target/hops/conf (no REPL /msg)
        โ”‚   โ”œโ”€โ”€ node.py         # REPL `node` command group
        โ”‚   โ”œโ”€โ”€ channel.py      # REPL `channel` command group
        โ”‚   โ””โ”€โ”€ packet.py       # REPL `packet` command group
        โ”œโ”€โ”€ services/                    # shared business logic (no Click deps) used by both trees
        โ”‚   โ”œโ”€โ”€ __init__.py
        โ”‚   โ”œโ”€โ”€ messaging.py    # trace/location/target/hops/conf/debug + entity resolution/send helpers (used by shell msg + @ENTITY & current-target sends)
        โ”‚   โ”œโ”€โ”€ node.py          # node group logic
        โ”‚   โ”œโ”€โ”€ channel.py       # channel group logic
        โ”‚   โ””โ”€โ”€ packet.py        # packet group logic
        โ”œโ”€โ”€ models.py          # Peewee models: ReplHistory, Node, Packet (+ Packet.process())
        โ”œโ”€โ”€ history.py         # prompt-toolkit history backed by ReplHistory
        โ”œโ”€โ”€ radio.py           # channel/target helpers, we_are_live()/wait_for_config(), pubsub callbacks
        โ”œโ”€โ”€ repl.py             # custom REPL loop + /command dispatch (replaces click-repl)
        โ”œโ”€โ”€ prompt.py           # REPL prompt message + bottom toolbar
        โ”œโ”€โ”€ decorators.py       # @requires_radio
        โ”œโ”€โ”€ state.py            # State singleton (iface, debug flag, ...) + the peewee db Proxy
        โ”œโ”€โ”€ formatting.py       # pprint() + pure formatting helpers (dates, positions, telemetry, ...)
        โ””โ”€โ”€ colors.py           # `clr` - ANSI color/style constants

The application used to live entirely in one ~1400-line app.py; it has since been split by responsibility as above, and then split again into two independent Click command trees (shell vs. REPL) sharing one business-logic layer. Module dependencies flow one way, with no cycles:

colors.py, state.py, formatting.py       (no internal deps)
        โ†“
     models.py                            (state, formatting, colors)
        โ†“
     radio.py                             (+ models, formatting, state)
        โ†“
  decorators.py, prompt.py                (+ radio, formatting, state, models)
        โ†“
     services/*.py                         (+ decorators, radio, models, state,
                                              formatting, colors - no Click, no
                                              knowledge of either command tree)
        โ†“                    โ†“
      cli.py            repl_root.py + repl.py   (each independently: + services,
        โ†“                    โ†“                     colors, formatting, prompt)
   commands/*.py       repl_commands/*.py         (+ their own root group, to attach
   (+ cli.py)           (+ repl_root.py)            subgroups; + services/*.py)
        โ†“                    โ†“
              app.py   (imports cli.py + commands/ + repl_commands/, re-exports `cli`)

Key pieces:

  • state.py - a single State instance (imported as from .state import state) holds all cross-command mutable runtime state: the active Meshtastic interface (state.iface), our own node ID (state.our_node_id), the debug flag, the current /target target (target_kind, selected_channel_index, target_node_id), the last-seen telemetry for our own node, and the position display format. The db = peewee.Proxy() handle used by every model also lives here. Read/write it as state.<attr> instead of module-level globals.
  • models.py - the peewee models:
    • ReplHistory - timestamped commands entered in the interactive REPL.
    • Node - one row per known Meshtastic node (by numeric node ID). Has .find(name_or_id) (resolves !hex, ^local, ^all, or a unique short name) and a rich __str__ used by node ls/node show.
    • Packet - one row per logged packet, storing the raw JSON blob plus a few indexed columns (msg_id, rx_time, from_id, to_id, portnum) for fast filtering. .process() parses the blob, updates the related Node (last-heard, telemetry, position, node-info), and is what both live packet reception and packet process/packet import call into.
  • radio.py - channel introspection/management helpers (get_channel_node, get_current_channel_index, parse_psk_value, find_channel_by_name, etc.), connection state (we_are_live(), wait_for_config()), the current /target target (set_target_channel, set_target_node), and the pubsub callbacks (on_receive, on_log) that turn incoming Meshtastic packets into Packet rows via .process(log=True).
  • decorators.py - the @requires_radio decorator, which guards commands that need a live radio connection (checked via we_are_live()).
  • prompt.py - the REPL prompt (PromptMessage, shows !<node-id> <shortname> @ <target>>; local and node targets use the Android-derived node-ID color, channel targets use bold bright white, and @ and > are gray) and bottom-toolbar (toolbar()) helpers.
  • repl.py - the interactive loop and /command dispatcher that replaced click-repl: run_repl() builds one click.Context for repl_root per REPL session, reads lines via a prompt_toolkit PromptSession, routes lines starting with / through dispatch_command() (which resolves and invokes the matching command directly against that repl_root context - see the module docstring), routes lines starting with @ENTITY through services.messaging.send_to_entity() (a one-off message to ENTITY, resolved the same way as /target, that doesn't touch the current target), and sends anything else as a text message to the current /target target via services.messaging.send_current_target(). Either message form's text is first checked by _extract_hop_prefix() for a leading #N hop-count override (see above), which is passed through as hops= to whichever send function handles the line. SlashCommandCompleter drives the /command completion dropdown: subcommand names at any nesting level, plus (once the current word starts with -) that command/group's own --option flag names, plus (once a flag name is finished with =) the values of a click.Choice-typed option (e.g. --merge= -> append/overwrite/ skip).
  • repl_root.py - the root click.group for the REPL-only tree (repl_root, also a ClickAliasedGroup). Its own callback is a no-op and is never actually invoked by dispatch_command() (only .get_command()/.list_commands() are used on it) - unlike cli(), there's no argv-driven setup to guard here.
  • formatting.py / colors.py - state-free helpers: pprint() (prints through prompt_toolkit so it plays nicely with the REPL's live input line), formatting helpers (format_position, format_dev_metrics, format_env_metrics, split_seconds, clock_emoji, etc.) used by model __str__ methods, and the clr class of raw ANSI codes used for coloring (no colorama dependency).
  • services/ - the shared business-logic layer, with no Click dependency and no awareness of either command tree:
    • messaging.py - do_trace/do_location/do_target/do_hops/ do_conf_list/do_conf_get/do_conf_set/do_conf_apply/ do_debug_toggle, log_sent_proto(), entity resolution, send_to_entity() for REPL @ENTITY lines, and send_current_target() for ordinary REPL message lines. do_hops reads/writes the radio's localConfig.lora.hop_limit directly (via localNode.writeConfig("lora")), which is what the meshtastic library falls back to whenever a send call's hopLimit is None.
    • node.py / channel.py / packet.py - the node and packet services are shared by shell and REPL wrappers; channel operations are REPL-only. @requires_radio (see below) is applied to shared functions that need a radio rather than to a Click wrapper.
  • cli.py - the shell root click.group (cli, using click_aliases.ClickAliasedGroup for short aliases), which sets up the DB connection/tables on startup (guarded by state.initialized so it only runs once per process - see the comment on cli()), plus the shell's own top-level commands: help, ble-scan, and live. ble-scan discovers nearby Meshtastic BLE devices without connecting. live (shell-only, see below) opens the Meshtastic interface (BLE/TCP/serial), subscribes to the pubsub topics, and starts repl.run_repl(). All radio-dependent operations are deliberately REPL-only.
  • commands/ - the shell side's offline node and packet groups; each attaches itself to cli (imported from mushtastic.cli) via @cli.group(...). mushtastic/commands/__init__.py imports both purely for that registration side effect.
  • repl_commands/ - the REPL side's top (help/debug/trace/ location/target/hops/conf), node, channel, and packet command definitions; each attaches itself to repl_root (imported from mushtastic.repl_root) instead of cli, and wraps the exact same services.* functions as its shell-side counterpart in cli.py/commands/ where one exists, but with its own independent Click arguments/options/aliases/docstrings. mushtastic/repl_commands/__init__.py imports all four purely for that registration side effect. There is deliberately no REPL counterpart for live (see below).
  • app.py - the thin assembler: imports cli from cli.py, imports commands (registering the shell's two subgroups) and repl_commands (registering the REPL's four command modules), and re-exports cli for the mush = mushtastic.app:cli entry point in pyproject.toml and for __init__.py/__main__.py.

Architecture notes

  • Single source of truth for packets: every packet, whether received live, sent locally, imported, or replayed, flows through Packet.process(). If you add a new derived field or need to react to a new portnum, that's the place to do it.
  • Two independent command trees, one shared business-logic layer: the shell (mushtastic.cli.cli, populated by mushtastic.commands.*) and the REPL (mushtastic.repl_root.repl_root, populated by mushtastic.repl_commands.*) are two entirely separate click.Group trees with their own Click command/group objects - separate arguments, options, aliases, help text, and tab-completion behavior. Editing one side's argument shape (or adding a new option, or tuning its completion) never touches the other. Both sides call into the same plain-function layer in mushtastic.services.* for actual behavior, so a bug fix or feature there benefits both automatically - only the Click-level plumbing is duplicated, not the logic itself. There are several deliberate exceptions where a command only exists on one side, rather than a blocklist bolted onto a shared tree - there's simply no Click command definition for it on the side that doesn't have it:
    • live only exists on the shell side (cli.py) - the radio is already live by the time the REPL prompt is running, so re-entering it from inside itself makes no sense. There's no do_live in services either.
    • location and target only exist on the REPL side (repl_commands/top.py) - target sets a REPL-only prompt target that doesn't persist across separate one-shot mush invocations, and location's response arrives asynchronously and is just logged, which is only useful to see live.
  • Adding a command requires deliberate tree selection: since the trees do not share Click objects, add a wrapper to the shell, REPL, or both as appropriate. Put behavior shared by multiple wrappers in services/*.py.
  • Commands vs. messages in the REPL: inside live, a line starting with / is always dispatched as a command against repl_root; anything else is sent as a text message to the current /target target (services.messaging.send_current_target()). There is intentionally no escaping mechanism for sending a literal message that starts with /.
  • requires_radio: use this decorator (from mushtastic.decorators) on the shared services.* function (not on either side's Click wrapper) for any command that talks to state.iface directly (sending messages, managing channels, etc.), so it fails fast with a clear message when invoked outside of live, on both sides at once.
  • Shared mutable state lives in one place: rather than module-level globals scattered across files (which don't work across module boundaries without extra plumbing), all cross-cutting runtime state is on the single state object from state.py. Import it (from .state import state) and read/write state.<attr> directly - no global declarations needed.
  • Output: always print through pprint() (wraps prompt_toolkit.print_formatted_text) rather than bare print() inside REPL-facing commands, so output doesn't clobber the active prompt line.
  • No circular imports: dependencies flow strictly downward through services/*.py and then out to the two independent trees (see the module dependency diagram above) - services/*.py never imports cli.py, commands/*.py, repl.py, repl_root.py, or repl_commands/*.py. Keep new code following that direction: if logic is needed by more than one command (on either side), it belongs in services/*.py (or, for lower-level/non-command logic, models.py/radio.py).

Adding a new command

  1. Put the actual behavior in services/*.py as a plain function (messaging.py for a new top-level command, or node.py/ channel.py/packet.py for a new subcommand of one of those groups; for a whole new command group, add a new services/<name>.py). Decorate it with @requires_radio (from mushtastic.decorators) if it needs state.iface. Use Node.find(name) / Packet queries (from mushtastic.models) for lookups, and pprint() (from mushtastic.formatting) for all output.
  2. Add a shell-side Click wrapper only if the command works without a live radio: top-level commands go in cli.py (@cli.command(...)); node/packet subcommands go in the matching file under commands/. For a whole new group, register it from commands/__init__.py. The wrapper's body should just call the service function.
  3. Add a REPL-side Click wrapper for interactive or radio operations, attached to repl_root (from ..repl_root import repl_root) in the matching file under repl_commands/ instead - top.py for top-level commands, or node.py/channel.py/packet.py (new group: repl_commands/<name>.py, imported from repl_commands/__init__.py). It's fine (expected, even) for this wrapper's arguments/options/ aliases/docstring to diverge from the shell one over time; it should still just call the same services function.
    • Follow the existing style: docstring with a Usage: /command ... section on the REPL side (Usage: mush command ... on the shell side), aliases=[...] for short forms.
  4. If you touch packet parsing, update Packet.process() and/or Packet.__str__() (for the colorized log line) in models.py consistently.
  5. Reinstall in editable mode if needed (pip install -e .), run the unit tests, and exercise the command in every command tree where it exists.

Testing

Run the unit test suite with:

python -m unittest discover -v

Tests use mocked Meshtastic interfaces and cover database persistence, packet filtering, command registration and completion, configuration staging, channel management, and serial reconnection. Changes involving the radio API should also be manually verified against a device or recorded packet input.

Build artifacts

build/, *.egg-info/, .direnv/, and local runtime files (*.mush, *.db) are git-ignored; don't commit them.

Dependency notes

  • click-repl was removed: the interactive loop is now hand-rolled in repl.py directly on top of prompt-toolkit (already a dependency for the rest of the REPL UI), so there's one fewer moving part and no more click-repl-driven upper bound on click.
  • Dependencies use minimum versions in pyproject.toml. Test compatibility when raising a minimum version or adding a new dependency.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mushtastic-0.1.0.tar.gz (66.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mushtastic-0.1.0-py3-none-any.whl (57.0 kB view details)

Uploaded Python 3

File details

Details for the file mushtastic-0.1.0.tar.gz.

File metadata

  • Download URL: mushtastic-0.1.0.tar.gz
  • Upload date:
  • Size: 66.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for mushtastic-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f61ed0c5ffd3705f61747dd338e20d7ae2c9b1f7e024cc93aa63e05880b45b48
MD5 8547806fc47e038fe8ac608d6a3ad2b0
BLAKE2b-256 f5fbbe6d419455cd74890adbf2e86ba527d8a45382b2803378b6f7c0f8f9b5fb

See more details on using hashes here.

File details

Details for the file mushtastic-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: mushtastic-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 57.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for mushtastic-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5b0fb5583a33829b5f7d7d26b8330215f2c8237970611c861088fa8bac62eae9
MD5 5662fc4f3058a4fcef4acc18c09b13dc
BLAKE2b-256 b545f93f5003320b052f1a93f62a35eebf1b670d2464ee7ef3b4b87cd5f580c7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page