muxit 0.1.0__py3-none-any.whl
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.
- muxit/__init__.py +46 -0
- muxit/__main__.py +286 -0
- muxit/_version.py +1 -0
- muxit/client.py +594 -0
- muxit/connector.py +206 -0
- muxit/errors.py +99 -0
- muxit-0.1.0.dist-info/METADATA +165 -0
- muxit-0.1.0.dist-info/RECORD +11 -0
- muxit-0.1.0.dist-info/WHEEL +4 -0
- muxit-0.1.0.dist-info/entry_points.txt +2 -0
- muxit-0.1.0.dist-info/licenses/LICENSE +21 -0
muxit/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Drive the instruments you set up in Muxit from Python.
|
|
2
|
+
|
|
3
|
+
import muxit
|
|
4
|
+
|
|
5
|
+
m = muxit.connect() # Muxit on this computer
|
|
6
|
+
psu = m.connector("psu")
|
|
7
|
+
psu.voltage = 3.0 # write
|
|
8
|
+
print(psu.voltage) # read (Muxit's latest polled value)
|
|
9
|
+
psu.reset() # action
|
|
10
|
+
|
|
11
|
+
Docs: https://docs.muxit.io/guides/python-client
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
from .client import Muxit, Settings, connect
|
|
16
|
+
from .connector import Connector
|
|
17
|
+
from .errors import (
|
|
18
|
+
AuthenticationFailed,
|
|
19
|
+
ConnectionFailed,
|
|
20
|
+
LimitReached,
|
|
21
|
+
MuxitError,
|
|
22
|
+
NotFound,
|
|
23
|
+
ReadOnly,
|
|
24
|
+
SafetyBlocked,
|
|
25
|
+
SimulatedWarning,
|
|
26
|
+
Timeout,
|
|
27
|
+
UnknownName,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"__version__",
|
|
32
|
+
"connect",
|
|
33
|
+
"Muxit",
|
|
34
|
+
"Connector",
|
|
35
|
+
"Settings",
|
|
36
|
+
"MuxitError",
|
|
37
|
+
"ConnectionFailed",
|
|
38
|
+
"AuthenticationFailed",
|
|
39
|
+
"NotFound",
|
|
40
|
+
"UnknownName",
|
|
41
|
+
"ReadOnly",
|
|
42
|
+
"SafetyBlocked",
|
|
43
|
+
"LimitReached",
|
|
44
|
+
"Timeout",
|
|
45
|
+
"SimulatedWarning",
|
|
46
|
+
]
|
muxit/__main__.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Command line: check the connection and try things without writing Python.
|
|
2
|
+
|
|
3
|
+
python -m muxit doctor # is everything set up?
|
|
4
|
+
python -m muxit list [connector] # what is there
|
|
5
|
+
python -m muxit get psu voltage
|
|
6
|
+
python -m muxit set psu voltage 3
|
|
7
|
+
python -m muxit call psu reset
|
|
8
|
+
python -m muxit demo # read and write the built-in test device
|
|
9
|
+
|
|
10
|
+
Every command takes --host, --port, --password, --token and --timeout, or the
|
|
11
|
+
MUXIT_HOST / MUXIT_PORT / MUXIT_PASSWORD / MUXIT_TOKEN / MUXIT_TIMEOUT variables.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import platform
|
|
19
|
+
import socket
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
import warnings
|
|
23
|
+
from typing import Any, List, Optional
|
|
24
|
+
|
|
25
|
+
from . import errors
|
|
26
|
+
from ._version import __version__
|
|
27
|
+
from .client import Muxit, Settings, login
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _out(text: str = "") -> None:
|
|
31
|
+
print(text, flush=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _status(tag: str, text: str) -> None:
|
|
35
|
+
# ASCII tags: a Windows console may not print check marks.
|
|
36
|
+
_out(f" [{tag:^4}] {text}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _value(text: str) -> Any:
|
|
40
|
+
"""Command-line value: JSON when it parses (3, true, [1,2], {"a":1}), else the plain string."""
|
|
41
|
+
try:
|
|
42
|
+
return json.loads(text)
|
|
43
|
+
except ValueError:
|
|
44
|
+
return text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _show(value: Any) -> str:
|
|
48
|
+
return json.dumps(value, ensure_ascii=False) if not isinstance(value, str) else value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _connect(args: argparse.Namespace) -> Muxit:
|
|
52
|
+
return Muxit(args.host, args.port, password=args.password, token=args.token, timeout=args.timeout)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── doctor ───────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def doctor(args: argparse.Namespace) -> int:
|
|
58
|
+
_out(f"muxit {__version__} (Python {platform.python_version()}, {platform.system()})")
|
|
59
|
+
_out()
|
|
60
|
+
try:
|
|
61
|
+
s = Settings(args.host, args.port, args.password, args.token, args.timeout)
|
|
62
|
+
except errors.MuxitError as e:
|
|
63
|
+
_status("FAIL", str(e))
|
|
64
|
+
return 1
|
|
65
|
+
_out("Settings")
|
|
66
|
+
_status("", f"host {s.host} (from {s.source['host']})")
|
|
67
|
+
_status("", f"port {s.port} (from {s.source['port']})")
|
|
68
|
+
_status("", f"password {'set' if s.password else 'not set'} (from {s.source['password']})")
|
|
69
|
+
if s.token:
|
|
70
|
+
_status("", f"token set (from {s.source['token']})")
|
|
71
|
+
_status("", f"address {s.ws_url}")
|
|
72
|
+
_out()
|
|
73
|
+
|
|
74
|
+
_out("Checks")
|
|
75
|
+
try:
|
|
76
|
+
with socket.create_connection((s.host, s.port), timeout=min(s.timeout, 5)):
|
|
77
|
+
pass
|
|
78
|
+
_status("OK", f"something is listening on {s.host}:{s.port}")
|
|
79
|
+
except OSError as e:
|
|
80
|
+
_status("FAIL", f"nothing answers on {s.host}:{s.port} ({e})")
|
|
81
|
+
if s.is_local:
|
|
82
|
+
_out(" Start Muxit on this computer, or check the port in its startup banner.")
|
|
83
|
+
else:
|
|
84
|
+
_out(" Is Muxit running on that computer, with remote access on (Settings > Remote")
|
|
85
|
+
_out(f" access), and does its firewall allow port {s.port}? Try the IP address too.")
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
if s.password and not s.token:
|
|
89
|
+
try:
|
|
90
|
+
s.token = login(s)
|
|
91
|
+
_status("OK", "logged in with the password")
|
|
92
|
+
except errors.MuxitError as e:
|
|
93
|
+
_status("FAIL", str(e))
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
m = Muxit(s.host, s.port, token=s.token, timeout=s.timeout)
|
|
98
|
+
except errors.MuxitError as e:
|
|
99
|
+
_status("FAIL", str(e))
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
problems = 0
|
|
103
|
+
with m:
|
|
104
|
+
_status("OK", "connected to Muxit")
|
|
105
|
+
if m.token_source:
|
|
106
|
+
_status("OK", f"access token read from workspace {m.token_source}")
|
|
107
|
+
try:
|
|
108
|
+
lic = m.license()
|
|
109
|
+
used, cap = lic.get("currentConnectors"), lic.get("maxConnectors")
|
|
110
|
+
cap_text = "unlimited" if cap in (None, -1) else str(cap)
|
|
111
|
+
_status("OK", f"license tier {lic.get('tierName') or lic.get('tierId', '?')}, "
|
|
112
|
+
f"connectors {used if used is not None else '?'} of {cap_text}")
|
|
113
|
+
except errors.MuxitError as e:
|
|
114
|
+
_status("WARN", f"could not read the license: {e}")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
level = m.safety().get("level", "?")
|
|
118
|
+
if level == "Observe":
|
|
119
|
+
problems += 1
|
|
120
|
+
_status("WARN", "safety level is Observe: writes and actions are SIMULATED, nothing")
|
|
121
|
+
_out(" reaches a device. Pick another level with the safety chip in the Muxit")
|
|
122
|
+
_out(" window (Free can use Unrestricted).")
|
|
123
|
+
else:
|
|
124
|
+
_status("OK", f"safety level {level}")
|
|
125
|
+
except errors.MuxitError as e:
|
|
126
|
+
_status("WARN", f"could not read the safety level: {e}")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
names = m.connectors()
|
|
130
|
+
except errors.MuxitError as e:
|
|
131
|
+
_status("FAIL", f"could not list connectors: {e}")
|
|
132
|
+
return 1
|
|
133
|
+
if not names:
|
|
134
|
+
_status("WARN", "Muxit has no connectors loaded yet: add one in the Muxit window")
|
|
135
|
+
else:
|
|
136
|
+
_status("OK", f"{len(names)} connector(s): {', '.join(names)}")
|
|
137
|
+
_out()
|
|
138
|
+
_out("Ready. In Python:" if not problems else "Connected, with the warning above. In Python:")
|
|
139
|
+
parts = [] if s.source["host"] == "default" else [f'"{s.host}"']
|
|
140
|
+
if s.port != 8765:
|
|
141
|
+
parts.append(f"port={s.port}")
|
|
142
|
+
_out(f" import muxit; m = muxit.connect({', '.join(parts)})")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── list / get / set / call ──────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
def list_cmd(args: argparse.Namespace) -> int:
|
|
149
|
+
with _connect(args) as m:
|
|
150
|
+
if args.connector:
|
|
151
|
+
_out(m.connector(args.connector).describe())
|
|
152
|
+
return 0
|
|
153
|
+
names = m.connectors()
|
|
154
|
+
if not names:
|
|
155
|
+
_out("No connectors loaded.")
|
|
156
|
+
for name in names:
|
|
157
|
+
_out(name)
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_cmd(args: argparse.Namespace) -> int:
|
|
162
|
+
with _connect(args) as m:
|
|
163
|
+
_out(_show(m.connector(args.connector).read(args.property)))
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def set_cmd(args: argparse.Namespace) -> int:
|
|
168
|
+
with _connect(args) as m:
|
|
169
|
+
c = m.connector(args.connector)
|
|
170
|
+
setattr(c, args.property, _value(args.value)) # checks read-only and spelling first
|
|
171
|
+
_out(f"{args.connector}.{args.property} = {args.value}")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def call_cmd(args: argparse.Namespace) -> int:
|
|
176
|
+
with _connect(args) as m:
|
|
177
|
+
result = m.connector(args.connector).call(args.action, *[_value(a) for a in args.args])
|
|
178
|
+
if result is not None:
|
|
179
|
+
_out(_show(result))
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── demo ─────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def demo(args: argparse.Namespace) -> int:
|
|
186
|
+
with _connect(args) as m:
|
|
187
|
+
names = m.connectors()
|
|
188
|
+
target: Optional[str] = args.connector
|
|
189
|
+
if target is None:
|
|
190
|
+
for name in names:
|
|
191
|
+
if str(m.schema(name).get("driver", "")).lower() == "testdevice":
|
|
192
|
+
target = name
|
|
193
|
+
break
|
|
194
|
+
if target is None:
|
|
195
|
+
_out("No TestDevice connector found. The default workspace has one called 'test-device';")
|
|
196
|
+
_out("or pass the name of another connector: python -m muxit demo <name>")
|
|
197
|
+
return 1
|
|
198
|
+
dev = m.connector(target)
|
|
199
|
+
_out(f"Using connector {target!r}")
|
|
200
|
+
_out()
|
|
201
|
+
_out(f">>> dev = m.connector({target!r})")
|
|
202
|
+
before = dev.temperature
|
|
203
|
+
_out(">>> dev.temperature")
|
|
204
|
+
_out(f"{before}")
|
|
205
|
+
new = 42.0 if before != 42.0 else 37.5
|
|
206
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
207
|
+
warnings.simplefilter("always")
|
|
208
|
+
_out(f">>> dev.temperature = {new}")
|
|
209
|
+
dev.temperature = new
|
|
210
|
+
time.sleep(0.5) # driver properties are polled; give the poll a cycle
|
|
211
|
+
after = dev.temperature
|
|
212
|
+
_out(">>> dev.temperature")
|
|
213
|
+
_out(f"{after}")
|
|
214
|
+
_out(">>> dev.calculate(2, 3)")
|
|
215
|
+
_out(f"{dev.calculate(2, 3)}")
|
|
216
|
+
dev.temperature = before
|
|
217
|
+
if any(issubclass(w.category, errors.SimulatedWarning) for w in caught):
|
|
218
|
+
_out()
|
|
219
|
+
_out("NOTE: Muxit's safety level is Observe, so the write was simulated and the action")
|
|
220
|
+
_out("returned nothing. Pick another level with the safety chip in the Muxit window.")
|
|
221
|
+
return 0
|
|
222
|
+
_out()
|
|
223
|
+
_out(f"Wrote {new}, read back {after}; restored {before}. Everything works.")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── entry point ──────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
def _parser() -> argparse.ArgumentParser:
|
|
230
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
231
|
+
common.add_argument("--host", help="IP address or host name of the Muxit computer (default 127.0.0.1)")
|
|
232
|
+
common.add_argument("--port", type=int, help="Muxit port (default 8765)")
|
|
233
|
+
common.add_argument("--password", help="remote-access password (only for another computer)")
|
|
234
|
+
common.add_argument("--token", help="remote-access token instead of a password")
|
|
235
|
+
common.add_argument("--timeout", type=float, help="seconds to wait for an answer (default 10)")
|
|
236
|
+
|
|
237
|
+
p = argparse.ArgumentParser(prog="python -m muxit", description="Muxit from the command line.")
|
|
238
|
+
p.add_argument("--version", action="version", version=f"muxit {__version__}")
|
|
239
|
+
sub = p.add_subparsers(dest="command", metavar="command")
|
|
240
|
+
|
|
241
|
+
sub.add_parser("doctor", parents=[common], help="check the connection and settings").set_defaults(fn=doctor)
|
|
242
|
+
|
|
243
|
+
q = sub.add_parser("list", parents=[common], help="list connectors, or one connector's properties and actions")
|
|
244
|
+
q.add_argument("connector", nargs="?")
|
|
245
|
+
q.set_defaults(fn=list_cmd)
|
|
246
|
+
|
|
247
|
+
q = sub.add_parser("get", parents=[common], help="read a property")
|
|
248
|
+
q.add_argument("connector")
|
|
249
|
+
q.add_argument("property")
|
|
250
|
+
q.set_defaults(fn=get_cmd)
|
|
251
|
+
|
|
252
|
+
q = sub.add_parser("set", parents=[common], help="write a property")
|
|
253
|
+
q.add_argument("connector")
|
|
254
|
+
q.add_argument("property")
|
|
255
|
+
q.add_argument("value", help="a number, true/false, JSON, or text")
|
|
256
|
+
q.set_defaults(fn=set_cmd)
|
|
257
|
+
|
|
258
|
+
q = sub.add_parser("call", parents=[common], help="call an action")
|
|
259
|
+
q.add_argument("connector")
|
|
260
|
+
q.add_argument("action")
|
|
261
|
+
q.add_argument("args", nargs="*", help="arguments (numbers, true/false, JSON, or text)")
|
|
262
|
+
q.set_defaults(fn=call_cmd)
|
|
263
|
+
|
|
264
|
+
q = sub.add_parser("demo", parents=[common], help="read and write the built-in test device")
|
|
265
|
+
q.add_argument("connector", nargs="?", help="connector to use (default: the first TestDevice)")
|
|
266
|
+
q.set_defaults(fn=demo)
|
|
267
|
+
return p
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
271
|
+
parser = _parser()
|
|
272
|
+
args = parser.parse_args(argv)
|
|
273
|
+
if not getattr(args, "fn", None):
|
|
274
|
+
parser.print_help()
|
|
275
|
+
return 0
|
|
276
|
+
try:
|
|
277
|
+
return args.fn(args)
|
|
278
|
+
except errors.MuxitError as e:
|
|
279
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
280
|
+
return 1
|
|
281
|
+
except KeyboardInterrupt:
|
|
282
|
+
return 130
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
sys.exit(main())
|
muxit/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|