Power, Cameras & Communications
With the XIAO ESP32S3 Sense series at its core.
Power, Cameras & Communications
With the XIAO ESP32S3 Sense series at its core.

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 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.
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.
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.
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.
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.
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.
I also found an excellent reference for Bluetooth code on the Apple developer’s website.
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.
This article had the peer code for the iOS client.
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 avoid — running 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.
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.
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