Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Transcend WiFi. Пишем заказчик Shoot&View для Windows, Mac и Linux

Anna | 15.06.2014 | нет комментариев
На прогре многократно упоминали о карте памяти формата SDHC со встроенным WiFi передатчиком. Купив эту карту, я был разочарован жутким программным обеспечением, которое идет «в комплекте» с картой. Если приложением для iOS и Android хоть как то дозволено пользоваться, то неимение заказчика под windows и macos, лишает карту вероятности применения ее специалистами. Вернее сказать, на PC есть веб интерфейс, но помимо страшного внешнего вида, меня разочаровало неимение актуальной у фотографов функции Shoot&View, которая разрешает фактически мигом видеть на большом экране компьютера итог съемки.

Любители geek-porno скорее каждого разочаруются — мы не будет модифицировать прошивку, хакать ее, вскрывать саму карту памяти. Мы будет трудиться со «стоковой» картой памяти, без каких либо модификаций.

Выходит, в этой статье, мы разберем с вами протокол Shoot&View карт памяти Transcend WiFi и напишем на python кроссплатформенный заказчик, тот, что запустится на windows, linux и MacOS. А для самых нетерпеливых, в конце статьи вас ждет готовый python модуль для своих планов, консольный заказчик, а так же GUI утилита, которая работает на windows, linux и macos.

Поиск карты памяти в сети.

Карта памяти может трудиться в 2-х режимах — режим точки доступа, когда карта создает свою точку достапа, и режим подключения к точке доступа, когда карта «цепляется» к предварительно прописанным в ее настройках точкам доступа. Для наших экспериментов, отменнее включить режим подключения к точке доступа, заранее настроив подключение из приложения на android либо ios. Так же не позабудьте настроить «Turn Off WiFi», установив Never. Эта опция отвечает за отключение WiFi, если никто не подключился к карте. На первом этапе, советую подключить карту к кард-ридеру, либо настроить фотоаппарат так, чтоб он не отключался при бездействии.

Вероятно начнем программировать. Для консольного заказчика нам не понадобятся какие либо добавочные модули, только «батарейки в комплекте». А начнем мы с:

import socket

class SDCard:
	def __init__(self,home_dir=''):
		self.home_dir=home_dir
		# узнаем ip адрес интерфейса, к которому в данный момент подключены
		self.ip=socket.gethostbyname(socket.gethostname())
                # переменная для ip карты
		self.card_ip=None

if __name__=='__main__':

	# подготовим папку для принимаемых фотографий
	HOME_DIR=os.path.expanduser('~')
	if not os.path.exists(HOME_DIR '/' 'ShootAndView'):
		os.mkdir(HOME_DIR '/' 'ShootAndView')
	HOME_DIR=HOME_DIR '/ShootAndView/'

	sd=SDCard(home_dir=HOME_DIR)

Если карта подключена к точке доступа, ее ip-адрес дозволено посмотреть, скажем, в web интерфейсе роутера, а если же у нас прямое подключение к карте, то ее ip-адрес равен 192.168.11.254 (в соответствии с дефолтными настройками).
Но не хотелось бы искать ее вручную, тем больше создатели карты предусмотрели поиск ее в сети, как это сделано в мобильном приложении. Для этого нам необходимо:

  1. Сделать сокет на порту 58255
  2. Отправить с него пустой широковещательный запрос на порт 55777
  3. Ждать Дива результата карты

Если нам повезет, то в результат мы получим вот такой текст:

Transcend WiFiSD - interface=mlan0
ip=192.168.0.16
netmask=255.255.255.0
router=192.168.0.1
mode=client
essid=WiFiSDCard
apmac=CE:5D:4E:5B:70:48

Из этого каждого, нам потребуется только ip адрес. Сейчас осталось запрограммировать все это дело:

import os
import socket
import thread
import time

class SDCard:
	def __init__(self,home_dir=''):
		self.home_dir=home_dir
		self.ip=socket.gethostbyname(socket.gethostname())
		self.card_ip=None

	def find_card(self,callback=None):
		"""запускаем поиск в отдельном потоку"""
		thread.start_new_thread(self.find_card_thread,(callback,))

	def find_card_thread(self,callback=None):

		while not self.card_ip:
			"""создаем UDP сокет """
			s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
			s.settimeout(5)
			s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
			s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)	
			""" и биндимся на необходимый порт """
			try:s.bind((self.ip, 58255))
			except socket.error:
				s.close()
				time.sleep(1)
				continue

			"""отправляем пустой широковещательный запрос на порт 55777"""
			s.sendto('', ('<broadcast>', 55777))
			try:				
				resp=s.recv(400)
				s.close()
				try:
					"""пробуем в лоб распарсить итог"""
					self.card_ip=resp.split('ip=')[1].split('n')[0]
				except IndexError:
					"""если не получилось информируем об этом"""
					if callback:callback(None)					

				"""если получилось информируем ip"""
				if callback:callback(self.card_ip)			
			except socket.timeout:
				callback(self.card_ip)
			finally:
				time.sleep(2)

