Compare commits

..

No commits in common. "main" and "v1.0" have entirely different histories.
main ... v1.0

54 changed files with 3116 additions and 2169 deletions

View File

@ -1,14 +0,0 @@
[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"]

BIN
3d_print/Deckel.3mf Normal file

Binary file not shown.

BIN
3d_print/Gehause.3mf Normal file

Binary file not shown.

BIN
3d_print/LWL.3mf Normal file

Binary file not shown.

Binary file not shown.

2377
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,78 +3,24 @@ name = "fw-anwesenheit"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "fw-anwesenheit"
path = "./src/main.rs"
test = false
doctest = false
bench = false
[features]
default = []
mock_pi = [] # Enable mocking of the rpi hardware
[dependencies]
esp-bootloader-esp-idf = "0.1.0"
embassy-net = { version = "0.7.0", features = [
"dhcpv4",
"medium-ethernet",
"tcp",
"udp",
] }
embedded-io = "0.6.1"
embedded-io-async = "0.6.1"
esp-alloc = "0.8.0"
esp-hal = { version = "1.0.0-beta.1", features = ["esp32c6", "unstable"] }
smoltcp = { version = "0.12.0", default-features = false, features = [
"medium-ethernet",
"multicast",
"proto-dhcpv4",
"proto-dns",
"proto-ipv4",
"socket-dns",
"socket-icmp",
"socket-raw",
"socket-tcp",
"socket-udp",
] }
# for more networking protocol support see https://crates.io/crates/edge-net
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [
"async",
"macros",
] }
critical-section = "1.2.0"
embassy-executor = { version = "0.7.0", features = ["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"] }
chrono = { version = "0.4.40", features = ["serde"] }
gpio = "0.4.1"
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
rocket = { version = "0.5.1", features = ["json"] }
tokio = { version = "1.44.2", features = ["full"] }
rust-embed = "8.7.0"
log = "0.4.27"
simplelog = "0.12.2"
rppal = { version = "0.22.1", features = ["hal"] }
smart-leds = "0.3"
ws2812-spi = "0.3"
rgb = "0.8.50"
anyhow = "1.0.98"
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 Normal file
View File

@ -0,0 +1,61 @@
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)

View File

@ -1,66 +0,0 @@
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 Normal file
View File

@ -0,0 +1,24 @@
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()

1
doc/Sequence_diagram.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

34
doc/Sequence_diagram.wsd Normal file
View File

@ -0,0 +1,34 @@
@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 Executable file
View File

@ -0,0 +1,23 @@
#!/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

BIN
pre-compiled/bootrom.bin Normal file

Binary file not shown.

BIN
pre-compiled/fullimage.bin Normal file

Binary file not shown.

550
pre-compiled/pm3 Executable file
View File

@ -0,0 +1,550 @@
#!/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 $?

4
pre-compiled/pm3-flash Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
PM3PATH=$(dirname "$0")
. "$PM3PATH/pm3"

4
pre-compiled/pm3-flash-all Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
PM3PATH=$(dirname "$0")
. "$PM3PATH/pm3"

4
pre-compiled/pm3-flash-bootrom Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
PM3PATH=$(dirname "$0")
. "$PM3PATH/pm3"

View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
PM3PATH=$(dirname "$0")
. "$PM3PATH/pm3"

BIN
pre-compiled/proxmark3 Executable file

Binary file not shown.

View File

@ -1,4 +0,0 @@
[toolchain]
channel = "nightly"
components = ["rust-src"]
targets = ["riscv32imac-unknown-none-elf"]

19
service/fwa-fail.service Normal file
View File

@ -0,0 +1,19 @@
[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

5
service/fwa.env Normal file
View File

@ -0,0 +1,5 @@
PM3_BIN=/usr/share/pm3/pm3
LOG_LEVEL=warn
HOTSPOT_IDS=578B5DF2;c1532b57
HOTSPOT_SSID=fwa
HOTSPOT_PW=a9LG2kUVrsRRVUo1

20
service/fwa.service Normal file
View File

@ -0,0 +1,20 @@
[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

View File

@ -1,3 +0,0 @@
pub mod nfc_reader;
pub mod rtc;
pub mod buzzer;

View File

View File

@ -1,9 +0,0 @@
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

View File

@ -1,28 +0,0 @@
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;
}
}

