mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2025-08-01 22:34:17 +00:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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.
2363
Cargo.lock
generated
2363
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
90
Cargo.toml
90
Cargo.toml
@ -3,24 +3,78 @@ 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-io = "0.6.1"
|
||||||
log = "0.4.27"
|
embedded-io-async = "0.6.1"
|
||||||
simplelog = "0.12.2"
|
esp-alloc = "0.8.0"
|
||||||
rppal = { version = "0.22.1", features = ["hal"] }
|
esp-hal = { version = "1.0.0-beta.1", features = ["esp32c6", "unstable"] }
|
||||||
smart-leds = "0.3"
|
smoltcp = { version = "0.12.0", default-features = false, features = [
|
||||||
ws2812-spi = "0.3"
|
"medium-ethernet",
|
||||||
rgb = "0.8.50"
|
"multicast",
|
||||||
anyhow = "1.0.98"
|
"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 = ["task-arena-size-20480"] }
|
||||||
|
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"] }
|
||||||
|
ws2812-spi = "0.5.1"
|
||||||
|
chrono = { version = "0.4.41", default-features = false }
|
||||||
|
dir-embed = "0.3.0"
|
||||||
|
|
||||||
|
[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
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
120
src/drivers/rtc.rs
Normal file
120
src/drivers/rtc.rs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
pub async fn rtc_task() {
|
||||||
|
info!("RTC task started");
|
||||||
|
// Initialize I2C and RTC here
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Read RTC time and update UTC_TIME signal
|
||||||
|
// let utc_time = read_rtc_time(&mut rtc).await.unwrap();
|
||||||
|
// UTC_TIME.signal(utc_time);
|
||||||
|
|
||||||
|
// Simulate waiting for an interrupt or event
|
||||||
|
Timer::after(Duration::from_millis(1000)).await;
|
||||||
|
info!("RTC tick");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ************************************************************************************** */
|
||||||
|
|
||||||
|
|
||||||
|
// use ds3231::{Alarm1Config, DS3231, DS3231Error, Seconds};
|
||||||
|
// use embassy_time::{Duration, Timer};
|
||||||
|
// use esp_hal::{
|
||||||
|
// Async,
|
||||||
|
// i2c::{self, master::I2c},
|
||||||
|
// peripherals,
|
||||||
|
// };
|
||||||
|
// use log::{debug, error, info};
|
||||||
|
|
||||||
|
// use crate::{UTC_TIME, drivers, init};
|
||||||
|
|
||||||
|
// const RTC_ADDRESS: u8 = 0x57;
|
||||||
|
|
||||||
|
// #[embassy_executor::task]
|
||||||
|
// pub async fn rtc_task(
|
||||||
|
// //i2c: i2c::master::I2c<'static, Async>,
|
||||||
|
// //sqw_pin: peripherals::GPIO21<'static>,
|
||||||
|
// ) {
|
||||||
|
// //UTC_TIME.signal(155510);
|
||||||
|
|
||||||
|
// // debug!("init rtc interrupt");
|
||||||
|
// // let mut rtc_interrupt = init::hardware::setup_rtc_iterrupt(sqw_pin).await;
|
||||||
|
// // debug!("configuring rtc");
|
||||||
|
// // let mut rtc = drivers::rtc::rtc_config(i2c).await;
|
||||||
|
|
||||||
|
// // let timestamp_result = drivers::rtc::read_rtc_time(&mut rtc).await;
|
||||||
|
// // UTC_TIME.signal(timestamp_result.unwrap());
|
||||||
|
|
||||||
|
// debug!("rtc up");
|
||||||
|
// loop {
|
||||||
|
// info!("Current UTC time: {}", UTC_TIME.wait().await);
|
||||||
|
// // debug!("Waiting for RTC interrupt...");
|
||||||
|
// // rtc_interrupt.wait_for_falling_edge().await;
|
||||||
|
// // debug!("RTC interrupt triggered");
|
||||||
|
// // let timestamp_result = drivers::rtc::read_rtc_time(&mut rtc).await;
|
||||||
|
// // UTC_TIME.signal(timestamp_result.unwrap());
|
||||||
|
// // Timer::after(Duration::from_secs(1)).await; // Debounce delay
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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 daily_alarm = Alarm1Config::AtTime {
|
||||||
|
// hours: 0, // set alarm every day 00:00:00 to sync time
|
||||||
|
// minutes: 0,
|
||||||
|
// seconds: 10,
|
||||||
|
// is_pm: None, // 24-hour mode
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let naive_dt = chrono::NaiveDateTime::from_timestamp_opt(*utc_time as i64, 0)
|
||||||
|
// .expect("Invalid timestamp for NaiveDateTime");
|
||||||
|
// rtc.set_datetime(&naive_dt).await.unwrap_or_else(|e| {
|
||||||
|
// error!("Failed to set RTC datetime: {:?}", e);
|
||||||
|
// panic!();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if let Err(e) = rtc.set_alarm1(&daily_alarm).await {
|
||||||
|
// error!("Failed to configure RTC: {:?}", e);
|
||||||
|
// panic!();
|
||||||
|
// }
|
||||||
|
// rtc
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub async fn read_rtc_time<'a>(
|
||||||
|
// rtc: &'a mut DS3231<I2c<'static, Async>>,
|
||||||
|
// ) -> Result<u64, DS3231Error<esp_hal::i2c::master::Error>> {
|
||||||
|
// match rtc.datetime().await {
|
||||||
|
// Ok(datetime) => {
|
||||||
|
// let utc_time = datetime.and_utc().timestamp() as u64;
|
||||||
|
// Ok(utc_time)
|
||||||
|
// }
|
||||||
|
// Err(e) => {
|
||||||
|
// error!("Failed to read RTC datetime: {:?}", e);
|
||||||
|
// Err(e)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO Update time when device is connected other device over Wifi
|
||||||
|
/* pub async fn update_rtc_time<'a>(rtc: &'a mut DS3231<I2c<'static, Async>>, datetime: u64) -> Result<(), DS3231Error<esp_hal::i2c::master::Error>> {
|
||||||
|
|
||||||
|
match rtc.set_datetime(datetime).await {
|
||||||
|
info!("RTC datetime updated to: {}", datetime);
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to update RTC datetime: {:?}", e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
324
src/feedback.rs
324
src/feedback.rs
@ -1,181 +1,161 @@
|
|||||||
use anyhow::Result;
|
use embassy_time::{Delay, Duration, Timer};
|
||||||
use log::error;
|
use esp_hal::{delay, gpio::Output, peripherals};
|
||||||
use rgb::RGB8;
|
use log::{debug, error, info};
|
||||||
use smart_leds::colors::{GREEN, RED};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::{join, time::sleep};
|
|
||||||
|
|
||||||
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);
|
Idle,
|
||||||
|
|
||||||
pub enum DeviceStatus {
|
|
||||||
NotReady,
|
|
||||||
Ready,
|
|
||||||
HotspotEnabled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceStatus {
|
#[embassy_executor::task]
|
||||||
pub fn color(&self) -> RGB8 {
|
pub async fn feedback_task(buzzer: peripherals::GPIO19<'static>) {
|
||||||
match self {
|
debug!("Starting feedback task");
|
||||||
Self::NotReady => RGB8::new(0, 0, 0),
|
let mut buzzer = init::hardware::setup_buzzer(buzzer);
|
||||||
Self::Ready => RGB8::new(0, 50, 0),
|
loop {
|
||||||
Self::HotspotEnabled => RGB8::new(0, 0, 50),
|
let feedback_state = FEEDBACK_STATE.wait().await;
|
||||||
}
|
match feedback_state {
|
||||||
}
|
FeedbackState::Ack => {
|
||||||
}
|
buzzer.set_high();
|
||||||
pub struct Feedback<B: Buzzer, L: StatusLed> {
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
device_status: DeviceStatus,
|
buzzer.set_low();
|
||||||
buzzer: B,
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
led: L,
|
}
|
||||||
}
|
FeedbackState::Nak => {
|
||||||
|
buzzer.set_high();
|
||||||
impl<B: Buzzer, L: StatusLed> Feedback<B, L> {
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
pub async fn success(&mut self) {
|
buzzer.set_low();
|
||||||
let buzzer_handle = Self::beep_ack(&mut self.buzzer);
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, LED_BLINK_DURATION);
|
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| {
|
}
|
||||||
error!("Failed to buzz: {err}");
|
FeedbackState::Error => {}
|
||||||
});
|
FeedbackState::Startup => {
|
||||||
|
buzzer.set_high();
|
||||||
let _ = self.led_to_status();
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
}
|
buzzer.set_low();
|
||||||
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
pub async fn failure(&mut self) {
|
buzzer.set_high();
|
||||||
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
|
buzzer.set_low();
|
||||||
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
buzzer.set_high();
|
||||||
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
buzzer_result.unwrap_or_else(|err| {
|
buzzer.set_low();
|
||||||
error!("Failed to buzz: {err}");
|
}
|
||||||
});
|
FeedbackState::Idle => {
|
||||||
|
// Do nothing
|
||||||
let _ = self.led_to_status();
|
}
|
||||||
}
|
};
|
||||||
|
debug!("Feedback state: {:?}", feedback_state);
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_device_status(&mut self, status: DeviceStatus){
|
|
||||||
self.device_status = status;
|
|
||||||
let _ = self.led_to_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn led_to_status(&mut self) -> Result<()> {
|
|
||||||
self.led.turn_on(self.device_status.color())
|
|
||||||
}
|
|
||||||
|
|
||||||
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")]
|
// async fn beep_ack() {
|
||||||
pub type FeedbackImpl = Feedback<MockBuzzer, MockLed>;
|
// buzzer.set_high();
|
||||||
#[cfg(not(feature = "mock_pi"))]
|
// buzzer.set_low();
|
||||||
pub type FeedbackImpl = Feedback<GPIOBuzzer, SpiLed>;
|
// //Timer::after(Duration::from_millis(100)).await;
|
||||||
|
// }
|
||||||
|
|
||||||
impl FeedbackImpl {
|
/* pub async fn failure(&mut self) {
|
||||||
pub fn new() -> Result<Self> {
|
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
|
||||||
#[cfg(feature = "mock_pi")]
|
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
|
||||||
{
|
|
||||||
Ok(Feedback {
|
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
||||||
device_status: DeviceStatus::NotReady,
|
|
||||||
buzzer: MockBuzzer {},
|
buzzer_result.unwrap_or_else(|err| { error!("Failed to buzz: {err}");
|
||||||
led: MockLed {},
|
});
|
||||||
})
|
|
||||||
}
|
let _ = self.led_to_status();
|
||||||
#[cfg(not(feature = "mock_pi"))]
|
|
||||||
{
|
|
||||||
Ok(Feedback {
|
|
||||||
device_status: DeviceStatus::NotReady,
|
|
||||||
buzzer: GPIOBuzzer::new_default()?,
|
|
||||||
led: SpiLed::new()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
137
src/init/hardware.rs
Normal file
137
src/init/hardware.rs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_net::{driver, Stack};
|
||||||
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||||
|
use embassy_sync::blocking_mutex::Mutex;
|
||||||
|
use esp_hal::config;
|
||||||
|
use esp_hal::gpio::{Input, Pull};
|
||||||
|
use esp_hal::i2c::master::Config;
|
||||||
|
use esp_hal::peripherals::{self, GPIO0, GPIO1, GPIO3, GPIO4, GPIO5, GPIO6, GPIO7, GPIO19, GPIO21, GPIO22, GPIO23, I2C0, UART1};
|
||||||
|
use esp_hal::time::Rate;
|
||||||
|
use esp_hal::{
|
||||||
|
Async,
|
||||||
|
clock::CpuClock,
|
||||||
|
timer::{systimer::SystemTimer, timg::TimerGroup},
|
||||||
|
uart::Uart,
|
||||||
|
i2c::master::I2c,
|
||||||
|
gpio::{Output, OutputConfig}
|
||||||
|
};
|
||||||
|
use esp_println::logger::init_logger;
|
||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
|
use crate::init::wifi;
|
||||||
|
use crate::init::network;
|
||||||
|
|
||||||
|
/*************************************************
|
||||||
|
* GPIO Pinout Xiao Esp32c6
|
||||||
|
*
|
||||||
|
* D0 -> GPIO0 -> Level Shifter OE
|
||||||
|
* D1 -> GPIO1 -> Level Shifter A0 -> LED
|
||||||
|
* D3 -> GPIO21 -> SQW Interrupt RTC
|
||||||
|
* D4 -> GPIO22 -> SDA
|
||||||
|
* D5 -> GPIO23 -> SCL
|
||||||
|
* D7 -> GPIO17 -> Level Shifter A1 -> NFC Reader
|
||||||
|
* D8 -> GPIO19 -> Buzzer
|
||||||
|
*
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn hardware_init(spawner: &mut Spawner) -> (Uart<'static, Async>, Stack<'static>, I2c<'static, Async>, GPIO21<'static>, GPIO19<'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);
|
||||||
|
|
||||||
|
init_lvl_shifter(peripherals.GPIO0);
|
||||||
|
|
||||||
|
let uart_device = setup_uart(peripherals.UART1, peripherals.GPIO7, peripherals.GPIO6);
|
||||||
|
|
||||||
|
let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23);
|
||||||
|
|
||||||
|
//RTC Interrupt pin
|
||||||
|
let sqw_pin = peripherals.GPIO21;
|
||||||
|
|
||||||
|
let buzzer_gpio = peripherals.GPIO19;
|
||||||
|
|
||||||
|
debug!("hardware init done");
|
||||||
|
|
||||||
|
(uart_device, stack, i2c_device, sqw_pin, 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());
|
||||||
|
oe_lvl_shifter.set_high();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_uart(
|
||||||
|
uart1: UART1<'static>,
|
||||||
|
uart_rx: GPIO7<'static>,
|
||||||
|
uart_tx: GPIO6<'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!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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!();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
i2c
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_rtc_iterrupt(sqw_pin: GPIO21<'static>) -> Input<'static> {
|
||||||
|
debug!("init rtc interrupt");
|
||||||
|
let config = esp_hal::gpio::InputConfig::default().with_pull(Pull::Up); //Active low interrupt in rtc
|
||||||
|
let sqw_interrupt = Input::new(sqw_pin, config);
|
||||||
|
sqw_interrupt
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_buzzer(buzzer_gpio: GPIO19<'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_spi_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);
|
|
||||||
}
|
|
248
src/main.rs
248
src/main.rs
@ -1,192 +1,96 @@
|
|||||||
#![allow(dead_code)]
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
#![feature(type_alias_impl_trait)]
|
||||||
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
|
|
||||||
use anyhow::Result;
|
use esp_alloc::EspHeap;
|
||||||
use feedback::{Feedback, FeedbackImpl};
|
use embassy_executor::Spawner;
|
||||||
use log::{error, info, warn};
|
use embassy_net::Stack;
|
||||||
use std::{
|
use embassy_sync::{
|
||||||
env::{self, args},
|
blocking_mutex::raw::{NoopRawMutex, CriticalSectionRawMutex},
|
||||||
sync::Arc,
|
pubsub::{
|
||||||
time::Duration,
|
PubSubChannel, Publisher,
|
||||||
};
|
WaitResult::{Lagged, Message},
|
||||||
use tally_id::TallyID;
|
|
||||||
use tokio::{
|
|
||||||
fs,
|
|
||||||
signal::unix::{SignalKind, signal},
|
|
||||||
sync::{
|
|
||||||
Mutex,
|
|
||||||
broadcast::{self, Receiver, Sender},
|
|
||||||
},
|
},
|
||||||
try_join,
|
signal::Signal,
|
||||||
};
|
};
|
||||||
use webserver::start_webserver;
|
use embassy_time::{Duration, Timer};
|
||||||
|
use log::{debug, info};
|
||||||
|
use static_cell::make_static;
|
||||||
|
|
||||||
use crate::{hardware::{create_hotspot, Hotspot}, pm3::run_pm3, store::IDStore, webserver::{spawn_idle_watcher, ActivityNotifier}};
|
use crate::{store::TallyID, webserver::start_webserver};
|
||||||
|
|
||||||
|
mod init;
|
||||||
|
mod drivers;
|
||||||
mod feedback;
|
mod feedback;
|
||||||
mod hardware;
|
|
||||||
mod pm3;
|
|
||||||
mod logger;
|
|
||||||
mod tally_id;
|
|
||||||
mod webserver;
|
|
||||||
mod store;
|
mod store;
|
||||||
|
mod webserver;
|
||||||
|
|
||||||
const STORE_PATH: &str = "./data.json";
|
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
|
||||||
|
|
||||||
async fn run_webserver<H>(
|
static UTC_TIME: Signal<CriticalSectionRawMutex, u64> = Signal::new();
|
||||||
store: Arc<Mutex<IDStore>>,
|
static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
|
||||||
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 {
|
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
sender: activity_channel,
|
type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
};
|
|
||||||
|
|
||||||
start_webserver(store, id_channel, notifier).await?;
|
#[esp_hal_embassy::main]
|
||||||
|
async fn main(mut spawner: Spawner) {
|
||||||
|
|
||||||
Ok(())
|
let (uart_device, stack, _i2c, sqw_pin, buzzer_gpio) =
|
||||||
}
|
init::hardware::hardware_init(&mut spawner).await;
|
||||||
|
|
||||||
async fn load_or_create_store() -> Result<IDStore> {
|
wait_for_stack_up(stack).await;
|
||||||
if fs::try_exists(STORE_PATH).await? {
|
|
||||||
info!("Loading data from file");
|
info!("Starting up...");
|
||||||
IDStore::new_from_json(STORE_PATH).await
|
|
||||||
} else {
|
let chan: &'static mut TallyChannel = make_static!(PubSubChannel::new());
|
||||||
info!("No data file found. Creating empty one.");
|
|
||||||
Ok(IDStore::new())
|
//start_webserver(&mut spawner, stack);
|
||||||
|
|
||||||
|
let publisher = chan.publisher().unwrap();
|
||||||
|
|
||||||
|
/****************************** Spawning tasks ***********************************/
|
||||||
|
debug!("spawing NFC reader task...");
|
||||||
|
spawner.must_spawn(drivers::nfc_reader::rfid_reader_task(
|
||||||
|
uart_device,
|
||||||
|
publisher,
|
||||||
|
));
|
||||||
|
|
||||||
|
debug!("spawing rtc task");
|
||||||
|
spawner.must_spawn(drivers::rtc::rtc_task());
|
||||||
|
|
||||||
|
debug!("spawing feedback task..");
|
||||||
|
spawner.must_spawn(feedback::feedback_task(buzzer_gpio));
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
let mut sub = chan.subscriber().unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
debug!("everythig spwawned");
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Startup);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
|
||||||
|
info!("runnung in main loop");
|
||||||
|
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,22 @@
|
|||||||
use crate::tally_id::TallyID;
|
extern crate alloc;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
use super::TallyID;
|
||||||
|
use alloc::collections::BTreeMap;
|
||||||
|
use alloc::string::String;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct Name {
|
pub struct Name {
|
||||||
pub first: String,
|
pub first: String,
|
||||||
pub last: String,
|
pub last: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
|
||||||
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,48 +29,3 @@ impl IDMapping {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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,20 @@
|
|||||||
use anyhow::{Result, anyhow};
|
extern crate alloc;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
use crate::{store::IDMapping, tally_id::TallyID};
|
use super::Date;
|
||||||
|
use super::IDMapping;
|
||||||
|
use super::TallyID;
|
||||||
|
use alloc::collections::BTreeMap;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
/// 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 +30,42 @@ impl AttendanceDay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_day_str() -> String {
|
pub struct IDStore {
|
||||||
let now = chrono::offset::Local::now();
|
days: BTreeMap<Date, AttendanceDay>,
|
||||||
now.format("%Y-%m-%d").to_string()
|
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
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user