Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614aef96c5 | ||
|
|
a638e0293c | ||
|
|
49c123809a | ||
|
|
f850e86b2f | ||
|
|
3143f3b37f | ||
|
|
2c741adac7 | ||
|
|
6892060d16 | ||
|
|
8bd69dce67 | ||
|
|
8ee32a8949 | ||
|
|
b9b9a0bdd0 | ||
|
|
4e07dbb461 | ||
|
|
9d5b9366d5 | ||
|
|
a164f3dda1 | ||
|
|
385988401c | ||
|
|
d87215bebd | ||
|
|
8e6fc96e4d | ||
|
|
9e3ae56aa1 | ||
|
|
cc163589fc | ||
|
|
f0685f9ce3 | ||
|
|
2c1ab09770 | ||
|
|
52b7a7258c | ||
|
|
c27e1aa057 |
No files matched your search
@@ -0,0 +1 @@
|
||||
/*Claude-PMME*
|
||||
@@ -0,0 +1,111 @@
|
||||
#+TITLE: PMME - Particulate Matter Monitoring Ecosystem
|
||||
#+OPTIONS: toc:nil
|
||||
#+AUTHOR:
|
||||
#+DATE:
|
||||
|
||||
* Overview
|
||||
|
||||
A complete IoT solution for monitoring air quality using PM2.5 sensors, built with Rust across embedded, cloud, and mobile platforms.
|
||||
|
||||
* Architecture Overview
|
||||
|
||||
The PMME system consists of four main components working together to provide real-time air quality monitoring:
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
[ESP32 Device] --WiFi--> [Cloud Server] <--HTTP--> [Mobile App]
|
||||
| |
|
||||
[Admin Panel] <--WiFi--> [PostgreSQL DB]
|
||||
#+END_EXAMPLE
|
||||
|
||||
* Components
|
||||
|
||||
** 🔧 ESP32 Device (~pmme-device/rust-version/~)
|
||||
|
||||
- *Platform*: ESP32 microcontroller running esp-idf with Rust
|
||||
- *Sensors*: PM2.5 particulate matter sensor (PMS7003)
|
||||
- *Display*: SSD1306 OLED for local readings
|
||||
- *Connectivity*: WiFi for data transmission to cloud
|
||||
- *Features*:
|
||||
- Real-time sensor data collection
|
||||
- Local display of current readings
|
||||
- Automatic cloud data transmission
|
||||
|
||||
** ☁️ Cloud Server (~pmme-cloud/~)
|
||||
|
||||
- *Framework*: Rocket web framework (Rust)
|
||||
- *Database*: PostgreSQL (planned integration)
|
||||
- *Purpose*:
|
||||
- Receive and store sensor data from ESP32 devices
|
||||
- Provide REST API for mobile app data access
|
||||
- Future: Historical data analysis and graphing
|
||||
|
||||
** 📱 Mobile App (~pmme-mobile/~)
|
||||
|
||||
- *Framework*: Tauri (Rust + Web frontend)
|
||||
- *Platform*: Cross-platform mobile application
|
||||
- *Features*:
|
||||
- Connect to cloud server via HTTP
|
||||
- Display current PM2.5 readings
|
||||
- Future: Historical data visualization and graphs
|
||||
- Future: Potential direct ESP32 admin panel access
|
||||
|
||||
** 🌐 Web UI & Admin Panel (~pmme-ui/~)
|
||||
|
||||
- *Framework*: ClojureScript with Shadow CLJS
|
||||
- *Purpose*:
|
||||
- Data visualization interface
|
||||
- Admin panel for ESP32 device management and provisioning
|
||||
|
||||
* Getting Started
|
||||
|
||||
** ESP32 Device Setup
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
cd pmme-device/rust-version/
|
||||
# Set up ESP-IDF environment
|
||||
./export-esp.sh
|
||||
cargo build
|
||||
cargo run
|
||||
#+END_SRC
|
||||
|
||||
** Cloud Server
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
cd pmme-cloud/
|
||||
cargo run
|
||||
#+END_SRC
|
||||
|
||||
** Mobile App
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
cd pmme-mobile/
|
||||
npm install
|
||||
npm run tauri dev
|
||||
#+END_SRC
|
||||
|
||||
** Web UI
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
cd pmme-ui/
|
||||
npm install
|
||||
npm run dev
|
||||
#+END_SRC
|
||||
|
||||
* Future Features
|
||||
|
||||
- *PostgreSQL Integration*: Historical data storage and analysis
|
||||
- *Data Visualization*: Graphs and trends in mobile app
|
||||
- *Multi-sensor Support*: Expansion beyond PM2.5 to other environmental sensors
|
||||
- *ESP32 HTTP Server*: Device-hosted admin interface
|
||||
|
||||
* Development Notes
|
||||
|
||||
- The project previously included a C version implementation which has been deprecated in favor of the Rust implementation
|
||||
- The system is designed to be scalable for multiple ESP32 devices reporting to a single cloud instance
|
||||
|
||||
* Tech Stack
|
||||
|
||||
- *Embedded*: Rust + esp-idf
|
||||
- *Backend*: Rust + Rocket + PostgreSQL
|
||||
- *Mobile*: Rust + Tauri
|
||||
- *Web/Admin*: ClojureScript + Shadow CLJS
|
||||
@@ -1,2 +1,2 @@
|
||||
idf_component_register(SRCS "pmme.c"
|
||||
idf_component_register(SRCS "pmme.c" "ssd1306/ssd1306.c"
|
||||
INCLUDE_DIRS ".")
|
||||
@@ -1,25 +1,108 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_lcd_panel_vendor.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "esp_lcd_panel_vendor.h"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "driver/uart.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "ssd1306/ssd1306.h"
|
||||
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_netif.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_event.h"
|
||||
|
||||
static const char *TAG = "PMME-Device";
|
||||
|
||||
#define I2C_HOST 0
|
||||
#define I2C_SDA_PIN 21
|
||||
#define I2C_SCL_PIN 22
|
||||
#define I2C_CLOCK_HZ 400000
|
||||
#define SSD1306_ADDR 0x3C
|
||||
#define SSD1306_WIDTH 128
|
||||
#define SSD1306_HEIGHT 64
|
||||
|
||||
#define UART_NUM UART_NUM_2
|
||||
#define BUF_SIZE 32
|
||||
#define UART_BUF_SIZE 128
|
||||
#define TXD_PIN 17
|
||||
#define RXD_PIN 16
|
||||
#define BUTTON_PIN GPIO_NUM_5
|
||||
#define LED_PIN GPIO_NUM_19
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
#define SWAP_BYTES(x) ((x >> 8) | (x << 8))
|
||||
|
||||
typedef struct {
|
||||
// PM concentrations (CF=1, standard particle) in μg/m³
|
||||
uint16_t pm1_0_cf1;
|
||||
uint16_t pm2_5_cf1;
|
||||
uint16_t pm10_cf1;
|
||||
|
||||
// PM concentrations (atmospheric environment) in μg/m³
|
||||
uint16_t pm1_0_atm;
|
||||
uint16_t pm2_5_atm;
|
||||
uint16_t pm10_atm;
|
||||
|
||||
// Particle counts (particles per 0.1L air)
|
||||
uint16_t particles_0_3um;
|
||||
uint16_t particles_0_5um;
|
||||
uint16_t particles_1_0um;
|
||||
uint16_t particles_2_5um;
|
||||
uint16_t particles_5_0um;
|
||||
uint16_t particles_10um;
|
||||
} pms7003_data_t;
|
||||
|
||||
static pms7003_data_t sensor_data;
|
||||
static SemaphoreHandle_t sensor_data_mutex = NULL;
|
||||
|
||||
void send_pms7003_command(uint8_t cmd, uint8_t datah, uint8_t datal) {
|
||||
uint8_t command[7] = {0x42, 0x4D, cmd, datah, datal, 0, 0};
|
||||
|
||||
uint16_t checksum = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
checksum += command[i];
|
||||
}
|
||||
command[5] = (checksum >> 8) & 0xFF;
|
||||
command[6] = checksum & 0xFF;
|
||||
uart_write_bytes(UART_NUM, command, 7);
|
||||
}
|
||||
|
||||
void read_pm_data(uint8_t *buf) {
|
||||
uint16_t checksum = 0;
|
||||
for (int i = 0; i <= 28; i++) {
|
||||
checksum += buf[i];
|
||||
}
|
||||
uint16_t c = (((uint16_t)buf[30]) << 8) | ((uint16_t)buf[31]);
|
||||
if (checksum != c) {
|
||||
ESP_LOGW(TAG, "Checksum from READ does not match, data corrupted");
|
||||
return;
|
||||
}
|
||||
// ESP_LOGI(TAG, "SIZEOF pms7003_data_t: %d", sizeof(pms7003_data_t));
|
||||
pms7003_data_t new_sensor_data = {0};
|
||||
memcpy(&new_sensor_data, &buf[4], sizeof(pms7003_data_t));
|
||||
uint16_t *sd = (uint16_t *)&new_sensor_data;
|
||||
for (int i = 0; i < sizeof(pms7003_data_t) / sizeof(uint16_t); i++) {
|
||||
sd[i] = SWAP_BYTES(sd[i]);
|
||||
}
|
||||
// ESP_LOGI(TAG, "PM Readings:\nPM 1.0: %d\nPM 2.5: %d\nPM 10.0:%d",
|
||||
// new_sensor_data.pm1_0_atm, new_sensor_data.pm2_5_atm,
|
||||
// new_sensor_data.pm10_atm);
|
||||
if (xSemaphoreTake(sensor_data_mutex, portMAX_DELAY)) {
|
||||
sensor_data = new_sensor_data;
|
||||
xSemaphoreGive(sensor_data_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void pm_sensor_task(void *pvParameters) {
|
||||
ESP_LOGI(TAG, "Initialize UART, Wait 1 second");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
ESP_LOGI(TAG, "Initialize UART, ok now go...");
|
||||
@@ -35,18 +118,218 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
|
||||
ESP_ERROR_CHECK(uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0));
|
||||
|
||||
ESP_LOGI(TAG, "Create a simple pattern");
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
send_pms7003_command(0xE1, 0x00, 0x00);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
ESP_LOGI(TAG, "Alloc %d buffer for UART data", BUF_SIZE);
|
||||
uint8_t uart_data[BUF_SIZE];
|
||||
int frequency_check = 2000;
|
||||
while (1) {
|
||||
int len = uart_read_bytes(UART_NUM, uart_data, BUF_SIZE, frequency_check / portTICK_PERIOD_MS);
|
||||
ESP_LOGI(TAG, "Received %d bytes from sensor", len);
|
||||
send_pms7003_command(0xE2, 0x00, 0x00);
|
||||
|
||||
int len = uart_read_bytes(UART_NUM, uart_data, BUF_SIZE, 100 / portTICK_PERIOD_MS);
|
||||
// ESP_LOGI(TAG, "Received %d bytes from sensor", len);
|
||||
if (len > 0) {
|
||||
for(int i = 0; i < len && i < 16; i++) {
|
||||
ESP_LOGI(TAG, "0x%02X", uart_data[i]);
|
||||
if (uart_data[0] != 0x42 || uart_data[1] != 0x4D) {
|
||||
ESP_LOGW(TAG, "Frame Start does not match 0x42, 0x4D, instead got %x %x", uart_data[0], uart_data[1]);
|
||||
} else if (len == 32) {
|
||||
read_pm_data(uart_data);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unexpected frame length: %d", len);
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(frequency_check));
|
||||
uart_flush(UART_NUM);
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
}
|
||||
}
|
||||
|
||||
void oled_display_task_manual(void *pvParameters) {
|
||||
ESP_LOGI(TAG, "Initialize I2C bus");
|
||||
i2c_master_bus_handle_t i2c_bus = NULL;
|
||||
i2c_master_bus_config_t bus_config = {
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.i2c_port = I2C_HOST,
|
||||
.sda_io_num = I2C_SDA_PIN,
|
||||
.scl_io_num = I2C_SCL_PIN,
|
||||
.flags.enable_internal_pullup = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus));
|
||||
|
||||
ESP_LOGI(TAG, "Install panel IO");
|
||||
esp_lcd_panel_io_handle_t io_handle = NULL;
|
||||
esp_lcd_panel_io_i2c_config_t io_config = {
|
||||
.dev_addr = SSD1306_ADDR,
|
||||
.scl_speed_hz = I2C_CLOCK_HZ, // Added this line to set the clock speed
|
||||
.control_phase_bytes = 1,
|
||||
.lcd_cmd_bits = 8,
|
||||
.lcd_param_bits = 8,
|
||||
.dc_bit_offset = 6,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus, &io_config, &io_handle));
|
||||
|
||||
ESP_LOGI(TAG, "Install SSD1306 panel driver");
|
||||
esp_lcd_panel_handle_t panel_handle = NULL;
|
||||
esp_lcd_panel_ssd1306_config_t vendor_config = {
|
||||
.height = SSD1306_HEIGHT,
|
||||
};
|
||||
esp_lcd_panel_dev_config_t panel_config = {
|
||||
.bits_per_pixel = 1,
|
||||
.reset_gpio_num = -1,
|
||||
.vendor_config = &vendor_config,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(io_handle, &panel_config, &panel_handle));
|
||||
|
||||
ESP_LOGI(TAG, "Initialize the display");
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
|
||||
ESP_LOGI(TAG, "Create a simple pattern");
|
||||
|
||||
uint8_t *fb = heap_caps_malloc(SSD1306_WIDTH * SSD1306_HEIGHT / 8, MALLOC_CAP_DMA);
|
||||
if (!fb) {
|
||||
ESP_LOGE(TAG, "Failed to initialize (fb), exiting OLED Display task");
|
||||
return;
|
||||
}
|
||||
int idx = 0;
|
||||
while (1) {
|
||||
memset(fb, 0, SSD1306_WIDTH * SSD1306_HEIGHT / 8);
|
||||
fb[idx] = 0xFF;
|
||||
|
||||
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, SSD1306_WIDTH, SSD1306_HEIGHT, fb);
|
||||
idx = (idx + 4) % (SSD1306_WIDTH * SSD1306_HEIGHT / 8);
|
||||
// shift = (shift + 1) % 8;
|
||||
vTaskDelay(pdMS_TO_TICKS(16));
|
||||
}
|
||||
memset(fb, 0, SSD1306_WIDTH * SSD1306_HEIGHT / 8);
|
||||
free(fb);
|
||||
esp_lcd_panel_del(panel_handle);
|
||||
esp_lcd_panel_io_del(io_handle);
|
||||
i2c_del_master_bus(i2c_bus);
|
||||
}
|
||||
|
||||
void oled_display_task(void *pvParameters) {
|
||||
init_ssd1306();
|
||||
char pm25_str[128];
|
||||
|
||||
ssd1306_fill_screen();
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
|
||||
while (1) {
|
||||
uint16_t pm25_value = 0;
|
||||
if (xSemaphoreTake(sensor_data_mutex, pdMS_TO_TICKS(10))) {
|
||||
pm25_value = sensor_data.pm2_5_atm;
|
||||
xSemaphoreGive(sensor_data_mutex);
|
||||
}
|
||||
|
||||
ssd1306_clear_screen();
|
||||
snprintf(pm25_str, sizeof(pm25_str), "Curr: %d ug/m3", pm25_value);
|
||||
ssd1306_print_str(2, 2, "PM Me 2.5", false);
|
||||
ssd1306_print_str(2, 15, "by Joe", false);
|
||||
ssd1306_print_str(5, 40, pm25_str, false);
|
||||
ssd1306_display();
|
||||
vTaskDelay(3000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void button_led_task(void *pvParameters) {
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << LED_PIN),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
io_conf.pin_bit_mask = (1ULL << BUTTON_PIN);
|
||||
io_conf.mode = GPIO_MODE_INPUT;
|
||||
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
gpio_set_level(LED_PIN, 0);
|
||||
|
||||
int curr_state = 1; // HIGH
|
||||
bool is_pressed = false;
|
||||
while (1) {
|
||||
int level = gpio_get_level(BUTTON_PIN);
|
||||
is_pressed = false;
|
||||
if (curr_state == 1 && level == 0) {
|
||||
gpio_set_level(LED_PIN, 1);
|
||||
curr_state = 0;
|
||||
is_pressed = true;
|
||||
} else if (curr_state == 0 && level == 1) {
|
||||
gpio_set_level(LED_PIN, 0);
|
||||
curr_state = 1;
|
||||
is_pressed = true;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(is_pressed ? 100 : 10));
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t provision_handler(httpd_req_t *req) {
|
||||
const char *response = "<html><body>Hello world!</body></html>";
|
||||
httpd_resp_send(req, response, strlen(response));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void wifi_task(void *pvParameters) {
|
||||
// Initialize NVS
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
esp_netif_create_default_wifi_ap();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.ap = {
|
||||
.ssid = "PMME-Wifi",
|
||||
.password = "88888888",
|
||||
.ssid_len = strlen("PMME-Wifi"),
|
||||
.channel = 1,
|
||||
.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
.max_connection = 2,
|
||||
},
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
ESP_LOGI(TAG, "WiFi AP started: SSID=PMME-Wifi");
|
||||
|
||||
httpd_handle_t server = NULL;
|
||||
httpd_config_t server_config = HTTPD_DEFAULT_CONFIG();
|
||||
ESP_ERROR_CHECK(httpd_start(&server, &server_config));
|
||||
|
||||
httpd_uri_t provision_uri = {
|
||||
.uri = "/api/provision",
|
||||
.method = HTTP_GET,
|
||||
.handler = provision_handler,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(server, &provision_uri));
|
||||
ESP_LOGI(TAG, "HTTP server started on port %d", server_config.server_port);
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
sensor_data_mutex = xSemaphoreCreateMutex();
|
||||
if (sensor_data_mutex == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create mutex");
|
||||
return;
|
||||
}
|
||||
xTaskCreate(pm_sensor_task, "pm_sensor_task", 4096, NULL, 5, NULL);
|
||||
xTaskCreate(oled_display_task, "oled_display_task", 4096, NULL, 5, NULL);
|
||||
xTaskCreate(button_led_task, "button_led_task", 2048, NULL, 5, NULL);
|
||||
xTaskCreate(wifi_task, "wifi_task", 8192, NULL, 5, NULL);
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
#include "ssd1306.h"
|
||||
#include "ssd1306_const.h"
|
||||
|
||||
|
||||
uint8_t ssd1306_logo[8][64] = {
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x0F, 0xEF, 0xEF, 0xEF, 0xEF, 0x0F, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x3F, 0x9F, 0x5F, 0x6F, 0xE7, 0xF3, 0xF9, 0xF9, 0xFB,
|
||||
0xF7, 0xE7, 0xCF, 0x9F, 0xBF, 0x7F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x3F, 0x9F, 0xDF,
|
||||
0xCF, 0xE7, 0xF3, 0xF9, 0xFD, 0xFE, 0xFE, 0xFF, 0xFF, 0xC0, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC, 0xF9, 0xF3, 0xF7, 0x67, 0x0F, 0x1F, 0x3F, 0x7F,
|
||||
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xFF, 0xFF, 0x7F, 0x3F, 0x80, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC, 0xFE, 0x3F, 0x1F, 0x1F,
|
||||
0x0F, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x03, 0x03, 0x07, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFB, 0xF9, 0xFC, 0xFC, 0xFE, 0xFF, 0xFF, 0xFF,
|
||||
0xFE, 0xFC, 0xF9, 0xF3, 0xF7, 0xEF, 0xCF, 0x9F, 0x3F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xFF, 0x00, 0xFE, 0x7F, 0x3F, 0x3F, 0xFF, 0xFF, 0xFF, 0x7F, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xE0, 0xE0, 0xF0, 0xF0, 0xF0, 0xF0, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0xFF, 0xFF, 0xFF, 0x7F, 0x7F,
|
||||
0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0xFF, 0xFF, 0x7F, 0x7F, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xFF, 0xFC, 0xFC, 0xFE, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x07, 0x07, 0x0F, 0x0F, 0x0F, 0x0F, 0x07, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xE0, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE,
|
||||
0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xF8, 0xF0, 0xE0,
|
||||
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xFC, 0x9E, 0x1F, 0x3F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFE, 0xFC, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60,
|
||||
0x78, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFC, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF,
|
||||
0xDF, 0xDF, 0xDF, 0xDF, 0xD0, 0xC0, 0xC0, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC0, 0xC0,
|
||||
0xD0, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF,
|
||||
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
|
||||
};
|
||||
|
||||
static i2c_ssd1306_handle_t i2c_ssd1306;
|
||||
static i2c_master_bus_handle_t i2c_master_bus;
|
||||
|
||||
/* I2C Master */
|
||||
static const i2c_master_bus_config_t i2c_master_bus_config = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.scl_io_num = GPIO_NUM_22,
|
||||
.sda_io_num = GPIO_NUM_21,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true};
|
||||
|
||||
|
||||
/* SSD1306 */
|
||||
static const i2c_ssd1306_config_t i2c_ssd1306_config = {
|
||||
.i2c_device_address = 0x3C,
|
||||
.i2c_scl_speed_hz = 400000,
|
||||
.width = 128,
|
||||
.height = 64,
|
||||
.wise = SSD1306_BOTTOM_TO_TOP};
|
||||
|
||||
|
||||
void init_ssd1306(void)
|
||||
{
|
||||
i2c_new_master_bus(&i2c_master_bus_config, &i2c_master_bus);
|
||||
i2c_ssd1306_init(i2c_master_bus, i2c_ssd1306_config, &i2c_ssd1306);
|
||||
i2c_ssd1306_buffer_image(&i2c_ssd1306, 32, 0, (const uint8_t *)ssd1306_logo, 64, 64, false);
|
||||
i2c_ssd1306_buffer_to_ram(&i2c_ssd1306);
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
i2c_ssd1306_buffer_clear(&i2c_ssd1306);
|
||||
}
|
||||
|
||||
esp_err_t ssd1306_print_str(uint8_t x, uint8_t y, const char *text, bool invert)
|
||||
{
|
||||
return (i2c_ssd1306_buffer_text(&i2c_ssd1306, x, y, text, invert));
|
||||
}
|
||||
|
||||
esp_err_t ssd1306_display(void)
|
||||
{
|
||||
return (i2c_ssd1306_buffer_to_ram(&i2c_ssd1306));
|
||||
}
|
||||
|
||||
esp_err_t ssd1306_clear_screen(void)
|
||||
{
|
||||
return (i2c_ssd1306_buffer_clear(&i2c_ssd1306));
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_init(i2c_master_bus_handle_t i2c_master_bus, i2c_ssd1306_config_t i2c_ssd1306_config, i2c_ssd1306_handle_t *i2c_ssd1306)
|
||||
{
|
||||
if (i2c_ssd1306_config.i2c_scl_speed_hz > 400000 || i2c_ssd1306_config.width > 128 || i2c_ssd1306_config.height % 8 != 0 || i2c_ssd1306_config.height < 16 || i2c_ssd1306_config.height > 64)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid SSD1306 configuration, 'i2c_scl_speed_hz' must be less than or equal to 400000, 'width' must be less than or equal to 128, 'height' must be between 16 and 64 and multiple of 8");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_LOGI(SSD1306_TAG, "Initializing I2C SSD1306...");
|
||||
esp_err_t ret = i2c_master_probe(i2c_master_bus, i2c_ssd1306_config.i2c_device_address, I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
switch (ret)
|
||||
{
|
||||
case ESP_ERR_NOT_FOUND:
|
||||
ESP_LOGE(SSD1306_TAG, "I2C SSD1306 device not found in address 0x%02X", i2c_ssd1306_config.i2c_device_address);
|
||||
break;
|
||||
case ESP_ERR_TIMEOUT:
|
||||
ESP_LOGE(SSD1306_TAG, "I2C SSD1306 device timeout in address 0x%02X", i2c_ssd1306_config.i2c_device_address);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(SSD1306_TAG, "I2C SSD1306 device error in address 0x%02X", i2c_ssd1306_config.i2c_device_address);
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
i2c_device_config_t i2c_device_config = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_7,
|
||||
.device_address = i2c_ssd1306_config.i2c_device_address,
|
||||
.scl_speed_hz = i2c_ssd1306_config.i2c_scl_speed_hz};
|
||||
ret = i2c_master_bus_add_device(i2c_master_bus, &i2c_device_config, &i2c_ssd1306->i2c_master_dev);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to add I2C SSD1306 device");
|
||||
return ret;
|
||||
}
|
||||
uint8_t ssd1306_init_cmd[] = {
|
||||
OLED_CONTROL_BYTE_CMD,
|
||||
OLED_CMD_DISPLAY_OFF,
|
||||
OLED_CMD_SET_MUX_RATIO, (i2c_ssd1306_config.height - 1),
|
||||
OLED_CMD_SET_VERT_DISPLAY_OFFSET, 0x00,
|
||||
OLED_MASK_DISPLAY_START_LINE | 0x00,
|
||||
OLED_CMD_COM_SCAN_DIRECTION_NORMAL,
|
||||
OLED_CMD_SEGMENT_REMAP_LEFT_TO_RIGHT,
|
||||
OLED_CMD_SET_COM_PIN_HARDWARE_MAP, 0x12,
|
||||
OLED_CMD_SET_MEMORY_ADDR_MODE, 0x02,
|
||||
OLED_CMD_SET_CONTRAST_CONTROL, 0xFF,
|
||||
OLED_CMD_SET_DISPLAY_CLK_DIVIDE, 0x80,
|
||||
OLED_CMD_ENABLE_DISPLAY_RAM,
|
||||
OLED_CMD_NORMAL_DISPLAY,
|
||||
OLED_CMD_SET_CHARGE_PUMP, 0x14,
|
||||
OLED_CMD_DISPLAY_ON};
|
||||
if (i2c_ssd1306_config.wise == SSD1306_BOTTOM_TO_TOP)
|
||||
{
|
||||
ssd1306_init_cmd[7] = OLED_CMD_COM_SCAN_DIRECTION_REMAP;
|
||||
ssd1306_init_cmd[8] = OLED_CMD_SEGMENT_REMAP_RIGHT_TO_LEFT;
|
||||
}
|
||||
ret = i2c_master_transmit(i2c_ssd1306->i2c_master_dev, ssd1306_init_cmd, sizeof(ssd1306_init_cmd), I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to initialize I2C SSD1306 device");
|
||||
return ret;
|
||||
}
|
||||
|
||||
i2c_ssd1306->width = i2c_ssd1306_config.width;
|
||||
i2c_ssd1306->height = i2c_ssd1306_config.height;
|
||||
i2c_ssd1306->total_pages = i2c_ssd1306_config.height / 8;
|
||||
|
||||
i2c_ssd1306->page = (ssd1306_page_t *)calloc(i2c_ssd1306->total_pages, sizeof(ssd1306_page_t));
|
||||
if (i2c_ssd1306->page == NULL)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to allocate memory for I2C SSD1306 device");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
for (uint8_t i = 0; i < i2c_ssd1306->total_pages; i++)
|
||||
{
|
||||
i2c_ssd1306->page[i].segment = (uint8_t *)calloc(i2c_ssd1306->width, sizeof(uint8_t));
|
||||
if (i2c_ssd1306->page[i].segment == NULL)
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
ESP_LOGI(SSD1306_TAG, "I2C SSD1306 initialized successfully");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_deinit(i2c_ssd1306_handle_t *i2c_ssd1306)
|
||||
{
|
||||
ESP_LOGI(SSD1306_TAG, "Deinitializing I2C SSD1306...");
|
||||
for (uint8_t i = 0; i < i2c_ssd1306->total_pages; i++)
|
||||
{
|
||||
free(i2c_ssd1306->page[i].segment);
|
||||
}
|
||||
free(i2c_ssd1306->page);
|
||||
esp_err_t ret = i2c_master_bus_rm_device(i2c_ssd1306->i2c_master_dev);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to remove I2C SSD1306 device");
|
||||
return ret;
|
||||
}
|
||||
ESP_LOGI(SSD1306_TAG, "I2C SSD1306 deinitialized successfully");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_check(i2c_ssd1306_handle_t *i2c_ssd1306)
|
||||
{
|
||||
for (uint8_t i = 0; i < i2c_ssd1306->total_pages; i++)
|
||||
{
|
||||
for (uint8_t j = 0; j < i2c_ssd1306->width; j++)
|
||||
{
|
||||
printf("%02X ", i2c_ssd1306->page[i].segment[j]);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_clear(i2c_ssd1306_handle_t *i2c_ssd1306)
|
||||
{
|
||||
for (uint8_t i = 0; i < i2c_ssd1306->total_pages; i++)
|
||||
{
|
||||
memset(i2c_ssd1306->page[i].segment, 0x00, i2c_ssd1306->width);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_fill(i2c_ssd1306_handle_t *i2c_ssd1306)
|
||||
{
|
||||
for (uint8_t i = 0; i < i2c_ssd1306->total_pages; i++)
|
||||
{
|
||||
memset(i2c_ssd1306->page[i].segment, 0xFF, i2c_ssd1306->width);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_fill_pixel(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, bool fill)
|
||||
{
|
||||
if (x >= i2c_ssd1306->width || y >= i2c_ssd1306->height)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid pixel coordinates, 'x' must be between 0 and %d, 'y' must be between 0 and %d", i2c_ssd1306->width - 1, i2c_ssd1306->height - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint8_t page = y / 8;
|
||||
uint8_t bit = 1 << (y % 8);
|
||||
if (fill)
|
||||
{
|
||||
i2c_ssd1306->page[page].segment[x] |= bit;
|
||||
}
|
||||
else
|
||||
{
|
||||
i2c_ssd1306->page[page].segment[x] &= ~bit;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_fill_space(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x1, uint8_t x2, uint8_t y1, uint8_t y2, bool fill)
|
||||
{
|
||||
if (x1 >= i2c_ssd1306->width || x2 >= i2c_ssd1306->width || y1 >= i2c_ssd1306->height || y2 >= i2c_ssd1306->height || x1 > x2 || y1 > y2)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid space coordinates, 'x1' and 'x2' must be between 0 and %d, 'y1' and 'y2' must be between 0 and %d, 'x1' must be less than 'x2', 'y1' must be less than 'y2'", i2c_ssd1306->width - 1, i2c_ssd1306->height - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t start_page = y1 / 8;
|
||||
uint8_t end_page = y2 / 8;
|
||||
uint8_t mask;
|
||||
uint8_t offset_start = y1 % 8;
|
||||
uint8_t offset_end = y2 % 8;
|
||||
|
||||
for (uint8_t page = start_page; page <= end_page; page++)
|
||||
{
|
||||
if (start_page == end_page)
|
||||
{
|
||||
mask = (0xFF << offset_start) & (0xFF >> (7 - offset_end));
|
||||
}
|
||||
else if (page == start_page)
|
||||
{
|
||||
mask = 0xFF << offset_start;
|
||||
}
|
||||
else if (page == end_page)
|
||||
{
|
||||
mask = 0xFF >> (7 - offset_end);
|
||||
}
|
||||
else
|
||||
{
|
||||
mask = 0xFF;
|
||||
}
|
||||
|
||||
for (uint8_t j = x1; j <= x2; j++)
|
||||
{
|
||||
if (fill)
|
||||
i2c_ssd1306->page[page].segment[j] |= mask;
|
||||
else
|
||||
i2c_ssd1306->page[page].segment[j] &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_text(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, const char *text, bool invert)
|
||||
{
|
||||
if (x >= i2c_ssd1306->width || y >= i2c_ssd1306->height || !text || strlen(text) == 0)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid text or coordinates: x=%d (max %d), y=%d (max %d)", x, i2c_ssd1306->width - 1, y, i2c_ssd1306->height - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t len = strlen(text);
|
||||
uint8_t page = y / 8;
|
||||
uint8_t offset = y % 8;
|
||||
bool has_next_page = (page + 1) < i2c_ssd1306->total_pages;
|
||||
|
||||
uint8_t max_chars = (i2c_ssd1306->width - x) / 8;
|
||||
if (len > max_chars)
|
||||
{
|
||||
ESP_LOGW(SSD1306_TAG, "Text truncated: text columns exceed display width, lost %d columns", x + (len * 8) - i2c_ssd1306->width);
|
||||
}
|
||||
|
||||
if (offset != 0 && !has_next_page)
|
||||
{
|
||||
ESP_LOGW(SSD1306_TAG, "Vertical truncation: text exceeds display height, lost %d rows", offset);
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len && x < i2c_ssd1306->width; i++)
|
||||
{
|
||||
const uint8_t *char_data = font8x8[(uint8_t)text[i]];
|
||||
uint8_t available_columns = i2c_ssd1306->width - x;
|
||||
uint8_t columns_to_draw = (available_columns < 8) ? available_columns : 8;
|
||||
|
||||
for (uint8_t j = 0; j < columns_to_draw; j++)
|
||||
{
|
||||
uint8_t char_col = char_data[j];
|
||||
|
||||
if (invert)
|
||||
{
|
||||
char_col = ~char_col;
|
||||
}
|
||||
|
||||
if (offset == 0)
|
||||
{
|
||||
i2c_ssd1306->page[page].segment[x + j] |= char_col;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t lower_part = char_col << offset;
|
||||
uint8_t upper_part = char_col >> (8 - offset);
|
||||
|
||||
i2c_ssd1306->page[page].segment[x + j] |= lower_part;
|
||||
if (has_next_page)
|
||||
{
|
||||
i2c_ssd1306->page[page + 1].segment[x + j] |= upper_part;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
x += 8;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_int(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, int value, bool invert)
|
||||
{
|
||||
char text[16];
|
||||
sprintf(text, "%d", value);
|
||||
|
||||
return i2c_ssd1306_buffer_text(i2c_ssd1306, x, y, text, invert);
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_float(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, float value, uint8_t decimals, bool invert)
|
||||
{
|
||||
char text[16];
|
||||
sprintf(text, "%.*f", decimals, value);
|
||||
|
||||
return i2c_ssd1306_buffer_text(i2c_ssd1306, x, y, text, invert);
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_image(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, const uint8_t *image, uint8_t img_width, uint8_t img_height, bool invert)
|
||||
{
|
||||
if (image == NULL || img_width == 0 || img_height == 0 || x >= i2c_ssd1306->width || y >= i2c_ssd1306->height)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid image or coordinates: x=%d (max %d), y=%d (max %d)", x, i2c_ssd1306->width - 1, y, i2c_ssd1306->height - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t draw_width = (img_width < (i2c_ssd1306->width - x)) ? img_width : (i2c_ssd1306->width - x);
|
||||
uint8_t draw_height = (img_height < (i2c_ssd1306->height - y)) ? img_height : (i2c_ssd1306->height - y);
|
||||
|
||||
uint8_t start_page = y / 8;
|
||||
uint8_t vertical_offset = y % 8;
|
||||
uint8_t num_pages = i2c_ssd1306->total_pages;
|
||||
uint8_t draw_pages = (((draw_height + 7) / 8) < (num_pages - start_page)) ? ((draw_height + 7) / 8) : (num_pages - start_page);
|
||||
|
||||
if (img_height > draw_height)
|
||||
{
|
||||
ESP_LOGW(SSD1306_TAG, "Vertical truncation: Lost %d rows", img_height - draw_height);
|
||||
}
|
||||
if (draw_width < img_width)
|
||||
{
|
||||
ESP_LOGW(SSD1306_TAG, "Horizontal truncation: Lost %d columns", img_width - draw_width);
|
||||
}
|
||||
|
||||
for (uint8_t col = 0; col < draw_width; col++)
|
||||
{
|
||||
for (uint8_t page = 0; page < draw_pages; page++)
|
||||
{
|
||||
uint8_t target_page = start_page + page;
|
||||
if (target_page >= num_pages)
|
||||
break;
|
||||
|
||||
uint8_t img_byte = image[page * img_width + col];
|
||||
if (invert)
|
||||
img_byte = ~img_byte;
|
||||
|
||||
if (vertical_offset == 0)
|
||||
{
|
||||
i2c_ssd1306->page[target_page].segment[x + col] |= img_byte;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t lower = img_byte << vertical_offset;
|
||||
uint8_t upper = img_byte >> (8 - vertical_offset);
|
||||
|
||||
i2c_ssd1306->page[target_page].segment[x + col] |= lower;
|
||||
if (target_page + 1 < num_pages)
|
||||
{
|
||||
i2c_ssd1306->page[target_page + 1].segment[x + col] |= upper;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_segment_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t page, uint8_t segment)
|
||||
{
|
||||
if (page >= i2c_ssd1306->total_pages || segment >= i2c_ssd1306->width)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid page or segment number, 'page' must be between 0 and %d, 'segment' must be between 0 and %d", i2c_ssd1306->total_pages - 1, i2c_ssd1306->width - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t ram_addr_cmd[] = {
|
||||
OLED_CONTROL_BYTE_CMD,
|
||||
OLED_MASK_PAGE_ADDR | page,
|
||||
OLED_MASK_LSB_NIBBLE_SEG_ADDR | (segment & 0x0F),
|
||||
OLED_MASK_HSB_NIBBLE_SEG_ADDR | (segment >> 4 & 0x0F)};
|
||||
esp_err_t err = i2c_master_transmit(i2c_ssd1306->i2c_master_dev, ram_addr_cmd, sizeof(ram_addr_cmd), I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to address the segment to the RAM of the SSD1306 device");
|
||||
return err;
|
||||
}
|
||||
uint8_t ram_data_cmd[] = {
|
||||
OLED_CONTROL_BYTE_DATA,
|
||||
i2c_ssd1306->page[page].segment[segment]};
|
||||
err = i2c_master_transmit(i2c_ssd1306->i2c_master_dev, ram_data_cmd, sizeof(ram_data_cmd), I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to transfer the segment to the RAM of the SSD1306 device");
|
||||
return err;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_segments_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t page, uint8_t initial_segment, uint8_t final_segment)
|
||||
{
|
||||
if (page >= i2c_ssd1306->total_pages || initial_segment >= i2c_ssd1306->width || final_segment >= i2c_ssd1306->width || initial_segment > final_segment)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid page or segment range, 'page' must be between 0 and %d, 'initial_segment' and 'final_segment' must be between 0 and %d, 'initial_segment' must be less than or equal to 'final_segment'", i2c_ssd1306->total_pages - 1, i2c_ssd1306->width - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t ram_addr_cmd[] = {
|
||||
OLED_CONTROL_BYTE_CMD,
|
||||
OLED_MASK_PAGE_ADDR | page,
|
||||
OLED_MASK_LSB_NIBBLE_SEG_ADDR | (initial_segment & 0x0F),
|
||||
OLED_MASK_HSB_NIBBLE_SEG_ADDR | (initial_segment >> 4 & 0x0F)};
|
||||
esp_err_t err = i2c_master_transmit(i2c_ssd1306->i2c_master_dev, ram_addr_cmd, sizeof(ram_addr_cmd), I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to address the initial segment to the RAM of the SSD1306 device");
|
||||
return err;
|
||||
}
|
||||
uint8_t ram_data_cmd[final_segment - initial_segment + 2];
|
||||
ram_data_cmd[0] = OLED_CONTROL_BYTE_DATA;
|
||||
for (uint8_t i = 0; i < final_segment - initial_segment + 1; i++)
|
||||
{
|
||||
ram_data_cmd[i + 1] = i2c_ssd1306->page[page].segment[initial_segment + i];
|
||||
}
|
||||
err = i2c_master_transmit(i2c_ssd1306->i2c_master_dev, ram_data_cmd, sizeof(ram_data_cmd), I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to transfer the segments to the RAM of the SSD1306 device");
|
||||
return err;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_page_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t page)
|
||||
{
|
||||
if (page >= i2c_ssd1306->total_pages)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid page number, must be between 0 and %d", i2c_ssd1306->total_pages - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t ram_addr_cmd[] = {
|
||||
OLED_CONTROL_BYTE_CMD,
|
||||
OLED_MASK_PAGE_ADDR | page,
|
||||
OLED_MASK_LSB_NIBBLE_SEG_ADDR | (0x00 & 0x0F),
|
||||
OLED_MASK_HSB_NIBBLE_SEG_ADDR | (0x00 >> 4 & 0x0F)};
|
||||
esp_err_t err = i2c_master_transmit(i2c_ssd1306->i2c_master_dev, ram_addr_cmd, sizeof(ram_addr_cmd), I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to address the page to the RAM of the SSD1306 device");
|
||||
return err;
|
||||
}
|
||||
uint8_t ram_data_cmd[i2c_ssd1306->width + 1];
|
||||
ram_data_cmd[0] = OLED_CONTROL_BYTE_DATA;
|
||||
for (uint8_t i = 0; i < i2c_ssd1306->width; i++)
|
||||
{
|
||||
ram_data_cmd[i + 1] = i2c_ssd1306->page[page].segment[i];
|
||||
}
|
||||
err = i2c_master_transmit(i2c_ssd1306->i2c_master_dev, ram_data_cmd, sizeof(ram_data_cmd), I2C_SSD1306_TIMEOUT_MS / portTICK_PERIOD_MS);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Failed to transfer the page to the RAM of the SSD1306 device");
|
||||
return err;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_pages_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t initial_page, uint8_t final_page)
|
||||
{
|
||||
if (initial_page >= i2c_ssd1306->total_pages || final_page >= i2c_ssd1306->total_pages || initial_page > final_page)
|
||||
{
|
||||
ESP_LOGE(SSD1306_TAG, "Invalid page range, 'initial_page' and 'final_page' must be between 0 and %d, 'initial_page' must be less than or equal to 'final_page'", i2c_ssd1306->total_pages - 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t err = ESP_OK;
|
||||
for (uint8_t i = initial_page; i <= final_page; i++)
|
||||
{
|
||||
esp_err_t err = i2c_ssd1306_page_to_ram(i2c_ssd1306, i);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t i2c_ssd1306_buffer_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
for (uint8_t i = 0; i < i2c_ssd1306->total_pages; i++)
|
||||
{
|
||||
err = i2c_ssd1306_page_to_ram(i2c_ssd1306, i);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
// My Functions
|
||||
esp_err_t ssd1306_fill_screen(void) {
|
||||
i2c_ssd1306_buffer_fill(&i2c_ssd1306);
|
||||
return i2c_ssd1306_buffer_to_ram(&i2c_ssd1306);
|
||||
}
|
||||
|
||||
void init_ssd1306_no_splash(void)
|
||||
{
|
||||
i2c_new_master_bus(&i2c_master_bus_config, &i2c_master_bus);
|
||||
i2c_ssd1306_init(i2c_master_bus, i2c_ssd1306_config, &i2c_ssd1306);
|
||||
// i2c_ssd1306_buffer_image(&i2c_ssd1306, 32, 0, (const uint8_t *)ssd1306_logo, 64, 64, false);
|
||||
// i2c_ssd1306_buffer_to_ram(&i2c_ssd1306);
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
i2c_ssd1306_buffer_clear(&i2c_ssd1306);
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
#pragma once
|
||||
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_log.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
#define SSD1306_TAG "SSD1306"
|
||||
|
||||
#define I2C_SSD1306_TIMEOUT_MS 1000
|
||||
|
||||
/**
|
||||
* @brief Enumeration for SSD1306 display orientation.
|
||||
*
|
||||
* Defines the orientation (drawing order) used for the SSD1306 display.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
SSD1306_TOP_TO_BOTTOM,
|
||||
SSD1306_BOTTOM_TO_TOP
|
||||
} ssd1306_wise_t;
|
||||
|
||||
/**
|
||||
* @brief Structure for an SSD1306 page segment.
|
||||
*
|
||||
* Contains a pointer to the segment data for a page in the SSD1306 buffer.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
uint8_t *segment;
|
||||
} ssd1306_page_t;
|
||||
|
||||
/**
|
||||
* @brief Configuration for the I2C SSD1306 display.
|
||||
*
|
||||
* Holds configuration parameters for both the SSD1306 display and the I2C master.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
uint16_t i2c_device_address;
|
||||
uint32_t i2c_scl_speed_hz;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
ssd1306_wise_t wise;
|
||||
} i2c_ssd1306_config_t;
|
||||
|
||||
/**
|
||||
* @brief Handle for the I2C SSD1306 display.
|
||||
*
|
||||
* Contains runtime information including the I2C device handle, display dimensions, and pointers to the page buffers.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
i2c_master_dev_handle_t i2c_master_dev;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
uint8_t total_pages;
|
||||
ssd1306_page_t *page;
|
||||
} i2c_ssd1306_handle_t;
|
||||
|
||||
|
||||
void init_ssd1306(void);
|
||||
esp_err_t ssd1306_print_str(uint8_t x, uint8_t y, const char *text, bool invert);
|
||||
esp_err_t ssd1306_display(void);
|
||||
esp_err_t ssd1306_clear_screen(void);
|
||||
|
||||
/**
|
||||
* @brief Initialize the I2C SSD1306 display.
|
||||
*
|
||||
* Configures and initializes the SSD1306 display using the provided I2C bus and configuration.
|
||||
*
|
||||
* @param i2c_master_bus An initialized I2C master bus handle.
|
||||
* @param i2c_ssd1306_config Configuration parameters for the SSD1306 display.
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle to be initialized.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success.
|
||||
* - ESP_ERR_INVALID_ARG if an argument is invalid.
|
||||
* - ESP_ERR_NO_MEM if memory allocation fails.
|
||||
* - ESP_FAIL on other failures.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_init(i2c_master_bus_handle_t i2c_master_bus, i2c_ssd1306_config_t i2c_ssd1306_config, i2c_ssd1306_handle_t *i2c_ssd1306);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the I2C SSD1306 display.
|
||||
*
|
||||
* Frees the memory allocated for the SSD1306 display handle.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_deinit(i2c_ssd1306_handle_t *i2c_ssd1306);
|
||||
|
||||
/**
|
||||
* @brief Display the contents of the SSD1306 buffer.
|
||||
*
|
||||
* Prints the hexadecimal values of each segment in every page of the SSD1306 buffer
|
||||
* for debugging purposes.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_check(i2c_ssd1306_handle_t *i2c_ssd1306);
|
||||
|
||||
/**
|
||||
* @brief Clear the SSD1306 display buffer.
|
||||
*
|
||||
* Resets all segments in the SSD1306 buffer to 0x00.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_clear(i2c_ssd1306_handle_t *i2c_ssd1306);
|
||||
|
||||
/**
|
||||
* @brief Fill the SSD1306 display buffer.
|
||||
*
|
||||
* Fills every segment in each page of the SSD1306 buffer with 0xFF.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_fill(i2c_ssd1306_handle_t *i2c_ssd1306);
|
||||
|
||||
/**
|
||||
* @brief Set or clear an individual pixel in the SSD1306 buffer.
|
||||
*
|
||||
* Modifies the state of a specific pixel in the SSD1306 buffer.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param x X-coordinate of the pixel.
|
||||
* @param y Y-coordinate of the pixel.
|
||||
* @param fill If true, the pixel is set; if false, it is cleared.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_fill_pixel(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, bool fill);
|
||||
|
||||
/**
|
||||
* @brief Fill or clear a rectangular area in the SSD1306 buffer.
|
||||
*
|
||||
* Sets or clears a block of pixels within the specified coordinate range.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param x1 X-coordinate of the starting pixel.
|
||||
* @param x2 X-coordinate of the ending pixel.
|
||||
* @param y1 Y-coordinate of the starting pixel.
|
||||
* @param y2 Y-coordinate of the ending pixel.
|
||||
* @param fill If true, pixels are set; if false, pixels are cleared.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_fill_space(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x1, uint8_t x2, uint8_t y1, uint8_t y2, bool fill);
|
||||
|
||||
/**
|
||||
* @brief Render text into the SSD1306 buffer.
|
||||
*
|
||||
* Copies 8x8 font characters representing the provided string into the SSD1306 buffer.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param x X-coordinate for the text's starting position.
|
||||
* @param y Y-coordinate for the text's starting position.
|
||||
* @param text Null-terminated string to render.
|
||||
* @param invert If true, the text is rendered inverted.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_text(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, const char *text, bool invert);
|
||||
|
||||
/**
|
||||
* @brief Render an integer into the SSD1306 buffer.
|
||||
*
|
||||
* Copies 8x8 font characters representing the given integer into the SSD1306 buffer.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param x X-coordinate for the integer's starting position.
|
||||
* @param y Y-coordinate for the integer's starting position.
|
||||
* @param value Integer value to render.
|
||||
* @param invert If true, the integer is rendered inverted.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_int(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, int value, bool invert);
|
||||
|
||||
/**
|
||||
* @brief Render a floating-point number into the SSD1306 buffer.
|
||||
*
|
||||
* Copies 8x8 font characters representing the given float value into the SSD1306 buffer.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param x X-coordinate for the float's starting position.
|
||||
* @param y Y-coordinate for the float's starting position.
|
||||
* @param value Float value to render.
|
||||
* @param decimals Number of decimal places to display.
|
||||
* @param invert If true, the float is rendered inverted.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_float(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, float value, uint8_t decimals, bool invert);
|
||||
|
||||
/**
|
||||
* @brief Render an image into the SSD1306 buffer.
|
||||
*
|
||||
* Copies the provided image data into the SSD1306 buffer at an 8x8 pixel resolution.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param x X-coordinate for the image's starting position.
|
||||
* @param y Y-coordinate for the image's starting position.
|
||||
* @param image Pointer to the image data.
|
||||
* @param width Width of the image in pixels.
|
||||
* @param height Height of the image in pixels.
|
||||
* @param invert If true, the image is rendered inverted.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_image(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t x, uint8_t y, const uint8_t *image, uint8_t width, uint8_t height, bool invert);
|
||||
|
||||
/**
|
||||
* @brief Transfer a specific buffer segment to the SSD1306 display RAM.
|
||||
*
|
||||
* Moves the designated segment from the buffer to the corresponding location in the display's RAM.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param page Page number that contains the segment.
|
||||
* @param segment Segment number within the page.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_segment_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t page, uint8_t segment);
|
||||
|
||||
/**
|
||||
* @brief Transfer a range of buffer segments to the SSD1306 display RAM.
|
||||
*
|
||||
* Moves a sequence of segments from the specified page in the buffer to the display's RAM.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param page Page number that contains the segments.
|
||||
* @param initial_segment Starting segment number.
|
||||
* @param final_segment Ending segment number.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_segments_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t page, uint8_t initial_segment, uint8_t final_segment);
|
||||
|
||||
/**
|
||||
* @brief Transfer an entire page from the buffer to the SSD1306 display RAM.
|
||||
*
|
||||
* Updates the display's RAM by transferring all segments of the specified page.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param page Page number to update.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_page_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t page);
|
||||
|
||||
/**
|
||||
* @brief Transfer a range of pages from the buffer to the SSD1306 display RAM.
|
||||
*
|
||||
* Updates the display's RAM by transferring all segments for pages within the specified range.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
* @param initial_page Starting page number.
|
||||
* @param final_page Ending page number.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_pages_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t initial_page, uint8_t final_page);
|
||||
|
||||
/**
|
||||
* @brief Transfer the entire buffer to the SSD1306 display RAM.
|
||||
*
|
||||
* Updates the display's RAM by transferring the contents of all pages in the buffer.
|
||||
*
|
||||
* @param i2c_ssd1306 Pointer to the SSD1306 handle.
|
||||
*
|
||||
* @return ESP_OK on success, or an error code otherwise.
|
||||
*/
|
||||
esp_err_t i2c_ssd1306_buffer_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306);
|
||||
|
||||
// My Functions;
|
||||
esp_err_t ssd1306_fill_screen(void);
|
||||
void init_ssd1306_no_splash(void);
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
#pragma once
|
||||
|
||||
/* https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf */
|
||||
|
||||
/* CONTROL BYTE
|
||||
A control byte mainly consists of Co (Continuation Bit) and D/C# (Data / Command Selection Bit) bits following by six “0”.
|
||||
Co: 0 = The transmission of the following information will contain data bytes only.
|
||||
1 = NOT DEFINED
|
||||
D/C#: 0 = The following data byte is a command.
|
||||
1 = The following data byte is a data which will be stored at the GDDRAM. The GDDRAM column address pointer will be
|
||||
increased by one automatically after each data write.
|
||||
*/
|
||||
#define OLED_CONTROL_BYTE_CMD 0x00 // Control Byte to transmit a command.
|
||||
#define OLED_CONTROL_BYTE_DATA 0x40 // Control byte to transmit data.
|
||||
|
||||
/* FUNDAMENTAL COMMAND */
|
||||
#define OLED_CMD_SET_CONTRAST_CONTROL 0x81 // Double byte command to set contrast setting of the display. [0x00 - 0xFF] (RESET: 0x7F)
|
||||
#define OLED_CMD_ENABLE_DISPLAY_RAM 0xA4 // Enable display outputs according to the GDDRAM contents. (Default during reset)
|
||||
#define OLED_CMD_FORCE_DISPLAY_ALLON 0xA5 // Forces the entire display to be “ON”.
|
||||
#define OLED_CMD_NORMAL_DISPLAY 0xA6 // RAM data of 1 indicates an “ON” pixel. (Default during reset)
|
||||
#define OLED_CMD_INVERTED_DISPLAY 0xA7 // RAM data of 0 indicates an “ON” pixel.
|
||||
#define OLED_CMD_DISPLAY_OFF 0xAE // Display OFF in sleep mode. (Default during reset)
|
||||
#define OLED_CMD_DISPLAY_ON 0xAF // Display ON in normal mode.
|
||||
|
||||
/* ADDRESSING SETTING */
|
||||
#define OLED_CMD_SET_MEMORY_ADDR_MODE 0x20 // Double byte command to set memory addressing mode. [0x00 | HORZ, 0x01 | VERT, 0x02 | PAGE] (RESET: 0x02)
|
||||
#define OLED_MASK_PAGE_ADDR 0xB0 // Mask to set the page address of pointer only in page addressing mode. [0xB0 - 0xB7]
|
||||
#define OLED_MASK_LSB_NIBBLE_SEG_ADDR 0x00 // Mask to set the lower start column address of pointer only in page addressing mode. [0x00 - 0x0F]
|
||||
#define OLED_MASK_HSB_NIBBLE_SEG_ADDR 0x10 // Mask to set the higher start column address of pointer only in page addressing mode. [0x10 - 0x1F]
|
||||
#define OLED_CMD_SET_COLUMN_ADDR_RANGE 0x21 // Three byte command to set start and end column address only in horizontal/vertical mode. [0x00 - 0x7F & 0x00 - 0x7F] (RESET: 0x00 & 0x7F)
|
||||
#define OLED_CMD_SET_PAGE_ADDR_RANGE 0x22 // Three byte command to set start and end page address only in horizontal/vertical mode. [0x00 - 0x07 & 0x00 - 0x07] (RESET: 0x00 & 0x07)
|
||||
|
||||
/* HARDWARE CONFIGURATION */
|
||||
#define OLED_MASK_DISPLAY_START_LINE 0x40 // Mask to set the display start line register to determine starting address of display RAM. [0x40 - 0x7F] (RESET: 0x40)
|
||||
#define OLED_CMD_SEGMENT_REMAP_LEFT_TO_RIGHT 0xA0 // Column address 0 is mapped to SEG0, indicating that the display is mapped from left to right. (Default during reset)
|
||||
#define OLED_CMD_SEGMENT_REMAP_RIGHT_TO_LEFT 0xA1 // Column address 127 is mapped to SEG0, indicating that the display is mapped from right to left.
|
||||
#define OLED_CMD_SET_MUX_RATIO 0xA8 // Double byte command to set MUX ratio. [0x0F - 0x3F] (RESET: 0x3F = 64MUX -1)
|
||||
#define OLED_CMD_COM_SCAN_DIRECTION_NORMAL 0xC0 // Normal COM scan direction. (COM0 -> COM[MUX-1] | BOTTOM TO TOP)
|
||||
#define OLED_CMD_COM_SCAN_DIRECTION_REMAP 0xC8 // Remapped/Flipped COM scan direction. (COM[MUX-1] -> COM0 | TOP TO BOTTOM)
|
||||
#define OLED_CMD_SET_COM_PIN_HARDWARE_MAP 0xDA // Double byte command to set COM pins hardware configuration. [0x02 | DISABLE COM REMAP - SEQUENTIAL COM, 0x12 | DISABLE COM REMAP - ALTERNATIVE COM, 0x22 | ENABLE COM REMAP - SEQUENTIAL COM, 0x32 | ENABLE COM REMAP - ALTERNATIVE COM] (RESET: 0x12)
|
||||
#define OLED_CMD_SET_VERT_DISPLAY_OFFSET 0xD3 // Double byte command to set vertical display offset by COM. [0x00 - 0x3F] (RESET: 0x00)
|
||||
|
||||
/* TIMING & DRIVING SCHEME SETTING */
|
||||
#define OLED_CMD_SET_DISPLAY_CLK_DIVIDE 0xD5 // Double byte command to set display clock oscillator frequency and divide ratio. [0x00 - 0x0F << 4 & 0x00->0x0F] ((OSCFREQ) << 4 | (DIVIDE RATIO)) (RESET: 0x80)
|
||||
#define OLED_CMD_SET_PRECHARGE_PERIOD 0xD9 // Double byte command to set pre-charge period. [0x00 - 0x0F << 4 & 0x00 - 0x0F](PHASE2/DOTS << 4 | PHASE1/DOTS) (RESET: 0x22)
|
||||
#define OLED_CMD_SET_VCOMH_DESELECT_LEVEL 0xDB // Double byte command to set VCOMH deselect level regulator output. [0X00 | 0.65, 0X20 | 0.77, 0X30 | 0.83 VCC] (RESET: 0x20)
|
||||
|
||||
/* CHARGE PUMP */
|
||||
#define OLED_CMD_SET_CHARGE_PUMP 0x8D // Double byte command to set charge pump. [0x10 | OFF, 0x14 | ON] (RESET: 0x14)
|
||||
|
||||
/* ADDITIONAL COMMANDS */
|
||||
#define OLED_CMD_NO_OPERATION 0xE3 // NO OPERATION COMMAND
|
||||
|
||||
/* FONT DEFINED FROM ASCII 32 -> 127 */
|
||||
static const uint8_t font8x8[256][8] = {
|
||||
/* ASCII CONTROL CHARACTERS */
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 000 -> 0x00 [NULL]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 001 -> 0x01 [SOH]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 002 -> 0x02 [STX]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 003 -> 0x03 [ETX]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 004 -> 0x04 [EOT]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 005 -> 0x05 [ENQ]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 006 -> 0x06 [ACK]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 007 -> 0x07 [BEL]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 008 -> 0x08 [BS]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 009 -> 0x09 [TAB]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 010 -> 0x0A [LF]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 011 -> 0x0B [VT]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 012 -> 0x0C [FF]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 013 -> 0x0D [CR]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 014 -> 0x0E [SO]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 015 -> 0x0F [SI]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 016 -> 0x10 [DLE]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 017 -> 0x11 [DC1]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 018 -> 0x12 [DC2]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 019 -> 0x13 [DC3]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 020 -> 0x14 [DC4]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 021 -> 0x15 [NAK]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 022 -> 0x16 [SYN]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 023 -> 0x17 [ETB]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 024 -> 0x18 [CAN]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 025 -> 0x19 [EM]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 026 -> 0x1A [SUB]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 027 -> 0x1B [ESC]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 028 -> 0x1C [FS]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 029 -> 0x1D [GS]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 030 -> 0x1E [RS]
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 031 -> 0x1F [US]
|
||||
/* ASCII PRINTABLE CHARACTERS */
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 032 -> 0x20 ( )
|
||||
{0x00, 0x00, 0x00, 0x5F, 0x5F, 0x00, 0x00, 0x00}, // 033 -> 0x21 (!)
|
||||
{0x00, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x00}, // 034 -> 0x22 (")
|
||||
{0x14, 0x7F, 0x7F, 0x14, 0x7F, 0x7F, 0x14, 0x00}, // 035 -> 0x23 (#)
|
||||
{0x00, 0x24, 0x2A, 0x7F, 0x7F, 0x2A, 0x12, 0x00}, // 036 -> 0x24 ($)
|
||||
{0x46, 0x66, 0x30, 0x18, 0x0C, 0x66, 0x62, 0x00}, // 037 -> 0x25 (%)
|
||||
{0x30, 0x7A, 0x4F, 0x5D, 0x37, 0x7A, 0x48, 0x00}, // 038 -> 0x26 (&)
|
||||
{0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0x00}, // 039 -> 0x27 (')
|
||||
{0x00, 0x00, 0x1C, 0x3E, 0x63, 0x41, 0x00, 0x00}, // 040 -> 0x28 (()
|
||||
{0x00, 0x00, 0x41, 0x63, 0x3E, 0x1C, 0x00, 0x00}, // 041 -> 0x29 ())
|
||||
{0x08, 0x2A, 0x3E, 0x1C, 0x1C, 0x3E, 0x2A, 0x08}, // 042 -> 0x2a (*)
|
||||
{0x00, 0x08, 0x08, 0x3E, 0x3E, 0x08, 0x08, 0x00}, // 043 -> 0x2b (+)
|
||||
{0x00, 0x00, 0x80, 0xE0, 0x60, 0x00, 0x00, 0x00}, // 044 -> 0x2c (,)
|
||||
{0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00}, // 045 -> 0x2d (-)
|
||||
{0x00, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00}, // 046 -> 0x2e (.)
|
||||
{0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // 047 -> 0x2f (/)
|
||||
{0x3E, 0x7F, 0x51, 0x49, 0x45, 0x7F, 0x3E, 0x00}, // 048 -> 0x30 (0)
|
||||
{0x00, 0x40, 0x42, 0x7F, 0x7F, 0x40, 0x40, 0x00}, // 049 -> 0x31 (1)
|
||||
{0x00, 0x72, 0x7B, 0x49, 0x49, 0x6F, 0x66, 0x00}, // 050 -> 0x32 (2)
|
||||
{0x00, 0x22, 0x63, 0x49, 0x49, 0x7F, 0x36, 0x00}, // 051 -> 0x33 (3)
|
||||
{0x18, 0x1C, 0x16, 0x53, 0x7F, 0x7F, 0x50, 0x00}, // 052 -> 0x34 (4)
|
||||
{0x00, 0x2F, 0x6F, 0x49, 0x49, 0x79, 0x33, 0x00}, // 053 -> 0x35 (5)
|
||||
{0x00, 0x3E, 0x7F, 0x49, 0x49, 0x7B, 0x32, 0x00}, // 054 -> 0x36 (6)
|
||||
{0x00, 0x03, 0x03, 0x71, 0x79, 0x0F, 0x07, 0x00}, // 055 -> 0x37 (7)
|
||||
{0x00, 0x36, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}, // 056 -> 0x38 (8)
|
||||
{0x00, 0x26, 0x6F, 0x49, 0x49, 0x7F, 0x3E, 0x00}, // 057 -> 0x39 (9)
|
||||
{0x00, 0x00, 0x00, 0x6C, 0x6C, 0x00, 0x00, 0x00}, // 058 -> 0x3a (:)
|
||||
{0x00, 0x00, 0x80, 0xEC, 0x6C, 0x00, 0x00, 0x00}, // 059 -> 0x3b (;)
|
||||
{0x00, 0x08, 0x1C, 0x36, 0x63, 0x41, 0x00, 0x00}, // 060 -> 0x3c (<)
|
||||
{0x00, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x00}, // 061 -> 0x3d (=)
|
||||
{0x00, 0x41, 0x63, 0x36, 0x1C, 0x08, 0x00, 0x00}, // 062 -> 0x3e (>)
|
||||
{0x00, 0x06, 0x07, 0x51, 0x59, 0x0F, 0x06, 0x00}, // 063 -> 0x3f (?)
|
||||
{0x3E, 0x7F, 0x41, 0x5D, 0x5D, 0x5F, 0x1E, 0x00}, // 064 -> 0x40 (@)
|
||||
{0x00, 0x7C, 0x7E, 0x13, 0x13, 0x7E, 0x7C, 0x00}, // 065 -> 0x41 (A)
|
||||
{0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}, // 066 -> 0x42 (B)
|
||||
{0x1C, 0x3E, 0x63, 0x41, 0x41, 0x63, 0x22, 0x00}, // 067 -> 0x43 (C)
|
||||
{0x41, 0x7F, 0x7F, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 068 -> 0x44 (D)
|
||||
{0x41, 0x7F, 0x7F, 0x49, 0x5D, 0x41, 0x63, 0x00}, // 069 -> 0x45 (E)
|
||||
{0x41, 0x7F, 0x7F, 0x49, 0x1D, 0x01, 0x03, 0x00}, // 070 -> 0x46 (F)
|
||||
{0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 071 -> 0x47 (G)
|
||||
{0x00, 0x7F, 0x7F, 0x08, 0x08, 0x7F, 0x7F, 0x00}, // 072 -> 0x48 (H)
|
||||
{0x00, 0x41, 0x41, 0x7F, 0x7F, 0x41, 0x41, 0x00}, // 073 -> 0x49 (I)
|
||||
{0x30, 0x70, 0x40, 0x41, 0x7F, 0x3F, 0x01, 0x00}, // 074 -> 0x4a (J)
|
||||
{0x41, 0x7F, 0x7F, 0x08, 0x1C, 0x77, 0x63, 0x00}, // 075 -> 0x4b (K)
|
||||
{0x41, 0x7F, 0x7F, 0x41, 0x40, 0x60, 0x70, 0x00}, // 076 -> 0x4c (L)
|
||||
{0x7F, 0x7F, 0x0E, 0x1C, 0x0E, 0x7F, 0x7F, 0x00}, // 077 -> 0x4d (M)
|
||||
{0x7F, 0x7F, 0x06, 0x0C, 0x18, 0x7F, 0x7F, 0x00}, // 078 -> 0x4e (N)
|
||||
{0x1C, 0x3E, 0x63, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 079 -> 0x4f (O)
|
||||
{0x41, 0x7F, 0x7F, 0x49, 0x09, 0x0F, 0x06, 0x00}, // 080 -> 0x50 (P)
|
||||
{0x3C, 0x7E, 0x43, 0x51, 0x33, 0x6E, 0x5C, 0x00}, // 081 -> 0x51 (Q)
|
||||
{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 082 -> 0x52 (R)
|
||||
{0x00, 0x26, 0x6F, 0x49, 0x49, 0x7B, 0x32, 0x00}, // 083 -> 0x53 (S)
|
||||
{0x00, 0x03, 0x41, 0x7F, 0x7F, 0x41, 0x03, 0x00}, // 084 -> 0x54 (T)
|
||||
{0x00, 0x3F, 0x7F, 0x40, 0x40, 0x7F, 0x3F, 0x00}, // 085 -> 0x55 (U)
|
||||
{0x00, 0x1F, 0x3F, 0x60, 0x60, 0x3F, 0x1F, 0x00}, // 086 -> 0x56 (V)
|
||||
{0x7F, 0x7F, 0x30, 0x18, 0x30, 0x7F, 0x7F, 0x00}, // 087 -> 0x57 (W)
|
||||
{0x61, 0x73, 0x1E, 0x0C, 0x1E, 0x73, 0x61, 0x00}, // 088 -> 0x58 (X)
|
||||
{0x00, 0x07, 0x4F, 0x78, 0x78, 0x4F, 0x07, 0x00}, // 089 -> 0x59 (Y)
|
||||
{0x47, 0x63, 0x71, 0x59, 0x4D, 0x67, 0x73, 0x00}, // 090 -> 0x5a (Z)
|
||||
{0x00, 0x00, 0x7F, 0x7F, 0x41, 0x41, 0x00, 0x00}, // 091 -> 0x5b ([)
|
||||
{0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00}, // 092 -> 0x5c (\)
|
||||
{0x00, 0x00, 0x41, 0x41, 0x7F, 0x7F, 0x00, 0x00}, // 093 -> 0x5d (])
|
||||
{0x08, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x08, 0x00}, // 094 -> 0x5e (^)
|
||||
{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}, // 095 -> 0x5f (_)
|
||||
{0x00, 0x00, 0x01, 0x03, 0x06, 0x04, 0x00, 0x00}, // 096 -> 0x60 (`)
|
||||
{0x20, 0x74, 0x54, 0x54, 0x3C, 0x78, 0x40, 0x00}, // 097 -> 0x61 (a)
|
||||
{0x41, 0x7F, 0x3F, 0x44, 0x44, 0x7C, 0x38, 0x00}, // 098 -> 0x62 (b)
|
||||
{0x00, 0x38, 0x7C, 0x44, 0x44, 0x6C, 0x28, 0x00}, // 099 -> 0x63 (c)
|
||||
{0x38, 0x7C, 0x44, 0x45, 0x3F, 0x7F, 0x40, 0x00}, // 100 -> 0x64 (d)
|
||||
{0x00, 0x38, 0x7C, 0x54, 0x54, 0x5C, 0x18, 0x00}, // 101 -> 0x65 (e)
|
||||
{0x00, 0x48, 0x7E, 0x7F, 0x49, 0x03, 0x02, 0x00}, // 102 -> 0x66 (f)
|
||||
{0x00, 0x98, 0xBC, 0xA4, 0xA4, 0xFC, 0x7C, 0x00}, // 103 -> 0x67 (g)
|
||||
{0x41, 0x7F, 0x7F, 0x08, 0x04, 0x7C, 0x78, 0x00}, // 104 -> 0x68 (h)
|
||||
{0x00, 0x00, 0x44, 0x7D, 0x7D, 0x40, 0x00, 0x00}, // 105 -> 0x69 (i)
|
||||
{0x00, 0x60, 0xE0, 0x80, 0x84, 0xFD, 0x7D, 0x00}, // 106 -> 0x6a (j)
|
||||
{0x41, 0x7F, 0x7F, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 107 -> 0x6b (k)
|
||||
{0x00, 0x00, 0x41, 0x7F, 0x7F, 0x40, 0x00, 0x00}, // 108 -> 0x6c (l)
|
||||
{0x78, 0x7C, 0x0C, 0x38, 0x0C, 0x7C, 0x78, 0x00}, // 109 -> 0x6d (m)
|
||||
{0x04, 0x7C, 0x78, 0x04, 0x04, 0x7C, 0x78, 0x00}, // 110 -> 0x6e (n)
|
||||
{0x00, 0x38, 0x7C, 0x44, 0x44, 0x7C, 0x38, 0x00}, // 111 -> 0x6f (o)
|
||||
{0x84, 0xFC, 0xF8, 0xA4, 0x24, 0x3C, 0x18, 0x00}, // 112 -> 0x70 (p)
|
||||
{0x18, 0x3C, 0x24, 0xA4, 0xF8, 0xFC, 0x84, 0x00}, // 113 -> 0x71 (q)
|
||||
{0x44, 0x7C, 0x78, 0x4C, 0x04, 0x0C, 0x08, 0x00}, // 114 -> 0x72 (r)
|
||||
{0x00, 0x48, 0x5C, 0x54, 0x54, 0x74, 0x20, 0x00}, // 115 -> 0x73 (s)
|
||||
{0x00, 0x04, 0x3F, 0x7F, 0x44, 0x64, 0x20, 0x00}, // 116 -> 0x74 (t)
|
||||
{0x00, 0x3C, 0x7C, 0x40, 0x40, 0x7C, 0x7C, 0x00}, // 117 -> 0x75 (u)
|
||||
{0x00, 0x1C, 0x3C, 0x60, 0x60, 0x3C, 0x1C, 0x00}, // 118 -> 0x76 (v)
|
||||
{0x3C, 0x7C, 0x60, 0x38, 0x60, 0x7C, 0x3C, 0x00}, // 119 -> 0x77 (w)
|
||||
{0x44, 0x6C, 0x38, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 120 -> 0x78 (x)
|
||||
{0x00, 0x9C, 0xBC, 0xA0, 0xA0, 0xFC, 0x7C, 0x00}, // 121 -> 0x79 (y)
|
||||
{0x00, 0x4C, 0x64, 0x74, 0x5C, 0x4C, 0x64, 0x00}, // 122 -> 0x7a (z)
|
||||
{0x00, 0x08, 0x08, 0x3E, 0x77, 0x41, 0x41, 0x00}, // 123 -> 0x7b ({)
|
||||
{0x00, 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00}, // 124 -> 0x7c (|)
|
||||
{0x00, 0x41, 0x41, 0x77, 0x3E, 0x08, 0x08, 0x00}, // 125 -> 0x7d (})
|
||||
{0x10, 0x18, 0x08, 0x18, 0x10, 0x18, 0x08, 0x00}, // 126 -> 0x7e (~)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 127 -> 0x7F [DEL]
|
||||
/* EXTENDED ASCII CHARACTERS */
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 128 -> 0x80 (Ç)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 129 -> 0x81 (ü)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 130 -> 0x82 (é)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 131 -> 0x83 (â)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 132 -> 0x84 (ä)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 133 -> 0x85 (à)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 134 -> 0x86 (å)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 135 -> 0x87 (ç)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 136 -> 0x88 (ê)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 137 -> 0x89 (ë)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 138 -> 0x8A (è)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 139 -> 0x8B (ï)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 140 -> 0x8C (î)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 141 -> 0x8D (ì)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 142 -> 0x8E (Ä)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 143 -> 0x8F (Å)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 144 -> 0x90 (É)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 145 -> 0x91 (æ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 146 -> 0x92 (Æ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 147 -> 0x93 (ô)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 148 -> 0x94 (ö)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 149 -> 0x95 (ò)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 150 -> 0x96 (û)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 151 -> 0x97 (ù)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 152 -> 0x98 (ÿ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 153 -> 0x99 (Ö)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 154 -> 0x9A (Ü)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 155 -> 0x9B (ø)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 156 -> 0x9C (£)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 157 -> 0x9D (Ø)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 158 -> 0x9E (×)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 159 -> 0x9F (ƒ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 160 -> 0xA0 (á)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 161 -> 0xA1 (í)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 162 -> 0xA2 (ó)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 163 -> 0xA3 (ú)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 164 -> 0xA4 (ñ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 165 -> 0xA5 (Ñ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 166 -> 0xA6 (ª)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 167 -> 0xA7 (º)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 168 -> 0xA8 (¿)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 169 -> 0xA9 (®)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 170 -> 0xAA (¬)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 171 -> 0xAB (½)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 172 -> 0xAC (¼)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 173 -> 0xAD (¡)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 174 -> 0xAE («)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 175 -> 0xAF (»)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 176 -> 0xB0 (░)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 177 -> 0xB1 (▒)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 178 -> 0xB2 (▓)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 179 -> 0xB3 (│)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 180 -> 0xB4 (┤)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 181 -> 0xB5 (Á)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 182 -> 0xB6 (Â)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 183 -> 0xB7 (À)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 184 -> 0xB8 (©)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 185 -> 0xB9 (╣)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 186 -> 0xBA (║)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 187 -> 0xBB (╗)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 188 -> 0xBC (╝)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 189 -> 0xBD (¢)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 190 -> 0xBE (¥)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 191 -> 0xBF (┐)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 192 -> 0xC0 (└)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 193 -> 0xC1 (┴)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 194 -> 0xC2 (┬)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 195 -> 0xC3 (├)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 196 -> 0xC4 (─)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 197 -> 0xC5 (┼)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 198 -> 0xC6 (ã)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 199 -> 0xC7 (Ã)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 200 -> 0xC8 (╚)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 201 -> 0xC9 (╔)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 202 -> 0xCA (╩)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 203 -> 0xCB (╦)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 204 -> 0xCC (╠)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 205 -> 0xCD (═)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 206 -> 0xCE (╬)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 207 -> 0xCF (¤)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 208 -> 0xD0 (ð)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 209 -> 0xD1 (Ð)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 210 -> 0xD2 (Ê)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 211 -> 0xD3 (Ë)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 212 -> 0xD4 (È)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 213 -> 0xD5 (ı)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 214 -> 0xD6 (Í)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 215 -> 0xD7 (Î)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 216 -> 0xD8 (Ï)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 217 -> 0xD9 (┘)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 218 -> 0xDA (┌)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 219 -> 0xDB (█)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 220 -> 0xDC (▄)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 221 -> 0xDD (¦)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 222 -> 0xDE (Ì)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 223 -> 0xDF (▀)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 224 -> 0xE0 (Ó)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 225 -> 0xE1 (ß)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 226 -> 0xE2 (Ô)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 227 -> 0xE3 (Ò)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 228 -> 0xE4 (õ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 229 -> 0xE5 (Õ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 230 -> 0xE6 (µ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 231 -> 0xE7 (þ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 232 -> 0xE8 (Þ)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 233 -> 0xE9 (Ú)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 234 -> 0xEA (Û)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 235 -> 0xEB (Ù)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 236 -> 0xEC (ý)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 237 -> 0xED (Ý)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 238 -> 0xEE (¯)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 239 -> 0xEF (´)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 240 -> 0xF0 (≡)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 241 -> 0xF1 (±)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 242 -> 0xF2 (‗)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 243 -> 0xF3 (¾)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 244 -> 0xF4 (¶)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 245 -> 0xF5 (§)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 246 -> 0xF6 (÷)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 247 -> 0xF7 (¸)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 248 -> 0xF8 (°)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 249 -> 0xF9 (¨)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 250 -> 0xFA (·)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 251 -> 0xFB (¹)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 252 -> 0xFC (³)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 253 -> 0xFD (²)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 254 -> 0xFE (■)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // 255 -> 0xFF ( )
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
|
||||
Output result
|
||||
Mainly output as the quality and number of each particles with different size
|
||||
per unit volume, the unit volume of particle number is 0.1L and the unit of
|
||||
mass concentration is μ g/m³.
|
||||
There are two options for digital output: passive and active. Default mode
|
||||
is active after power up. In this mode sensor would send serial data to the
|
||||
host automatically .The active mode is divided into two sub-modes: stable
|
||||
mode and fast mode. If the concentration change is small the sensor
|
||||
would run at stable mode with the real interval of 2.3s.And if the change is
|
||||
big the sensor would be changed to fast mode automatically with the
|
||||
interval of 200~800ms, the higher of the concentration, the shorter of the
|
||||
interval.
|
||||
|
||||
Circuit Attentions
|
||||
1) DC 5V power supply is needed because the FAN should be driven by 5V.
|
||||
But the high level of data pin is 3.3V. Level conversion unit should be
|
||||
used if the power of host MCU is 5V.
|
||||
2) The SET and RESET pins are pulled up inside so they should not be
|
||||
connected if without usage.
|
||||
3) PIN7 and PIN8 should not be connected.
|
||||
4) Stable data should be got at least 30 seconds after the sensor wakeup
|
||||
from the sleep mode because of the fan’s performance.
|
||||
Installation Attentions
|
||||
1) Metal shell is connected to the GND so be careful not to let it shorted with
|
||||
the other parts of circuit except GND.
|
||||
2) The best way of install is making the plane of inset and outset closely to
|
||||
the plane of the host. Or some shield should be placed between inset and
|
||||
outset in order to prevent the air flow from inner loop.
|
||||
3) The blowhole in the shell of the host should not be smaller than the inset.
|
||||
4) The sensor should not be installed in the air flow way of the air cleaner or
|
||||
should be shielded by some structure.
|
||||
5) The sensor should be installed at least 20cm higher than the grand in
|
||||
order to prevent it from blocking by the flock dust.
|
||||
6) Do not break up the sensor.
|
||||
Other Attentions
|
||||
1) Only the consistency of all the PM sensors of PLANTOWER is promised
|
||||
and ensured. And the sensor should not be checked with any third party
|
||||
equipment.
|
||||
2) The sensor is usually used in the common indoor environment. So some
|
||||
protection must be added if using in the conditions as followed:
|
||||
a) The time of concentration ≥300μ g/m³ is longer than 50% of the
|
||||
whole year or concentration≥500μ g/m³ is longer than20% of the
|
||||
whole year.
|
||||
b) Kitchen
|
||||
c) Water mist condition such as bathroom or hot spring.
|
||||
d) outdoor
|
||||
Appendix I:PMS7003 transport protocol-Active Mode
|
||||
Default baud rate:9600bps Check bit:None Stop bit:1 bit
|
||||
32 Bytes
|
||||
Start character 1 0x42 (Fixed)
|
||||
Start character2 0x4d (Fixed)
|
||||
Frame length high
|
||||
8 bits
|
||||
…… Frame length=2x13+2(data+check bytes)
|
||||
Frame length low 8
|
||||
bits
|
||||
……
|
||||
Data 1 high 8 bits …… Data1 refers to PM1.0 concentration unit
|
||||
μ g/m3(CF=1,standard particle)*
|
||||
Data 1 low 8 bits ……
|
||||
Data2 high 8 bits …… Data2 refers to PM2.5 concentration unit
|
||||
μ g/m3(CF=1,standard particle)
|
||||
Data2 low 8 bits ……
|
||||
Data3 high 8 bits …… Data3 refers to PM10 concentration unit
|
||||
μ g/m3(CF=1,standard particle)
|
||||
Data3 low 8 bits ……
|
||||
Data4 high 8 bits …… Data4 refers to PM1.0 concentration unit *
|
||||
μ g/m3(under atmospheric environment)
|
||||
Data4 low 8 bits ……
|
||||
Data5 high 8 bits …… Data 5 refers to PM2.5 concentration unit
|
||||
μ g/m3(under atmospheric environment)
|
||||
Data5 low 8 bits ……
|
||||
Data6 high 8 bits ……. Data 6 refers to concentration unit (under
|
||||
atmospheric environment) μ g/m3 Data6 low 8 bits ……
|
||||
Data7 high 8 bits …… Data7 indicates the number of
|
||||
particles with diameter beyond 0.3 um
|
||||
in 0.1 L of air. Data7 low 8 bits ……
|
||||
Data8 high 8 bits …… Data 8 indicates the number of
|
||||
particles with diameter beyond 0.5 um
|
||||
in 0.1 L of air. Data8 low 8 bits ……
|
||||
Data9 high 8 bits …… Data 9 indicates the number of
|
||||
particles with diameter beyond 1.0 um
|
||||
in 0.1 L of air.
|
||||
Data9 low 8 bits ……
|
||||
2016 product data manual of PLANTOWER
|
||||
Data10 high 8 bits …… Data10 indicates the number of
|
||||
particles with diameter beyond 2.5 um
|
||||
in 0.1 L of air.
|
||||
Data10 low 8 bits ……
|
||||
Data11 high 8 bits …… Data11 indicates the number of
|
||||
particles with diameter beyond 5.0 um
|
||||
Data11 low 8 bits …… in 0.1 L of air.
|
||||
Data12 high 8 bits …… Data12 indicates the number of
|
||||
particles with diameter beyond 10 um
|
||||
Data12 low 8 bits …… in 0.1 L of air.
|
||||
Data13 high 8 bits …… Data13 Reserved
|
||||
Data13 low 8 bits ……
|
||||
Data and check
|
||||
high 8 bits
|
||||
…… Check code=Start character1+ Start
|
||||
character2+……..+data13
|
||||
Low 8 bits
|
||||
Data and check
|
||||
low 8 bits
|
||||
……
|
||||
Note: CF=1 should be used in the factory environment
|
||||
.
|
||||
2016 product data manual of PLANTOWER
|
||||
Appendix II:PMS7003 transport protocol-Passive Mode
|
||||
Default baud rate:9600bps Check bit:None Stop bit:1 bit
|
||||
Host Protocol
|
||||
Start Byte
|
||||
1
|
||||
Start Byte
|
||||
2
|
||||
Command Data 1 Data 2 Verify Byte
|
||||
1
|
||||
Verify Byte
|
||||
2
|
||||
0x42 0x4d CMD DATAH DATAL LRCH LRCL
|
||||
1. Command Definition
|
||||
CMD DATAH DATAL
|
||||
0xe2 X X Read in passive
|
||||
mode
|
||||
0xe1 X 00H-passive
|
||||
01H-active
|
||||
Change mode
|
||||
0xe4 X 00H-sleep
|
||||
01H-wakeup
|
||||
Sleep set
|
||||
2. Answer
|
||||
0xe2: 32 bytes , same as appendix I
|
||||
3. Verify Bytes :
|
||||
Add of all the bytes except verify bytes.
|
||||
@@ -0,0 +1,17 @@
|
||||
[build]
|
||||
target = "xtensa-esp32-espidf"
|
||||
|
||||
[target.xtensa-esp32-espidf]
|
||||
linker = "ldproxy"
|
||||
# runner = "espflash flash --monitor --speed 921600 # or even 1500000"
|
||||
runner = "espflash flash --monitor --baud 921600"
|
||||
rustflags = [ "--cfg", "espidf_time64"]
|
||||
|
||||
[unstable]
|
||||
build-std = ["std", "panic_abort"]
|
||||
|
||||
[env]
|
||||
MCU="esp32"
|
||||
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
|
||||
ESP_IDF_VERSION = "v5.2.3"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
((nil
|
||||
(lsp-rust-analyzer-exclude-dirs . ["./.embuild/"])))
|
||||
@@ -0,0 +1,4 @@
|
||||
/.vscode
|
||||
/.embuild
|
||||
/target
|
||||
/Cargo.lock
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "pmme-device"
|
||||
version = "0.1.0"
|
||||
authors = ["Joseph Ferano <joseph@ferano.io>"]
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
rust-version = "1.77"
|
||||
|
||||
[[bin]]
|
||||
name = "pmme-device"
|
||||
harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
|
||||
[profile.dev]
|
||||
debug = true # Symbols are nice and they don't increase the size on Flash
|
||||
opt-level = "z"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
experimental = ["esp-idf-svc/experimental"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-driver", "embassy-sync"] }
|
||||
anyhow = "1.0.98"
|
||||
embedded-graphics = "0.8.1"
|
||||
ssd1306 = "0.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
embuild = "0.33"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
embuild::espidf::sysenv::output();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export PATH="/home/joe/.local/share/rustup/toolchains/esp/xtensa-esp-elf/esp-14.2.0_20240906/xtensa-esp-elf/bin:$PATH"
|
||||
export LIBCLANG_PATH="/home/joe/.local/share/rustup/toolchains/esp/xtensa-esp32-elf-clang/esp-19.1.2_20250225/esp-clang/lib"
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "esp"
|
||||
@@ -0,0 +1,40 @@
|
||||
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
# CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="./partitions.csv"
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_BLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_HOST_ENABLED=y
|
||||
CONFIG_WIFI_PROV_SCHEME_BLE=y
|
||||
# CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
|
||||
# CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
|
||||
|
||||
# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y
|
||||
# CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y
|
||||
# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
|
||||
|
||||
# Increase stack sizes
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=8192
|
||||
|
||||
|
||||
# CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
|
||||
# CONFIG_LOG_COLORS=y
|
||||
CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
|
||||
CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y
|
||||
|
||||
|
||||
CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y
|
||||
CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION=y
|
||||
# CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=10
|
||||
# CONFIG_ESP_PHY_REDUCE_TX_POWER=y
|
||||
|
||||
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
|
||||
# This allows to use 1 ms granularity for thread sleeps (10 ms by default).
|
||||
#CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
# Workaround for https://github.com/espressif/esp-idf/issues/7631
|
||||
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
|
||||
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::Pms7003Data;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::info;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use embedded_graphics::draw_target::DrawTarget;
|
||||
use embedded_graphics::{
|
||||
image::{Image, ImageRaw},
|
||||
mono_font::{ascii::FONT_10X20, MonoTextStyle},
|
||||
pixelcolor::BinaryColor,
|
||||
prelude::Point,
|
||||
text::Text,
|
||||
Drawable,
|
||||
};
|
||||
use esp_idf_svc::hal::{delay::FreeRtos, i2c::I2cDriver};
|
||||
use ssd1306::{
|
||||
mode::DisplayConfig, prelude::DisplayRotation, size::DisplaySize128x64, I2CDisplayInterface,
|
||||
Ssd1306,
|
||||
};
|
||||
|
||||
pub fn oled_task(pm_data: Arc<Mutex<Pms7003Data>>, i2c: I2cDriver) -> Result<()> {
|
||||
info!("Staring OLED Task");
|
||||
|
||||
let interface = I2CDisplayInterface::new(i2c);
|
||||
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||
.into_buffered_graphics_mode();
|
||||
|
||||
display.init().unwrap();
|
||||
|
||||
let raw: ImageRaw<BinaryColor> = ImageRaw::new(include_bytes!("../rust.raw"), 64);
|
||||
let im = Image::new(&raw, Point::new(32, 0));
|
||||
|
||||
im.draw(&mut display).unwrap();
|
||||
display
|
||||
.flush()
|
||||
.map_err(|e| anyhow!("Could not flush display! {:?}", e))?;
|
||||
|
||||
FreeRtos::delay_ms(2000);
|
||||
loop {
|
||||
let current_pm_data = pm_data.lock().unwrap().clone();
|
||||
display
|
||||
.clear(BinaryColor::Off)
|
||||
.map_err(|e| anyhow!("Could not clear display! {:?}", e))?;
|
||||
Text::new(
|
||||
&format!("PM 2.5: {}", current_pm_data.pm2_5_atm),
|
||||
Point::new(20, 40),
|
||||
MonoTextStyle::new(&FONT_10X20, BinaryColor::On),
|
||||
)
|
||||
.draw(&mut display)
|
||||
.map_err(|e| anyhow!("Could not draw text! {:?}", e))?;
|
||||
display
|
||||
.flush()
|
||||
.map_err(|e| anyhow!("Could not flush display! {:?}", e))?;
|
||||
FreeRtos::delay_ms(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
pub mod display;
|
||||
pub mod sensors;
|
||||
pub mod wifi;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Pms7003Data {
|
||||
// PM concentrations (CF=1, standard particle) in μg/m³
|
||||
pub pm1_0_cf1: u16,
|
||||
pub pm2_5_cf1: u16,
|
||||
pub pm10_cf1: u16,
|
||||
|
||||
// PM concentrations (atmospheric environment) in μg/m³
|
||||
pub pm1_0_atm: u16,
|
||||
pub pm2_5_atm: u16,
|
||||
pub pm10_atm: u16,
|
||||
|
||||
// Particle counts (particles per 0.1L air)
|
||||
pub particles_0_3um: u16,
|
||||
pub particles_0_5um: u16,
|
||||
pub particles_1_0um: u16,
|
||||
pub particles_2_5um: u16,
|
||||
pub particles_5_0um: u16,
|
||||
pub particles_10um: u16,
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use anyhow::{bail, Result};
|
||||
use log::info;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
use esp_idf_svc::{
|
||||
eventloop::EspSystemEventLoop,
|
||||
hal::{
|
||||
delay::FreeRtos,
|
||||
gpio::{self, Level, PinDriver},
|
||||
i2c::{I2cConfig, I2cDriver},
|
||||
peripherals::Peripherals,
|
||||
prelude::FromValueType,
|
||||
uart::{self, UartDriver},
|
||||
units::Hertz,
|
||||
},
|
||||
log::EspLogger,
|
||||
nvs::EspDefaultNvsPartition,
|
||||
sys::CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT,
|
||||
wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi},
|
||||
};
|
||||
use pmme_device::{display::oled_task, sensors::sensors_task, wifi::wifi_task, Pms7003Data};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// It is necessary to call this function once. Otherwise some patches to the runtime
|
||||
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
|
||||
esp_idf_svc::sys::link_patches();
|
||||
|
||||
// Initialize NVS
|
||||
EspLogger::initialize_default();
|
||||
let nvs = EspDefaultNvsPartition::take()?;
|
||||
|
||||
FreeRtos::delay_ms(100);
|
||||
|
||||
let peripherals = Peripherals::take()?;
|
||||
FreeRtos::delay_ms(100);
|
||||
|
||||
let mut button = PinDriver::input(peripherals.pins.gpio5)?;
|
||||
let mut led = PinDriver::output(peripherals.pins.gpio19)?;
|
||||
led.set_low()?;
|
||||
|
||||
let i2c = peripherals.i2c0;
|
||||
let sda = peripherals.pins.gpio21;
|
||||
let scl = peripherals.pins.gpio22;
|
||||
|
||||
let config = I2cConfig::new().baudrate(100.kHz().into());
|
||||
let i2c = I2cDriver::new(i2c, sda, scl, &config)?;
|
||||
|
||||
let tx = peripherals.pins.gpio17;
|
||||
let rx = peripherals.pins.gpio16;
|
||||
|
||||
let config = uart::config::Config::new().baudrate(Hertz(9600));
|
||||
let uart = UartDriver::new(
|
||||
peripherals.uart2,
|
||||
tx,
|
||||
rx,
|
||||
Option::<gpio::Gpio0>::None,
|
||||
Option::<gpio::Gpio1>::None,
|
||||
&config,
|
||||
)?;
|
||||
|
||||
let sysloop = EspSystemEventLoop::take()?;
|
||||
let sysloop_clone = sysloop.clone();
|
||||
let modem = peripherals.modem;
|
||||
|
||||
let pm_data = Arc::new(Mutex::new(Pms7003Data::default()));
|
||||
let oled_arc = Arc::clone(&pm_data);
|
||||
let sensors_arc = Arc::clone(&pm_data);
|
||||
// let wifi_arc = Arc::clone(&pm_data);
|
||||
|
||||
let handles = vec![
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = oled_task(oled_arc, i2c) {
|
||||
log::error!("OLED Task Error: {:?}", e);
|
||||
}
|
||||
}),
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = sensors_task(sensors_arc, &uart) {
|
||||
log::error!("PM Sensor Task Error: {:?}", e);
|
||||
}
|
||||
}),
|
||||
std::thread::Builder::new()
|
||||
// NOTE: According to CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT, this was set to 3072 and the wifi task
|
||||
// was getting a stack overflow
|
||||
.stack_size(12288)
|
||||
.spawn(move || {
|
||||
unsafe {
|
||||
let free_stack =
|
||||
esp_idf_svc::sys::uxTaskGetStackHighWaterMark(std::ptr::null_mut());
|
||||
let stack_size = CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT;
|
||||
log::info!(
|
||||
"Pthread stack size configured: {} bytes, Current free stack: {} bytes",
|
||||
stack_size,
|
||||
free_stack * 4
|
||||
);
|
||||
}
|
||||
if let Err(e) = wifi_task(modem, sysloop_clone, nvs) {
|
||||
log::error!("PM Wifi Task Error: {:?}", e);
|
||||
}
|
||||
})?,
|
||||
thread::spawn(move || {
|
||||
let mut is_pressed = false;
|
||||
let mut curr_state = Level::High;
|
||||
loop {
|
||||
if curr_state == Level::High && button.is_low() {
|
||||
led.set_high().unwrap();
|
||||
curr_state = Level::Low;
|
||||
is_pressed = true;
|
||||
} else if curr_state == Level::Low && button.is_high() {
|
||||
led.set_low().unwrap();
|
||||
curr_state = Level::High;
|
||||
is_pressed = true;
|
||||
}
|
||||
if is_pressed {
|
||||
FreeRtos::delay_ms(100);
|
||||
} else {
|
||||
FreeRtos::delay_ms(10);
|
||||
}
|
||||
}
|
||||
}),
|
||||
];
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use crate::Pms7003Data;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::{info, warn};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use esp_idf_svc::hal::{
|
||||
delay::{FreeRtos, BLOCK},
|
||||
uart::UartDriver,
|
||||
};
|
||||
|
||||
// u16::from_be_bytes is too verbose
|
||||
fn get_u16(buf: &[u8]) -> u16 {
|
||||
((buf[0] as u16) << 8) | buf[1] as u16
|
||||
}
|
||||
|
||||
fn read_pm_data(buf: &[u8; 32]) -> Option<Pms7003Data> {
|
||||
let checksum: u16 = buf.iter().take(29).copied().map(u16::from).sum();
|
||||
let csum_target = get_u16(&buf[30..32]);
|
||||
if checksum != csum_target {
|
||||
warn!("Checksum from PMS7003 READ does not match, skipping read");
|
||||
return None;
|
||||
}
|
||||
Some(Pms7003Data {
|
||||
pm1_0_cf1: get_u16(&buf[ 4..6 ]),
|
||||
pm2_5_cf1: get_u16(&buf[ 6..8 ]),
|
||||
pm10_cf1: get_u16(&buf[ 8..10]),
|
||||
pm1_0_atm: get_u16(&buf[10..12]),
|
||||
pm2_5_atm: get_u16(&buf[12..14]),
|
||||
pm10_atm: get_u16(&buf[14..16]),
|
||||
particles_0_3um: get_u16(&buf[16..18]),
|
||||
particles_0_5um: get_u16(&buf[18..20]),
|
||||
particles_1_0um: get_u16(&buf[20..22]),
|
||||
particles_2_5um: get_u16(&buf[22..24]),
|
||||
particles_5_0um: get_u16(&buf[24..26]),
|
||||
particles_10um: get_u16(&buf[26..28]),
|
||||
})
|
||||
}
|
||||
|
||||
fn send_pms7003_command(uart: &UartDriver, cmd: u8, datah: u8, datal: u8) -> Result<()> {
|
||||
let mut command = [0x42u8, 0x4D, cmd, datah, datal, 0, 0];
|
||||
|
||||
let checksum: u16 = command.iter().take(5).copied().map(u16::from).sum();
|
||||
command[5] = ((checksum >> 8) & 0xFF) as u8;
|
||||
command[6] = (checksum & 0xFF) as u8;
|
||||
|
||||
uart.write(&command)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sensors_task(pm_data: Arc<Mutex<Pms7003Data>>, uart: &UartDriver) -> Result<()> {
|
||||
info!("Staring UART PM Sensor Task");
|
||||
|
||||
send_pms7003_command(uart, 0xE1, 0x00, 0x00)?;
|
||||
|
||||
loop {
|
||||
send_pms7003_command(uart, 0xE2, 0x00, 0x00)?;
|
||||
|
||||
let mut buf = [0_u8; 32];
|
||||
let len = uart.read(&mut buf, BLOCK)?;
|
||||
if len > 0 {
|
||||
if let Some(data) = read_pm_data(&buf) {
|
||||
let mut pm_data = pm_data.lock().unwrap();
|
||||
*pm_data = data;
|
||||
}
|
||||
} else {
|
||||
// info!("No bytes read");
|
||||
}
|
||||
uart.clear_rx()?;
|
||||
FreeRtos::delay_ms(5000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use anyhow::{bail, Result};
|
||||
use log::info;
|
||||
|
||||
use esp_idf_svc::{
|
||||
eventloop::EspSystemEventLoop, hal::{delay::FreeRtos, peripheral}, http::{server::EspHttpServer, Method}, io::Write, nvs::EspDefaultNvsPartition, sys::{
|
||||
esp, esp_err_t, wifi_prov_event_handler_t, wifi_prov_mgr_config_t, wifi_prov_mgr_deinit,
|
||||
wifi_prov_mgr_init, wifi_prov_mgr_is_provisioned, wifi_prov_mgr_start_provisioning,
|
||||
wifi_prov_mgr_stop_provisioning, wifi_prov_mgr_wait, wifi_prov_scheme_ble,
|
||||
wifi_prov_security_WIFI_PROV_SECURITY_1, wifi_prov_security_t, EspError,
|
||||
}, wifi::{AccessPointConfiguration, AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi}
|
||||
};
|
||||
use std::ffi::c_void;
|
||||
use std::ffi::CString;
|
||||
use std::ptr;
|
||||
// NOTE: We decided against standard BLE provisioning. Refer to this in case we reconsider
|
||||
// https://github.com/mzakharo/esp32_rust_provisioning/blob/master/src/main.rs
|
||||
|
||||
pub fn wifi_task(
|
||||
modem: impl peripheral::Peripheral<P = esp_idf_svc::hal::modem::Modem> + 'static,
|
||||
sysloop: EspSystemEventLoop,
|
||||
nvs: EspDefaultNvsPartition,
|
||||
) -> Result<()> {
|
||||
info!("Enabling Hotspot");
|
||||
let mut wifi = BlockingWifi::wrap(
|
||||
EspWifi::new(modem, sysloop.clone(), Some(nvs))?,
|
||||
sysloop,
|
||||
)?;
|
||||
|
||||
wifi.set_configuration(&Configuration::AccessPoint(AccessPointConfiguration {
|
||||
ssid: "PMME-Wifi".try_into().unwrap(),
|
||||
password: "88888888".try_into().unwrap(),
|
||||
ssid_hidden: false,
|
||||
channel: 1,
|
||||
secondary_channel: None,
|
||||
protocols: Default::default(),
|
||||
auth_method: AuthMethod::WPA2Personal,
|
||||
max_connections: 2,
|
||||
}))?;
|
||||
|
||||
wifi.start()?;
|
||||
info!("Wifi Started");
|
||||
wifi.wait_netif_up()?;
|
||||
info!("Wifi netif up");
|
||||
|
||||
let server_conf = esp_idf_svc::http::server::Configuration::default();
|
||||
|
||||
let mut server = EspHttpServer::new(&server_conf)?;
|
||||
|
||||
info!("Started web server on port {}", server_conf.http_port);
|
||||
server.fn_handler("/api/provision", Method::Get, |req| {
|
||||
req.into_ok_response()?
|
||||
.write_all(b"<html><body>Hello world!</body></html>")
|
||||
.map(|_| ())
|
||||
})?;
|
||||
|
||||
loop {
|
||||
FreeRtos::delay_ms(500);
|
||||
}
|
||||
// Ok(())
|
||||
}
|
||||
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.jks
|
||||
src/
|
||||
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"name": "pmme",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pmme",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.5.0.tgz",
|
||||
"integrity": "sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.5.0",
|
||||
"@tauri-apps/cli-darwin-x64": "2.5.0",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.5.0",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.5.0",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.5.0",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.5.0",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.5.0",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.5.0",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.5.0",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.5.0",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz",
|
||||
"integrity": "sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz",
|
||||
"integrity": "sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.5.0.tgz",
|
||||
"integrity": "sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.5.0.tgz",
|
||||
"integrity": "sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz",
|
||||
"integrity": "sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.5.0.tgz",
|
||||
"integrity": "sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.5.0.tgz",
|
||||
"integrity": "sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz",
|
||||
"integrity": "sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.5.0.tgz",
|
||||
"integrity": "sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.5.0.tgz",
|
||||
"integrity": "sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.5.0.tgz",
|
||||
"integrity": "sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,5 @@ tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-http = "2"
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"http:default"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
@@ -0,0 +1,20 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
keystore.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
@@ -0,0 +1,6 @@
|
||||
/src/main/java/com/pmme/app/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
@@ -0,0 +1,85 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
namespace = "com.pmme.app"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "com.pmme.app"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["password"] as String
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["password"] as String
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.6.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.pmme"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.pmme.app
|
||||
|
||||
class MainActivity : TauriActivity()
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.pmme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">pmme</string>
|
||||
<string name="main_activity_title">pmme</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.pmme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
@@ -0,0 +1,22 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.5.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("pluginsForCoolKids") {
|
||||
id = "rust"
|
||||
implementationClass = "RustPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:8.5.1")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import java.io.File
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """cargo""";
|
||||
try {
|
||||
runTauriCli(executable)
|
||||
} catch (e: Exception) {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
runTauriCli("$executable.cmd")
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runTauriCli(executable: String) {
|
||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||
val target = target ?: throw GradleException("target cannot be null")
|
||||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.get
|
||||
|
||||
const val TASK_GROUP = "rust"
|
||||
|
||||
open class Config {
|
||||
lateinit var rootDirRel: String
|
||||
}
|
||||
|
||||
open class RustPlugin : Plugin<Project> {
|
||||
private lateinit var config: Config
|
||||
|
||||
override fun apply(project: Project) = with(project) {
|
||||
config = extensions.create("rust", Config::class.java)
|
||||
|
||||
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||
|
||||
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||
|
||||
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
@Suppress("UnstableApiUsage")
|
||||
flavorDimensions.add("abi")
|
||||
productFlavors {
|
||||
create("universal") {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters += abiList
|
||||
}
|
||||
}
|
||||
defaultArchList.forEachIndexed { index, arch ->
|
||||
create(arch) {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters.add(defaultAbiList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
for (profile in listOf("debug", "release")) {
|
||||
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||
val buildTask = tasks.maybeCreate(
|
||||
"rustBuildUniversal$profileCapitalized",
|
||||
DefaultTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for all targets"
|
||||
}
|
||||
|
||||
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||
|
||||
for (targetPair in targetsList.withIndex()) {
|
||||
val targetName = targetPair.value
|
||||
val targetArch = archList[targetPair.index]
|
||||
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||
val targetBuildTask = project.tasks.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
@@ -0,0 +1,6 @@
|
||||
#Tue May 10 19:22:52 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,3 @@
|
||||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
@@ -7,6 +7,7 @@ fn greet(name: &str) -> String {
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.pmme.app",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
"devUrl": "http://localhost:3000"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri App</title>
|
||||
<script type="module" src="/main.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container">
|
||||
<h1>Welcome to Tauri</h1>
|
||||
|
||||
<div class="row">
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/assets/tauri.svg" class="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="/assets/javascript.svg"
|
||||
class="logo vanilla"
|
||||
alt="JavaScript logo"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<p>Click on the Tauri logo to learn more about the framework</p>
|
||||
|
||||
<form class="row" id="greet-form">
|
||||
<input id="greet-input" placeholder="Enter a name..." />
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
<p id="greet-msg"></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
|
||||
let greetInputEl;
|
||||
let greetMsgEl;
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
greetInputEl = document.querySelector("#greet-input");
|
||||
greetMsgEl = document.querySelector("#greet-msg");
|
||||
document.querySelector("#greet-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #ffe21c);
|
||||
}
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
((nil . ((joe/shell-command-silent-default . "./pmme-ui/browser-reload.sh"))))
|
||||
@@ -0,0 +1,26 @@
|
||||
node_modules/
|
||||
public/js
|
||||
|
||||
/target
|
||||
/checkouts
|
||||
/src/gen
|
||||
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.iml
|
||||
*.jar
|
||||
*.log
|
||||
.shadow-cljs
|
||||
.idea
|
||||
.lein-*
|
||||
.nrepl-*
|
||||
.DS_Store
|
||||
|
||||
.hgignore
|
||||
.hg/
|
||||
|
||||
node_modules/
|
||||
|
||||
.tab-id
|
||||
\*Claude*
|
||||
public/styles.css
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "pmme-ui",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"daisyui": "^5.0.43",
|
||||
"shadow-cljs": "3.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.1.10",
|
||||
"tailwindcss": "^4.1.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>PMME</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/styles.css"
|
||||
</head>
|
||||
<body>
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{:source-paths
|
||||
["src/main"
|
||||
"src/test"]
|
||||
|
||||
:dependencies
|
||||
[[no.cjohansen/replicant "2025.03.27"]]
|
||||
|
||||
:dev-http {3000 "public"}
|
||||
|
||||
:builds
|
||||
{:dev {:target :browser
|
||||
:output-dir "public/js"
|
||||
:asset-path "/js"
|
||||
:modules {:main {:init-fn pmme-ui.shell/main}}}
|
||||
|
||||
:mobile {:target :browser
|
||||
:output-dir "public/js"
|
||||
:asset-path "/js"
|
||||
:modules {:main {:init-fn pmme-ui.shell/main}}
|
||||
:devtools {:devtools-url "http://192.168.110.133:9630"}}
|
||||
|
||||
:release {:target :browser
|
||||
:output-dir "../pmme-mobile/src/js"
|
||||
:asset-path "/js"
|
||||
:modules {:main {:init-fn pmme-ui.shell/main}}
|
||||
:compiler-options {:optimizations :advanced}}}}
|
||||
@@ -0,0 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@source "./**/*.{clj,cljs,cljc}";
|
||||
@plugin "daisyui";
|
||||
|
||||
@layer utilities {
|
||||
.badge-error { /* This forces Tailwind to include it */ }
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
(ns pmme-ui.core
|
||||
(:require [replicant.dom :as r]))
|
||||
|
||||
(defn render-pm25-display [state]
|
||||
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
|
||||
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
|
||||
[:div.card-body.text-center.p-12
|
||||
[:div.mb-6
|
||||
[:h1.text-2xl.font-light.text-slate-600.mb-2 "PME"]
|
||||
[:div.text-sm.text-slate-400.uppercase.tracking-wide.font-medium "PM 2.5 Level"]]
|
||||
|
||||
[:div.text-center.mb-8
|
||||
[:div.relative.inline-block
|
||||
[:span.text-8xl.md:text-9xl.font-black.text-slate-800.leading-none.tracking-tight
|
||||
(:pm25 state "250")]
|
||||
[:span.absolute.top-0.-right-20.text-2xl.text-slate-400.font-light "μg/m³"]]]
|
||||
|
||||
[:div.mt-4
|
||||
(let [pm25-val (js/parseInt (:pm25 state "0"))
|
||||
status (cond
|
||||
(<= pm25-val 12) {:text "Good" :color "text-success" :bg "bg-success/10"}
|
||||
(<= pm25-val 35) {:text "Moderate" :color "text-warning" :bg "bg-warning/10"}
|
||||
:else {:text "Unhealthy" :color "text-error" :bg "bg-error/10"})]
|
||||
[:div.badge.badge-lg.px-6.py-3.text-base.font-semibold
|
||||
{:class [(:bg status) (:color status)]}
|
||||
(:text status)])]
|
||||
]]])
|
||||
|
||||
(defn render-wifi-table [wifi-networks]
|
||||
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
|
||||
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
|
||||
[:div.card-body.p-8
|
||||
[:h2.text-2xl.font-bold.text-slate-800.mb-6.text-center "Saved WiFi Networks"]
|
||||
[:div.overflow-x-auto
|
||||
[:table.table.w-full
|
||||
[:thead
|
||||
[:tr
|
||||
[:th.w-full "SSID"]
|
||||
[:th "Actions"]]]
|
||||
[:tbody
|
||||
(for [wifi wifi-networks]
|
||||
[:tr {:key (:ssid wifi)}
|
||||
[:td.font-bold (:ssid wifi)]
|
||||
[:td
|
||||
[:div.flex.gap-2
|
||||
(if (:active wifi)
|
||||
[:button.btn.btn-success.btn-sm.w-32.text-black.font-normal "Active"]
|
||||
[:button.btn.btn-info.btn-sm.w-32.text-black.font-normal "Make Active"])
|
||||
[:button.btn.btn-error.btn-sm.flex-1.font-normal "Delete"]]]])]]]
|
||||
[:btn.btn.btn-accent.mt-2 {:on {:click ::add-wifi}} "+ Add Wifi"]]]])
|
||||
|
||||
(defn render-admin-panel [state]
|
||||
(render-wifi-table [{:ssid "The Brick" :active true} {:ssid "Home" :active false}])
|
||||
)
|
||||
|
||||
;; [:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
|
||||
;; [:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
|
||||
;; [:div.card-body.p-8
|
||||
;; [:h2.text-2xl.font-bold.text-slate-800.mb-6.text-center "ESP32 Admin Panel"]
|
||||
|
||||
;; [:div.space-y-4
|
||||
;; [:div.form-control
|
||||
;; [:label.label.w-32.items-center [:span.label-text "WiFi SSID"]]
|
||||
;; [:input.input.input-bordered {:type "text" :placeholder "Enter WiFi network name"}]]
|
||||
|
||||
;; [:div.form-control
|
||||
;; [:label.label.w-32 [:span.label-text "WiFi Password"]]
|
||||
;; [:input.input.input-bordered {:type "password" :placeholder "Enter WiFi password"}]]
|
||||
|
||||
;; [:div.form-control
|
||||
;; [:label.label.w-32 [:span.label-text "Device Name"]]
|
||||
;; [:input.input.input-bordered {:type "text" :placeholder "Enter device name"}]]
|
||||
|
||||
;; [:div.flex.gap-2.mt-6
|
||||
;; [:button.btn.btn-primary.flex-1 "Provision Device"]
|
||||
;; [:button.btn.btn-outline.flex-1 "Reset Device"]]]]]]
|
||||
|
||||
(defn render-current-content [state]
|
||||
(case (:current-tab state)
|
||||
:admin (render-admin-panel state)
|
||||
:readings (render-pm25-display state)))
|
||||
|
||||
(defn render-tabs [state]
|
||||
(when (:show-tabs state)
|
||||
[:div.fixed.top-4.w-full.flex.justify-center
|
||||
[:div.tabs.tabs-boxed.bg-white-90.backdrop-blur-sm.shadow-lg.border.border-slate-200
|
||||
[:button.tab
|
||||
{:class (when (= (:current-tab state) :readings) "tab-active")
|
||||
:on {:click [::switch-tab :readings]}}
|
||||
"Current Readings"]
|
||||
[:button.tab
|
||||
{:class (when (= (:current-tab state) :admin) "tab-active")
|
||||
:on {:click [::switch-tab :admin]}}
|
||||
"Admin Panel"
|
||||
(case (:admin-status state)
|
||||
::available
|
||||
[:div.ml-2 {:class ["inline-grid" "*:[grid-area:1/1]"]}
|
||||
[:div.status.status-success.animate-ping]
|
||||
[:div.status.status-success]]
|
||||
::unavailable
|
||||
[:div.status.status-error.ml-2]
|
||||
::unknown
|
||||
[:div.status.status-neutral.ml-2])]]]))
|
||||
|
||||
(defn render-ui [state]
|
||||
(r/render
|
||||
js/document.body
|
||||
[:div
|
||||
(render-tabs state)
|
||||
(render-current-content state)]))
|
||||
@@ -0,0 +1,46 @@
|
||||
(ns pmme-ui.shell
|
||||
(:require [replicant.dom :as r]
|
||||
[pmme-ui.core :as core]
|
||||
[cljs.core.async :as async :refer [go <!]]
|
||||
[cljs.core.async.interop :refer [<p!]]))
|
||||
|
||||
(defonce store (atom {:number 0
|
||||
:current-tab :readings
|
||||
:show-tabs true
|
||||
:admin-status ::core/unknown}))
|
||||
|
||||
(defn ping-admin-endpoint []
|
||||
(go
|
||||
(try
|
||||
(<p! (js/fetch "http://localhost:8080"))
|
||||
true
|
||||
(catch js/Error e
|
||||
false))))
|
||||
|
||||
(defn init [store]
|
||||
(add-watch store ::render (fn [_ _ _ new-state] (core/render-ui new-state)))
|
||||
(r/set-dispatch!
|
||||
(fn [_ event-data]
|
||||
(let [[action & args] event-data]
|
||||
(case action
|
||||
::core/switch-tab
|
||||
(let [target-tab (first args)]
|
||||
(if (= target-tab :admin)
|
||||
(go
|
||||
;; (let [admin-available? (<! (ping-admin-endpoint))]
|
||||
(let [admin-available? true]
|
||||
(if admin-available?
|
||||
(do
|
||||
(swap! store assoc :current-tab :admin)
|
||||
(swap! store assoc :admin-status ::core/available))
|
||||
(swap! store assoc :admin-status ::core/unavailable))))
|
||||
(swap! store assoc :current-tab target-tab)))))))
|
||||
(swap! store assoc ::loaded-at (.getTime (js/Date.))))
|
||||
|
||||
(defn main []
|
||||
(init store)
|
||||
(println "Loaded!"))
|
||||
|
||||
(defn ^:dev/after-load reload []
|
||||
(init store)
|
||||
(println "Reloaded"))
|
||||
@@ -0,0 +1,83 @@
|
||||
*** Which is the one I care about? Like which is the one I would use to know if Southeast Asia's burning season is getting bad?
|
||||
|
||||
You want *=pm2_5_atm=* - the PM2.5 concentration under atmospheric environment.
|
||||
|
||||
PM2.5 (particles ≤ 2.5 micrometers) is the key metric for air quality and health because these particles are small enough to penetrate deep into your lungs and bloodstream. During Southeast Asia's burning season, this is what gets dangerously high.
|
||||
|
||||
The "atmospheric environment" reading (=_atm=) is more accurate for real-world conditions compared to the "CF=1 standard particle" (=_cf1=) reading, which uses a correction factor.
|
||||
|
||||
*Reference levels:*
|
||||
- 0-12 μg/m³: Good
|
||||
- 13-35 μg/m³: Moderate
|
||||
- 36-55 μg/m³: Unhealthy for sensitive groups
|
||||
- 56-150 μg/m³: Unhealthy
|
||||
- 151-250 μg/m³: Very unhealthy
|
||||
- 251+ μg/m³: Hazardous
|
||||
|
||||
During bad haze episodes, you'll see =pm2_5_atm= values well into the hundreds or even 300+ μg/m³.
|
||||
|
||||
*** Ok, so why would I want to know about 1.0 and 10.0? What would those be useful for? This is for a home sensor project I'm building
|
||||
|
||||
Great question! Here's why you'd want the other PM readings for a home sensor:
|
||||
|
||||
*PM1.0 (=pm1_0_atm=):*
|
||||
- *Ultra-fine particles* - these penetrate deepest into lungs and bloodstream
|
||||
- *Indoor sources*: cooking (especially frying/grilling), candles, incense, vaping
|
||||
- *Early warning*: Often spikes before PM2.5 gets bad
|
||||
- *Health*: Most concerning for cardiovascular effects
|
||||
|
||||
*PM10 (=pm10_atm=):*
|
||||
- *Coarse particles* - dust, pollen, mold spores
|
||||
- *Indoor sources*: vacuuming, pets, dusty activities, open windows on windy days
|
||||
- *Allergies*: Key metric for people with dust/pollen allergies
|
||||
- *HVAC effectiveness*: Shows if your air filters are working
|
||||
|
||||
*For your home sensor project:*
|
||||
- *PM1.0*: "Is my cooking/candle affecting air quality?"
|
||||
- *PM2.5*: "Is the outdoor haze/pollution getting inside?"
|
||||
- *PM10*: "Should I run the air purifier due to dust/allergens?"
|
||||
|
||||
You could set different alert thresholds:
|
||||
#+begin_src c
|
||||
if (data.pm1_0_atm > 15) printf("Cooking/indoor pollution detected\n");
|
||||
if (data.pm2_5_atm > 35) printf("Outdoor pollution - close windows!\n");
|
||||
if (data.pm10_atm > 50) printf("High dust/allergens - run air purifier\n");
|
||||
#+end_src
|
||||
|
||||
All three together give you a complete picture of your indoor air quality!
|
||||
|
||||
*** How does AQI work though? Seems like the AQI score is different from the PM 2.5 reading
|
||||
|
||||
Yes! AQI (Air Quality Index) is a /calculated score/ that converts raw PM2.5 readings into a standardized 0-500 scale that's easier for people to understand.
|
||||
|
||||
*The conversion uses breakpoints:*
|
||||
|
||||
| AQI Range | PM2.5 (μg/m³) | Category |
|
||||
|-----------|---------------|----------|
|
||||
| 0-50 | 0.0-12.0 | Good |
|
||||
| 51-100 | 12.1-35.4 | Moderate |
|
||||
| 101-150 | 35.5-55.4 | Unhealthy for Sensitive |
|
||||
| 151-200 | 55.5-150.4 | Unhealthy |
|
||||
| 201-300 | 150.5-250.4 | Very Unhealthy |
|
||||
| 301-500 | 250.5-500.4 | Hazardous |
|
||||
|
||||
*Example:*
|
||||
- PM2.5 = 25 μg/m³ → AQI ≈ 79 (Moderate)
|
||||
- PM2.5 = 100 μg/m³ → AQI ≈ 170 (Unhealthy)
|
||||
|
||||
*For your home sensor:*
|
||||
#+begin_src c
|
||||
int pm25_to_aqi(float pm25) {
|
||||
if (pm25 <= 12.0) return (50.0/12.0) * pm25;
|
||||
if (pm25 <= 35.4) return 51 + ((49.0/23.4) * (pm25 - 12.1));
|
||||
if (pm25 <= 55.4) return 101 + ((49.0/19.9) * (pm25 - 35.5));
|
||||
// ... continue for higher ranges
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Why AQI is useful:*
|
||||
- Color-coded (Green=Good, Red=Unhealthy)
|
||||
- Same scale regardless of pollutant type
|
||||
- Easy for non-technical users: "AQI 150 = stay indoors"
|
||||
|
||||
The raw PM2.5 number is more precise for data logging, but AQI is better for user alerts!
|
||||