def monitor(ip):
	if not ip:return
	print 'Find card on ip:',ip

if __name__=='__main__':
	HOME_DIR=os.path.expanduser('~')
	if not os.path.exists(HOME_DIR '/' 'ShootAndView'):
		os.mkdir(HOME_DIR '/' 'ShootAndView')
	HOME_DIR=HOME_DIR '/ShootAndView/'
	if options.dir:HOME_DIR=options.dir	

	sd=SDCard(home_dir=HOME_DIR)
	# мне комфортнее сделать все на "коллбэках",
	# так как с GUI потом будет проще
	sd.find_card(callback=monitor)

	# так как поиск запускаем в отдельном потоке, 
	# приложение не должно завершаться
	while 1:
		time.sleep(1)

На самом деле, самое трудное теснее позади. Осталось только узнать, как нам получать информацию о «поступлении» новых фотографий и скачивать их.

Приобретение новых фотографий.

С приобретением фотографий все дюже легко. Позже того, как мы обнаружили карту, довольно присоединиться к карте на порт 5566.
Сейчас, как только фотоаппарат сделает новейший кадр, через 7-8 секунд к нам через открытый сокет придет информация о новых файлах, которые возникли на карте, выглядит это так:
>/mnt/DCIM/101CANON/IMG_1754.JPG 
Если сделали несколько фотографий, то в одном сообщении эти строки поделены нулевым байтом (0×00)

Хочу подчеркнуть — именно через 7-8 секунд. Отчего так сделано, не вовсе ясно, но повлиять на это мы не можем. Так же, приходит только информация о новых снимках в формате jpeg, причем ПО карты имеет вероятность вытягивать вшитую jpg превьюшку из RAW файла (об этом чуть ниже), но программисты выбрали лишить нас вероятности снимать в jpg, принуждая снимать в RAW jpg, либо писать RAW на одну карту, а jpg на иную. Так же, у меня не получалось копировать фотографии с кард-ридера, Shoot&View реагирует только на новые снимки, сделанные камерой.

Запрограммировать все это дело проще простого. Я вероятно начну показывать отрывки кода, а полный код вы сумеете обнаружить в конце статьи:

	def listener_thread(self,callback):

		sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		# коннектимся к карте памяти
		sock.connect((self.card_ip, 5566))
		while self.listen_flag:
			message=sock.recv(1024)
			# разделяем сообщение по нулевому байту (если пришло несколько фотографий)
			new_files=message.split('0')
			for x in new_files:
				if x:
					# добавляем все файлы в список загрузок
					self.all_files.append(x[1:]) # x[1:] - спускаем символ ">", он нам не необходим
			# добавляем конечный файл в очередь загрузок
			self.download_list.put(self.all_files[-1])

			if callback:callback(self.all_files[-1])

Загрузка фотографий с карты памяти

Сейчас у нас есть список новых файлов, остался самый конечный шаг — загрузка фотографий на компьютер. Сама по себе загрузка реализуется через встроенный веб сервер карты. Ошеломительно, но факт — все что мы делали прежде, а так же загрузка фотографий и некоторые действия, такие как приобретение списка файлов, приобретение превью и пр., абсолютно НЕ ТРЕБУЮТ АВТОРИЗАЦИИ. То есть если карта настроена как точка доступа, и пользователь не сменил пароль WiFi, вы можете спокойно подключиться к ней, и скачать все что там есть. Нужно будет как нибудь пройтись летом по туристическим местам и поискать WiFi сети среди путешественников с фотоаппаратами.

Если заглянуть в папку cgi-bin, то мы обнаружим много чего увлекательного, что может потребоваться в других планах. Заглянуть в нее легко, довольно поднять на карте telnet, согласно простым инструкциям. А внутри у нас:

Скажем, бинарник wifi_filelist отдаст нас список файлов в директории (в формате XML), довольно обратиться к нему так: CARD_IP/cgi-bin/wifi_filelist?fn=DIR, где CARD_IP — ip адрес карты памяти, тот, что мы теснее обнаружили, а DIR — директория (скажем, /mnt/DCIM). Бинарник thumbNail отдаст нам превьюшку фотографии, довольно скормить ему таким же образом путь к файлу. Причем на стороне сервера не делается ресурсоемкий резайс фотографии, а выдергивается вшитая в jpg либо в raw превьюшка.

