mqtty 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ runs-on: ubuntu-latest
10
+
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+
15
+ steps:
16
+ - name: Check out the repository
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: '3.13'
23
+
24
+ - name: Install build dependencies
25
+ run: python -m pip install --upgrade build
26
+
27
+ - name: Build sdist and wheel
28
+ run: python -m build
29
+
30
+ - name: Publish to PyPI
31
+ uses: pypa/gh-action-pypi-publish@release/v1
mqtty-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025, Mark Featherston
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
+
mqtty-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: mqtty
3
+ Version: 0.1.0
4
+ Summary: Bridges MQTT topics to a pseudo-terminal device
5
+ Author-email: Mark Featherston <mark@embeddedTS.com>
6
+ Project-URL: homepage, https://github.com/embeddedts/mqtty
7
+ Project-URL: bugs, https://github.com/embeddedts/mqtty/issues
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: paho-mqtt
11
+ Dynamic: license-file
12
+
13
+ # mqtty
14
+
15
+ `mqtty` bridges an MQTT topic to a local pseudoterminal (PTY), allowing you to interact with serial ports over MQTT as if they were local TTY devices.
16
+
17
+ This is useful for debugging, automation, or tunneling serial communication through a networked MQTT broker.
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ mqtty mqtt://<host>/<topic> [--pts-only | --wrap-picocom]
23
+ ```
24
+
25
+ You must choose one of the two modes:
26
+
27
+ * `--pts-only`, `-p`:
28
+ Print the path to the PTS device and keep it open. Useful for tools or scripts that want to use the virtual serial port.
29
+
30
+ * `--wrap-picocom`, `-w`:
31
+ Launch `picocom --quiet` attached to the PTS device. This allows interactive use of a remote serial port over MQTT.
32
+
33
+ The `<topic>` segment of the URI is used as the base path for MQTT messages:
34
+
35
+ * `.../device_serial_input` receives data from your local PTY (sent to the device)
36
+ * `.../device_serial_output` sends incoming data from the device to your PTY
37
+
38
+ ## Example
39
+
40
+ ```bash
41
+ mqtty mqtt://broker.local/mydevice --wrap-picocom
42
+ ```
43
+
44
+ This opens a local PTS device, bridges it to `mydevice/device_serial_input` and `mydevice/device_serial_output`, and launches `picocom` on the virtual port.
45
+
46
+ ## License
47
+
48
+ MIT
mqtty-0.1.0/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # mqtty
2
+
3
+ `mqtty` bridges an MQTT topic to a local pseudoterminal (PTY), allowing you to interact with serial ports over MQTT as if they were local TTY devices.
4
+
5
+ This is useful for debugging, automation, or tunneling serial communication through a networked MQTT broker.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ mqtty mqtt://<host>/<topic> [--pts-only | --wrap-picocom]
11
+ ```
12
+
13
+ You must choose one of the two modes:
14
+
15
+ * `--pts-only`, `-p`:
16
+ Print the path to the PTS device and keep it open. Useful for tools or scripts that want to use the virtual serial port.
17
+
18
+ * `--wrap-picocom`, `-w`:
19
+ Launch `picocom --quiet` attached to the PTS device. This allows interactive use of a remote serial port over MQTT.
20
+
21
+ The `<topic>` segment of the URI is used as the base path for MQTT messages:
22
+
23
+ * `.../device_serial_input` receives data from your local PTY (sent to the device)
24
+ * `.../device_serial_output` sends incoming data from the device to your PTY
25
+
26
+ ## Example
27
+
28
+ ```bash
29
+ mqtty mqtt://broker.local/mydevice --wrap-picocom
30
+ ```
31
+
32
+ This opens a local PTS device, bridges it to `mydevice/device_serial_input` and `mydevice/device_serial_output`, and launches `picocom` on the virtual port.
33
+
34
+ ## License
35
+
36
+ MIT
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "setuptools-scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mqtty"
7
+ description = "Bridges MQTT topics to a pseudo-terminal device"
8
+ readme = "README.md"
9
+ authors = [
10
+ {name = "Mark Featherston", email = "mark@embeddedTS.com"}
11
+ ]
12
+ dependencies = [
13
+ "paho-mqtt"
14
+ ]
15
+
16
+ dynamic = ["version"]
17
+
18
+ [tool.setuptools_scm]
19
+ version_scheme = "guess-next-dev"
20
+ local_scheme = "node-and-date"
21
+
22
+ [project.urls]
23
+ homepage = "https://github.com/embeddedts/mqtty"
24
+ bugs = "https://github.com/embeddedts/mqtty/issues"
25
+
26
+ [project.scripts]
27
+ mqtty = "mqtty.mqtty:main"
28
+
29
+ [tool.mypy]
30
+ files = "."
31
+ # Use strict defaults
32
+ strict = true
33
+ warn_unreachable = true
34
+ warn_no_return = true
35
+
36
+ [tool.ruff]
37
+ line-length = 120
mqtty-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ !_TAG_EXTRA_DESCRIPTION anonymous /Include tags for non-named objects like lambda/
2
+ !_TAG_EXTRA_DESCRIPTION fileScope /Include tags of file scope/
3
+ !_TAG_EXTRA_DESCRIPTION pseudo /Include pseudo tags/
4
+ !_TAG_EXTRA_DESCRIPTION subparser /Include tags generated by subparsers/
5
+ !_TAG_FIELD_DESCRIPTION epoch /the last modified time of the input file (only for F\/file kind tag)/
6
+ !_TAG_FIELD_DESCRIPTION file /File-restricted scoping/
7
+ !_TAG_FIELD_DESCRIPTION input /input file/
8
+ !_TAG_FIELD_DESCRIPTION name /tag name/
9
+ !_TAG_FIELD_DESCRIPTION pattern /pattern/
10
+ !_TAG_FIELD_DESCRIPTION typeref /Type and name of a variable or typedef/
11
+ !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
12
+ !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
13
+ !_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/
14
+ !_TAG_OUTPUT_FILESEP slash /slash or backslash/
15
+ !_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/
16
+ !_TAG_OUTPUT_VERSION 0.0 /current.age/
17
+ !_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/
18
+ !_TAG_PROC_CWD /home/mark/Downloads/tmp/mqtt2pty/src/mqtt2pty/ //
19
+ !_TAG_PROGRAM_AUTHOR Universal Ctags Team //
20
+ !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/
21
+ !_TAG_PROGRAM_URL https://ctags.io/ /official site/
22
+ !_TAG_PROGRAM_VERSION 6.0.0 //
@@ -0,0 +1,152 @@
1
+ import os
2
+ import pty
3
+ import select
4
+ import threading
5
+ from typing import Any, Dict, Optional, Literal
6
+ from paho.mqtt.client import Client, MQTTMessage
7
+ from paho.mqtt.properties import Properties
8
+ from paho.mqtt.reasoncodes import ReasonCode
9
+ from paho.mqtt.enums import CallbackAPIVersion
10
+ from urllib.parse import urlparse
11
+ import sys
12
+ import argparse
13
+ import subprocess
14
+ import signal
15
+
16
+
17
+ class MQTTY:
18
+ def __init__(self, mqtt_uri: str) -> None:
19
+ self.mqtt_uri = urlparse(mqtt_uri)
20
+
21
+ if self.mqtt_uri.scheme not in ["mqtt", "ws", "wss"]:
22
+ raise ValueError("Invalid URI scheme. Expected 'mqtt://', 'ws://', or 'wss://'.")
23
+
24
+ self.mqtt_host = self.mqtt_uri.hostname
25
+ self.mqtt_port = self.mqtt_uri.port or (443 if self.mqtt_uri.scheme == "wss" else 80)
26
+ self.mqtt_transport: Literal["tcp", "websockets"] = (
27
+ "websockets" if self.mqtt_uri.scheme in ["ws", "wss"] else "tcp"
28
+ )
29
+
30
+ base_path = self.mqtt_uri.path.lstrip("/") # Remove leading slash from path
31
+ self.device_serial_input_topic = f"{base_path}/device_serial_input"
32
+ self.device_serial_output_topic = f"{base_path}/device_serial_output"
33
+
34
+ self.mqtt_client = Client(
35
+ transport=self.mqtt_transport,
36
+ callback_api_version=CallbackAPIVersion.VERSION2,
37
+ )
38
+
39
+ self.master_fd, self.slave_fd = pty.openpty()
40
+ self.slave_name = os.ttyname(self.slave_fd)
41
+
42
+ self.connected = False
43
+ self.stop_event = threading.Event()
44
+
45
+ def on_message(self, _client: Client, _userdata: object, msg: MQTTMessage) -> None:
46
+ try:
47
+ os.write(self.master_fd, msg.payload)
48
+ except Exception as e:
49
+ sys.stderr.write(f"Error writing to PTY: {e}\n")
50
+
51
+ def mqtt_connect(self) -> None:
52
+ self.mqtt_client.on_message = self.on_message
53
+
54
+ def on_connect(
55
+ client: Client,
56
+ userdata: Any,
57
+ flags: Dict[str, Any],
58
+ reason_code: ReasonCode,
59
+ properties: Optional[Properties],
60
+ ) -> None:
61
+ if reason_code == 0:
62
+ self.connected = True
63
+ client.subscribe(self.device_serial_output_topic)
64
+ else:
65
+ sys.stderr.write(f"Failed to connect to MQTT broker, return code {reason_code}\n")
66
+
67
+ def on_disconnect(
68
+ client: Client,
69
+ userdata: Any,
70
+ reason_code: ReasonCode | int | None,
71
+ properties: Optional[Properties],
72
+ packet_from_broker: object,
73
+ ) -> None:
74
+ self.connected = False
75
+
76
+ self.mqtt_client.on_connect = on_connect
77
+ self.mqtt_client.on_disconnect = on_disconnect
78
+
79
+ if self.mqtt_host is None:
80
+ raise ValueError("MQTT URI must include a hostname.")
81
+ try:
82
+ self.mqtt_client.connect_async(self.mqtt_host, self.mqtt_port)
83
+ self.mqtt_client.loop_forever()
84
+ except Exception as e:
85
+ sys.stderr.write(f"MQTT connection failed: {e}\n")
86
+
87
+ def pty_to_mqtt(self) -> None:
88
+ while not self.stop_event.is_set():
89
+ try:
90
+ ready, _, _ = select.select([self.master_fd], [], [], 1)
91
+ if self.master_fd in ready:
92
+ data = os.read(self.master_fd, 1024)
93
+ if self.connected:
94
+ self.mqtt_client.publish(self.device_serial_input_topic, data)
95
+ except Exception as e:
96
+ sys.stderr.write(f"Error reading from PTY: {e}\n")
97
+ break
98
+
99
+ def start_threads(self) -> None:
100
+ mqtt_thread = threading.Thread(target=self.mqtt_connect, daemon=True)
101
+ mqtt_thread.start()
102
+
103
+ pty_thread = threading.Thread(target=self.pty_to_mqtt, daemon=True)
104
+ pty_thread.start()
105
+
106
+ def shutdown(self) -> None:
107
+ self.stop_event.set()
108
+ self.mqtt_client.loop_stop()
109
+ self.mqtt_client.disconnect()
110
+ os.close(self.master_fd)
111
+ os.close(self.slave_fd)
112
+
113
+
114
+ def main() -> None:
115
+ parser = argparse.ArgumentParser(description="MQTTY: Bridge MQTT to PTY.")
116
+ parser.add_argument("mqtt_uri", help="MQTT URI (e.g., mqtt://broker/topic)")
117
+ group = parser.add_mutually_exclusive_group(required=True)
118
+ group.add_argument(
119
+ "-p",
120
+ "--pts-only",
121
+ action="store_true",
122
+ help="Only create and expose the PTS device",
123
+ )
124
+ group.add_argument(
125
+ "-w",
126
+ "--wrap-picocom",
127
+ action="store_true",
128
+ help="Wrap and run picocom on the PTS device",
129
+ )
130
+ args = parser.parse_args()
131
+
132
+ try:
133
+ bridge = MQTTY(args.mqtt_uri)
134
+ if args.pts_only:
135
+ print(bridge.slave_name, flush=True)
136
+ bridge.start_threads()
137
+
138
+ if args.pts_only:
139
+ signal.signal(signal.SIGINT, lambda sig, frame: bridge.shutdown())
140
+ signal.pause() # Wait until interrupted
141
+ elif args.wrap_picocom:
142
+ try:
143
+ subprocess.run(["picocom", "--quiet", bridge.slave_name])
144
+ finally:
145
+ bridge.shutdown()
146
+
147
+ except ValueError as e:
148
+ sys.stderr.write(f"Error: {e}\n")
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: mqtty
3
+ Version: 0.1.0
4
+ Summary: Bridges MQTT topics to a pseudo-terminal device
5
+ Author-email: Mark Featherston <mark@embeddedTS.com>
6
+ Project-URL: homepage, https://github.com/embeddedts/mqtty
7
+ Project-URL: bugs, https://github.com/embeddedts/mqtty/issues
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: paho-mqtt
11
+ Dynamic: license-file
12
+
13
+ # mqtty
14
+
15
+ `mqtty` bridges an MQTT topic to a local pseudoterminal (PTY), allowing you to interact with serial ports over MQTT as if they were local TTY devices.
16
+
17
+ This is useful for debugging, automation, or tunneling serial communication through a networked MQTT broker.
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ mqtty mqtt://<host>/<topic> [--pts-only | --wrap-picocom]
23
+ ```
24
+
25
+ You must choose one of the two modes:
26
+
27
+ * `--pts-only`, `-p`:
28
+ Print the path to the PTS device and keep it open. Useful for tools or scripts that want to use the virtual serial port.
29
+
30
+ * `--wrap-picocom`, `-w`:
31
+ Launch `picocom --quiet` attached to the PTS device. This allows interactive use of a remote serial port over MQTT.
32
+
33
+ The `<topic>` segment of the URI is used as the base path for MQTT messages:
34
+
35
+ * `.../device_serial_input` receives data from your local PTY (sent to the device)
36
+ * `.../device_serial_output` sends incoming data from the device to your PTY
37
+
38
+ ## Example
39
+
40
+ ```bash
41
+ mqtty mqtt://broker.local/mydevice --wrap-picocom
42
+ ```
43
+
44
+ This opens a local PTS device, bridges it to `mydevice/device_serial_input` and `mydevice/device_serial_output`, and launches `picocom` on the virtual port.
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ .github/workflows/release.yml
5
+ src/mqtty/.vscode-ctags
6
+ src/mqtty/mqtty.py
7
+ src/mqtty.egg-info/PKG-INFO
8
+ src/mqtty.egg-info/SOURCES.txt
9
+ src/mqtty.egg-info/dependency_links.txt
10
+ src/mqtty.egg-info/entry_points.txt
11
+ src/mqtty.egg-info/requires.txt
12
+ src/mqtty.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mqtty = mqtty.mqtty:main
@@ -0,0 +1 @@
1
+ paho-mqtt
@@ -0,0 +1 @@
1
+ mqtty