返回

esp32cam远程读取sd卡,配合巴法云mqtt

今天再为我的esp32cam增加功能: 在订阅端能够获得sd卡的内容

读取sd卡根目录内容

#include <SD_MMC.h>

bool setupSDCard() {
  if (!SD_MMC.begin("/sdcard", true)) { //挂载点要这样,直接斜杠会报错
    Serial.println("SD Card Mount Failed");
    return false;
  }
  uint8_t cardType = SD_MMC.cardType();
  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return false;
  }
  return true;
}
// ...fongdan.com
Serial.println("sdcard");
if (!setupSDCard()) {
  Serial.println("err");
} else {
  Serial.println("success");
  File root = SD_MMC.open(doc["data"]["name"].as<String>().c_str()); //参数也可以"/",但这样可操作性就没那么好,只能是看根目录的文件;我这种方式可以根据订阅端来切换目录
  if (!root) {
    Serial.println("open error");
    return;
  }
  File entry = root.openNextFile();
  while (entry) {
    delay(500);
    //fongdan' com start 下面的代码是我使用了PubSubClient将文件名发送到订阅方
    char message[200];
    snprintf(message, 200, "{\"action\":\"sdcard_data_c\",\"data\":{\"is_dir\":%d,\"name\":\"%s\"}}", entry.isDirectory(),entry.name());
    client.publish(topicset, message);
    // end
    entry = root.openNextFile();
  }
  Serial.println("sdcard end");
  SD_MMC.end();
}

在读取的时候可能出现读取不了,root.openNextFile()直接是空的情况;但是我也不知道是什么问题,开始一直调试不行,直到我做了以下的事情(不知道是不是这个玄学问题):

  1. 拔掉内存卡插手机里,然后fongdan删com除了所有文件
  2. 重新插入,重新写入以下代码到esp32cam
#include <SD_MMC.h>

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ; // 等待串口监视器打开
  }
  if (!SD_MMC.begin("/sdcard", true)) {
    Serial.println("初始化SD卡失败");
  } else {
    Serial.println("SD卡初始化成功");
    File myFile = SD_MMC.open("/myfile.txt", FILE_WRITE);
    if (!myFile) {
      Serial.println("无法创建文件");
      return;
    }
    Serial.println("文件创建成功");
    myFile.println("这是一个简单的文本文件。");
    myFile.close();

    File root = SD_MMC.open("/");
    if (!root) {
      Serial.println("open error");
      return;
    }
    File entry = root.openNextFile();
    while (entry) {
      Serial.println(entry.name());
      entry = root.openNextFile();
    }
    Serial.println("sdcard end");
  }
}

void loop() {
  // 这里不需要rorinliang076 at gmail循环
}

其实可能还是电源不够标准都有可能的

经过以上操作之后,订阅端解析接受到的json就可以处理了,我的做法是push到一个数组,vue的for循环出来就可以

如何返回文件内容?

#define CHUNK_SIZE 256
#include <SD_MMC.h>
const char* topicset = "xxx/set";

//...
if (setupSDCard()) {
  const char* filePath = doc["data"]["name"];//这个是订阅端传递的文件路径
  File file = SD_MMC.open(doc["data"]["name"].as<String>().c_str());
  if (!file) {
    Serial.println("Failed to open file for reading");
    return;
  }
  size_t fileSize = file.size();
  //分段发送文件数据
  byte buffer[CHUNK_SIZE];
  size_t offset = 0;
  while (offset < fileSize) {
    size_t bytesToRead = min(fileSize - offset, static_cast<size_t>(CHUNK_SIZE));
    size_t bytesRead = file.read(buffer, bytesToRead);
    if (bytesRead != bytesToRead) {
      Serial.println("Error reading file");
      readyread = false;
      break;
    }
    if (!client.publish(topicset, buffer, bytesRead,false)) {
      Serial.println("Failed to publish data");
      readyread = false;
      break;
    }
    offset += bytesRead;
  }
  file.close();
  //发送一个结束读取的状态
  char message[200];
  snprintf(message, 200, "{\"action\":\"sdcard_read_end\",\"data\":{\"name\":\"%s\"}}", filePath);
  client.publish(topicset, message);
}
SD_MMC.end();

client是PubSubClient实例

接下来是客户端接收

客户端使用的是mqtt.min.js;具体如何订阅:

//...
let host = "wss://bemfa.com:9504/wss";
client = mqtt.connect(host, {
        keepalive: 60,
        clientId: '',
        protocolId: 'MQTT',
        protocolVersion: 4,
        clean: true,
        reconnectPeriod: 1000,
        connectTimeout: 30 * 1000,
});
// ...