View File

@ -1,120 +0,0 @@
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)
}
}
}
*/

View File

@ -1,161 +1,181 @@
use embassy_time::{Delay, Duration, Timer};
use esp_hal::{delay, gpio::Output, peripherals};
use log::{debug, error, info};
use anyhow::Result;
use log::error;
use rgb::RGB8;
use smart_leds::colors::{GREEN, RED};
use std::time::Duration;
use tokio::{join, time::sleep};
use crate::{FEEDBACK_STATE, init};
use crate::hardware::{Buzzer, StatusLed};
#[derive(Copy, Clone, Debug)]
pub enum FeedbackState {
Ack,
Nak,
Error,
Startup,
Idle,
#[cfg(not(feature = "mock_pi"))]
use crate::{hardware::GPIOBuzzer, hardware::SpiLed};
#[cfg(feature = "mock_pi")]
use crate::hardware::{MockBuzzer, MockLed};
const LED_BLINK_DURATION: Duration = Duration::from_secs(1);
pub enum DeviceStatus {
NotReady,
Ready,
HotspotEnabled,
}
#[embassy_executor::task]
pub async fn feedback_task(buzzer: peripherals::GPIO19<'static>) {
debug!("Starting feedback task");
let mut buzzer = init::hardware::setup_buzzer(buzzer);
loop {
let feedback_state = FEEDBACK_STATE.wait().await;
match feedback_state {
FeedbackState::Ack => {
buzzer.set_high();
Timer::after(Duration::from_millis(100)).await;
buzzer.set_low();
Timer::after(Duration::from_millis(50)).await;
}
FeedbackState::Nak => {
buzzer.set_high();
Timer::after(Duration::from_millis(100)).await;
buzzer.set_low();
Timer::after(Duration::from_millis(100)).await;
buzzer.set_high();
Timer::after(Duration::from_millis(100)).await;
buzzer.set_low();
}
FeedbackState::Error => {}
FeedbackState::Startup => {
buzzer.set_high();
Timer::after(Duration::from_millis(10)).await;
buzzer.set_low();
Timer::after(Duration::from_millis(10)).await;
buzzer.set_high();
Timer::after(Duration::from_millis(10)).await;
buzzer.set_low();
Timer::after(Duration::from_millis(50)).await;
buzzer.set_high();
Timer::after(Duration::from_millis(100)).await;
buzzer.set_low();
}
FeedbackState::Idle => {
// Do nothing
}
};
debug!("Feedback state: {:?}", feedback_state);
impl DeviceStatus {
pub fn color(&self) -> RGB8 {
match self {
Self::NotReady => RGB8::new(0, 0, 0),
Self::Ready => RGB8::new(0, 50, 0),
Self::HotspotEnabled => RGB8::new(0, 0, 50),
}
}
}
pub struct Feedback<B: Buzzer, L: StatusLed> {
device_status: DeviceStatus,
buzzer: B,
led: L,
}
impl<B: Buzzer, L: StatusLed> Feedback<B, L> {
pub async fn success(&mut self) {
let buzzer_handle = Self::beep_ack(&mut self.buzzer);
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, LED_BLINK_DURATION);
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
buzzer_result.unwrap_or_else(|err| {
error!("Failed to buzz: {err}");
});
let _ = self.led_to_status();
}
pub async fn failure(&mut self) {
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
buzzer_result.unwrap_or_else(|err| {
error!("Failed to buzz: {err}");
});
let _ = self.led_to_status();
}
pub async fn activate_error_state(&mut self) -> Result<()> {
self.led.turn_on(RED)?;
Self::beep_nak(&mut self.buzzer).await?;
Ok(())
}
pub async fn startup(&mut self){
self.device_status = DeviceStatus::Ready;
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, Duration::from_secs(1));
let buzzer_handle = Self::beep_startup(&mut self.buzzer);
let (buzzer_result, led_result) = join!(buzzer_handle, led_handle);
buzzer_result.unwrap_or_else(|err| {
error!("Failed to buzz: {err}");
});
led_result.unwrap_or_else(|err| {
error!("Failed to blink led: {err}");
});
let _ = self.led_to_status();
}
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(())
}
}
// async fn beep_ack() {
// buzzer.set_high();
// buzzer.set_low();
// //Timer::after(Duration::from_millis(100)).await;
// }
#[cfg(feature = "mock_pi")]
pub type FeedbackImpl = Feedback<MockBuzzer, MockLed>;
#[cfg(not(feature = "mock_pi"))]
pub type FeedbackImpl = Feedback<GPIOBuzzer, SpiLed>;
/* pub async fn failure(&mut self) {
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
buzzer_result.unwrap_or_else(|err| { error!("Failed to buzz: {err}");
});
let _ = self.led_to_status();
impl FeedbackImpl {
pub fn new() -> Result<Self> {
#[cfg(feature = "mock_pi")]
{
Ok(Feedback {
device_status: DeviceStatus::NotReady,
buzzer: MockBuzzer {},
led: MockLed {},
})
}
#[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(())
}
*/

