Compare commits

...
22 Commits
Author SHA1 Message Date
JosephFerano 614aef96c5 📟 pmme-device: backporting - Match uart_data reading from Rust 2026-05-17 07:37:52 +07:00
JosephFerano a638e0293c 📟 pmme-device: backporting - add wifi provision code 2026-05-17 07:31:26 +07:00
JosephFerano 49c123809a 📟 pmme-device: backporting - GPIO button 2026-05-16 12:57:31 +07:00
JosephFerano f850e86b2f 🌐️ pmme-ui: Refactor admin/sensors split into single core module
- Merged admin.cljs and sensors.cljs into core.cljs to consolidate UI logic
- Added WiFi network management interface with active/inactive status indicators
- Improved admin panel with async status checking and visual feedback
- Updated shadow-cljs config to use port 3000 and simplified mobile build
- Enhanced CSS with utility layer for badge-error class
2025-07-12 13:31:43 +07:00
JosephFerano 3143f3b37f 🌐️ pmme-ui: New replicant design with sensors and admin panel tabs 2025-07-12 13:31:37 +07:00
JosephFerano 2c741adac7 🌐 pmme-ui: Shadow-cljs configured for hot reloading, moved static files to ui side 2025-06-16 19:17:51 +07:00
JosephFerano 6892060d16 📱 pmme-mobile: Get android build running with http plugin, gen android project 2025-06-16 12:37:55 +07:00
JosephFerano 8bd69dce67 📟 pmme-device: Add README and export path script 2025-06-14 19:31:29 +07:00
JosephFerano 8ee32a8949 📟 pmme-device: Provisioning removed in favor or an access point + server 2025-06-14 19:31:22 +07:00
JosephFerano b9b9a0bdd0 📟 pmme-device: Switch back to povisioning test, add a button + LED for potential later use 2025-06-13 18:00:52 +07:00
JosephFerano 4e07dbb461 📟 pmme-device: BT_NIMBLE support for wifi provisioning 2025-06-12 14:40:59 +07:00
JosephFerano 9d5b9366d5 📟 pmme-device: Project now split into separate modules 2025-06-11 17:22:21 +07:00
JosephFerano a164f3dda1 📟 pmme-device: wifi.rs added along with lib.rs and ran cargo fmt 2025-06-11 16:48:09 +07:00
JosephFerano 385988401c 📟 pmme-device: Move oled_task function down 2025-06-11 13:26:44 +07:00
JosephFerano d87215bebd 📟 pmme-device: PM2.5 is now being read and written to an ArcMutex 2025-06-11 13:26:29 +07:00
JosephFerano 8e6fc96e4d 📟 pmme-device: UartDriver now reads PM data from sensor 2025-06-10 18:56:01 +07:00
JosephFerano 9e3ae56aa1 📟 pmme-device: Use embedded_graphics + ssd1306 crates to display logo on the OLED 2025-06-10 18:04:23 +07:00
JosephFerano cc163589fc 📟 pmme-device: Rust version with the esp-idf-template 2025-06-09 18:06:58 +07:00
JosephFerano f0685f9ce3 📟 pmme-device: Use ssd1306 lib to display text, read/write pm 2.5 with mutex 2025-06-07 18:28:00 +07:00
JosephFerano 2c1ab09770 📟 pmme-device: Reading PM 1.0, 2.5, and 10.0. Split sensor and OLED display to separate tasks 2025-06-07 16:13:31 +07:00
JosephFerano 52b7a7258c 📱 pmme-mobile: Run npm install to get tauri 2025-06-07 16:12:19 +07:00
JosephFerano c27e1aa057 🎨 pmme-ui: Commit files 2025-06-07 16:10:29 +07:00
85 changed files with 11459 additions and 609 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
/*Claude-PMME*
+111
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
idf_component_register(SRCS "pmme.c"
idf_component_register(SRCS "pmme.c" "ssd1306/ssd1306.c"
INCLUDE_DIRS ".")
+298 -15
View File
@@ -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 ( )
};
File diff suppressed because it is too large. Load diff
+135
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
((nil
(lsp-rust-analyzer-exclude-dirs . ["./.embuild/"])))
+4
View File
@@ -0,0 +1,4 @@
/.vscode
/.embuild
/target
/Cargo.lock
+32
View File
@@ -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"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
embuild::espidf::sysenv::output();
}
+2
View File
@@ -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"
Binary file not shown.
@@ -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
+57
View File
@@ -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);
}
}
+24
View File
@@ -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,
}
+129
View File
@@ -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(())
}
+72
View File
@@ -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);
}
}
+60
View File
@@ -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(())
}
+2
View File
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.jks
src/
+220
View File
@@ -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"
}
}
}
}
+5462
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

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
+185
View File
@@ -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" "$@"
+89
View File
@@ -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'
+1
View File
@@ -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!())
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.1.0",
"identifier": "com.pmme.app",
"build": {
"frontendDist": "../src"
"devUrl": "http://localhost:3000"
},
"app": {
"withGlobalTauri": true,
-39
View File
@@ -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>
-18
View File
@@ -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();
});
});
-112
View File
@@ -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;
}
}
+1
View File
@@ -0,0 +1 @@
((nil . ((joe/shell-command-silent-default . "./pmme-ui/browser-reload.sh"))))
+26
View File
@@ -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
+1270
View File
File diff suppressed because it is too large. Load diff
+13
View File
@@ -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"
}
}
File renamed without changes.
File renamed without changes.
+12
View File
@@ -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>
+26
View File
@@ -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}}}}
+7
View File
@@ -0,0 +1,7 @@
@import "tailwindcss";
@source "./**/*.{clj,cljs,cljc}";
@plugin "daisyui";
@layer utilities {
.badge-error { /* This forces Tailwind to include it */ }
}
+110
View File
@@ -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)]))
+46
View File
@@ -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"))
+83
View File
@@ -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!