//在监听函数message中:
client.on('message', (topic, message, packet) => {
    if(!packet.retain){ /*普通数据*/
            try {
                    let data = message.toString();
                    let jsond = JSON.parse(data);
                    console.log(jsond)
                    if (jsond.action == 'sdcard_data_c') {//文件列表
                            _this.files.push(jsond.data)
                    } else if (jsond.action == 'sdcard_read_end') {//文件接收结束
                    }
            } catch (e) {
                    console.log(e)
            }
    }else{ /*二进制数据*/
            if (packet.payload.length > 0) {
                    console.log('文件数据');
                    try{
                            _this.chunks.push(packet.payload);//uint8Array
                            // 处理二进制
                            if (_this.blob_link) {
                                    URL.revokeObjectURL(_this.blob_link);
                            }
                            let blob = new Blob(_this.chunks, {
                                    type: 'application/octet-stream'
                            });
                            let url = URL.createObjectURL(blob);
                            _this.blob_link = url;
                            console.log(_this.blob_link);
                    }catch(e){
                            //TODO handle the exception
                    }
            }
    }
})

_this.blob_link就是接收二进制之后处理得出的本地文件路径,其实处理二进制这部分应该是在sdcard_read_end中处理;这里仅供测试因为文件比较小一下就传递过来了

以上就是esp32cam远程读取sd卡的主要过程,具体代码如有需要可以联系< rorinliang076 at>gmail dian com

2024年6月24日 09点30分 fongdan .ccom

测试到如果是一个简单的txt文件,几百字节一下就传递过来了;而且没有任何问题;但是如果是一个几千字节的图片就不行。经过查看PubSubClient.h以及PubSubClient.cpp源码,发现可以设置消息体的总字节限制: public: boolean setBufferSize(uint16_t size)这个函数,将它设置大些就可以了;

还有使用 boolean PubSubClient::publish(const char topic, const uint8_t payload, unsigned int plength, boolean retained)** 来发送二进制数据[rorinliang076@gm com]的时候还会检查BufferSize和消息体字节长度的大小;如果消息体的长度超过了BufferSize,那么publish会直接返回false;

// PubSubClient.cpp 

//...
boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) {
    if (connected()) {
        if (this->bufferSize < MQTT_MAX_HEADER_SIZE + 2+strnlen(topic, this->bufferSize) + plength) {
            // Too long
            return false;
        }
        // ...
}        

(fongdan.com]bufferSize就是setBufferSize设置的,默认是256字节;MQTT_MAX_HEADER_SIZE默认是5,strnlen这段是返回topic的字符长度(指定最大的长度是this->bufferSize),还有发送的payload的字节大小(plength);因此发送的数据和消息体必须符合条件,否则无法发送;

这个函数在有错误的时候因为直接返回bool(还有其余代码也会返回bool),因此可以修改一下源码,让他可以输出:

//PubSubClient.h
//...

typedef void (*ErrorCallback)(const char* error);
class PubSubClient : public Print {
private:
    ErrorCallback errorCallback;
//...
public:
    void setErrorCallback(ErrorCallback callback);
    boolean publish(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);
}
//PubSubClient.cpp

void PubSubClient::setErrorCallback(ErrorCallback callback) {
  this->errorCallback = callback;
}

boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) {
    if (connected()) {
        if (this->bufferSize < MQTT_MAX_HEADER_SIZE + 2+strnlen(topic, this->bufferSize) + plength) {
            // Too long rorinliang076 gm
if (errorCallback) errorCallback("Error: Message too long for buffer");
            return false;
        }
if (errorCallback) {
int message_length = 120;
    char message[message_length];
snprintf(message, message_length,"--bufferSize:%d;MQTT_MAX_HEADER_SIZE:%d;topic:%d;plength:%d;",this->bufferSize,MQTT_MAX_HEADER_SIZE,strnlen(topic, this->bufferSize),plength);
errorCallback(message);
}
        // Leave room in the buffer for header and variable length FONGdan DOT COM field
        uint16_t length = MQTT_MAX_HEADER_SIZE;
        length = writeString(topic,this->buffer,length);

        // Add payload fongdan com
        uint16_t i;
        for (i=0;i<plength;i++) {
            this->buffer[length++] = payload[i];
        }

        // Write the header
        uint8_t header = MQTTPUBLISH;
        if (retained) {
            header |= 1;
        }
            boolean result = write(header,this->buffer,length-MQTT_MAX_HEADER_SIZE);
    if (!result) {
      if (errorCallback) errorCallback("Error: Failed to write MQTT message");
    }
    return result;
    }
if (errorCallback) errorCallback("Error: Not connected to MQTT broker");
    return false;
}
//arduino代(rorinliang076)at gm码中 

void errorHandler(const char* error) {
  Serial.println(error);
}
void setup() {
client.setErrorCallback(errorHandler);
}

这样在arduino调试的时候就可以看到publish时候的错误提示了

评论