← Back to list

Power, Cameras & Communications

With the XIAO ESP32S3 Sense series at its core.

Mark Lucking in Level Up Coding · 2024-09-12 16:01 · 62 claps · 10.5 min read paywalled
#seeed-studio #swift #sockets #mirroring #ios
Open on Medium ↗
Wiki topics: STP · Startups & Venture 📱 · Mobile Development 🔒 · Cybersecurity 📷 · Photography

Power, Cameras & Communications

With the XIAO ESP32S3 Sense series at its core.

A composite of images taken and saved on an SD card

A composite of images taken and saved on an SD card

This is the third article in a series on building an open-source camera using Seeed Studio as a central piece. If you come to this one first, here are the links to the other two articles.

[embed]An Open Source Security Camera Prototype: Using XIAO ESP32S3 Sense and an iOS device.levelup.gitconnected.com

[embed]An Open Source Security Camera Network & Storage: Using the XIAO ESP32S3 Sense at its core.levelup.gitconnected.com

[embed]An Open Source Security Camera More Camera, More Comms, Finishing Uplevelup.gitconnected.com

The moment of truth was Friday evening. I unplugged the camera and checked the power and the SD card. I had over 400 photos, and the battery reported 100% power. The images taken were also at full resolution, some 1600 x 1200. It's almost HD, but not quite. I needed to do more research on the subject.

Since I wasn’t sure what the power drain had been in earlier tests, I decided to re-group and review what I had. During that process, I found this excellent thread on the ESP32S3's power consumption and some code fixes. The main topic of discussion is the camera options and their power signatures.

[embed]Xiao esp32s3 sense camera sleep current When I put the Xiao sense in deep sleep mode, I still see 20ma of current and not the 3ma spec'd. Could you please…forum.seeedstudio.com

As you can see, the OV5640 consumes more than four times the power of the OV2640 when running, although ironically, it uses far, far less than it when in sleep mode.

Was this the answer to the power drain? This is the code to put the thing into sleep mode should you want to save power.

[embed]ESP32 Timer Wake Up from Deep Sleep | Random Nerd Tutorials This tutorial shows how to put the ESP32 in deep sleep mode and wake it up with a timer after a predetermined amount of…randomnerdtutorials.com

I updated my code to save images to the SD with the sleeping instructions and plugged the weaker power pack to test it. I was pleasantly surprised at how long it lasted with the power-saving code. I took 282 photos, lasting some 3 hours and 45 minutes. I then tested it again without the power-saving code. I was shocked to see the difference. I took just 22 photos in 45 minutes. It was so bad that I tested it again the next day; it did even worse the second time.

This was the most surprising result I had encountered on the journey. I am sure the makers of these little boards had watched Apollo 11 and learnt from the lessons there. I had expected an improvement, but most certainly not as sublime as this.

[embed]Sleep Modes - ESP32-S3 - - ESP-IDF Programming Guide v5.3.1 documentation ESP32-S3 supports two major power-saving modes: Light-sleep and Deep-sleep. According to the features used by an…docs.espressif.com

I decided to try out the new code with sleep configured using the OV5640 to save images taken to the SD with the weaker power pack again. Remember, this consumes five times the power when running but a fraction of the power when it is on standby. It ran for three hours and twenty minutes and took 191 photos, 5K images with a resolution of 2560x1920. That's a pretty reasonable result.

[embed]Camera Usage for Sense Version | Seeed Studio Wiki This tutorial will explain how to properly use the camera on the XIAO ESP32S3 Sense.wiki.seeedstudio.com

Here is a graphic to illustrate the difference. The OV2640 is between the yellow and orange squares, while the OV5640 is in the blue, green, orange, and yellow squares.

I asked Google about commercial security cameras on the market, and it said.

So this is one of those tough calls. I can get out-of-this-world resolution, but the price paid, even with sleep mode, is power. Coincidently, I had hardly scratched the surface of the SD card, using just 1% of it to save these images.

Of course, capturing all these images on the SD card was one thing. I still needed a way to view them easily without consuming even more power by running a webserver on it.

Communications

Although there were several protocols for these chips to talk to each other and beyond, on closer inspection, few looked very promising when tasked with transferring files.

