WiFi panoramic camera: remote monitoring and control via web


WiFi panoramic camera: remote control your vision. Have you ever wanted to have complete control of your surveillance camera, being able to rotate the lens to explore every corner of your space? Thanks to the power of technology and your creativity, it is now possible. Welcome to the world of the motorized camera, a project that allows you to transform a simple camera into a dynamic and personalized surveillance tool, which can be consulted and operated directly from your mobile phone or PC.

This panoramic camera helps explore the possibilities offered by IoT (Internet of Things) technology and remote control. Using a microcontroller like Freenove ESP32-WROVER and an SG90 servo motor (whose rotor is integrally connected to it), you will make a device capable of rotating the camera lens remotely, allowing you to control the direction of the shot from anywhere via a web page visible both on your PC browser and on your mobile phone.

Thanks to this innovative solution, you have the freedom to monitor and protect your spaces with unprecedented flexibility. Whether you’re surveying your home, office, or any other environment, you now have the power to explore every corner and tailor your vision to your specific needs, right from the palm of your hand.

In this article, we will explore the making of this project in detail, from assembling the components to writing the code and implementing the mobile phone remote control functionality. We will discover the challenges encountered along the way and the creative solutions found to overcome them, paving the way for future projects and innovations in the field of intelligent surveillance.

Get ready to immerse yourself in an adventure of creativity and technology, and discover the limitless potential of the motorized camera.

As in every project in this blog we will use the excellent IDE Platformio.

In this article we will use a file system internal to the microcontroller called LittleFS and which is very useful when you want to store files internally without having to use a module for managing an external SD card. So let’s spend a few words on this file system.

The LittleFS filesystem

The LittleFS file system is a lightweight and efficient solution for data storage on embedded devices such as the Freenove ESP32-WROVER (but also other devices such as the ESP8266 and ESP32). Using LittleFS, you can store files such as HTML, CSS, JavaScript and other multimedia assets (for example images to be used, eventually, as icons or wallpapers) in an organized and accessible way for applications. The LittleFS file system operates similarly to the better-known SPIFFS (SPI Flash File System), but offers significant advantages in terms of reliability and memory management.

The files are stored in blocks of data called “pages” within the flash memory of the Freenove ESP32-WROVER. LittleFS organizes these blocks to maximize space efficiency and ensure rapid data accessibility. Additionally, LittleFS implements a metadata management system that allows you to track files, directories, and allocation information, ensuring reliable and consistent file system operation.

To transfer files to the Freenove ESP32-WROVER’s LittleFS file system, you need to use an upload tool like the PlatformIO command (which we’ll look at later) or the Arduino framework with support for LittleFS. Once files are uploaded, they can be accessed via the file system APIs provided by the LittleFS libraries. These APIs allow applications to read, write, and manage files similar to operations on a regular file system.

The LittleFS file system is especially useful for applications that require the storage of static files such as HTML web pages, CSS style sheets, and JavaScript scripts used for creating web-based user interfaces. These files can be accessed directly from the Freenove ESP32-WROVER firmware and used to deliver dynamic and interactive content over a Wi-Fi connection.

In summary, the LittleFS file system offers a reliable and efficient solution for storing files on devices (in this case the Freenove ESP32-WROVER), enabling easy data access and management for embedded applications.

The SG90 servomotor

The SG90 servo motor, which we will use to move the Freenove ESP32-WROVER so that it frames the scene in the desired direction, is an electromechanical component widely used in a wide range of applications, ranging from robotics to radio-controlled models, from home automation to electronic hobbies. Its popularity is due to its compactness, its reliability and its ease of use, making it one of the most popular and accessible servomotors on the market.

The operation of the SG90 servo motor is based on a precise position control mechanism. Inside the servo motor, there is an electric motor, a set of gears and a control circuit. The control signal sent to the servo motor determines the position of the motor shaft, allowing accurate control of its rotation over a range of approximately 180 degrees.

A key point of the SG90 servo motor is its ability to operate in position feedback mode, which means the motor constantly monitors the position of the shaft and works to keep it in the desired position. This makes it ideal for applications requiring precise and reliable position control, such as steering controls in radio-controlled vehicles, movements of robotic arms or automatic door openings.

