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 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())