Zigbee

I needed an XIAO ESP32C6 to implement the ZigBee protocol on both sides, so this wasn’t remotely a contender. Worse, I would also have to ditch the Arduino IDE and use ESP-IDF v5.1.3.

ESP-Now

I looked into the ESP-NOW protocol. It didn’t fit the open-source agenda, but it comes with all the chips and is free, so we’re halfway there.

[embed]ESP-NOW protocol on XIAO ESP32 Series | Seeed Studio Wiki Using XIAO ESP32S3/XIAO ESP32C3/XIAO ESP32C6 conduct communcation on ESP-NOW protocolwiki.seeedstudio.com

The documentation claimed that ESP-NOW is superior to BLE as a meshing protocol because it doesn’t require peering or WiFi. However, reading through the tutorial, I realized that data packets are limited to 250 characters—a limit that wouldn’t be practical if you wanted to send images.

Although it looked like an excellent choice for remotely turning things on and off, so it wasn’t a complete loss.

BLE and Bluetooth

I looked through the BLE code Seeed Studio has on its Wikipedia pages; it looked good, but it is all based on Seeed Studio chips talking to each other.

[embed]Bluetooth for both versions | Seeed Studio Wiki Bluetooth usage with Seeed Studio XIAO ESP32S3.wiki.seeedstudio.com

I also found an excellent reference for Bluetooth code on the Apple developer’s website.

[embed]Transferring Data Between Bluetooth Low Energy Devices | Apple Developer Documentation Create a Bluetooth low energy central and peripheral device, and allow them to discover each other and exchange data.developer.apple.com

But that was where the theory crashed into practice. BLE and Blue Tooth are pretty different. After a few SO exchanges and hours of futile coding, I eventually conceded I wouldn’t get the ESP32S3 talking to my iOS device without some serious code tweaking and a far better understanding of BLE and Bluetooth than I had the patience to master. I was sure it was possible since I had seen a few YouTube tutorials making them talk together, but I wasn’t convinced I would get the bandwidth needed to do the transfers anyway. It wasn’t worth the effort.

WebSockets

Was this the answer? Since sockets are stateful and HTTP requests are not, it was a more efficient way to transfer data blocks than a web server. I googled the subject and hit the jackpot with this excellent article.

[embed]ESP32 WebSocket Server: Control Outputs (Arduino IDE) | Random Nerd Tutorials Build a web server with the ESP32 using WebSocket communication protocol. We’ll create a web page to control the ESP32…randomnerdtutorials.com

This article had the peer code for the iOS client.

[embed]WWDC Review: How to use URLSessionWebSocketTask in Swift With the latest updates from WWDC 2019, WebSocket is now a first-class citizen in iOS, macOS, tvOS, and watchOS.www.appspector.com

I followed the tutorial from the random nerd guy, and with a few tiny changes and this command line tool websocat I got everything working.

while [ 1 ]
do
now=$(date +%s)
date 
echo "toggle" | websocat --one-message ws://192.168.1.119/ws
sleep 60
done

But — I soon discovered I could not disable the webserver and keep the web sockets running; one relied on the other. I don’t know how I missed that detail. Web sockets were the best fit for copying data, but who am I kidding? I was trying to avoidrunning a web server on the camera processor.

As I regrouped again — and reflected on the architecture, I recalled one of my first thoughts: including expensive AppleKit in the solution made no sense. What was I thinking? I should test the BLE protocol between the camera server and the ESP32C3 or ESP32C6, although I was missing a small detail here — unlike the S3, neither had an SD card slot.

At that point, I came across this little beast from Seeed.

[embed]Hardware Usage | Seeed Studio Wiki This tutorial introduces the use of the expansion board function of the circular screen.wiki.seeedstudio.com

It had an SD card slot and provided the perfect means to configure my camera options and give feedback. But wait—I was distracted. I still needed a way to copy data from one SD card to the other, and BLE wasn’t going to cut it.

I decided to take a longer look through the Seeed Wikipedia WiFi documentation.

[embed]WiFi for both versions | Seeed Studio Wiki WiFi usage with Seeed Studio XIAO ESP32S3.wiki.seeedstudio.com