To use an SG90 servo motor, you need to power it with an appropriate voltage and provide a control signal via an input pin. The control signal is a pulse width modulated (PWM) pulse with a typical frequency of 50 Hz and a variable duty cycle, which determines the desired position of the servo motor. You can drive the servo motor using a microcontroller such as the Freenove ESP32-WROVER, which generates control signals based on program instructions.

In summary, the SG90 servo motor is a critical component for a variety of electronic projects requiring precise and reliable position control. With its ease of use and reliable performance, it has become a favorite actuator for hobbyists and engineers looking to bring their ideas to life.

The SG90 servo motor that we will use here has 3 connection wires: two for the 5V power supply and the third for its control. Being small and not very powerful, it is suitable for acting on small loads.

What is PWM?

PWM, an acronym for Pulse Width Modulation, is a technique used to control the power supplied to electronic devices such as motors, LEDs and servomotors. This technique works by varying the pulse width of a digital signal at a constant frequency, allowing you to adjust the amount of energy delivered to the load.

The basic principle of PWM consists of rapid alternation between two states: a high state (ON) and a low state (OFF). During the high state, the signal is active and the load receives energy; during the low state, the signal is inactive and the load receives no power. The duration of the high pulse relative to the total cycle duration defines the duty cycle ratio, generally expressed as a percentage. For example, a 50% duty cycle means the signal is active for half the total cycle time.

The use of PWM allows you to adjust the power supplied to the load by varying the duty cycle ratio. Increasing the duty cycle increases the power supplied to the load, while decreasing the duty cycle reduces the power supplied. This ability to regulate power quickly and efficiently makes PWM ideal for controlling devices that require power modulation, such as DC motors, brushless motors, LED lamps, and servo motors.

In the context of servo motor control, PWM is used to send control signals that determine the desired position of the servo. These signals, called PWM pulses, have a typical frequency of 50 Hz and a pulse duration that varies between 1 ms and 2 ms, with a duration of 1.5 ms usually corresponding to the center position of the servo. By changing the duration of the PWM pulse, you can adjust the position of the servo over a range of approximately 180 degrees, allowing precise control of its rotation.

In conclusion, PWM is a fundamental technique in modern electronics, used to control the power supplied to electronic devices efficiently and precisely. Its versatility makes it widely used in a variety of applications, from regulating the speed of motors to regulating the light intensity of LEDs, up to the position control of servomotors.

The microcontroller with webcam used in the project

Nowadays we all have to deal with video surveillance systems: in the streets, in shops, at home (to control unwanted access by any criminals or perhaps to secretly observe the behavior of our furry friends in our absence). Today’s technology comes to our aid as it provides us with increasingly miniaturized and economical boards, cameras and storage systems. Just think of the size (and cost) of video cameras from a few decades ago!

Today we have boards available that are capable of managing a very small camera, connecting to the Internet via WiFi and sending us the images taken in real time, without needing our presence.

One of these boards is the Freenove based on ESP32-WROVER chip equipped with a tiny but effective camera:

Front view of the Freenove board
Front view of the Freenove board

Side view of the Freenove board
Side view of the Freenove board

As mentioned, the board is equipped with an OV2640 model camera capable of taking still and moving images.

What components do we need?

The list of components is not particularly long:

  • an SG90 servomotor
  • some DuPont wires (male – male, male – female, female – female)
  • and, obviously, a Freenove ESP32-WROVER CAM Board with camera, which can be purchased on Amazon at this link!

Let’s carry out the project

The implementation scheme

Let’s first see the pinout of our board:

The pinout of the Freenove ESP32 WROVER CAM
The pinout of the Freenove ESP32 WROVER CAM

Not all pins will be freely usable as they are used by the camera (those marked with light blue labels starting with CAM). The GPIO that we will use in this project will be 33, not used by the camera, and we will need it to send the control signal to the SG90 servomotor.

While the servomotor looks like this:

The SG90 servo motor used in this project
The SG90 servo motor used in this project

Note the unusual colors used for the connecting wires. In particular we have:

  • the brown wire will go to ground
  • the red wire will go to the 5V pin of the Freenove ESP32-WROVER
  • the orange wire is the control one and we will connect it to GPIO 33 of the Freenove ESP32-WROVER

Consequently, the electrical diagram is extremely simple, as demonstrated by the following image created with Fritzing:

