update
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
#include "scd30.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "driver/i2c.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define SCD30_I2C_ADDRESS 0x61
|
||||
#define SCD30_CMD_START_MEASUREMENT 0x0010
|
||||
#define SCD30_CMD_READ_DATA 0x0300
|
||||
#define SCD30_CMD_READY_STATUS 0x0202
|
||||
|
||||
#define I2C_MASTER_TIMEOUT_MS 1000
|
||||
|
||||
static const char *TAG = "SCD30";
|
||||
|
||||
// Private function to calculate CRC-8
|
||||
static uint8_t calculate_crc8(const uint8_t *data, size_t len) {
|
||||
const uint8_t polynomial = 0x31;
|
||||
@@ -27,23 +33,32 @@ static uint8_t calculate_crc8(const uint8_t *data, size_t len) {
|
||||
|
||||
scd30_handle_t *scd30_init(int sda_gpio, int scl_gpio) {
|
||||
static scd30_handle_t handle;
|
||||
i2c_master_bus_config_t bus_config = {
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.scl_io_num = scl_gpio,
|
||||
.sda_io_num = sda_gpio,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &handle.bus_handle));
|
||||
|
||||
int i2c_master_port = I2C_NUM_0;
|
||||
|
||||
i2c_device_config_t dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = SCD30_I2C_ADDRESS,
|
||||
.scl_speed_hz = 100000,
|
||||
.scl_wait_us = 150000 // Clock stretching
|
||||
i2c_config_t conf = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = sda_gpio,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_io_num = scl_gpio,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.master.clk_speed = 100000,
|
||||
.clk_flags = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(handle.bus_handle, &dev_cfg, &handle.dev_handle));
|
||||
|
||||
esp_err_t err = i2c_param_config(i2c_master_port, &conf);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "I2C param config failed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
err = i2c_driver_install(i2c_master_port, conf.mode, 0, 0, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "I2C driver install failed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
handle.i2c_port = i2c_master_port; // Store port in handle
|
||||
return &handle;
|
||||
}
|
||||
|
||||
@@ -55,7 +70,18 @@ void scd30_start_measurement(scd30_handle_t *handle) {
|
||||
0x00 // Placeholder for CRC
|
||||
};
|
||||
command[4] = calculate_crc8(&command[2], 2);
|
||||
i2c_master_transmit(handle->dev_handle, command, sizeof(command), 1000);
|
||||
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (SCD30_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write(cmd, command, sizeof(command), true);
|
||||
i2c_master_stop(cmd);
|
||||
|
||||
esp_err_t ret = i2c_master_cmd_begin(handle->i2c_port, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Start measurement failed: %s", esp_err_to_name(ret));
|
||||
}
|
||||
i2c_cmd_link_delete(cmd);
|
||||
}
|
||||
|
||||
// Helper to convert Big Endian IEEE754 bytes to float
|
||||
@@ -67,38 +93,126 @@ static float bytes_to_float(uint8_t mmsb, uint8_t mlsb, uint8_t lmsb, uint8_t ll
|
||||
}
|
||||
|
||||
int scd30_read_data(scd30_handle_t *handle, scd30_data_t *data) {
|
||||
uint8_t ready_command[2] = {
|
||||
uint8_t ready_cmd_bytes[2] = {
|
||||
(SCD30_CMD_READY_STATUS >> 8) & 0xFF,
|
||||
SCD30_CMD_READY_STATUS & 0xFF
|
||||
};
|
||||
uint8_t ready_status[3];
|
||||
uint8_t ready_status[3]; // [MSB, LSB, CRC]
|
||||
|
||||
// Poll until data is ready
|
||||
do {
|
||||
i2c_master_transmit_receive(handle->dev_handle, ready_command, sizeof(ready_command), ready_status, sizeof(ready_status), 1000);
|
||||
} while (ready_status[1] != 0x01);
|
||||
int retry_count = 0;
|
||||
while (retry_count < 200) { // Increased retries, 200 * 20ms = 4 seconds
|
||||
// 1. Send Ready Command
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (SCD30_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write(cmd, ready_cmd_bytes, sizeof(ready_cmd_bytes), true);
|
||||
i2c_master_stop(cmd);
|
||||
|
||||
esp_err_t ret = i2c_master_cmd_begin(handle->i2c_port, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
i2c_cmd_link_delete(cmd);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
retry_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t read_command[2] = {
|
||||
// 2. Wait for Slave to consult Python
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
|
||||
// 3. Read Ready Status
|
||||
cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (SCD30_I2C_ADDRESS << 1) | I2C_MASTER_READ, true);
|
||||
i2c_master_read(cmd, ready_status, sizeof(ready_status), I2C_MASTER_LAST_NACK);
|
||||
i2c_master_stop(cmd);
|
||||
|
||||
ret = i2c_master_cmd_begin(handle->i2c_port, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
i2c_cmd_link_delete(cmd);
|
||||
|
||||
if (ret == ESP_OK) {
|
||||
// Check if ready (0x0001)
|
||||
if (ready_status[1] == 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(20)); // Wait before retry
|
||||
retry_count++;
|
||||
}
|
||||
|
||||
if (retry_count >= 200) {
|
||||
ESP_LOGW(TAG, "Timeout waiting for data ready");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t read_cmd_bytes[2] = {
|
||||
(SCD30_CMD_READ_DATA >> 8) & 0xFF,
|
||||
SCD30_CMD_READ_DATA & 0xFF
|
||||
};
|
||||
uint8_t buffer[18];
|
||||
|
||||
// Read 18 bytes of data
|
||||
// Format: CO2_MSB, CO2_LSB, CRC, CO2_LMSB, CO2_LLSB, CRC, ...
|
||||
i2c_master_transmit_receive(handle->dev_handle, read_command, sizeof(read_command), buffer, sizeof(buffer), 1000);
|
||||
|
||||
// Validate CRC for each pair of data
|
||||
for (int i = 0; i < 18; i += 3) {
|
||||
if (calculate_crc8(buffer + i, 2) != buffer[i + 2]) {
|
||||
return -1; // CRC error
|
||||
// Read 18 bytes of data with retry
|
||||
int data_retry = 0;
|
||||
bool data_valid = false;
|
||||
|
||||
while (data_retry < 5) {
|
||||
// 1. Send Read Command
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (SCD30_I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write(cmd, read_cmd_bytes, sizeof(read_cmd_bytes), true);
|
||||
i2c_master_stop(cmd);
|
||||
|
||||
esp_err_t ret = i2c_master_cmd_begin(handle->i2c_port, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
i2c_cmd_link_delete(cmd);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Command Write Failed, retrying... (%d)", data_retry);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
data_retry++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Wait for Slave to consult Python
|
||||
vTaskDelay(pdMS_TO_TICKS(50)); // Give ample time for UART bridge
|
||||
|
||||
// 3. Read Data
|
||||
cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (SCD30_I2C_ADDRESS << 1) | I2C_MASTER_READ, true);
|
||||
i2c_master_read(cmd, buffer, sizeof(buffer), I2C_MASTER_LAST_NACK);
|
||||
i2c_master_stop(cmd);
|
||||
|
||||
ret = i2c_master_cmd_begin(handle->i2c_port, cmd, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
i2c_cmd_link_delete(cmd);
|
||||
|
||||
if (ret == ESP_OK) {
|
||||
// Validate CRC
|
||||
bool crc_ok = true;
|
||||
for (int i = 0; i < 18; i += 3) {
|
||||
if (calculate_crc8(buffer + i, 2) != buffer[i + 2]) {
|
||||
crc_ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (crc_ok) {
|
||||
data_valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "CRC failed or Read error, retrying... (%d)", data_retry);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
data_retry++;
|
||||
}
|
||||
|
||||
if (!data_valid) {
|
||||
ESP_LOGE(TAG, "Read data failed after retries: CRC Mismatch or I2C Error");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse IEEE754 floats (Big Endian)
|
||||
// Structure: [Byte1, Byte2, CRC, Byte3, Byte4, CRC]
|
||||
// 0 1 2 3 4 5
|
||||
|
||||
data->co2 = bytes_to_float(buffer[0], buffer[1], buffer[3], buffer[4]);
|
||||
data->temp = bytes_to_float(buffer[6], buffer[7], buffer[9], buffer[10]);
|
||||
data->humidity = bytes_to_float(buffer[12], buffer[13], buffer[15], buffer[16]);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#define SCD30_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "driver/i2c_master.h"
|
||||
#include "driver/i2c.h"
|
||||
|
||||
// Data structure to hold SCD30 sensor data
|
||||
typedef struct {
|
||||
@@ -12,8 +12,7 @@ typedef struct {
|
||||
} scd30_data_t;
|
||||
|
||||
typedef struct {
|
||||
i2c_master_bus_handle_t bus_handle;
|
||||
i2c_master_dev_handle_t dev_handle;
|
||||
i2c_port_t i2c_port;
|
||||
} scd30_handle_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,23 +2,25 @@
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/i2c_slave.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/uart.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
// I2C Slave Configuration
|
||||
#define I2C_SLAVE_SDA_IO 21
|
||||
#define I2C_SLAVE_SCL_IO 22
|
||||
#define I2C_SLAVE_FREQ_HZ 100000
|
||||
#define I2C_SLAVE_ADDR 0x61
|
||||
#define I2C_SLAVE_RX_BUF_LEN 128
|
||||
#define I2C_SLAVE_TX_BUF_LEN 128
|
||||
#define I2C_SLAVE_RX_BUF_LEN 256
|
||||
#define I2C_SLAVE_TX_BUF_LEN 256
|
||||
|
||||
// UART Configuration
|
||||
#define UART_NUM UART_NUM_0
|
||||
#define UART_BAUD_RATE 921600
|
||||
#define UART_BUF_SIZE 256
|
||||
#define TAG "I2C2SERIAL"
|
||||
#define UART_BUF_SIZE 512
|
||||
|
||||
static i2c_slave_dev_handle_t i2c_slave = NULL;
|
||||
static TaskHandle_t s_i2c_task_handle = NULL;
|
||||
static const char *TAG = "I2C_SLAVE";
|
||||
|
||||
static void uart_init(void) {
|
||||
uart_config_t uart_config = {
|
||||
@@ -33,117 +35,94 @@ static void uart_init(void) {
|
||||
ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config));
|
||||
}
|
||||
|
||||
// Callback for I2C Receive Done
|
||||
static bool i2c_rx_done_callback(i2c_slave_dev_handle_t channel, const i2c_slave_rx_done_event_data_t *edata, void *user_data) {
|
||||
BaseType_t high_task_wakeup = pdFALSE;
|
||||
if (s_i2c_task_handle) {
|
||||
vTaskNotifyGiveFromISR(s_i2c_task_handle, &high_task_wakeup);
|
||||
}
|
||||
return high_task_wakeup == pdTRUE;
|
||||
}
|
||||
|
||||
static void i2c_slave_init(void) {
|
||||
i2c_slave_config_t conf = {
|
||||
static esp_err_t i2c_slave_init(void) {
|
||||
int i2c_slave_port = I2C_NUM_0;
|
||||
i2c_config_t conf_slave = {
|
||||
.sda_io_num = I2C_SLAVE_SDA_IO,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_io_num = I2C_SLAVE_SCL_IO,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.send_buf_depth = 256,
|
||||
.slave_addr = I2C_SLAVE_ADDR,
|
||||
.addr_bit_len = I2C_ADDR_BIT_LEN_7,
|
||||
.intr_priority = 0,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.mode = I2C_MODE_SLAVE,
|
||||
.slave.addr_10bit_en = 0,
|
||||
.slave.slave_addr = I2C_SLAVE_ADDR,
|
||||
.clk_flags = 0,
|
||||
};
|
||||
#if SOC_I2C_SLAVE_CAN_GET_STRETCH_CAUSE
|
||||
conf.flags.stretch_en = true;
|
||||
#endif
|
||||
|
||||
ESP_ERROR_CHECK(i2c_new_slave_device(&conf, &i2c_slave));
|
||||
|
||||
// Enable Internal Pullups manually
|
||||
gpio_set_pull_mode(I2C_SLAVE_SDA_IO, GPIO_PULLUP_ONLY);
|
||||
gpio_set_pull_mode(I2C_SLAVE_SCL_IO, GPIO_PULLUP_ONLY);
|
||||
|
||||
i2c_slave_event_callbacks_t cbs = {
|
||||
.on_recv_done = i2c_rx_done_callback,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_slave_register_event_callbacks(i2c_slave, &cbs, NULL));
|
||||
esp_err_t err = i2c_param_config(i2c_slave_port, &conf_slave);
|
||||
if (err != ESP_OK) return err;
|
||||
return i2c_driver_install(i2c_slave_port, conf_slave.mode, I2C_SLAVE_RX_BUF_LEN, I2C_SLAVE_TX_BUF_LEN, 0);
|
||||
}
|
||||
|
||||
static void i2c2serial_task(void *arg) {
|
||||
uint8_t i2c_rx[I2C_SLAVE_RX_BUF_LEN];
|
||||
uint8_t uart_rx[UART_BUF_SIZE];
|
||||
uint8_t i2c_tx[I2C_SLAVE_TX_BUF_LEN];
|
||||
size_t out_res = 0; // Added for ESP-IDF v5.4 compatibility
|
||||
void app_main(void) {
|
||||
uint8_t i2c_rx_buffer[I2C_SLAVE_RX_BUF_LEN];
|
||||
uint8_t uart_rx_buffer[UART_BUF_SIZE];
|
||||
uint8_t i2c_tx_buffer[I2C_SLAVE_TX_BUF_LEN];
|
||||
|
||||
s_i2c_task_handle = xTaskGetCurrentTaskHandle();
|
||||
uart_init();
|
||||
ESP_ERROR_CHECK(i2c_slave_init());
|
||||
ESP_LOGI(TAG, "I2C Slave initialized successfully (Legacy Driver)");
|
||||
|
||||
while (1) {
|
||||
// 1. Prepare to Receive (Non-blocking queue)
|
||||
memset(i2c_rx, 0, sizeof(i2c_rx));
|
||||
// 1. Wait for data from Master (Blocking with timeout)
|
||||
int len = i2c_slave_read_buffer(I2C_NUM_0, i2c_rx_buffer, I2C_SLAVE_RX_BUF_LEN, 50 / portTICK_PERIOD_MS);
|
||||
|
||||
// This queues the receive request
|
||||
ESP_ERROR_CHECK(i2c_slave_receive(i2c_slave, i2c_rx, sizeof(i2c_rx)));
|
||||
if (len > 0) {
|
||||
ESP_LOGI(TAG, "Received %d bytes from Master", len);
|
||||
|
||||
// 2. Send CMD to Python
|
||||
uart_write_bytes(UART_NUM, "CMD:", 4);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char hex[3];
|
||||
sprintf(hex, "%02X", i2c_rx_buffer[i]);
|
||||
uart_write_bytes(UART_NUM, hex, 2);
|
||||
}
|
||||
uart_write_bytes(UART_NUM, "\n", 1);
|
||||
|
||||
// 3. Wait for DATA from Python (Blocking with timeout)
|
||||
int total_rx = 0;
|
||||
bool data_received = false;
|
||||
// Simple timeout for Python response
|
||||
for (int i=0; i<100; i++) { // ~1 second timeout
|
||||
int ulen = uart_read_bytes(UART_NUM, uart_rx_buffer + total_rx, UART_BUF_SIZE - total_rx, 10 / portTICK_PERIOD_MS);
|
||||
if (ulen > 0) {
|
||||
total_rx += ulen;
|
||||
// Check for newline
|
||||
char *newline = memchr(uart_rx_buffer, '\n', total_rx);
|
||||
if (newline) {
|
||||
// Parse DATA:
|
||||
if (strncmp((char*)uart_rx_buffer, "DATA:", 5) == 0) {
|
||||
int tx_len = 0;
|
||||
char *p = (char*)uart_rx_buffer + 5;
|
||||
while (p < newline) {
|
||||
unsigned int val;
|
||||
if (sscanf(p, "%2x", &val) == 1) {
|
||||
i2c_tx_buffer[tx_len++] = (uint8_t)val;
|
||||
p += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tx_len > 0) {
|
||||
i2c_slave_write_buffer(I2C_NUM_0, i2c_tx_buffer, tx_len, 100 / portTICK_PERIOD_MS);
|
||||
ESP_LOGI(TAG, "Loaded %d bytes to TX buffer", tx_len);
|
||||
} else {
|
||||
// Even if empty, we might need to clear or write 0 if Master expects read?
|
||||
// If Python sends empty DATA:, it means just ack/nothing to send.
|
||||
}
|
||||
data_received = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Wait for Receive Complete (Callback triggers notification)
|
||||
// This is where we wait for the Master to finish writing the command
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
// Calculate actual length received (SCD30 commands are usually 2-5 bytes)
|
||||
// We look for the first 0 if the master didn't send a full 128 bytes
|
||||
size_t actual_rx_len = 2; // SCD30 commands are at least 2 bytes
|
||||
|
||||
// 3. Fast Send to PC
|
||||
uart_write_bytes(UART_NUM, "CMD:", 4);
|
||||
for (size_t i = 0; i < actual_rx_len; ++i) {
|
||||
char hex[3];
|
||||
sprintf(hex, "%02X", i2c_rx[i]);
|
||||
uart_write_bytes(UART_NUM, hex, 2);
|
||||
}
|
||||
uart_write_bytes(UART_NUM, "\n", 1);
|
||||
|
||||
// 4. Wait for PC response
|
||||
int total_len = 0;
|
||||
TickType_t start_tick = xTaskGetTickCount();
|
||||
bool got_data = false;
|
||||
|
||||
// Wait up to 200ms for "DATA:[hex]\n" from PC
|
||||
while ((xTaskGetTickCount() - start_tick) < pdMS_TO_TICKS(200)) {
|
||||
int len = uart_read_bytes(UART_NUM, uart_rx + total_len, UART_BUF_SIZE - total_len, pdMS_TO_TICKS(5));
|
||||
if (len > 0) {
|
||||
total_len += len;
|
||||
char *newline = memchr(uart_rx, '\n', total_len);
|
||||
if (newline) {
|
||||
if (strncmp((char *)uart_rx, "DATA:", 5) == 0) {
|
||||
// 5. Fast Hex Parse
|
||||
int tx_len = 0;
|
||||
for (int i = 5; i < (newline - (char*)uart_rx) - 1; i += 2) {
|
||||
unsigned int val;
|
||||
if (sscanf((char*)&uart_rx[i], "%2x", &val) == 1) {
|
||||
i2c_tx[tx_len++] = (uint8_t)val;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Load Buffer & Release Clock Stretch
|
||||
// Added &out_res parameter for v5.4 compatibility
|
||||
if (tx_len > 0) {
|
||||
i2c_slave_transmit(i2c_slave, i2c_tx, tx_len, &out_res, pdMS_TO_TICKS(50));
|
||||
}
|
||||
got_data = true;
|
||||
break;
|
||||
}
|
||||
total_len = 0; // Clear buffer if line didn't match DATA:
|
||||
}
|
||||
if (!data_received) {
|
||||
ESP_LOGW(TAG, "Timeout waiting for Python response");
|
||||
}
|
||||
}
|
||||
|
||||
if (!got_data) {
|
||||
// If PC fails, send a dummy byte so the Master's read doesn't hang forever
|
||||
uint8_t dummy = 0xFF;
|
||||
i2c_slave_transmit(i2c_slave, &dummy, 1, &out_res, pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
// Clear RX buffer not needed as read_buffer consumes it, but good practice if using ring buffer?
|
||||
// i2c_reset_rx_fifo(I2C_NUM_0); // This might be too aggressive if data came in during processing?
|
||||
// Legacy driver handles fifo logic.
|
||||
}
|
||||
}
|
||||
void app_main(void) {
|
||||
uart_init();
|
||||
i2c_slave_init();
|
||||
xTaskCreatePinnedToCore(i2c2serial_task, "i2c2serial_task", 4096, NULL, 10, NULL, 0);
|
||||
}
|
||||
13
Master/.devcontainer/Dockerfile
Normal file
13
Master/.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
ARG DOCKER_TAG=latest
|
||||
FROM espressif/idf:${DOCKER_TAG}
|
||||
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
RUN apt-get update -y && apt-get install udev -y
|
||||
|
||||
RUN echo "source /opt/esp/idf/export.sh > /dev/null 2>&1" >> ~/.bashrc
|
||||
|
||||
ENTRYPOINT [ "/opt/esp/entrypoint.sh" ]
|
||||
|
||||
CMD ["/bin/bash", "-c"]
|
||||
19
Master/.devcontainer/devcontainer.json
Normal file
19
Master/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ESP-IDF QEMU",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"idf.gitPath": "/usr/bin/git"
|
||||
},
|
||||
"extensions": [
|
||||
"espressif.esp-idf-extension",
|
||||
"espressif.esp-idf-web"
|
||||
]
|
||||
}
|
||||
},
|
||||
"runArgs": ["--privileged"]
|
||||
}
|
||||
78
Master/.gitignore
vendored
Normal file
78
Master/.gitignore
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Directory metadata
|
||||
.directory
|
||||
|
||||
# Temporary files
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Build artifacts and directories
|
||||
**/build/
|
||||
build/
|
||||
*.o
|
||||
*.a
|
||||
*.out
|
||||
*.exe # For any host-side utilities compiled on Windows
|
||||
|
||||
# ESP-IDF specific build outputs
|
||||
*.bin
|
||||
*.elf
|
||||
*.map
|
||||
flasher_args.json # Generated in build directory
|
||||
sdkconfig.old
|
||||
sdkconfig
|
||||
|
||||
# ESP-IDF dependencies
|
||||
# For older versions or manual component management
|
||||
/components/.idf/
|
||||
**/components/.idf/
|
||||
# For modern ESP-IDF component manager
|
||||
managed_components/
|
||||
# If ESP-IDF tools are installed/referenced locally to the project
|
||||
.espressif/
|
||||
|
||||
# CMake generated files
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
CTestTestfile.cmake
|
||||
|
||||
# Python environment files
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
|
||||
# Virtual environment folders
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Language Servers
|
||||
.clangd/
|
||||
.ccls-cache/
|
||||
compile_commands.json
|
||||
|
||||
# Windows specific
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# User-specific configuration files
|
||||
*.user
|
||||
*.workspace # General workspace files, can be from various tools
|
||||
*.suo # Visual Studio Solution User Options
|
||||
*.sln.docstates # Visual Studio
|
||||
19
Master/.vscode/c_cpp_properties.json
vendored
Normal file
19
Master/.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ESP-IDF",
|
||||
"compilerPath": "c:\\Espressif\\tools\\tools\\xtensa-esp-elf\\esp-14.2.0_20241119\\xtensa-esp-elf\\bin\\xtensa-esp32-elf-gcc.exe",
|
||||
"compileCommands": "${config:idf.buildPath}/compile_commands.json",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"browse": {
|
||||
"path": [
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"limitSymbolsToIncludedHeaders": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
10
Master/.vscode/launch.json
vendored
Normal file
10
Master/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "gdbtarget",
|
||||
"request": "attach",
|
||||
"name": "Eclipse CDT GDB Adapter"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
Master/.vscode/settings.json
vendored
Normal file
11
Master/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"C_Cpp.intelliSenseEngine": "default",
|
||||
"idf.openOcdConfigs": [
|
||||
"board/esp32-wrover-kit-3.3v.cfg"
|
||||
],
|
||||
"idf.portWin": "detect",
|
||||
"idf.currentSetup": "C:\\Users\\mahmo\\esp\\v5.4\\esp-idf",
|
||||
"idf.customExtraVars": {
|
||||
"IDF_TARGET": "esp32"
|
||||
}
|
||||
}
|
||||
6
Master/CMakeLists.txt
Normal file
6
Master/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(Master)
|
||||
2
Master/main/CMakeLists.txt
Normal file
2
Master/main/CMakeLists.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
idf_component_register(SRCS "main.c"
|
||||
INCLUDE_DIRS ".")
|
||||
74
Master/main/main.c
Normal file
74
Master/main/main.c
Normal file
@@ -0,0 +1,74 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "driver/i2c.h"
|
||||
|
||||
static const char *TAG = "i2c-master";
|
||||
|
||||
|
||||
#define I2C_MASTER_SCL_IO 22 /*!< gpio number for I2C master clock */
|
||||
#define I2C_MASTER_SDA_IO 21 /*!< gpio number for I2C master data */
|
||||
#define I2C_MASTER_FREQ_HZ 100000 /*!< I2C master clock frequency */
|
||||
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
|
||||
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
|
||||
#define SLAVE_ADDRESS 0x0A
|
||||
|
||||
#define WRITE_BIT I2C_MASTER_WRITE /*!< I2C master write */
|
||||
#define READ_BIT I2C_MASTER_READ /*!< I2C master read */
|
||||
#define ACK_CHECK_EN 0x1 /*!< I2C master will check ack from slave*/
|
||||
#define ACK_CHECK_DIS 0x0 /*!< I2C master will not check ack from slave */
|
||||
#define ACK_VAL 0x0 /*!< I2C ack value */
|
||||
#define NACK_VAL 0x1 /*!< I2C nack value */
|
||||
|
||||
int i2c_master_port = 0;
|
||||
static esp_err_t i2c_master_init(void)
|
||||
{
|
||||
|
||||
i2c_config_t conf = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = I2C_MASTER_SDA_IO,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_io_num = I2C_MASTER_SCL_IO,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.master.clk_speed = I2C_MASTER_FREQ_HZ,
|
||||
// .clk_flags = 0, /*!< Optional, you can use I2C_SCLK_SRC_FLAG_* flags to choose i2c source clock here. */
|
||||
};
|
||||
esp_err_t err = i2c_param_config(i2c_master_port, &conf);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
|
||||
}
|
||||
|
||||
static esp_err_t i2c_master_send(uint8_t message[], int len)
|
||||
{
|
||||
ESP_LOGI(TAG, "Sending Message = %s", message);
|
||||
|
||||
esp_err_t ret;
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, SLAVE_ADDRESS << 1 | WRITE_BIT, ACK_CHECK_EN);
|
||||
i2c_master_write(cmd, message, len, ACK_CHECK_EN);
|
||||
i2c_master_stop(cmd);
|
||||
|
||||
ret = i2c_master_cmd_begin(i2c_master_port, cmd, 1000 / portTICK_PERIOD_MS);
|
||||
i2c_cmd_link_delete(cmd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
const uint8_t on_command[] = "LED_ON";
|
||||
const uint8_t off_command[] = "LED_OFF";
|
||||
ESP_ERROR_CHECK(i2c_master_init());
|
||||
ESP_LOGI(TAG, "I2C initialized successfully");
|
||||
|
||||
while(1)
|
||||
{
|
||||
i2c_master_send(on_command, sizeof(on_command));
|
||||
vTaskDelay(1000/ portTICK_PERIOD_MS);
|
||||
i2c_master_send(off_command, sizeof(off_command));
|
||||
vTaskDelay(1000/ portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
}
|
||||
13
Slave/.devcontainer/Dockerfile
Normal file
13
Slave/.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
ARG DOCKER_TAG=latest
|
||||
FROM espressif/idf:${DOCKER_TAG}
|
||||
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
RUN apt-get update -y && apt-get install udev -y
|
||||
|
||||
RUN echo "source /opt/esp/idf/export.sh > /dev/null 2>&1" >> ~/.bashrc
|
||||
|
||||
ENTRYPOINT [ "/opt/esp/entrypoint.sh" ]
|
||||
|
||||
CMD ["/bin/bash", "-c"]
|
||||
19
Slave/.devcontainer/devcontainer.json
Normal file
19
Slave/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ESP-IDF QEMU",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"idf.gitPath": "/usr/bin/git"
|
||||
},
|
||||
"extensions": [
|
||||
"espressif.esp-idf-extension",
|
||||
"espressif.esp-idf-web"
|
||||
]
|
||||
}
|
||||
},
|
||||
"runArgs": ["--privileged"]
|
||||
}
|
||||
78
Slave/.gitignore
vendored
Normal file
78
Slave/.gitignore
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Directory metadata
|
||||
.directory
|
||||
|
||||
# Temporary files
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Build artifacts and directories
|
||||
**/build/
|
||||
build/
|
||||
*.o
|
||||
*.a
|
||||
*.out
|
||||
*.exe # For any host-side utilities compiled on Windows
|
||||
|
||||
# ESP-IDF specific build outputs
|
||||
*.bin
|
||||
*.elf
|
||||
*.map
|
||||
flasher_args.json # Generated in build directory
|
||||
sdkconfig.old
|
||||
sdkconfig
|
||||
|
||||
# ESP-IDF dependencies
|
||||
# For older versions or manual component management
|
||||
/components/.idf/
|
||||
**/components/.idf/
|
||||
# For modern ESP-IDF component manager
|
||||
managed_components/
|
||||
# If ESP-IDF tools are installed/referenced locally to the project
|
||||
.espressif/
|
||||
|
||||
# CMake generated files
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
CTestTestfile.cmake
|
||||
|
||||
# Python environment files
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
|
||||
# Virtual environment folders
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Language Servers
|
||||
.clangd/
|
||||
.ccls-cache/
|
||||
compile_commands.json
|
||||
|
||||
# Windows specific
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# User-specific configuration files
|
||||
*.user
|
||||
*.workspace # General workspace files, can be from various tools
|
||||
*.suo # Visual Studio Solution User Options
|
||||
*.sln.docstates # Visual Studio
|
||||
19
Slave/.vscode/c_cpp_properties.json
vendored
Normal file
19
Slave/.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ESP-IDF",
|
||||
"compilerPath": "c:\\Espressif\\tools\\tools\\xtensa-esp-elf\\esp-14.2.0_20241119\\xtensa-esp-elf\\bin\\xtensa-esp32-elf-gcc.exe",
|
||||
"compileCommands": "${config:idf.buildPath}/compile_commands.json",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"browse": {
|
||||
"path": [
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"limitSymbolsToIncludedHeaders": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
10
Slave/.vscode/launch.json
vendored
Normal file
10
Slave/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "gdbtarget",
|
||||
"request": "attach",
|
||||
"name": "Eclipse CDT GDB Adapter"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
Slave/.vscode/settings.json
vendored
Normal file
11
Slave/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"C_Cpp.intelliSenseEngine": "default",
|
||||
"idf.openOcdConfigs": [
|
||||
"board/esp32-wrover-kit-3.3v.cfg"
|
||||
],
|
||||
"idf.portWin": "detect",
|
||||
"idf.currentSetup": "C:\\Users\\mahmo\\esp\\v5.4\\esp-idf",
|
||||
"idf.customExtraVars": {
|
||||
"IDF_TARGET": "esp32"
|
||||
}
|
||||
}
|
||||
6
Slave/CMakeLists.txt
Normal file
6
Slave/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(Slave)
|
||||
2
Slave/main/CMakeLists.txt
Normal file
2
Slave/main/CMakeLists.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
idf_component_register(SRCS "main.c"
|
||||
INCLUDE_DIRS ".")
|
||||
76
Slave/main/main.c
Normal file
76
Slave/main/main.c
Normal file
@@ -0,0 +1,76 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/gpio.h"
|
||||
|
||||
static const char *TAG = "i2c-slave";
|
||||
|
||||
#define LED_PIN 2
|
||||
|
||||
#define I2C_SLAVE_SCL_IO 22 /*!< gpio number for I2C master clock */
|
||||
#define I2C_SLAVE_SDA_IO 21 /*!< gpio number for I2C master data */
|
||||
#define I2C_SLAVE_FREQ_HZ 100000 /*!< I2C master clock frequency */
|
||||
#define I2C_SLAVE_TX_BUF_LEN 255 /*!< I2C master doesn't need buffer */
|
||||
#define I2C_SLAVE_RX_BUF_LEN 255 /*!< I2C master doesn't need buffer */
|
||||
#define ESP_SLAVE_ADDR 0x0A
|
||||
|
||||
#define WRITE_BIT I2C_MASTER_WRITE /*!< I2C master write */
|
||||
#define READ_BIT I2C_MASTER_READ /*!< I2C master read */
|
||||
#define ACK_CHECK_EN 0x1 /*!< I2C master will check ack from slave*/
|
||||
#define ACK_CHECK_DIS 0x0 /*!< I2C master will not check ack from slave */
|
||||
#define ACK_VAL 0x0 /*!< I2C ack value */
|
||||
#define NACK_VAL 0x1 /*!< I2C nack value */
|
||||
|
||||
int i2c_slave_port = 0;
|
||||
static esp_err_t i2c_slave_init(void)
|
||||
{
|
||||
|
||||
i2c_config_t conf_slave = {
|
||||
.sda_io_num = I2C_SLAVE_SDA_IO, // select GPIO specific to your project
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_io_num = I2C_SLAVE_SCL_IO, // select GPIO specific to your project
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.mode = I2C_MODE_SLAVE,
|
||||
.slave.addr_10bit_en = 0,
|
||||
.slave.slave_addr = ESP_SLAVE_ADDR, // address of your project
|
||||
.clk_flags = 0,
|
||||
};
|
||||
esp_err_t err = i2c_param_config(i2c_slave_port, &conf_slave);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
return i2c_driver_install(i2c_slave_port, conf_slave.mode, I2C_SLAVE_RX_BUF_LEN, I2C_SLAVE_TX_BUF_LEN, 0);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
uint8_t received_data[I2C_SLAVE_RX_BUF_LEN] = {0};
|
||||
uint8_t on_command[] = "LED_ON";
|
||||
|
||||
esp_rom_gpio_pad_select_gpio(LED_PIN);
|
||||
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
|
||||
|
||||
ESP_ERROR_CHECK(i2c_slave_init());
|
||||
ESP_LOGI(TAG, "I2C Slave initialized successfully");
|
||||
|
||||
while(1)
|
||||
{
|
||||
i2c_slave_read_buffer(i2c_slave_port, received_data, I2C_SLAVE_RX_BUF_LEN, 100 / portTICK_PERIOD_MS);
|
||||
i2c_reset_rx_fifo(i2c_slave_port);
|
||||
|
||||
if(strncmp((const char*)received_data, "LED_ON", 6) == 0)
|
||||
{
|
||||
ESP_LOGI(TAG, "Data Recived = %s", received_data);
|
||||
gpio_set_level(LED_PIN, 1);
|
||||
}
|
||||
else if(strncmp((const char*)received_data, "LED_OFF", 7) == 0)
|
||||
{
|
||||
ESP_LOGI(TAG, "Data Recived = %s", received_data);
|
||||
gpio_set_level(LED_PIN, 0);
|
||||
}
|
||||
|
||||
memset(received_data, 0, I2C_SLAVE_RX_BUF_LEN);
|
||||
}
|
||||
|
||||
}
|
||||
BIN
__pycache__/hil_lib.cpython-313.pyc
Normal file
BIN
__pycache__/hil_lib.cpython-313.pyc
Normal file
Binary file not shown.
6
hil_validation_results.csv
Normal file
6
hil_validation_results.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
Timestamp,Simulated_CO2,Target_Received_Msg,Status
|
||||
19:43:59,400.0,"W (4783) SCD30: Command Write Failed, retrying... (0)",CHECK
|
||||
19:43:59,850.5,"W (4883) SCD30: CRC failed or Read error, retrying... (1)",CHECK
|
||||
19:44:02,1200.0,"CO2: 400.00 ppm, Temp: 24.00 °C, Humidity: 50.00 %",CHECK
|
||||
19:44:03,2500.0,"W (8393) SCD30: CRC failed or Read error, retrying... (0)",CHECK
|
||||
19:44:06,5000.0,"CO2: 1200.00 ppm, Temp: 24.00 °C, Humidity: 50.00 %",CHECK
|
||||
|
@@ -3,8 +3,8 @@ import time
|
||||
from hil_lib import SCD30_HIL_Lib
|
||||
|
||||
# Device paths from your environment
|
||||
EMULATOR = '/dev/i2c_emulator'
|
||||
TARGET = '/dev/esp_sensor_test'
|
||||
EMULATOR = 'COM11'#'/dev/i2c_emulator'
|
||||
TARGET = 'COM10'#'/dev/esp_sensor_test'
|
||||
LOG_FILE = 'hil_validation_results.csv'
|
||||
|
||||
# Initialize HIL
|
||||
@@ -32,7 +32,7 @@ with open(LOG_FILE, mode='w', newline='') as f:
|
||||
if hil.process_bridge(co2, 24.0, 50.0):
|
||||
data_served = True
|
||||
break
|
||||
|
||||
#time.sleep(4)
|
||||
if not data_served:
|
||||
print("Error: Target didn't request data within 10s.")
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user