Compare commits
5
Commits
718fb49bf2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614aef96c5 | ||
|
|
a638e0293c | ||
|
|
49c123809a | ||
|
|
f850e86b2f | ||
|
|
3143f3b37f |
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
/*Claude-PMME*
|
||||||
@@ -12,8 +12,15 @@
|
|||||||
|
|
||||||
#include "driver/i2c_master.h"
|
#include "driver/i2c_master.h"
|
||||||
#include "driver/uart.h"
|
#include "driver/uart.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
#include "ssd1306/ssd1306.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";
|
static const char *TAG = "PMME-Device";
|
||||||
|
|
||||||
#define I2C_HOST 0
|
#define I2C_HOST 0
|
||||||
@@ -29,6 +36,8 @@ static const char *TAG = "PMME-Device";
|
|||||||
#define UART_BUF_SIZE 128
|
#define UART_BUF_SIZE 128
|
||||||
#define TXD_PIN 17
|
#define TXD_PIN 17
|
||||||
#define RXD_PIN 16
|
#define RXD_PIN 16
|
||||||
|
#define BUTTON_PIN GPIO_NUM_5
|
||||||
|
#define LED_PIN GPIO_NUM_19
|
||||||
|
|
||||||
#define SWAP_BYTES(x) ((x >> 8) | (x << 8))
|
#define SWAP_BYTES(x) ((x >> 8) | (x << 8))
|
||||||
|
|
||||||
@@ -69,7 +78,7 @@ void send_pms7003_command(uint8_t cmd, uint8_t datah, uint8_t datal) {
|
|||||||
|
|
||||||
void read_pm_data(uint8_t *buf) {
|
void read_pm_data(uint8_t *buf) {
|
||||||
uint16_t checksum = 0;
|
uint16_t checksum = 0;
|
||||||
for (int i = 0; i <= 29; i++) {
|
for (int i = 0; i <= 28; i++) {
|
||||||
checksum += buf[i];
|
checksum += buf[i];
|
||||||
}
|
}
|
||||||
uint16_t c = (((uint16_t)buf[30]) << 8) | ((uint16_t)buf[31]);
|
uint16_t c = (((uint16_t)buf[30]) << 8) | ((uint16_t)buf[31]);
|
||||||
@@ -121,13 +130,12 @@ void pm_sensor_task(void *pvParameters) {
|
|||||||
int len = uart_read_bytes(UART_NUM, uart_data, BUF_SIZE, 100 / portTICK_PERIOD_MS);
|
int len = uart_read_bytes(UART_NUM, uart_data, BUF_SIZE, 100 / portTICK_PERIOD_MS);
|
||||||
// ESP_LOGI(TAG, "Received %d bytes from sensor", len);
|
// ESP_LOGI(TAG, "Received %d bytes from sensor", len);
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
if (len == 32 && (uart_data[0] != 0x42 || uart_data[1] != 0x4D)) {
|
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]);
|
ESP_LOGW(TAG, "Frame Start does not match 0x42, 0x4D, instead got %x %x", uart_data[0], uart_data[1]);
|
||||||
} else {
|
} else if (len == 32) {
|
||||||
read_pm_data(uart_data);
|
read_pm_data(uart_data);
|
||||||
// for(int i = 0; i < len && i < BUF_SIZE; i++) {
|
} else {
|
||||||
// ESP_LOGI(TAG, "0x%02X", uart_data[i]);
|
ESP_LOGW(TAG, "Unexpected frame length: %d", len);
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uart_flush(UART_NUM);
|
uart_flush(UART_NUM);
|
||||||
@@ -202,6 +210,10 @@ void oled_display_task_manual(void *pvParameters) {
|
|||||||
void oled_display_task(void *pvParameters) {
|
void oled_display_task(void *pvParameters) {
|
||||||
init_ssd1306();
|
init_ssd1306();
|
||||||
char pm25_str[128];
|
char pm25_str[128];
|
||||||
|
|
||||||
|
ssd1306_fill_screen();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
uint16_t pm25_value = 0;
|
uint16_t pm25_value = 0;
|
||||||
if (xSemaphoreTake(sensor_data_mutex, pdMS_TO_TICKS(10))) {
|
if (xSemaphoreTake(sensor_data_mutex, pdMS_TO_TICKS(10))) {
|
||||||
@@ -219,6 +231,97 @@ void oled_display_task(void *pvParameters) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
void app_main(void) {
|
||||||
sensor_data_mutex = xSemaphoreCreateMutex();
|
sensor_data_mutex = xSemaphoreCreateMutex();
|
||||||
if (sensor_data_mutex == NULL) {
|
if (sensor_data_mutex == NULL) {
|
||||||
@@ -227,4 +330,6 @@ void app_main(void) {
|
|||||||
}
|
}
|
||||||
xTaskCreate(pm_sensor_task, "pm_sensor_task", 4096, NULL, 5, NULL);
|
xTaskCreate(pm_sensor_task, "pm_sensor_task", 4096, NULL, 5, NULL);
|
||||||
xTaskCreate(oled_display_task, "oled_display_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);
|
||||||
}
|
}
|
||||||
@@ -559,3 +559,19 @@ esp_err_t i2c_ssd1306_buffer_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306)
|
|||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -282,3 +282,8 @@ esp_err_t i2c_ssd1306_pages_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306, uint8_t in
|
|||||||
* @return ESP_OK on success, or an error code otherwise.
|
* @return ESP_OK on success, or an error code otherwise.
|
||||||
*/
|
*/
|
||||||
esp_err_t i2c_ssd1306_buffer_to_ram(i2c_ssd1306_handle_t *i2c_ssd1306);
|
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);
|
||||||
|
|
||||||
+1079
-420
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
|||||||
|
((nil . ((joe/shell-command-silent-default . "./pmme-ui/browser-reload.sh"))))
|
||||||
@@ -18,3 +18,9 @@ pom.xml.asc
|
|||||||
|
|
||||||
.hgignore
|
.hgignore
|
||||||
.hg/
|
.hg/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
.tab-id
|
||||||
|
\*Claude*
|
||||||
|
public/styles.css
|
||||||
Generated
+1076
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,11 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"daisyui": "^5.0.43",
|
||||||
"shadow-cljs": "3.1.5"
|
"shadow-cljs": "3.1.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.1.10",
|
||||||
|
"tailwindcss": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,12 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<title>PMME</title>
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Tauri App</title>
|
<link rel="stylesheet" type="text/css" href="/styles.css"
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main class="container">
|
<script src="/js/main.js"></script>
|
||||||
<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>
|
|
||||||
<script src="./js/main.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-10
@@ -1,26 +1,26 @@
|
|||||||
;; shadow-cljs configuration
|
|
||||||
{:source-paths
|
{:source-paths
|
||||||
["src/main"
|
["src/main"
|
||||||
"src/test"]
|
"src/test"]
|
||||||
|
|
||||||
:dependencies
|
:dependencies
|
||||||
[]
|
[[no.cjohansen/replicant "2025.03.27"]]
|
||||||
|
|
||||||
:dev-http {3000 "public"}
|
:dev-http {3000 "public"}
|
||||||
|
|
||||||
:builds
|
:builds
|
||||||
{:app {:target :browser
|
{:dev {:target :browser
|
||||||
:output-dir "public/js"
|
:output-dir "public/js"
|
||||||
:asset-path "/js"
|
:asset-path "/js"
|
||||||
:modules {:main {:init-fn pmme-ui.core/init}}
|
: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"}}
|
:devtools {:devtools-url "http://192.168.110.133:9630"}}
|
||||||
|
|
||||||
:release {:target :browser
|
:release {:target :browser
|
||||||
:output-dir "../pmme-mobile/src/js"
|
:output-dir "../pmme-mobile/src/js"
|
||||||
:asset-path "/js"
|
:asset-path "/js"
|
||||||
:modules {:main {:init-fn pmme-ui.core/init}}
|
:modules {:main {:init-fn pmme-ui.shell/main}}
|
||||||
:compiler-options {:optimizations :advanced}
|
:compiler-options {:optimizations :advanced}}}}
|
||||||
:build-hooks [(shadow.resource/copy-resources
|
|
||||||
{:from "public"
|
|
||||||
:to "../pmme-mobile/src"
|
|
||||||
:exclude ["**/*.js"]})]}}}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@source "./**/*.{clj,cljs,cljc}";
|
||||||
|
@plugin "daisyui";
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.badge-error { /* This forces Tailwind to include it */ }
|
||||||
|
}
|
||||||
@@ -1,36 +1,110 @@
|
|||||||
(ns pmme-ui.core
|
(ns pmme-ui.core
|
||||||
(:require [clojure.string :as str]))
|
(:require [replicant.dom :as r]))
|
||||||
|
|
||||||
(defn handle-greet [event]
|
(defn render-pm25-display [state]
|
||||||
(.preventDefault event)
|
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
|
||||||
(let [input (.getElementById js/document "greet-input")
|
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
|
||||||
msg-element (.getElementById js/document "greet-msg")
|
[:div.card-body.text-center.p-12
|
||||||
name (.-value input)]
|
[:div.mb-6
|
||||||
(if (empty? (str/trim name))
|
[:h1.text-2xl.font-light.text-slate-600.mb-2 "PME"]
|
||||||
(set! (.-innerHTML msg-element) "Please enter a name!")
|
[:div.text-sm.text-slate-400.uppercase.tracking-wide.font-medium "PM 2.5 Level"]]
|
||||||
(do
|
|
||||||
;; You could call a Tauri command here instead
|
|
||||||
(js/console.log "Testing the chrome thingie")
|
|
||||||
(set! (.-innerHTML msg-element)
|
|
||||||
(str "Hello, " name "! 👋 (from ClojureScript)!"))
|
|
||||||
(set! (.-value input) "")))))
|
|
||||||
|
|
||||||
(defn greet-handler [event]
|
[:div.text-center.mb-8
|
||||||
(handle-greet event))
|
[: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³"]]]
|
||||||
|
|
||||||
(defn setup-greet-form []
|
[:div.mt-4
|
||||||
(when-let [form (.getElementById js/document "greet-form")]
|
(let [pm25-val (js/parseInt (:pm25 state "0"))
|
||||||
(.addEventListener form "submit" greet-handler)))
|
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 add-dynamic-styling []
|
(defn render-wifi-table [wifi-networks]
|
||||||
(let [style (.createElement js/document "style")]
|
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
|
||||||
(set! (.-textContent style)
|
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
|
||||||
"#greet-msg { color: #646cff; font-weight: bold; margin-top: 1rem; }")
|
[:div.card-body.p-8
|
||||||
(.appendChild (.-head js/document) style)))
|
[: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 init []
|
(defn render-admin-panel [state]
|
||||||
(js/console.log "PMME UI initialized!")
|
(render-wifi-table [{:ssid "The Brick" :active true} {:ssid "Home" :active false}])
|
||||||
(setup-greet-form)
|
)
|
||||||
(add-dynamic-styling)
|
|
||||||
;; Remove the app element modification since it doesn't exist in this HTML
|
;; [:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
|
||||||
(js/console.log "Greet form is now powered by ClojureScript!"))
|
;; [:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
|
||||||
|
;; [:div.card-body.p-8
|
||||||
|
;; [:h2.text-2xl.font-bold.text-slate-800.mb-6.text-center "ESP32 Admin Panel"]
|
||||||
|
|
||||||
|
;; [:div.space-y-4
|
||||||
|
;; [:div.form-control
|
||||||
|
;; [:label.label.w-32.items-center [:span.label-text "WiFi SSID"]]
|
||||||
|
;; [:input.input.input-bordered {:type "text" :placeholder "Enter WiFi network name"}]]
|
||||||
|
|
||||||
|
;; [:div.form-control
|
||||||
|
;; [:label.label.w-32 [:span.label-text "WiFi Password"]]
|
||||||
|
;; [:input.input.input-bordered {:type "password" :placeholder "Enter WiFi password"}]]
|
||||||
|
|
||||||
|
;; [:div.form-control
|
||||||
|
;; [:label.label.w-32 [:span.label-text "Device Name"]]
|
||||||
|
;; [:input.input.input-bordered {:type "text" :placeholder "Enter device name"}]]
|
||||||
|
|
||||||
|
;; [:div.flex.gap-2.mt-6
|
||||||
|
;; [:button.btn.btn-primary.flex-1 "Provision Device"]
|
||||||
|
;; [:button.btn.btn-outline.flex-1 "Reset Device"]]]]]]
|
||||||
|
|
||||||
|
(defn render-current-content [state]
|
||||||
|
(case (:current-tab state)
|
||||||
|
:admin (render-admin-panel state)
|
||||||
|
:readings (render-pm25-display state)))
|
||||||
|
|
||||||
|
(defn render-tabs [state]
|
||||||
|
(when (:show-tabs state)
|
||||||
|
[:div.fixed.top-4.w-full.flex.justify-center
|
||||||
|
[:div.tabs.tabs-boxed.bg-white-90.backdrop-blur-sm.shadow-lg.border.border-slate-200
|
||||||
|
[:button.tab
|
||||||
|
{:class (when (= (:current-tab state) :readings) "tab-active")
|
||||||
|
:on {:click [::switch-tab :readings]}}
|
||||||
|
"Current Readings"]
|
||||||
|
[:button.tab
|
||||||
|
{:class (when (= (:current-tab state) :admin) "tab-active")
|
||||||
|
:on {:click [::switch-tab :admin]}}
|
||||||
|
"Admin Panel"
|
||||||
|
(case (:admin-status state)
|
||||||
|
::available
|
||||||
|
[:div.ml-2 {:class ["inline-grid" "*:[grid-area:1/1]"]}
|
||||||
|
[:div.status.status-success.animate-ping]
|
||||||
|
[:div.status.status-success]]
|
||||||
|
::unavailable
|
||||||
|
[:div.status.status-error.ml-2]
|
||||||
|
::unknown
|
||||||
|
[:div.status.status-neutral.ml-2])]]]))
|
||||||
|
|
||||||
|
(defn render-ui [state]
|
||||||
|
(r/render
|
||||||
|
js/document.body
|
||||||
|
[:div
|
||||||
|
(render-tabs state)
|
||||||
|
(render-current-content state)]))
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
(ns pmme-ui.shell
|
||||||
|
(:require [replicant.dom :as r]
|
||||||
|
[pmme-ui.core :as core]
|
||||||
|
[cljs.core.async :as async :refer [go <!]]
|
||||||
|
[cljs.core.async.interop :refer [<p!]]))
|
||||||
|
|
||||||
|
(defonce store (atom {:number 0
|
||||||
|
:current-tab :readings
|
||||||
|
:show-tabs true
|
||||||
|
:admin-status ::core/unknown}))
|
||||||
|
|
||||||
|
(defn ping-admin-endpoint []
|
||||||
|
(go
|
||||||
|
(try
|
||||||
|
(<p! (js/fetch "http://localhost:8080"))
|
||||||
|
true
|
||||||
|
(catch js/Error e
|
||||||
|
false))))
|
||||||
|
|
||||||
|
(defn init [store]
|
||||||
|
(add-watch store ::render (fn [_ _ _ new-state] (core/render-ui new-state)))
|
||||||
|
(r/set-dispatch!
|
||||||
|
(fn [_ event-data]
|
||||||
|
(let [[action & args] event-data]
|
||||||
|
(case action
|
||||||
|
::core/switch-tab
|
||||||
|
(let [target-tab (first args)]
|
||||||
|
(if (= target-tab :admin)
|
||||||
|
(go
|
||||||
|
;; (let [admin-available? (<! (ping-admin-endpoint))]
|
||||||
|
(let [admin-available? true]
|
||||||
|
(if admin-available?
|
||||||
|
(do
|
||||||
|
(swap! store assoc :current-tab :admin)
|
||||||
|
(swap! store assoc :admin-status ::core/available))
|
||||||
|
(swap! store assoc :admin-status ::core/unavailable))))
|
||||||
|
(swap! store assoc :current-tab target-tab)))))))
|
||||||
|
(swap! store assoc ::loaded-at (.getTime (js/Date.))))
|
||||||
|
|
||||||
|
(defn main []
|
||||||
|
(init store)
|
||||||
|
(println "Loaded!"))
|
||||||
|
|
||||||
|
(defn ^:dev/after-load reload []
|
||||||
|
(init store)
|
||||||
|
(println "Reloaded"))
|
||||||
Reference in new issue
Block a user