├── .gitignore ├── .vscode ├── arduino.json └── c_cpp_properties.json ├── LICENSE ├── README.md ├── settings.h.template └── src ├── interface ├── decoder │ ├── controller │ │ ├── decoder_controller.hpp │ │ └── gm7150_decoder_controller.hpp │ └── decoder_interface.hpp └── wifi_interface.hpp ├── main.ino └── usersettings.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | src/settings.h 3 | -------------------------------------------------------------------------------- /.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "board": "esp32:esp32:esp32wrover", 3 | "configuration": "PartitionScheme=default,FlashMode=qio,FlashFreq=80,UploadSpeed=921600,DebugLevel=none", 4 | "sketch": "src/main.ino", 5 | "output": "build" 6 | } -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "C:\\Users\\Yakov\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\**", 7 | "C:\\Users\\Yakov\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\1.0.2\\**", 8 | "${workspaceFolder}/src" 9 | ], 10 | "forcedInclude": [], 11 | "intelliSenseMode": "${default}", 12 | "compilerPath": "/usr/bin/gcc", 13 | "cStandard": "c11", 14 | "cppStandard": "c++14" 15 | } 16 | ], 17 | "version": 4 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yakov Lipkovich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Analog Composite Video Transmission over Wi-Fi for ESP32 2 | ======================================================== 3 | 4 | Goals 5 | ----- 6 | * Transmit a NTSC signal captured by a CCTV camera over Wi-Fi to a central monitoring server 7 | * Reduce bandwidth for transmission, as a raw BT.601/BT.656 stream requires a bandwidth of 216Mbps, which is unacceptable even over LAN 8 | * Minimize costs per converter/transmitter 9 | 10 | Equipment 11 | --------- 12 | * Any NTSC composite camera - in my case, I have a handful of Bosch VDC-455V04-20 CCTV cameras 13 | * NTSC-M digital video decoder - whatever the cheapest you can find on eBay, e.g. 14 | * TW9900 15 | * AK8859VN 16 | * GM7150BC 17 | * ESP32 module **with >=1MB pSRAM** - ESP32's SRAM is not big enough to store even a single frame 18 | * Various power supplies and/or voltage regulators required to power the decoder/ESP32 19 | * Additional components as required by the decoder/ESP32 20 | 21 | ### Video Decoder Requirements 22 | * Compliant BT.601 output 23 | * Has I²C interface (AKA two wire serial interface) 24 | * Can output synced, hsync, vsync signals 25 | * Ability to remove hsync/vsync, SAV/EAV signals from transmission 26 | 27 | Interface Pinout 28 | ---------------- 29 | ``` 30 | +-----------+ 31 | | AIN | <---- () Analog NTSC composite video 32 | | | 33 | | | +---------------+ 34 | | SCLK | <---- | I2C_SCL | 35 | | SDAT | <---> | I2C_SDA | 36 | | | | | 37 | | DCLK | ----> | I2S0I_WS_in | 38 | | SYNC (EN) | ----> | I2S0_H_ENABLE | 39 | | HSYNC | ----> | I2S0_H_SYNC | 40 | | VSYNC | ----> | I2S0_V_SYNC | 41 | | DATA[7:0] | ----> | I2S0I_Data_in | 42 | +-----------+ +---------------+ 43 | ``` 44 | 45 | Order of Operations 46 | ------------------- 47 | 1. Set up logging 48 | 2. Set up Wi-Fi 49 | 3. Set up I²S transfer 50 | * This includes allocating enough DMA RAM for one line of data 51 | * Invert HSYNC/VSYNC/enable signals in software if necessary 52 | 4. Allocate enough RAM on pSRAM for one complete frame 53 | 5. Set up decoder 54 | 6. Wait until client has connected 55 | 7. Start decoder 56 | 8. Wait until signal is syncronized so data transfer can begin 57 | 9. When HSYNC is active: 58 | * Copy the line data to pSRAM, swapping BT.601's `CbY0CrY1` data order to the JPEG library's `Y0CbY1Cr` expected input 59 | * Remember that since the signal is interlaced, copy into rows 1, 3, 5, ..., and then to rows 2, 4, 6, ... 60 | * In the event this copying takes longer than HSYNC, allocate a second DMA RAM chunk for the next line 61 | 10. When VSYNC is reached, repeat step 7, but prepare to copy field 2 (even rows) 62 | 11. On the next VSYNC: 63 | * Call the JPEG library to convert the resulting YUV frame to JPEG 64 | * Transfer result, along with headers, over Wi-Fi 65 | * If this takes longer than the VBLANK period, offload these tasks to the second core. 66 | * If the conversion is not done before end of VBLANK period, create another buffer for the frame on pSRAM 67 | 12. Go to step 8, waiting until VBLANK is done -------------------------------------------------------------------------------- /settings.h.template: -------------------------------------------------------------------------------- 1 | .wifi_ssid = "xxx", 2 | .wifi_pass = "xxx", 3 | 4 | .scl_pin = 0, 5 | .sda_pin = 0, 6 | 7 | .dclk_pin = 0, 8 | .enable_pin = 0, 9 | .hsync_pin = 0, 10 | .vsync_pin = 0, 11 | .d7_pin = 0, 12 | .d6_pin = 0, 13 | .d5_pin = 0, 14 | .d4_pin = 0, 15 | .d3_pin = 0, 16 | .d2_pin = 0, 17 | .d1_pin = 0, 18 | .d0_pin = 0 19 | 20 | #define DECODER_MODEL XXXXXX 21 | -------------------------------------------------------------------------------- /src/interface/decoder/controller/decoder_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../../usersettings.hpp" 4 | 5 | class BaseDecoderController 6 | { 7 | protected: 8 | BaseDecoderController(const UserSettings& settings) 9 | : _settings(settings) 10 | {} 11 | 12 | public: 13 | virtual void setup(); 14 | 15 | protected: 16 | const UserSettings& _settings; 17 | }; 18 | 19 | #if DECODER_MODEL == GM7150 /* AKA SLES213 */ 20 | #include "gm7150_decoder_controller.hpp" 21 | #else 22 | #error Video decoder model is unknown or undefined 23 | #endif 24 | -------------------------------------------------------------------------------- /src/interface/decoder/controller/gm7150_decoder_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "decoder_controller.hpp" 4 | 5 | class DecoderController : public BaseDecoderController 6 | { 7 | public: 8 | DecoderController(const UserSettings& settings) 9 | : BaseDecoderController(settings) 10 | {} 11 | 12 | void setup() override 13 | { 14 | // TODO: 15 | // 0x03 enable output ??? 16 | // 0x0D Set mode to BT.601 17 | // 0x0F Set clock to pixel clock ??? -> probably not 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/interface/decoder/decoder_interface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "controller/decoder_controller.hpp" 4 | 5 | class DecoderInterface 6 | { 7 | public: 8 | DecoderInterface(const UserSettings& settings) 9 | : _controller(settings) 10 | {} 11 | 12 | inline void setup() 13 | { 14 | _controller.setup(); 15 | } 16 | 17 | private: 18 | DecoderController _controller; 19 | }; 20 | -------------------------------------------------------------------------------- /src/interface/wifi_interface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../usersettings.hpp" 4 | #include 5 | 6 | class WifiInterface 7 | { 8 | public: 9 | WifiInterface(const UserSettings& settings) 10 | : _settings(settings) 11 | {} 12 | 13 | void start() 14 | { 15 | WiFi.begin(_settings.wifi_ssid, _settings.wifi_pass); 16 | } 17 | 18 | private: 19 | const UserSettings& _settings; 20 | }; 21 | -------------------------------------------------------------------------------- /src/main.ino: -------------------------------------------------------------------------------- 1 | #include "usersettings.hpp" 2 | 3 | UserSettings settings { 4 | #include "settings.h" 5 | }; 6 | 7 | #include "interface/decoder/decoder_interface.hpp" 8 | #include "interface/wifi_interface.hpp" 9 | 10 | WifiInterface wifi(settings); 11 | DecoderInterface decoder(settings); 12 | 13 | void setup() { 14 | wifi.start(); 15 | decoder.setup(); 16 | } 17 | 18 | void loop() { 19 | // put your main code here, to run repeatedly: 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/usersettings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct UserSettings 4 | { 5 | const char *wifi_ssid; 6 | const char *wifi_pass; 7 | 8 | char scl_pin; 9 | char sda_pin; 10 | 11 | int dclk_pin; 12 | int enable_pin; 13 | int hsync_pin; 14 | int vsync_pin; 15 | int d7_pin; 16 | int d6_pin; 17 | int d5_pin; 18 | int d4_pin; 19 | int d3_pin; 20 | int d2_pin; 21 | int d1_pin; 22 | int d0_pin; 23 | }; 24 | --------------------------------------------------------------------------------