OK! We have a workign thing! (Sorta). This now presents a web page which automatically updates the CO2, temp, and humitity values via JavaScript fetching from the Pico. The code diffs contain some gnarly stuff... I've thrown more debug prints and some sleeps in as I was hitting a sort of concurrently issue I think. I need to dig deeper into how these locks are working. Also I turned off keepalives as with the JS in the mix we were basically self-dossing... and adding more HTTP workers is bad solution as it just locks up the Pi (so those concurrently issues again?!) Anyway... it's definitely a milestone. Next up a spot of cleaning and debugging.

This commit is contained in:
Yvan 2025-03-17 21:13:50 +00:00
parent 8ec0ccbbad
commit 54930ec630
4 changed files with 101 additions and 10 deletions

View file

@ -3,10 +3,19 @@
<head> <head>
<meta charset="UTF-8"> <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.0">
<title>Meat-Pi</title> <title>SCD40 CO2 PPM Sensor</title>
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
<script src="main.js"></script>
</head> </head>
<body> <body>
<h1>Meat-Pi 🍖🌡️</h1> <h1>SCD40 CO2 PPM Sensor</h1>
<dl>
<dt>CO<sub>2</sub></dt>
<dd id="co2ppm">enable JavaScript!</dd>
<dt>Temperature</dt>
<dd id="temperature">enable JavaScript!</dd>
<dt>Humidity</dt>
<dd id="humidity">enable JavaScript!</dd>
</dl>
</body> </body>
</html> </html>

View file

@ -17,3 +17,16 @@ body {
align-items: center; align-items: center;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
} }
dl {
display: grid;
grid-template-columns: max-content auto;
}
dt {
grid-column-start: 1;
}
dd {
grid-column-start: 2;
}

28
src/html/main.js Normal file
View file

@ -0,0 +1,28 @@
// data field names
var data = [ "co2ppm", "temperature", "humidity" ];
// when the DOM is ready use JS to remove the "enable JS" text
document.addEventListener("DOMContentLoaded", function() {
data.forEach(( datum, i ) => document.getElementById(datum).innerHTML = "&hellip;");
});
var i1 = setInterval(
function() {
fetch("data/humidity")
.then(function(response) { return response.json(); })
.then(function(json) {
document.getElementById("humidity").innerHTML = parseFloat(json).toFixed(0) + "%";
return fetch("data/temperature");
})
.then(function(response) { return response.json(); })
.then(function(json) {
document.getElementById("temperature").innerHTML = parseFloat(json).toFixed(1) + "&deg;C";
return fetch("data/co2ppm")
})
.then(function(response) { return response.json(); })
.then(function(json) {
document.getElementById("co2ppm").innerHTML = json + " PPM";
});
},
2000
);

View file