And saw something I had previously missed. It looked like I could connect to a port directly on a second platform, the subject of one of the first articles I published on medium.com. So, a custom socket — precisely what I needed.

[embed]Sockets in iOS marklucking.medium.com

I connect and transfer files through sockets to other ESPs or iOS devices without worrying about protocol compatibility issues. But going as ever going from theory to practice took some time. I spent a good day or two arsing around with the code until I finally found the solution.

First, I present the Swift code, which is incomplete but has the essentials. It runs a socket server that receives JPEGs over the air and displays their contents. This is a slightly modified version of the code I referenced earlier.

import Network
import SwiftUI
import Combine

let downloadedImage = PassthroughSubject<UIImage,Never>()

class Connect: NSObject {

    private var talking: NWConnection?
    private var listening: NWListener?
    private var nString: String = ""
    private var cString: String = ""

    func listenTCP(port: NWEndpoint.Port) {
        do {
            self.listening = try NWListener(using: .tcp, on: port)
            self.listening?.stateUpdateHandler = {(newState) in
                switch newState {
                case .ready:
                    print("ready")
                default:
                    break
                }
            }

            self.listening?.newConnectionHandler = {(newConnection) in
                newConnection.stateUpdateHandler = {newState in
                    switch newState {
                    case .ready:
                        print("new connection")
                        self.receive(on: newConnection)
                    default:
                        break
                    }
                }
                newConnection.start(queue: DispatchQueue(label: "new client"))
            }
        } catch {
            print("unable to create listener")
        }
        self.listening?.start(queue: .main)
    }

    func connectToTCP(hostTCP:NWEndpoint.Host,portTCP:NWEndpoint.Port) {
        talking = NWConnection(host: hostTCP, port: portTCP, using: .tcp)
        talking?.stateUpdateHandler = { (newState) in
            switch (newState) {
            case .ready:
                break
            default:
                break
            }
        }
        talking?.start(queue: .main)
    }

    func connectToUDP(hostUDP:NWEndpoint.Host,portUDP:NWEndpoint.Port) {
        talking = NWConnection(host: hostUDP, port: portUDP, using: .udp)
        talking?.stateUpdateHandler = { (newState) in
            switch (newState) {
            case .ready:
                break
            default:
                break
            }
        }
        talking?.start(queue: .main)
    }

    func sendUDP(_ content: String) {
        let contentToSendUDP = content.data(using: String.Encoding.utf8)
        talking?.send(content: contentToSendUDP, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
            if (NWError == nil) {
                // code
            } else {
                print("ERROR! Error when data (Type: String) sending. NWError: \n \(NWError!) ")
            }
        })))
    }

    func sendTCP(_ content: String) {
        let contentToSendTCP = content.data(using: String.Encoding.utf8)
        talking?.send(content: contentToSendTCP, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
            if (NWError == nil) {
                // code
            } else {
                print("ERROR! Error when data (Type: String) sending. NWError: \n \(NWError!) ")
            }
        })))
    }

    func receive(on connection: NWConnection) {
        print("receiving image")
        connection.receiveMessage { (data, context, isComplete, error) in
            if let error = error {
                print(error)
                return
            }
            print("decoding image")
            if let data = data, !data.isEmpty {

                let libraryDirectory = NSSearchPathForDirectoriesInDomains(.libraryDirectory,
                                                                               .userDomainMask,
                                                                               true)[0]
                let libraryURL = URL(fileURLWithPath: libraryDirectory, isDirectory: true)
                let fileURL = libraryURL.appendingPathComponent("image.jpg")
                print("file URL ",fileURL)
                do {
                    try data.write(to: fileURL)
                } catch {
                    print(error)
                }
                DispatchQueue.main.sync {
                    let image = UIImage(contentsOfFile: fileURL.path())
                    downloadedImage.send((image ?? UIImage(named:"man"))!)
                }
                print("saved image ")
            }
        }
    }

    func makeConnect(port: String, message: String) {
        print("makeConnect ",port,message)
        let host = NWEndpoint.Host.init("192.168.1.119")
        let port = NWEndpoint.Port.init(port)
        communication.connectToTCP(hostTCP: host, portTCP: port!)
        communication.sendTCP(message)
    }

}