Electrical diagram made with Fritzing of the panoramic camera
Electrical diagram made with Fritzing of the panoramic camera

Here the brown wire is represented with a black wire while the orange wire with a yellow wire.

To complete everything, a simple structure must be created that fixes the servo rotor integrally to one of the short sides of the Freenove ESP32-WROVER. I simply tied them with a thin string just to see how it worked (which you will see in the video at the end of the article).

The sketch

Let’s create the PlatformIO project

We have already seen in a previous article how to create a project using the excellent IDE PlatformIO. Let’s then create our project following the instructions in the article How to create a project for NodeMCU ESP8266 with PlatformIO. Although it refers to the ESP8266 board, the procedure is similar. Simply, when choosing the platform we will have to choose the Espressif ESP-WROVER-KIT.

Let’s skip the part on how to add libraries to the project as we don’t need them.

Now edit the platformio.ini file to add these two lines:

monitor_speed = 115200
upload_speed = 921600

so that the file looks like this:

platform = espressif32
board = esp-wrover-kit
framework = arduino
monitor_speed = 115200
upload_speed = 921600

At this point we add to the same file the part concerning the management of the file system, the memory partitions, the camera and the libraries used, so that it has this form:

platform = espressif32
board = esp-wrover-kit
framework = arduino
monitor_speed = 115200
upload_speed = 921600
board_build.partitions = min_spiffs.csv
board_build.filesystem = littlefs
build_flags = -DBOARD_HAS_PSRAM
lib_deps =

As you will see, the two imported libraries are the one that manages the web server (ESPAsyncWebServer) which we download directly from its git repository and the one that manages the servomotor (ESP32Servo).

Download the project now from the link below:

Replace the main.cpp file of the project you created with the one present in the zip file. Then, in the project you created, create a folder named data at the same level as the src folder and copy into it the index.html, script.js and styles.css files that you will find in the data folder of the project you just downloaded. These files will be transferred (with a procedure that we will see later) into the LittleFS file system inside the board and constitute the web part of the application. The index.html file is the actual web page, the styles.css file gives it a style, albeit quite minimal and simple, while the script.js file is a javascript that manages the actions on the buttons on the page to rotate right/left/center the servomotor (and consequently the Freenove mechanically attached to it) interacting with the appropriate functions present in the main.cpp file.

Still from the downloaded project, copy the two files camera_index.h and camera_pins.h from the include folder and paste them into the include folder of the project you created. These two files describe the technical characteristics of the camera.

How to transfer index.html, script.js and styles.css files to the LittleFS file system

The operation is quite simple. It is necessary to open a new terminal on PlatformIO with the button indicated in the figure:

Button to open a new terminal
Button to open a new terminal

write the following command:

pio run -t uploadfs

and press the ENTER key. If all goes well, the three files will be transferred to the LittleFS file system. If necessary, stop viewing via Serial Monitor as it may conflict with the upload operation as it uses (and sometimes monopolizes) the communications port.

PLEASE NOTE: it is important that the index.html, script.js and styles.css files are located, as already mentioned at the beginning, in a folder named data at the same level as the src folder.

Loading the sketch, however, follows the normal way.

Now let’s take a look at the sketch that starts with a #define:


which defines the type of camera we are using on the board.

The inclusion of the necessary libraries follows:

#include "camera_pins.h"
#include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include "esp_camera.h"
#include <LittleFS.h>
#include <ESP32Servo.h>

After which we will have to provide the access data to our WiFi network (SSID and PASSWORD):

const char* ssid = "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZ";                    // put here the SSID of your WiFi network
const char* password = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY";    // put here the password of your WiFi network

The instructions that initialize the servo follow: the servo object that manages the servomotor is created, the GPIO to which it will be connected for its control is defined, the servoPosition variable is defined, initialized at 90 degrees, which contains the current rotor position of the servo and the increment variable which will cause the servo rotor to increment or decrement by 5 degrees when you press the Left/Right buttons on the web interface:

Servo servo;  // create servo object to control a servo
static const int servoPin = 33;
int servoPosition = 90;    // variable to store the servo position
int increment = 5;

Obviously you can put the value you prefer in the increment variable.

The webserver is then initialized and listens on port 80. The prototypes of the handleLeft, handleCenter and handleRight functions follow, which are developed at the end of the sketch:

AsyncWebServer server(80);

