pyks2 1.0.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.
- pyks2/__init__.py +69 -0
- pyks2/cli.py +251 -0
- pyks2/client.py +635 -0
- pyks2/constants.py +115 -0
- pyks2/errors.py +59 -0
- pyks2/events.py +225 -0
- pyks2/models.py +295 -0
- pyks2-1.0.0.dist-info/METADATA +212 -0
- pyks2-1.0.0.dist-info/RECORD +13 -0
- pyks2-1.0.0.dist-info/WHEEL +5 -0
- pyks2-1.0.0.dist-info/entry_points.txt +2 -0
- pyks2-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyks2-1.0.0.dist-info/top_level.txt +1 -0
pyks2/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""pyks2 — a Python client for the Pentax K-S2 built-in WiFi HTTP API.
|
|
2
|
+
|
|
3
|
+
A clean, camera-only library built on a complete, hardware-verified map of the
|
|
4
|
+
K-S2's undocumented WiFi API. See the docs/ directory for the full protocol
|
|
5
|
+
dissection and the reverse-engineering methodology.
|
|
6
|
+
|
|
7
|
+
Quick start:
|
|
8
|
+
>>> from pyks2 import K_S2_WiFi
|
|
9
|
+
>>> cam = K_S2_WiFi() # defaults to 192.168.0.1
|
|
10
|
+
>>> cam.ping()
|
|
11
|
+
True
|
|
12
|
+
>>> info = cam.capture(af="off") # baseline+shoot+wait in one, race-free
|
|
13
|
+
>>> cam.download(info.path, "shot.dng")
|
|
14
|
+
|
|
15
|
+
Event-driven (no polling):
|
|
16
|
+
>>> with cam.events() as ev:
|
|
17
|
+
... for change in ev:
|
|
18
|
+
... if change.is_storage:
|
|
19
|
+
... print("captured:", cam.latest_info().path)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from .client import K_S2_WiFi
|
|
25
|
+
from .errors import (
|
|
26
|
+
KS2APIError,
|
|
27
|
+
KS2ConnectionError,
|
|
28
|
+
KS2Error,
|
|
29
|
+
KS2NotFoundError,
|
|
30
|
+
KS2UnsupportedError,
|
|
31
|
+
)
|
|
32
|
+
from .events import ChangesClient
|
|
33
|
+
from .models import (
|
|
34
|
+
CameraConstants,
|
|
35
|
+
CameraParams,
|
|
36
|
+
ChangeEvent,
|
|
37
|
+
DeviceInfo,
|
|
38
|
+
LensState,
|
|
39
|
+
PhotoEntry,
|
|
40
|
+
PhotoInfo,
|
|
41
|
+
PhotoListing,
|
|
42
|
+
ShootResult,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__version__ = "1.0.0"
|
|
46
|
+
__author__ = "Jamal El Siblany (pickle)"
|
|
47
|
+
__email__ = "jamalsiblani@gmail.com"
|
|
48
|
+
__url__ = "https://github.com/PICKLERICK2005/pyks2"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"K_S2_WiFi",
|
|
52
|
+
"ChangesClient",
|
|
53
|
+
# models
|
|
54
|
+
"CameraParams",
|
|
55
|
+
"CameraConstants",
|
|
56
|
+
"LensState",
|
|
57
|
+
"DeviceInfo",
|
|
58
|
+
"PhotoInfo",
|
|
59
|
+
"PhotoEntry",
|
|
60
|
+
"PhotoListing",
|
|
61
|
+
"ShootResult",
|
|
62
|
+
"ChangeEvent",
|
|
63
|
+
# errors
|
|
64
|
+
"KS2Error",
|
|
65
|
+
"KS2ConnectionError",
|
|
66
|
+
"KS2APIError",
|
|
67
|
+
"KS2UnsupportedError",
|
|
68
|
+
"KS2NotFoundError",
|
|
69
|
+
]
|
pyks2/cli.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""pyks2 command-line interface.
|
|
2
|
+
|
|
3
|
+
A headless companion to the library — full camera control, scriptable from a
|
|
4
|
+
terminal. This is the "runs anywhere" surface (and the fallback for the planned
|
|
5
|
+
web GUI).
|
|
6
|
+
|
|
7
|
+
pyks2 ping
|
|
8
|
+
pyks2 info
|
|
9
|
+
pyks2 shoot [--af off]
|
|
10
|
+
pyks2 settings [get|set av=8.0 sv=400]
|
|
11
|
+
pyks2 browse [--limit N]
|
|
12
|
+
pyks2 download DIR/FILE [-o out] [--size view|full]
|
|
13
|
+
pyks2 liveview -o frame.jpg [--frames N]
|
|
14
|
+
pyks2 watch # stream /v1/changes events
|
|
15
|
+
pyks2 apis
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
from typing import List, Optional
|
|
23
|
+
|
|
24
|
+
from . import __version__
|
|
25
|
+
from .client import K_S2_WiFi
|
|
26
|
+
from .errors import KS2Error
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cam(args) -> K_S2_WiFi:
|
|
30
|
+
logger = (lambda m: print(f" · {m}", file=sys.stderr)) if args.verbose else None
|
|
31
|
+
return K_S2_WiFi(ip=args.ip, timeout=args.timeout, logger=logger)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# -- commands ---------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def cmd_ping(args) -> int:
|
|
37
|
+
cam = _cam(args)
|
|
38
|
+
ok = cam.ping()
|
|
39
|
+
print("OK" if ok else "unreachable")
|
|
40
|
+
return 0 if ok else 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_apis(args) -> int:
|
|
44
|
+
for a in _cam(args).apis():
|
|
45
|
+
print(a)
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_info(args) -> int:
|
|
50
|
+
cam = _cam(args)
|
|
51
|
+
dev = cam.get_device_info()
|
|
52
|
+
st = {}
|
|
53
|
+
try:
|
|
54
|
+
st = cam.status("camera")
|
|
55
|
+
except KS2Error:
|
|
56
|
+
pass
|
|
57
|
+
print(f"model : {dev.model}")
|
|
58
|
+
print(f"firmware : {dev.firmware_version}")
|
|
59
|
+
print(f"serial : {dev.serial_no}")
|
|
60
|
+
print(f"battery : {dev.battery}%")
|
|
61
|
+
print(f"state : {st.get('state')}")
|
|
62
|
+
for s in dev.storages:
|
|
63
|
+
print(f"storage : {s.get('name')} "
|
|
64
|
+
f"remain={s.get('remain')} format={s.get('format')}")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_shoot(args) -> int:
|
|
69
|
+
cam = _cam(args)
|
|
70
|
+
if args.wait:
|
|
71
|
+
# capture() records the baseline BEFORE firing, so completion
|
|
72
|
+
# detection can't be fooled by the pre-existing last image.
|
|
73
|
+
info = cam.capture(af=args.af, timeout=args.wait_timeout,
|
|
74
|
+
download_to=args.download, size=args.size)
|
|
75
|
+
print(f"captured: {info.path}")
|
|
76
|
+
if args.download:
|
|
77
|
+
print(f"downloaded -> {args.download}")
|
|
78
|
+
else:
|
|
79
|
+
res = cam.shoot(af=args.af)
|
|
80
|
+
print(f"triggered (focused={res.focused})")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cmd_settings(args) -> int:
|
|
85
|
+
cam = _cam(args)
|
|
86
|
+
if not args.assignments:
|
|
87
|
+
# GET
|
|
88
|
+
cp = cam.get_camera_params()
|
|
89
|
+
for k in ("av", "tv", "sv", "xv", "wb_mode", "shoot_mode",
|
|
90
|
+
"exposure_mode", "still_size", "effect", "filter"):
|
|
91
|
+
print(f"{k:14}: {getattr(cp, k)}")
|
|
92
|
+
return 0
|
|
93
|
+
# SET
|
|
94
|
+
kv = {}
|
|
95
|
+
for a in args.assignments:
|
|
96
|
+
if "=" not in a:
|
|
97
|
+
print(f"bad assignment {a!r}, expected key=value", file=sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
k, v = a.split("=", 1)
|
|
100
|
+
kv[k] = v
|
|
101
|
+
cam.set_camera_params(**kv)
|
|
102
|
+
print("set:", ", ".join(f"{k}={v}" for k, v in kv.items()))
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cmd_lists(args) -> int:
|
|
107
|
+
cc = _cam(args).get_camera_constants()
|
|
108
|
+
for name in ("av_list", "tv_list", "sv_list", "xv_list", "wb_mode_list",
|
|
109
|
+
"exposure_mode_list", "shoot_mode_list", "still_size_list",
|
|
110
|
+
"effect_list", "filter_list"):
|
|
111
|
+
vals = getattr(cc, name)
|
|
112
|
+
print(f"{name:20}: {', '.join(vals)}")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_focus(args) -> int:
|
|
117
|
+
res = _cam(args).focus(args.x, args.y)
|
|
118
|
+
print(f"focus: focused={res.focused}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def cmd_browse(args) -> int:
|
|
123
|
+
listing = _cam(args).list_photos(limit=args.limit)
|
|
124
|
+
for e in listing:
|
|
125
|
+
print(e.path)
|
|
126
|
+
print(f"# {len(listing)} file(s)", file=sys.stderr)
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cmd_download(args) -> int:
|
|
131
|
+
cam = _cam(args)
|
|
132
|
+
out = args.output or args.path.replace("/", "_")
|
|
133
|
+
n = cam.download(args.path, out, size=args.size)
|
|
134
|
+
print(f"downloaded {n:,} bytes -> {out}")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cmd_liveview(args) -> int:
|
|
139
|
+
cam = _cam(args)
|
|
140
|
+
n = 0
|
|
141
|
+
for frame in cam.iter_liveview_frames(max_frames=args.frames):
|
|
142
|
+
n += 1
|
|
143
|
+
path = args.output if args.frames == 1 else f"{args.output}.{n:04d}"
|
|
144
|
+
with open(path, "wb") as f:
|
|
145
|
+
f.write(frame)
|
|
146
|
+
print(f"frame {n}: {len(frame):,} bytes -> {path}")
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_bulb(args) -> int:
|
|
151
|
+
cam = _cam(args)
|
|
152
|
+
print(f"bulb exposure: {args.seconds}s (dial must be on B)...")
|
|
153
|
+
info = cam.bulb_exposure(args.seconds, af=args.af)
|
|
154
|
+
print(f"captured: {info.path}")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_watch(args) -> int:
|
|
159
|
+
cam = _cam(args)
|
|
160
|
+
print("watching /v1/changes (Ctrl-C to stop)...", file=sys.stderr)
|
|
161
|
+
try:
|
|
162
|
+
with cam.events() as ev:
|
|
163
|
+
for change in ev:
|
|
164
|
+
print(f"changed: {change.changed}")
|
|
165
|
+
if change.is_storage and args.resolve:
|
|
166
|
+
try:
|
|
167
|
+
print(f" -> {cam.latest_info().path}")
|
|
168
|
+
except KS2Error:
|
|
169
|
+
pass
|
|
170
|
+
except KeyboardInterrupt:
|
|
171
|
+
print()
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# -- parser -----------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
178
|
+
p = argparse.ArgumentParser(
|
|
179
|
+
prog="pyks2", description="Pentax K-S2 WiFi control (CLI).")
|
|
180
|
+
p.add_argument("--version", action="version",
|
|
181
|
+
version=f"pyks2 {__version__}")
|
|
182
|
+
p.add_argument("--ip", default="192.168.0.1", help="camera IP")
|
|
183
|
+
p.add_argument("--timeout", type=float, default=15.0)
|
|
184
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
185
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
186
|
+
|
|
187
|
+
sub.add_parser("ping", help="check connectivity").set_defaults(func=cmd_ping)
|
|
188
|
+
sub.add_parser("apis", help="list all endpoints").set_defaults(func=cmd_apis)
|
|
189
|
+
sub.add_parser("info", help="device + status summary").set_defaults(func=cmd_info)
|
|
190
|
+
sub.add_parser("lists", help="capability lists").set_defaults(func=cmd_lists)
|
|
191
|
+
|
|
192
|
+
sp = sub.add_parser("shoot", help="fire the shutter")
|
|
193
|
+
sp.add_argument("--af", choices=["auto", "on", "off"], default=None,
|
|
194
|
+
help="AF mode (default: auto-detect from AF/MF lever)")
|
|
195
|
+
sp.add_argument("--wait", action="store_true", help="wait for capture")
|
|
196
|
+
sp.add_argument("--wait-timeout", type=float, default=30.0)
|
|
197
|
+
sp.add_argument("--download", metavar="OUT", help="download the shot after")
|
|
198
|
+
sp.add_argument("--size", choices=["view", "full"], default="full")
|
|
199
|
+
sp.set_defaults(func=cmd_shoot)
|
|
200
|
+
|
|
201
|
+
sp = sub.add_parser("settings", help="get or set camera settings")
|
|
202
|
+
sp.add_argument("assignments", nargs="*", metavar="key=value",
|
|
203
|
+
help="omit to GET; provide to SET (e.g. av=8.0 sv=400)")
|
|
204
|
+
sp.set_defaults(func=cmd_settings)
|
|
205
|
+
|
|
206
|
+
sp = sub.add_parser("focus", help="drive AF / set point")
|
|
207
|
+
sp.add_argument("-x", type=int, default=52)
|
|
208
|
+
sp.add_argument("-y", type=int, default=52)
|
|
209
|
+
sp.set_defaults(func=cmd_focus)
|
|
210
|
+
|
|
211
|
+
sp = sub.add_parser("browse", help="list photos on the card")
|
|
212
|
+
sp.add_argument("--limit", type=int, default=None)
|
|
213
|
+
sp.set_defaults(func=cmd_browse)
|
|
214
|
+
|
|
215
|
+
sp = sub.add_parser("download", help="download a photo")
|
|
216
|
+
sp.add_argument("path", help="DIR/FILE")
|
|
217
|
+
sp.add_argument("-o", "--output")
|
|
218
|
+
sp.add_argument("--size", choices=["view", "full"], default=None)
|
|
219
|
+
sp.set_defaults(func=cmd_download)
|
|
220
|
+
|
|
221
|
+
sp = sub.add_parser("liveview", help="grab live-view frame(s)")
|
|
222
|
+
sp.add_argument("-o", "--output", default="frame.jpg")
|
|
223
|
+
sp.add_argument("--frames", type=int, default=1)
|
|
224
|
+
sp.set_defaults(func=cmd_liveview)
|
|
225
|
+
|
|
226
|
+
sp = sub.add_parser("bulb", help="timed bulb exposure (dial must be on B)")
|
|
227
|
+
sp.add_argument("seconds", type=float, help="exposure length in seconds")
|
|
228
|
+
sp.add_argument("--af", default="off", choices=["auto", "on", "off"])
|
|
229
|
+
sp.set_defaults(func=cmd_bulb)
|
|
230
|
+
|
|
231
|
+
sp = sub.add_parser("watch", help="stream /v1/changes events")
|
|
232
|
+
sp.add_argument("--resolve", action="store_true",
|
|
233
|
+
help="on storage events, print the new file path")
|
|
234
|
+
sp.set_defaults(func=cmd_watch)
|
|
235
|
+
|
|
236
|
+
return p
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
240
|
+
args = build_parser().parse_args(argv)
|
|
241
|
+
try:
|
|
242
|
+
return args.func(args)
|
|
243
|
+
except KS2Error as e:
|
|
244
|
+
print(f"error: {e}", file=sys.stderr)
|
|
245
|
+
return 1
|
|
246
|
+
except KeyboardInterrupt:
|
|
247
|
+
return 130
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
sys.exit(main())
|