@ -31,10 +31,13 @@ use {defmt_rtt as _, panic_probe as _};
// ensure the network/password files have no trailing newline // ensure the network/password files have no trailing newline
// i.e. generate like: echo -n "password" > src/secrets/wifi-password // i.e. generate like: echo -n "password" > src/secrets/wifi-password
const WIFI_NETWORK: &str = include_str!("secrets/wifi-network"); // see README in src/secrets
const WIFI_PASSWORD: &str = include_str!("secrets/wifi-password"); const WIFI_NETWORK: &str = "bendybogalow"; // include_str!("secrets/wifi-network");
const WIFI_PASSWORD: &str = "parsnipcabbageonion"; // include_str!("secrets/wifi-password");
// web content
const INDEX: &str = include_str!("html/index.html"); const INDEX: &str = include_str!("html/index.html");
const CSS: &str = include_str!("html/main.css"); const CSS: &str = include_str!("html/main.css");
const JS: &str = include_str!("html/main.js");
// TODO: I think these calls can be combined? // TODO: I think these calls can be combined?
bind_interrupts!(struct Irqs { bind_interrupts!(struct Irqs {
@ -109,7 +112,12 @@ impl AppWithStateBuilder for AppProps {
get_service(File::css(CSS)), get_service(File::css(CSS)),
) )
.route( .route(
"/data", "/main.js",
get_service(File::javascript(JS)),
)
/*.route( // FIXME: for some reason this causes the whole pi to lock up...
// mutex shenanigans....
"/data.json",
get( get(
|State(SharedSCD40Data(scd40data)): State<SharedSCD40Data>| //newbie note: | delimits a closure |State(SharedSCD40Data(scd40data)): State<SharedSCD40Data>| //newbie note: | delimits a closure
async move { picoserve::response::Json( async move { picoserve::response::Json(
@ -123,6 +131,27 @@ impl AppWithStateBuilder for AppProps {
) )
} }
), ),
)*/
.route(
"/data/co2ppm",
get(
|State(SharedSCD40Data(scd40data)): State<SharedSCD40Data>| //newbie note: | delimits a closure
async move { picoserve::response::Json( scd40data.lock().await.co2ppm ) }
),
)
.route(
"/data/temperature",
get(
|State(SharedSCD40Data(scd40data)): State<SharedSCD40Data>| //newbie note: | delimits a closure
async move { picoserve::response::Json( scd40data.lock().await.temperature ) }
),
)
.route(
"/data/humidity",
get(
|State(SharedSCD40Data(scd40data)): State<SharedSCD40Data>| //newbie note: | delimits a closure
async move { picoserve::response::Json( scd40data.lock().await.humidity ) }
),
) )
} }
} }
@ -167,9 +196,12 @@ async fn read_co2(
shared_scd40data: SharedSCD40Data shared_scd40data: SharedSCD40Data
) { ) {
log::info!("Enter sensor read loop"); log::info!("Enter sensor read loop");
Timer::after_millis(1000).await;
loop { loop {
if scd.data_ready().unwrap() { if scd.data_ready().unwrap() {
let m = scd.read_measurement().unwrap(); let m = scd.read_measurement().unwrap();
log::info!("Read a measurement");
Timer::after_millis(1000).await;
// TODO: is there a way to write this in one block/struct rather than three locks? // TODO: is there a way to write this in one block/struct rather than three locks?
shared_scd40data.0.lock().await.co2ppm = m.co2; shared_scd40data.0.lock().await.co2ppm = m.co2;
shared_scd40data.0.lock().await.temperature = m.temperature; shared_scd40data.0.lock().await.temperature = m.temperature;
@ -178,7 +210,7 @@ async fn read_co2(
"CO2: {}\nHumidity: {}\nTemperature: {}", m.co2, m.humidity, m.temperature "CO2: {}\nHumidity: {}\nTemperature: {}", m.co2, m.humidity, m.temperature
) )
} }
Timer::after_secs(1).await; Timer::after_secs(5).await;
} }
} }
@ -250,7 +282,7 @@ async fn main(spawner: Spawner) {
// if static IP then use this code: // if static IP then use this code:
log::info!("main: configure static IP"); log::info!("main: configure static IP");
let config = embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { let config = embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 {
address: Ipv4Cidr::new(Ipv4Address::new(192, 168, 1, 113), 24), address: Ipv4Cidr::new(Ipv4Address::new(192, 168, 1, 39), 24),
dns_servers: Vec::new(), dns_servers: Vec::new(),
gateway: Some(Ipv4Address::new(192, 168, 1, 254)), gateway: Some(Ipv4Address::new(192, 168, 1, 254)),
}); });
@ -267,6 +299,7 @@ async fn main(spawner: Spawner) {
defmt::unwrap!(spawner.spawn(net_task(runner))); defmt::unwrap!(spawner.spawn(net_task(runner)));
log::info!("main: await network join"); log::info!("main: await network join");
Timer::after_millis(1000).await;
loop { loop {
match control match control
.join(WIFI_NETWORK, JoinOptions::new(WIFI_PASSWORD.as_bytes())) .join(WIFI_NETWORK, JoinOptions::new(WIFI_PASSWORD.as_bytes()))
@ -279,6 +312,8 @@ async fn main(spawner: Spawner) {
} }
Timer::after_millis(100).await; Timer::after_millis(100).await;
} }
log::info!("main: network up");
Timer::after_millis(1000).await;
// Wait for DHCP, not necessary when using static IP // Wait for DHCP, not necessary when using static IP
/*log::info!("waiting for DHCP..."); /*log::info!("waiting for DHCP...");
@ -301,31 +336,37 @@ async fn main(spawner: Spawner) {
// Set up the SCD40 I2C sensor // Set up the SCD40 I2C sensor
log::info!("Starting I2C Comms with SCD40"); log::info!("Starting I2C Comms with SCD40");
Timer::after_millis(1000).await;
// this code derived from: https://github.com/SvetlinZarev/libscd/blob/main/examples/embassy-scd4x/src/main.rs // this code derived from: https://github.com/SvetlinZarev/libscd/blob/main/examples/embassy-scd4x/src/main.rs
// TODO: how to make pins configurable? // TODO: how to make pins configurable?
let sda = p.PIN_26; let sda = p.PIN_26;
let scl = p.PIN_27; let scl = p.PIN_27;
let i2c = i2c::I2c::new_blocking(p.I2C1, scl, sda, Config::default()); let i2c = i2c::I2c::new_blocking(p.I2C1, scl, sda, Config::default());
log::info!("Initialise Scd4x"); log::info!("Initialise Scd4x");
Timer::after_millis(1000).await;
let mut scd = Scd4x::new(i2c, Delay); let mut scd = Scd4x::new(i2c, Delay);
// When re-programming, the controller will be restarted, but not the sensor. We try to stop it // When re-programming, the controller will be restarted, but not the sensor. We try to stop it
// in order to prevent the rest of the commands failing. // in order to prevent the rest of the commands failing.
log::info!("Stop periodic measurements"); log::info!("Stop periodic measurements");
Timer::after_millis(1000).await;
_ = scd.stop_periodic_measurement(); _ = scd.stop_periodic_measurement();
log::info!("Sensor serial number: {:?}", scd.serial_number()); log::info!("Sensor serial number: {:?}", scd.serial_number());
Timer::after_millis(1000).await;
if let Err(e) = scd.start_periodic_measurement() { if let Err(e) = scd.start_periodic_measurement() {
log::error!("Failed to start periodic measurement: {:?}", e ); log::error!("Failed to start periodic measurement: {:?}", e );
} }
log::info!("Spawn Sensor worker");
Timer::after_millis(1000).await;
spawner.must_spawn(read_co2(scd, shared_scd40data)); spawner.must_spawn(read_co2(scd, shared_scd40data));
//////////////////////////////////////////// ////////////////////////////////////////////
// Set up the HTTP service // Set up the HTTP service
log::info!("Commence HTTP service"); log::info!("Commence HTTP service");
Timer::after_millis(5000).await;
let app = make_static!(AppRouter<AppProps>, AppProps.build_app()); let app = make_static!(AppRouter<AppProps>, AppProps.build_app());
@ -336,7 +377,6 @@ async fn main(spawner: Spawner) {
read_request: Some(Duration::from_secs(1)), read_request: Some(Duration::from_secs(1)),
write: Some(Duration::from_secs(1)), write: Some(Duration::from_secs(1)),
}) })
.keep_connection_alive()
); );
for id in 0..WEB_TASK_POOL_SIZE { for id in 0..WEB_TASK_POOL_SIZE {
@ -348,5 +388,6 @@ async fn main(spawner: Spawner) {
AppState{ shared_scd40data }, AppState{ shared_scd40data },
)); ));
} }
} }