motion-master-client 6.0.0a124__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.
- motion_master_client-6.0.0a124.dist-info/METADATA +383 -0
- motion_master_client-6.0.0a124.dist-info/RECORD +7 -0
- motion_master_client-6.0.0a124.dist-info/WHEEL +5 -0
- motion_master_client-6.0.0a124.dist-info/licenses/LICENSE +674 -0
- motion_master_client-6.0.0a124.dist-info/top_level.txt +2 -0
- motion_master_client.py +1000 -0
- motion_master_objects.py +2515 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: motion-master-client
|
|
3
|
+
Version: 6.0.0a124
|
|
4
|
+
Summary: Python client for the Motion Master HTTP API and WebSocket
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
|
+
Project-URL: Homepage, https://motion-master.synapticon.com
|
|
7
|
+
Project-URL: Source, https://github.com/synapticon/motion-master/tree/main/clients/python
|
|
8
|
+
Project-URL: Issues, https://github.com/synapticon/motion-master/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests>=2.31
|
|
15
|
+
Requires-Dist: pyyaml>=6
|
|
16
|
+
Requires-Dist: websockets>=13
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Motion Master reference Python client
|
|
20
|
+
|
|
21
|
+
This is a reference Python client for the Motion Master HTTP API and the WebSocket.
|
|
22
|
+
At startup it reads the OpenAPI spec from the server and calls each operation by its
|
|
23
|
+
`operationId`. See [Why it reads the spec](#why-it-reads-the-spec).
|
|
24
|
+
|
|
25
|
+
There are two ways to get it:
|
|
26
|
+
|
|
27
|
+
- **Install it with pip** to use it in your own program. See [Install with pip](#install-with-pip).
|
|
28
|
+
- **Clone this repository** to run the example scripts, or to change the client. See
|
|
29
|
+
[Setup](#setup).
|
|
30
|
+
|
|
31
|
+
The example scripts are a starting point for your own programs. We recommend an AI coding
|
|
32
|
+
assistant, such as Claude Code. Open it in `clients/python` of a clone and tell it what your
|
|
33
|
+
program must do. It reads the client, the scripts and `swagger.yml`, and writes the program.
|
|
34
|
+
|
|
35
|
+
The recording below shows a first run from a clone. The steps are in [Setup](#setup) and
|
|
36
|
+
[Run](#run).
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## Install with pip
|
|
41
|
+
|
|
42
|
+
Python 3.11 or newer is necessary, because the client reads the config with the standard library
|
|
43
|
+
module `tomllib`.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install motion-master-client
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The package has the same version as the server, and is published with each release. A version of
|
|
50
|
+
the client works with the server of the same version. Every version is a pre-release until 6.0.0.
|
|
51
|
+
pip installs a pre-release when no final release exists, so `pip install` needs no `--pre`.
|
|
52
|
+
|
|
53
|
+
The package holds the two modules, `motion_master_client` and `motion_master_objects`. It does not
|
|
54
|
+
hold the example scripts. Write `config.toml` in the directory that your program runs from. Take
|
|
55
|
+
the content from [`config.example.toml`](https://github.com/synapticon/motion-master/blob/main/clients/python/config.example.toml), and set `adapter` as [Setup](#setup) says. Then:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from motion_master_client import AlState, MotionMasterClient, run
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
client = MotionMasterClient.from_config()
|
|
63
|
+
client.ensure_state(AlState.PREOP)
|
|
64
|
+
for device in client.devices():
|
|
65
|
+
print(device["slavePosition"], device["productName"])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
run(main)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`from_config()` reads `config.toml` in the current directory. It writes the spec of the server to
|
|
72
|
+
`swagger.yml` beside that file. To keep the config in another place, give its path:
|
|
73
|
+
`MotionMasterClient.from_config("/path/to/config.toml")`.
|
|
74
|
+
|
|
75
|
+
## Setup
|
|
76
|
+
|
|
77
|
+
Python 3.11 or newer is necessary, because the client reads the config with the standard library
|
|
78
|
+
module `tomllib`.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/synapticon/motion-master.git
|
|
82
|
+
cd motion-master/clients/python
|
|
83
|
+
source setup.sh
|
|
84
|
+
cp config.example.toml config.toml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`setup.sh` creates a virtual environment in `.venv`, activates it, upgrades pip in it, and
|
|
88
|
+
installs `requirements.txt` into it. A virtual environment is a directory that holds the
|
|
89
|
+
dependencies of this client apart from your system Python. Source the script, so that the
|
|
90
|
+
activation stays in your shell. Git ignores `.venv`. To remove the environment, delete `.venv`.
|
|
91
|
+
|
|
92
|
+
To do the same by hand, for example on Windows:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python -m venv .venv
|
|
96
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
97
|
+
python -m pip install --upgrade pip
|
|
98
|
+
pip install -r requirements.txt
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then edit `config.toml`. Set `adapter` in the `[fieldbus]` table to the network interface that
|
|
102
|
+
the drives are connected to. Give an interface name, such as `eth0`, or a MAC address.
|
|
103
|
+
`python examples/server-info.py` lists the network adapters of the host when the server runs.
|
|
104
|
+
|
|
105
|
+
## Run
|
|
106
|
+
|
|
107
|
+
1. Download a build from the [releases](https://github.com/synapticon/motion-master/releases).
|
|
108
|
+
2. Start the binary. It listens on `127.0.0.1` by default.
|
|
109
|
+
3. Run a script, for example `python examples/scan.py`.
|
|
110
|
+
|
|
111
|
+
## Scripts
|
|
112
|
+
|
|
113
|
+
Any script can run first, because it brings the server to the state it needs. A script that
|
|
114
|
+
needs a bus opens the fieldbus driver from `config.toml` when no driver is open. It scans when no
|
|
115
|
+
devices are known. A script that addresses one device takes `--slave`, the position of the device
|
|
116
|
+
on the bus. The default is 1. Each script prints its options with `--help`.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python examples/scan.py
|
|
120
|
+
python examples/transition-state.py --state op
|
|
121
|
+
python examples/read-sdo.py --index 0x6064 --subindex 0
|
|
122
|
+
python examples/read-file.py --filename .hardware_description
|
|
123
|
+
python examples/read-all-parameters.py --slave 2
|
|
124
|
+
python examples/monitor.py --count 10
|
|
125
|
+
python examples/reset.py
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**The running server is shared.** A Console session that is connected to the same server sees
|
|
129
|
+
every change that a script makes. A script that opens the driver, scans, resets or moves a
|
|
130
|
+
device to another AL state changes what the Console shows.
|
|
131
|
+
|
|
132
|
+
The AL state (Application Layer state, the EtherCAT state machine of a device) column gives
|
|
133
|
+
the lowest state the script needs. A script moves the device up to that state when the device is
|
|
134
|
+
lower. A device that is already higher stays where it is. The states, from low to high, are INIT,
|
|
135
|
+
PREOP, SAFEOP and OP. BOOT is separate: a device enters and leaves BOOT only through INIT.
|
|
136
|
+
|
|
137
|
+
Three values in that column are not states:
|
|
138
|
+
|
|
139
|
+
- "Scanned" means that the script needs the devices known. It opens the driver and scans when
|
|
140
|
+
needed, but it moves no device.
|
|
141
|
+
- "Driver open" means that the script opens the driver when none is open, and scans nothing.
|
|
142
|
+
- "No bus" means that the script opens no driver and leaves the server as it is.
|
|
143
|
+
|
|
144
|
+
### Server and bus
|
|
145
|
+
|
|
146
|
+
| Script | What it does | AL state |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `server-info.py` | Prints the server version, the host, the network adapters and the health of the game loop. | No bus |
|
|
149
|
+
| `set-game-loop-period.py` | Changes the cycle period of the real-time game loop until the server stops. | No bus |
|
|
150
|
+
| `notifications.py` | Prints the notifications that the server sends over the WebSocket. | No bus |
|
|
151
|
+
| `scan.py` | Scans the bus and lists the devices it finds. A scan leaves every device in INIT. | Scanned |
|
|
152
|
+
| `transition-state.py` | Moves the devices to the AL state you give with `--state`, through every state between. | The state you give |
|
|
153
|
+
| `restart-devices.py` | Restarts devices through INIT up to an AL state. An enabled drive is disabled. | Scanned |
|
|
154
|
+
| `reset.py` | Closes the fieldbus driver and clears the device list. | No bus |
|
|
155
|
+
| `diagnostics.py` | Prints the ESC (EtherCAT Slave Controller) error counters and the distributed clock sync of the devices. | Scanned |
|
|
156
|
+
| `capture.py` | Records the EtherCAT frames on the bus adapter for some seconds, and prints how late the frames came back. | Driver open |
|
|
157
|
+
|
|
158
|
+
### Parameters
|
|
159
|
+
|
|
160
|
+
| Script | What it does | AL state |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `read-parameter.py` | Reads one parameter with its name, data type and decoded value. `--cache` reads no bus. | PREOP |
|
|
163
|
+
| `write-parameter.py` | Writes one parameter. It converts the value to the data type, and prints the value before and after. | PREOP |
|
|
164
|
+
| `read-all-parameters.py` | Reads the whole object dictionary of the device with the value of every entry. | PREOP |
|
|
165
|
+
| `read-sdo.py` | Reads one object with an SDO upload (Service Data Object, a mailbox read of one object dictionary entry), and prints its raw bytes. | PREOP |
|
|
166
|
+
| `write-sdo.py` | Writes raw bytes to one object with an SDO download. | PREOP |
|
|
167
|
+
| `store-parameters.py` | Stores the current parameter values in the flash of the device. | PREOP |
|
|
168
|
+
| `restore-default-parameters.py` | Restores the default values of a parameter group. **Values not stored are gone.** | PREOP |
|
|
169
|
+
|
|
170
|
+
### Files and firmware
|
|
171
|
+
|
|
172
|
+
| Script | What it does | AL state |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| `list-files.py` | Lists the files stored on the device with their sizes. | PREOP |
|
|
175
|
+
| `read-file.py` | Reads a file from the device over FoE (File access over EtherCAT), and saves it in `files/`. | PREOP |
|
|
176
|
+
| `write-file.py` | Writes a local file to the device over FoE. A file of the same name is replaced. | PREOP |
|
|
177
|
+
| `delete-file.py` | Deletes a file from the flash of a SOMANET device. | PREOP |
|
|
178
|
+
| `read-hardware-description.py` | Prints what a SOMANET device says it is: the device, the assembly and their components. | PREOP |
|
|
179
|
+
| `check-firmware-compatibility.py` | Says whether a firmware package belongs on the device, from the package filename alone. It installs nothing. | PREOP |
|
|
180
|
+
| `firmware-installation.py` | Uploads a firmware package to the server and installs it on the device. The device is not controlled while it is off OP. | Scanned |
|
|
181
|
+
|
|
182
|
+
### ESC registers and SII
|
|
183
|
+
|
|
184
|
+
| Script | What it does | AL state |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| `read-register.py` | Reads bytes from an ESC register. | Scanned |
|
|
187
|
+
| `write-register.py` | Writes bytes to an ESC register. | Scanned |
|
|
188
|
+
| `read-sii.py` | Prints the SII (Slave Information Interface, the configuration in the EEPROM of the device), or saves the raw image with `--output`. | Scanned |
|
|
189
|
+
| `write-sii.py` | Writes a raw SII image to the EEPROM. **A wrong image can leave the device unidentifiable.** The device adopts it after a power cycle. | Scanned |
|
|
190
|
+
|
|
191
|
+
### Process data
|
|
192
|
+
|
|
193
|
+
| Script | What it does | AL state |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `read-pdo-mapping.py` | Prints the PDO (Process Data Object) mapping of the device, with the bit offset and the name of each entry. | PREOP |
|
|
196
|
+
| `write-pdo-mapping.py` | Writes the PDO mapping from a JSON file. It moves the device to PREOP for the write, and back to SAFEOP or OP. | PREOP for the write |
|
|
197
|
+
| `monitor.py` | Creates a monitoring and prints its batches of process data from the WebSocket. | OP |
|
|
198
|
+
| `monitoring.py` | Records a move in Cyclic Synchronous Velocity with `client.monitoring()`: 250 for 2 s, then 0. Prints the target, the actual velocity and the CiA402 state, and saves every cycle as a CSV file in `files/`. **The motor turns.** | OP |
|
|
199
|
+
| `dump-process-data.py` | Saves the recorder of the server, every recorded cycle of process data, as a `.mmpd` file in `files/`. | SAFEOP |
|
|
200
|
+
|
|
201
|
+
### Drive
|
|
202
|
+
|
|
203
|
+
| Script | What it does | AL state |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `cia402-state.py` | Prints the CiA 402 state of the drive with its actual and target values. | PREOP |
|
|
206
|
+
| `set-target.py` | Sets the position, velocity or torque target. **An enabled drive moves to it at once.** | PREOP |
|
|
207
|
+
| `release-brake.py` | Releases the brake of a SOMANET drive. It releases only while the drive is in Operation Enabled, and stays released. | PREOP |
|
|
208
|
+
| `engage-brake.py` | Engages the brake of a SOMANET drive. | PREOP |
|
|
209
|
+
| `device-locate.py` | Blinks the LEDs of the device. Only SPoE offers it. | PREOP |
|
|
210
|
+
|
|
211
|
+
### Procedures
|
|
212
|
+
|
|
213
|
+
| Script | What it does | AL state |
|
|
214
|
+
| --- | --- | --- |
|
|
215
|
+
| `run-procedure.py` | Lists the procedures of a device. With a name, it runs that procedure with parameters given as `name=value`, and prints each step with its result. `--describe` prints the description, caveats and parameters instead. | PREOP, or `--state` |
|
|
216
|
+
| `offset-detection.py` | Runs the offset detection procedure on a SOMANET drive, and prints each step with its result. **It turns the motor and releases the brake.** Ctrl-C cancels the run. | OP |
|
|
217
|
+
| `system-identification.py` | Configures the system identification chirp, and arms it with `--start`. **Once armed, it turns the motor.** | PREOP |
|
|
218
|
+
| `plant-identification.py` | Identifies the plant model with a chirp, and writes `plant_model.csv`. **It turns the motor in both directions.** | OP |
|
|
219
|
+
|
|
220
|
+
### ESI and ENI
|
|
221
|
+
|
|
222
|
+
| Script | What it does | AL state |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| `parse-esi.py` | Parses an ESI (EtherCAT Slave Information) file and lists the devices it describes. | No bus |
|
|
225
|
+
| `export-eni.py` | Saves the bus as an ENI (EtherCAT Network Information) file in `files/`, for another master to replay. | SAFEOP |
|
|
226
|
+
|
|
227
|
+
### Production
|
|
228
|
+
|
|
229
|
+
A production script runs many steps in order on one drive, asks before the steps that need a
|
|
230
|
+
decision, and writes a log and a record of the drive. Its steps are in
|
|
231
|
+
[`examples/production/README.md`](https://github.com/synapticon/motion-master/blob/main/clients/python/examples/production/README.md).
|
|
232
|
+
|
|
233
|
+
| Script | What it does | AL state |
|
|
234
|
+
| --- | --- | --- |
|
|
235
|
+
| `production/integro/commission.py` | Commissions an Integro drive. | PREOP |
|
|
236
|
+
| `production/c021/commission.py` | Commissions a C021 drive up to its assigned name. | PREOP |
|
|
237
|
+
|
|
238
|
+
## Calls by operationId
|
|
239
|
+
|
|
240
|
+
Every request goes through one method, `MotionMasterClient.call(operationId, ...)`. It takes
|
|
241
|
+
the path parameters as `path`, a JSON body as `body`, and the query string as `query`:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
client.call("init", body={"driver": "soem", "adapter": "eth0"})
|
|
245
|
+
client.call("sdoUpload", path={"slavePosition": 1, "index": 0x6041, "subindex": 0})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The steps that most scripts begin with are methods on the client:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
client = MotionMasterClient.from_config() # reads config.toml
|
|
252
|
+
client.init() # opens the driver, or accepts the one that is open
|
|
253
|
+
client.ensure_state(AlState.PREOP, positions=[1]) # scans when needed, then moves device 1 up
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`ensure_scanned()` scans when needed and moves no device. `init()`, `ensure_scanned()` and
|
|
257
|
+
`ensure_state()` log through the `motion_master_client` logger. Turn on logging at INFO to see
|
|
258
|
+
each step.
|
|
259
|
+
|
|
260
|
+
Other methods:
|
|
261
|
+
|
|
262
|
+
- `reset()` closes the fieldbus driver and forgets every device. It commands no AL state.
|
|
263
|
+
- `scan()` scans the bus every time and returns the number of devices it found.
|
|
264
|
+
- `start_capture(adapter, seconds)` starts a capture of the EtherCAT frames that runs until
|
|
265
|
+
`stop_capture()`, which returns the path of the pcap file in the user cache and the summary of
|
|
266
|
+
late frames. Linux and macOS servers only. Both raise `CaptureError` with the server's reason.
|
|
267
|
+
- `process_data_mark()` returns the recorder clock of the server, in epoch microseconds.
|
|
268
|
+
`process_data_dump(from_us, to_us)` returns the `.mmpd` dump of the cycles between two marks as
|
|
269
|
+
bytes, or of the whole recorder without them. It returns `None` when the server has no cycle to
|
|
270
|
+
dump, and raises `ProcessDataDumpError` for any other refusal. `mmpd_flags()` of the result has
|
|
271
|
+
`MMPD_FLAG_INCOMPLETE_START` set when the server no longer held every cycle right after the
|
|
272
|
+
start.
|
|
273
|
+
- `run_procedure()` runs a procedure to its end.
|
|
274
|
+
- `monitor(topic)` streams the batches of a monitoring from the WebSocket.
|
|
275
|
+
- `monitoring(parameters)` returns a `Monitoring` for a `with` block. It creates the monitoring,
|
|
276
|
+
keeps every row while the block runs, and deletes the monitoring at the end. Then `column()`
|
|
277
|
+
returns the values of one object with their timestamps, `values()` returns the values in a time
|
|
278
|
+
window, `first()` returns the time of the first value that meets a condition,
|
|
279
|
+
`print_table()` prints one row every `every_ms`, with the CiA402 state beside a statusword, and
|
|
280
|
+
`write_csv(path)` writes every row to a CSV file. Both name a column like `1:STATUSWORD`, as
|
|
281
|
+
`names` lists them.
|
|
282
|
+
- `notifications()` streams the notifications from the WebSocket.
|
|
283
|
+
- `devices()` returns the scanned devices in bus order, as `GET /api/devices` gives them.
|
|
284
|
+
- `parameters(position)` returns every parameter of a device, with its value and metadata.
|
|
285
|
+
- `hardware_description(position)` returns the parsed `.hardware_description` of a device.
|
|
286
|
+
- `read_value(position, obj)` reads one object and returns the value that the server decoded with
|
|
287
|
+
the data type from the object dictionary.
|
|
288
|
+
- `wait_for_value(position, obj, predicate)` reads one object until the predicate is true, and
|
|
289
|
+
raises `TimeoutError` with the last value when the time runs out.
|
|
290
|
+
- `restart(target, positions)` restarts devices through INIT and walks them up to an AL state. The
|
|
291
|
+
server does the walk.
|
|
292
|
+
- `set_operation_mode()`, `set_target()`, `enable()`, `disable()` and `quick_stop()` drive the
|
|
293
|
+
CiA402 state machine and the targets of one drive, by its position.
|
|
294
|
+
- `run(main)` calls `main` and prints an error that a user can correct as one line instead of a
|
|
295
|
+
traceback. An example is a server that is not running. Set `MM_TRACEBACK=1` to get the
|
|
296
|
+
traceback.
|
|
297
|
+
|
|
298
|
+
The module names objects and states with classes:
|
|
299
|
+
|
|
300
|
+
- `Profile`, `Cia402` and `Somanet` name every object of a SOMANET device as an address, for example
|
|
301
|
+
`Cia402.STATUSWORD`. `Profile` holds the CiA 301 communication area (0x1xxx) and the standard MDP
|
|
302
|
+
objects, `Cia402` the CiA 402 drive profile (0x6xxx), and `Somanet` the SOMANET vendor area
|
|
303
|
+
(0x2xxx) and FSoE. An address unpacks to `(index, subindex)`, so `read_value(1, Cia402.STATUSWORD)`
|
|
304
|
+
and the monitoring parameter `(1, *Cia402.STATUSWORD)` name the object the same way. An address
|
|
305
|
+
also carries the type of the value, for a type checker. The names are the same as in the
|
|
306
|
+
TypeScript client, and the C++ names of the server without their `k`, in capitals. The list holds
|
|
307
|
+
every object of every device in the ESI, so an address can name an object that the drive does not
|
|
308
|
+
have. A read of it then fails.
|
|
309
|
+
- `AlState` names the AL states, `INIT`, `PREOP`, `BOOT`, `SAFEOP` and `OP`. It is an `IntEnum`, so
|
|
310
|
+
a member goes into a request body as a number, and `AlState(value)` converts an `alState` from a
|
|
311
|
+
response. The name of a member is its label. The server reports `UNKNOWN`, 0, for a device that
|
|
312
|
+
does not answer.
|
|
313
|
+
- `Cia402State` names the CiA402 states. It is a `StrEnum` whose members equal the names that the
|
|
314
|
+
API uses: `Cia402State.OPERATION_ENABLED == "OperationEnabled"`. `Cia402.state(statusword)`
|
|
315
|
+
returns the state that a statusword reports. It is meant for recorded rows, where the server
|
|
316
|
+
sends the raw statusword.
|
|
317
|
+
|
|
318
|
+
`motion_master_objects.py` holds the addresses. The server's `generate-object-addresses` command
|
|
319
|
+
writes it from the ESI, with the C++ headers and the TypeScript file, so do not edit it.
|
|
320
|
+
|
|
321
|
+
## TLS
|
|
322
|
+
|
|
323
|
+
Motion Master accepts TLS connections only. A release binary includes a Let's Encrypt
|
|
324
|
+
certificate for `local.motion-master.synapticon.com`, which resolves to `127.0.0.1`. A public CA
|
|
325
|
+
(certificate authority, an organisation that operating systems trust to sign certificates) signs
|
|
326
|
+
it. So with the default `host` you can set `verify = true`. The client then verifies the
|
|
327
|
+
certificate against the CA store of the system, with no other setup.
|
|
328
|
+
|
|
329
|
+
The template sets `verify = false`, because that works with every certificate. This includes the
|
|
330
|
+
self-signed certificate that the binary uses when it cannot download a real one, for example on a
|
|
331
|
+
host with no internet access. Set `verify = true`, or set `verify` to the path of a `cert.pem`,
|
|
332
|
+
when you know which certificate the server presents.
|
|
333
|
+
|
|
334
|
+
## Two ports
|
|
335
|
+
|
|
336
|
+
Motion Master serves two endpoints, each on its own event loop. The HTTP API is on `http_port`,
|
|
337
|
+
61447. The WebSocket is on `ws_port`, 62281. That is why `host` in the config has no port.
|
|
338
|
+
`client.call(...)` uses the HTTP API. `client.monitor(topic)` and `client.notifications()` use
|
|
339
|
+
the WebSocket. `monitor.py` and `notifications.py` show the WebSocket.
|
|
340
|
+
|
|
341
|
+
## Why it reads the spec
|
|
342
|
+
|
|
343
|
+
The scripts name an `operationId`, such as `init` or `sdoUpload`. They never name a URL or an
|
|
344
|
+
HTTP method. At startup the client reads `GET /api/swagger.yml` and builds one table from
|
|
345
|
+
`operationId` to the method and the URL template. `call()` looks up the ID, fills the
|
|
346
|
+
placeholders of the template from `path`, and sends the request. So the URLs and the methods
|
|
347
|
+
are in one place only, the spec of the server.
|
|
348
|
+
|
|
349
|
+
That gives four results:
|
|
350
|
+
|
|
351
|
+
- **There is one source of truth.** The same binary implements the API and serves the spec. URLs
|
|
352
|
+
in the scripts would be a second copy of the contract, written by hand, and it would be wrong
|
|
353
|
+
after the API changes. An `operationId` is a stable name, so a script keeps working when a URL
|
|
354
|
+
on the server changes.
|
|
355
|
+
- **The client always matches its server.** The spec comes from the running server. So the client
|
|
356
|
+
is correct against the contract of that server, older or newer.
|
|
357
|
+
- **The client is standalone.** There is no bundled spec, no code generation and no checkout of
|
|
358
|
+
this repository. Install it with pip, and point it at a
|
|
359
|
+
server.
|
|
360
|
+
- **A script shows the API.** Each call names the operation that it invokes. There is no URL
|
|
361
|
+
string to read past.
|
|
362
|
+
|
|
363
|
+
The cost is one request at startup, and the server must be reachable when the client is built.
|
|
364
|
+
|
|
365
|
+
## Layout
|
|
366
|
+
|
|
367
|
+
| File | Purpose |
|
|
368
|
+
| --- | --- |
|
|
369
|
+
| `motion_master_client.py` | `MotionMasterClient`, which calls the HTTP API and the WebSocket. See [Calls by operationId](#calls-by-operationid). |
|
|
370
|
+
| `setup.sh` | Creates and activates the virtual environment. See [Setup](#setup). |
|
|
371
|
+
| `motion_master_objects.py` | The generated object addresses. See [Calls by operationId](#calls-by-operationid). |
|
|
372
|
+
| `requirements.txt` | The Python packages that the client needs. Keep it the same as the dependencies in `pyproject.toml`. |
|
|
373
|
+
| `pyproject.toml` | The package that a release publishes to PyPI. The version is set by `tools/bump-version.sh`. |
|
|
374
|
+
| `LICENSE` | A copy of the GPL v3 licence of the repository, which the package carries. |
|
|
375
|
+
| `config.example.toml` | The config template. Copy it to `config.toml`. |
|
|
376
|
+
| `demo.tape` | The [VHS](https://github.com/charmbracelet/vhs) script that records `demo.gif`. Run `vhs demo.tape`. |
|
|
377
|
+
| `examples/` | The example scripts. See [Scripts](#scripts). |
|
|
378
|
+
| `files/` | Where a script saves a file when `--output` names no other path. Git ignores this directory. |
|
|
379
|
+
| `swagger.yml` | A copy of the spec that the client read from the server at its last startup. `from_config()` writes it beside `config.toml`. Look up an `operationId` here when you write a new script. The client writes it again at each startup, and Git ignores it. |
|
|
380
|
+
|
|
381
|
+
## Licence
|
|
382
|
+
|
|
383
|
+
GPL v3, the licence of the whole Motion Master repository. See `LICENSE`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
motion_master_client.py,sha256=tDwpp3wgx_sNjCbpY06StoEzPVGxk4iPqAxIwZek3lA,44233
|
|
2
|
+
motion_master_objects.py,sha256=2HAZNASJyig3P-6RGDHms6KityAtXCkY3D_fGyKtMLM,114299
|
|
3
|
+
motion_master_client-6.0.0a124.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
4
|
+
motion_master_client-6.0.0a124.dist-info/METADATA,sha256=pHBYNIVUrj5ZhP1AbDYCju9cL5LrtNTS3YraXlfl7ss,22159
|
|
5
|
+
motion_master_client-6.0.0a124.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
motion_master_client-6.0.0a124.dist-info/top_level.txt,sha256=SLGCbwm4_HjW6x5RKe6DqJ3tj6YC9531ZkK59TS9uMQ,43
|
|
7
|
+
motion_master_client-6.0.0a124.dist-info/RECORD,,
|