Но нам увлекательна загрузка фотографии. Приобретение требуемой фотографии реализуется простым GET запросом на адрес CARD_IP/cgi-bin/wifi_download?fn=IMAGE_PATH, где IMAGE_PATH путь к фотографий, тот, что нам приходит по сокету, тот, что мы сотворили выше. Для загрузки в python’e в данном случае подходит функция urlretrieve библиотеки urllib. Она разрешает сразу же сберегать итог запроса в файл, и самое основное — получать прогресс загрузки, что потом сгодится в GUI.
Функция загрузки выглядит так:

	def download_thread(self,download_callback,download_complete):
		while self.listen_flag:
			# если очередь на загрузку не пуста
			if not self.download_list.empty():
				# берем путь из очереди
				fl=self.download_list.get(block=0)
				# и загружаем его в папку с фотографиями
				urllib.urlretrieve('http://%s/cgi-bin/wifi_download?fn=%s'%(self.card_ip,fl),self.home_dir fl.split('/')[-1],download_callback if download_callback else None)
				if download_complete:download_complete(self.download_now)
			time.sleep(0.1)

Сейчас объединим все вместе, сделав готовый модуль, заодно получив консольный заказчик, тот, что будет трудиться на windows, linux и macos.

sdwificard.py

#coding:utf-8
"""
    Copyright (C) 2010 Igor zalomskij <igor.kaist@gmail.com>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""

import os
import socket
import thread
import time
import ping
import Queue
import urllib
import sys

class SDCard:
	def __init__(self,home_dir=''):
		self.home_dir=home_dir
		# выясняем ip адрес сетевого интерфейса компьютера
		self.ip=socket.gethostbyname(socket.gethostname())
		self.card_ip=None #переменная с ip адресом карты памяти
		self.all_files=[] # список всех файлов

		self.download_list=Queue.Queue() # очередь для загрузки фотографий
		self.in_queue=[] # что в очереди на загрузку, потребуется в GUI

	def find_card(self,callback=None):
		# стартуем новейший поток с поиском карты
		thread.start_new_thread(self.find_card_thread,(callback,))

	def find_card_thread(self,callback=None):
		""" поток поиска карты памяти """
		while not self.card_ip:
			# создаем UDP сокет
			s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
			s.settimeout(5)
			s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
			s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)			
			try:s.bind((self.ip, 58255)) #биндим его на необходимый порт
			except socket.error:
				s.close()
				time.sleep(1)
				continue

			# посылаем широковещательный запрос на порт 55777
			s.sendto('', ('<broadcast>', 55777))
			try:
				resp=s.recv(400)
				s.close()
				try:
					# пробуем распарсить результат 
					self.card_ip=resp.split('ip=')[1].split('n')[0]
				except IndexError:
					# напротив информируем о неудаче
					if callback:callback(None)

				if callback:callback(self.card_ip)

			except socket.timeout:
				callback(None)
			finally:
				time.sleep(2)

	def start_listen(self,callback=None,download_callback=None,download_complete=None):
		""" Запуск мониторинга новых фотографий. запускаем три потока """
		self.listen_flag=True
		# поток сокета, тот, что будет слушать сокет с новыми фотографиями
		thread.start_new_thread(self.listener_thread,(callback,))

		# время от времени советую пинговать карту, чтоб она не отвелилась.
		thread.start_new_thread(self.ping_card,())

		# поток фоновой загрузки фотографий
		thread.start_new_thread(self.download_thread,(download_callback,download_complete))

	def ping_card(self):
		# пингуем карту с переодичность 20 секунд.
		while self.listen_flag:
			try:
				resp=ping.do_one(self.card_ip)
			except socket.error: # во время загрузки фотографий карта может не отвечать на пинги, это типично

				pass
			time.sleep(20)

	def listener_thread(self,callback):
		# поток приобретения информации о новых фотографиях

		sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		# коннектимся к карте на порт 5566
		sock.connect((self.card_ip, 5566))
		while self.listen_flag:
			message=sock.recv(1024)
			new_files=message.split('0') # разделяем сообщение по нулевому байту (если пришло несколько фотографий)
			for x in new_files:
				if x:
					# добавляем все файлы в список всех файлов ;) 
					self.all_files.append(x[1:]) # x[1:] отсекаем 1-й символ ">", он нам не необходим

			self.download_list.put(self.all_files[-1]) # добавляем конечный файл в очередь на загрузку
			self.in_queue.append(self.all_files[-1]) # добавляем так же в иной список, он необходим для GUI
			if callback:callback(self.all_files[-1]) 

	def download_thread(self,download_callback,download_complete):
		# поток загрузки фотографий
		while self.listen_flag:
			if not self.download_list.empty(): # если очередь не пуста
				fl=self.download_list.get(block=0)
				self.download_now=fl # что в данный момент загружается, необходимо для GUI

				# загружаем 
				urllib.urlretrieve('http://%s/cgi-bin/wifi_download?fn=%s'%(self.card_ip,fl),self.home_dir fl.split('/')[-1],download_callback if download_callback else None)
				if download_complete:download_complete(self.download_now)
			time.sleep(0.1)

def find_callback(ip):
	if not ip:return
	print 'Find card on ip:',ip
	# если определен IP адрес карты, стартуем мониторинг новых файлов
	sd.start_listen(download_complete=download_complete)

def download_complete(fname):
	print 'New image: %s'%(HOME_DIR fname.split('/')[-1])

if __name__=='__main__':
	""" Для консольного заказчика, парсим опции. Допустимо пользователь
	захочет переопределить папку для загрузки изображений, либо ip адрес интерфейса компьютера
	"""
	from optparse import OptionParser
	parser = OptionParser()
	parser.add_option("-d", "--dir", dest="dir",default=None,help="directory for storing images")
	parser.add_option("-i", "--ip", dest="ip",default=None,help="ip address of the computer (default %s)"%(socket.gethostbyname(socket.gethostname())))
	(options, args) = parser.parse_args()
	# готовим папку для загрузки изображений по умолчанию.
	HOME_DIR=os.path.expanduser('~')
	if not os.path.exists(HOME_DIR '/' 'ShootAndView'):
		os.mkdir(HOME_DIR '/' 'ShootAndView')
	HOME_DIR=HOME_DIR '/ShootAndView/'
	if options.dir:HOME_DIR=options.dir

	sd=SDCard(home_dir=HOME_DIR)	

	if options.ip:sd.ip=options.ip
	print 'Finding sd card...'
	# запускаем поиск карты памяти
	sd.find_card(callback=find_callback)

	while 1:
		time.sleep(1)

Я умоляю не бранить меня за допустимые отступления от pep-8, теперь я практикую программирование довольно редко, да и люблю про себя повторять: «В голове моей опилки не-бе-да, pep-8 не читал я, да-да-да».
Все начальные коды вы можете взять на github.com/kaist/shoot-and-view

Позабыл упомянуть, что во время работы с картой памяти, ее желанно время от времени пинговать. В скрипте я не стал искать методов делать ping на различных платформах, тем больше, консольная утилита ping на некоторых платформах требует привилегий менеджера. Я легко применял реализацию ping на чистом питоне. Данный модуль необходимо разместить рядом со скриптом.

GUI

Для GUI я применял самое примитивное средство в питоне, это Tkinter. Он доступен «из коробки» в windows и MacOS, и к тому же занимает немного места, если собирать standalone приложение. Процесс написания GUI, вероятно описывать не буду, ограничусь только маленький инструкцией:

  1. Импортируйте Tkinter
    from Tkinter import * 
  2. Напишите GUI

Консольное приложение не требует дополнительных библиотек, а вот GUI версия хочет различных плюшек, таких как чтение exif, работа с изображениями и пр. Если вы хотите запустить ее из исходников (простите, на Linux я подготовил только такой вариант), то вам понадобится:
sudo apt-get install python-tk python-imagetk python-imaging libimage-exiftool-perl
А так же, установить вручную биндинг к exiftool (sudo python setup.py install)
В windows, помимо python 2.7 и биндинга к exiftool, требуется PIL и exiftool.
Так же установка exiftool и биндинга к нему требуется на MacOS, см. ссылки выше.

Приложение собирается при помощи py2exe на windows и py2app на MacOS, скрипты вы сумеете так же обнаружить среди исходников.

Вывод

Как и обещал, для самых ленивых доступны готовые сборки для Windows и MacOS. Взять их дозволено на этойстранице.
Кое что из вероятностей:

  • История съемки — вероятность клавишами налево-право просмотреть предыдущую либо следующую отснятую фотографию.
  • Зум — при нажатии клавиши «задач», фотография зуммируется до 100%
  • Механический поиск карты памяти
  • Ну и безусловно же, все даром для торгового и некоммерческого применения. Исходники доступны по лицензии GPL v2

Ну и напоследок пару скриншотов:

P.S. Я собирал приложение для MacOS впервой, умоляю протестировать, работает ли, исключительно не на машине python разработчиков )

Эта статья распространяется на условиях лицензии Creative Commons Attribution 3.0 Unported (CC BY 3.0)

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB