В данной статье речь пойдет о Wi-Fi мини видеокамере из семейства А9 от китайских производителей. Цель исследования этих камер – расширить возможность их применения, которая ограничена использованием только стандартных приложений для мобильных устройств на базе Android или iOS.
О камерах семейства А9
Стоимость данных видеокамер варьируется от 2$ до 4$ (в зависимости от производителя, типа процессора, наличия ИК подсветки и др.). На известной экспресс торговой площадке её можно найти по запросу «мини-камера A9». Ссылка на англоязычный форум по теме разбора камер А9.
Внешний вид моих подопытных камер представлен на фотографии ниже (и, да, внутри они немного разные).
Типовая видеокамера А9 имеет встроенный аккумулятор, а внешнее питание подается через разъем microUSB. Она может обеспечивать два режима работы с сетью: как точка доступа (AP), и как клиент сети (STA). Режим работы настраивается через стандартное приложение на смартфоне. По умолчанию всегда включен режим АР. IP адрес для камеры в режиме АР может отличаться в зависимости от производителя, у моих подопытных – 192.168.1.1. При проведении исследований этих камер использовалось приложение FtyCamPro рекомендованное производителем, хотя в «интернетах» пишут, что работают эти камеры и с более известным приложением Little Stars. Пользовательский обзор в достаточном объеме представлен на YouTube по запросу «Подключение китайской камеры А9».
Рассматриваемые здесь дешевые видеокамеры построены на основе процессора TXW806-840. Ссылка на сайт производителя чипа TWX806 (только китайский язык) https://www.taixin-semi.com/. В продаже имеются также и аналоги с процессором BK7231, которые несколько дороже. Электронная часть камеры выглядит вот так:
Структурная схема процессора, взятая из официального даташита показана ниже.
Согласно данным от производителя TXW806 - это высокоинтегрированный небольшой многорежимный чип для IoT с частотой 2,4 ГГц. Чип включает в себя 32-битный микроконтроллер, встроенный MJPEG (поддерживает VGA/720P), имеет DVP интерфейс, высокоскоростной хост USB2.0, хост SDMMC, ведомое устройство SDIO2.0, интерфейс RMII MAC, ведущее устройство SPI, UART, IIC, IIS, IR Send/Receive, PWM, GPIO и ADC/DAC, поддерживают запуск программ на SPI Flash. Базовый модуль Wi-Fi TXW806 реализует технологию мультиплексирования с ортогональным частотным разделением каналов (OFDM), обратно совместима с технологией расширения спектра прямой последовательностью (DSSS), дополнительной кодовой манипуляцией (CCK) и поддерживает протокол IEEE 802.11 b/g/n. Wi-Fi поддерживает стандартную полосу пропускания 20 МГц и узкую полосу пропускания 5 МГц/10 МГц, обеспечивая скорость физического уровня 72,2 Мбит/с. Имеет усилитель мощности, малошумящий усилитель LNA, радиочастотный балун, антенный переключатель, модуль управления питанием и т. д. Поддерживает RTOS и сторонние операционные системы, а также предоставляет открытую и простую в использовании среду разработки и отладки.
Используется этот чип в основном для беспроводных аудио и видео устройств, аэрофотосъемки, видео глазков для домофонов и т.п. Богатый функционал, за такую стоимость, не правда ли? От себя могу похвалить этот чип за его стрессоустойчивость, которую он проявил во время пыток его паяльником и тестером.
Безоперационное определение сознания камеры
До препарирования камеры было проведено сканирование доступных портов утилитой nmap в Ubuntu, при этом камера была подключена к роутеру в режиме STA. По результатам сканирования, были определены открытые порты камеры:
PORT | STATE | SERVICE |
68/udp | open|filtered | dhcpc |
7070/tcp | open | rtsp/realserver |
32108/udp | open|filtered | unknown |
7070/tcp open rtsp
| fingerprint-strings:
| RTSPRequest:
| RTSP/1.0 200 OK
|_ Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE
Из доступных для чтения видеопотока оказался порт 7070 по которому предоставляется потоковое видео и аудио по RTSP. Первые попытки получить видео посредством ПО работающего с протоколом RTSP не принесли успеха. Для этого использовались VLC player и openRTSP под Ubuntu (http://www.live555.com/openRTSP/).
На запрос: /home/live/testProgs# ./openRTSP ‑T 7070 rtsp://192.168.1.1:7070
Камера отвечала:
Hidden text
Created new TCP socket 3 for connection
Connecting to 192.168.1.1, port 7070 on socket 3...
...remote connection opened
Sending request: OPTIONS rtsp://192.168.1.1:7070 RTSP/1.0
CSeq: 2
User-Agent: ./openRTSP (LIVE555 Streaming Media v2023.11.30)
Received 76 new bytes of response data.
Received a complete OPTIONS response:
RTSP/1.0 200 OK
CSeq: 2
Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE
Sending request: DESCRIBE rtsp://192.168.1.1:7070 RTSP/1.0
CSeq: 3
User-Agent: ./openRTSP (LIVE555 Streaming Media v2023.11.30)
Accept: application/sdp
Received 35 new bytes of response data.
Received a complete DESCRIBE response:
RTSP/1.0 404 Not Found
CSeq: 3
Итогом такого общения был ответ от камеры:
Failed to get a SDP description for the URL "rtsp://192.168.1.1:7070": 404 Not Found
При использовании VLC результат был примерно такой же.
Еще отмечу, что камера в режиме STA интенсивно соединялась со сторонними серверами по UDP, IP которых: 170.106.50.82, 146.56.226.66 и 35.156.204.247. Это общение пришлось закрыть фаерволом, на всякий случай. Кстати, фаервол был включен на эти IP уже после тестирования с помощью утилиты openRTSP, на случай если при отсутствии соединения с этими серверами камера будет уходить в глухую оборону и в полное молчание.
P.S. Печально конечно, но IP адреса сторонних серверов потом меняются.
Вскрытие
Следующим шагом исследования было вскрытие корпуса и привлечение паяльника. На фотографиях ниже представлены PCB двух моих камер. Слева уже распаянная для работы, справа – новая, не тронутая. Обратите внимание на контактные площадки.
Да, и давайте камеру слева назовем FTYB, а камеру справа – BATC. Не спрашивайте почему. Дальше, надеюсь, будет понятно.
Контакты на плате слева имеют следующие обозначения: RX_PA9, TX_PA10, PA8, GND и CEN (chip enable). На плате справа аналогичные контакты обозначены несколько иначе: HCK, без обозначения, HDA, GND и nRST. В даташите на процессор TXW806 есть следующая информация:
Обозначение вывода | Тип | Назначение |
PA8 | I/O | LEDTMR2_PWM_OUT, ADKEY1_N0, ADKEY0_P1, ADKEY1_P0, TK8, LED_SEG4, QSPI_CLK |
PA9 | I/O | ADKEY0_P1, ADKEY1_P0, TK9, LED_SEG9, QSPI_IO2 |
PA10 | I/O | ADKEY0_P1, ADKEY1_P0, TK10, LED_SEG8, QSPI_IO1 |
CHIP_EN | I | Chip enable:0:Chip off, 1:Chip enable |
И таблица о порядке подключения чипа к программатору:
Кроме того, производитель сообщает, что имеется два интерфейса отладки: PA9 (DebugIO) и PA10 (DebugCLK) с внутренним сопротивлением подтягивающего резистора 10 кОм. Когда функция отладки не используется, интерфейс отладки можно использовать как обычный GPIO, и его необходимо настроить с помощью программного обеспечения. Во время компоновки печатной платы должны быть зарезервированы контрольные точки на плате, чтобы облегчить отладку. В то же время PA8, CHIP_EN также необходимо зарезервировать для вспомогательного тестирования.
С помощью переходника USB-TTL (в моем случае на микросхеме CP2102) камера была подключена к последовательному порту компьютера. Кстати! Контакт TX на плате это PA8, а на другой камере TX это HDA! Общение с платой осуществлялось посредством Putty с настройкой COM порта на скорость 1 000 000 (106) бод, все остальные параметры по умолчанию. Было задействовано три точки: GND, PA8 и PA9. Скорость взаимодействия определена опытным путем. На стандартной максимальной скорости 512 000 бод и менее в терминал сыпались нечитаемые знаки. Думаю, что весь лог загрузки приводить не целесообразно, здесь акцентирую внимание лишь на некоторых моментах. В логах загрузки и работы камеры содержатся наименование сборки и дата сборки ПО, наименование клиента (станции), пароль, наименование прошивки, калибровочные параметры для видео сенсора, настройки для АЦП, для TF (SD) карты памяти (пытается ее найти, создать там рабочие папки для записи видео и фото), настройки сети Wi-Fi (MAC, FLAGS, UP LINK_UP ETHARP IGMP, ip address 192.168.1.1, gateway, net mask), информация о состоянии системы (температура чипа, рабочая частота, свободная память, работающие процессы и уровни загрузки ими ЦП), например:
• начало загрузки, с наименованием сборки ПО выглядит так:
Hidden text
** hgSDK-v2.2.0.7-22619, app-0, build time: Apr 13 2023 10:15:54 **
------- system restart fault -----------
------- lvd fault -----------
---------------------------------------
[1]time=0, tick=1
…
get flash addr:fd000 size:4096,
[9]col_flag:1
[9]get_parse_cloud_id:126
[10]selfId:FTYB931188RLTOV:15
[10]col_flag:1
[10]col_flag:2
[10]get_parse_cloud_id: 126
[11]parId: WRAWEJ: 6
[IpcCfgInit][ 448] 1:616 --> get from ezConfig FTYB931188RLTOV:ZZZZZ:
[IpcCfgInit][ 462] 0 --> get from ezConfig FTYB931188RLTOV:ZZZZZ: <-- FTYB931188RLTOV – имя камеры и имя точки доступа
• сообщение о разрешении матрицы сенсора:
Hidden text
[148] SENSR ident ok: 640*480
[148] csi set size ====>640*480
• информация о том, что открытым является порт 7070:
Hidden text
t_s> spook init
listening on tcp port 7070
port:7070 fd:3
jpeg set_output
live start_block
live set_path /webcam <‑- /webcam – имя папки для работы по протоколу rtsp!
live set_track jpeg_dvp
jpeg get_framerate
jpeg set_running: 1
live end_block
len:1200
Соответственно, полный путь к устройству для приема потокового видео будет rtsp://192.168.1.1:7070/webcam
• данные о соединении с сетью:
Hidden text
[sys_wifi_init][ 419] CONFIG_UMAC4 ---- yes
-------ssid: FTYB931188RLTOV, key: 12345678
[52] lmac_bgn_lo_freq_set: 2457
[54] vif2 state WPA_DISCONNECTED -> WPA_COMPLETED
…
network interface: w0 (Default)
MTU: 1600
MAC: d8 83 32 8e a8 24
FLAGS: UP LINK_UP ETHARP IGMP
ip address: 192.168.1.1
gw address: 192.168.1.1
net mask: 255.255.255.0
network interface: lo
MTU: 0
MAC:
FLAGS: UP LINK_UP
ip address: 127.0.0.1
gw address: 127.0.0.1
net mask: 255.0.0.0
• данные о состоянии системы:
Hidden text
local:d8:83:32:8e:a8:24
chip-temperature: 28
freq:2457, bg_rssi:-73
gain_table:0
cca: -63, -53, -55
tx: txq:0, ps:0, tx_stat_q:0,
tx dma:12, total tx:12, retry:32, tx lost:3, tx err:0
rx: rx irq:148
rx dma free: 17232
rx err: dma err:7, phy err:6714, rx frm err:467,
• данные о запущенных процессах:
Hidden text
[6244]ip:101a8c0 freemem:40224
[6244]---------------------------------------------------
[6245]Task Runtime Statistic, interval: 6244ms
[6245]PID Name %CPU(Time) Stack Prio
[6246]--------------------------------------------------------------------------------------
[6247] 1 idle_task 94%(5930) 1024 61
[6247] 2 timer_task 0%(1) 800 5
[6248] 3 MAIN 2%(173) 2048 43
[6248] 4 lmac tx 0%(3) 640 27
[6249] 5 lmac tx status 0%(1) 512 27
[6250] 6 lmac beacon task 0%(9) 640 26
[6250] 7 lmac rx 0%(11) 1024 19
[6251] 8 lmac_bgn_test 0%(1) 512 51
[6251] 9 lmac main 0%(5) 1024 43
[6252]10 hw 0%(21) 2048 26
[6253]12 tcpip_thread 0%(2) 1024 49
[6253]13 hg_sdh_test 0%(11) 1024 43
[6254]14 hgpdm_sample_task 0%(61) 1024 43
[6255]17 ThLstn 0%(5) 1024 49
[6255]20 ThSysMon 0%(5) 3072 49
[6256]---------------------------------------------------
[6256]WARNING: work 0x20004248 (func:0x1800438c) use 12 ticks
host->flags:14
host->flags:14
При подключении смартфона и открытии приложения FtyCamPro:
• настройка соединения со смартфоном:
Hidden text
[26499]lmac_bgn_add_sta: if:2, aid1, addr:b4:хх:хх:хх:хх:58 ß хх – это для конфиденциальности ?
[26500]inteface2: sta b4: хх:хх:хх:хх:58 connected
[26501]user_sta_add: b4 хх хх хх хх 58
[26813]send DHCP_OFFER ...
[26813]Next IP: 192.168.1.101
[26814]Assign IP 192.168.1.100 for b4:хх:хх:хх:хх:58, flags=0 (next:192.168.1.101)
[26847]send DHCP_ACK ...
[26847]Assign IP 192.168.1.100 for b4: хх:хх:хх:хх:58, flags=0 (next:192.168.1.101)
[26849]EVENT 10007 IGNORED
[26849]IP Pool:
[26850] ip:192.168.1.100 - b4 хх:хх:хх:хх:58
…
• вход пользователя и начало передачи видео:
Hidden text
[cbEvenNoteFun][ 109] p2pSession [privilege=0,status=1,peerAddr=]
[_P2pSessionEvent][ 29] p2pSession.status=[1]
[_P2pSessionEvent][ 70] session[0] connected from []
[IlnkConnAdd][ 23] set a connection--->[0]->2
[IlnkConnAdd][ 40] set a connection--->[0]
[cbEvenNoteFun][ 123] exter--------
[cbSysCtrlFun][ 915] SysCtrl->ctrlType->(3)
[_SysUsrChk][ 675] usrchk s [admin, admin], d [admin, admin]
[_SysUsrChk][ 727] usrchk [name=admin, pass=admin] [len=8, privilege=255, ticket=ZtsS] ß обратите внимание на name, pass и ticket!
firstRTT=6, srtt=48
…
[_CustomCmdPrc][2531] PUSHPARAM Set---------------
[PushParamSet][ 358] failed[336]--CfgItemGet
[PushParamSet][ 364] GPushSet:
access_id=0
xsecret_key=AIzaSyB-boxOG5n6AbKMLAOwmm1PZzqPkyZjMwc <--- обратите внимание на xsecret_key!
msgType=2
environment=1
[PushParamSet][ 396] Gpush-----index=0
• запрос на синхронизацию времени:
Hidden text
[_SysTimeSet][ 820]now: 1705692126
ntpEnable: 0
ntpSvr: time.windows.com
timezone:-10800
dayLight:0
[IpcTimeSyncFromStamp][ 307]timeStamp=1705702926-->cur_time=1705702926, Fri Jan 19 22:22:06 2024
[IpcTimeSyncFromStamp][ 329] 1705702926--> datetime [2024-01-19 22:22:06]
[_SysTimeSet][ 835] now: [1705702926:1705702926]
ntpEnable:0
ntpSvr: time.windows.com
timezone:-10800
daylight: 0
[_SysTimeSet][ 844] xqTime = 1705702926
Ну, наверное, это все что мне показалось понятным и интересным. Далее камера была протестирована на предмет приема информации по линии RX камеры, и, о чудо, она откликалась на нажатие кнопок на клавиатуре ПК. Изучение даташитов сильно не помогло, разве что в описании для платы разработчика (development board) для TXW806 было вскользь отмечено об АТ командах. На простой запрос к камере «AT+» (через «ctrl+c – ctrl+v») камера выдала список из 40 возможных команд:
Hidden text
Acmd_input_user_deal: AT+, len:3
valid cmds:
0. AT+BSS_BW
1. AT+CCA
2. AT+CFG_RX_AGC
3. AT+CLR_RX_CNT
4. AT+DBG_LEVEL
5. AT+EDCA_AIFS
6. AT+EDCA_CW
7. AT+EDCA_TXOP
8. AT+FCC
9. AT+TX_FRM_TYPE
10. AT+TX_GAIN
11. AT+GET_RX_CNT
12. AT+LO_FREQ
13. AT+MAC_ADDR
14. AT+PRINT_PERIOD
15. AT+REG_RD
16. AT+REG_WT
17. AT+SET_XOSC
18. AT+SET_TX_DPD_GAIN
19. AT+TEST_START
20. AT+TEST_ADDR
21. AT+TEMP_EN=1
22. AT+TX_CONT
23. AT+TX_DELAY
24. AT+TX_MCS
25. AT+TX_PROBE
26. AT+SLEEP
27. AT+TX_DUTY_CYCLE
28. AT+BSS_SCAN
29. AT+TX_PHA_AMP
30. AT+TX_PWR_AUTO
31. AT+TX_PWR_SUPER
32. AT+TX_PWR_TEST
33. AT+TX_START
34. AT+TX_STEP
35. AT+TX_TRIG
36. AT+TX_TYPE
37. AT+WAVE_DUMP
38. AT+WRITE_MAC_ADDR
39. AT+WRITE_TX_DPD_GAIN
40. AT+WRITE_XOSC
Конечно, из любопытства пришлось немного с ними побаловаться, так как поиск смысла этих команд по просторам сети ничего путного не дал. В результате опытов было выявлено несколько интересных (но не сильно полезных) команд. Вот некоторые из них:
AT+PRINT_PERIOD=1000 – установка задержки по времени выдачи в терминал информации, единица измерения – мс. Если установить значение 0, печать отключается;
AT+REG_RD= 0x01 – чтение данных из регистра 0x01;
AT+REG_WT=0x20001000, 0x123 – запись 0x123 в регистр 0x20001000;
AT+BSS_BW=20 – изменение полосы пропускания Wi-Fi.
В общем, для выполнения задачи по трансляции видео выявить пользу АТ команд мне не удалось.
Результаты перехвата потока информации по беспроводной сети
Для перехвата сетевого трафика между камерой и смартфоном использовалось приложение PCAPdroid (установленное на смартфоне), для перехвата трафика между ПК и камерой – Wireshark. Для анализа шестнадцатеричного кода использовался HxD Hex Editor. (https://mh-nexus.de/en/hxd/ редактор hex, https://www.wireshark.org Wireshark).
По умолчанию номер порта на моих подопытных камерах для обмена по UDP был постоянный – 32108 (см. выше результаты nmap), номер порта на смартфоне назначался автоматически и отличался в зависимости от сессии. Из полученных данных выделялись только UDP пакеты, а из них бралась только часть пакета с данными.
Полный дамп трафика, наверное, приводить не имеет смыла, здесь приведу только анализ части дампа общения камеры со смартфоном.
Обозначения: # – пакет от смартфона, @ – пакет от камеры. Данные представлены в шестнадцатеричном виде.
Номера пакетов Wireshark естественно будут отличаться от ваших. В первой группе данных (выделено желтым цветом) первый байт всегда f1, это так называемый «magic number». Второй байт этой группы идентифицирует запрос, следующим образом:
42 – «приветствие», P2pRdy
e0 – «запрос проверки состояния», P2PAlive
e1 – «ответ на запрос проверки состояния», P2PAliveAck
Вторая группа данных (выделено голубым цветом) это два байта обозначающие размер основных данных:
Порядок байт в группе – big endian, то есть 0x0014 – 20 байт, 0х0123 – 291 байт
В третьем (от смартфона) и седьмом (от камеры) пакетах, обозначающих, в моем понимании, обмен «приветствиями», в основных данных содержится наименование камеры или сети FTYB931188RLTOV, где цифры указаны в hex (0x0E3574 – 931188).
Наименование второй подопытной камеры BATC-185207-YDEIB и соответствующая часть дампа пакета UDP.
Ну и наконец, проверка состояния f1e0: пакет 4 от смартфона это запрос, пакет 9 от камеры f1e1 – ответ, и наоборот пакет 6 – запрос от камеры, пакет 8 – ответ смартфона. В пакетах с запросом проверки состояния передаются только заголовки, данные отсутствуют.
Следующие после пакетов «приветствия» и опроса состояния идут пакеты авторизации.
В пакете 10 от смартфона сообщается:
f1d0 (поз. группы 1) – данные необходимо принять и на них ответить;
00b0 (поз. гр. 2) – размер основных данных 176 байт;
d100 (поз. гр. 3) – маркер;
0000 (поз. гр. 4) – номер отправляемого пакета, который необходимо будет подтвердить при ответе;
110a (поз. гр. 5) – стартовые байты;
2010 (поз. гр. 6) – байты, сообщающие что будет проводиться авторизация пользователя
Байты 6065 6c68 6f01 (выделены песочным цветом, поз. 12,13,14) это ничто иное как логин и пароль (они между собой совпадают, см. выше логи с последовательного порта). Обратите внимание, что тут применяется простой метод шифрования:
my_byte=(my_byte%2==0)?my_byte+1:my_byte-1 и наш «’elho» становится «admin»:
В пакете 11 содержится ответ камеры, что она приняла пакет с номером 0000 от смартфона, поэтому и размер основных данных 0х0006, куда входят 0000 (позиция 5 группы данных). В пакете 12 камера отправляет запрос (f1d0) смартфону с данными о том, что пользователь авторизован (2011 поз. 6) и «выдаёт» смартфону «ticket» (см. выше логи последовательного порта) – «7669 3942» (поз.11, 12, выделено розовым цветом). «Ticket» зашифрован тем же методом что и имя пользователя и выдается один раз на всю сессию, но каждый раз новый. В пакете 13 смартфон отвечает камере (f1d1), что принял пакет с номером 0000 (поз. 5) от камеры.
Пакет 14 от смартфона к камере как раз и отличает одну камеру от другой. Этот дамп для камеры FTYB. В пакете содержится xsecret_key=AIzaSyB-boxOG5n6AbKMLAOwmm1PZzqPkyZjMwc (см. выше дамп последовательного порта). Этот «секретный ключ» выделен шрифтом красного цвета. В пакете 14 он представлен в зашифрованном виде. Камера BATC не имеет xsecret_key и поэтому в дампах трафика сетевого обмена между камерой и смартфоном такой пакет отсутствует. Желтым выделено название камеры/сети – FTYB931188RLTOV, часть, содержащая цифры теперь представлена как текстовая в формате UTF-8. Также в пакете 14 указан расшифрованный ticket – «7768 3843» (выделено коричневым цветом, первая строка 9 и 10 группы), в формате UTF-8 это текст «wh8C».
В пакете 15 смартфон в запросе (f1d0) сообщает, что приложение пытается синхронизировать время для камеры на сайте time.windows.com (зашифрованные байты, выделены красным), но смартфон подключен к камере как к AP, и поэтому попасть туда никак не может! Следовательно, время на камере не синхронизируется.
В пакете 16 смартфон делает запрос на соединение уже имея при себе билет (0810, поз. 6) и его указывая (поз.9 и 10). Пакет 17 от смартфона дублирует пакет 15 только уже без ссылки на time.windows.com.
Пакетом 18 камера «отчитывается» перед смартфоном, что получила все его пакеты (номера полученных пакетов показаны красным цветом, поз. 5-8).
В общем-то дальше уже не важно что отправляет нам камера, главное перед ней отчитываться, что получены все ее пакеты и на вопрос «как дела? – f1e0», всегда отвечать «все отлично» – f1e1. Это будет понятно из кода на Python представленного далее.
В определенный момент камера перестанет присылать вопросы «как дела?». Это видно по логам с любой камеры и будет ожидать от смартфона «серьезных» предложений. Которые выглядят следующим образом:
После чего камера подтвердит их получение, пришлёт пару «ненужных» запросов, на которые также надо будет просто отчитаться о получении. А затем камера пришлет первый пакет, в котором так называемый маркер измениться. Это и будет пакет с полезными данными!
Маркер (поз. 3, показано красным) с d100 сменился на d101, что говорит о пакете с полезной для нас информацией (аудио или видео). В данном случае, в пакете 59 передан фрагмент звука 55aa 15a8 (выделено желтым, поз. 5 и 6) – AUDIO_HEADER.
И в пакете 60 смартфон отвечает, что принял пакет 0001 (показано желтым, поз. 5) от камеры. При этом маркер у него будет d201 (поз. 3). Дальше камера беспрерывно начнет присылать jpeg и звук. А смартфону важно отчитываться за полученные пакеты.
Видно, что в пакете 61 размером 1028 байт с маркером d101 есть заголовок jpeg – ffd8. Пакеты с jpeg идут с размером 1028 байт и только у пакета, завершающего полный видеокадр, размер может быть меньше (в конце пакета должно быть ffd9).
Для примера, восстановленный (путем копирования пакетов из Wireshark в редактор HxD) фрагмент видео с моей кошкой!
Подробнее о структуре jpeg файла можно почитать на https://habr.com/ru/articles/454944/ (автор @SLY_G)
Реализация на Python
Программа имитирует общение камеры BATC с приложением на смартфоне по логике обмена пакетами описанной выше. Камера включена в режиме AP. В программе не предусмотрена отправка пакета с xsecret_key. Полученные кадры потока MJPEG сохраняются в локальную папку на ПК. Звук игнорируется. Для завершения работы программы используется магическое сочетание клавиш «ctrl+Break». Вот сам текст программы:
Hidden text
import socket
from PIL import Image, ImageDraw, ImageFont
import time, io
def pack_size(mydata) :
#преобразуем размер данных в 2 байта hex для вставки в пакет
return (len(bytes.fromhex(mydata)).to_bytes(2, byteorder='big'))
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
BROADCAST_CAM_PORT = 32108
cam_name='42415443000000000002d3775944454942000000' # имя камеры/сети BATC185207YDEIB, 185207 в hex
MSG_HELLO=bytes.fromhex('f1420014')+bytes.fromhex(cam_name)
MSG_E0_Q=b'\xf1\xe0\x00\x00' # Ты как? Q-question
MSG_E1_ANSW=b'\xf1\xe1\x00\x00' # Норм! ANSW - answer
HDR_D0=b'\xf1\xd0' # принимай!
HDR_D1=b'\xf1\xd1' # принял!
data_login='d100'+'0000'+'110a2010a400ff'+'00'*5+'6f'+'01'*27+'60656c686f'+'01'*123+'60656c68'
MSG_LGN_ADMIN=HDR_D0+pack_size(data_login)+bytes.fromhex(data_login)
MSG_TICKET_TIME_START=HDR_D0+bytes.fromhex('0060'+'d100'+'0001'+'110a'+'401054000000')
MSG_TICKET_TIME_END=bytes.fromhex('d1d4fefe'+'01'*8+'75686c642f76686f656e76722f626e6c'+
'01'*48+'230e0e67')
MSG_TICKET_START=HDR_D0+bytes.fromhex('0010'+'d100'+'0002'+'110a'+'081004000000')
##########################
MSG_TICKET_START_2=HDR_D0+bytes.fromhex('0018d1000003'+'110a18300c000000')
MSG_TICKET_END_2=bytes.fromhex('0301010100010101')
MSG_TICKET_LONG_START=HDR_D0+bytes.fromhex('0114d1000004'+'110a103008010000')
MSG_TICKET_LONG_END=bytes.fromhex('01'*260)
MSG_TICKET_TIME_START_2=HDR_D0+bytes.fromhex('0060'+'d100'+'0005'+'110a'+'401054000000')
MSG_TICKET_TIME_END_2=bytes.fromhex('d1d4fefe'+'01'*8+'75686c642f76686f656e76722f626e6c'+
'01'*48+'240e0e67')
###########################
def take_ticket (indata):
ticket=indata.hex(' ').split(' ')
hex_numbers = [ticket[len(ticket)-8], ticket[len(ticket)-7], ticket[len(ticket)-6], ticket[len(ticket)-5]]
dec_numbers = [int(hex, 16) for hex in hex_numbers]
for i in range(0, len(dec_numbers)):
if dec_numbers[i]%2==0:
dec_numbers[i]=(dec_numbers[i]+1)
else:
dec_numbers[i]=(dec_numbers[i]-1)
return (bytearray(dec_numbers)) # да, можно было бы напрямую работать с hex
def my_send (mybyte):
s.sendto(mybyte, ('192.168.1.1', BROADCAST_CAM_PORT))
print ('I SEND: ', mybyte)
fnum=0 # счетчик для кадров MJPEG
def take_full_jpeg (jpeg_data):
jpeg_return=bytes.fromhex(''.join(jpeg_data[4:]))
data_send=['d201','0001']
data_send.append(jpeg_data[3])
my_send (HDR_D1+pack_size(''.join(data_send))+bytes.fromhex(''.join(data_send)))
jpeg_recvdata=s.recvfrom(4096)[0]
while jpeg_recvdata.find(b'\xff\xd9') == -1 :
jpeg_return=jpeg_return+jpeg_recvdata[8:]
data_send.clear()
data_send=['d201','0001']
data_send.append(jpeg_recvdata[6:8].hex())
my_send (HDR_D1+pack_size(''.join(data_send))+bytes.fromhex(''.join(data_send)))
if (jpeg_recvdata[4:6]!= b'\xd1\x01') :
tmp=s.recvfrom(4096)[0]
recive_explain(tmp)
jpeg_recvdata=s.recvfrom(4096)[0]
if jpeg_recvdata.find(b'\xff\xd9') != -1 :
print ('This is the end!')
jpeg_return=jpeg_return+jpeg_recvdata[8:]
global fnum
fnum=fnum+1
t = time.localtime()
current_time = time.strftime("%D %H:%M:%S", t) # на каждый кадр наносим метку с датой и временем
myfont = ImageFont.truetype("arial.ttf", 20)
try:
io_bytes = io.BytesIO(jpeg_return)
im = Image.open(io_bytes)
draw_text = ImageDraw.Draw(im)
draw_text.text((10,10),current_time,fill=('#1C0606'), font=myfont)
im.save('D:\\pywrk\\cam\\my_jpeg\\pic'+str(fnum)+'.jpeg')
except:
pass
pac_num=list() # список с номерами полученных от камеры пакетов
def answer_to_cam(datinlist):
k='d200'
global pac_num
if datinlist[2] == 'd101' :
k='d201'
pac_num.clear()
if (datinlist[4]=='ffd8') : take_full_jpeg(datinlist)
if datinlist[3] in pac_num :
pac_num.clear()
pac_num.append (datinlist[3])
else :
pac_num.append (datinlist[3])
data_send=[k,'0001']
for x in range(0, len(pac_num)):data_send.append(pac_num[x])
my_send (HDR_D1+pack_size(''.join(data_send))+bytes.fromhex(''.join(data_send)))
def recive_explain(mydata):
partion=mydata.hex()
data_in_list=[]
for x in range (0, len (partion), 4) :
if (x>len (partion)) : break
data_in_list.append(partion[x:x+4])
if data_in_list[0]=='f1d0' :
answer_to_cam(data_in_list)
if data_in_list[0]=='f1d1' :
print ('')
if data_in_list[0]=='f1e0' :
my_send (MSG_E1_ANSW)
my_send (MSG_E0_Q)
if data_in_list[0]=='f1e1' :
print ('#cam answer tel - Fine!')
if data_in_list[0]=='f142' :
print ('#cam send to tel MSG_HELLO')
return (data_in_list[0])
def main():
i=0
my_send (MSG_HELLO)
my_send (MSG_E0_Q)
while (recive_explain(s.recvfrom(4096)[0])) != 'f1e1' : print ('In while f1e0')
my_send (MSG_LGN_ADMIN)
data=s.recvfrom(4096)
while data[0].find(b'\xf1\xd0\x00\x18') != 0 :
data=s.recvfrom(4096)
recive_explain(data[0])
my_ticket=take_ticket(data[0])
print(f"my_ticket - {my_ticket}")
my_send (MSG_E0_Q)
recive_explain(s.recvfrom(4096)[0])
my_send (MSG_TICKET_TIME_START+my_ticket+MSG_TICKET_TIME_END)
my_send (MSG_TICKET_START+my_ticket)
while (recive_explain(s.recvfrom(4096)[0])) != 'f1d0' : print ('In while d0')
for i in range(0, 15):
what_rec=recive_explain(s.recvfrom(4096)[0])
if what_rec == 'f1e0' : break
my_send (MSG_TICKET_START_2+my_ticket+MSG_TICKET_END_2) #то, самое "серьезное" предложение
my_send (MSG_TICKET_LONG_START+my_ticket+MSG_TICKET_LONG_END)
my_send (MSG_TICKET_TIME_START_2+my_ticket+MSG_TICKET_TIME_END_2)
while True:
tt=recive_explain(s.recvfrom(4096)[0]) #получаем постоянный поток кадров в формате jpeg
if __name__ == "__main__":
main()
Результат работы программы, мой Кадр – кошка Фрося.
P.S. 2
Нашёл это https://github.com/DavidVentura/cam-reverse «сокровище» уже после своих мучений.