class BlobModel: ObservableObject {
    static let shared = BlobModel()
    @Published var score: String = ""
}

var globalVariable = BlobModel()
let communication = Connect()
let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()

struct ContentView: View {
    @State var refresh = 0
    @State var newImage = UIImage(named:"chip")

    @ObservedObject var globalVariable = BlobModel.shared
    var body: some View {
        HStack {
            Image(uiImage: newImage!)
                .onReceive(downloadedImage) { image in
                    newImage = image
                }
                .onAppear {
                    let port2U = NWEndpoint.Port.init(integerLiteral: 49156)
                    communication.listenTCP(port: port2U)

                }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(globalVariable: globalVariable)
    }
}

Then, I present the Arduino code, which is more difficult to understand because it's in C++. When will you catch up with Apple — Microsoft and launch an updated language?

Now, this is also a prototype, but it functions adequately. It reads image files created on the SD card and sends them through a socket connection to the previous app. Sorry, there are a lot of debugging messages here.

#include <FS.h>
#include <SD.h>

#include <SPI.h>
#include <WiFi.h>
#include <WiFiClient.h>

// Set these to your desired credentials.
const char *ssid = "ssid";
const char *password = "passwd";

int imageCount = 0;
int status = WL_IDLE_STATUS;
IPAddress server(192,168,1,124);  // Private

// Initialize the client library
WiFiClient client;

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\n", path);

    File file = fs.open(path);
    if(!file){
        Serial.println("Failed to open file for reading");
        return;
    }

    Serial.print("Read from file: ");
    while(file.available()){
        client.write(file.read());
    }
    file.close();
    Serial.print("Sent file: ");
}

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("Failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println("Not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.path(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("  SIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void setup() {
  Serial.begin(115200);
  Serial.println();

  Serial.println("Attempting to connect to WPA network...");
  Serial.print("SSID: ");
  Serial.println(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  WiFi.setSleep(false);

  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    // don't do anything else:
  }
    Serial.println("Connected to wifi");

    while(!Serial);
    if(!SD.begin(21)){
        Serial.println("Card Mount Failed");
        return;
    }
    uint8_t cardType = SD.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD card attached");
        return;
    }

    Serial.print("SD Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    listDir(SD,"/",0);

    Serial.println("\nStarting connection...");
    // if you get a connection, report back via serial:
    client.stop();
    client.setTimeout(60);
}

void showImage() {
    client.connect(server, 49156);

    char *path = "          ";
    sprintf(path, "/image%d.jpg", imageCount);
    readFile(SD, path);
    Serial.println("Wait ...");
    delay(256);
    client.stop();
    Serial.println("Client Disconnected.");
}

void loop() {
  imageCount++;
  showImage();
  delay(2560);
}

Why did it take me a few days? Well, the secret lay in the port selection. I already knew that ports below 1024 were reserved and thought using the ones above them would be okay. It is on iOS but not on Arduino. Arduino considers ports below 49152 reserved, so I needed to go higher.

Mirror, Mirror

I take a photo with the Arduino, save it on the SD card, and send it through a custom socket to an iOS device that saves and displays the image.

The finished project would work in the same way; only the images wouldn’t be displayed; they would simply be copied, so we’re mirroring the SD card on the iOS device through a socket connection. I would rewrite the app so that you could choose which images you subsequently want to view.

Yes, tomorrow, when I finish the code, I will test the power drain on this plan.

The next stage will be to run a socket app on a second ESP32C6 mirroring the SD card of the ESP32S3 Sense, which will give the user a view of the images being captured on the small round display.

The last of these articles will do just that. If I describe the process in a reasonable number of words, I will also consider copying the data to the cloud so you can view the images you have mirrored anywhere.


메타데이터
post_id
dc3bf8f03379
slug
an-open-source-security-camera-dc3bf8f03379
url
https://levelup.gitconnected.com/an-open-source-security-camera-dc3bf8f03379
canonical_url
https://levelup.gitconnected.com/an-open-source-security-camera-dc3bf8f03379
author_url
https://medium.com/@marklucking
status
ok
fetched_at
2026-07-15 17:35:12