View File

@ -0,0 +1,37 @@
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(())
}
}

131
src/hardware/hotspot.rs Normal file
View File

@ -0,0 +1,131 @@
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(())
}
}

45
src/hardware/mod.rs Normal file
View File

@ -0,0 +1,45 @@
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()
}
}

33
src/hardware/spi_led.rs Normal file
View File

@ -0,0 +1,33 @@
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(())
}
}

View File

@ -1,3 +0,0 @@
pub mod hardware;
pub mod network;
pub mod wifi;

View File

@ -1,137 +0,0 @@
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() {
}

View File

@ -1,72 +0,0 @@
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;
}

View File

@ -1,56 +0,0 @@
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();
}
}
}

View File

@ -1 +0,0 @@
#![no_std]

25
src/logger.rs Normal file
View File

@ -0,0 +1,25 @@
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);
}

View File

@ -1,96 +1,192 @@
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
#![feature(impl_trait_in_assoc_type)]
#![allow(dead_code)]
use esp_alloc::EspHeap;
use embassy_executor::Spawner;
use embassy_net::Stack;
use embassy_sync::{
blocking_mutex::raw::{NoopRawMutex, CriticalSectionRawMutex},
pubsub::{
PubSubChannel, Publisher,
WaitResult::{Lagged, Message},
},
signal::Signal,
use anyhow::Result;
use feedback::{Feedback, FeedbackImpl};
use log::{error, info, warn};
use std::{
env::{self, args},
sync::Arc,
time::Duration,
};
use embassy_time::{Duration, Timer};
use log::{debug, info};
use static_cell::make_static;
use tally_id::TallyID;
use tokio::{
fs,
signal::unix::{SignalKind, signal},
sync::{
Mutex,
broadcast::{self, Receiver, Sender},
},
try_join,
};
use webserver::start_webserver;
use crate::{store::TallyID, webserver::start_webserver};
use crate::{hardware::{create_hotspot, Hotspot}, pm3::run_pm3, store::IDStore, webserver::{spawn_idle_watcher, ActivityNotifier}};
mod init;
mod drivers;
mod feedback;
mod store;
mod hardware;
mod pm3;
mod logger;
mod tally_id;
mod webserver;
mod store;
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
const STORE_PATH: &str = "./data.json";
static UTC_TIME: Signal<CriticalSectionRawMutex, u64> = Signal::new();
static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
async fn run_webserver<H>(
store: Arc<Mutex<IDStore>>,
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);
});
});
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
let notifier = ActivityNotifier {
sender: activity_channel,
};
#[esp_hal_embassy::main]
async fn main(mut spawner: Spawner) {
start_webserver(store, id_channel, notifier).await?;
let (uart_device, stack, _i2c, sqw_pin, buzzer_gpio) =
init::hardware::hardware_init(&mut spawner).await;
Ok(())
}
wait_for_stack_up(stack).await;
info!("Starting up...");
let chan: &'static mut TallyChannel = make_static!(PubSubChannel::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:?}"),
// }
async fn load_or_create_store() -> Result<IDStore> {
if fs::try_exists(STORE_PATH).await? {
info!("Loading data from file");
IDStore::new_from_json(STORE_PATH).await
} else {
info!("No data file found. Creating empty one.");
Ok(IDStore::new())
}
}
async fn wait_for_stack_up(stack: Stack<'static>) {
loop {
if stack.is_link_up() {
break;
}
Timer::after(Duration::from_millis(500)).await;
if stack.is_config_up() {
break;
}
Timer::after(Duration::from_millis(500)).await;
fn get_hotspot_enable_ids() -> Vec<TallyID> {
let hotspot_ids: Vec<TallyID> = env::var("HOTSPOT_IDS")
.map(|ids| ids.split(";").map(|id| TallyID(id.to_owned())).collect())
.unwrap_or_default();
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 ?
}
if id_store.lock().await.add_id(tally_id) {
info!("Added new id to current day");
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 ?
}
}
}
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(())
}

