Skip to content

Commit

Permalink
feature: motion detection and server notifier
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-eof committed Nov 22, 2023
0 parents commit add04b9
Show file tree
Hide file tree
Showing 19 changed files with 505 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[build]
target = "xtensa-esp32-espidf"

[target.xtensa-esp32-espidf]
linker = "ldproxy"
# runner = "espflash --monitor" # Select this runner for espflash v1.x.x
runner = "espflash flash --monitor" # Select this runner for espflash v2.x.x
rustflags = [
"--cfg",
"espidf_time64",
] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110

[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.1.1"

40 changes: 40 additions & 0 deletions .github/workflows/rust_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Continuous Integration

on:
push:
paths-ignore:
- "**/README.md"
pull_request:
workflow_dispatch:

env:
CARGO_TERM_COLOR: always
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
rust-checks:
name: Rust Checks
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
action:
- command: build
args: --release
- command: fmt
args: --all -- --check --color always
- command: clippy
args: --all-targets --all-features --workspace -- -D warnings
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable caching
uses: Swatinem/rust-cache@v2
- name: Setup Rust
uses: esp-rs/xtensa-toolchain@v1.5
with:
default: true
buildtargets: esp32
ldproxy: true
- name: Run command
run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/.vscode
/.embuild
/target
/Cargo.lock
/src/config/config.rs
57 changes: 57 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[package]
name = "esp32-motion-detector-and-server-notifier-rust"
version = "0.2.0"
authors = ["Andrei Dodu"]
edition = "2021"
resolver = "2"
rust-version = "1.71"

[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 = ["std", "hal", "esp-idf-sys/native"]


pio = ["esp-idf-sys/pio"]
all = ["std", "nightly", "experimental", "embassy"]
hal = ["esp-idf-hal", "embedded-svc", "esp-idf-svc"]
std = [
"alloc",
"esp-idf-sys/std",
"esp-idf-sys/binstart",
"embedded-svc?/std",
"esp-idf-hal?/std",
"esp-idf-svc?/std",
]
alloc = ["embedded-svc?/alloc", "esp-idf-hal?/alloc", "esp-idf-svc?/alloc"]
nightly = [
"embedded-svc?/nightly",
"esp-idf-svc?/nightly",
] # Future: "esp-idf-hal?/nightly"
experimental = ["embedded-svc?/experimental", "esp-idf-svc?/experimental"]
embassy = [
"esp-idf-hal?/embassy-sync",
"esp-idf-hal?/critical-section",
"esp-idf-svc?/embassy-time-driver",
"esp-idf-svc?/embassy-time-isr-queue",
]

[dependencies]
macaddr = "1.0.1"
anyhow = "1.0.75"
log = { version = "0.4.17", default-features = false }
esp-idf-sys = { version = "0.33", default-features = false }
esp-idf-hal = { version = "0.42.5", optional = true, default-features = false }
esp-idf-svc = { version = "0.47.3", optional = true, default-features = false }
embedded-svc = { version = "0.26.4", optional = true, default-features = false }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = { version = "1.0.108", features = ["raw_value"] }

[build-dependencies]
embuild = "0.31.2"
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ESP32 IDF | Motion Detector and Server Notifier (Rust)

Motion Detector and Server Notifier (Rust) is a motion detector application for ESP32 device (should be [this one](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-devkitc.html#get-started-esp32-devkitc-board-front)), implemented using Rust programming language, that allows to detect movements and send an alert to a server which will send a notification to a smartphone. **Please refer to the `How to configure and install it?` section if your application does not compiles** (it is a configuration issue).

# Architecture

Motion Detector application uses 3 devices:

- led - used for notifying the motion sensor status and application errors. The led is configured to work with GPIO5(Out);
- buzzer - used to emit a sound in order to notify the success of communication with the server after a motion detection. It is configured on GPIO15(Out);
- movement sensor - around which the entire application revolves. It is configured on GPIO4(In).

# How it works?

When the ESP32 is turned on, the application tries to establish an WiFi connection. If the device fails to connect to the WiFi, the application will retry until it succeeds. Then the led will blink one time for one second: this means that the software is configured correctly. After a movement detection, a post request is made which contains the MAC address wrapped in a JSON, useful to identify the device that sent the request. If this request was sent successfully then the the led blinks for less that one second and the buzzer emits a short sound. If the request to the server fails, the led blinks for 2 times. The request then is handled by the server, that I wrote using Java (Spring Boot), and a new message is sent to a Discord channel. So that I receive a notification on my smartphone. If the notification was sent successfully, the server sends a positive status, else, a false is returned wrapped in a JSON.

# How to configure and install it?

Before installing the Motion Detector application on a ESP32, it is necessary to rename the `src/config/config.sample.rs` to `src/config/config.rs`. Then you should change the configuration in the `config.rs` by defining your WiFi SSID, password and your remote server alert request handler.
I suppose that the environment is configured correctly, so that in order to run ESP32 Motion Detector application on an ESP32 device just run `cargo clean && cargo build && cargo run` (sometime I succeed in installing the software by doing a simple `cargo run`, other times i had to hold the boot button of ESP32).

# Photo

![board](images/board.jpg)
3 changes: 3 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
embuild::espidf::sysenv::output();
}
Binary file added images/board.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "esp"
11 changes: 11 additions & 0 deletions sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 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=8000

# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
# This allows to use 1 ms granuality 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
CONFIG_ESP_TASK_WDT_PANIC=n
5 changes: 5 additions & 0 deletions src/config/config.sample.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// rename the file in config.rs
// customize your settings by editing this variables
pub const WIFI_SSID: &str = "wifi name";
pub const WIFI_PASS: &str = "wifi password";
pub const ALERT_URL: &str = "http://server_url:8080/alert";
1 change: 1 addition & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod config;
14 changes: 14 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use anyhow::Ok;
use esp_idf_sys as _;
mod service;
mod util;
use crate::service::orchestrator_service::orchestrate;
mod config;
fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();
esp_idf_svc::log::EspLogger::initialize_default();

orchestrate();

return Ok(());
}
3 changes: 3 additions & 0 deletions src/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod config;
pub mod service;
pub mod util;
102 changes: 102 additions & 0 deletions src/service/client_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use anyhow::{Error, Ok};
use embedded_svc::{http::client::Client as HttpClient, io::Write, utils::io};
use esp_idf_svc::http::client::EspHttpConnection;
use esp_idf_sys as _;
use log::{error, info};
use serde::Serialize;

pub struct ClientService {
alert_url: String,
}

impl ClientService {
pub fn new(alert_url: &str) -> ClientService {
ClientService {
alert_url: alert_url.to_owned(),
}
}

pub fn post_request(&self, mac_address: &str) -> anyhow::Result<(), anyhow::Error> {
let mut client = HttpClient::wrap(EspHttpConnection::new(&Default::default())?);

let payload = serde_json::to_string(&RequestAlert::new(mac_address.to_owned())).unwrap();
let payload = payload.as_bytes();

let content_length_header = format!("{}", payload.len());
let headers = [
("content-type", "application/json"),
("content-length", &*content_length_header),
];

let request = client.post(&self.alert_url, &headers);

if request.is_err() {
let message = format!("connection error: {:?}", request.err());
error!("{}", message);
return Err(Error::msg(message));
}
let mut request = request.unwrap();

if request.write_all(payload).is_err() {
let message = format!("connection error while trying to write all");
error!("{}", message);
return Err(Error::msg(message));
}
if request.flush().is_err() {
let message = format!("connection error while trying to flush");
error!("{}", message);
return Err(Error::msg(message));
}
info!("-> POST {}", self.alert_url);
let response = request.submit();
if response.is_err() {
let message = format!("connection error while trying to read response");
error!("{}", message);
return Err(Error::msg(message));
}
let mut response = response.unwrap();

let status = response.status();
info!("<- {}", status);
let mut buf = [0u8; 1024];
let bytes_read = io::try_read_full(&mut response, &mut buf).map_err(|e| e.0);

if bytes_read.is_err() {
let message = format!(
"connection error while trying to read response: {:?}",
bytes_read.err()
);
error!("{}", message);
return Err(Error::msg(message));
} else {
let bytes_read = bytes_read.unwrap();
info!("Read {} bytes", bytes_read);
match std::str::from_utf8(&buf[0..bytes_read]) {
std::result::Result::Ok(body_string) => info!(
"Response body (truncated to {} bytes): {:?}",
buf.len(),
body_string
),
Err(e) => error!("Error decoding response body: {}", e),
};

// Drain the remaining response bytes
while response.read(&mut buf).unwrap_or(0) > 0 {}
}
Ok(())
}
}

#[derive(Serialize)]
#[warn(non_snake_case)]
struct RequestAlert {
macAddress: String,
}

impl RequestAlert {
pub fn new(mac_address: String) -> RequestAlert {
RequestAlert {
macAddress: mac_address,
}
}
}
3 changes: 3 additions & 0 deletions src/service/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod client_service;
pub mod orchestrator_service;
pub mod peripheral_service;
36 changes: 36 additions & 0 deletions src/service/orchestrator_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use super::{client_service, peripheral_service::PeripheralService};
use crate::{config::config, util::thread_util};
use log::info;

pub fn orchestrate() {
let mut peripheral_service = PeripheralService::new(config::WIFI_SSID, config::WIFI_PASS);
let client_service = client_service::ClientService::new(config::ALERT_URL);

let mut detection = false;
peripheral_service.led_blink_1_time_long();

loop {
if !peripheral_service.is_motion_detected() && detection {
info!("no detection");
detection = false;
peripheral_service.power_off_output_devices();
} else if peripheral_service.is_motion_detected() && !detection {
info!("MOVEMENT DETECTED");
while !peripheral_service.retry_wifi_connection_if_necessary_and_return_status() {
peripheral_service.led_blink_3_time_long();
thread_util::sleep_short();
}
let mac_address = peripheral_service.get_mac_address();
let mac_address = &mac_address.as_str();
if client_service.post_request(mac_address).is_err() {
peripheral_service.led_blink_2_time_long();
detection = false;
} else {
peripheral_service.led_blink_1_time_short();
peripheral_service.buzz_1_time_short();
detection = true;
}
}
thread_util::sleep_time(20);
}
}
Loading

0 comments on commit add04b9

Please sign in to comment.