├── .gitattributes ├── .gitignore ├── README.md ├── Yi4kAPI.do ├── __init__.py ├── changelog.txt ├── test ├── kilog.py └── testApi.py ├── yiAPI.py ├── yiAPICommand.py └── yiAPIListener.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | *.sublime-project 49 | *.sublime-workspace 50 | *.sublime-commands 51 | gitk.exe 52 | tmp.py 53 | __pycache__ 54 | build 55 | dist 56 | *.do.cfg 57 | *.do.state 58 | .gitignore 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yi4kAPI 2 | Python API for Yi 4k camera. 3 | 4 | It is based on official Yi 4k Java API. 5 | Differences from original API: 6 | - Commands are blocking waiting for result, while in original API they return promices. 7 | - 'stopRecording' and 'capturePhoto' waits till operation is really ended and return produced file name. 8 | - Values provided to commands should be correct strings, compared to integers in original API. 9 | - 'adapter', 'adapter_status' and 'battery_status' callbacks are available in addition to all other. .setCB() used to set/get callbacks. 10 | - 'deleteFile' and 'getFileList' commands require path relative to DCIM folder. 11 | 12 | Try not to call same commands simultaneously from different threads: camera response is not clearly identified with caller and response can be mixed. 13 | 14 | ###Commands not implemented: 15 | 16 | - formatSDCard 17 | NSFW 18 | 19 | - downloadFile 20 | - cancelDownload 21 | Redundant, available by http 22 | 23 | - getRtspURL 24 | Redundant, available at YiAPI() creation 25 | 26 | - buildLiveVideoQRCode 27 | Maybe later 28 | 29 | - startRecording datetime 30 | Lazy to implement due to specific input value format 31 | -------------------------------------------------------------------------------- /Yi4kAPI.do: -------------------------------------------------------------------------------- 1 | -feature 1: +0 "yiClasses.py" kii 17/01/12 17:35:58 2 | add error constants 3 | 4 | feature 2: +0 "yiClasses.py" kii 17/01/12 21:23:35 5 | implement nonblocking execution, use callbacks 6 | 7 | !feature 3: +0 "yiClasses.py" kii 17/05/21 04:42:41 8 | predefined limit of supplied variables 9 | 10 | !feature 4: +0 "" kii 17/01/15 05:17:26 11 | redundant 12 | 13 | !feature 5: +0 "yiClasses.py" kii 17/04/02 15:11:56 14 | continously recieve result 15 | 16 | +ux 6: +0 "__init__.py" kii 17/05/21 04:41:47 17 | add sample usecase 18 | 19 | + 8: +0 "" kii 17/05/21 04:41:19 20 | detect disconnect 21 | 22 | +check 9: +0 "yiAPI.py" kii 17/05/21 04:41:21 23 | test if disconnected 24 | 25 | +fix 10: +0 "__init__.py" kii 17/05/21 04:41:44 26 | getFileList not listing all files 27 | 28 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Lightweighted version of Yi4k API, based on official Java API. 3 | Values provided to commands should be correct strings. 4 | 5 | deleteFile and getFileList commands require path relative to DCIM folder. 6 | deleteFile though could be unsafe by supplying '..' path. 7 | 8 | Commands not implemented: 9 | 10 | formatSDCard 11 | NSFW 12 | 13 | downloadFile 14 | cancelDownload 15 | Redundant, available by http 16 | 17 | getRtspURL 18 | Redundant, available at YiAPI() creation 19 | 20 | buildLiveVideoQRCode 21 | Maybe later 22 | 23 | startRecording datetime 24 | Lazy to implement due to specific input value format 25 | ''' 26 | 27 | 28 | 29 | from .yiAPI import * 30 | 31 | 32 | ''' 33 | Define public commands 34 | ''' 35 | 36 | startRecording= YiAPICommandGen(513, 'startRecording') 37 | stopRecording= YiAPICommandGen(514, 'stopRecording', 38 | resultReq= {'msg_id': 7, 'type': 'video_record_complete'} 39 | ) 40 | 41 | capturePhoto= YiAPICommandGen(16777220, 'capturePhoto', 42 | params= {'param':'precise quality;off'}, 43 | resultReq= {'msg_id': 7, 'type': 'photo_taken'} 44 | ) 45 | 46 | getFileList= YiAPICommandGen(1282, 'getFileList', 47 | params= {'param':'/tmp/fuse_d/DCIM/'}, 48 | variable= 'param', 49 | resultCB= lambda res: res['listing'] 50 | ) 51 | 52 | deleteFile= YiAPICommandGen(1281, 'deleteFile', 53 | params= {'param':'/tmp/fuse_d/DCIM/'}, 54 | variable= 'param' 55 | ) 56 | 57 | startViewFinder= YiAPICommandGen(259, 'startViewFinder') 58 | stopViewFinder= YiAPICommandGen(260, 'stopViewFinder') 59 | 60 | 61 | getSettings= YiAPICommandGen(3, 'getSettings', 62 | resultCB= lambda res:{key:val for d in res['param'] for key,val in d.items()} 63 | ) 64 | 65 | setDateTime= YiAPICommandGen(2, 'setDateTime "yyyy-MM-dd HH:mm:ss"', 66 | params= {'type':'camera_clock'}, 67 | variable= 'param' 68 | ) 69 | 70 | setSystemMode= YiAPICommandGen(2, 'setSystemMode', 71 | values= ["capture", "record"], 72 | params= {'type':'system_mode'}, 73 | variable= 'param' 74 | ) 75 | 76 | getVideoResolution= YiAPICommandGen(1, 'getVideoResolution', 77 | params={'type':'video_resolution'} 78 | ) 79 | setVideoResolution= YiAPICommandGen(2, 'setVideoResolution', 80 | values= ["3840x2160 30P 16:9", "3840x2160 30P 16:9 super", "2560x1920 30P 4:3", "1920x1440 60P 4:3", "1920x1440 30P 4:3", "1920x1080 120P 16:9", "1920x1080 120P 16:9 super", "1920x1080 60P 16:9", "1920x1080 60P 16:9 super", "1920x1080 30P 16:9", "1920x1080 30P 16:9 super", "1280x960 120P 4:3", "1280x960 60P 4:3", "1280x720 240P 16:9", "1280x720 120P 16:9 super", "1280x720 60P 16:9 super", "840x480 240P 16:9"], 81 | params= {'type':'video_resolution'}, 82 | variable= 'param' 83 | ) 84 | 85 | getPhotoResolution= YiAPICommandGen(1, 'getPhotoResolution', 86 | params={'type':'photo_size'} 87 | ) 88 | setPhotoResolution= YiAPICommandGen(2, 'setPhotoResolution', 89 | values= ["12MP (4000x3000 4:3) fov:w", "7MP (3008x2256 4:3) fov:w", "7MP (3008x2256 4:3) fov:m", "5MP (2560x1920 4:3) fov:m", "8MP (3840x2160 16:9) fov:w"], 90 | params= {'type':'photo_size'}, 91 | variable= 'param' 92 | ) 93 | 94 | getPhotoWhiteBalance= YiAPICommandGen(1, 'getPhotoWhiteBalance', 95 | params={'type':'iq_photo_wb'} 96 | ) 97 | setPhotoWhiteBalance= YiAPICommandGen(2, 'setPhotoWhiteBalance', 98 | values= ["auto", "native", "3000k", "5500k", "6500k"], 99 | params= {'type':'iq_photo_wb'}, 100 | variable= 'param' 101 | ) 102 | 103 | getVideoWhiteBalance= YiAPICommandGen(1, 'getVideoWhiteBalance', 104 | params={'type':'iq_video_wb'} 105 | ) 106 | setVideoWhiteBalance= YiAPICommandGen(2, 'setVideoWhiteBalance', 107 | values= ["auto", "native", "3000k", "5500k", "6500k"], 108 | params= {'type':'iq_video_wb'}, 109 | variable= 'param' 110 | ) 111 | 112 | getPhotoISO= YiAPICommandGen(1, 'getPhotoISO', 113 | params={'type':'iq_photo_iso'} 114 | ) 115 | setPhotoISO= YiAPICommandGen(2, 'setPhotoISO', 116 | values= ["auto", "100", "200", "400", "800", "1600", "6400"], 117 | params= {'type':'iq_photo_iso'}, 118 | variable= 'param' 119 | ) 120 | 121 | getVideoISO= YiAPICommandGen(1, 'getVideoISO', 122 | params={'type':'iq_video_iso'} 123 | ) 124 | setVideoISO= YiAPICommandGen(2, 'setVideoISO', 125 | values= ["auto", "100", "200", "400", "800", "1600", "6400"], 126 | params= {'type':'iq_video_iso'}, 127 | variable= 'param' 128 | ) 129 | 130 | getPhotoExposureValue= YiAPICommandGen(1, 'getPhotoExposureValue', 131 | params={'type':'iq_photo_ev'} 132 | ) 133 | setPhotoExposureValue= YiAPICommandGen(2, 'setPhotoExposureValue', 134 | values= ["-2.0", "-1.5", "-1.0", "-0.5", "0", "+0.5", "+1.0", "+1.5", "+2.0"], 135 | params= {'type':'iq_photo_ev'}, 136 | variable= 'param' 137 | ) 138 | 139 | getVideoExposureValue= YiAPICommandGen(1, 'getVideoExposureValue', 140 | params={'type':'iq_video_ev'} 141 | ) 142 | setVideoExposureValue= YiAPICommandGen(2, 'setVideoExposureValue', 143 | values= ["-2.0", "-1.5", "-1.0", "-0.5", "0", "+0.5", "+1.0", "+1.5", "+2.0"], 144 | params= {'type':'iq_video_ev'}, 145 | variable= 'param' 146 | ) 147 | 148 | getPhotoShutterTime= YiAPICommandGen(1, 'getPhotoShutterTime', 149 | params={'type':'iq_photo_shutter'} 150 | ) 151 | setPhotoShutterTime= YiAPICommandGen(2, 'setPhotoShutterTime', 152 | values= ["auto", "2s", "5s", "10s", "20s", "30s"], 153 | params= {'type':'iq_photo_shutter'}, 154 | variable= 'param' 155 | ) 156 | 157 | getVideoSharpness= YiAPICommandGen(1, 'getVideoSharpness', 158 | params={'type':'video_sharpness'} 159 | ) 160 | setVideoSharpness= YiAPICommandGen(2, 'setVideoSharpness', 161 | values= ["low", "medium", "high"], 162 | params= {'type':'video_sharpness'}, 163 | variable= 'param' 164 | ) 165 | 166 | getPhotoSharpness= YiAPICommandGen(1, 'getPhotoSharpness', 167 | params={'type':'photo_sharpness'} 168 | ) 169 | setPhotoSharpness= YiAPICommandGen(2, 'setPhotoSharpness', 170 | values= ["low", "medium", "high"], 171 | params= {'type':'photo_sharpness'}, 172 | variable= 'param' 173 | ) 174 | 175 | getVideoFieldOfView= YiAPICommandGen(1, 'getVideoFieldOfView', 176 | params={'type':'fov'} 177 | ) 178 | setVideoFieldOfView= YiAPICommandGen(2, 'setVideoFieldOfView', 179 | values= ["wide", "medium", "narrow"], 180 | params= {'type':'fov'}, 181 | variable= 'param' 182 | ) 183 | 184 | getRecordMode= YiAPICommandGen(1, 'getRecordMode', 185 | params={'type':'rec_mode'} 186 | ) 187 | setRecordMode= YiAPICommandGen(2, 'setRecordMode', 188 | values= ["record", "record_timelapse", "record_slow_motion", "record_loop", "record_photo"], 189 | params= {'type':'rec_mode'}, 190 | variable= 'param' 191 | ) 192 | 193 | getCaptureMode= YiAPICommandGen(1, 'getCaptureMode', 194 | params={'type':'capture_mode'} 195 | ) 196 | setCaptureMode= YiAPICommandGen(2, 'setCaptureMode', 197 | ["precise quality", "precise self quality", "burst quality", "precise quality cont."], 198 | {'type':'capture_mode'}, 199 | variable= 'param' 200 | ) 201 | 202 | getMeteringMode= YiAPICommandGen(1, 'getMeteringMode', 203 | params={'type':'meter_mode'} 204 | ) 205 | setMeteringMode= YiAPICommandGen(2, 'setMeteringMode', 206 | values= ["center", "average", "spot"], 207 | params= {'type':'meter_mode'}, 208 | variable= 'param' 209 | ) 210 | 211 | getVideoQuality= YiAPICommandGen(1, 'getVideoQuality', 212 | params={'type':'video_quality'} 213 | ) 214 | setVideoQuality= YiAPICommandGen(2, 'setVideoQuality', 215 | values= ["S.Fine", "Fine", "Normal"], 216 | params= {'type':'video_quality'}, 217 | variable= 'param' 218 | ) 219 | 220 | getVideoColorMode= YiAPICommandGen(1, 'getVideoColorMode', 221 | params={'type':'video_flat_color'} 222 | ) 223 | setVideoColorMode= YiAPICommandGen(2, 'setVideoColorMode', 224 | values= ["yi", "flat"], 225 | params= {'type':'video_flat_color'}, 226 | variable= 'param' 227 | ) 228 | 229 | getPhotoColorMode= YiAPICommandGen(1, 'getPhotoColorMode', 230 | params={'type':'photo_flat_color'} 231 | ) 232 | setPhotoColorMode= YiAPICommandGen(2, 'setPhotoColorMode', 233 | values= ["yi", "flat"], 234 | params= {'type':'photo_flat_color'}, 235 | variable= 'param' 236 | ) 237 | 238 | getElectronicImageStabilizationState= YiAPICommandGen(1, 'getElectronicImageStabilizationState', 239 | params={'type':'iq_eis_enable'} 240 | ) 241 | setElectronicImageStabilizationState= YiAPICommandGen(2, 'setElectronicImageStabilizationState', 242 | values= ["on", "off"], 243 | params= {'type':'iq_eis_enable'}, 244 | variable= 'param' 245 | ) 246 | 247 | getAdjustLensDistortionState= YiAPICommandGen(1, 'getAdjustLensDistortionState', 248 | params={'type':'warp_enable'} 249 | ) 250 | setAdjustLensDistortionState= YiAPICommandGen(2, 'setAdjustLensDistortionState', 251 | values= ["on", "off"], 252 | params= {'type':'warp_enable'}, 253 | variable= 'param' 254 | ) 255 | 256 | getVideoMuteState= YiAPICommandGen(1, 'getVideoMuteState', 257 | params={'type':'video_mute_set'} 258 | ) 259 | setVideoMuteState= YiAPICommandGen(2, 'setVideoMuteState', 260 | values= ["on", "off"], 261 | params= {'type':'video_mute_set'}, 262 | variable= 'param' 263 | ) 264 | 265 | getVideoTimestamp= YiAPICommandGen(1, 'getVideoTimestamp', 266 | params={'type':'video_stamp'} 267 | ) 268 | setVideoTimestamp= YiAPICommandGen(2, 'setVideoTimestamp', 269 | values= ["off", "time", "date", "date/time"], 270 | params= {'type':'video_stamp'}, 271 | variable= 'param' 272 | ) 273 | 274 | getPhotoTimestamp= YiAPICommandGen(1, 'getPhotoTimestamp', 275 | params={'type':'photo_stamp'} 276 | ) 277 | setPhotoTimestamp= YiAPICommandGen(2, 'setPhotoTimestamp', 278 | values= ["off", "time", "date", "date/time"], 279 | params= {'type':'photo_stamp'}, 280 | variable= 'param' 281 | ) 282 | 283 | getLEDMode= YiAPICommandGen(1, 'getLEDMode', 284 | params={'type':'led_mode'} 285 | ) 286 | setLEDMode= YiAPICommandGen(2, 'setLEDMode', 287 | values= ["all enable", "all disable", "status enable"], 288 | params= {'type':'led_mode'}, 289 | variable= 'param' 290 | ) 291 | 292 | getVideoStandard= YiAPICommandGen(1, 'getVideoStandard', 293 | params={'type':'video_standard'} 294 | ) 295 | setVideoStandard= YiAPICommandGen(2, 'setVideoStandard', 296 | values= ["PAL", "NTSC"], 297 | params= {'type':'video_standard'}, 298 | variable= 'param' 299 | ) 300 | 301 | getTimeLapseVideoInterval= YiAPICommandGen(1, 'getTimeLapseVideoInterval', 302 | params={'type':'timelapse_video'} 303 | ) 304 | setTimeLapseVideoInterval= YiAPICommandGen(2, 'setTimeLapseVideoInterval', 305 | values= ["0.5", "1", "2", "5", "10", "30", "60"], 306 | params= {'type':'timelapse_video'}, 307 | variable= 'param' 308 | ) 309 | 310 | getTimeLapsePhotoInterval= YiAPICommandGen(1, 'getTimeLapsePhotoInterval', 311 | params={'type':'precise_cont_time'} 312 | ) 313 | setTimeLapsePhotoInterval= YiAPICommandGen(2, 'setTimeLapsePhotoInterval', 314 | values= ["continue", "0.5 sec", "1.0 sec", "2.0 sec", "5.0 sec", "10.0 sec", "30.0 sec", "60.0 sec", "2.0 min", "5.0 min", "10.0 min", "30.0 min", "60.0 min"], 315 | params= {'type':'precise_cont_time'}, 316 | variable= 'param' 317 | ) 318 | 319 | getTimeLapseVideoDuration= YiAPICommandGen(1, 'getTimeLapseVideoDuration', 320 | params={'type':'timelapse_video_duration'} 321 | ) 322 | setTimeLapseVideoDuration= YiAPICommandGen(2, 'setTimeLapseVideoDuration', 323 | values= ["off", "6s", "8s", "10s", "20s", "30s", "60s", "120s"], 324 | params= {'type':'timelapse_video_duration'}, 325 | variable= 'param' 326 | ) 327 | 328 | getScreenAutoLock= YiAPICommandGen(1, 'getScreenAutoLock', 329 | params={'type':'screen_auto_lock'} 330 | ) 331 | setScreenAutoLock= YiAPICommandGen(2, 'setScreenAutoLock', 332 | values= ["never", "30s", "60s", "120s"], 333 | params= {'type':'screen_auto_lock'}, 334 | variable= 'param' 335 | ) 336 | 337 | getAutoPowerOff= YiAPICommandGen(1, 'getAutoPowerOff', 338 | params={'type':'auto_power_off'} 339 | ) 340 | setAutoPowerOff= YiAPICommandGen(2, 'setAutoPowerOff', 341 | values= ["off", "3 minutes", "5 minutes", "10 minutes"], 342 | params= {'type':'auto_power_off'}, 343 | variable= 'param' 344 | ) 345 | 346 | getVideoRotateMode= YiAPICommandGen(1, 'getVideoRotateMode', 347 | params={'type':'video_rotate'} 348 | ) 349 | setVideoRotateMode= YiAPICommandGen(2, 'setVideoRotateMode', 350 | values= ["off", "on", "auto"], 351 | params= {'type':'video_rotate'}, 352 | variable= 'param' 353 | ) 354 | 355 | getBuzzerVolume= YiAPICommandGen(1, 'getBuzzerVolume', 356 | params={'type':'buzzer_volume'} 357 | ) 358 | setBuzzerVolume= YiAPICommandGen(2, 'setBuzzerVolume', 359 | values= ["high", "low", "mute"], 360 | params= {'type':'buzzer_volume'}, 361 | variable= 'param' 362 | ) 363 | 364 | getLoopDuration= YiAPICommandGen(1, 'getLoopDuration', 365 | params={'type':'loop_rec_duration'} 366 | ) 367 | setLoopDuration= YiAPICommandGen(2, 'setLoopDuration', 368 | values= ["5 minutes", "20 minutes", "60 minutes", "120 minutes", "max"], 369 | params= {'type':'loop_rec_duration'}, 370 | variable= 'param' 371 | ) 372 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | v1.4 2 | add: 3 | - handle connection loss 4 | - deleteFile command, path is relative to DCIM folder 5 | fix: 6 | - getFileList accepts path relative to DCIM folder 7 | 8 | v1.3 9 | add: 10 | - 'start_album' and 'stop_album' events. 11 | 12 | fix: 13 | - clean blocking Timer after command done. 14 | - *Viewfinder commands dont wait event enymore. 15 | 16 | 17 | v1.2 18 | More flexible communicating, tho still blocking. 19 | 20 | add: 21 | - 'stopRecording' and 'capturePhoto' waits for actual end of execution, returning file name. 22 | - YiAPI.setCB(name, callback) for background callbacks. 23 | - 'adapter', 'adapter_status' and 'battery_status' callbacks in addition to original ones. 24 | 25 | v1.1 26 | Bugfix 27 | 28 | v1.0 29 | Basic Yi4k API functionality. 30 | 31 | add: 32 | - Most of commands to communicate with camera. 33 | 34 | limits: 35 | - communication are blocking. 36 | - values provided to commands should be correct strings, which list is stored at YiAPICommandGen.values -------------------------------------------------------------------------------- /test/kilog.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Modify logging output by adding class name 3 | ''' 4 | import logging, inspect 5 | 6 | 7 | DEBUG= logging.DEBUG 8 | INFO= logging.INFO 9 | WARNING= logging.WARNING 10 | ERROR= logging.ERROR 11 | CRITICAL= logging.CRITICAL 12 | 13 | 14 | namesAllowed= {} 15 | 16 | def hook(_name, _level, _fn, _ln, _msg, _args, _exInfo, _func, _stack): 17 | stack= inspect.stack() 18 | callObj= stack[5][0].f_locals #skip stack to first 'outer' level 19 | _name= ( 20 | ('self' in callObj) 21 | and hasattr(callObj['self'],'__class__') 22 | and callObj['self'].__class__.__name__ 23 | or '' 24 | ) 25 | 26 | if len(namesAllowed): 27 | if not (_name in namesAllowed): 28 | _level= -1 29 | 30 | return logging.LogRecord(_name, _level, _fn, _ln, _msg, _args, _exInfo) 31 | 32 | logging.setLogRecordFactory(hook) 33 | 34 | 35 | 36 | def names(_names, _state=True): 37 | for name in _names: 38 | if _state: 39 | namesAllowed[name]= True 40 | continue 41 | 42 | if name in namesAllowed: 43 | del namesAllowed[name] 44 | 45 | 46 | def config(level= WARNING): 47 | logging.basicConfig(level= level, format= '%(name)s %(levelname)s: %(message)s') 48 | 49 | config(DEBUG) 50 | -------------------------------------------------------------------------------- /test/testApi.py: -------------------------------------------------------------------------------- 1 | import kilog 2 | kilog.names(('',)) 3 | 4 | ''' 5 | Bunch of Yi commands to test. 6 | ''' 7 | 8 | 9 | import sys, os 10 | sys.path.append(os.path.abspath('../..')) 11 | 12 | from Yi4kAPI import * 13 | 14 | 15 | 16 | import time 17 | 18 | 19 | def vid_ok(_res): 20 | print('Video recorded:', _res['param'], _res['msize'], 'bytes') 21 | print(_res) 22 | 23 | def set_ok(_res): 24 | print('Setting changed') 25 | 26 | a= YiAPI() 27 | a.setCB('video_record_complete', vid_ok) 28 | a.setCB('setting_changed', set_ok) 29 | 30 | print('Started: %s' % str(bool(a))) 31 | res= a.cmd(getSettings) 32 | print('getSettings: %s' % str(res)) 33 | res= a.cmd(stopRecording) 34 | print('(err:stopped) stopRecording: %s' % str(res)) 35 | print('=============Paused to test manual recording start/stop and shange settings') 36 | time.sleep(20) 37 | print('') 38 | 39 | 40 | resEv= a.cmd(getVideoExposureValue) 41 | print('getVideoExposureValue: %s' % str(resEv)) 42 | res= a.cmd(setVideoExposureValue, '+1.5') 43 | print('setVideoExposureValue: %s' % str(res)) 44 | res= a.cmd(startRecording) 45 | print('startRecording: %s' % str(res)) 46 | 47 | time.sleep(4) 48 | print('Video') 49 | 50 | res= a.cmd(stopRecording) 51 | print('stopRecording: %s' % str(res)) 52 | res= a.cmd(deleteFile, str(res).split('/tmp/fuse_d/DCIM/')[1]) 53 | print('deleted: %s' % str(res)) 54 | res= a.cmd(setVideoExposureValue, resEv) 55 | print('setVideoExposureValue: %s' % str(res)) 56 | time.sleep(4) 57 | print('=============') 58 | print('') 59 | 60 | res= a.cmd(capturePhoto) 61 | print('(err:mode) capturePhoto: %s' % str(res)) 62 | res= a.cmd(capturePhoto) 63 | print('capturePhoto: %s' % str(res)) 64 | res= a.cmd(capturePhoto) 65 | print('capturePhoto: %s' % str(res)) 66 | time.sleep(4) 67 | print('=============') 68 | print('') 69 | 70 | #+ 71 | res= a.cmd(getFileList) 72 | print('getFileList: %s' % str(res)) 73 | res= a.cmd(capturePhoto) 74 | print('capturePhoto: %s' % str(res)) 75 | time.sleep(4) 76 | print('=============') 77 | print('') 78 | 79 | #+ 80 | res= a.cmd(startViewFinder) 81 | print('startViewFinder: %s' % str(res)) 82 | res= a.cmd(stopViewFinder) 83 | print('stopViewFinder: %s' % str(res)) 84 | res= a.cmd(capturePhoto) 85 | print('capturePhoto: %s' % str(res)) 86 | time.sleep(4) 87 | print('=============') 88 | print('') 89 | 90 | #+ 91 | res= a.cmd(setDateTime, '2017-02-24 00:25:01') 92 | print('setDateTime: %s' % str(res)) 93 | res= a.cmd(capturePhoto) 94 | print('capturePhoto: %s' % str(res)) 95 | time.sleep(4) 96 | print('=============') 97 | print('') 98 | 99 | #+ 100 | res= a.cmd(setSystemMode, "record") 101 | print('setSystemMode: %s' % str(res)) 102 | res= a.cmd(capturePhoto) 103 | print('capturePhoto: %s' % str(res)) 104 | time.sleep(4) 105 | print('=============') 106 | print('') 107 | 108 | res= a.cmd(setRecordMode, "record") 109 | print('setRecordMode: %s' % str(res)) 110 | res= a.cmd(capturePhoto) 111 | print('(err:mode) capturePhoto: %s' % str(res)) 112 | time.sleep(4) 113 | print('=============') 114 | print('') 115 | 116 | res= a.cmd(setRecordMode, "record") 117 | print('setRecordMode: %s' % str(res)) 118 | res= a.cmd(setCaptureMode, "precise quality") 119 | print('setCaptureMode: %s' % str(res)) 120 | res= a.cmd(capturePhoto) 121 | print('capturePhoto: %s' % str(res)) 122 | time.sleep(4) 123 | print('=============') 124 | print('') 125 | 126 | res= a.cmd(setRecordMode, "record_loop") 127 | print('setRecordMode: %s' % str(res)) 128 | 129 | res= a.close() 130 | print('Stop state: %s' % str(res)) 131 | -------------------------------------------------------------------------------- /yiAPI.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import socket, json, threading 4 | 5 | from .yiAPICommand import * 6 | from .yiAPIListener import * 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | class YiAPI(): 16 | #private commands 17 | startSession= YiAPICommandGen(257) 18 | stopSession= YiAPICommandGen(258) 19 | 20 | 21 | ip= '192.168.42.1' 22 | sock= None 23 | tick= 0 24 | sessionId= 0 25 | 26 | commandTimeout= 10 27 | connectionTimeout= .5 28 | connectionTries= 10 29 | 30 | listener= None 31 | 32 | 33 | 34 | @staticmethod 35 | def defaults(ip): 36 | if ip: 37 | YiAPI.ip= ip 38 | 39 | 40 | 41 | def __init__(self, _ip=None): 42 | if _ip: 43 | self.ip= _ip 44 | 45 | #sometimes camera couldnt be connected after a pause. Try to connect several times. 46 | for i in range(0,self.connectionTries): 47 | try: 48 | self.sock= socket.create_connection((self.ip,7878),self.connectionTimeout) 49 | break 50 | except: 51 | None 52 | 53 | if not self.sock: 54 | logging.critical('Not connected') 55 | return 56 | 57 | 58 | self.sock.settimeout(None) 59 | self.listener= YiAPIListener(self.sock) 60 | 61 | res= self.cmd(self.startSession) 62 | if res<0: 63 | self.sock= None 64 | else: 65 | self.sessionId= res 66 | 67 | 68 | #shoud be called at very end to tell camera it's released 69 | def close(self): 70 | self.cmd(self.stopSession) 71 | 72 | if self.sock: 73 | self.sock.close() 74 | self.sock= None 75 | 76 | 77 | 78 | ''' 79 | Run predefined _command. 80 | if _vals provided, it's a value assigned to YiAPICommand.values respectively. 81 | ''' 82 | def cmd(self, _command, _val=None): 83 | if not self.sock: 84 | logging.error('Camera disconnected') 85 | return -99999 86 | 87 | 88 | runCmd= _command.makeCmd({'token':self.sessionId, 'heartbeat':self.tick}, _val) 89 | self.listener.instantCB(runCmd) 90 | 91 | timeoutCmd= threading.Timer(self.commandTimeout, runCmd.blockingEvent.set) 92 | timeoutCmd.start() 93 | 94 | if not self.cmdSend(runCmd.cmdSend): 95 | logging.critical('Socket error while sending') 96 | 97 | runCmd.blockingEvent.set() 98 | self.sock= None 99 | 100 | runCmd.blockingEvent.wait() 101 | timeoutCmd.cancel() 102 | 103 | logging.debug('Result %s' % runCmd.resultDict) 104 | 105 | return runCmd.result() 106 | 107 | 108 | 109 | ''' 110 | Sent YiAPICommandGen co camera. 111 | ''' 112 | def cmdSend(self, _cmdDict): 113 | logging.debug("Send %s" % _cmdDict) 114 | 115 | try: 116 | self.sock.sendall( bytes(json.dumps(_cmdDict),'ascii') ) 117 | except: 118 | return 119 | 120 | self.tick+= 1 121 | return True 122 | 123 | 124 | 125 | def setCB(self, _type=None, _cb=None): 126 | if not self.listener: 127 | return 128 | 129 | return self.listener.setCB(_type, _cb) 130 | -------------------------------------------------------------------------------- /yiAPICommand.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | ''' 5 | Public available commands. 6 | ''' 7 | commands= [] 8 | 9 | 10 | ''' 11 | Class usable to pass to YiAPI.cmd() 12 | .makeCmd() is called then to generate runtime command object. 13 | 14 | params 15 | dict of non-changing parameters. 16 | 17 | variable 18 | name or list of names to be assigned later with makeCmd(). 19 | If variable is defined in params as string, it's appended. 20 | ''' 21 | class YiAPICommandGen(): 22 | resultReq= None 23 | resultCB= None 24 | 25 | commandName= '' 26 | params= None 27 | 28 | variable= None 29 | values= None #available values list to assign to .variable. Logging use only. 30 | 31 | 32 | def __init__(self, 33 | _id, 34 | commandName='', 35 | values=None, 36 | params=None, 37 | variable=[], 38 | resultReq=None, 39 | resultCB=None 40 | ): 41 | self.resultReq= resultReq 42 | self.resultCB= resultCB 43 | 44 | self.params= {'msg_id':int(_id)} 45 | if params: 46 | self.params.update(params) 47 | 48 | if not isinstance(variable, (list, tuple)): 49 | variable= [variable] 50 | 51 | self.variable= variable 52 | 53 | 54 | if values: 55 | self.values= values 56 | 57 | 58 | if commandName: 59 | self.commandName= commandName 60 | commands.append(self) 61 | 62 | 63 | 64 | ''' 65 | Create object representing command at runtime. 66 | Append stored params to provided dict and apply _val to stored .variable respectively 67 | 68 | Return YiAPICommand. 69 | ''' 70 | def makeCmd(self, _cmdPrep, _val=None): 71 | _cmdPrep.update(self.params) 72 | 73 | 74 | #assign provided _val[] values to stored .variable[] parameters 75 | if not isinstance(_val, list) and not isinstance(_val, tuple): 76 | _val= [_val] 77 | 78 | for pair in zip(self.variable,_val): 79 | if pair[0] in _cmdPrep and isinstance(_cmdPrep[pair[0]], str): 80 | if isinstance(pair[1], str): 81 | _cmdPrep[pair[0]]+= pair[1] 82 | else: 83 | _cmdPrep[pair[0]]= pair[1] 84 | 85 | 86 | return YiAPICommand(_cmdPrep, self.resultReq, self.resultCB) 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | import threading 95 | 96 | ''' 97 | Runtime command class. Lives from command send to command response. 98 | ''' 99 | class YiAPICommand(): 100 | cmdSend= None 101 | resultReq= None 102 | resultCB= None 103 | blockingCnt= 1 104 | blockingCB= None 105 | blockingEvent= None 106 | 107 | resultDict= None 108 | 109 | def __init__(self, _cmdSend, _resultReq, _resultCB): 110 | self.cmdSend= _cmdSend 111 | self.resultDict= {} 112 | self.resultReq= _resultReq 113 | self.resultCB= _resultCB 114 | 115 | if _resultReq: 116 | self.blockingCnt= 2 117 | 118 | self.blockingCB, self.blockingEvent = self.blockingCBGen() 119 | 120 | 121 | 122 | def result(self): 123 | if not 'rval' in self.resultDict: 124 | logging.error('Camera timeout') 125 | return -99998 126 | 127 | if self.resultDict['rval']: 128 | logging.warning('Camera error %d' % self.resultDict['rval']) 129 | return self.resultDict['rval'] 130 | 131 | if callable(self.resultCB): 132 | return self.resultCB(self.resultDict) 133 | 134 | if 'param' in self.resultDict: 135 | return self.resultDict['param'] 136 | 137 | 138 | ''' 139 | Generate callback suitable for supplying to YiAPIListener.assing() 140 | and Event fired at callback call. 141 | ''' 142 | def blockingCBGen(self): 143 | cbEvent= threading.Event() 144 | 145 | def func(_res): 146 | self.resultDict.update(_res) 147 | 148 | 149 | self.blockingCnt-= 1 150 | if ('rval' in _res) and (_res['rval']): 151 | self.blockingCnt= 0 152 | 153 | if not self.blockingCnt: 154 | cbEvent.set() 155 | return True 156 | 157 | return (func, cbEvent) 158 | 159 | 160 | ''' 161 | Check if Yi response matches command callback conditions 162 | ''' 163 | def cbMatch(self, _res): 164 | matchTmpl= [{'msg_id': self.cmdSend['msg_id']}] 165 | if self.resultReq: 166 | matchTmpl.append(self.resultReq) 167 | 168 | 169 | for cMatch in matchTmpl: 170 | isMatch= True 171 | for cField in cMatch: 172 | if (cField not in _res) or (cMatch[cField]!=_res[cField]): 173 | isMatch= False 174 | 175 | if isMatch: 176 | return True 177 | -------------------------------------------------------------------------------- /yiAPIListener.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re, json, threading 3 | 4 | 5 | ''' 6 | Constantly listen to opened Yi4k camera for suitable response. 7 | Most of response will be the result of commands sent to camera, while some of the responce 8 | is camera state, pushed periodically. 9 | ''' 10 | 11 | class YiAPIListener(threading.Thread): 12 | commandsCB= [] #runtime commands listening 13 | constantCB= {} 14 | 15 | jsonStream= None 16 | 17 | def __init__(self, _sock): 18 | threading.Thread.__init__(self) 19 | 20 | self.sock= _sock 21 | self.commandsCB= [] 22 | self.constantCB= { 23 | "start_video_record": None, 24 | "video_record_complete": None, 25 | "start_photo_capture": None, 26 | "photo_taken": None, 27 | "vf_start": None, 28 | "vf_stop": None, 29 | "enter_album": None, 30 | "exit_album": None, 31 | "battery": None, 32 | "battery_status": None, 33 | "adapter": None, 34 | "adapter_status": None, 35 | "sdcard_format_done": None, 36 | "setting_changed": None 37 | } 38 | 39 | self.jsonStream= JSONStream() 40 | 41 | self.start() 42 | 43 | 44 | 45 | 46 | 47 | def run(self): 48 | while True: 49 | logging.info('Wait...') 50 | try: 51 | recv= self.sock.recv(1024) 52 | except: 53 | logging.info("...stopped") 54 | return 55 | 56 | logging.debug("Part %db" % len(recv)) 57 | jsonA= self.jsonStream.find(recv.decode()) 58 | 59 | for resJSON in jsonA: 60 | logging.info('Res %s' % str(resJSON)) 61 | if not 'msg_id' in resJSON: 62 | logging.warning('Insufficient response, no msg_id') 63 | 64 | continue 65 | 66 | 67 | self.commandsCB= self.applyCB(self.commandsCB, resJSON) 68 | 69 | if resJSON['msg_id']==7: 70 | for cCb in self.constantCB: 71 | if self.constantCB[cCb] and resJSON['type']==cCb: 72 | logging.info('Callback static') 73 | self.constantCB[cCb](resJSON) 74 | 75 | 76 | 77 | ''' 78 | Assign Yi response callback. 79 | ''' 80 | def setCB(self, _type=None, _cb=None): 81 | if not _type: 82 | return self.constantCB.copy() 83 | 84 | if _type not in self.constantCB: 85 | return 86 | 87 | if callable(_cb): 88 | self.constantCB[_type]= _cb 89 | else: 90 | self.constantCB[_type]= None 91 | 92 | return True 93 | 94 | 95 | 96 | ''' 97 | Assign one-time callback for awaited command responce. 98 | ''' 99 | def instantCB(self, _command): 100 | self.commandsCB.append(_command) 101 | 102 | 103 | 104 | 105 | ''' 106 | Rolling over assigned callbacks, call them if all of template values exists in response. 107 | Then wipe callback out. 108 | ''' 109 | def applyCB(self, _commandsA, _res): 110 | unusedCBA= [] 111 | 112 | for cCommand in _commandsA: 113 | wipeCB= False 114 | if cCommand.cbMatch(_res): 115 | if callable(cCommand.blockingCB): 116 | logging.info('Callback') 117 | wipeCB= cCommand.blockingCB(_res) 118 | 119 | if not wipeCB: 120 | unusedCBA.append(cCommand) 121 | 122 | return unusedCBA 123 | 124 | 125 | 126 | 127 | 128 | class JSONStream(): 129 | jsonTest= re.compile('Extra data: line \d+ column \d+ - line \d+ column \d+ \(char (?P\d+) - \d+\)') 130 | 131 | jsonStr= '' 132 | 133 | 134 | def __init__(self): 135 | self.jsonStr= '' 136 | 137 | 138 | 139 | def find(self, _in=''): 140 | self.jsonStr+= _in 141 | 142 | jsonA, jsonDone= self.jsonRestore() 143 | 144 | self.jsonStr= self.jsonStr[jsonDone:] 145 | 146 | return jsonA 147 | 148 | 149 | 150 | ''' 151 | Detect json-restored values from string containing several json-encoded blocks. 152 | 153 | Return array of detected json found. 154 | ''' 155 | def jsonRestore(self): 156 | jsonDone= 0 157 | jsonFound= [] 158 | 159 | while True: 160 | #try full, then try part 161 | try: 162 | jsonTry= json.loads(self.jsonStr[jsonDone:]) 163 | jsonFound.append(jsonTry) #rest 164 | 165 | return jsonFound, len(self.jsonStr) #json ended up 166 | 167 | except Exception as exc: 168 | jsonErr= self.jsonTest.match(str(exc)) 169 | if not jsonErr: #assumed unfinished json 170 | return jsonFound, jsonDone 171 | 172 | jsonLen= int(jsonErr.group('char')) 173 | jsonFound.append( json.loads(self.jsonStr[jsonDone:jsonDone+jsonLen]) ) 174 | 175 | jsonDone+= jsonLen 176 | 177 | --------------------------------------------------------------------------------