mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-04-30 18:49:09 +00:00
Compare commits
29 Commits
v1.0
...
21480cef4f
| Author | SHA1 | Date | |
|---|---|---|---|
| 21480cef4f | |||
|
|
fabb14de86 | ||
|
|
6a2d448f86 | ||
|
|
fc7bd8b089 | ||
|
|
3117c55b1c | ||
|
|
593d98df74 | ||
|
|
fa6d1f024c | ||
|
|
36dc52f464 | ||
|
|
6831d7776c | ||
| a015d6b983 | |||
|
|
1ae5250449 | ||
|
|
2f502e908e | ||
|
|
5950279dc4 | ||
| fe6540ca3d | |||
|
|
161ebf9bd2 | ||
|
|
c1b54920ff | ||
|
|
5a2beb1fb3 | ||
|
|
d5c20bf348 | ||
|
|
49027fed99 | ||
|
|
4dda9548d3 | ||
|
|
46e207bd2a | ||
|
|
8cb118e0ee | ||
|
|
9b4df77112 | ||
| 23bb1126a6 | |||
| a97e9c8080 | |||
| 4b39529a65 | |||
| c91d2f070f | |||
| 2e6094ea11 | |||
| 43e964b5a0 |
14
.cargo/config.toml
Normal file
14
.cargo/config.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[target.riscv32imac-unknown-none-elf]
|
||||||
|
runner = "espflash flash --monitor --chip esp32c6"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
rustflags = [
|
||||||
|
# Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.)
|
||||||
|
# NOTE: May negatively impact performance of produced code
|
||||||
|
"-C", "force-frame-pointers",
|
||||||
|
]
|
||||||
|
|
||||||
|
target = "riscv32imac-unknown-none-elf"
|
||||||
|
|
||||||
|
[unstable]
|
||||||
|
build-std = ["alloc", "core"]
|
||||||
Binary file not shown.
Binary file not shown.
BIN
3d_print/LWL.3mf
BIN
3d_print/LWL.3mf
Binary file not shown.
Binary file not shown.
2404
Cargo.lock
generated
2404
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
93
Cargo.toml
93
Cargo.toml
@@ -3,24 +3,81 @@ name = "fw-anwesenheit"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[[bin]]
|
||||||
default = []
|
name = "fw-anwesenheit"
|
||||||
mock_pi = [] # Enable mocking of the rpi hardware
|
path = "./src/main.rs"
|
||||||
|
test = false
|
||||||
|
doctest = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.40", features = ["serde"] }
|
esp-bootloader-esp-idf = "0.1.0"
|
||||||
gpio = "0.4.1"
|
embassy-net = { version = "0.7.0", features = [
|
||||||
regex = "1.11.1"
|
"dhcpv4",
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
"medium-ethernet",
|
||||||
serde_json = "1.0.140"
|
"tcp",
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
"udp",
|
||||||
tokio = { version = "1.44.2", features = ["full"] }
|
] }
|
||||||
rust-embed = "8.7.0"
|
embedded-hal = "=1.0.0"
|
||||||
log = "0.4.27"
|
embedded-io = "0.6.1"
|
||||||
simplelog = "0.12.2"
|
embedded-io-async = "0.6.1"
|
||||||
rppal = { version = "0.22.1", features = ["hal"] }
|
esp-alloc = "0.8.0"
|
||||||
smart-leds = "0.3"
|
esp-hal = { version = "1.0.0-beta.1", features = ["esp32c6", "unstable"] }
|
||||||
ws2812-spi = "0.3"
|
smoltcp = { version = "0.12.0", default-features = false, features = [
|
||||||
rgb = "0.8.50"
|
"medium-ethernet",
|
||||||
anyhow = "1.0.98"
|
"multicast",
|
||||||
|
"proto-dhcpv4",
|
||||||
|
"proto-dns",
|
||||||
|
"proto-ipv4",
|
||||||
|
"socket-dns",
|
||||||
|
"socket-icmp",
|
||||||
|
"socket-raw",
|
||||||
|
"socket-tcp",
|
||||||
|
"socket-udp",
|
||||||
|
] }
|
||||||
|
# for more networking protocol support see https://crates.io/crates/edge-net
|
||||||
|
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [
|
||||||
|
"async",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
|
critical-section = "1.2.0"
|
||||||
|
embassy-executor = { version = "0.7.0", features = ["nightly"] }
|
||||||
|
embassy-time = { version = "0.4.0", features = ["generic-queue-8"] }
|
||||||
|
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6"] }
|
||||||
|
esp-wifi = { version = "0.15.0", features = [
|
||||||
|
"wifi",
|
||||||
|
"builtin-scheduler",
|
||||||
|
"esp-alloc",
|
||||||
|
"esp32c6",
|
||||||
|
"log-04",
|
||||||
|
] }
|
||||||
|
heapless = { version = "0.8.0", default-features = false }
|
||||||
|
static_cell = { version = "2.1.0", features = ["nightly"] }
|
||||||
|
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
|
||||||
|
log = { version = "0.4" }
|
||||||
|
edge-dhcp = { version = "0.6.0", features = ["log"] }
|
||||||
|
edge-nal = "0.5.0"
|
||||||
|
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
|
||||||
|
picoserve = { version = "0.16.0", features = ["embassy", "log"] }
|
||||||
|
embassy-sync = { version = "0.7.0", features = ["log"] }
|
||||||
|
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
|
||||||
|
chrono = { version = "0.4.41", default-features = false }
|
||||||
|
dir-embed = "0.3.0"
|
||||||
|
embedded-sdmmc-dev = "0.8.2"
|
||||||
|
esp-hal-smartled = { git = "https://github.com/esp-rs/esp-hal-community.git", package = "esp-hal-smartled", branch = "main", features = ["esp32c6"]}
|
||||||
|
smart-leds = "0.4.0"
|
||||||
|
serde = { version = "1.0.219", default-features = false, features = ["derive", "alloc"] }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
# Rust debug is too slow.
|
||||||
|
# For debug builds always builds with some optimization
|
||||||
|
opt-level = "s"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
||||||
|
debug = 2
|
||||||
|
debug-assertions = false
|
||||||
|
incremental = false
|
||||||
|
lto = 'fat'
|
||||||
|
opt-level = 's'
|
||||||
|
overflow-checks = false
|
||||||
|
|||||||
61
Makefile
61
Makefile
@@ -1,61 +0,0 @@
|
|||||||
PACKAGE_NAME := fwa
|
|
||||||
VERSION := 1.0
|
|
||||||
ARCH := armhf
|
|
||||||
BUILD_DIR := build
|
|
||||||
DEB_DIR := $(BUILD_DIR)/$(PACKAGE_NAME)-$(VERSION)
|
|
||||||
BIN_DIR := $(DEB_DIR)/usr/local/bin
|
|
||||||
SERVICE_DIR := $(DEB_DIR)/lib/systemd/system
|
|
||||||
CONFIG_DIR := $(DEB_DIR)/etc
|
|
||||||
PM3_DIR := $(DEB_DIR)/usr/share/pm3
|
|
||||||
|
|
||||||
.PHONY: all build clean package prepare_package
|
|
||||||
|
|
||||||
all: package
|
|
||||||
|
|
||||||
build: $(BUILD_DIR)/fwa
|
|
||||||
|
|
||||||
$(BUILD_DIR)/fwa: web/dist
|
|
||||||
cross build --release --target arm-unknown-linux-gnueabihf
|
|
||||||
cp ./target/arm-unknown-linux-gnueabihf/release/fw-anwesenheit $@
|
|
||||||
|
|
||||||
prepare_package: $(DEB_DIR)/DEBIAN $(BIN_DIR)/fwa
|
|
||||||
mkdir -p $(SERVICE_DIR)
|
|
||||||
cp ./service/fwa.service $(SERVICE_DIR)/
|
|
||||||
cp ./service/fwa-fail.service $(SERVICE_DIR)/
|
|
||||||
|
|
||||||
mkdir -p $(CONFIG_DIR)
|
|
||||||
cp ./service/fwa.env $(CONFIG_DIR)/
|
|
||||||
|
|
||||||
mkdir -p $(PM3_DIR)
|
|
||||||
cp -r ./pre-compiled/* $(PM3_DIR)/
|
|
||||||
|
|
||||||
mkdir -p $(DEB_DIR)/var/lib/fwa/
|
|
||||||
|
|
||||||
$(BIN_DIR)/fwa: $(BUILD_DIR)/fwa
|
|
||||||
mkdir -p $(BIN_DIR)
|
|
||||||
cp $< $@
|
|
||||||
|
|
||||||
|
|
||||||
$(DEB_DIR)/DEBIAN:
|
|
||||||
mkdir -p $(DEB_DIR)/DEBIAN
|
|
||||||
echo "Package: $(PACKAGE_NAME)" > $(DEB_DIR)/DEBIAN/control
|
|
||||||
echo "Version: $(VERSION)" >> $(DEB_DIR)/DEBIAN/control
|
|
||||||
echo "Section: utils" >> $(DEB_DIR)/DEBIAN/control
|
|
||||||
echo "Priority: optional" >> $(DEB_DIR)/DEBIAN/control
|
|
||||||
echo "Architecture: $(ARCH)" >> $(DEB_DIR)/DEBIAN/control
|
|
||||||
echo "Depends: libc6 (>= 2.28)" >> $(DEB_DIR)/DEBIAN/control
|
|
||||||
echo "Maintainer: Niklas Kapelle <niklas@kapelle.org>" >> $(DEB_DIR)/DEBIAN/control
|
|
||||||
echo "Description: Feuerwehr anwesenheit" >> $(DEB_DIR)/DEBIAN/control
|
|
||||||
|
|
||||||
echo "/etc/fwa.env" > $(DEB_DIR)/DEBIAN/conffiles
|
|
||||||
|
|
||||||
web/dist:
|
|
||||||
(cd web && npm run build)
|
|
||||||
|
|
||||||
package: prepare_package
|
|
||||||
dpkg-deb --build $(DEB_DIR)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
cargo clean
|
|
||||||
rm -rf web/dist
|
|
||||||
rm -rf $(BUILD_DIR)
|
|
||||||
66
build.rs
Normal file
66
build.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
linker_be_nice();
|
||||||
|
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
||||||
|
println!("cargo:rustc-link-arg=-Tlinkall.x");
|
||||||
|
save_build_time();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_build_time() {
|
||||||
|
let out_dir = env::var("OUT_DIR").unwrap();
|
||||||
|
let dest_path = Path::new(&out_dir).join("build_time.rs");
|
||||||
|
let system_time = std::time::SystemTime::now();
|
||||||
|
let unix_time = system_time
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
println!("cargo:rustc-env=BUILD_TIME={}", unix_time);
|
||||||
|
let content = format!(
|
||||||
|
"/// compile time as UNIX-Timestamp (seconds since 1970-01-01)
|
||||||
|
pub const BUILD_UNIX_TIME: u64 = {};",
|
||||||
|
unix_time
|
||||||
|
);
|
||||||
|
let mut f = File::create(dest_path).unwrap();
|
||||||
|
f.write_all(content.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linker_be_nice() {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
if args.len() > 1 {
|
||||||
|
let kind = &args[1];
|
||||||
|
let what = &args[2];
|
||||||
|
|
||||||
|
match kind.as_str() {
|
||||||
|
"undefined-symbol" => match what.as_str() {
|
||||||
|
"_defmt_timestamp" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
"_stack_start" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("💡 Is the linker script `linkall.x` missing?");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
// we don't have anything helpful for "missing-lib" yet
|
||||||
|
_ => {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-arg=--error-handling-script={}",
|
||||||
|
std::env::current_exe().unwrap().display()
|
||||||
|
);
|
||||||
|
}
|
||||||
24
buzzer.py
24
buzzer.py
@@ -1,24 +0,0 @@
|
|||||||
import RPi.GPIO as GPIO
|
|
||||||
import time
|
|
||||||
|
|
||||||
BUZZER_PIN = 26
|
|
||||||
|
|
||||||
def beep(frequency, duration):
|
|
||||||
pwm = GPIO.PWM(BUZZER_PIN, frequency)
|
|
||||||
pwm.start(50) # 50 % duty cycle
|
|
||||||
time.sleep(duration)
|
|
||||||
pwm.stop()
|
|
||||||
|
|
||||||
def main():
|
|
||||||
GPIO.setmode(GPIO.BCM)
|
|
||||||
GPIO.setup(BUZZER_PIN, GPIO.OUT)
|
|
||||||
|
|
||||||
try:
|
|
||||||
beep(523, 0.3) # C5
|
|
||||||
beep(659, 0.3) # E5
|
|
||||||
beep(784, 0.3) # G5
|
|
||||||
finally:
|
|
||||||
GPIO.cleanup()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,34 +0,0 @@
|
|||||||
@startuml
|
|
||||||
actor user
|
|
||||||
|
|
||||||
|
|
||||||
main -> pm3 :start
|
|
||||||
loop
|
|
||||||
pm3 -> pm3 : look for HITAG
|
|
||||||
end
|
|
||||||
|
|
||||||
user -> pm3 :show HITAG
|
|
||||||
|
|
||||||
pm3 -> parser : parse ID
|
|
||||||
parser -> pm3 : return ID
|
|
||||||
|
|
||||||
pm3 --> main : send ID
|
|
||||||
|
|
||||||
loop
|
|
||||||
main -> main : look for IDs
|
|
||||||
end
|
|
||||||
|
|
||||||
main -> idstore : send ID
|
|
||||||
idstore -> System : ask for day
|
|
||||||
alt
|
|
||||||
System -> idstore : return attendence_day
|
|
||||||
else
|
|
||||||
create collections attendence_day
|
|
||||||
idstore -> attendence_day : create new attendence_day
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
idstore -> attendence_day : add ID
|
|
||||||
attendence_day -> system : save json
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
105757
pcb/fw-anwesenheit/fp-info-cache
Normal file
105757
pcb/fw-anwesenheit/fp-info-cache
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9320
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pcb
Normal file
9320
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pcb
Normal file
File diff suppressed because it is too large
Load Diff
131
pcb/fw-anwesenheit/fw-anwesenheit.kicad_prl
Normal file
131
pcb/fw-anwesenheit/fw-anwesenheit.kicad_prl
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"board": {
|
||||||
|
"active_layer": 25,
|
||||||
|
"active_layer_preset": "",
|
||||||
|
"auto_track_width": true,
|
||||||
|
"hidden_netclasses": [],
|
||||||
|
"hidden_nets": [],
|
||||||
|
"high_contrast_mode": 0,
|
||||||
|
"net_color_mode": 1,
|
||||||
|
"opacity": {
|
||||||
|
"images": 0.6,
|
||||||
|
"pads": 1.0,
|
||||||
|
"shapes": 1.0,
|
||||||
|
"tracks": 1.0,
|
||||||
|
"vias": 1.0,
|
||||||
|
"zones": 0.6
|
||||||
|
},
|
||||||
|
"selection_filter": {
|
||||||
|
"dimensions": true,
|
||||||
|
"footprints": true,
|
||||||
|
"graphics": true,
|
||||||
|
"keepouts": true,
|
||||||
|
"lockedItems": false,
|
||||||
|
"otherItems": true,
|
||||||
|
"pads": true,
|
||||||
|
"text": true,
|
||||||
|
"tracks": true,
|
||||||
|
"vias": true,
|
||||||
|
"zones": true
|
||||||
|
},
|
||||||
|
"visible_items": [
|
||||||
|
"vias",
|
||||||
|
"footprint_text",
|
||||||
|
"footprint_anchors",
|
||||||
|
"ratsnest",
|
||||||
|
"grid",
|
||||||
|
"footprints_front",
|
||||||
|
"footprints_back",
|
||||||
|
"footprint_values",
|
||||||
|
"footprint_references",
|
||||||
|
"tracks",
|
||||||
|
"drc_errors",
|
||||||
|
"drawing_sheet",
|
||||||
|
"bitmaps",
|
||||||
|
"pads",
|
||||||
|
"zones",
|
||||||
|
"drc_warnings",
|
||||||
|
"drc_exclusions",
|
||||||
|
"locked_item_shadows",
|
||||||
|
"conflict_shadows",
|
||||||
|
"shapes"
|
||||||
|
],
|
||||||
|
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
|
||||||
|
"zone_display_mode": 0
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"repo_type": "",
|
||||||
|
"repo_username": "",
|
||||||
|
"ssh_key": ""
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filename": "fw-anwesenheit.kicad_prl",
|
||||||
|
"version": 5
|
||||||
|
},
|
||||||
|
"net_inspector_panel": {
|
||||||
|
"col_hidden": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"col_order": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"col_widths": [
|
||||||
|
162,
|
||||||
|
147,
|
||||||
|
91,
|
||||||
|
72,
|
||||||
|
91,
|
||||||
|
100,
|
||||||
|
91,
|
||||||
|
76,
|
||||||
|
91,
|
||||||
|
91
|
||||||
|
],
|
||||||
|
"custom_group_rules": [],
|
||||||
|
"expanded_rows": [],
|
||||||
|
"filter_by_net_name": true,
|
||||||
|
"filter_by_netclass": true,
|
||||||
|
"filter_text": "",
|
||||||
|
"group_by_constraint": false,
|
||||||
|
"group_by_netclass": false,
|
||||||
|
"show_unconnected_nets": false,
|
||||||
|
"show_zero_pad_nets": false,
|
||||||
|
"sort_ascending": true,
|
||||||
|
"sorting_column": 0
|
||||||
|
},
|
||||||
|
"open_jobsets": [],
|
||||||
|
"project": {
|
||||||
|
"files": []
|
||||||
|
},
|
||||||
|
"schematic": {
|
||||||
|
"selection_filter": {
|
||||||
|
"graphics": true,
|
||||||
|
"images": true,
|
||||||
|
"labels": true,
|
||||||
|
"lockedItems": false,
|
||||||
|
"otherItems": true,
|
||||||
|
"pins": true,
|
||||||
|
"symbols": true,
|
||||||
|
"text": true,
|
||||||
|
"wires": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
618
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pro
Normal file
618
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pro
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
{
|
||||||
|
"board": {
|
||||||
|
"3dviewports": [],
|
||||||
|
"design_settings": {
|
||||||
|
"defaults": {
|
||||||
|
"apply_defaults_to_fp_fields": false,
|
||||||
|
"apply_defaults_to_fp_shapes": false,
|
||||||
|
"apply_defaults_to_fp_text": false,
|
||||||
|
"board_outline_line_width": 0.05,
|
||||||
|
"copper_line_width": 0.2,
|
||||||
|
"copper_text_italic": false,
|
||||||
|
"copper_text_size_h": 1.5,
|
||||||
|
"copper_text_size_v": 1.5,
|
||||||
|
"copper_text_thickness": 0.3,
|
||||||
|
"copper_text_upright": false,
|
||||||
|
"courtyard_line_width": 0.05,
|
||||||
|
"dimension_precision": 4,
|
||||||
|
"dimension_units": 3,
|
||||||
|
"dimensions": {
|
||||||
|
"arrow_length": 1270000,
|
||||||
|
"extension_offset": 500000,
|
||||||
|
"keep_text_aligned": true,
|
||||||
|
"suppress_zeroes": true,
|
||||||
|
"text_position": 0,
|
||||||
|
"units_format": 0
|
||||||
|
},
|
||||||
|
"fab_line_width": 0.1,
|
||||||
|
"fab_text_italic": false,
|
||||||
|
"fab_text_size_h": 1.0,
|
||||||
|
"fab_text_size_v": 1.0,
|
||||||
|
"fab_text_thickness": 0.15,
|
||||||
|
"fab_text_upright": false,
|
||||||
|
"other_line_width": 0.1,
|
||||||
|
"other_text_italic": false,
|
||||||
|
"other_text_size_h": 1.0,
|
||||||
|
"other_text_size_v": 1.0,
|
||||||
|
"other_text_thickness": 0.15,
|
||||||
|
"other_text_upright": false,
|
||||||
|
"pads": {
|
||||||
|
"drill": 0.85,
|
||||||
|
"height": 2.0,
|
||||||
|
"width": 1.6
|
||||||
|
},
|
||||||
|
"silk_line_width": 0.1,
|
||||||
|
"silk_text_italic": false,
|
||||||
|
"silk_text_size_h": 1.0,
|
||||||
|
"silk_text_size_v": 1.0,
|
||||||
|
"silk_text_thickness": 0.1,
|
||||||
|
"silk_text_upright": false,
|
||||||
|
"zones": {
|
||||||
|
"min_clearance": 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diff_pair_dimensions": [],
|
||||||
|
"drc_exclusions": [],
|
||||||
|
"meta": {
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"rule_severities": {
|
||||||
|
"annular_width": "error",
|
||||||
|
"clearance": "error",
|
||||||
|
"connection_width": "warning",
|
||||||
|
"copper_edge_clearance": "error",
|
||||||
|
"copper_sliver": "warning",
|
||||||
|
"courtyards_overlap": "error",
|
||||||
|
"creepage": "error",
|
||||||
|
"diff_pair_gap_out_of_range": "error",
|
||||||
|
"diff_pair_uncoupled_length_too_long": "error",
|
||||||
|
"drill_out_of_range": "error",
|
||||||
|
"duplicate_footprints": "warning",
|
||||||
|
"extra_footprint": "warning",
|
||||||
|
"footprint": "error",
|
||||||
|
"footprint_filters_mismatch": "ignore",
|
||||||
|
"footprint_symbol_mismatch": "warning",
|
||||||
|
"footprint_type_mismatch": "ignore",
|
||||||
|
"hole_clearance": "error",
|
||||||
|
"hole_to_hole": "warning",
|
||||||
|
"holes_co_located": "warning",
|
||||||
|
"invalid_outline": "error",
|
||||||
|
"isolated_copper": "warning",
|
||||||
|
"item_on_disabled_layer": "error",
|
||||||
|
"items_not_allowed": "error",
|
||||||
|
"length_out_of_range": "error",
|
||||||
|
"lib_footprint_issues": "warning",
|
||||||
|
"lib_footprint_mismatch": "warning",
|
||||||
|
"malformed_courtyard": "error",
|
||||||
|
"microvia_drill_out_of_range": "error",
|
||||||
|
"mirrored_text_on_front_layer": "warning",
|
||||||
|
"missing_courtyard": "ignore",
|
||||||
|
"missing_footprint": "warning",
|
||||||
|
"net_conflict": "warning",
|
||||||
|
"nonmirrored_text_on_back_layer": "warning",
|
||||||
|
"npth_inside_courtyard": "ignore",
|
||||||
|
"padstack": "warning",
|
||||||
|
"pth_inside_courtyard": "ignore",
|
||||||
|
"shorting_items": "error",
|
||||||
|
"silk_edge_clearance": "warning",
|
||||||
|
"silk_over_copper": "warning",
|
||||||
|
"silk_overlap": "warning",
|
||||||
|
"skew_out_of_range": "error",
|
||||||
|
"solder_mask_bridge": "error",
|
||||||
|
"starved_thermal": "error",
|
||||||
|
"text_height": "warning",
|
||||||
|
"text_on_edge_cuts": "error",
|
||||||
|
"text_thickness": "warning",
|
||||||
|
"through_hole_pad_without_hole": "error",
|
||||||
|
"too_many_vias": "error",
|
||||||
|
"track_angle": "error",
|
||||||
|
"track_dangling": "warning",
|
||||||
|
"track_segment_length": "error",
|
||||||
|
"track_width": "error",
|
||||||
|
"tracks_crossing": "error",
|
||||||
|
"unconnected_items": "error",
|
||||||
|
"unresolved_variable": "error",
|
||||||
|
"via_dangling": "warning",
|
||||||
|
"zones_intersect": "error"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"max_error": 0.005,
|
||||||
|
"min_clearance": 0.0,
|
||||||
|
"min_connection": 0.0,
|
||||||
|
"min_copper_edge_clearance": 0.5,
|
||||||
|
"min_groove_width": 0.0,
|
||||||
|
"min_hole_clearance": 0.25,
|
||||||
|
"min_hole_to_hole": 0.25,
|
||||||
|
"min_microvia_diameter": 0.2,
|
||||||
|
"min_microvia_drill": 0.1,
|
||||||
|
"min_resolved_spokes": 2,
|
||||||
|
"min_silk_clearance": 0.0,
|
||||||
|
"min_text_height": 0.8,
|
||||||
|
"min_text_thickness": 0.08,
|
||||||
|
"min_through_hole_diameter": 0.3,
|
||||||
|
"min_track_width": 0.0,
|
||||||
|
"min_via_annular_width": 0.1,
|
||||||
|
"min_via_diameter": 0.5,
|
||||||
|
"solder_mask_to_copper_clearance": 0.0,
|
||||||
|
"use_height_for_length_calcs": true
|
||||||
|
},
|
||||||
|
"teardrop_options": [
|
||||||
|
{
|
||||||
|
"td_onpthpad": true,
|
||||||
|
"td_onroundshapesonly": false,
|
||||||
|
"td_onsmdpad": true,
|
||||||
|
"td_ontrackend": false,
|
||||||
|
"td_onvia": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"teardrop_parameters": [
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_round_shape",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_rect_shape",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_track_end",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"track_widths": [],
|
||||||
|
"tuning_pattern_settings": {
|
||||||
|
"diff_pair_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 1.0
|
||||||
|
},
|
||||||
|
"diff_pair_skew_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 0.6
|
||||||
|
},
|
||||||
|
"single_track_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 0.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"via_dimensions": [],
|
||||||
|
"zones_allow_external_fillets": false
|
||||||
|
},
|
||||||
|
"ipc2581": {
|
||||||
|
"dist": "",
|
||||||
|
"distpn": "",
|
||||||
|
"internal_id": "",
|
||||||
|
"mfg": "",
|
||||||
|
"mpn": ""
|
||||||
|
},
|
||||||
|
"layer_pairs": [],
|
||||||
|
"layer_presets": [],
|
||||||
|
"viewports": []
|
||||||
|
},
|
||||||
|
"boards": [],
|
||||||
|
"cvpcb": {
|
||||||
|
"equivalence_files": []
|
||||||
|
},
|
||||||
|
"erc": {
|
||||||
|
"erc_exclusions": [],
|
||||||
|
"meta": {
|
||||||
|
"version": 0
|
||||||
|
},
|
||||||
|
"pin_map": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"rule_severities": {
|
||||||
|
"bus_definition_conflict": "error",
|
||||||
|
"bus_entry_needed": "error",
|
||||||
|
"bus_to_bus_conflict": "error",
|
||||||
|
"bus_to_net_conflict": "error",
|
||||||
|
"different_unit_footprint": "error",
|
||||||
|
"different_unit_net": "error",
|
||||||
|
"duplicate_reference": "error",
|
||||||
|
"duplicate_sheet_names": "error",
|
||||||
|
"endpoint_off_grid": "warning",
|
||||||
|
"extra_units": "error",
|
||||||
|
"footprint_filter": "ignore",
|
||||||
|
"footprint_link_issues": "warning",
|
||||||
|
"four_way_junction": "ignore",
|
||||||
|
"global_label_dangling": "warning",
|
||||||
|
"hier_label_mismatch": "error",
|
||||||
|
"label_dangling": "error",
|
||||||
|
"label_multiple_wires": "warning",
|
||||||
|
"lib_symbol_issues": "warning",
|
||||||
|
"lib_symbol_mismatch": "warning",
|
||||||
|
"missing_bidi_pin": "warning",
|
||||||
|
"missing_input_pin": "warning",
|
||||||
|
"missing_power_pin": "error",
|
||||||
|
"missing_unit": "warning",
|
||||||
|
"multiple_net_names": "warning",
|
||||||
|
"net_not_bus_member": "warning",
|
||||||
|
"no_connect_connected": "warning",
|
||||||
|
"no_connect_dangling": "warning",
|
||||||
|
"pin_not_connected": "error",
|
||||||
|
"pin_not_driven": "error",
|
||||||
|
"pin_to_pin": "warning",
|
||||||
|
"power_pin_not_driven": "error",
|
||||||
|
"same_local_global_label": "warning",
|
||||||
|
"similar_label_and_power": "warning",
|
||||||
|
"similar_labels": "warning",
|
||||||
|
"similar_power": "warning",
|
||||||
|
"simulation_model_issue": "ignore",
|
||||||
|
"single_global_label": "ignore",
|
||||||
|
"unannotated": "error",
|
||||||
|
"unconnected_wire_endpoint": "warning",
|
||||||
|
"unit_value_mismatch": "error",
|
||||||
|
"unresolved_variable": "error",
|
||||||
|
"wire_dangling": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"pinned_footprint_libs": [],
|
||||||
|
"pinned_symbol_libs": []
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filename": "fw-anwesenheit.kicad_pro",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"net_settings": {
|
||||||
|
"classes": [
|
||||||
|
{
|
||||||
|
"bus_width": 12,
|
||||||
|
"clearance": 0.2,
|
||||||
|
"diff_pair_gap": 0.25,
|
||||||
|
"diff_pair_via_gap": 0.25,
|
||||||
|
"diff_pair_width": 0.2,
|
||||||
|
"line_style": 0,
|
||||||
|
"microvia_diameter": 0.3,
|
||||||
|
"microvia_drill": 0.1,
|
||||||
|
"name": "Default",
|
||||||
|
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||||
|
"priority": 2147483647,
|
||||||
|
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||||
|
"track_width": 0.2,
|
||||||
|
"via_diameter": 0.6,
|
||||||
|
"via_drill": 0.3,
|
||||||
|
"wire_width": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"version": 4
|
||||||
|
},
|
||||||
|
"net_colors": null,
|
||||||
|
"netclass_assignments": null,
|
||||||
|
"netclass_patterns": []
|
||||||
|
},
|
||||||
|
"pcbnew": {
|
||||||
|
"last_paths": {
|
||||||
|
"gencad": "",
|
||||||
|
"idf": "",
|
||||||
|
"netlist": "",
|
||||||
|
"plot": "",
|
||||||
|
"pos_files": "",
|
||||||
|
"specctra_dsn": "",
|
||||||
|
"step": "",
|
||||||
|
"svg": "",
|
||||||
|
"vrml": ""
|
||||||
|
},
|
||||||
|
"page_layout_descr_file": ""
|
||||||
|
},
|
||||||
|
"schematic": {
|
||||||
|
"annotate_start_num": 0,
|
||||||
|
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||||
|
"bom_fmt_presets": [],
|
||||||
|
"bom_fmt_settings": {
|
||||||
|
"field_delimiter": ",",
|
||||||
|
"keep_line_breaks": false,
|
||||||
|
"keep_tabs": false,
|
||||||
|
"name": "CSV",
|
||||||
|
"ref_delimiter": ",",
|
||||||
|
"ref_range_delimiter": "",
|
||||||
|
"string_delimiter": "\""
|
||||||
|
},
|
||||||
|
"bom_presets": [],
|
||||||
|
"bom_settings": {
|
||||||
|
"exclude_dnp": false,
|
||||||
|
"fields_ordered": [
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Reference",
|
||||||
|
"name": "Reference",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Qty",
|
||||||
|
"name": "${QUANTITY}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Value",
|
||||||
|
"name": "Value",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "DNP",
|
||||||
|
"name": "${DNP}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Exclude from BOM",
|
||||||
|
"name": "${EXCLUDE_FROM_BOM}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Exclude from Board",
|
||||||
|
"name": "${EXCLUDE_FROM_BOARD}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Footprint",
|
||||||
|
"name": "Footprint",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Datasheet",
|
||||||
|
"name": "Datasheet",
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filter_string": "",
|
||||||
|
"group_symbols": true,
|
||||||
|
"include_excluded_from_bom": true,
|
||||||
|
"name": "Default Editing",
|
||||||
|
"sort_asc": true,
|
||||||
|
"sort_field": "Referenz"
|
||||||
|
},
|
||||||
|
"connection_grid_size": 50.0,
|
||||||
|
"drawing": {
|
||||||
|
"dashed_lines_dash_length_ratio": 12.0,
|
||||||
|
"dashed_lines_gap_length_ratio": 3.0,
|
||||||
|
"default_line_thickness": 6.0,
|
||||||
|
"default_text_size": 50.0,
|
||||||
|
"field_names": [],
|
||||||
|
"intersheets_ref_own_page": false,
|
||||||
|
"intersheets_ref_prefix": "",
|
||||||
|
"intersheets_ref_short": false,
|
||||||
|
"intersheets_ref_show": false,
|
||||||
|
"intersheets_ref_suffix": "",
|
||||||
|
"junction_size_choice": 3,
|
||||||
|
"label_size_ratio": 0.375,
|
||||||
|
"operating_point_overlay_i_precision": 3,
|
||||||
|
"operating_point_overlay_i_range": "~A",
|
||||||
|
"operating_point_overlay_v_precision": 3,
|
||||||
|
"operating_point_overlay_v_range": "~V",
|
||||||
|
"overbar_offset_ratio": 1.23,
|
||||||
|
"pin_symbol_size": 25.0,
|
||||||
|
"text_offset_ratio": 0.15
|
||||||
|
},
|
||||||
|
"legacy_lib_dir": "",
|
||||||
|
"legacy_lib_list": [],
|
||||||
|
"meta": {
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"net_format_name": "",
|
||||||
|
"page_layout_descr_file": "",
|
||||||
|
"plot_directory": "",
|
||||||
|
"space_save_all_events": true,
|
||||||
|
"spice_current_sheet_as_root": false,
|
||||||
|
"spice_external_command": "spice \"%I\"",
|
||||||
|
"spice_model_current_sheet_as_root": true,
|
||||||
|
"spice_save_all_currents": false,
|
||||||
|
"spice_save_all_dissipations": false,
|
||||||
|
"spice_save_all_voltages": false,
|
||||||
|
"subpart_first_id": 65,
|
||||||
|
"subpart_id_separator": 0
|
||||||
|
},
|
||||||
|
"sheets": [
|
||||||
|
[
|
||||||
|
"ccbf1fda-befd-42da-bcb2-5d3829184012",
|
||||||
|
"Root"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"text_variables": {}
|
||||||
|
}
|
||||||
11392
pcb/fw-anwesenheit/fw-anwesenheit.kicad_sch
Normal file
11392
pcb/fw-anwesenheit/fw-anwesenheit.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
23
pm3_mock.sh
23
pm3_mock.sh
@@ -1,23 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
generate_random_hex() {
|
|
||||||
openssl rand -hex 4 | tr '[:lower:]' '[:upper:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
output_hotspot_id() {
|
|
||||||
echo "[+] UID.... C1532B57"
|
|
||||||
}
|
|
||||||
|
|
||||||
trap 'output_hotspot_id' SIGUSR1
|
|
||||||
|
|
||||||
echo "Proxmark 3 mock script"
|
|
||||||
echo "Outputs a random ID every 1 to 5 seconds"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
random_id=$(generate_random_hex)
|
|
||||||
echo "[+] UID.... $random_id"
|
|
||||||
if (( RANDOM % 2 == 0 )); then
|
|
||||||
echo "[+] UID.... $random_id"
|
|
||||||
fi
|
|
||||||
sleep "$((RANDOM % 5 + 1))"
|
|
||||||
done
|
|
||||||
Binary file not shown.
Binary file not shown.
550
pre-compiled/pm3
550
pre-compiled/pm3
@@ -1,550 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Usage: run option -h to get help
|
|
||||||
|
|
||||||
# BT auto detection
|
|
||||||
# Shall we look for white HC-06-USB dongle ?
|
|
||||||
FINDBTDONGLE=true
|
|
||||||
# Shall we look for rfcomm interface ?
|
|
||||||
FINDBTRFCOMM=true
|
|
||||||
# Shall we look for registered BT device ? (Linux only)
|
|
||||||
FINDBTDIRECT=true
|
|
||||||
|
|
||||||
PM3PATH=$(dirname "$0")
|
|
||||||
EVALENV=""
|
|
||||||
FULLIMAGE="fullimage.elf"
|
|
||||||
BOOTIMAGE="bootrom.elf"
|
|
||||||
|
|
||||||
#Skip check if --list is used
|
|
||||||
if [ ! "$1" == "--list" ]; then
|
|
||||||
# try pm3 dirs in current repo workdir
|
|
||||||
if [ -d "$PM3PATH/client/" ]; then
|
|
||||||
if [ -x "$PM3PATH/client/proxmark3" ]; then
|
|
||||||
CLIENT="$PM3PATH/client/proxmark3"
|
|
||||||
elif [ -x "$PM3PATH/client/build/proxmark3" ]; then
|
|
||||||
CLIENT="$PM3PATH/client/build/proxmark3"
|
|
||||||
else
|
|
||||||
echo >&2 "[!!] In devel workdir but no executable found, did you compile it?"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# try install dir
|
|
||||||
elif [ -x "$PM3PATH/proxmark3" ]; then
|
|
||||||
CLIENT="$PM3PATH/proxmark3"
|
|
||||||
else
|
|
||||||
# hope it's installed somehow, still not sure where fw images and pm3.py are...
|
|
||||||
CLIENT="proxmark3"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# LeakSanitizer suppressions
|
|
||||||
if [ -e .lsan_suppressions ]; then
|
|
||||||
EVALENV+=" LSAN_OPTIONS=suppressions=.lsan_suppressions"
|
|
||||||
fi
|
|
||||||
if [ "$EVALENV" != "" ]; then
|
|
||||||
EVALENV="export $EVALENV"
|
|
||||||
fi
|
|
||||||
PM3LIST=()
|
|
||||||
SHOWLIST=false
|
|
||||||
|
|
||||||
function get_pm3_list_Linux {
|
|
||||||
N=$1
|
|
||||||
PM3LIST=()
|
|
||||||
if [ ! -c "/dev/tty0" ]; then
|
|
||||||
echo >&2 "[!!] Script cannot access /dev/ttyXXX files, insufficient privileges"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
for DEV in $(find /dev/ttyACM* 2>/dev/null); do
|
|
||||||
if command -v udevadm >/dev/null; then
|
|
||||||
# WSL1 detection
|
|
||||||
if udevadm info -q property -n "$DEV" | grep -q "ID_VENDOR=proxmark.org"; then
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# WSL2 with usbipd detection - doesn't report same things as WSL1
|
|
||||||
if grep -q "proxmark.org" "/sys/class/tty/${DEV#/dev/}/../../../manufacturer" 2>/dev/null; then
|
|
||||||
if echo "${PM3LIST[*]}" | grep -qv "${DEV}"; then
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if $FINDBTDONGLE; then
|
|
||||||
# check if the HC-06-USB white dongle is present (still, that doesn't tell us if it's paired with a Proxmark3...)
|
|
||||||
for DEV in $(find /dev/ttyUSB* 2>/dev/null); do
|
|
||||||
if command -v udevadm >/dev/null; then
|
|
||||||
if udevadm info -q property -n "$DEV" | grep -q "ID_MODEL=CP2104_USB_to_UART_Bridge_Controller"; then
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if grep -q "DRIVER=cp210x" "/sys/class/tty/${DEV#/dev/}/../../uevent" 2>/dev/null; then
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
if $FINDBTRFCOMM; then
|
|
||||||
# check if the MAC of a Proxmark3 was bound to a local rfcomm interface
|
|
||||||
# (on OSes without deprecated rfcomm and hcitool, the loop will be simply skipped)
|
|
||||||
for DEVMAC in $(rfcomm -a 2>/dev/null | grep " 20:19:0[45]" | sed 's/^\(.*\): \([0-9:]*\) .*/\1@\2/'); do
|
|
||||||
DEV=${DEVMAC/@*/}
|
|
||||||
MAC=${DEVMAC/*@/}
|
|
||||||
# check which are Proxmark3 and, side-effect, if they're actually present
|
|
||||||
if hcitool name "$MAC" | grep -q "PM3"; then
|
|
||||||
PM3LIST+=("/dev/$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
if $FINDBTDIRECT; then
|
|
||||||
# check if the MAC of a Proxmark3 was registered in the known devices
|
|
||||||
for MAC in $(dbus-send --system --print-reply --type=method_call --dest='org.bluez' '/' org.freedesktop.DBus.ObjectManager.GetManagedObjects 2>/dev/null|\
|
|
||||||
awk '/"Address"/{getline;gsub(/"/,"",$3);a=$3}/Name/{getline;if (/PM3_RDV4/ || /Proxmark3 SE/) print a}'); do
|
|
||||||
PM3LIST+=("bt:$MAC")
|
|
||||||
done
|
|
||||||
# we don't probe the device so there is no guarantee the device is actually present
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_pm3_list_macOS {
|
|
||||||
N=$1
|
|
||||||
PM3LIST=()
|
|
||||||
for DEV in $(ioreg -r -c "IOUSBHostDevice" -l | awk -F '"' '
|
|
||||||
$2=="USB Vendor Name"{b=($4=="proxmark.org")}
|
|
||||||
b==1 && $2=="IODialinDevice"{print $4}'); do
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_pm3_list_Windows {
|
|
||||||
N=$1
|
|
||||||
PM3LIST=()
|
|
||||||
|
|
||||||
# Normal SERIAL PORTS (COM)
|
|
||||||
for DEV in $(wmic /locale:ms_409 path Win32_SerialPort Where "PNPDeviceID LIKE '%VID_9AC4&PID_4B8F%' Or PNPDeviceID LIKE '%VID_2D2D&PID_504D%'" Get DeviceID 2>/dev/null | awk -b '/^COM/{print $1}'); do
|
|
||||||
DEV=${DEV/ */}
|
|
||||||
#prevent soft bricking when using pm3-flash-all on an outdated bootloader
|
|
||||||
if [ $(basename -- "$0") = "pm3-flash-all" ]; then
|
|
||||||
line=$(wmic /locale:ms_409 path Win32_SerialPort Where "DeviceID='$DEV'" Get PNPDeviceID 2>/dev/null | awk -b '/^USB/{print $1}');
|
|
||||||
if [[ ! $line =~ ^"USB\VID_9AC4&PID_4B8F\ICEMAN" ]]; then
|
|
||||||
echo -e "\033[0;31m[!] Using pm3-flash-all on an oudated bootloader, use pm3-flash-bootrom first!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
#BT direct SERIAL PORTS (COM)
|
|
||||||
if $FINDBTRFCOMM; then
|
|
||||||
for DEV in $(wmic /locale:ms_409 path Win32_PnPEntity Where "Caption LIKE '%Bluetooth%(COM%'" Get Name 2> /dev/null | awk -b 'match($0,/(COM[0-9]+)/,m){print m[1]}'); do
|
|
||||||
DEV=${DEV/ */}
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
#white BT dongle SERIAL PORTS (COM)
|
|
||||||
if $FINDBTDONGLE; then
|
|
||||||
for DEV in $(wmic /locale:ms_409 path Win32_SerialPort Where "PNPDeviceID LIKE '%VID_10C4&PID_EA60%'" Get DeviceID 2>/dev/null | awk -b '/^COM/{print $1}'); do
|
|
||||||
DEV=${DEV/ */}
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_pm3_list_WSL {
|
|
||||||
N=$1
|
|
||||||
PM3LIST=()
|
|
||||||
|
|
||||||
# Normal SERIAL PORTS (COM)
|
|
||||||
for DEV in $($PSHEXE -command "Get-CimInstance -ClassName Win32_serialport | Where-Object {\$_.PNPDeviceID -like '*VID_9AC4&PID_4B8F*' -or \$_.PNPDeviceID -like '*VID_2D2D&PID_504D*'} | Select -expandproperty DeviceID" 2>/dev/null); do
|
|
||||||
DEV=$(echo $DEV | tr -dc '[:print:]')
|
|
||||||
_comport=$DEV
|
|
||||||
DEV=$(echo $DEV | sed -nr 's#^COM([0-9]+)\b#/dev/ttyS\1#p')
|
|
||||||
# ttyS counterpart takes some more time to appear
|
|
||||||
if [ -e "$DEV" ]; then
|
|
||||||
|
|
||||||
#prevent soft bricking when using pm3-flash-all on an outdated bootloader
|
|
||||||
if [ $(basename -- "$0") = "pm3-flash-all" ]; then
|
|
||||||
line=$($PSHEXE -command "Get-CimInstance -ClassName Win32_serialport | Where-Object {\$_.DeviceID -eq '$_comport'} | Select -expandproperty PNPDeviceID" 2>/dev/null | tr -dc '[:print:]');
|
|
||||||
if [[ ! $line =~ ^"USB\VID_9AC4&PID_4B8F\ICEMAN" ]]; then
|
|
||||||
echo -e "\033[0;31m[!] Using pm3-flash-all on an oudated bootloader, use pm3-flash-bootrom first!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ! -w "$DEV" ]; then
|
|
||||||
echo "[!] Let's give users read/write access to $DEV"
|
|
||||||
sudo chmod 666 "$DEV"
|
|
||||||
fi
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
#BT direct SERIAL PORTS (COM)
|
|
||||||
if $FINDBTRFCOMM; then
|
|
||||||
for DEV in $($PSHEXE -command "Get-CimInstance -ClassName Win32_PnPEntity | Where-Object Caption -like 'Standard Serial over Bluetooth link (COM*' | Select Name" 2> /dev/null | sed -nr 's#.*\bCOM([0-9]+)\b.*#/dev/ttyS\1#p'); do
|
|
||||||
# ttyS counterpart takes some more time to appear
|
|
||||||
if [ -e "$DEV" ]; then
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ! -w "$DEV" ]; then
|
|
||||||
echo "[!] Let's give users read/write access to $DEV"
|
|
||||||
sudo chmod 666 "$DEV"
|
|
||||||
fi
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
#white BT dongle SERIAL PORTS (COM)
|
|
||||||
if $FINDBTDONGLE; then
|
|
||||||
for DEV in $($PSHEXE -command "Get-CimInstance -ClassName Win32_serialport | Where-Object PNPDeviceID -like '*VID_10C4&PID_EA60*' | Select DeviceID" 2>/dev/null | sed -nr 's#^COM([0-9]+)\b#/dev/ttyS\1#p'); do
|
|
||||||
# ttyS counterpart takes some more time to appear
|
|
||||||
if [ -e "$DEV" ]; then
|
|
||||||
PM3LIST+=("$DEV")
|
|
||||||
if [ ! -w "$DEV" ]; then
|
|
||||||
echo "[!] Let's give users read/write access to $DEV"
|
|
||||||
sudo chmod 666 "$DEV"
|
|
||||||
fi
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT=$(basename -- "$0")
|
|
||||||
|
|
||||||
if [ "$SCRIPT" = "pm3" ]; then
|
|
||||||
CMD() { eval "$EVALENV"; $CLIENT "$@"; }
|
|
||||||
HELP() {
|
|
||||||
cat << EOF
|
|
||||||
|
|
||||||
Quick helper script for proxmark3 client when working with a Proxmark3 device
|
|
||||||
|
|
||||||
Description:
|
|
||||||
The usage is the same as for the proxmark3 client, with the following differences:
|
|
||||||
* the correct port name will be automatically guessed;
|
|
||||||
* the script will wait for a Proxmark3 to be connected (same as option -w of the client).
|
|
||||||
You can also specify a first option -n N to access the Nth Proxmark3 connected.
|
|
||||||
To see a list of available ports, use --list.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
$SCRIPT [-n <N>] [<any other proxmark3 client option>]
|
|
||||||
$SCRIPT [--list] [-h|--help] [-hh|--helpclient]
|
|
||||||
$SCRIPT [-o|--offline]
|
|
||||||
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
-h/--help this help
|
|
||||||
-hh/--helpclient proxmark3 client help (the script will forward these options)
|
|
||||||
--list list all detected com ports
|
|
||||||
-n <N> connect device referred to the N:th number on the --list output
|
|
||||||
-o/--offline shortcut to use directly the proxmark3 client without guessing ports
|
|
||||||
|
|
||||||
Samples:
|
|
||||||
./$SCRIPT -- Auto detect/ select com port in the following order BT, USB/CDC, BT DONGLE
|
|
||||||
./$SCRIPT -p /dev/ttyACM0 -- connect to port /dev/ttyACM0
|
|
||||||
./$SCRIPT -n 2 -- use second item from the --list output
|
|
||||||
./$SCRIPT -c 'lf search' -i -- run command and stay in client once completed
|
|
||||||
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
elif [ "$SCRIPT" = "pm3-flash" ]; then
|
|
||||||
FINDBTDONGLE=false
|
|
||||||
FINDBTRFCOMM=false
|
|
||||||
FINDBTDIRECT=false
|
|
||||||
CMD() {
|
|
||||||
ARGS=("--port" "$1" "--flash")
|
|
||||||
shift;
|
|
||||||
while [ "$1" != "" ]; do
|
|
||||||
if [ "$1" == "-b" ]; then
|
|
||||||
ARGS+=("--unlock-bootloader")
|
|
||||||
elif [ "$1" == "--force" ]; then
|
|
||||||
ARGS+=("--force")
|
|
||||||
else
|
|
||||||
ARGS+=("--image" "$1")
|
|
||||||
fi
|
|
||||||
shift;
|
|
||||||
done
|
|
||||||
$CLIENT "${ARGS[@]}";
|
|
||||||
}
|
|
||||||
HELP() {
|
|
||||||
cat << EOF
|
|
||||||
Quick helper script for flashing a Proxmark3 device via USB
|
|
||||||
|
|
||||||
Description:
|
|
||||||
The usage is similar to the old proxmark3-flasher binary, except that the correct port name will be automatically guessed.
|
|
||||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
|
||||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
|
||||||
To see a list of available ports, use --list.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
$SCRIPT [-n <N>] [-b] image.elf [image.elf...]
|
|
||||||
$SCRIPT --list
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-b Enable flashing of bootloader area (DANGEROUS)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
$SCRIPT -b bootrom.elf fullimage.elf
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
elif [ "$SCRIPT" = "pm3-flash-all" ]; then
|
|
||||||
FINDBTDONGLE=false
|
|
||||||
FINDBTRFCOMM=false
|
|
||||||
FINDBTDIRECT=false
|
|
||||||
|
|
||||||
|
|
||||||
CMD() {
|
|
||||||
ARGS=("--port" "$1" "--flash" "--unlock-bootloader" "--image" "$BOOTIMAGE" "--image" "$FULLIMAGE")
|
|
||||||
shift;
|
|
||||||
while [ "$1" != "" ]; do
|
|
||||||
if [ "$1" == "--force" ]; then
|
|
||||||
ARGS+=("--force")
|
|
||||||
fi
|
|
||||||
shift;
|
|
||||||
done
|
|
||||||
$CLIENT "${ARGS[@]}";
|
|
||||||
}
|
|
||||||
HELP() {
|
|
||||||
cat << EOF
|
|
||||||
Quick helper script for flashing a Proxmark3 device via USB
|
|
||||||
|
|
||||||
Description:
|
|
||||||
The correct port name will be automatically guessed and the stock bootloader and firmware image will be flashed.
|
|
||||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
|
||||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
|
||||||
To see a list of available ports, use --list.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
$SCRIPT [-n <N>]
|
|
||||||
$SCRIPT --list
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
elif [ "$SCRIPT" = "pm3-flash-fullimage" ]; then
|
|
||||||
FINDBTDONGLE=false
|
|
||||||
FINDBTRFCOMM=false
|
|
||||||
FINDBTDIRECT=false
|
|
||||||
CMD() {
|
|
||||||
ARGS=("--port" "$1" "--flash" "--image" "$FULLIMAGE")
|
|
||||||
shift;
|
|
||||||
while [ "$1" != "" ]; do
|
|
||||||
if [ "$1" == "--force" ]; then
|
|
||||||
ARGS+=("--force")
|
|
||||||
fi
|
|
||||||
shift;
|
|
||||||
done
|
|
||||||
$CLIENT "${ARGS[@]}";
|
|
||||||
}
|
|
||||||
HELP() {
|
|
||||||
cat << EOF
|
|
||||||
Quick helper script for flashing a Proxmark3 device via USB
|
|
||||||
|
|
||||||
Description:
|
|
||||||
The correct port name will be automatically guessed and the stock firmware image will be flashed.
|
|
||||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
|
||||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
|
||||||
To see a list of available ports, use --list.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
$SCRIPT [-n <N>]
|
|
||||||
$SCRIPT --list
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
elif [ "$SCRIPT" = "pm3-flash-bootrom" ]; then
|
|
||||||
FINDBTDONGLE=false
|
|
||||||
FINDBTRFCOMM=false
|
|
||||||
FINDBTDIRECT=false
|
|
||||||
CMD() {
|
|
||||||
ARGS=("--port" "$1" "--flash" "--unlock-bootloader" "--image" "$BOOTIMAGE")
|
|
||||||
shift;
|
|
||||||
while [ "$1" != "" ]; do
|
|
||||||
if [ "$1" == "--force" ]; then
|
|
||||||
ARGS+=("--force")
|
|
||||||
fi
|
|
||||||
shift;
|
|
||||||
done
|
|
||||||
$CLIENT "${ARGS[@]}";
|
|
||||||
}
|
|
||||||
HELP() {
|
|
||||||
cat << EOF
|
|
||||||
Quick helper script for flashing a Proxmark3 device via USB
|
|
||||||
|
|
||||||
Description:
|
|
||||||
The correct port name will be automatically guessed and the stock bootloader will be flashed.
|
|
||||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
|
||||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
|
||||||
To see a list of available ports, use --list.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
$SCRIPT [-n <N>]
|
|
||||||
$SCRIPT --list
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
else
|
|
||||||
echo >&2 "[!!] Script ran under unknown name, abort: $SCRIPT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# priority to the help options
|
|
||||||
for ARG; do
|
|
||||||
if [ "$ARG" == "-h" ] || [ "$ARG" == "--help" ]; then
|
|
||||||
HELP
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if [ "$ARG" == "-hh" ] || [ "$ARG" == "--helpclient" ]; then
|
|
||||||
CMD "-h"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# if offline, bypass the script and forward all other args
|
|
||||||
for ARG; do
|
|
||||||
shift
|
|
||||||
if [ "$ARG" == "-o" ] || [ "$ARG" == "--offline" ]; then
|
|
||||||
CMD "$@"
|
|
||||||
exit $?
|
|
||||||
fi
|
|
||||||
set -- "$@" "$ARG"
|
|
||||||
done
|
|
||||||
|
|
||||||
# if a port is already provided, let's just run the command as such
|
|
||||||
for ARG; do
|
|
||||||
shift
|
|
||||||
if [ "$ARG" == "-p" ]; then
|
|
||||||
CMD "$@"
|
|
||||||
exit $?
|
|
||||||
fi
|
|
||||||
set -- "$@" "$ARG"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$1" == "--list" ]; then
|
|
||||||
shift
|
|
||||||
if [ "$1" != "" ]; then
|
|
||||||
echo >&2 "[!!] Option --list must be used alone"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
SHOWLIST=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Number of the Proxmark3 we're interested in
|
|
||||||
N=1
|
|
||||||
if [ "$1" == "-n" ]; then
|
|
||||||
shift
|
|
||||||
if [ "$1" -ge 1 ] && [ "$1" -lt 10 ]; then
|
|
||||||
N=$1
|
|
||||||
shift
|
|
||||||
else
|
|
||||||
echo >&2 "[!!] Option -n requires a number between 1 and 9, got \"$1\""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
HOSTOS=$(uname | awk '{print toupper($0)}')
|
|
||||||
if [ "$HOSTOS" = "LINUX" ]; then
|
|
||||||
# Detect when running under WSL1 (but exclude WSL2)
|
|
||||||
if uname -a | grep -qi Microsoft && uname -a | grep -qvi WSL2; then
|
|
||||||
# First try finding it using the PATH environment variable
|
|
||||||
PSHEXE=$(command -v powershell.exe 2>/dev/null)
|
|
||||||
|
|
||||||
# If it fails (such as if WSLENV is not set), try using the default installation path
|
|
||||||
if [ -z "$PSHEXE" ]; then
|
|
||||||
PSHEXE=/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Finally test if PowerShell is working
|
|
||||||
if ! "$PSHEXE" exit >/dev/null 2>&1; then
|
|
||||||
echo >&2 "[!!] Cannot run powershell.exe, are you sure your WSL is authorized to run Windows processes? (cf WSL interop flag)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
GETPM3LIST=get_pm3_list_WSL
|
|
||||||
else
|
|
||||||
GETPM3LIST=get_pm3_list_Linux
|
|
||||||
fi
|
|
||||||
elif [ "$HOSTOS" = "DARWIN" ]; then
|
|
||||||
GETPM3LIST=get_pm3_list_macOS
|
|
||||||
elif [[ "$HOSTOS" =~ MINGW(32|64)_NT* ]]; then
|
|
||||||
GETPM3LIST=get_pm3_list_Windows
|
|
||||||
else
|
|
||||||
echo >&2 "[!!] Host OS not recognized, abort: $HOSTOS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $SHOWLIST; then
|
|
||||||
# Probe for up to 9 devs
|
|
||||||
$GETPM3LIST 9
|
|
||||||
if [ ${#PM3LIST} -lt 1 ]; then
|
|
||||||
echo >&2 "[!!] No port found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
n=1
|
|
||||||
for DEV in "${PM3LIST[@]}"
|
|
||||||
do
|
|
||||||
echo "$n: $DEV"
|
|
||||||
n=$((n+1))
|
|
||||||
done
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Wait till we get at least N Proxmark3 devices
|
|
||||||
$GETPM3LIST "$N"
|
|
||||||
if [ ${#PM3LIST} -lt "$N" ]; then
|
|
||||||
echo >&2 "[=] Waiting for Proxmark3 to appear..."
|
|
||||||
fi
|
|
||||||
while true; do
|
|
||||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep .1
|
|
||||||
$GETPM3LIST "$N"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#PM3LIST} -lt "$N" ]; then
|
|
||||||
HELP() {
|
|
||||||
cat << EOF
|
|
||||||
[!!] No port found, abort
|
|
||||||
|
|
||||||
[?] Hint: try '$SCRIPT --list' to see list of available ports, and use the -n command like below
|
|
||||||
[?] $SCRIPT [-n <N>]
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
HELP
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
CMD "${PM3LIST[$((N-1))]}" "$@"
|
|
||||||
exit $?
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
PM3PATH=$(dirname "$0")
|
|
||||||
. "$PM3PATH/pm3"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
PM3PATH=$(dirname "$0")
|
|
||||||
. "$PM3PATH/pm3"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
PM3PATH=$(dirname "$0")
|
|
||||||
. "$PM3PATH/pm3"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
PM3PATH=$(dirname "$0")
|
|
||||||
. "$PM3PATH/pm3"
|
|
||||||
Binary file not shown.
4
rust-toolchain.toml
Normal file
4
rust-toolchain.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
|
components = ["rust-src"]
|
||||||
|
targets = ["riscv32imac-unknown-none-elf"]
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Failure state for fwa.service
|
|
||||||
Requires=local-fs.target
|
|
||||||
After=local-fs.target
|
|
||||||
StartLimitIntervalSec=500
|
|
||||||
StartLimitBurst=5
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/fwa --error
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
WorkingDirectory=/var/lib/fwa
|
|
||||||
EnvironmentFile=/etc/fwa.env
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
PM3_BIN=/usr/share/pm3/pm3
|
|
||||||
LOG_LEVEL=warn
|
|
||||||
HOTSPOT_IDS=578B5DF2;c1532b57
|
|
||||||
HOTSPOT_SSID=fwa
|
|
||||||
HOTSPOT_PW=a9LG2kUVrsRRVUo1
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Feuerwehr Anwesenheit Service
|
|
||||||
Requires=local-fs.target
|
|
||||||
After=local-fs.target
|
|
||||||
StartLimitIntervalSec=500
|
|
||||||
StartLimitBurst=5
|
|
||||||
OnFailure= fwa-fail.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/fwa
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
WorkingDirectory=/var/lib/fwa
|
|
||||||
EnvironmentFile=/etc/fwa.env
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
3
src/drivers.rs
Normal file
3
src/drivers.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod nfc_reader;
|
||||||
|
pub mod rtc;
|
||||||
|
pub mod buzzer;
|
||||||
0
src/drivers/buzzer.rs
Normal file
0
src/drivers/buzzer.rs
Normal file
9
src/drivers/fram.rs
Normal file
9
src/drivers/fram.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const DEVICE_TYPE_CODE: u8 = 0b10100000;
|
||||||
|
|
||||||
|
const DEVICE_ADDRESS_CODE: u8 = 0b000000; // 3 bits for device address | default A0 = 0 A1 = 0 A2 = 0
|
||||||
|
|
||||||
|
const WRITE_CODE: u8 = 0b00000000; // 0 for write
|
||||||
|
const READ_CODE: u8 = 0b00000001; // 1 for read
|
||||||
|
|
||||||
|
const DEVICE_ADDRESS_WRITE: u8 = DEVICE_TYPE_CODE | DEVICE_ADDRESS_CODE | WRITE_CODE; // I2C address write for FRAM
|
||||||
|
const DEVICE_ADDRESS_READ: u8 = DEVICE_TYPE_CODE | DEVICE_ADDRESS_CODE | READ_CODE; // I2C address read for FRAM
|
||||||
28
src/drivers/nfc_reader.rs
Normal file
28
src/drivers/nfc_reader.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_hal::{Async, uart::Uart};
|
||||||
|
use log::{debug, info};
|
||||||
|
|
||||||
|
use crate::TallyPublisher;
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: TallyPublisher) {
|
||||||
|
let mut uart_buffer = [0u8; 64];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
debug!("Looking for NFC...");
|
||||||
|
match uart_device.read_async(&mut uart_buffer).await {
|
||||||
|
Ok(n) => {
|
||||||
|
let mut hex_str = heapless::String::<64>::new();
|
||||||
|
for byte in &uart_buffer[..n] {
|
||||||
|
core::fmt::Write::write_fmt(&mut hex_str, format_args!("{:02X} ", byte)).ok();
|
||||||
|
}
|
||||||
|
info!("Read {n} bytes from UART: {hex_str}");
|
||||||
|
chan.publish([1, 0, 2, 5, 0, 8, 12, 15]).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error reading from UART: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer::after(Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/drivers/rtc.rs
Normal file
96
src/drivers/rtc.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use ds3231::{
|
||||||
|
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
|
||||||
|
TimeRepresentation,
|
||||||
|
};
|
||||||
|
use esp_hal::{
|
||||||
|
Async,
|
||||||
|
i2c::{self, master::I2c},
|
||||||
|
};
|
||||||
|
use log::{debug, error, info};
|
||||||
|
|
||||||
|
use crate::{FEEDBACK_STATE, drivers, feedback};
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
|
||||||
|
|
||||||
|
const RTC_ADDRESS: u8 = 0x68;
|
||||||
|
|
||||||
|
pub struct RTCClock {
|
||||||
|
dev: DS3231<I2c<'static, Async>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RTCClock {
|
||||||
|
pub async fn new(i2c: i2c::master::I2c<'static, Async>) -> Self {
|
||||||
|
debug!("configuring rtc...");
|
||||||
|
let rtc = drivers::rtc::rtc_config(i2c).await;
|
||||||
|
debug!("rtc up");
|
||||||
|
|
||||||
|
RTCClock { dev: rtc }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_time(&mut self) -> u64 {
|
||||||
|
match self.dev.datetime().await {
|
||||||
|
Ok(datetime) => {
|
||||||
|
let utc_time = datetime.and_utc().timestamp() as u64;
|
||||||
|
utc_time
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
|
||||||
|
error!("Failed to read RTC datetime: {:?}", e);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>> {
|
||||||
|
let mut rtc: DS3231<I2c<'static, Async>> = DS3231::new(i2c, RTC_ADDRESS);
|
||||||
|
let naive_dt = Utc
|
||||||
|
.timestamp_opt(BUILD_UNIX_TIME as i64, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap()
|
||||||
|
.naive_utc();
|
||||||
|
|
||||||
|
let rtc_config = Config {
|
||||||
|
time_representation: TimeRepresentation::TwentyFourHour,
|
||||||
|
square_wave_frequency: SquareWaveFrequency::Hz1,
|
||||||
|
interrupt_control: InterruptControl::Interrupt, // Enable interrupt mode
|
||||||
|
battery_backed_square_wave: false,
|
||||||
|
oscillator_enable: Oscillator::Disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
match rtc.configure(&rtc_config).await {
|
||||||
|
Ok(_) => info!("DS3231 configured successfully"),
|
||||||
|
Err(e) => {
|
||||||
|
info!("Failed to configure DS3231: {:?}", e);
|
||||||
|
panic!("DS3231 configuration failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc.set_datetime(&naive_dt).await.unwrap_or_else(|e| {
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
|
||||||
|
error!("Failed to set RTC datetime: {:?}", e);
|
||||||
|
});
|
||||||
|
info!("RTC datetime set to: {}", naive_dt);
|
||||||
|
|
||||||
|
match rtc.status().await {
|
||||||
|
Ok(mut status) => {
|
||||||
|
status.set_alarm1_flag(false);
|
||||||
|
status.set_alarm2_flag(false);
|
||||||
|
match rtc.set_status(status).await {
|
||||||
|
Ok(_) => info!("Alarm flags cleared"),
|
||||||
|
Err(e) => info!("Failed to clear alarm flags: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => info!("Failed to read status: {:?}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_rtc_time<'a>(
|
||||||
|
rtc: &'a mut DS3231<I2c<'static, Async>>,
|
||||||
|
) -> Result<u64, DS3231Error<esp_hal::i2c::master::Error>> {
|
||||||
|
let timestamp_result = rtc.datetime().await?;
|
||||||
|
Ok(timestamp_result.and_utc().timestamp() as u64)
|
||||||
|
}
|
||||||
348
src/feedback.rs
348
src/feedback.rs
@@ -1,181 +1,187 @@
|
|||||||
use anyhow::Result;
|
use embassy_time::{Delay, Duration, Timer};
|
||||||
use log::error;
|
use esp_hal::{delay, gpio::Output, peripherals, rmt::ConstChannelAccess};
|
||||||
use rgb::RGB8;
|
use esp_hal_smartled::SmartLedsAdapterAsync;
|
||||||
use smart_leds::colors::{GREEN, RED};
|
use log::{debug, error, info};
|
||||||
use std::time::Duration;
|
use init::hardware;
|
||||||
use tokio::{join, time::sleep};
|
use smart_leds::colors::{BLACK, GREEN, RED, YELLOW};
|
||||||
|
use smart_leds::{brightness, colors::BLUE};
|
||||||
|
use smart_leds::SmartLedsWriteAsync;
|
||||||
|
|
||||||
use crate::hardware::{Buzzer, StatusLed};
|
use crate::{FEEDBACK_STATE, init};
|
||||||
|
|
||||||
#[cfg(not(feature = "mock_pi"))]
|
#[derive(Copy, Clone, Debug)]
|
||||||
use crate::{hardware::GPIOBuzzer, hardware::SpiLed};
|
pub enum FeedbackState {
|
||||||
|
Ack,
|
||||||
#[cfg(feature = "mock_pi")]
|
Nak,
|
||||||
use crate::hardware::{MockBuzzer, MockLed};
|
Error,
|
||||||
|
Startup,
|
||||||
const LED_BLINK_DURATION: Duration = Duration::from_secs(1);
|
WIFI,
|
||||||
|
Idle,
|
||||||
pub enum DeviceStatus {
|
|
||||||
NotReady,
|
|
||||||
Ready,
|
|
||||||
HotspotEnabled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceStatus {
|
const LED_LEVEL: u8 = 255;
|
||||||
pub fn color(&self) -> RGB8 {
|
|
||||||
match self {
|
|
||||||
Self::NotReady => RGB8::new(0, 0, 0),
|
|
||||||
Self::Ready => RGB8::new(0, 50, 0),
|
|
||||||
Self::HotspotEnabled => RGB8::new(0, 0, 50),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct Feedback<B: Buzzer, L: StatusLed> {
|
|
||||||
device_status: DeviceStatus,
|
|
||||||
buzzer: B,
|
|
||||||
led: L,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: Buzzer, L: StatusLed> Feedback<B, L> {
|
#[embassy_executor::task]
|
||||||
pub async fn success(&mut self) {
|
pub async fn feedback_task(mut led: SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, { init::hardware::LED_BUFFER_SIZE }>, buzzer: peripherals::GPIO21<'static>) {
|
||||||
let buzzer_handle = Self::beep_ack(&mut self.buzzer);
|
debug!("Starting feedback task");
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, LED_BLINK_DURATION);
|
let mut buzzer = init::hardware::setup_buzzer(buzzer);
|
||||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
loop {
|
||||||
|
let feedback_state = FEEDBACK_STATE.wait().await;
|
||||||
buzzer_result.unwrap_or_else(|err| {
|
match feedback_state {
|
||||||
error!("Failed to buzz: {err}");
|
FeedbackState::Ack => {
|
||||||
});
|
led.write(brightness([GREEN; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
||||||
|
buzzer.set_high();
|
||||||
let _ = self.led_to_status();
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
}
|
buzzer.set_low();
|
||||||
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
pub async fn failure(&mut self) {
|
}
|
||||||
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
|
FeedbackState::Nak => {
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
|
led.write(brightness([YELLOW; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
||||||
|
buzzer.set_high();
|
||||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
|
buzzer.set_low();
|
||||||
buzzer_result.unwrap_or_else(|err| {
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
error!("Failed to buzz: {err}");
|
buzzer.set_high();
|
||||||
});
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
|
buzzer.set_low();
|
||||||
let _ = self.led_to_status();
|
led.write(brightness([BLACK; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
||||||
}
|
}
|
||||||
|
FeedbackState::Error => {
|
||||||
pub async fn activate_error_state(&mut self) -> Result<()> {
|
led.write(brightness([RED; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
||||||
self.led.turn_on(RED)?;
|
buzzer.set_high();
|
||||||
Self::beep_nak(&mut self.buzzer).await?;
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
Ok(())
|
buzzer.set_low();
|
||||||
}
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
buzzer.set_high();
|
||||||
pub async fn startup(&mut self){
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
self.device_status = DeviceStatus::Ready;
|
buzzer.set_low();
|
||||||
|
}
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, Duration::from_secs(1));
|
FeedbackState::Startup => {
|
||||||
let buzzer_handle = Self::beep_startup(&mut self.buzzer);
|
led.write(brightness([GREEN; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
||||||
|
buzzer.set_high();
|
||||||
let (buzzer_result, led_result) = join!(buzzer_handle, led_handle);
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
|
buzzer.set_low();
|
||||||
buzzer_result.unwrap_or_else(|err| {
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
error!("Failed to buzz: {err}");
|
buzzer.set_high();
|
||||||
});
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
|
buzzer.set_low();
|
||||||
led_result.unwrap_or_else(|err| {
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
error!("Failed to blink led: {err}");
|
buzzer.set_high();
|
||||||
});
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
|
buzzer.set_low();
|
||||||
let _ = self.led_to_status();
|
led.write(brightness([BLACK; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
||||||
}
|
}
|
||||||
|
FeedbackState::WIFI => {
|
||||||
pub fn set_device_status(&mut self, status: DeviceStatus){
|
led.write(brightness([BLUE; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
||||||
self.device_status = status;
|
}
|
||||||
let _ = self.led_to_status();
|
FeedbackState::Idle => {
|
||||||
}
|
// Do nothing
|
||||||
|
}
|
||||||
fn led_to_status(&mut self) -> Result<()> {
|
};
|
||||||
self.led.turn_on(self.device_status.color())
|
debug!("Feedback state: {:?}", feedback_state);
|
||||||
}
|
|
||||||
|
|
||||||
async fn flash_led_for_duration(led: &mut L, color: RGB8, duration: Duration) -> Result<()> {
|
|
||||||
led.turn_on(color)?;
|
|
||||||
|
|
||||||
sleep(duration).await;
|
|
||||||
|
|
||||||
led.turn_off()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn beep_ack(buzzer: &mut B) -> Result<()> {
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(1200.0, Duration::from_millis(100))
|
|
||||||
.await?;
|
|
||||||
sleep(Duration::from_millis(10)).await;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(2000.0, Duration::from_millis(50))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn beep_nak(buzzer: &mut B) -> Result<()> {
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(600.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(600.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn beep_startup(buzzer: &mut B) -> Result<()> {
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(523.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(659.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(784.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(1046.0, Duration::from_millis(200))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(784.0, Duration::from_millis(100))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(880.0, Duration::from_millis(200))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "mock_pi")]
|
|
||||||
pub type FeedbackImpl = Feedback<MockBuzzer, MockLed>;
|
|
||||||
#[cfg(not(feature = "mock_pi"))]
|
|
||||||
pub type FeedbackImpl = Feedback<GPIOBuzzer, SpiLed>;
|
|
||||||
|
|
||||||
impl FeedbackImpl {
|
// async fn beep_ack() {
|
||||||
pub fn new() -> Result<Self> {
|
// buzzer.set_high();
|
||||||
#[cfg(feature = "mock_pi")]
|
// buzzer.set_low();
|
||||||
{
|
// //Timer::after(Duration::from_millis(100)).await;
|
||||||
Ok(Feedback {
|
// }
|
||||||
device_status: DeviceStatus::NotReady,
|
|
||||||
buzzer: MockBuzzer {},
|
/* pub async fn failure(&mut self) {
|
||||||
led: MockLed {},
|
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
|
||||||
})
|
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
|
||||||
}
|
|
||||||
#[cfg(not(feature = "mock_pi"))]
|
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
||||||
{
|
|
||||||
Ok(Feedback {
|
buzzer_result.unwrap_or_else(|err| { error!("Failed to buzz: {err}");
|
||||||
device_status: DeviceStatus::NotReady,
|
});
|
||||||
buzzer: GPIOBuzzer::new_default()?,
|
|
||||||
led: SpiLed::new()?,
|
let _ = self.led_to_status();
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn activate_error_state(&mut self) -> Result<()> {
|
||||||
|
self.led.turn_on(RED)?;
|
||||||
|
Self::beep_nak(&mut self.buzzer).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn startup(&mut self){
|
||||||
|
self.device_status = DeviceStatus::Ready;
|
||||||
|
|
||||||
|
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, Duration::from_secs(1));
|
||||||
|
let buzzer_handle = Self::beep_startup(&mut self.buzzer);
|
||||||
|
|
||||||
|
let (buzzer_result, led_result) = join!(buzzer_handle, led_handle);
|
||||||
|
|
||||||
|
buzzer_result.unwrap_or_else(|err| {
|
||||||
|
error!("Failed to buzz: {err}");
|
||||||
|
});
|
||||||
|
|
||||||
|
led_result.unwrap_or_else(|err| {
|
||||||
|
error!("Failed to blink led: {err}");
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = self.led_to_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn flash_led_for_duration(led: &mut L, color: RGB8, duration: Duration) -> Result<()> {
|
||||||
|
led.turn_on(color)?;
|
||||||
|
|
||||||
|
sleep(duration).await;
|
||||||
|
|
||||||
|
led.turn_off()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn beep_ack(buzzer: &mut B) -> Result<()> {
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(1200.0, Duration::from_millis(100))
|
||||||
|
.await?;
|
||||||
|
sleep(Duration::from_millis(10)).await;
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(2000.0, Duration::from_millis(50))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn beep_nak(buzzer: &mut B) -> Result<()> {
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(600.0, Duration::from_millis(150))
|
||||||
|
.await?;
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(600.0, Duration::from_millis(150))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn beep_startup(buzzer: &mut B) -> Result<()> {
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(523.0, Duration::from_millis(150))
|
||||||
|
.await?;
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(659.0, Duration::from_millis(150))
|
||||||
|
.await?;
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(784.0, Duration::from_millis(150))
|
||||||
|
.await?;
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(1046.0, Duration::from_millis(200))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(784.0, Duration::from_millis(100))
|
||||||
|
.await?;
|
||||||
|
buzzer
|
||||||
|
.modulated_tone(880.0, Duration::from_millis(200))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use rppal::pwm::{Channel, Polarity, Pwm};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
use crate::hardware::Buzzer;
|
|
||||||
|
|
||||||
const DEFAULT_PWM_CHANNEL_BUZZER: Channel = Channel::Pwm0; //PWM0 = GPIO18/Physical pin 12
|
|
||||||
|
|
||||||
pub struct GPIOBuzzer {
|
|
||||||
pwm: Pwm,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GPIOBuzzer {
|
|
||||||
pub fn new_from_channel(channel: Channel) -> Result<Self, rppal::pwm::Error> {
|
|
||||||
// Enable with dummy values; we'll set frequency/duty in the tone method
|
|
||||||
let duty_cycle: f64 = 0.5;
|
|
||||||
let pwm = Pwm::with_frequency(channel, 1000.0, duty_cycle, Polarity::Normal, true)?;
|
|
||||||
pwm.disable()?;
|
|
||||||
|
|
||||||
Ok(GPIOBuzzer { pwm })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_default() -> Result<Self, rppal::pwm::Error> {
|
|
||||||
Self::new_from_channel(DEFAULT_PWM_CHANNEL_BUZZER)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Buzzer for GPIOBuzzer {
|
|
||||||
async fn modulated_tone(&mut self, frequency_hz: f64, duration: Duration) -> Result<()> {
|
|
||||||
self.pwm.set_frequency(frequency_hz, 0.5)?; // 50% duty cycle (square wave)
|
|
||||||
self.pwm.enable()?;
|
|
||||||
sleep(duration).await;
|
|
||||||
self.pwm.disable()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
use anyhow::{Result, anyhow};
|
|
||||||
use log::{trace, warn};
|
|
||||||
use std::env;
|
|
||||||
use tokio::process::Command;
|
|
||||||
|
|
||||||
use crate::hardware::Hotspot;
|
|
||||||
|
|
||||||
const SSID: &str = "fwa";
|
|
||||||
const CON_NAME: &str = "fwa-hotspot";
|
|
||||||
const PASSWORD: &str = "a9LG2kUVrsRRVUo1";
|
|
||||||
const IPV4_ADDRES: &str = "192.168.4.1/24";
|
|
||||||
|
|
||||||
/// NetworkManager Hotspot
|
|
||||||
pub struct NMHotspot {
|
|
||||||
ssid: String,
|
|
||||||
con_name: String,
|
|
||||||
password: String,
|
|
||||||
ipv4: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NMHotspot {
|
|
||||||
pub fn new_from_env() -> Result<Self> {
|
|
||||||
let ssid = env::var("HOTSPOT_SSID").unwrap_or(SSID.to_owned());
|
|
||||||
let password = env::var("HOTSPOT_PW").unwrap_or_else(|_| {
|
|
||||||
warn!("HOTSPOT_PW not set. Using default password");
|
|
||||||
PASSWORD.to_owned()
|
|
||||||
});
|
|
||||||
|
|
||||||
if password.len() < 8 {
|
|
||||||
return Err(anyhow!("Hotspot password to short"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(NMHotspot {
|
|
||||||
ssid,
|
|
||||||
con_name: CON_NAME.to_owned(),
|
|
||||||
password,
|
|
||||||
ipv4: IPV4_ADDRES.to_owned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_hotspot(&self) -> Result<()> {
|
|
||||||
let cmd = Command::new("nmcli")
|
|
||||||
.args(["device", "wifi", "hotspot"])
|
|
||||||
.arg("con-name")
|
|
||||||
.arg(&self.con_name)
|
|
||||||
.arg("ssid")
|
|
||||||
.arg(&self.ssid)
|
|
||||||
.arg("password")
|
|
||||||
.arg(&self.password)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
|
||||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
|
||||||
|
|
||||||
if !cmd.status.success() {
|
|
||||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let cmd = Command::new("nmcli")
|
|
||||||
.arg("connection")
|
|
||||||
.arg("modify")
|
|
||||||
.arg(&self.con_name)
|
|
||||||
.arg("ipv4.method")
|
|
||||||
.arg("shared")
|
|
||||||
.arg("ipv4.addresses")
|
|
||||||
.arg(&self.ipv4)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !cmd.status.success() {
|
|
||||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the connection already exists
|
|
||||||
async fn exists(&self) -> Result<bool> {
|
|
||||||
let cmd = Command::new("nmcli")
|
|
||||||
.args(["connection", "show"])
|
|
||||||
.arg(&self.con_name)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
|
||||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
|
||||||
|
|
||||||
Ok(cmd.status.success())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hotspot for NMHotspot {
|
|
||||||
async fn enable_hotspot(&self) -> Result<()> {
|
|
||||||
if !self.exists().await? {
|
|
||||||
self.create_hotspot().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cmd = Command::new("nmcli")
|
|
||||||
.args(["connection", "up"])
|
|
||||||
.arg(&self.con_name)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
|
||||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
|
||||||
|
|
||||||
if !cmd.status.success() {
|
|
||||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn disable_hotspot(&self) -> Result<()> {
|
|
||||||
let cmd = Command::new("nmcli")
|
|
||||||
.args(["connection", "down"])
|
|
||||||
.arg(&self.con_name)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
|
||||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
|
||||||
|
|
||||||
if !cmd.status.success() {
|
|
||||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
mod gpio_buzzer;
|
|
||||||
mod hotspot;
|
|
||||||
mod mock;
|
|
||||||
mod spi_led;
|
|
||||||
|
|
||||||
pub use gpio_buzzer::GPIOBuzzer;
|
|
||||||
pub use mock::{MockBuzzer, MockHotspot, MockLed};
|
|
||||||
pub use spi_led::SpiLed;
|
|
||||||
|
|
||||||
pub trait StatusLed {
|
|
||||||
fn turn_off(&mut self) -> Result<()>;
|
|
||||||
|
|
||||||
fn turn_on(&mut self, color: rgb::RGB8) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Buzzer {
|
|
||||||
fn modulated_tone(
|
|
||||||
&mut self,
|
|
||||||
frequency_hz: f64,
|
|
||||||
duration: Duration,
|
|
||||||
) -> impl Future<Output = Result<()>> + std::marker::Send;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Hotspot {
|
|
||||||
fn enable_hotspot(&self) -> impl std::future::Future<Output = Result<()>> + std::marker::Send;
|
|
||||||
|
|
||||||
fn disable_hotspot(&self) -> impl std::future::Future<Output = Result<()>> + std::marker::Send;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a struct to manage the hotspot
|
|
||||||
/// Respects the `mock_pi` flag.
|
|
||||||
pub fn create_hotspot() -> Result<impl Hotspot> {
|
|
||||||
#[cfg(feature = "mock_pi")]
|
|
||||||
{
|
|
||||||
Ok(mock::MockHotspot {})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "mock_pi"))]
|
|
||||||
{
|
|
||||||
hotspot::NMHotspot::new_from_env()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use rppal::spi::{Bus, Mode, SlaveSelect, Spi};
|
|
||||||
use smart_leds::SmartLedsWrite;
|
|
||||||
use ws2812_spi::Ws2812;
|
|
||||||
|
|
||||||
use crate::hardware::StatusLed;
|
|
||||||
|
|
||||||
const SPI_CLOCK_SPEED: u32 = 3_800_000;
|
|
||||||
|
|
||||||
pub struct SpiLed {
|
|
||||||
controller: Ws2812<Spi>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpiLed {
|
|
||||||
pub fn new() -> Result<Self, rppal::spi::Error> {
|
|
||||||
let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, SPI_CLOCK_SPEED, Mode::Mode0)?;
|
|
||||||
let controller = Ws2812::new(spi);
|
|
||||||
Ok(SpiLed { controller })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatusLed for SpiLed {
|
|
||||||
fn turn_off(&mut self) -> Result<()> {
|
|
||||||
self.controller
|
|
||||||
.write(vec![rgb::RGB8::new(0, 0, 0)].into_iter())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn turn_on(&mut self, color: rgb::RGB8) -> Result<()> {
|
|
||||||
self.controller.write(vec![color].into_iter())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
src/init.rs
Normal file
3
src/init.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod hardware;
|
||||||
|
pub mod network;
|
||||||
|
pub mod wifi;
|
||||||
215
src/init/hardware.rs
Normal file
215
src/init/hardware.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_net::Stack;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use embedded_sdmmc_dev::SdCard;
|
||||||
|
use esp_hal::i2c::master::Config;
|
||||||
|
use esp_hal::peripherals::{
|
||||||
|
self, GPIO0, GPIO1, GPIO2, GPIO10, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, GPIO22,
|
||||||
|
GPIO23, I2C0, RMT, SPI2, UART1,
|
||||||
|
};
|
||||||
|
use esp_hal::rmt::{ConstChannelAccess, Rmt, Tx};
|
||||||
|
use esp_hal::spi::{
|
||||||
|
Mode,
|
||||||
|
master::{Config as Spi_config, Spi},
|
||||||
|
};
|
||||||
|
|
||||||
|
use esp_hal::time::Rate;
|
||||||
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
|
use esp_hal::{
|
||||||
|
Async,
|
||||||
|
clock::CpuClock,
|
||||||
|
gpio::{Output, OutputConfig},
|
||||||
|
i2c::master::I2c,
|
||||||
|
timer::systimer::SystemTimer,
|
||||||
|
uart::Uart,
|
||||||
|
};
|
||||||
|
|
||||||
|
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
||||||
|
|
||||||
|
use smart_leds::colors::{BLUE, GREEN, RED};
|
||||||
|
use smart_leds::{
|
||||||
|
RGB8, SmartLedsWriteAsync, brightness, gamma,
|
||||||
|
hsv::{Hsv, hsv2rgb},
|
||||||
|
};
|
||||||
|
|
||||||
|
use esp_println::logger::init_logger;
|
||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
|
use crate::init::network;
|
||||||
|
use crate::init::wifi;
|
||||||
|
|
||||||
|
/*************************************************
|
||||||
|
* GPIO Pinout Xiao Esp32c6
|
||||||
|
*
|
||||||
|
* D0 -> GPIO0 -> SD DECT
|
||||||
|
* D1 -> GPIO1 -> Level Shifter A0 -> LED
|
||||||
|
* D2 -> GPIO2 -> SPI/CS
|
||||||
|
* D3 -> GPIO21 -> Buzzer
|
||||||
|
* D4 -> GPIO22 -> I2C/SDA
|
||||||
|
* D5 -> GPIO23 -> I2C/SCL
|
||||||
|
* D6 -> GPIO16 -> UART/TX
|
||||||
|
* D7 -> GPIO17 -> UART/RX -> Level Shifter A1 -> NFC Reader
|
||||||
|
* D8 -> GPIO19 -> SPI/SCLK
|
||||||
|
* D9 -> GPIO20 -> SPI/MISO
|
||||||
|
* D10 -> GPIO10 -> SPI/MOSI
|
||||||
|
*
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
pub const NUM_LEDS: usize = 66;
|
||||||
|
pub const LED_BUFFER_SIZE: usize = NUM_LEDS * 25;
|
||||||
|
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {
|
||||||
|
error!("PANIC: {info}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
|
|
||||||
|
pub async fn hardware_init(
|
||||||
|
spawner: &mut Spawner,
|
||||||
|
) -> (
|
||||||
|
Uart<'static, Async>,
|
||||||
|
Stack<'static>,
|
||||||
|
I2c<'static, Async>,
|
||||||
|
SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE>,
|
||||||
|
GPIO21<'static>,
|
||||||
|
) {
|
||||||
|
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
||||||
|
let peripherals = esp_hal::init(config);
|
||||||
|
|
||||||
|
esp_alloc::heap_allocator!(size: 72 * 1024);
|
||||||
|
|
||||||
|
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
|
||||||
|
esp_hal_embassy::init(timer0.alarm0);
|
||||||
|
|
||||||
|
init_logger(log::LevelFilter::Debug);
|
||||||
|
|
||||||
|
let timer1 = TimerGroup::new(peripherals.TIMG0);
|
||||||
|
let mut rng = esp_hal::rng::Rng::new(peripherals.RNG);
|
||||||
|
let network_seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
||||||
|
|
||||||
|
wifi::set_antenna_mode(peripherals.GPIO3, peripherals.GPIO14).await;
|
||||||
|
let interfaces = wifi::setup_wifi(timer1.timer0, rng, peripherals.WIFI, spawner);
|
||||||
|
let stack = network::setup_network(network_seed, interfaces.ap, spawner);
|
||||||
|
|
||||||
|
Timer::after(Duration::from_millis(1)).await;
|
||||||
|
init_lvl_shifter(peripherals.GPIO0);
|
||||||
|
|
||||||
|
let uart_device = setup_uart(peripherals.UART1, peripherals.GPIO16, peripherals.GPIO17);
|
||||||
|
|
||||||
|
let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23);
|
||||||
|
|
||||||
|
let spi_device = setup_spi(
|
||||||
|
peripherals.SPI2,
|
||||||
|
peripherals.GPIO19,
|
||||||
|
peripherals.GPIO20,
|
||||||
|
peripherals.GPIO18,
|
||||||
|
peripherals.GPIO2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let sd_card = setup_sdcard(spi_device);
|
||||||
|
|
||||||
|
let buzzer_gpio = peripherals.GPIO21;
|
||||||
|
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
|
||||||
|
|
||||||
|
debug!("hardware init done");
|
||||||
|
|
||||||
|
(uart_device, stack, i2c_device, led, buzzer_gpio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the level shifter for the NFC reader and LED (output-enable (OE) input is low, all outputs are placed in the high-impedance (Hi-Z) state)
|
||||||
|
fn init_lvl_shifter(oe_pin: GPIO0<'static>) {
|
||||||
|
let mut oe_lvl_shifter =
|
||||||
|
Output::new(oe_pin, esp_hal::gpio::Level::Low, OutputConfig::default().with_drive_mode(esp_hal::gpio::DriveMode::PushPull).with_drive_strength(esp_hal::gpio::DriveStrength::_10mA));
|
||||||
|
oe_lvl_shifter.set_high();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_uart(
|
||||||
|
uart1: UART1<'static>,
|
||||||
|
uart_tx: GPIO16<'static>,
|
||||||
|
uart_rx: GPIO17<'static>,
|
||||||
|
) -> Uart<'static, Async> {
|
||||||
|
let uard_device = Uart::new(uart1, esp_hal::uart::Config::default().with_baudrate(9600));
|
||||||
|
|
||||||
|
match uard_device {
|
||||||
|
Ok(block) => block.with_rx(uart_rx).with_tx(uart_tx).into_async(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize UART: {e}");
|
||||||
|
panic!(); //TODO panic!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_i2c(
|
||||||
|
i2c0: I2C0<'static>,
|
||||||
|
sda: GPIO22<'static>,
|
||||||
|
scl: GPIO23<'static>,
|
||||||
|
) -> I2c<'static, Async> {
|
||||||
|
debug!("init I2C");
|
||||||
|
let config = Config::default().with_frequency(Rate::from_khz(400));
|
||||||
|
let i2c = match I2c::new(i2c0, config) {
|
||||||
|
Ok(i2c) => i2c.with_sda(sda).with_scl(scl).into_async(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize I2C: {:?}", e);
|
||||||
|
panic!(); //TODO panic!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
i2c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_spi(
|
||||||
|
spi2: SPI2<'static>,
|
||||||
|
sck: GPIO19<'static>,
|
||||||
|
miso: GPIO20<'static>,
|
||||||
|
mosi: GPIO18<'static>,
|
||||||
|
cs: GPIO2<'static>,
|
||||||
|
) -> Spi<'static, Async> {
|
||||||
|
let spi = match Spi::new(spi2, Spi_config::default()) {
|
||||||
|
Ok(spi) => spi
|
||||||
|
.with_sck(sck)
|
||||||
|
.with_miso(miso)
|
||||||
|
.with_mosi(mosi)
|
||||||
|
.with_cs(cs)
|
||||||
|
.into_async(),
|
||||||
|
Err(e) => panic!("Failed to initialize SPI: {:?}", e),
|
||||||
|
};
|
||||||
|
spi
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_sdcard(spi_device: Spi<'static, Async>) {
|
||||||
|
//let sdcard = SdCard::new(spi_device as embedded_hal::spi::SpiDevice(), delayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_buzzer(buzzer_gpio: GPIO21<'static>) -> Output<'static> {
|
||||||
|
let config = esp_hal::gpio::OutputConfig::default()
|
||||||
|
.with_drive_strength(esp_hal::gpio::DriveStrength::_40mA);
|
||||||
|
let buzzer = Output::new(buzzer_gpio, esp_hal::gpio::Level::Low, config);
|
||||||
|
|
||||||
|
buzzer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_led(
|
||||||
|
rmt: RMT<'static>,
|
||||||
|
led_gpio: GPIO1<'static>,
|
||||||
|
) -> SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE> {
|
||||||
|
debug!("setup led");
|
||||||
|
let rmt: Rmt<'_, esp_hal::Async> = {
|
||||||
|
let frequency: Rate = Rate::from_mhz(80);
|
||||||
|
Rmt::new(rmt, frequency)
|
||||||
|
}
|
||||||
|
.expect("Failed to initialize RMT")
|
||||||
|
.into_async();
|
||||||
|
|
||||||
|
let rmt_channel = rmt.channel0;
|
||||||
|
let rmt_buffer = [0_u32; buffer_size_async(NUM_LEDS)];
|
||||||
|
|
||||||
|
let led: SmartLedsAdapterAsync<_, LED_BUFFER_SIZE> =
|
||||||
|
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
|
||||||
|
|
||||||
|
led
|
||||||
|
}
|
||||||
72
src/init/network.rs
Normal file
72
src/init/network.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use core::{net::Ipv4Addr, str::FromStr};
|
||||||
|
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_wifi::wifi::WifiDevice;
|
||||||
|
use static_cell::make_static;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: &mut Spawner) -> Stack<'a> {
|
||||||
|
let gw_ip_addr_str = "192.168.2.1";
|
||||||
|
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
|
||||||
|
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
||||||
|
address: Ipv4Cidr::new(gw_ip_addr, 24),
|
||||||
|
gateway: Some(gw_ip_addr),
|
||||||
|
dns_servers: Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let (stack, runner) =
|
||||||
|
embassy_net::new(wifi, config, make_static!(StackResources::<3>::new()), seed);
|
||||||
|
|
||||||
|
spawner.must_spawn(net_task(runner));
|
||||||
|
spawner.must_spawn(run_dhcp(stack, gw_ip_addr_str));
|
||||||
|
|
||||||
|
stack
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
||||||
|
use core::net::{Ipv4Addr, SocketAddrV4};
|
||||||
|
|
||||||
|
use edge_dhcp::{
|
||||||
|
io::{self, DEFAULT_SERVER_PORT},
|
||||||
|
server::{Server, ServerOptions},
|
||||||
|
};
|
||||||
|
use edge_nal::UdpBind;
|
||||||
|
use edge_nal_embassy::{Udp, UdpBuffers};
|
||||||
|
|
||||||
|
let ip = Ipv4Addr::from_str(gw_ip_addr).expect("dhcp task failed to parse gw ip");
|
||||||
|
|
||||||
|
let mut buf = [0u8; 1500];
|
||||||
|
|
||||||
|
let mut gw_buf = [Ipv4Addr::UNSPECIFIED];
|
||||||
|
|
||||||
|
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
|
||||||
|
let unbound_socket = Udp::new(stack, &buffers);
|
||||||
|
let mut bound_socket = unbound_socket
|
||||||
|
.bind(core::net::SocketAddr::V4(SocketAddrV4::new(
|
||||||
|
Ipv4Addr::UNSPECIFIED,
|
||||||
|
DEFAULT_SERVER_PORT,
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
_ = io::server::run(
|
||||||
|
&mut Server::<_, 64>::new_with_et(ip),
|
||||||
|
&ServerOptions::new(ip, Some(&mut gw_buf)),
|
||||||
|
&mut bound_socket,
|
||||||
|
&mut buf,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::warn!("DHCP server error: {e:?}"));
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
||||||
|
runner.run().await;
|
||||||
|
}
|
||||||
|
|
||||||
56
src/init/wifi.rs
Normal file
56
src/init/wifi.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_hal::gpio::{Output, OutputConfig};
|
||||||
|
use esp_hal::peripherals::{GPIO3, GPIO14, WIFI};
|
||||||
|
use esp_wifi::wifi::{AccessPointConfiguration, Configuration, WifiController, WifiEvent, WifiState};
|
||||||
|
use esp_wifi::{EspWifiRngSource, EspWifiTimerSource, wifi::Interfaces};
|
||||||
|
use static_cell::make_static;
|
||||||
|
|
||||||
|
pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
|
||||||
|
let mut rf_switch = Output::new(gpio3, esp_hal::gpio::Level::Low, OutputConfig::default());
|
||||||
|
|
||||||
|
rf_switch.set_low();
|
||||||
|
|
||||||
|
Timer::after_millis(150).await;
|
||||||
|
|
||||||
|
let mut antenna_mode = Output::new(gpio14, esp_hal::gpio::Level::Low, OutputConfig::default());
|
||||||
|
|
||||||
|
antenna_mode.set_low();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_wifi<'d: 'static>(
|
||||||
|
timer: impl EspWifiTimerSource + 'd,
|
||||||
|
rng: impl EspWifiRngSource + 'd,
|
||||||
|
wifi: WIFI<'static>,
|
||||||
|
spawner: &mut Spawner,
|
||||||
|
) -> Interfaces<'d> {
|
||||||
|
let esp_wifi_ctrl = make_static!(esp_wifi::init(timer, rng).unwrap());
|
||||||
|
|
||||||
|
let (controller, interfaces) = esp_wifi::wifi::new(esp_wifi_ctrl, wifi).unwrap();
|
||||||
|
|
||||||
|
spawner.must_spawn(connection(controller));
|
||||||
|
|
||||||
|
interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn connection(mut controller: WifiController<'static>) {
|
||||||
|
loop {
|
||||||
|
match esp_wifi::wifi::wifi_state() {
|
||||||
|
WifiState::ApStarted => {
|
||||||
|
// wait until we're no longer connected
|
||||||
|
controller.wait_for_event(WifiEvent::ApStop).await;
|
||||||
|
Timer::after(Duration::from_millis(5000)).await
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if !matches!(controller.is_started(), Ok(true)) {
|
||||||
|
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
|
||||||
|
ssid: "esp-wifi".try_into().unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
controller.set_configuration(&client_config).unwrap();
|
||||||
|
controller.start_async().await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/lib.rs
Normal file
1
src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#![no_std]
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
|
|
||||||
use log::LevelFilter;
|
|
||||||
use simplelog::{ConfigBuilder, SimpleLogger};
|
|
||||||
|
|
||||||
pub fn setup_logger() {
|
|
||||||
let log_level = env::var("LOG_LEVEL")
|
|
||||||
.ok()
|
|
||||||
.and_then(|level| level.parse::<LevelFilter>().ok())
|
|
||||||
.unwrap_or({
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
LevelFilter::Debug
|
|
||||||
} else {
|
|
||||||
LevelFilter::Warn
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let config = ConfigBuilder::new()
|
|
||||||
.set_target_level(LevelFilter::Off)
|
|
||||||
.set_location_level(LevelFilter::Off)
|
|
||||||
.set_thread_level(LevelFilter::Off)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let _ = SimpleLogger::init(log_level, config);
|
|
||||||
}
|
|
||||||
244
src/main.rs
244
src/main.rs
@@ -1,192 +1,90 @@
|
|||||||
#![allow(dead_code)]
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
#![feature(type_alias_impl_trait)]
|
||||||
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
|
|
||||||
use anyhow::Result;
|
use embassy_executor::Spawner;
|
||||||
use feedback::{Feedback, FeedbackImpl};
|
use embassy_net::Stack;
|
||||||
use log::{error, info, warn};
|
use embassy_sync::{
|
||||||
use std::{
|
blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, channel::Channel, pubsub::{
|
||||||
env::{self, args},
|
PubSubChannel, Publisher,
|
||||||
sync::Arc,
|
WaitResult::{Lagged, Message},
|
||||||
time::Duration,
|
}, signal::Signal
|
||||||
};
|
};
|
||||||
use tally_id::TallyID;
|
use embassy_time::{Duration, Timer};
|
||||||
use tokio::{
|
use log::{debug, info};
|
||||||
fs,
|
use static_cell::make_static;
|
||||||
signal::unix::{SignalKind, signal},
|
|
||||||
sync::{
|
|
||||||
Mutex,
|
|
||||||
broadcast::{self, Receiver, Sender},
|
|
||||||
},
|
|
||||||
try_join,
|
|
||||||
};
|
|
||||||
use webserver::start_webserver;
|
|
||||||
|
|
||||||
use crate::{hardware::{create_hotspot, Hotspot}, pm3::run_pm3, store::IDStore, webserver::{spawn_idle_watcher, ActivityNotifier}};
|
use crate::{store::TallyID, webserver::start_webserver};
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
mod drivers;
|
||||||
mod feedback;
|
mod feedback;
|
||||||
mod hardware;
|
mod init;
|
||||||
mod pm3;
|
|
||||||
mod logger;
|
|
||||||
mod tally_id;
|
|
||||||
mod webserver;
|
|
||||||
mod store;
|
mod store;
|
||||||
|
mod webserver;
|
||||||
|
|
||||||
const STORE_PATH: &str = "./data.json";
|
static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
|
||||||
|
|
||||||
async fn run_webserver<H>(
|
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
store: Arc<Mutex<IDStore>>,
|
type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
id_channel: Sender<String>,
|
|
||||||
hotspot: Arc<Mutex<H>>,
|
|
||||||
user_feedback: Arc<Mutex<FeedbackImpl>>,
|
|
||||||
) -> Result<()>
|
|
||||||
where
|
|
||||||
H: Hotspot + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let activity_channel = spawn_idle_watcher(Duration::from_secs(60 * 30), move || {
|
|
||||||
info!("No activity on webserver. Disabling hotspot");
|
|
||||||
let cloned_hotspot = hotspot.clone();
|
|
||||||
let cloned_user_feedback = user_feedback.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = cloned_hotspot.lock().await.disable_hotspot().await;
|
|
||||||
cloned_user_feedback
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.set_device_status(feedback::DeviceStatus::Ready);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let notifier = ActivityNotifier {
|
#[esp_hal_embassy::main]
|
||||||
sender: activity_channel,
|
async fn main(mut spawner: Spawner) {
|
||||||
};
|
let (uart_device, stack, _i2c, _led, buzzer_gpio) =
|
||||||
|
init::hardware::hardware_init(&mut spawner).await;
|
||||||
|
|
||||||
start_webserver(store, id_channel, notifier).await?;
|
wait_for_stack_up(stack).await;
|
||||||
|
|
||||||
Ok(())
|
info!("Starting up...");
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_or_create_store() -> Result<IDStore> {
|
let chan: &'static mut TallyChannel = make_static!(PubSubChannel::new());
|
||||||
if fs::try_exists(STORE_PATH).await? {
|
|
||||||
info!("Loading data from file");
|
//start_webserver(&mut spawner, stack);
|
||||||
IDStore::new_from_json(STORE_PATH).await
|
|
||||||
} else {
|
let publisher = chan.publisher().unwrap();
|
||||||
info!("No data file found. Creating empty one.");
|
|
||||||
Ok(IDStore::new())
|
let mut rtc = drivers::rtc::RTCClock::new(_i2c).await;
|
||||||
|
|
||||||
|
/****************************** Spawning tasks ***********************************/
|
||||||
|
debug!("spawing NFC reader task...");
|
||||||
|
spawner.must_spawn(drivers::nfc_reader::rfid_reader_task(
|
||||||
|
uart_device,
|
||||||
|
publisher,
|
||||||
|
));
|
||||||
|
|
||||||
|
debug!("spawing feedback task..");
|
||||||
|
spawner.must_spawn(feedback::feedback_task(_led, buzzer_gpio));
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
let mut sub = chan.subscriber().unwrap();
|
||||||
|
|
||||||
|
debug!("everything spawned");
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Startup);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
rtc.get_time().await;
|
||||||
|
info!("Current RTC time: {}", rtc.get_time().await);
|
||||||
|
Timer::after(Duration::from_millis(1000)).await;
|
||||||
|
|
||||||
|
// let wait_result = sub.next_message().await;
|
||||||
|
// match wait_result {
|
||||||
|
// Lagged(_) => debug!("Lagged"),
|
||||||
|
// Message(msg) => debug!("Got message: {msg:?}"),
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_hotspot_enable_ids() -> Vec<TallyID> {
|
async fn wait_for_stack_up(stack: Stack<'static>) {
|
||||||
let hotspot_ids: Vec<TallyID> = env::var("HOTSPOT_IDS")
|
loop {
|
||||||
.map(|ids| ids.split(";").map(|id| TallyID(id.to_owned())).collect())
|
if stack.is_link_up() {
|
||||||
.unwrap_or_default();
|
break;
|
||||||
|
|
||||||
if hotspot_ids.is_empty() {
|
|
||||||
warn!(
|
|
||||||
"HOTSPOT_IDS is not set or empty. You will not be able to activate the hotspot via a tally!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
hotspot_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_ids_loop(
|
|
||||||
mut id_channel: Receiver<String>,
|
|
||||||
hotspot_enable_ids: Vec<TallyID>,
|
|
||||||
id_store: Arc<Mutex<IDStore>>,
|
|
||||||
hotspot: Arc<Mutex<impl Hotspot>>,
|
|
||||||
user_feedback: Arc<Mutex<FeedbackImpl>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
while let Ok(tally_id_string) = id_channel.recv().await {
|
|
||||||
let tally_id = TallyID(tally_id_string);
|
|
||||||
|
|
||||||
if hotspot_enable_ids.contains(&tally_id) {
|
|
||||||
info!("Enableing hotspot");
|
|
||||||
let hotspot_enable_result = hotspot.lock().await.enable_hotspot().await;
|
|
||||||
|
|
||||||
match hotspot_enable_result {
|
|
||||||
Ok(_) => {
|
|
||||||
user_feedback
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.set_device_status(feedback::DeviceStatus::HotspotEnabled);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Hotspot: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Should the ID be added anyway or ignored ?
|
|
||||||
}
|
}
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
if id_store.lock().await.add_id(tally_id) {
|
if stack.is_config_up() {
|
||||||
info!("Added new id to current day");
|
break;
|
||||||
|
|
||||||
user_feedback.lock().await.success().await;
|
|
||||||
|
|
||||||
if let Err(e) = id_store.lock().await.export_json(STORE_PATH).await {
|
|
||||||
error!("Failed to save id store to file: {e}");
|
|
||||||
user_feedback.lock().await.failure().await;
|
|
||||||
// TODO: How to handle a failure to save ?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn enter_error_state(feedback: Arc<Mutex<FeedbackImpl>>, hotspot: Arc<Mutex<impl Hotspot>>) {
|
|
||||||
let _ = feedback.lock().await.activate_error_state().await;
|
|
||||||
let _ = hotspot.lock().await.enable_hotspot().await;
|
|
||||||
|
|
||||||
let mut sigterm = signal(SignalKind::terminate()).unwrap();
|
|
||||||
sigterm.recv().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
logger::setup_logger();
|
|
||||||
|
|
||||||
info!("Starting application");
|
|
||||||
|
|
||||||
let user_feedback = Arc::new(Mutex::new(Feedback::new()?));
|
|
||||||
let hotspot = Arc::new(Mutex::new(create_hotspot()?));
|
|
||||||
|
|
||||||
let error_flag_set = args().any(|e| e == "--error" || e == "-e");
|
|
||||||
if error_flag_set {
|
|
||||||
error!("Error flag set. Entering error state");
|
|
||||||
enter_error_state(user_feedback.clone(), hotspot).await;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let store: Arc<Mutex<IDStore>> = Arc::new(Mutex::new(load_or_create_store().await?));
|
|
||||||
let hotspot_enable_ids = get_hotspot_enable_ids();
|
|
||||||
|
|
||||||
let (tx, rx) = broadcast::channel::<String>(32);
|
|
||||||
let sse_tx = tx.clone();
|
|
||||||
|
|
||||||
let pm3_handle = run_pm3(tx);
|
|
||||||
|
|
||||||
user_feedback.lock().await.startup().await;
|
|
||||||
|
|
||||||
let loop_handle = handle_ids_loop(
|
|
||||||
rx,
|
|
||||||
hotspot_enable_ids,
|
|
||||||
store.clone(),
|
|
||||||
hotspot.clone(),
|
|
||||||
user_feedback.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let webserver_handle = run_webserver(
|
|
||||||
store.clone(),
|
|
||||||
sse_tx,
|
|
||||||
hotspot.clone(),
|
|
||||||
user_feedback.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let run_result = try_join!(pm3_handle, loop_handle, webserver_handle);
|
|
||||||
|
|
||||||
if let Err(e) = run_result {
|
|
||||||
error!("Failed to run application: {e}");
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
mod runner;
|
|
||||||
mod parser;
|
|
||||||
|
|
||||||
pub use runner::run_pm3;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
use regex::Regex;
|
|
||||||
|
|
||||||
/// Parses the output of PM3 finds the read IDs
|
|
||||||
/// Example input: `[+] UID.... 3112B710`
|
|
||||||
pub fn parse_line(line: &str) -> Option<String> {
|
|
||||||
let regex = Regex::new(r"(?m)^\[\+\] UID.... (.*)$").unwrap();
|
|
||||||
let result = regex.captures(line);
|
|
||||||
|
|
||||||
result.map(|c| c.get(1).unwrap().as_str().to_owned())
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
use anyhow::{Result, anyhow};
|
|
||||||
use log::{debug, info, trace, warn};
|
|
||||||
use std::env;
|
|
||||||
use std::process::Stdio;
|
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
||||||
use tokio::process::Command;
|
|
||||||
use tokio::select;
|
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
|
||||||
use tokio::sync::broadcast;
|
|
||||||
|
|
||||||
/// Runs the pm3 binary and monitors it's output
|
|
||||||
/// The pm3 binary is ether set in the env var PM3_BIN or found in the path
|
|
||||||
/// The ouput is parsed and send via the `tx` channel
|
|
||||||
pub async fn run_pm3(tx: broadcast::Sender<String>) -> Result<()> {
|
|
||||||
kill_orphans().await;
|
|
||||||
|
|
||||||
let pm3_path = match env::var("PM3_BIN") {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(_) => {
|
|
||||||
info!("PM3_BIN not set. Using default value");
|
|
||||||
"pm3".to_owned()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cmd = Command::new("stdbuf")
|
|
||||||
.arg("-oL")
|
|
||||||
.arg(pm3_path)
|
|
||||||
.arg("-c")
|
|
||||||
.arg("lf hitag reader -@")
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
let stdout = cmd.stdout.take().ok_or(anyhow!("Failed to get stdout"))?;
|
|
||||||
let mut stdin = cmd.stdin.take().ok_or(anyhow!("Failed to get stdin"))?;
|
|
||||||
|
|
||||||
let mut reader = BufReader::new(stdout).lines();
|
|
||||||
|
|
||||||
let mut sigterm = signal(SignalKind::terminate())?;
|
|
||||||
|
|
||||||
let child_handle = tokio::spawn(async move {
|
|
||||||
let mut last_id: String = "".to_owned();
|
|
||||||
while let Some(line) = reader.next_line().await.unwrap_or(None) {
|
|
||||||
trace!("PM3: {line}");
|
|
||||||
if let Some(uid) = super::parser::parse_line(&line) {
|
|
||||||
if last_id == uid {
|
|
||||||
let _ = tx.send(uid.clone());
|
|
||||||
}
|
|
||||||
last_id = uid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
select! {
|
|
||||||
_ = child_handle => {}
|
|
||||||
_ = sigterm.recv() => {
|
|
||||||
debug!("Graceful shutdown of PM3");
|
|
||||||
let _ = stdin.write_all(b"\n").await;
|
|
||||||
let _ = stdin.flush().await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let status = cmd.wait().await?;
|
|
||||||
|
|
||||||
// We use the exit code here because status.success() is false if the child was terminated by a
|
|
||||||
// signal
|
|
||||||
let code = status.code().unwrap_or(0);
|
|
||||||
|
|
||||||
if code == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("PM3 exited with a non-zero exit code: {code}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Kills any open pm3 instances
|
|
||||||
/// Also funny name. hehehe.
|
|
||||||
async fn kill_orphans() {
|
|
||||||
let kill_result = Command::new("pkill")
|
|
||||||
.arg("-KILL")
|
|
||||||
.arg("-x")
|
|
||||||
.arg("proxmark3")
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match kill_result {
|
|
||||||
Ok(_) => {
|
|
||||||
debug!("Successfully killed orphaned pm3 instances");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to kill pm3 orphans: {e} Continuing anyway");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
use crate::tally_id::TallyID;
|
use super::TallyID;
|
||||||
use serde::{Deserialize, Serialize};
|
use alloc::collections::BTreeMap;
|
||||||
use std::collections::HashMap;
|
use alloc::string::String;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
|
#[derive(Clone, Serialize)]
|
||||||
pub struct Name {
|
pub struct Name {
|
||||||
pub first: String,
|
pub first: String,
|
||||||
pub last: String,
|
pub last: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Clone, Serialize)]
|
||||||
pub struct IDMapping {
|
pub struct IDMapping {
|
||||||
id_map: HashMap<TallyID, Name>,
|
id_map: BTreeMap<TallyID, Name>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IDMapping {
|
impl IDMapping {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
IDMapping {
|
IDMapping {
|
||||||
id_map: HashMap::new(),
|
id_map: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,49 +29,3 @@ impl IDMapping {
|
|||||||
self.id_map.insert(id, name);
|
self.id_map.insert(id, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic() {
|
|
||||||
let mut map = IDMapping::new();
|
|
||||||
let id1 = TallyID("A2Fb44".to_owned());
|
|
||||||
let name1 = Name {
|
|
||||||
first: "Max".to_owned(),
|
|
||||||
last: "Mustermann".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
map.add_mapping(id1.clone(), name1.clone());
|
|
||||||
|
|
||||||
let res = map.map(&id1);
|
|
||||||
|
|
||||||
assert_eq!(res, Some(&name1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multiple() {
|
|
||||||
let mut map = IDMapping::new();
|
|
||||||
let id1 = TallyID("A2Fb44".to_owned());
|
|
||||||
let name1 = Name {
|
|
||||||
first: "Max".to_owned(),
|
|
||||||
last: "Mustermann".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let id2 = TallyID("7D3DF5B5".to_owned());
|
|
||||||
let name2 = Name {
|
|
||||||
first: "First".to_owned(),
|
|
||||||
last: "Last".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
map.add_mapping(id1.clone(), name1.clone());
|
|
||||||
map.add_mapping(id2.clone(), name2.clone());
|
|
||||||
|
|
||||||
let res = map.map(&id1);
|
|
||||||
assert_eq!(res, Some(&name1));
|
|
||||||
|
|
||||||
let res = map.map(&id2);
|
|
||||||
assert_eq!(res, Some(&name2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,123 +1,19 @@
|
|||||||
use anyhow::{Result, anyhow};
|
use super::Date;
|
||||||
use serde::{Deserialize, Serialize};
|
use super::IDMapping;
|
||||||
use std::collections::{HashMap, HashSet};
|
use super::TallyID;
|
||||||
use tokio::fs;
|
use alloc::collections::BTreeMap;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use crate::{store::IDMapping, tally_id::TallyID};
|
#[derive(Clone)]
|
||||||
|
|
||||||
/// Represents a single day that IDs can attend
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct AttendanceDay {
|
pub struct AttendanceDay {
|
||||||
date: String,
|
date: Date,
|
||||||
ids: Vec<TallyID>,
|
ids: Vec<TallyID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores all the days
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct IDStore {
|
|
||||||
days: HashMap<String, AttendanceDay>,
|
|
||||||
pub mapping: IDMapping,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IDStore {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
IDStore {
|
|
||||||
days: HashMap::new(),
|
|
||||||
mapping: IDMapping::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creats a new `IDStore` from a json file
|
|
||||||
pub async fn new_from_json(filepath: &str) -> Result<Self> {
|
|
||||||
let read_string = fs::read_to_string(filepath).await?;
|
|
||||||
Ok(serde_json::from_str(&read_string)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new id for the current day
|
|
||||||
/// Returns false if ID is already present at the current day.
|
|
||||||
pub fn add_id(&mut self, id: TallyID) -> bool {
|
|
||||||
self.get_current_day().add_id(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the `AttendanceDay` of the current day
|
|
||||||
/// Creates a new if not exists
|
|
||||||
pub fn get_current_day(&mut self) -> &mut AttendanceDay {
|
|
||||||
let current_day = get_day_str();
|
|
||||||
|
|
||||||
if self.days.contains_key(¤t_day) {
|
|
||||||
return self.days.get_mut(¤t_day).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.days.insert(
|
|
||||||
current_day.clone(),
|
|
||||||
AttendanceDay::new(¤t_day.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.days.get_mut(¤t_day.clone()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes the store to a json file
|
|
||||||
pub async fn export_json(&self, filepath: &str) -> Result<()> {
|
|
||||||
fs::write(filepath, serde_json::to_string(&self)?).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Export the store to a csv file.
|
|
||||||
/// With days in the rows and IDs in the collum.
|
|
||||||
pub fn export_csv(&self) -> Result<String> {
|
|
||||||
let mut csv = String::new();
|
|
||||||
let seperator = ";";
|
|
||||||
let mut user_ids: HashSet<TallyID> = HashSet::new();
|
|
||||||
|
|
||||||
for day in self.days.values() {
|
|
||||||
for id in day.ids.iter() {
|
|
||||||
user_ids.insert(id.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut user_ids: Vec<TallyID> = user_ids.into_iter().collect();
|
|
||||||
user_ids.sort();
|
|
||||||
|
|
||||||
let mut days: Vec<String> = self.days.keys().cloned().collect();
|
|
||||||
days.sort();
|
|
||||||
|
|
||||||
let header = days.join(seperator);
|
|
||||||
csv.push_str(&format!(
|
|
||||||
"ID{seperator}Nachname{seperator}Vorname{seperator}{header}\n"
|
|
||||||
));
|
|
||||||
|
|
||||||
for user_id in user_ids.iter() {
|
|
||||||
let id = &user_id.0.to_string();
|
|
||||||
let name = self.mapping.map(user_id);
|
|
||||||
|
|
||||||
let firstname = name.map(|e| e.first.clone()).unwrap_or("".to_owned());
|
|
||||||
let lastname = name.map(|e| e.last.clone()).unwrap_or("".to_owned());
|
|
||||||
|
|
||||||
csv.push_str(&format!("{id}{seperator}{lastname}{seperator}{firstname}"));
|
|
||||||
for day in days.iter() {
|
|
||||||
let was_there: bool = self
|
|
||||||
.days
|
|
||||||
.get(day)
|
|
||||||
.ok_or(anyhow!("Failed to access day"))?
|
|
||||||
.ids
|
|
||||||
.contains(user_id);
|
|
||||||
|
|
||||||
if was_there {
|
|
||||||
csv.push_str(&format!("{seperator}x"));
|
|
||||||
} else {
|
|
||||||
csv.push_str(seperator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
csv.push('\n');
|
|
||||||
}
|
|
||||||
Ok(csv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttendanceDay {
|
impl AttendanceDay {
|
||||||
fn new(day: &str) -> Self {
|
fn new(date: Date) -> Self {
|
||||||
Self {
|
Self {
|
||||||
date: day.to_owned(),
|
date,
|
||||||
ids: Vec::new(),
|
ids: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +29,43 @@ impl AttendanceDay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_day_str() -> String {
|
#[derive(Clone)]
|
||||||
let now = chrono::offset::Local::now();
|
pub struct IDStore {
|
||||||
now.format("%Y-%m-%d").to_string()
|
pub days: BTreeMap<Date, AttendanceDay>,
|
||||||
|
pub mapping: IDMapping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IDStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
IDStore {
|
||||||
|
days: BTreeMap::new(),
|
||||||
|
mapping: IDMapping::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_from_storage() -> Self {
|
||||||
|
// TODO: implement
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new id for the current day
|
||||||
|
/// Returns false if ID is already present at the current day.
|
||||||
|
pub fn add_id(&mut self, id: TallyID) -> bool {
|
||||||
|
self.get_current_day().add_id(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `AttendanceDay` of the current day
|
||||||
|
/// Creates a new if not exists
|
||||||
|
pub fn get_current_day(&mut self) -> &mut AttendanceDay {
|
||||||
|
let current_day: Date = 1;
|
||||||
|
|
||||||
|
if self.days.contains_key(¤t_day) {
|
||||||
|
return self.days.get_mut(¤t_day).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.days
|
||||||
|
.insert(current_day, AttendanceDay::new(current_day));
|
||||||
|
|
||||||
|
self.days.get_mut(¤t_day.clone()).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
mod id_store;
|
|
||||||
mod id_mapping;
|
mod id_mapping;
|
||||||
|
mod id_store;
|
||||||
|
|
||||||
|
pub use id_mapping::{IDMapping, Name};
|
||||||
pub use id_store::IDStore;
|
pub use id_store::IDStore;
|
||||||
pub use id_mapping::{IDMapping,Name};
|
|
||||||
|
pub type TallyID = [u8; 8];
|
||||||
|
pub type Date = u64;
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cmp::Ordering,
|
|
||||||
fmt::Display,
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Represents the ID that is stored on the Tally
|
|
||||||
/// Is case-insensitive.
|
|
||||||
/// While any string can be a ID, most IDs are going to be a hex string.
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
|
||||||
pub struct TallyID(pub String);
|
|
||||||
|
|
||||||
impl PartialEq for TallyID {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.0.eq_ignore_ascii_case(&other.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for TallyID {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
self.0.to_uppercase().hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for TallyID {}
|
|
||||||
|
|
||||||
impl Ord for TallyID {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
self.0.to_uppercase().cmp(&other.0.to_uppercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for TallyID {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for TallyID {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0.to_uppercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use log::error;
|
|
||||||
use rocket::{
|
|
||||||
Data, Request,
|
|
||||||
fairing::{Fairing, Info, Kind},
|
|
||||||
};
|
|
||||||
use tokio::{sync::mpsc, time::timeout};
|
|
||||||
|
|
||||||
pub struct ActivityNotifier {
|
|
||||||
pub sender: mpsc::Sender<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl Fairing for ActivityNotifier {
|
|
||||||
fn info(&self) -> Info {
|
|
||||||
Info {
|
|
||||||
name: "Keeps track of time since the last request",
|
|
||||||
kind: Kind::Request | Kind::Response,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_request(&self, _: &mut Request<'_>, _: &mut Data<'_>) {
|
|
||||||
error!("on_request");
|
|
||||||
let _ = self.sender.try_send(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn_idle_watcher<F>(idle_duration: Duration, mut on_idle: F) -> mpsc::Sender<()>
|
|
||||||
where
|
|
||||||
F: FnMut() + Send + 'static,
|
|
||||||
{
|
|
||||||
let (tx, mut rx) = mpsc::channel::<()>(100);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let idle = timeout(idle_duration, rx.recv()).await;
|
|
||||||
if idle.is_err() {
|
|
||||||
// No activity received in the duration
|
|
||||||
on_idle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tx
|
|
||||||
}
|
|
||||||
77
src/webserver/assets.rs
Normal file
77
src/webserver/assets.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use dir_embed::Embed;
|
||||||
|
use picoserve::response::Content;
|
||||||
|
|
||||||
|
#[derive(Embed)]
|
||||||
|
#[dir = "../../web/dist"]
|
||||||
|
#[mode = "mime"]
|
||||||
|
pub struct Assets;
|
||||||
|
|
||||||
|
impl<State, CurrentPathParameters>
|
||||||
|
picoserve::routing::PathRouterService<State, CurrentPathParameters> for Assets
|
||||||
|
{
|
||||||
|
async fn call_request_handler_service<
|
||||||
|
R: picoserve::io::embedded_io_async::Read,
|
||||||
|
W: picoserve::response::ResponseWriter<Error = R::Error>,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
state: &State,
|
||||||
|
current_path_parameters: CurrentPathParameters,
|
||||||
|
path: picoserve::request::Path<'_>,
|
||||||
|
request: picoserve::request::Request<'_, R>,
|
||||||
|
response_writer: W,
|
||||||
|
) -> Result<picoserve::ResponseSent, W::Error> {
|
||||||
|
let requested_path = path.encoded();
|
||||||
|
|
||||||
|
let requested_file = if requested_path == "/" {
|
||||||
|
Self::get("index.html")
|
||||||
|
} else if let Some(striped_path) = requested_path.strip_prefix("/") {
|
||||||
|
Self::get(striped_path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match requested_file {
|
||||||
|
Some(content) => {
|
||||||
|
let response = picoserve::response::Response::new(
|
||||||
|
picoserve::response::StatusCode::OK,
|
||||||
|
StaticAsset(content.0, content.1),
|
||||||
|
);
|
||||||
|
|
||||||
|
response_writer
|
||||||
|
.write_response(request.body_connection.finalize().await.unwrap(), response)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
use picoserve::routing::PathRouter;
|
||||||
|
picoserve::routing::NotFound
|
||||||
|
.call_path_router(
|
||||||
|
state,
|
||||||
|
current_path_parameters,
|
||||||
|
path,
|
||||||
|
request,
|
||||||
|
response_writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StaticAsset(pub &'static [u8], pub &'static str);
|
||||||
|
|
||||||
|
impl Content for StaticAsset {
|
||||||
|
fn content_type(&self) -> &'static str {
|
||||||
|
self.1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_length(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_content<W: embedded_io_async::Write>(
|
||||||
|
self,
|
||||||
|
mut writer: W,
|
||||||
|
) -> Result<(), W::Error> {
|
||||||
|
writer.write_all(self.0).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,54 @@
|
|||||||
mod server;
|
use embassy_executor::Spawner;
|
||||||
mod activity_fairing;
|
use embassy_net::Stack;
|
||||||
|
use embassy_time::Duration;
|
||||||
|
use picoserve::{AppBuilder, AppRouter, routing::get};
|
||||||
|
use static_cell::make_static;
|
||||||
|
|
||||||
pub use activity_fairing::{ActivityNotifier,spawn_idle_watcher};
|
mod assets;
|
||||||
pub use server::start_webserver;
|
|
||||||
|
|
||||||
|
pub fn start_webserver(spawner: &mut Spawner, stack: Stack<'static>) {
|
||||||
|
let app = make_static!(AppProps.build_app());
|
||||||
|
|
||||||
|
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
||||||
|
start_read_request: Some(Duration::from_secs(5)),
|
||||||
|
persistent_start_read_request: Some(Duration::from_secs(1)),
|
||||||
|
read_request: Some(Duration::from_secs(1)),
|
||||||
|
write: Some(Duration::from_secs(1)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let _ = spawner.spawn(webserver_task(0, stack, app, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppProps;
|
||||||
|
|
||||||
|
impl AppBuilder for AppProps {
|
||||||
|
type PathRouter = impl picoserve::routing::PathRouter;
|
||||||
|
|
||||||
|
fn build_app(self) -> picoserve::Router<Self::PathRouter> {
|
||||||
|
picoserve::Router::from_service(assets::Assets).route("/api/a", get(async move || "Hello"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn webserver_task(
|
||||||
|
id: usize,
|
||||||
|
stack: embassy_net::Stack<'static>,
|
||||||
|
app: &'static AppRouter<AppProps>,
|
||||||
|
config: &'static picoserve::Config<Duration>,
|
||||||
|
) -> ! {
|
||||||
|
let mut tcp_rx_buffer = [0u8; 1024];
|
||||||
|
let mut tcp_tx_buffer = [0u8; 1024];
|
||||||
|
let mut http_buffer = [0u8; 2048];
|
||||||
|
|
||||||
|
picoserve::listen_and_serve(
|
||||||
|
id,
|
||||||
|
app,
|
||||||
|
config,
|
||||||
|
stack,
|
||||||
|
80,
|
||||||
|
&mut tcp_rx_buffer,
|
||||||
|
&mut tcp_tx_buffer,
|
||||||
|
&mut http_buffer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
use log::{error, info, warn};
|
|
||||||
use rocket::http::Status;
|
|
||||||
use rocket::response::stream::{Event, EventStream};
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
use rocket::{Config, Shutdown, State, post};
|
|
||||||
use rocket::{get, http::ContentType, response::content::RawHtml, routes};
|
|
||||||
use rust_embed::Embed;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::env;
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::select;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tokio::sync::broadcast::Sender;
|
|
||||||
|
|
||||||
use crate::store::{IDMapping, IDStore, Name};
|
|
||||||
use crate::tally_id::TallyID;
|
|
||||||
use crate::webserver::ActivityNotifier;
|
|
||||||
|
|
||||||
#[derive(Embed)]
|
|
||||||
#[folder = "web/dist"]
|
|
||||||
struct Asset;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct NewMapping {
|
|
||||||
id: String,
|
|
||||||
name: Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_webserver(
|
|
||||||
store: Arc<Mutex<IDStore>>,
|
|
||||||
sse_broadcaster: Sender<String>,
|
|
||||||
fairing: ActivityNotifier,
|
|
||||||
) -> Result<(), rocket::Error> {
|
|
||||||
let port = match env::var("HTTP_PORT") {
|
|
||||||
Ok(port) => port.parse().unwrap_or_else(|_| {
|
|
||||||
warn!("Failed to parse HTTP_PORT. Using default 80");
|
|
||||||
80
|
|
||||||
}),
|
|
||||||
Err(_) => 80,
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
address: "0.0.0.0".parse().unwrap(), // Listen on all interfaces
|
|
||||||
port,
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
rocket::custom(config)
|
|
||||||
.attach(fairing)
|
|
||||||
.mount(
|
|
||||||
"/",
|
|
||||||
routes![
|
|
||||||
static_files,
|
|
||||||
index,
|
|
||||||
export_csv,
|
|
||||||
id_event,
|
|
||||||
get_mapping,
|
|
||||||
add_mapping
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.manage(store)
|
|
||||||
.manage(sse_broadcaster)
|
|
||||||
.launch()
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
fn index() -> Option<RawHtml<Cow<'static, [u8]>>> {
|
|
||||||
let asset = Asset::get("index.html")?;
|
|
||||||
Some(RawHtml(asset.data))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/<file..>")]
|
|
||||||
fn static_files(file: std::path::PathBuf) -> Option<(ContentType, Vec<u8>)> {
|
|
||||||
let filename = file.display().to_string();
|
|
||||||
let asset = Asset::get(&filename)?;
|
|
||||||
let content_type = file
|
|
||||||
.extension()
|
|
||||||
.and_then(OsStr::to_str)
|
|
||||||
.and_then(ContentType::from_extension)
|
|
||||||
.unwrap_or(ContentType::Bytes);
|
|
||||||
|
|
||||||
Some((content_type, asset.data.into_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/api/idevent")]
|
|
||||||
fn id_event(sse_broadcaster: &State<Sender<String>>, shutdown: Shutdown) -> EventStream![] {
|
|
||||||
let mut rx = sse_broadcaster.subscribe();
|
|
||||||
EventStream! {
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
msg = rx.recv() => {
|
|
||||||
if let Ok(id) = msg {
|
|
||||||
yield Event::data(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = &mut shutdown.clone() => {
|
|
||||||
// Shutdown signal received, exit the loop
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/api/csv")]
|
|
||||||
async fn export_csv(manager: &State<Arc<Mutex<IDStore>>>) -> Result<String, Status> {
|
|
||||||
info!("Exporting CSV");
|
|
||||||
match manager.lock().await.export_csv() {
|
|
||||||
Ok(csv) => Ok(csv),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to generate csv: {e}");
|
|
||||||
Err(Status::InternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/api/mapping")]
|
|
||||||
async fn get_mapping(store: &State<Arc<Mutex<IDStore>>>) -> Json<IDMapping> {
|
|
||||||
Json(store.lock().await.mapping.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/api/mapping", format = "json", data = "<new_mapping>")]
|
|
||||||
async fn add_mapping(store: &State<Arc<Mutex<IDStore>>>, new_mapping: Json<NewMapping>) -> Status {
|
|
||||||
if new_mapping.id.is_empty()
|
|
||||||
|| new_mapping.name.first.is_empty()
|
|
||||||
|| new_mapping.name.last.is_empty()
|
|
||||||
{
|
|
||||||
return Status::BadRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
store
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.mapping
|
|
||||||
.add_mapping(TallyID(new_mapping.id.clone()), new_mapping.name.clone());
|
|
||||||
|
|
||||||
Status::Created
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user