4
src/pm3/mod.rs Normal file
View File

@ -0,0 +1,4 @@
mod runner;
mod parser;
pub use runner::run_pm3;

10
src/pm3/parser.rs Normal file
View File

@ -0,0 +1,10 @@
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())
}

95
src/pm3/runner.rs Normal file
View File

@ -0,0 +1,95 @@
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");
}
}
}

View File

@ -1,22 +1,22 @@
extern crate alloc;
use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::string::String;
use crate::tally_id::TallyID;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct Name {
pub first: String,
pub last: String,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct IDMapping {
id_map: BTreeMap<TallyID, Name>,
id_map: HashMap<TallyID, Name>,
}
impl IDMapping {
pub fn new() -> Self {
IDMapping {
id_map: BTreeMap::new(),
id_map: HashMap::new(),
}
}
@ -29,3 +29,48 @@ 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));
}
}

View File

@ -1,20 +1,123 @@
extern crate alloc;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tokio::fs;
use super::Date;
use super::IDMapping;
use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::{store::IDMapping, tally_id::TallyID};
/// Represents a single day that IDs can attend
#[derive(Deserialize, Serialize)]
pub struct AttendanceDay {
date: Date,
date: String,
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(&current_day) {
return self.days.get_mut(&current_day).unwrap();
}
self.days.insert(
current_day.clone(),
AttendanceDay::new(&current_day.clone()),
);
self.days.get_mut(&current_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 {
fn new(date: Date) -> Self {
fn new(day: &str) -> Self {
Self {
date,
date: day.to_owned(),
ids: Vec::new(),
}
}
@ -30,42 +133,7 @@ impl AttendanceDay {
}
}
pub struct IDStore {
days: BTreeMap<Date, AttendanceDay>,
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(&current_day) {
return self.days.get_mut(&current_day).unwrap();
}
self.days
.insert(current_day, AttendanceDay::new(current_day));
self.days.get_mut(&current_day.clone()).unwrap()
}
fn get_day_str() -> String {
let now = chrono::offset::Local::now();
now.format("%Y-%m-%d").to_string()
}

View File

@ -1,8 +1,5 @@
mod id_mapping;
mod id_store;
mod id_mapping;
pub use id_mapping::{IDMapping, Name};
pub use id_store::IDStore;
pub type TallyID = [u8; 8];
pub type Date = u64;
pub use id_mapping::{IDMapping,Name};

45
src/tally_id.rs Normal file
View File

@ -0,0 +1,45 @@
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())
}
}

View File

@ -0,0 +1,46 @@
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
}

View File

@ -1,77 +0,0 @@
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
}
}

View File

@ -1,54 +1,6 @@
use embassy_executor::Spawner;
use embassy_net::Stack;
use embassy_time::Duration;
use picoserve::{AppBuilder, AppRouter, routing::get};
use static_cell::make_static;
mod server;
mod activity_fairing;
mod assets;
pub use activity_fairing::{ActivityNotifier,spawn_idle_watcher};
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
}

142
src/webserver/server.rs Normal file
View File

@ -0,0 +1,142 @@
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
}