void handleLeft();
void handleCenter();
void handleRight();

Then follows the setup function. Initially activates the serial port and prints a message attempting to connect to the network with the SSID previously specified:


Serial.print("WiFi connection to ");

Then try to establish connection with the wireless network. If successful, print the IP assigned to it by the router on the Serial Monitor:

WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {

Serial.println("WiFi connection established");

Serial.print("IP address: ");

It is very important to take note of this IP address as the URL to put in the browser (on your PC or mobile phone) will be of the type:


Then follows the activation of the LittleFS file system:

  if (!LittleFS.begin()) {
    Serial.println("Error initializing the LittleFS file system");

In case of failure the board crashes as it does not have access to the files in the file system and therefore cannot continue to function.

Then follows a series of camera configuration parameters:

camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;

The most significant block for us is the following:

config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 1;

The first line decides the size of the captured image. This value was chosen to be compatible with a smartphone. If you want to change it to have larger or smaller images, you can try these values ​​to see which is most suitable:

FRAMESIZE_96X96,    // 96x96
FRAMESIZE_QQVGA,    // 160x120
FRAMESIZE_QCIF,     // 176x144
FRAMESIZE_HQVGA,    // 240x176
FRAMESIZE_240X240,  // 240x240
FRAMESIZE_QVGA,     // 320x240
FRAMESIZE_CIF,      // 400x296
FRAMESIZE_HVGA,     // 480x320
FRAMESIZE_VGA,      // 640x480
FRAMESIZE_SVGA,     // 800x600
FRAMESIZE_XGA,      // 1024x768
FRAMESIZE_HD,       // 1280x720
FRAMESIZE_SXGA,     // 1280x1024
FRAMESIZE_UXGA,     // 1600x1200
// 3MP Sensors
FRAMESIZE_FHD,      // 1920x1080
FRAMESIZE_P_HD,     //  720x1280
FRAMESIZE_P_3MP,    //  864x1536
FRAMESIZE_QXGA,     // 2048x1536
// 5MP Sensors
FRAMESIZE_QHD,      // 2560x1440
FRAMESIZE_WQXGA,    // 2560x1600
FRAMESIZE_P_FHD,    // 1080x1920
FRAMESIZE_QSXGA,    // 2560x1920

For example, if we wanted a 1600×1200 image we would choose the FRAMESIZE_UXGA value and put it in place of FRAMESIZE_QVGA.

The quality of the reproduced image ranges from 0 (maximum quality) to 63 (minimum quality) and is managed by the config.jpeg_quality parameter.

The config.fb_count parameter indicates the number of frame buffers to allocate. If greater than one, each frame will be captured.

We proceed by trial and error with these parameters to obtain the desired result.

The camera is then initialized:

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Error initializing the camera: %d", err);

Here too, in case of failure the sketch stops.

Then follow the functions which, in the event of a request, read the index.html, styles.css and script.js files from the file system and serve them via the webserver. A further function serves the stream of images coming from the camera:

server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/index.html", "text/html");

server.on("/styles.css", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/styles.css", "text/css");

server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/script.js", "text/javascript");

server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request){
camera_fb_t * fb = NULL;
fb = esp_camera_fb_get();
if (!fb) {
    request->send(500, "text/plain", "Error during frame acquisition");

The following functions serve the web page as a response following the request or return a 404 error, classic of pages not found:

    AsyncWebServerResponse *response = request->beginResponse_P(200, "image/jpeg", fb->buf, fb->len);

  server.onNotFound([](AsyncWebServerRequest *request){
    request->send(404, "text/plain", "Page not found");

We now meet the three functions that manage the three left/center/right direction buttons (via the aforementioned javascript) and call, respectively, the handleLeft(), handleCenter() and handleRight() functions that will manage the servomotor:

server.on("/left", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");

server.on("/center", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");

server.on("/right", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");

Immediately afterwards the server is initialized:


The function ends with the setting of the parameters to control the servomotor:

servo.setPeriodHertz(50);    // standard 50 hz servo
servo.attach(servoPin, 790, 4000);   // choose these values ​​by trial and error in order to calibrate the two extremes of the servomotor between 0 and 180 degrees

In particular, the values ​​790 and 4000 in the function servo.attach(servoPin, 790, 4000); they must be determined by trial and error in order to calibrate the extreme values ​​(from 0 to 180 degrees) of the angle assumed by the servomotor rotor.

We then encounter the loop function. Empty because it doesn’t have to do anything.

At the end there are the three functions handleLeft, handleCenter and handleRight that control the servomotor:

void handleLeft() {
  // Move left
  Serial.println("Moving left");
  servoPosition = servoPosition + increment;
  if(servoPosition > 170) {servoPosition = 170;};
  servo.write(servoPosition);  // Change the desired angle
  delay(1000);       // Add a pause to allow the servo to reach the desired position

void handleCenter() {
  // Move to the center
  Serial.println("Moving center");
  servoPosition = 80;
  servo.write(servoPosition);  // Change the desired angle
  delay(1000);       // Add a pause to allow the servo to reach the desired position

void handleRight() {
  // Move right
  Serial.println("Moving right");
  servoPosition = servoPosition - increment;
  if(servoPosition < 10) {servoPosition = 10;};
  servo.write(servoPosition);  // Change the desired angle
  delay(1000);       // Add a pause to allow the servo to reach the desired position

The handleLeft adds to the servoPosition variable (and therefore to the position of the servo) a value in degrees equal to that contained in the increment variable (which you can modify as desired) and gives the command to the servomotor to assume this angle.

The handleRight subtracts from the servoPosition variable (and therefore from the position of the servo) a value in degrees equal to that contained in the increment variable (which you can modify as desired) and gives the command to the servomotor to assume this angle.

The handleCenter brings the servomotor to the central position. Both handleLeft and handleRight limit the value assumed by the servoPosition variable (170 and 10 degrees respectively) so as not to assign unacceptable values ​​to the servomotor.

Access from outside

The IP address assigned by the router to the board is obviously part of a set of addresses of an internal network (typically our home or office) which therefore has no access to the outside. This means that our motorized camera is not visible from the outside. What should be done to be able to access it from the outside?

To allow access to your motorized camera remotely, you need to expose your device outside of your local network.

PLEASE NOTE: this is a potentially dangerous procedure which, if performed by inexperienced people, could expose the internal network to the outside in an uncontrolled manner. So only continue if you know exactly what you are doing (or have an expert systems engineer do it)!

This involves a few steps:

  1. Configure port forwarding on your router: access the router settings and configure port forwarding. This will redirect traffic arriving on the specific port of your router to the local IP of your motorized camera. Typically, you should forward the ports used to access the camera’s web server and for video streaming (for example, HTTP and RTSP ports).
  2. Static IP address or DDNS service: to ensure that your device is always reachable from the outside, consider using a static IP address assigned by your ISP or a DDNS (Dynamic DNS) service. With a DDNS service, you can use a custom domain name that automatically updates when your router’s IP address changes.
  3. Configure security: make sure you implement appropriate security measures to protect your device from unauthorized access. Use strong passwords to access your device and consider using encrypted connections via HTTPS to protect transmitted data.
  4. Test remote access: once you have configured port forwarding and any DDNS service, try accessing your motorized camera from an external device using your public IP address or custom domain name. Make sure everything is working properly and that you can view the video stream and control the servo remotely.

Remember that exposing devices on the public network poses some security risks, so it’s important to take the necessary precautions to protect your device and your data!

Video of the operation of the panoramic camera

Let’s summarize the operations to be carried out once the project is completed:

  • upload the static files present in the folder named data with the pio run -t uploadfs command
  • compile and upload the sketch to the board as usual
  • write down the IP address (which we will call IP_ADDRESS) provided by the board on the Serial Monitor
  • open a browser on your mobile phone or PC and write the URL: http://IP_ADDRESS/index.html

The functioning of the prototype is shown below. As I already said, I simply tied the servo rotor to the body of the board with a simple string and kept the servo suspended by simply holding it between my fingers. The more daring will be able to create a more elaborate structure (perhaps with 3D printing) so that the whole thing is more stable and, above all, able to stand upright independently.


If you want to be informed about the release of new articles, subscribe to the newsletter. Before subscribing to the newsletter read the page Privacy Policy (UE)

If you want to unsubscribe from the newsletter, click on the link that you will find in the newsletter email.

Enter your name
Enter your email
0 0 votes
Article Rating
Inline Feedbacks
View all comments
Would love your thoughts, please comment.x
Scroll to Top