Compare commits

118 Commits

Author SHA1 Message Date
Philipp_EndevourOS
fb8d98da28 added counter for all mappings in ui 2025-10-31 18:12:07 +01:00
8100748f8a change sse event listner 2025-10-28 16:08:42 +01:00
Philipp_EndevourOS
f22eb91b67 bump vite version 2025-10-27 20:36:01 +01:00
Philipp_EndevourOS
a04400a3a0 rtc will only setup with buildtime when it is larger then actual time in rtc 2025-10-27 19:51:29 +01:00
Philipp_EndevourOS
6f7561a295 deleted some imports 2025-10-27 19:50:19 +01:00
Philipp_EndevourOS
279e2f7454 chanched startup routine 2025-10-27 19:32:53 +01:00
Philipp_EndevourOS
610840311f added red led flash in inti to see a failer on init routine 2025-10-27 19:29:33 +01:00
56d2dbfa7c removed unused code 2025-10-27 16:38:02 +01:00
35f12a4c45 pass current day to IDStore constructor 2025-10-27 16:08:18 +01:00
7346fb42bd improved networks stack & DHCP server 2025-10-27 15:33:19 +01:00
c0bf8399a3 removed empty files 2025-10-27 15:31:16 +01:00
9852534dc6 improved wifi setup 2025-10-27 15:23:06 +01:00
d63e9e964d improved hardware init 2025-10-27 15:15:01 +01:00
16ea1db55f Merge branch 'feature/newlib' 2025-10-27 14:34:31 +01:00
a0ed04a560 moved LED and feedback to new lib 2025-10-27 14:25:26 +01:00
Philipp_EndevourOS
4e988e8f01 changed LED amount to 1 2025-10-27 14:16:36 +01:00
009f6cbb2e changed to new ESP libs WIP 2025-10-27 13:56:13 +01:00
Philipp_EndevourOS
967da9fc30 feedback ack turns LED off after lighting green 2025-10-25 15:04:05 +02:00
00cb7efedb unwraps now give a warning 2025-10-24 13:24:44 +02:00
ebbec7885e added missing persist mapping 2025-10-23 17:55:29 +02:00
7ecd2052d8 handle error in sse event route 2025-10-23 17:37:46 +02:00
96512c8a12 added wifi password set at compiletime 2025-10-23 16:55:16 +02:00
c3eaff03d9 implemented year select for export csv 2025-10-23 16:33:15 +02:00
4bf89626b9 changed defaults in csv generation 2025-10-23 16:32:55 +02:00
7c0c0699b5 added umlaut user to mock data 2025-10-23 16:32:38 +02:00
1ea70e4993 improved panic restart 2025-10-20 15:13:16 +02:00
770dca5b0f enabled download of exported csv 2025-10-20 14:30:22 +02:00
2e75ba2908 simplified csv generator 2025-10-20 14:30:22 +02:00
141c1aa9cb added downloadBlob function 2025-10-20 14:30:22 +02:00
Philipp_EndevourOS
4abbd844d2 added software resett after 10min when panic 2025-10-20 13:35:14 +02:00
7346b47816 added csv exporting logic 2025-10-17 13:10:23 +02:00
cd63dd3ee4 added ExportModal 2025-10-17 13:09:23 +02:00
f5d4ae1e05 added new routes & more mock data to server mock 2025-10-17 13:08:51 +02:00
bd3f6731fd renamed vars in main 2025-10-15 16:03:36 +02:00
6fdcf7679f Revert "updated panic handler"
This reverts commit c4d6ed45f1.
2025-10-15 15:50:26 +02:00
Philipp_EndevourOS
c4d6ed45f1 updated panic handler 2025-10-14 14:35:25 +02:00
Philipp_EndevourOS
41adf7353d delete old FRAM driver 2025-10-14 14:17:40 +02:00
6421074931 added routes for listing days & getting days
/api/days?from=...&to=...
/api/day?day=...
2025-10-13 16:32:28 +02:00
a34dc18381 implemented load_day & list_day_in_timespan in IDStore 2025-10-13 16:31:15 +02:00
252e63c607 fixed RFID reader outputting un-aligned IDs in buffer 2025-10-11 14:48:01 +02:00
99848f0e6d added TryFrom<[u8;12]> to TallyID 2025-10-11 14:47:29 +02:00
f46cdc4d29 implemented Display for TallyID 2025-10-11 14:31:26 +02:00
a8d64f6af5 fixed list_day for sd_card 2025-10-11 14:16:28 +02:00
8fb6bac651 added Ord to Day 2025-10-11 14:16:12 +02:00
7eb18376e1 fixed changed /api/mapping structure in frontend 2025-10-10 16:09:53 +02:00
b8bba28bda added mock server for frontedn dev 2025-10-10 16:09:17 +02:00
5c0ad18b94 re-enabled mapping loading 2025-10-10 02:02:00 +02:00
75130e2d20 implemented missing load & save mapping functions 2025-10-10 01:58:25 +02:00
6b2c56f3e5 added Deserialize to IDMapping 2025-10-10 01:57:48 +02:00
2980d34394 flatten IDMapping for serde 2025-10-10 01:50:04 +02:00
9b926f7a34 propper mapping of day to filename 2025-10-10 01:49:32 +02:00
f1b471c6d8 changed Day implementation 2025-10-10 01:12:46 +02:00
030a372949 updated cargo lock 2025-10-09 01:47:34 +02:00
211961a770 fixed RFID read id parser 2025-10-08 16:21:16 +02:00
dfe5197ab8 changed TallyID to a struct instead of a type alias 2025-10-08 16:14:10 +02:00
0f5ca88ae4 fixed many warning by removing unused imports
Removed a lot of imports — believe me, so many imports were totally
unnecessary. Nobody’s seen imports like these. Cleaned up the code, made
it faster, smarter, the best code. People are talking about it!
Tremendous work by me. Some say i am the best at it. It may be true.
2025-10-08 01:54:51 +02:00
9dd2f88cbc implemented SSE 2025-10-08 01:44:32 +02:00
aa91d69f0b explicit type of tallyid channels 2025-10-08 01:43:48 +02:00
b13ae76bc5 moved TallyID str function to right module 2025-10-08 01:42:25 +02:00
4a9ff47dcc 12 chars of hex are 6 Bytes not 12 2025-10-07 23:02:44 +02:00
92c7fec283 changed NET_STACK_SIZE & WEB_TASK_SIZE 2025-10-07 22:48:17 +02:00
082f1faba9 fixed webserver not working
- shared store in main
- multiple tasks for webserver
2025-10-07 17:29:14 +02:00
8cbdf834a1 make network stack size a constant 2025-10-07 17:28:12 +02:00
3eefcdd35a pined picoserve to current git version 2025-10-07 17:27:44 +02:00
4531ef72ae added Deserialize to id mapping Name 2025-10-07 17:27:17 +02:00
Psenfft
2078a3bab0 Update README with images and remove setup details
Removed setup instructions and added images.
2025-10-04 16:17:44 +02:00
Philipp_EndevourOS
7e59d836a1 added get date method for rtc 2025-10-04 15:46:30 +02:00
Philipp_EndevourOS
09f21403ec changed Date to u8 array 2025-10-04 15:45:56 +02:00
Philipp_EndevourOS
db7e22f45d new file will be created when sd card is empty 2025-10-01 18:58:08 +02:00
Philipp_EndevourOS
c91f290c31 removed dummy data and pass read tally id 2025-10-01 18:56:37 +02:00
becdd43738 connect RFID reader with IDStore 2025-10-01 18:00:45 +02:00
Philipp
453b653ac5 updated enclousure top 3mf 2025-10-01 17:56:30 +02:00
cc3605b75d return sdcard from hardware init 2025-10-01 17:54:54 +02:00
57ccc0cc8b fixed missing await 2025-10-01 17:51:51 +02:00
Philipp_EndevourOS
d90376121e added kicad backups to gitignore 2025-09-30 16:05:29 +02:00
2a81499f7c added pm3 write script & already used ids 2025-09-29 16:20:08 +02:00
Philipp
4ff8ff0f77 added some descriptions on pcb layout 2025-09-21 23:32:27 +02:00
Philipp_EndevourOS
781d27ae48 deleted KiCAD backup files 2025-09-21 03:50:40 +02:00
Philipp
671fb0cbdd fixed buzzer footprint 2025-09-21 03:37:09 +02:00
Philipp
99d9cf306e changed some pcb descriptions and Resistor for LED 2025-09-21 03:19:45 +02:00
Philipp_EndevourOS
b551f4521f sd card detection works (own embassy task) 2025-09-09 17:24:47 +02:00
Philipp_EndevourOS
adcbe87bd7 there are some problems to implemet the IO Mux interrupt handler 2025-09-09 16:50:14 +02:00
Philipp_EndevourOS
d96b3ed11a implemented save function for SD Card 2025-09-08 19:26:56 +02:00
Philipp_EndevourOS
dcb4b14854 added todo 2025-09-08 18:14:17 +02:00
fe90ca9aa9 implemented SD card abstraction & used it in IDStore 2025-09-08 18:11:33 +02:00
Philipp
b031a47e85 changed via size 2025-09-01 16:00:29 +02:00
Philipp
bf59b6eed3 updated pcb 2025-09-01 15:12:40 +02:00
Philipp
59d87eb199 fixed RTC footprint and some labels 2025-08-23 15:18:17 +02:00
Philipp
630fc4aaf9 pcb v1 finish 2025-08-16 00:22:37 +02:00
21480cef4f added serde for serializing in the webserver 2025-08-15 16:46:44 +02:00
Philipp
fabb14de86 circuit diagram is finish v1 2025-08-15 15:57:41 +02:00
Psenfft
6a2d448f86 Update hardware.rs
changed pinlayout to add SD DECT top GPIO0
2025-08-15 14:04:02 +02:00
Philipp_EndevourOS
fc7bd8b089 still some errors with LED an levelshifter... 2025-08-13 02:36:25 +02:00
Philipp_EndevourOS
3117c55b1c LED is working and implement in feedback 2025-08-13 02:05:49 +02:00
Philipp_EndevourOS
593d98df74 test LED Array works.. 2025-08-13 01:51:00 +02:00
Philipp_EndevourOS
fa6d1f024c test LED Array works.. 2025-08-13 01:36:13 +02:00
Philipp_EndevourOS
36dc52f464 try to control LED with SmartLED and RMT 2025-08-13 00:55:25 +02:00
Philipp_EndevourOS
6831d7776c chanched GPIO Pin configuration 2025-08-11 14:27:41 +02:00
a015d6b983 fixed embassy taks arena size 2025-08-04 18:52:03 +02:00
Philipp_EndevourOS
1ae5250449 reworked RTC without alarms and without own thread 2025-08-04 18:12:46 +02:00
Philipp_EndevourOS
2f502e908e rtc is synchronized with compile time. start pub sub approach to share time 2025-08-02 00:39:48 +02:00
Philipp_EndevourOS
5950279dc4 buzzer without pwm, and feedback with embassy Sync. Error when more then 4 tasks spawn 2025-08-01 16:42:14 +02:00
fe6540ca3d added static assets on webserver 2025-07-30 22:13:52 +02:00
Philipp_EndevourOS
161ebf9bd2 added buzzer and rtc. rtc freeze the system, bevore interrupt can be initilized.. 2025-07-30 18:50:47 +02:00
Philipp_EndevourOS
c1b54920ff added comment for Pinout 2025-07-28 22:22:07 +02:00
Philipp_EndevourOS
5a2beb1fb3 deleted todos for rtc 2025-07-28 22:03:19 +02:00
Philipp_EndevourOS
d5c20bf348 worted on rtc task, still not tested 2025-07-28 17:53:26 +02:00
Philipp_EndevourOS
49027fed99 redesigned dir structure for rust 2018 style guide. made (untested) rtc funtion 2025-07-28 17:25:39 +02:00
Philipp_EndevourOS
4dda9548d3 added some todos 2025-07-27 02:48:27 +02:00
Philipp_EndevourOS
46e207bd2a addded lvl shifter init 2025-07-27 02:24:29 +02:00
Philipp_EndevourOS
8cb118e0ee added i2c init 2025-07-27 01:30:38 +02:00
Philipp_EndevourOS
9b4df77112 added hardware files (rtc and fram) 2025-07-27 00:32:41 +02:00
23bb1126a6 implemented channel for TallyIDs 2025-07-26 19:18:14 +02:00
a97e9c8080 added embassy-sync crate 2025-07-26 19:17:54 +02:00
4b39529a65 first implementation of IDStore and IDmapping 2025-07-26 18:30:45 +02:00
c91d2f070f improved project structure and hardware init 2025-07-26 16:53:23 +02:00
2e6094ea11 added esp bootload 2025-07-24 17:37:59 +02:00
43e964b5a0 init v2 2025-07-24 17:22:50 +02:00
103 changed files with 653495 additions and 3360 deletions

18
.cargo/config.toml Normal file
View File

@@ -0,0 +1,18 @@
[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"]
[env]
WIFI_PASSWD = "hunter22"
WIFI_SSID = "fwa"

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
/target /target
/build /build
pcb/fw-anwesenheit-backups

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2695
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,24 +3,65 @@ 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" esp-hal = { version = "1.0.0-rc.1", features = ["esp32c6", "unstable"] }
regex = "1.11.1" esp-alloc = "0.9.0"
serde = { version = "1.0.219", features = ["derive"] } esp-println = { version = "0.16.0", features = ["esp32c6", "log-04"] }
serde_json = "1.0.140" esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "log-04", "smoltcp","unstable"]}
rocket = { version = "0.5.1", features = ["json"] } esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
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"
critical-section = "1.2.0"
log = { version = "0.4" }
static_cell = { version = "2.1.1", features = ["nightly"] }
heapless = { version = "0.8.0", default-features = false }
chrono = { version = "0.4.41", default-features = false }
embedded-hal = "1.0.0"
embedded-io = "0.7.1"
embedded-io-async = "0.7.0"
embassy-executor = { version = "0.9.0", features = [] }
embassy-time = { version = "0.5.0", features = [] }
embassy-futures = { version = "0.1.2", features = ["log"] }
embassy-sync = { version = "0.7.2", features = ["log"] }
embassy-net = { version = "0.7.0", features = [ "dhcpv4", "medium-ethernet", "tcp", "udp" ] }
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" ] }
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "async", "macros" ] }
edge-dhcp = { version = "0.6.0", features = ["log"] }
edge-nal = "0.5.0"
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
picoserve = { git = "https://github.com/sammhicks/picoserve.git", rev = "400df53f61137e1bb2883ec610fc191bfe551a3a", features = ["embassy", "log", "json"] }
dir-embed = "0.3.0"
serde = { version = "1.0.219", default-features = false, features = ["derive", "alloc"] }
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"]}
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
esp-hal-smartled = { git = "https://github.com/esp-rs/esp-hal-community.git", package = "esp-hal-smartled", branch = "main", features = ["esp32c6"]}
smart-leds = "0.4.0"
embedded-sdmmc = "0.8.0"
embedded-hal-bus = "0.3.0"
thiserror = { version = "2.0.17", default-features = false }
[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

View File

@@ -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)

View File

@@ -1,32 +1,6 @@
# fw-anwesenheit # fw-anwesenheit
# Setup ![PXL_20251004_141110955 MP (1)](https://github.com/user-attachments/assets/afffd664-507d-439a-a428-2477b2fe4de2)
In order to use the LED we need to enable the SPI interface on the Rpi. ![PXL_20251001_103957322 MP~2](https://github.com/user-attachments/assets/8bc182df-a93d-4923-b8d8-88b56d1aa441)
You can enable it by running `sudo raspi-config`, or by manually adding `dtparam=spi=on` to `/boot/firmware/config.txt`.
Enable PWM -> add dtoverlay=pwm to /boot/config.txt
I²C fpr RTC `sudo raspi-config` -> interface -> enable I²C
# Config
Flags:
`--error` or `-e`: Enters error state. The LED turns red and the hotspot is activated. This state gets called from systemd if the service is in a failure state.
Environment variables:
- `PM3_BIN`: Path to the pm3 binary. Seach in path if not set. Can also be set to the `pm3_mock.sh` for testing.
- `LOG_LEVEL`: Can be set to either "debug","warn","error","trace" or "info". Defaults to "warn" in production.
- `HTTP_PORT`: What port to listen on. Defaults to 80.
- `HOTSPOT_IDS`: A semicolon seperated list of ids to activate the hotspot with e.g. `578B5DF2;c1532b57`.
- `HOTSPOT_SSID`: Set the hotspot ssid. Defaults to "fwa".
- `HOTSPOT_PW`: Set the hotspot password. Default to "a9LG2kUVrsRRVUo1". Recommended to change.
Systemd:
The service is run as a systemd service. There are two service `fwa.service` and `fwa-fail.service`. They read their config
from a env file located at `/etc/fwa.env`. See example [env file](service/fwa.env).
# Building
Run `make package` to create `.deb` file. [Cross](https://github.com/cross-rs/cross) is used for building the rust code.

66
build.rs Normal file
View 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()
);
}

View File

@@ -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

View File

@@ -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

61
em4100_write.lua Normal file
View File

@@ -0,0 +1,61 @@
local used_ids = {}
local id_file_path = "used_ids.txt"
local function load_ids()
local file = io.open(id_file_path, "r")
if not file then return end
for line in file:lines() do
used_ids[line:lower()] = true
end
file:close()
end
local function save_id(id)
local file = io.open(id_file_path, "a")
if file then
file:write(id:lower() .. "\n")
file:close()
end
end
local function gen_id()
local id = ""
for i = 1, 10 do
id = id .. string.format("%x", math.random(0, 15))
end
return id
end
local function get_new_id()
local tries = 0
while tries < 10000 do
local id = gen_id()
if not used_ids[id:lower()] then
return id
end
tries = tries + 1
end
error("Could not generate a new unused ID after 10000 tries")
end
local function write_new_card()
local id = get_new_id()
local cmd = string.format("lf em 410x clone --id %s", id)
core.console(cmd)
used_ids[id:lower()] = true
save_id(id)
print("Wrote new EM4100 card with ID:", id)
end
local function write_new_card(id)
local cmd = string.format("lf em 410x clone --id %s", id)
core.console(cmd)
used_ids[id:lower()] = true
save_id(id)
print("Wrote new EM4100 card with ID:", id)
end
math.randomseed(os.time())
load_ids()
local id = get_new_id()
write_new_card(id)

4709
pcb/bom/ibom.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"ARCHIVE_NAME": "", "EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}

105757
pcb/fp-info-cache Normal file

File diff suppressed because it is too large Load Diff

27885
pcb/fw-anwesenheit.kicad_pcb Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,147 @@
{
"board": {
"active_layer": 7,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"locked_item_shadows",
"conflict_shadows",
"shapes"
],
<<<<<<< HEAD
"visible_layers": "00000000_00000000_0fffffff_fffff8aa",
=======
"visible_layers": "00000000_00000000_0fffffff_fffff8ab",
>>>>>>> 15c64e4 (updated enclousure top 3mf)
"zone_display_mode": 0
},
"git": {
"repo_password": "",
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "fw-anwesenheit.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13
],
"col_widths": [
162,
147,
91,
72,
91,
100,
91,
76,
91,
91,
91,
91,
91,
91
],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": 0
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

View File

@@ -0,0 +1,633 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.0,
"height": 1.7,
"width": 1.7
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"min_clearance": 0.5
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_near_hole": "error",
"hole_to_hole": "error",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "warning",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.2,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.2,
"min_microvia_diameter": 0.45,
"min_microvia_drill": 0.3,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.16,
"min_via_annular_width": 0.075,
"min_via_diameter": 0.45,
"solder_mask_to_copper_clearance": 0.005,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
}
],
"zones_allow_external_fillets": false
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "fw-anwesenheit.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "production/",
"pos_files": "",
"specctra_dsn": "",
"step": "fw-anwesenheit.step",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Referenz"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [
[
"ccbf1fda-befd-42da-bcb2-5d3829184012",
"Root"
]
],
"text_variables": {}
}

11815
pcb/fw-anwesenheit.kicad_sch Normal file

File diff suppressed because it is too large Load Diff

122063
pcb/fw-anwesenheit.step Normal file

File diff suppressed because it is too large Load Diff

374733
pcb/fw-anwesenheit.stl Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0")
)

View File

@@ -0,0 +1,98 @@
{
"board": {
"active_layer": 0,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"drc_exclusions",
"locked_item_shadows",
"conflict_shadows",
"shapes"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 0
},
"git": {
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "fw-anwesenheit.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [],
"col_order": [],
"col_widths": [],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": -1
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

View File

@@ -0,0 +1,413 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"rules": {},
"track_widths": [],
"via_dimensions": []
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "fw-anwesenheit.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "",
"pos_files": "",
"specctra_dsn": "",
"step": "",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Reference"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [],
"text_variables": {}
}

View File

@@ -0,0 +1,14 @@
(kicad_sch
(version 20250114)
(generator "eeschema")
(generator_version "9.0")
(uuid 35cd442a-c7c9-4bc2-bfa5-9414b343d8e4)
(paper "A4")
(lib_symbols)
(sheet_instances
(path "/"
(page "1")
)
)
(embedded_fonts no)
)

35
pcb/production/bom.csv Normal file
View File

@@ -0,0 +1,35 @@
Designator,Footprint,Quantity,Value,LCSC Part #
BT1,Battery_Panasonic_CR2032-HFN_Horizontal_CircularHoles,1,Battery_Cell,
BZ1,PinSocket_1x02_P2.54mm_Vertical,1,Buzzer,
"C1, C4",0603,2,10µF,
"C2, C3, C5",0603,3,100nF,
D2,0603,1,LED,
J1,PinHeader_1x02_P2.54mm_Vertical,1,Conn_01x02_Pin,
J2,PinHeader_1x04_P2.54mm_Vertical,1,I2C,
J3,PinHeader_1x03_P2.54mm_Vertical,1,LED,
J4,WURTH_693071020811,1,MicroSD,
R1,0603,1,150R,
"R10, R11, R12, R13, R14, R15, R9",0603,7,47k,
R2,0603,1,100R,
R3,0603,1,10K,
R4,0603,1,20k,
"R5, R6",0603,2,4k7,
"R7, R8",0603,2,NC,
RDM1,RDM6300,1,RDM6300,
"TP1, TP10, TP9",TestPoint_Pad_D1.5mm,3,TestPoint,
TP11,TestPoint_Pad_D1.5mm,1,MOSI,
TP12,TestPoint_Pad_D1.5mm,1,MISO,
TP13,TestPoint_Pad_D1.5mm,1,SPI SCL,
TP14,TestPoint_Pad_D1.5mm,1,DAT1,
TP15,TestPoint_Pad_D1.5mm,1,DAT2,
TP2,TestPoint_Pad_D1.5mm,1,+5V,
TP3,TestPoint_Pad_D1.5mm,1,GND,
TP4,TestPoint_Pad_D1.5mm,1,"3,3V",
TP5_2,TestPoint_Pad_D1.5mm,1,Din,
TP5,TestPoint_Pad_D1.5mm,1,CS,
TP6,TestPoint_Pad_D1.5mm,1,SD_DECT,
TP7,TestPoint_Pad_D1.5mm,1,UART_RX,
TP8,TestPoint_Pad_D1.5mm,1,UART_TX,
U1,XIAO-ESP32C6-SMD,1,XIAO-ESP32-S3-SMD,
U2,SOT95P280X145-5N,1,SN74AHCT1G125DBVT,SN74AHCT1G125DBVT
U3,SOIC-16W_7.5x10.3mm_P1.27mm,1,DS3231M,
1 Designator Footprint Quantity Value LCSC Part #
2 BT1 Battery_Panasonic_CR2032-HFN_Horizontal_CircularHoles 1 Battery_Cell
3 BZ1 PinSocket_1x02_P2.54mm_Vertical 1 Buzzer
4 C1, C4 0603 2 10µF
5 C2, C3, C5 0603 3 100nF
6 D2 0603 1 LED
7 J1 PinHeader_1x02_P2.54mm_Vertical 1 Conn_01x02_Pin
8 J2 PinHeader_1x04_P2.54mm_Vertical 1 I2C
9 J3 PinHeader_1x03_P2.54mm_Vertical 1 LED
10 J4 WURTH_693071020811 1 MicroSD
11 R1 0603 1 150R
12 R10, R11, R12, R13, R14, R15, R9 0603 7 47k
13 R2 0603 1 100R
14 R3 0603 1 10K
15 R4 0603 1 20k
16 R5, R6 0603 2 4k7
17 R7, R8 0603 2 NC
18 RDM1 RDM6300 1 RDM6300
19 TP1, TP10, TP9 TestPoint_Pad_D1.5mm 3 TestPoint
20 TP11 TestPoint_Pad_D1.5mm 1 MOSI
21 TP12 TestPoint_Pad_D1.5mm 1 MISO
22 TP13 TestPoint_Pad_D1.5mm 1 SPI SCL
23 TP14 TestPoint_Pad_D1.5mm 1 DAT1
24 TP15 TestPoint_Pad_D1.5mm 1 DAT2
25 TP2 TestPoint_Pad_D1.5mm 1 +5V
26 TP3 TestPoint_Pad_D1.5mm 1 GND
27 TP4 TestPoint_Pad_D1.5mm 1 3,3V
28 TP5_2 TestPoint_Pad_D1.5mm 1 Din
29 TP5 TestPoint_Pad_D1.5mm 1 CS
30 TP6 TestPoint_Pad_D1.5mm 1 SD_DECT
31 TP7 TestPoint_Pad_D1.5mm 1 UART_RX
32 TP8 TestPoint_Pad_D1.5mm 1 UART_TX
33 U1 XIAO-ESP32C6-SMD 1 XIAO-ESP32-S3-SMD
34 U2 SOT95P280X145-5N 1 SN74AHCT1G125DBVT SN74AHCT1G125DBVT
35 U3 SOIC-16W_7.5x10.3mm_P1.27mm 1 DS3231M

View File

@@ -0,0 +1,47 @@
BT1:1
BZ1:1
C1:1
C2:1
C3:1
C4:1
C5:1
D2:1
J1:1
J2:1
J3:1
J4:1
JP1:1
R1:1
R10:1
R11:1
R12:1
R13:1
R14:1
R15:1
R2:1
R3:1
R4:1
R5:1
R6:1
R7:1
R8:1
R9:1
RDM1:1
TP1:1
TP10:1
TP11:1
TP12:1
TP13:1
TP14:1
TP15:1
TP2:1
TP3:1
TP4:1
TP5:2
TP6:1
TP7:1
TP8:1
TP9:1
U1:1
U2:1
U3:1
1 BT1:1
2 BZ1:1
3 C1:1
4 C2:1
5 C3:1
6 C4:1
7 C5:1
8 D2:1
9 J1:1
10 J2:1
11 J3:1
12 J4:1
13 JP1:1
14 R1:1
15 R10:1
16 R11:1
17 R12:1
18 R13:1
19 R14:1
20 R15:1
21 R2:1
22 R3:1
23 R4:1
24 R5:1
25 R6:1
26 R7:1
27 R8:1
28 R9:1
29 RDM1:1
30 TP1:1
31 TP10:1
32 TP11:1
33 TP12:1
34 TP13:1
35 TP14:1
36 TP15:1
37 TP2:1
38 TP3:1
39 TP4:1
40 TP5:2
41 TP6:1
42 TP7:1
43 TP8:1
44 TP9:1
45 U1:1
46 U2:1
47 U3:1

Binary file not shown.

354
pcb/production/netlist.ipc Normal file
View File

@@ -0,0 +1,354 @@
P CODE 00
P UNITS CUST 0
P arrayDim N
317GND VIA MD0118PA00X+035433Y-045241X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+047750Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046550Y-043700X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046950Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+044450Y-033050X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043600Y-040300X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050950Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038800Y-048850X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+034150Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+040550Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+044550Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+040120Y-047880X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048850Y-037100X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+049500Y-036550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+036500Y-038600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+042300Y-042500X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+040900Y-042500X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048750Y-038400X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+041150Y-049250X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038800Y-049100X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+034850Y-040600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+051100Y-045600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+047150Y-036350X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048100Y-050150X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+034950Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+033051Y-044472X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043600Y-041800X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+044650Y-036950X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046340Y-045450X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038450Y-048600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+047750Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+049600Y-045500X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050787Y-045776X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+034050Y-039300X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+049350Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038950Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+042850Y-047400X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038600Y-041300X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050500Y-045950X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048100Y-049850X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050950Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+037200Y-048900X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+035750Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038600Y-041800X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+033200Y-049950X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+049950Y-038400X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050150Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+033350Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+044550Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043600Y-039800X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043600Y-041300X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+042950Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+047250Y-039150X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046340Y-044709X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+041350Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+041200Y-038450X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048400Y-049850X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+037350Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+037350Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050750Y-036850X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+032550Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+037550Y-046600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+045450Y-036150X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046950Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048420Y-050150X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038950Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043850Y-042500X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038150Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+045350Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046150Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038100Y-048600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046650Y-039000X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043750Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048550Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+045350Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043750Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050150Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+051100Y-045950X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+046150Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038800Y-048600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048050Y-039000X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043500Y-045350X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048550Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+040550Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+036550Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+049350Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050650Y-033050X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+041350Y-044650X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+042150Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038600Y-040300X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+043600Y-040800X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038100Y-048850X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038150Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+041350Y-044300X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+047250Y-046950X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+042950Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+041526Y-041339X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+050500Y-045600X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048850Y-037400X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+039750Y-031750X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+037645Y-038800X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+039750Y-050550X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038450Y-049100X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038600Y-040800X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+041640Y-047740X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038450Y-048850X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+048228Y-046885X0177Y0000R000S-2119174445
317GND VIA MD0118PA00X+038100Y-049100X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+050480Y-044960X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038820Y-050080X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+050480Y-044600X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038460Y-050080X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+051040Y-044620X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+047360Y-044720X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038160Y-049860X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+037200Y-049500X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038820Y-049640X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+034451Y-049801X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+051040Y-044980X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038820Y-049860X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+047150Y-043700X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038460Y-049640X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038460Y-049860X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+049600Y-044900X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+050780Y-044780X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038160Y-050080X0177Y0000R000S-2119174445
317+5V VIA MD0118PA00X+038160Y-049640X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+041050Y-047400X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+032874Y-036304X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+049606Y-040320X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038255Y-038800X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+040400Y-037150X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+047244Y-047638X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+035595Y-038976X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048000Y-037450X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+046457Y-040320X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+043350Y-048650X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+040450Y-048550X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038800Y-037150X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+046350Y-048650X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048150Y-044050X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+041300Y-047400X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048031Y-040320X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038450Y-047600X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048000Y-037100X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+041300Y-047150X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+046600Y-048650X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038800Y-047850X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+041050Y-047150X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048100Y-038400X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+050394Y-040354X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+043650Y-048650X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038450Y-048100X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038450Y-047850X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038800Y-048100X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+045669Y-040320X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+040350Y-038800X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+032750Y-043500X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+035595Y-038189X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048819Y-040320X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+047244Y-040320X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038800Y-047600X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038100Y-048100X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048228Y-047603X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038100Y-047850X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+036100Y-050350X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+038100Y-047600X0177Y0000R000S-2119174445
317+3.3V VIA MD0118PA00X+048150Y-045550X0177Y0000R000S-2119174445
317NET-(R3-PAD1) VIA MD0118PA00X+035433Y-042948X0177Y0000R000S-2119174445
317/SD_DECT VIA MD0118PA00X+050138Y-037264X0177Y0000R000S-2119174445
317/SD_DECT VIA MD0118PA00X+044800Y-049900X0177Y0000R000S-2119174445
317/MISO VIA MD0118PA00X+038450Y-045800X0177Y0000R000S-2119174445
317/MISO VIA MD0118PA00X+049272Y-037264X0177Y0000R000S-2119174445
317/SPI_SCL VIA MD0118PA00X+048400Y-037264X0177Y0000R000S-2119174445
317/SPI_SCL VIA MD0118PA00X+038450Y-044900X0177Y0000R000S-2119174445
317/SPI_SCL VIA MD0118PA00X+048031Y-039601X0177Y0000R000S-2119174445
317/MOSI VIA MD0118PA00X+037500Y-044550X0177Y0000R000S-2119174445
317/MOSI VIA MD0118PA00X+047539Y-037264X0177Y0000R000S-2119174445
317/SPI_CS VIA MD0118PA00X+046250Y-046000X0177Y0000R000S-2119174445
317/SPI_CS VIA MD0118PA00X+047106Y-037264X0177Y0000R000S-2119174445
317/BUZZER VIA MD0118PA00X+044450Y-046650X0177Y0000R000S-2119174445
317-GPIO16_D6_TX) VIA MD0118PA00X+036480Y-047320X0177Y0000R000S-2119174445
317-GPIO16_D6_TX) VIA MD0118PA00X+045700Y-046650X0177Y0000R000S-2119174445
317/I2C_SCL VIA MD0118PA00X+043000Y-038300X0177Y0000R000S-2119174445
317/I2C_SCL VIA MD0118PA00X+045600Y-046100X0177Y0000R000S-2119174445
317/I2C_SCL VIA MD0118PA00X+043225Y-043575X0177Y0000R000S-2119174445
317/I2C_SCL VIA MD0118PA00X+034680Y-046240X0177Y0000R000S-2119174445
317/I2C_SDA VIA MD0118PA00X+042900Y-044100X0177Y0000R000S-2119174445
317/I2C_SDA VIA MD0118PA00X+042900Y-038800X0177Y0000R000S-2119174445
317/I2C_SDA VIA MD0118PA00X+044812Y-045842X0177Y0000R000S-2119174445
317/I2C_SDA VIA MD0118PA00X+033300Y-046050X0177Y0000R000S-2119174445
317/DAT2 VIA MD0118PA00X+046673Y-037264X0177Y0000R000S-2119174445
317/DAT1 VIA MD0118PA00X+049705Y-037264X0177Y0000R000S-2119174445
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-049800X0177Y0000R000S-2119174445
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050000X0177Y0000R000S-2119174445
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050200X0177Y0000R000S-2119174445
317ET-(U3-~{RST}) VIA MD0118PA00X+033850Y-038500X0177Y0000R000S-2119174445
317ET-(U3-~{RST}) VIA MD0118PA00X+037750Y-039800X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+042450Y-045000X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+045800Y-049600X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+042250Y-045000X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044750X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044450X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+046050Y-049350X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044450X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+045850Y-049400X0177Y0000R000S-2119174445
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044750X0177Y0000R000S-2119174445
327GND C5 -1 A01X+048730Y-038386X0354Y0374R180S2
327+3.3V C5 -2 A01X+048120Y-038386X0354Y0374R180S2
327/LED_DIN J3 -1 A01X+050700Y-043800X0984Y0669R000S2
327+5V J3 -2 A01X+050700Y-044800X0984Y0669R000S2
327GND J3 -3 A01X+050700Y-045800X0984Y0669R000S2
327/SD_DECT U1 -1 A01X+044812Y-049842X1083Y0787R180S2
327/LED_DRIVER U1 -2 A01X+044812Y-048842X1083Y0787R180S2
327/SPI_CS U1 -3 A01X+044812Y-047842X1083Y0787R180S2
327/BUZZER U1 -4 A01X+044812Y-046842X1083Y0787R180S2
327/I2C_SDA U1 -5 A01X+044812Y-045842X1083Y0787R180S2
327/I2C_SCL U1 -6 A01X+044812Y-044842X1083Y0787R180S2
327-GPIO16_D6_TX) U1 -7 A01X+044812Y-043842X1083Y0787R180S2
327/UART_RX U1 -8 A01X+038447Y-043842X1083Y0787R180S2
327/SPI_SCL U1 -9 A01X+038447Y-044842X1083Y0787R180S2
327/MISO U1 -10 A01X+038447Y-045842X1083Y0787R180S2
327/MOSI U1 -11 A01X+038447Y-046842X1083Y0787R180S2
327+3.3V U1 -12 A01X+038447Y-047842X1083Y0787R180S2
327GND U1 -13 A01X+038447Y-048842X1083Y0787R180S2
327+5V U1 -14 A01X+038447Y-049842X1083Y0787R180S2
327NET-(JP1-A) U1 -15 A01X+042342Y-044664X0984Y0433R270S2
327GND U1 -16 A01X+041342Y-044664X0984Y0433R270S2
327U1-MTDI-PAD17) U1 -17 A01X+042142Y-050244X0669Y0000R180S2
327U1-MTDO-PAD18) U1 -18 A01X+041142Y-050244X0669Y0000R180S2
327CHIP_EN-PAD19) U1 -19 A01X+042142Y-049244X0669Y0000R180S2
327GND U1 -20 A01X+041142Y-049244X0669Y0000R180S2
327U1-MTMS-PAD21) U1 -21 A01X+042142Y-048244X0669Y0000R180S2
327U1-MTCK-PAD22) U1 -22 A01X+041142Y-048244X0669Y0000R180S2
327U1-BOOT-PAD23) U1 -23 A01X+042142Y-047244X0669Y0000R180S2
327+3.3V U1 -24 A01X+041178Y-047266X0669Y0000R180S2
327GND TP3 -1 A01X+032677Y-050197X0591Y0000R000S2
327NET-(D2-K) D2 -1 A01X+032874Y-036924X0344Y0374R270S2
327+3.3V D2 -2 A01X+032874Y-036304X0344Y0374R270S2
327+3.3V R14 -1 A01X+047244Y-040320X0384Y0374R270S2
327/MOSI R14 -2 A01X+047244Y-039601X0384Y0374R270S2
327+3.3V R13 -1 A01X+048031Y-040320X0384Y0374R270S2
327/SPI_SCL R13 -2 A01X+048031Y-039601X0384Y0374R270S2
327NET-(R3-PAD1) R3 -1 A01X+035433Y-042948X0384Y0374R090S2
327/UART_RX R3 -2 A01X+035433Y-043666X0384Y0374R090S2
327GND C2 -1 A01X+037645Y-038800X0354Y0374R000S2
327+3.3V C2 -2 A01X+038255Y-038800X0354Y0374R000S2
327NT}{SLASH}SQW) TP1 -1 A01X+033071Y-040748X0591Y0000R000S2
327NET-(U3-32KHZ) U3 -1 A01X+039319Y-038300X0807Y0236R000S2
327+3.3V U3 -2 A01X+039319Y-038800X0807Y0236R000S2
327NT}{SLASH}SQW) U3 -3 A01X+039319Y-039300X0807Y0236R000S2
327ET-(U3-~{RST}) U3 -4 A01X+039319Y-039800X0807Y0236R000S2
327GND U3 -5 A01X+039319Y-040300X0807Y0236R000S2
327GND U3 -6 A01X+039319Y-040800X0807Y0236R000S2
327GND U3 -7 A01X+039319Y-041300X0807Y0236R000S2
327GND U3 -8 A01X+039319Y-041800X0807Y0236R000S2
327GND U3 -9 A01X+042981Y-041800X0807Y0236R000S2
327GND U3 -10 A01X+042981Y-041300X0807Y0236R000S2
327GND U3 -11 A01X+042981Y-040800X0807Y0236R000S2
327GND U3 -12 A01X+042981Y-040300X0807Y0236R000S2
327GND U3 -13 A01X+042981Y-039800X0807Y0236R000S2
327/BATTERY_CELL U3 -14 A01X+042981Y-039300X0807Y0236R000S2
327/I2C_SDA U3 -15 A01X+042981Y-038800X0807Y0236R000S2
327/I2C_SCL U3 -16 A01X+042981Y-038300X0807Y0236R000S2
317/BUZZER BZ1 -1 D0394PA00X+035400Y-046806X0669Y0669R000S0
317GND BZ1 -2 D0394PA00X+035400Y-047806X0669Y0000R000S0
327+3.3V R15 -1 A01X+045669Y-040320X0384Y0374R270S2
327/DAT2 R15 -2 A01X+045669Y-039601X0384Y0374R270S2
327-GPIO16_D6_TX) TP8 -1 A01X+033927Y-046604X0591Y0000R000S2
327NET-(JP1-A) JP1 -1 A01X+046450Y-050000X0118Y0118R000S2
327NET-(J1-PIN_1) JP1 -2 A01X+047021Y-050000X0118Y0118R000S2
317(RDM1-PADANT1) RDM1 -ANT1 D0551PA00X+035236Y-023433X0827Y0000R090S0
317(RDM1-PADANT2) RDM1 -ANT2 D0551PA00X+035236Y-024433X0827Y0827R090S0
317D-(RDM1-PADD0) RDM1 -D0 D0551PA00X+041236Y-034433X0827Y0000R090S0
317GND RDM1 -GND D0551PA00X+041236Y-033433X0827Y0000R090S0
317GND RDM1 -GND1 D0551PA00X+035236Y-034433X0827Y0000R090S0
317NET-(R1-PAD1) RDM1 -LED D0551PA00X+035236Y-036433X0827Y0827R090S0
317D-(RDM1-PADRX) RDM1 -RX D0551PA00X+041236Y-035433X0827Y0000R090S0
317NET-(R3-PAD1) RDM1 -TX D0551PA00X+041236Y-036433X0827Y0827R090S0
317+5V RDM1 -VCC D0551PA00X+041236Y-032433X0827Y0000R090S0
317+5V RDM1 -VCC1 D0551PA00X+035236Y-035433X0827Y0000R090S0
327+3.3V R7 -1 A01X+035595Y-038189X0384Y0374R180S2
327NET-(U3-32KHZ) R7 -2 A01X+034877Y-038189X0384Y0374R180S2
327+3.3V R8 -1 A01X+035595Y-038976X0384Y0374R180S2
327NT}{SLASH}SQW) R8 -2 A01X+034877Y-038976X0384Y0374R180S2
327GND U2 -1 A01X+046340Y-044709X0500Y0220R000S2
327/LED_DRIVER U2 -2 A01X+046340Y-045079X0500Y0220R000S2
327GND U2 -3 A01X+046340Y-045449X0500Y0220R000S2
327NET-(U2-Y) U2 -4 A01X+047360Y-045449X0500Y0220R000S2
327+5V U2 -5 A01X+047360Y-044709X0500Y0220R000S2
327GND C3 -1 A01X+046545Y-043701X0354Y0374R000S2
327+5V C3 -2 A01X+047156Y-043701X0354Y0374R000S2
327NET-(R1-PAD1) R1 -1 A01X+033858Y-036255X0384Y0374R090S2
327NET-(D2-K) R1 -2 A01X+033858Y-036973X0384Y0374R090S2
327NET-(U2-Y) R2 -1 A01X+048460Y-043701X0384Y0374R000S2
327/LED_DIN R2 -2 A01X+049178Y-043701X0384Y0374R000S2
327+3.3V R12 -1 A01X+048819Y-040320X0384Y0374R270S2
327/MISO R12 -2 A01X+048819Y-039601X0384Y0374R270S2
327NET-(U3-32KHZ) TP10 -1 A01X+033071Y-039567X0591Y0000R000S2
327+5V TP2 -1 A01X+034055Y-050197X0591Y0000R000S2
327+3.3V TP4 -1 A01X+035433Y-050197X0591Y0000R000S2
327+3.3V R11 -1 A01X+050394Y-040354X0384Y0374R270S2
327/SD_DECT R11 -2 A01X+050394Y-039636X0384Y0374R270S2
327/LED_DIN TP5 -1 A01X+049016Y-042717X0591Y0000R000S2
327+3.3V R9 -1 A01X+046457Y-040320X0384Y0374R270S2
327/SPI_CS R9 -2 A01X+046457Y-039601X0384Y0374R270S2
327GND C4 -1 A01X+049600Y-045505X0354Y0374R270S2
327+5V C4 -2 A01X+049600Y-044895X0354Y0374R270S2
327+3.3V R6 -1 A01X+048228Y-047603X0384Y0374R270S2
327GND R6 -2 A01X+048228Y-046885X0384Y0374R270S2
327/UART_RX R4 -1 A01X+035433Y-044523X0384Y0374R090S2
327GND R4 -2 A01X+035433Y-045241X0384Y0374R090S2
327ET-(U3-~{RST}) TP9 -1 A01X+033071Y-038386X0591Y0000R270S2
367N/C J4 D0394UA00X+049508Y-032913X0394Y0000R180S0
367N/C J4 D0394UA00X+046358Y-032913X0394Y0000R180S0
327/DAT2 J4 -1 A01X+046673Y-037264X0276Y0748R180S2
327/SPI_CS J4 -2 A01X+047106Y-037264X0276Y0748R180S2
327/MOSI J4 -3 A01X+047539Y-037264X0276Y0748R180S2
327+3.3V J4 -4 A01X+047972Y-037264X0276Y0748R180S2
327/SPI_SCL J4 -5 A01X+048406Y-037264X0276Y0748R180S2
327GND J4 -6 A01X+048839Y-037264X0276Y0748R180S2
327/MISO J4 -7 A01X+049272Y-037264X0276Y0748R180S2
327/DAT1 J4 -8 A01X+049705Y-037264X0276Y0748R180S2
327/SD_DECT J4 -9 A01X+050138Y-037264X0276Y0748R180S2
327GND J4 -10 A01X+050748Y-036850X0669Y0709R180S2
327GND J4 -11 A01X+044646Y-036850X0669Y0709R180S2
327GND J4 -12 A01X+050650Y-033071X0551Y1024R180S2
327GND J4 -13 A01X+044469Y-033071X0551Y1024R180S2
327+3.3V R10 -1 A01X+049606Y-040320X0384Y0374R270S2
327/DAT1 R10 -2 A01X+049606Y-039601X0384Y0374R270S2
327/UART_RX TP7 -1 A01X+033927Y-045422X0591Y0000R000S2
327+3.3V R5 -1 A01X+047244Y-047609X0384Y0374R270S2
327GND R5 -2 A01X+047244Y-046891X0384Y0374R270S2
327GND C1 -1 A01X+037205Y-048907X0354Y0374R090S2
327+5V C1 -2 A01X+037205Y-049518X0354Y0374R090S2
327/DAT2 TP15 -1 A06X+042323Y-035433X0591Y0000R000S1
327/SPI_CS TP5 -1 A06X+043701Y-035433X0591Y0000R180S1
327/SD_DECT TP6 -1 A06X+050591Y-035433X0591Y0000R180S1
317/BATTERY_CELL BT1 -1 D0335PA00X+044650Y-040700X0630Y0787R180S0
317GND BT1 -2 D0335PA00X+036579Y-040700X0630Y0787R180S0
327/DAT1 TP14 -1 A06X+049213Y-035433X0591Y0000R180S1
327NET-(J1-PIN_1) J1 -1 A06X+047248Y-050000X0669Y0669R090S1
327GND J1 -2 A06X+048248Y-050000X0669Y0000R090S1
327/MOSI TP11 -1 A06X+045079Y-035433X0591Y0000R180S1
327+3.3V J2 -1 A06X+032750Y-043500X0669Y0669R180S1
327GND J2 -2 A06X+032750Y-044500X0669Y0000R180S1
327/I2C_SDA J2 -3 A06X+032750Y-045500X0669Y0000R180S1
327/I2C_SCL J2 -4 A06X+032750Y-046500X0669Y0000R180S1
327/MISO TP12 -1 A06X+047835Y-035433X0591Y0000R180S1
327/SPI_SCL TP13 -1 A06X+046457Y-035433X0591Y0000R180S1
999

View File

@@ -0,0 +1,29 @@
Designator,Mid X,Mid Y,Rotation,Layer
BT1,103.161,-103.378,0.0,bottom
BZ1,89.916,-120.158,0.0,top
C1,94.5,-125.0,270.0,top
C2,96.393,-98.552,0.0,top
C3,119.0,-111.0,0.0,top
C4,125.984,-114.808,90.0,top
C5,123.0,-97.5,180.0,top
D2,83.5,-93.0,90.0,top
J1,120.01,-127.0,270.0,bottom
J2,83.185,-110.49,0.0,bottom
J4,120.8,-83.6,180.0,top
R1,86.0,-93.0,270.0,top
R10,126.0,-101.5,90.0,top
R11,128.0,-101.5875,90.0,top
R12,124.0,-101.5,90.0,top
R13,122.0,-101.5,90.0,top
R14,120.0,-101.5,90.0,top
R15,116.0,-101.5,90.0,top
R2,124.0,-111.0,0.0,top
R3,90.0,-110.0,270.0,top
R4,90.0,-114.0,270.0,top
R5,120.0,-120.015,90.0,top
R6,122.5,-120.0,90.0,top
R9,118.0,-101.5,90.0,top
RDM1,97.12,-76.03,270.0,top
U1,105.7391,-119.41425,180.0,top
U2,119.0,-114.5,0.0,top
U3,104.521,-101.727,270.0,top
1 Designator Mid X Mid Y Rotation Layer
2 BT1 103.161 -103.378 0.0 bottom
3 BZ1 89.916 -120.158 0.0 top
4 C1 94.5 -125.0 270.0 top
5 C2 96.393 -98.552 0.0 top
6 C3 119.0 -111.0 0.0 top
7 C4 125.984 -114.808 90.0 top
8 C5 123.0 -97.5 180.0 top
9 D2 83.5 -93.0 90.0 top
10 J1 120.01 -127.0 270.0 bottom
11 J2 83.185 -110.49 0.0 bottom
12 J4 120.8 -83.6 180.0 top
13 R1 86.0 -93.0 270.0 top
14 R10 126.0 -101.5 90.0 top
15 R11 128.0 -101.5875 90.0 top
16 R12 124.0 -101.5 90.0 top
17 R13 122.0 -101.5 90.0 top
18 R14 120.0 -101.5 90.0 top
19 R15 116.0 -101.5 90.0 top
20 R2 124.0 -111.0 0.0 top
21 R3 90.0 -110.0 270.0 top
22 R4 90.0 -114.0 270.0 top
23 R5 120.0 -120.015 90.0 top
24 R6 122.5 -120.0 90.0 top
25 R9 118.0 -101.5 90.0 top
26 RDM1 97.12 -76.03 270.0 top
27 U1 105.7391 -119.41425 180.0 top
28 U2 119.0 -114.5 0.0 top
29 U3 104.521 -101.727 270.0 top

View File

@@ -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.

View File

@@ -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 $?

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

4
rust-toolchain.toml Normal file
View File

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

View File

@@ -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

View File

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

View File

@@ -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

2
src/drivers.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod nfc_reader;
pub mod rtc;

68
src/drivers/nfc_reader.rs Normal file
View File

@@ -0,0 +1,68 @@
use embassy_time::{Duration, Timer};
use esp_hal::{Async, uart::Uart};
use log::{debug, info, warn};
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}");
match extract_id(&uart_buffer) {
Some(read) => {
chan.publish(read.try_into().unwrap()).await;
}
None => {
warn!("Invalid read from the RFID reader");
}
};
}
Err(e) => {
log::error!("Error reading from UART: {e}");
}
}
Timer::after(Duration::from_millis(200)).await;
}
}
/// Scans the UART output and retuns the first propper read ID
/// This ensures that only valid ID are parsed
///
/// A valid read looks like this:
/// The first byte is always 0x02 (Start of text)
/// Followed by 12 Bytes of chars
/// Ended by 0x03 (End of text)
pub fn extract_id(buffer: &[u8]) -> Option<[u8; 12]> {
const STX: u8 = 0x02; // Start of Text ASCII char
const ETX: u8 = 0x03; // End of Text ASCII char
const ID_LENGTH: usize = 12;
const MINIMUM_SEQUENCE: usize = ID_LENGTH + 2; // STX + 12 bytes + ETX
if buffer.len() < MINIMUM_SEQUENCE {
return None;
}
for window_start in 0..=buffer.len() - MINIMUM_SEQUENCE {
if buffer[window_start] == STX {
let id_end = window_start + ID_LENGTH + 1;
if buffer[id_end] == ETX {
let mut id = [0u8; ID_LENGTH];
id.copy_from_slice(&buffer[window_start + 1..id_end]);
return Some(id);
}
}
}
None
}

87
src/drivers/rtc.rs Normal file
View File

@@ -0,0 +1,87 @@
use chrono::{TimeZone, Utc};
use ds3231::{
Config, DS3231, InterruptControl, Oscillator, SquareWaveFrequency, TimeRepresentation,
};
use esp_hal::{
Async,
i2c::{self, master::I2c},
};
use log::{debug, error, info};
use crate::{FEEDBACK_STATE, drivers, feedback};
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
const RTC_ADDRESS: u8 = 0x68;
pub struct RTCClock {
dev: DS3231<I2c<'static, Async>>,
}
impl RTCClock {
pub async fn new(i2c: i2c::master::I2c<'static, Async>) -> Self {
debug!("configuring rtc...");
let rtc = drivers::rtc::rtc_config(i2c).await;
debug!("rtc up");
RTCClock { dev: rtc }
}
pub async fn get_time(&mut self) -> u64 {
match self.dev.datetime().await {
Ok(datetime) => datetime.and_utc().timestamp() as u64,
Err(e) => {
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
error!("Failed to read RTC datetime: {:?}", e);
0
}
}
}
}
pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>> {
let mut rtc: DS3231<I2c<'static, Async>> = DS3231::new(i2c, RTC_ADDRESS);
let naive_dt = Utc
.timestamp_opt(BUILD_UNIX_TIME as i64, 0)
.single()
.unwrap()
.naive_utc();
let rtc_config = Config {
time_representation: TimeRepresentation::TwentyFourHour,
square_wave_frequency: SquareWaveFrequency::Hz1,
interrupt_control: InterruptControl::Interrupt, // Enable interrupt mode
battery_backed_square_wave: false,
oscillator_enable: Oscillator::Disabled,
};
match rtc.configure(&rtc_config).await {
Ok(_) => info!("DS3231 configured successfully"),
Err(e) => {
error!("Failed to configure DS3231: {:?}", e);
error!("DS3231 configuration failed");
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
}
}
if rtc.datetime().await.unwrap() < naive_dt {
rtc.set_datetime(&naive_dt).await.unwrap_or_else(|e| {
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
error!("Failed to set RTC datetime: {:?}", e);
});
info!("RTC datetime set to: {}", naive_dt);
}
match rtc.status().await {
Ok(mut status) => {
status.set_alarm1_flag(false);
status.set_alarm2_flag(false);
match rtc.set_status(status).await {
Ok(_) => info!("Alarm flags cleared"),
Err(e) => info!("Failed to clear alarm flags: {:?}", e),
}
}
Err(e) => info!("Failed to read status: {:?}", e),
}
rtc
}

View File

@@ -1,181 +1,135 @@
use anyhow::Result; use embassy_time::{Duration, Timer};
use log::error; use esp_hal::gpio::Output;
use rgb::RGB8; use esp_hal_smartled::SmartLedsAdapterAsync;
use smart_leds::colors::{GREEN, RED}; use log::debug;
use std::time::Duration; use smart_leds::SmartLedsWriteAsync;
use tokio::{join, time::sleep}; use smart_leds::colors::{BLACK, GREEN, RED, YELLOW};
use smart_leds::{brightness, colors::BLUE};
use crate::hardware::{Buzzer, StatusLed}; use crate::init::hardware;
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")] Nack,
use crate::hardware::{MockBuzzer, MockLed}; Error,
Startup,
const LED_BLINK_DURATION: Duration = Duration::from_secs(1); WIFI,
Idle,
pub enum DeviceStatus {
NotReady,
Ready,
HotspotEnabled,
} }
impl DeviceStatus { const LED_LEVEL: u8 = 255;
pub fn color(&self) -> RGB8 {
match self { //TODO ERROR STATE: 1 Blink = unknows error, 3 Blink = no sd card
Self::NotReady => RGB8::new(0, 0, 0),
Self::Ready => RGB8::new(0, 50, 0), #[embassy_executor::task]
Self::HotspotEnabled => RGB8::new(0, 0, 50), pub async fn feedback_task(
} mut led: SmartLedsAdapterAsync<'static, { hardware::LED_BUFFER_SIZE }>,
} mut buzzer: Output<'static>,
} ) {
pub struct Feedback<B: Buzzer, L: StatusLed> { debug!("Starting feedback task");
device_status: DeviceStatus, loop {
buzzer: B, let feedback_state = FEEDBACK_STATE.wait().await;
led: L, match feedback_state {
} FeedbackState::Ack => {
led.write(brightness(
impl<B: Buzzer, L: StatusLed> Feedback<B, L> { [GREEN; init::hardware::NUM_LEDS].into_iter(),
pub async fn success(&mut self) { LED_LEVEL,
let buzzer_handle = Self::beep_ack(&mut self.buzzer); ))
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, LED_BLINK_DURATION); .await
let (buzzer_result, _) = join!(buzzer_handle, led_handle); .unwrap();
buzzer.set_high();
buzzer_result.unwrap_or_else(|err| { Timer::after(Duration::from_millis(100)).await;
error!("Failed to buzz: {err}"); buzzer.set_low();
}); Timer::after(Duration::from_millis(50)).await;
led.write(brightness(
let _ = self.led_to_status(); [BLACK; init::hardware::NUM_LEDS].into_iter(),
} LED_LEVEL,
))
pub async fn failure(&mut self) { .await
let buzzer_handle = Self::beep_nak(&mut self.buzzer); .unwrap();
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION); }
FeedbackState::Nack => {
let (buzzer_result, _) = join!(buzzer_handle, led_handle); led.write(brightness(
[YELLOW; init::hardware::NUM_LEDS].into_iter(),
buzzer_result.unwrap_or_else(|err| { LED_LEVEL,
error!("Failed to buzz: {err}"); ))
}); .await
.unwrap();
let _ = self.led_to_status(); buzzer.set_high();
} Timer::after(Duration::from_millis(100)).await;
buzzer.set_low();
pub async fn activate_error_state(&mut self) -> Result<()> { Timer::after(Duration::from_millis(100)).await;
self.led.turn_on(RED)?; buzzer.set_high();
Self::beep_nak(&mut self.buzzer).await?; Timer::after(Duration::from_millis(100)).await;
Ok(()) buzzer.set_low();
} led.write(brightness(
[BLACK; init::hardware::NUM_LEDS].into_iter(),
pub async fn startup(&mut self){ LED_LEVEL,
self.device_status = DeviceStatus::Ready; ))
.await
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, Duration::from_secs(1)); .unwrap();
let buzzer_handle = Self::beep_startup(&mut self.buzzer); }
FeedbackState::Error => {
let (buzzer_result, led_result) = join!(buzzer_handle, led_handle); led.write(brightness(
[RED; init::hardware::NUM_LEDS].into_iter(),
buzzer_result.unwrap_or_else(|err| { LED_LEVEL,
error!("Failed to buzz: {err}"); ))
}); .await
.unwrap();
led_result.unwrap_or_else(|err| { buzzer.set_high();
error!("Failed to blink led: {err}"); Timer::after(Duration::from_millis(500)).await;
}); buzzer.set_low();
Timer::after(Duration::from_millis(500)).await;
let _ = self.led_to_status(); buzzer.set_high();
} Timer::after(Duration::from_millis(500)).await;
buzzer.set_low();
pub fn set_device_status(&mut self, status: DeviceStatus){ }
self.device_status = status; FeedbackState::Startup => {
let _ = self.led_to_status(); led.write(brightness(
} [GREEN; init::hardware::NUM_LEDS].into_iter(),
LED_LEVEL,
fn led_to_status(&mut self) -> Result<()> { ))
self.led.turn_on(self.device_status.color()) .await
} .unwrap();
buzzer.set_high();
async fn flash_led_for_duration(led: &mut L, color: RGB8, duration: Duration) -> Result<()> { Timer::after(Duration::from_millis(10)).await;
led.turn_on(color)?; buzzer.set_low();
Timer::after(Duration::from_millis(10)).await;
sleep(duration).await; buzzer.set_high();
Timer::after(Duration::from_millis(10)).await;
led.turn_off()?; buzzer.set_low();
Timer::after(Duration::from_millis(50)).await;
Ok(()) buzzer.set_high();
} Timer::after(Duration::from_millis(100)).await;
buzzer.set_low();
async fn beep_ack(buzzer: &mut B) -> Result<()> {
buzzer Timer::after(Duration::from_secs(2)).await;
.modulated_tone(1200.0, Duration::from_millis(100)) led.write(brightness(
.await?; [BLACK; init::hardware::NUM_LEDS].into_iter(),
sleep(Duration::from_millis(10)).await; LED_LEVEL,
buzzer ))
.modulated_tone(2000.0, Duration::from_millis(50)) .await
.await?; .unwrap();
Ok(()) }
} FeedbackState::WIFI => {
led.write(brightness(
async fn beep_nak(buzzer: &mut B) -> Result<()> { [BLUE; init::hardware::NUM_LEDS].into_iter(),
buzzer LED_LEVEL,
.modulated_tone(600.0, Duration::from_millis(150)) ))
.await?; .await
sleep(Duration::from_millis(100)).await; .unwrap();
buzzer }
.modulated_tone(600.0, Duration::from_millis(150)) FeedbackState::Idle => {
.await?; led.write(brightness(
Ok(()) [BLACK; init::hardware::NUM_LEDS].into_iter(),
} LED_LEVEL,
))
async fn beep_startup(buzzer: &mut B) -> Result<()> { .await
buzzer .unwrap();
.modulated_tone(523.0, Duration::from_millis(150)) }
.await?; };
buzzer debug!("Feedback state: {:?}", feedback_state);
.modulated_tone(659.0, Duration::from_millis(150))
.await?;
buzzer
.modulated_tone(784.0, Duration::from_millis(150))
.await?;
buzzer
.modulated_tone(1046.0, Duration::from_millis(200))
.await?;
sleep(Duration::from_millis(100)).await;
buzzer
.modulated_tone(784.0, Duration::from_millis(100))
.await?;
buzzer
.modulated_tone(880.0, Duration::from_millis(200))
.await?;
Ok(())
}
}
#[cfg(feature = "mock_pi")]
pub type FeedbackImpl = Feedback<MockBuzzer, MockLed>;
#[cfg(not(feature = "mock_pi"))]
pub type FeedbackImpl = Feedback<GPIOBuzzer, SpiLed>;
impl FeedbackImpl {
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()?,
})
}
} }
} }

View File

@@ -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(())
}
}

View File

@@ -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(())
}
}

View File

@@ -1,44 +0,0 @@
use anyhow::Result;
use log::debug;
use std::time::Duration;
use tokio::time::sleep;
use crate::hardware::{Buzzer, Hotspot, StatusLed};
pub struct MockBuzzer {}
impl Buzzer for MockBuzzer {
async fn modulated_tone(&mut self, frequency_hz: f64, duration: Duration) -> Result<()> {
debug!("MockBuzzer: modulte tone: {frequency_hz} Hz");
sleep(duration).await;
Ok(())
}
}
pub struct MockLed {}
impl StatusLed for MockLed {
fn turn_off(&mut self) -> Result<()> {
debug!("Turn mock LED off");
Ok(())
}
fn turn_on(&mut self, color: rgb::RGB8) -> Result<()> {
debug!("Turn mock LED on to: {color}");
Ok(())
}
}
pub struct MockHotspot {}
impl Hotspot for MockHotspot {
async fn enable_hotspot(&self) -> Result<()> {
debug!("Mockhotspot: Enable hotspot");
Ok(())
}
async fn disable_hotspot(&self) -> Result<()> {
debug!("Mockhotspot: Disable hotspot");
Ok(())
}
}

View File

@@ -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()
}
}

View File

@@ -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(())
}
}

4
src/init.rs Normal file
View File

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

220
src/init/hardware.rs Normal file
View File

@@ -0,0 +1,220 @@
use embassy_executor::Spawner;
use embassy_net::Stack;
use embassy_time::{Duration, Timer};
use esp_hal::Blocking;
use esp_hal::delay::Delay;
use esp_hal::gpio::AnyPin;
use esp_hal::i2c::master::Config;
use esp_hal::peripherals::{
GPIO1, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, GPIO22, GPIO23, I2C0, RMT, SPI2, UART1,
};
use esp_hal::rmt::Rmt;
use esp_hal::spi::master::{Config as Spi_config, Spi};
use esp_hal::system::software_reset;
use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::{
Async,
clock::CpuClock,
gpio::{Output, OutputConfig},
i2c::master::I2c,
uart::Uart,
};
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
use esp_println::logger::init_logger;
use log::{debug, error};
use smart_leds::SmartLedsWriteAsync;
use smart_leds::brightness;
use smart_leds::colors::RED;
use thiserror::Error;
use crate::init::network;
use crate::init::sd_card::{SDCardPersistence, setup_sdcard};
use crate::init::wifi;
/*************************************************
* GPIO Pinout Xiao Esp32c6
*
* D0 -> GPIO0 -> SD DECT
* D1 -> GPIO1 -> Level Shifter A0 -> LED
* D2 -> GPIO2 -> SPI/CS
* D3 -> GPIO21 -> Buzzer
* D4 -> GPIO22 -> I2C/SDA
* D5 -> GPIO23 -> I2C/SCL
* D6 -> GPIO16 -> UART/TX
* D7 -> GPIO17 -> UART/RX -> Level Shifter A1 -> NFC Reader
* D8 -> GPIO19 -> SPI/SCLK
* D9 -> GPIO20 -> SPI/MISO
* D10 -> GPIO18 -> SPI/MOSI
*
*************************************************/
pub const NUM_LEDS: usize = 1;
pub const LED_BUFFER_SIZE: usize = buffer_size_async(NUM_LEDS);
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
let delay = Delay::new();
error!("PANIC: {info}");
delay.delay(esp_hal::time::Duration::from_secs(30));
software_reset()
}
esp_bootloader_esp_idf::esp_app_desc!();
#[derive(Error, Debug)]
pub enum HardwareInitError {
#[error("Failed to etup UART")]
Uart(#[from] esp_hal::uart::ConfigError),
#[error("Failed to setup I2C")]
I2C(#[from] esp_hal::i2c::master::ConfigError),
#[error("Failed to setup SPI")]
Spi(#[from] esp_hal::spi::master::ConfigError),
#[error("Failed to setuo LED")]
Led(#[from] esp_hal::rmt::Error),
#[error("Failed to setup wifi")]
Wifi(#[from] wifi::WifiError),
}
pub struct AppHardware {
pub uart: Uart<'static, Async>,
pub network_stack: Stack<'static>,
pub i2c: I2c<'static, Async>,
pub buzzer: Output<'static>,
pub sd_present: AnyPin<'static>,
pub led: SmartLedsAdapterAsync<'static, LED_BUFFER_SIZE>,
pub sdcard: SDCardPersistence,
}
impl AppHardware {
pub async fn init(spawner: Spawner) -> Result<Self, HardwareInitError> {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 65536);
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt =
esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
init_logger(log::LevelFilter::Debug);
let mut led = setup_led(peripherals.RMT, peripherals.GPIO1)?;
let _ = led.write(brightness(
[RED; NUM_LEDS].into_iter(),
255,
))
.await;
let rng = esp_hal::rng::Rng::new();
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(peripherals.WIFI, spawner)?;
let network_stack = network::setup_network(network_seed, interfaces.ap, spawner);
Timer::after(Duration::from_millis(1)).await;
let uart_device = setup_uart(peripherals.UART1, peripherals.GPIO16, peripherals.GPIO17)?;
let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23)?;
let sd_det_gpio = peripherals.GPIO0;
let spi_bus = setup_spi(
peripherals.SPI2,
peripherals.GPIO19,
peripherals.GPIO20,
peripherals.GPIO18,
)?;
let sd_cs_pin = Output::new(
peripherals.GPIO2,
esp_hal::gpio::Level::High,
OutputConfig::default(),
);
let vol_mgr = setup_sdcard(spi_bus, sd_cs_pin);
let buzzer_gpio = peripherals.GPIO21;
let buzzer = setup_buzzer(buzzer_gpio);
Timer::after(Duration::from_millis(500)).await;
debug!("hardware init done");
Ok(Self {
uart: uart_device,
network_stack,
i2c: i2c_device,
buzzer,
sd_present: sd_det_gpio.into(),
led,
sdcard: vol_mgr,
})
}
}
fn setup_uart(
uart1: UART1<'static>,
uart_tx: GPIO16<'static>,
uart_rx: GPIO17<'static>,
) -> Result<Uart<'static, Async>, esp_hal::uart::ConfigError> {
let uart_device = Uart::new(uart1, esp_hal::uart::Config::default().with_baudrate(9600))?;
Ok(uart_device.with_rx(uart_rx).with_tx(uart_tx).into_async())
}
fn setup_i2c(
i2c0: I2C0<'static>,
sda: GPIO22<'static>,
scl: GPIO23<'static>,
) -> Result<I2c<'static, Async>, esp_hal::i2c::master::ConfigError> {
debug!("init I2C");
let config = Config::default().with_frequency(Rate::from_khz(400));
let i2c = I2c::new(i2c0, config)?;
Ok(i2c.with_sda(sda).with_scl(scl).into_async())
}
fn setup_spi(
spi2: SPI2<'static>,
sck: GPIO19<'static>,
miso: GPIO20<'static>,
mosi: GPIO18<'static>,
) -> Result<Spi<'static, Blocking>, esp_hal::spi::master::ConfigError> {
let spi = Spi::new(spi2, Spi_config::default())?;
Ok(spi.with_sck(sck).with_miso(miso).with_mosi(mosi))
}
pub fn setup_buzzer(buzzer_gpio: GPIO21<'static>) -> Output<'static> {
let config = esp_hal::gpio::OutputConfig::default()
.with_drive_strength(esp_hal::gpio::DriveStrength::_40mA);
Output::new(buzzer_gpio, esp_hal::gpio::Level::Low, config)
}
fn setup_led<'a>(
rmt: RMT<'a>,
led_gpio: GPIO1<'a>,
) -> Result<esp_hal_smartled::SmartLedsAdapterAsync<'a, LED_BUFFER_SIZE>, esp_hal::rmt::Error> {
let rmt: Rmt<'_, esp_hal::Async> = {
let frequency: Rate = Rate::from_mhz(80);
Rmt::new(rmt, frequency)
}?
.into_async();
let rmt_channel = rmt.channel0;
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); LED_BUFFER_SIZE];
Ok(SmartLedsAdapterAsync::new(
rmt_channel,
led_gpio,
rmt_buffer,
))
}

69
src/init/network.rs Normal file
View File

@@ -0,0 +1,69 @@
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};
use embassy_executor::Spawner;
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
use embassy_time::{Duration, Timer};
use esp_radio::wifi::WifiDevice;
use static_cell::StaticCell;
use crate::webserver::WEB_TAKS_SIZE;
pub const NETWORK_STACK_SIZE: usize = WEB_TAKS_SIZE + 2; // + 2 for other network taks. Breaks without
pub const GW_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 2, 1);
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: Spawner) -> Stack<'a> {
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
address: Ipv4Cidr::new(GW_IP, 24),
gateway: Some(GW_IP),
dns_servers: Default::default(),
});
static NETWORK_STACK: StaticCell<StackResources<NETWORK_STACK_SIZE>> = StaticCell::new();
let nw_stack = NETWORK_STACK.init(StackResources::new());
let (stack, runner) = embassy_net::new(wifi, config, nw_stack, seed);
spawner.must_spawn(net_task(runner));
spawner.must_spawn(run_dhcp(stack));
stack
}
#[embassy_executor::task]
async fn run_dhcp(stack: Stack<'static>) {
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
.expect("Failed to bind socket for DHCP server");
loop {
_ = io::server::run(
&mut Server::<_, 64>::new_with_et(GW_IP),
&ServerOptions::new(GW_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;
}

151
src/init/sd_card.rs Normal file
View File

@@ -0,0 +1,151 @@
use alloc::vec::Vec;
use embassy_time::Delay;
use embedded_hal_bus::spi::ExclusiveDevice;
use embedded_sdmmc::{SdCard, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager};
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
use crate::store::{AttendanceDay, IDMapping, day::Day, persistence::Persistence};
pub struct DummyTimesource;
impl TimeSource for DummyTimesource {
fn get_timestamp(&self) -> Timestamp {
Timestamp {
year_since_1970: 0,
zero_indexed_month: 0,
zero_indexed_day: 0,
hours: 0,
minutes: 0,
seconds: 0,
}
}
}
pub type VolMgr = VolumeManager<
SdCard<ExclusiveDevice<Spi<'static, Blocking>, Output<'static>, Delay>, Delay>,
DummyTimesource,
>;
pub fn setup_sdcard(spi_bus: Spi<'static, Blocking>, cs_pin: Output<'static>) -> SDCardPersistence {
let spi_device = ExclusiveDevice::new(spi_bus, cs_pin, Delay).unwrap();
let sd_card = SdCard::new(spi_device, Delay);
let vol_mgr = VolumeManager::new(sd_card, DummyTimesource);
SDCardPersistence { vol_mgr }
}
pub struct SDCardPersistence {
vol_mgr: VolMgr,
}
impl SDCardPersistence {
const MAPPING_FILENAME: &'static str = "MAPPING.JS";
fn generate_filename(day: Day) -> ShortFileName {
let basename = day.to_string();
let mut filename: heapless::String<11> = heapless::String::new();
filename.push_str(&basename).unwrap();
filename.push_str(".js").unwrap();
ShortFileName::create_from_str(&filename).unwrap()
}
}
impl Persistence for SDCardPersistence {
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let filename = Self::generate_filename(day);
let file = root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly);
if file.is_err() {
return None;
}
let mut open_file = file.unwrap();
let mut read_buffer: [u8; 1024] = [0; 1024];
let read = open_file.read(&mut read_buffer).unwrap();
open_file.close().unwrap();
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read]).unwrap();
Some(day)
}
async fn save_day(&mut self, day: Day, data: &AttendanceDay) {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let filename = Self::generate_filename(day);
let mut file = root_dir
.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
.unwrap();
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
file.flush().unwrap();
file.close().unwrap();
}
async fn load_mapping(&mut self) -> Option<crate::store::IDMapping> {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let file =
root_dir.open_file_in_dir(Self::MAPPING_FILENAME, embedded_sdmmc::Mode::ReadOnly);
if file.is_err() {
return None;
}
let mut open_file = file.unwrap();
let mut read_buffer: [u8; 1024] = [0; 1024];
let read = open_file.read(&mut read_buffer).unwrap();
open_file.close().unwrap();
let mapping: IDMapping = serde_json::from_slice(&read_buffer[..read]).unwrap();
Some(mapping)
}
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let mut file = root_dir
.open_file_in_dir(
Self::MAPPING_FILENAME,
embedded_sdmmc::Mode::ReadWriteCreateOrTruncate,
)
.unwrap();
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
file.flush().unwrap();
file.close().unwrap();
}
async fn list_days(&mut self) -> Vec<Day> {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let mut days_dir = root_dir.open_dir(".").unwrap();
let mut days: Vec<Day> = Vec::new();
days_dir
.iterate_dir(|e| {
let filename = e.name.clone();
if let Ok(day) = filename.try_into() {
days.push(day);
}
})
.unwrap();
days
}
}

77
src/init/wifi.rs Normal file
View File

@@ -0,0 +1,77 @@
use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use esp_hal::gpio::{Output, OutputConfig};
use esp_hal::peripherals::{GPIO3, GPIO14, WIFI};
use esp_radio::Controller;
use esp_radio::wifi::{
AccessPointConfig, Interfaces, ModeConfig, WifiApState, WifiController, WifiEvent,
};
use log::debug;
use static_cell::StaticCell;
use thiserror::Error;
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();
}
#[derive(Error, Debug)]
pub enum WifiError {
#[error("Failed to init radio")]
Init(#[from] esp_radio::InitializationError),
#[error("Failed to init wifi")]
Wifi(#[from] esp_radio::wifi::WifiError),
#[error("Failed to spawn wifi task")]
Spawn(#[from] embassy_executor::SpawnError),
}
pub fn setup_wifi<'d: 'static>(
wifi: WIFI<'static>,
spawner: Spawner,
) -> Result<Interfaces<'d>, WifiError> {
static ESP_WIFI_CTRL: StaticCell<Controller<'static>> = StaticCell::new();
let esp_wifi_ctrl = ESP_WIFI_CTRL.init(esp_radio::init()?);
let config = esp_radio::wifi::Config::default();
let (controller, interfaces) = esp_radio::wifi::new(esp_wifi_ctrl, wifi, config)?;
spawner.spawn(connection(controller))?;
Ok(interfaces)
}
#[embassy_executor::task]
async fn connection(mut controller: WifiController<'static>) {
debug!("start connection task");
debug!("Device capabilities: {:?}", controller.capabilities());
loop {
if esp_radio::wifi::ap_state() == WifiApState::Started {
// 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 = ModeConfig::AccessPoint(
AccessPointConfig::default()
.with_ssid(env!("WIFI_SSID").try_into().unwrap())
.with_password(env!("WIFI_PASSWD").try_into().unwrap())
.with_auth_method(esp_radio::wifi::AuthMethod::Wpa2Personal),
);
controller.set_config(&client_config).unwrap();
debug!("Starting wifi");
controller.start_async().await.unwrap();
debug!("Wifi started!");
}
}
}

1
src/lib.rs Normal file
View File

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

View File

@@ -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);
}

View File

@@ -1,192 +1,145 @@
#![allow(dead_code)] #![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
#![feature(impl_trait_in_assoc_type)]
#![warn(clippy::unwrap_used)]
use anyhow::Result; use alloc::rc::Rc;
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::{CriticalSectionRawMutex, NoopRawMutex},
sync::Arc, mutex::Mutex,
time::Duration, pubsub::{
}; PubSubChannel, Publisher, Subscriber,
use tally_id::TallyID; WaitResult::{Lagged, Message},
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 esp_hal::gpio::InputConfig;
use esp_hal::gpio::{AnyPin, Input};
use log::{debug, info};
use static_cell::StaticCell;
use crate::{hardware::{create_hotspot, Hotspot}, pm3::run_pm3, store::IDStore, webserver::{spawn_idle_watcher, ActivityNotifier}}; extern crate alloc;
use crate::{
init::{hardware::AppHardware, sd_card::SDCardPersistence},
store::{IDStore, day::Day, tally_id::TallyID},
webserver::start_webserver,
};
mod drivers;
mod feedback; mod feedback;
mod hardware; mod init;
mod pm3;
mod logger;
mod tally_id;
mod webserver;
mod store; mod store;
mod webserver;
const STORE_PATH: &str = "./data.json"; static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
async fn run_webserver<H>( type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
store: Arc<Mutex<IDStore>>, type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
id_channel: Sender<String>, type TallySubscriber = Subscriber<'static, NoopRawMutex, TallyID, 8, 2, 1>;
hotspot: Arc<Mutex<H>>, type UsedStore = IDStore<SDCardPersistence>;
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 { static CHAN: StaticCell<TallyChannel> = StaticCell::new();
sender: activity_channel,
};
start_webserver(store, id_channel, notifier).await?; #[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
let app_hardware = AppHardware::init(spawner).await.unwrap();
Ok(()) info!("Starting up...");
}
async fn load_or_create_store() -> Result<IDStore> { let mut rtc = drivers::rtc::RTCClock::new(app_hardware.i2c).await;
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())
}
}
fn get_hotspot_enable_ids() -> Vec<TallyID> { let current_day: Day = rtc.get_time().await.into();
let hotspot_ids: Vec<TallyID> = env::var("HOTSPOT_IDS") let store: UsedStore = IDStore::new_from_storage(app_hardware.sdcard, current_day).await;
.map(|ids| ids.split(";").map(|id| TallyID(id.to_owned())).collect()) let shared_store = Rc::new(Mutex::new(store));
.unwrap_or_default();
if hotspot_ids.is_empty() { let chan: &'static mut TallyChannel = CHAN.init(PubSubChannel::new());
warn!( let publisher: TallyPublisher = chan.publisher().unwrap();
"HOTSPOT_IDS is not set or empty. You will not be able to activate the hotspot via a tally!" let mut sub: TallySubscriber = chan.subscriber().unwrap();
);
}
hotspot_ids wait_for_stack_up(app_hardware.network_stack).await;
}
async fn handle_ids_loop( start_webserver(
mut id_channel: Receiver<String>, spawner,
hotspot_enable_ids: Vec<TallyID>, app_hardware.network_stack,
id_store: Arc<Mutex<IDStore>>, shared_store.clone(),
hotspot: Arc<Mutex<impl Hotspot>>, chan,
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) { /****************************** Spawning tasks ***********************************/
info!("Enableing hotspot"); debug!("spawing NFC reader task...");
let hotspot_enable_result = hotspot.lock().await.enable_hotspot().await; spawner.must_spawn(drivers::nfc_reader::rfid_reader_task(
app_hardware.uart,
publisher,
));
match hotspot_enable_result { debug!("spawing feedback task..");
Ok(_) => { spawner.must_spawn(feedback::feedback_task(
user_feedback app_hardware.led,
.lock() app_hardware.buzzer,
.await ));
.set_device_status(feedback::DeviceStatus::HotspotEnabled);
debug!("spawn sd detect task");
spawner.must_spawn(sd_detect_task(app_hardware.sd_present));
/******************************************************************************/
debug!("everything spawned");
FEEDBACK_STATE.signal(feedback::FeedbackState::Startup);
loop {
let wait_result = sub.next_message().await;
match wait_result {
Lagged(_) => debug!("Lagged"),
Message(msg) => {
debug!("Got message: {msg:?}");
let day: Day = rtc.get_time().await.into();
let added = shared_store.lock().await.add_id(msg, day).await;
if added {
FEEDBACK_STATE.signal(feedback::FeedbackState::Ack);
} }
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>>) { #[embassy_executor::task]
let _ = feedback.lock().await.activate_error_state().await; async fn sd_detect_task(sd_det_gpio: AnyPin<'static>) {
let _ = hotspot.lock().await.enable_hotspot().await; let mut sd_det = Input::new(sd_det_gpio, InputConfig::default());
sd_det.wait_for(esp_hal::gpio::Event::AnyEdge).await;
let mut sigterm = signal(SignalKind::terminate()).unwrap(); loop {
sigterm.recv().await; sd_det.wait_for_any_edge().await;
} {
if sd_det.is_high() {
#[tokio::main] FEEDBACK_STATE.signal(feedback::FeedbackState::Ack);
async fn main() -> Result<()> { debug!("card insert");
logger::setup_logger(); }
//card is not insert on low
info!("Starting application"); else {
FEEDBACK_STATE.signal(feedback::FeedbackState::Nack);
let user_feedback = Arc::new(Mutex::new(Feedback::new()?)); debug!("card removed");
let hotspot = Arc::new(Mutex::new(create_hotspot()?)); }
}
let error_flag_set = args().any(|e| e == "--error" || e == "-e"); //debounce time
if error_flag_set { Timer::after(Duration::from_millis(100)).await;
error!("Error flag set. Entering error state"); }
enter_error_state(user_feedback.clone(), hotspot).await; }
return Ok(());
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;
} }
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(())
} }

View File

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

View File

@@ -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())
}

View File

@@ -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");
}
}
}

63
src/store/day.rs Normal file
View File

@@ -0,0 +1,63 @@
use core::fmt::Write;
use embedded_sdmmc::ShortFileName;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Day(u32);
impl Day {
const SECONDS_PER_DAY: u64 = 86_400;
pub fn new(daystamp: u32) -> Self {
Day(daystamp)
}
pub fn new_from_timestamp(time: u64) -> Self {
let day = time / Self::SECONDS_PER_DAY;
if day > u32::MAX as u64 {
// TBH this would only happen if about 11 million years have passed
// I sure hope i don't have to work on this project any more then
// So we just cap it at this
Day(u32::MAX)
} else {
Day(day as u32)
}
}
pub fn to_timestamp(self) -> u64 {
(self.0 as u64) * Self::SECONDS_PER_DAY
}
pub fn to_string(self) -> heapless::String<8> {
let mut s: heapless::String<8> = heapless::String::new();
write!(s, "{:08X}", self.0).unwrap();
s
}
pub fn from_hex_str(s: &str) -> Result<Self, &'static str> {
if s.len() > 8 {
return Err("hex string too long");
}
u32::from_str_radix(s, 16)
.map_err(|_| "invalid hex string")
.map(Day)
}
}
impl From<u64> for Day {
fn from(value: u64) -> Self {
Self::new_from_timestamp(value)
}
}
impl TryFrom<ShortFileName> for Day {
type Error = ();
fn try_from(value: ShortFileName) -> Result<Self, Self::Error> {
let name = core::str::from_utf8(value.base_name()).map_err(|_| ())?;
Self::from_hex_str(name).map_err(|_| ())
}
}

View File

@@ -1,22 +1,25 @@
use crate::tally_id::TallyID; use alloc::collections::BTreeMap;
use alloc::string::String;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] use crate::store::tally_id::TallyID;
#[derive(Clone, Serialize, Deserialize)]
pub struct Name { pub struct Name {
pub first: String, pub first: String,
pub last: String, pub last: String,
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Clone, Serialize, Deserialize)]
pub struct IDMapping { pub struct IDMapping {
id_map: HashMap<TallyID, Name>, #[serde(flatten)]
id_map: BTreeMap<TallyID, Name>,
} }
impl IDMapping { impl IDMapping {
pub fn new() -> Self { pub fn new() -> Self {
IDMapping { IDMapping {
id_map: HashMap::new(), id_map: BTreeMap::new(),
} }
} }
@@ -28,49 +31,3 @@ impl IDMapping {
self.id_map.insert(id, name); self.id_map.insert(id, name);
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let mut map = IDMapping::new();
let id1 = TallyID("A2Fb44".to_owned());
let name1 = Name {
first: "Max".to_owned(),
last: "Mustermann".to_owned(),
};
map.add_mapping(id1.clone(), name1.clone());
let res = map.map(&id1);
assert_eq!(res, Some(&name1));
}
#[test]
fn multiple() {
let mut map = IDMapping::new();
let id1 = TallyID("A2Fb44".to_owned());
let name1 = Name {
first: "Max".to_owned(),
last: "Mustermann".to_owned(),
};
let id2 = TallyID("7D3DF5B5".to_owned());
let name2 = Name {
first: "First".to_owned(),
last: "Last".to_owned(),
};
map.add_mapping(id1.clone(), name1.clone());
map.add_mapping(id2.clone(), name2.clone());
let res = map.map(&id1);
assert_eq!(res, Some(&name1));
let res = map.map(&id2);
assert_eq!(res, Some(&name2));
}
}

View File

@@ -1,123 +1,22 @@
use anyhow::{Result, anyhow}; use alloc::vec::Vec;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use std::collections::{HashMap, HashSet}; use serde::Serialize;
use tokio::fs;
use crate::{store::IDMapping, tally_id::TallyID}; use super::IDMapping;
use crate::store::day::Day;
use crate::store::persistence::Persistence;
use crate::store::tally_id::TallyID;
/// Represents a single day that IDs can attend #[derive(Clone, Serialize, Deserialize, Debug)]
#[derive(Deserialize, Serialize)]
pub struct AttendanceDay { pub struct AttendanceDay {
date: String, date: Day,
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(&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 { impl AttendanceDay {
fn new(day: &str) -> Self { pub fn new(date: Day) -> Self {
Self { Self {
date: day.to_owned(), date,
ids: Vec::new(), ids: Vec::new(),
} }
} }
@@ -133,7 +32,79 @@ impl AttendanceDay {
} }
} }
fn get_day_str() -> String { #[derive(Clone)]
let now = chrono::offset::Local::now(); pub struct IDStore<T: Persistence> {
now.format("%Y-%m-%d").to_string() pub current_day: AttendanceDay,
pub mapping: IDMapping,
persistence_layer: T,
}
impl<T: Persistence> IDStore<T> {
pub async fn new_from_storage(mut persistence_layer: T, current_date: Day) -> Self {
let mapping = match persistence_layer.load_mapping().await {
Some(map) => map,
None => IDMapping::new(),
};
let day = persistence_layer
.load_day(current_date)
.await
.unwrap_or(AttendanceDay::new(current_date));
Self {
current_day: day,
mapping,
persistence_layer,
}
}
async fn persist_day(&mut self) {
self.persistence_layer
.save_day(self.current_day.date, &self.current_day)
.await
}
pub async fn persist_mapping(&mut self) {
self.persistence_layer.save_mapping(&self.mapping).await
}
/// Add a new id for the current day
/// Returns false if ID is already present at the current day.
pub async fn add_id(&mut self, id: TallyID, current_date: Day) -> bool {
if self.current_day.date == current_date {
let changed = self.current_day.add_id(id);
if changed {
self.persist_day().await;
}
return changed;
}
let new_day = AttendanceDay::new(current_date);
self.current_day = new_day;
let changed = self.current_day.add_id(id);
if changed {
self.persist_day().await;
}
changed
}
/// Load and return a AttendanceDay. Nothing more. Nothing less.
pub async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
if day == self.current_day.date {
return Some(self.current_day.clone());
}
self.persistence_layer.load_day(day).await
}
pub async fn list_days_in_timespan(&mut self, from: Day, to: Day) -> Vec<Day> {
let all_days = self.persistence_layer.list_days().await;
all_days
.into_iter()
.filter(|e| *e >= from)
.filter(|e| *e <= to)
.collect()
}
} }

View File

@@ -1,5 +1,9 @@
mod id_store; pub use id_mapping::{IDMapping, Name};
mod id_mapping; pub use id_store::{IDStore,AttendanceDay};
mod id_mapping;
pub mod persistence;
mod id_store;
pub mod tally_id;
pub mod day;
pub use id_store::IDStore;
pub use id_mapping::{IDMapping,Name};

12
src/store/persistence.rs Normal file
View File

@@ -0,0 +1,12 @@
use alloc::vec::Vec;
use crate::store::{IDMapping, day::Day, id_store::AttendanceDay};
pub trait Persistence {
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay>;
async fn save_day(&mut self, day: Day, data: &AttendanceDay);
async fn list_days(&mut self) -> Vec<Day>;
async fn load_mapping(&mut self) -> Option<IDMapping>;
async fn save_mapping(&mut self, data: &IDMapping);
}

108
src/store/tally_id.rs Normal file
View File

@@ -0,0 +1,108 @@
use core::{fmt::Display, str::FromStr};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct TallyID([u8; 6]);
impl FromStr for TallyID {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.as_bytes().try_into()
}
}
impl TryFrom<heapless::String<12>> for TallyID {
type Error = ();
fn try_from(value: heapless::String<12>) -> Result<Self, Self::Error> {
let bytes = value.as_bytes();
let mut out: [u8; 6] = [0; 6];
for i in 0..6 {
let hi = hex_val(bytes[2 * i])?;
let lo = hex_val(bytes[2 * i + 1])?;
out[i] = (hi << 4) | lo;
}
Ok(TallyID(out))
}
}
fn hex_val(b: u8) -> Result<u8, ()> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(()),
}
}
impl From<TallyID> for heapless::String<12> {
fn from(value: TallyID) -> Self {
const HEX_CHARS: &[u8; 16] = b"0123456789ABCDEF";
let mut s: Self = Self::new();
for &b in &value.0 {
// Should be safe to unwrap since the string is already long enough
s.push(HEX_CHARS[(b >> 4) as usize] as char).unwrap();
s.push(HEX_CHARS[(b & 0x0F) as usize] as char).unwrap();
}
s
}
}
/// From a array of hex chars
impl TryFrom<&[u8]> for TallyID {
type Error = ();
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() != 12 {
return Err(());
}
let mut out: [u8; 6] = [0; 6];
for i in 0..6 {
let hi = hex_val(value[2 * i])?;
let lo = hex_val(value[2 * i + 1])?;
out[i] = (hi << 4) | lo;
}
Ok(TallyID(out))
}
}
impl TryFrom<[u8; 12]> for TallyID {
type Error = ();
fn try_from(value: [u8; 12]) -> Result<Self, Self::Error> {
Self::try_from(&value as &[u8])
}
}
impl Display for TallyID {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s: heapless::String<12> = (*self).into();
write!(f, "{}", s)
}
}
impl Serialize for TallyID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s: heapless::String<12> = (*self).into();
serializer.serialize_str(&s)
}
}
impl<'de> Deserialize<'de> for TallyID {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
TallyID::from_str(s).map_err(|_| de::Error::custom("Failed to parse Tally ID"))
}
}

View File

@@ -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())
}
}

View File

@@ -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
}

94
src/webserver/api.rs Normal file
View File

@@ -0,0 +1,94 @@
use log::error;
use picoserve::{
extract::{Json, Query, State},
response::{self, IntoResponse},
};
use serde::Deserialize;
use crate::{
store::{Name, day::Day, tally_id::TallyID},
webserver::{app::AppState, sse::IDEvents},
};
#[derive(Deserialize)]
pub struct NewMapping {
id: TallyID,
name: Name,
}
#[derive(Deserialize)]
pub struct QueryTimespan {
from: u64,
to: u64,
}
#[derive(Deserialize)]
pub struct QueryDay {
timestamp: Option<u64>,
day: Option<u32>,
}
// GET /api/mapping
pub async fn get_mapping(State(state): State<AppState>) -> impl IntoResponse {
let store = state.store.lock().await;
response::Json(store.mapping.clone())
}
// POST /api/mapping
pub async fn add_mapping(
State(state): State<AppState>,
Json(data): Json<NewMapping>,
) -> impl IntoResponse {
let mut store = state.store.lock().await;
store.mapping.add_mapping(data.id, data.name);
store.persist_mapping().await;
}
// SSE /api/idevent
pub async fn get_idevent(
State(state): State<AppState>,
) -> Result<impl IntoResponse, impl IntoResponse> {
match state.chan.subscriber() {
Ok(chan) => Ok(response::EventStream(IDEvents(chan))),
Err(e) => {
error!("Failed to create SSE: {:?}", e);
Err((
response::StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error",
))
}
}
}
// GET /api/days
pub async fn get_days(
State(state): State<AppState>,
Query(QueryTimespan { from, to }): Query<QueryTimespan>,
) -> impl IntoResponse {
let from_day = Day::new_from_timestamp(from);
let to_day = Day::new_from_timestamp(to);
let mut store = state.store.lock().await;
let days = store.list_days_in_timespan(from_day, to_day).await;
response::Json(days)
}
// GET /api/day
pub async fn get_day(
State(state): State<AppState>,
Query(QueryDay { timestamp, day }): Query<QueryDay>,
) -> Result<impl IntoResponse, impl IntoResponse> {
let parsed_day = timestamp
.map(Day::new_from_timestamp)
.or_else(|| day.map(Day::new))
.ok_or((response::StatusCode::NOT_FOUND, "Not found"))?;
let mut store = state.store.lock().await;
match store.load_day(parsed_day).await {
Some(att_day) => Ok(response::Json(att_day)),
None => Err((response::StatusCode::NOT_FOUND, "Not found")),
}
}

32
src/webserver/app.rs Normal file
View File

@@ -0,0 +1,32 @@
use alloc::rc::Rc;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
use picoserve::{AppWithStateBuilder, routing::get};
use crate::{
TallyChannel, UsedStore,
webserver::{
api::{add_mapping, get_day, get_days, get_idevent, get_mapping},
assets::Assets,
},
};
#[derive(Clone)]
pub struct AppState {
pub store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
pub chan: &'static TallyChannel,
}
pub struct AppProps;
impl AppWithStateBuilder for AppProps {
type State = AppState;
type PathRouter = impl picoserve::routing::PathRouter<AppState>;
fn build_app(self) -> picoserve::Router<Self::PathRouter, AppState> {
picoserve::Router::from_service(Assets)
.route("/api/mapping", get(get_mapping).post(add_mapping))
.route("/api/idevent", get(get_idevent))
.route("/api/days", get(get_days))
.route("/api/day", get(get_day))
}
}

74
src/webserver/assets.rs Normal file
View File

@@ -0,0 +1,74 @@
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?, 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: edge_nal::io::Write>(self, mut writer: W) -> Result<(), W::Error> {
writer.write_all(self.0).await
}
}

View File

@@ -1,6 +1,59 @@
mod server; use alloc::rc::Rc;
mod activity_fairing; use embassy_executor::Spawner;
use embassy_net::Stack;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
use embassy_time::Duration;
use picoserve::{AppRouter, AppWithStateBuilder};
use static_cell::make_static;
pub use activity_fairing::{ActivityNotifier,spawn_idle_watcher}; use crate::{
pub use server::start_webserver; TallyChannel, UsedStore,
webserver::app::{AppProps, AppState},
};
mod api;
mod app;
mod assets;
mod sse;
pub const WEB_TAKS_SIZE: usize = 5; // Up this number if request start fail with Timeouts.
pub fn start_webserver(
spawner: Spawner,
stack: Stack<'static>,
store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
chan: &'static TallyChannel,
) {
let app = make_static!(AppProps.build_app());
let state = make_static!(AppState { store, chan });
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(5)),
read_request: Some(Duration::from_secs(5)),
write: Some(Duration::from_secs(5)),
}));
for task_id in 0..WEB_TAKS_SIZE {
spawner.must_spawn(webserver_task(task_id, stack, app, config, state));
}
}
#[embassy_executor::task(pool_size = WEB_TAKS_SIZE)]
async fn webserver_task(
task_id: usize,
stack: embassy_net::Stack<'static>,
app: &'static AppRouter<AppProps>,
config: &'static picoserve::Config<Duration>,
state: &'static AppState,
) -> ! {
let mut tcp_rx_buffer = [0u8; 1024];
let mut tcp_tx_buffer = [0u8; 1024];
let mut http_buffer = [0u8; 2048];
picoserve::Server::new(&app.shared().with_state(state), config, &mut http_buffer)
.listen_and_serve(task_id, stack, 80, &mut tcp_rx_buffer, &mut tcp_tx_buffer)
.await
.into_never()
}

View File

@@ -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
}

32
src/webserver/sse.rs Normal file
View File

@@ -0,0 +1,32 @@
use embassy_time::{Duration, Timer};
use log::warn;
use picoserve::response;
use crate::TallySubscriber;
pub struct IDEvents(pub TallySubscriber);
impl response::sse::EventSource for IDEvents {
async fn write_events<W: picoserve::io::Write>(
mut self,
mut writer: response::sse::EventWriter<W>,
) -> Result<(), W::Error> {
loop {
let timeout = Timer::after(Duration::from_secs(15));
let sel = embassy_futures::select::select(self.0.next_message(), timeout);
match sel.await {
embassy_futures::select::Either::First(msg) => match msg {
embassy_sync::pubsub::WaitResult::Message(id) => {
let id_str: heapless::String<12> = id.into();
writer.write_event("msg", id_str.as_str()).await?
}
embassy_sync::pubsub::WaitResult::Lagged(_) => {
warn!("SSE subscriber got lagged");
}
},
embassy_futures::select::Either::Second(_) => writer.write_keepalive().await?,
}
}
}
}

88
used_ids.txt Normal file
View File

@@ -0,0 +1,88 @@
8f801f988c
6fc90dd450
edf3f0793a
df39cf9566
b9f998924d
f7686ed090
c4dc09d5fa
78908e3baa
da874600cd
66beb3dfad
b5758c2ada
aee543f099
ea912de917
d309e7a0da
54d1fbd27a
a2f95c6f50
b663118bfd
8d639f381c
25ec58dace
a0ecef7443
cab4672699
f8a021b691
5314cc63d8
ad4a8a3882
927ead1dec
743a8e8162
f54694666a
38f8ff49c4
6da025be13
afe671009f
8a18526cc5
fe6ead39e7
07b0391c5b
aaf6d9cef5
ee12fe5bbf
96b833ccc2
690eec798e
142c3cb709
cc585eea85
1b426bf077
3df69e83bc
8fba107bbc
79e24823d2
3ec6e52678
e1e0d87659
4c12460af8
7d506534de
c4946d9a72
80d2b13291
0c36d4a7a7
776cef50a2
1cc64b5158
ee78890172
63fa57ad63
9072b8fad8
b3c407a858
833c54a7a5
99d2c32c35
4f5e5357e2
82cea5924d
fec8fa57ef
11b49b1b2b
e40a8e6e3f
2fbe63bb85
f76830b226
76544233e5
2fc8c544ef
2a4cc77d6b
f52eebdd85
508e07aca5
936aed7997
0fbf70c4c6
bf47dfd6b7
81e7d42454
96b701ef5d
11d3ecfa1b
e0bd39c427
6fa914114e
d7a3b89055
e417131533
1fef16c2ce
1af12ecd77
37e1a8f1dc
65e4521004
a0be6cc4fa
90bcf9dbaa
8f169642c4
ac5b109c5b

108
web/mock/data.json Normal file
View File

@@ -0,0 +1,108 @@
{
"mapping": [
[
"123456789ABC",
{
"first": "Feuerwehrman",
"last": "Sam"
}
],
[
"A1B2C3D4E5F6",
{
"first": "Luna",
"last": "Starforge"
}
],
[
"0F1E2D3C4B5A",
{
"first": "Gareth",
"last": "Ironwill"
}
],
[
"ABCDEF123456",
{
"first": "Nina",
"last": "Skylark"
}
],
[
"654321FEDCBA",
{
"first": "Tobias",
"last": "Marrow"
}
],
[
"DEADBEEFCAFE",
{
"first": "Astra",
"last": "Vale"
}
],
[
"BADA55C0FFEE",
{
"first": "Rowan",
"last": "Tempest"
}
],
[
"C001D00D1337",
{
"first": "Juniper",
"last": "Voss"
}
],
[
"A0B1DB0D133B",
{
"first": "Öäü",
"last": "ßẞ"
}
]
],
"days": [
{
"date": 20372,
"ids": [
"123456789ABC",
"A1B2C3D4E5F6",
"A0B1DB0D133B"
]
},
{
"date": 20373,
"ids": [
"0F1E2D3C4B5A",
"ABCDEF123456"
]
},
{
"date": 20374,
"ids": [
"654321FEDCBA",
"DEADBEEFCAFE",
"BADA55C0FFEE",
"A0B1DB0D133B"
]
},
{
"date": 20375,
"ids": [
"C001D00D1337",
"A1B2C3D4E5F6",
"123456789ABC"
]
},
{
"date": 20376,
"ids": [
"N0T3X1ST1D0",
"654321FEDCBA"
]
}
]
}

111
web/mock/server.js Normal file
View File

@@ -0,0 +1,111 @@
import express from "express";
import bodyParser from "body-parser";
import mockData from "./data.json" with {type: "json"};
const app = express();
const port = 3000;
const SECS_IN_DAY = 86_400;
app.use(bodyParser.json());
function generateRandomId() {
const chars = "ABCDEF0123456789";
let id = "";
for (let i = 0; i < 12; i++) {
id += chars.charAt(Math.floor(Math.random() * chars.length));
}
return id;
}
// GET /api/mapping
app.get("/api/mapping", (req, res) => {
res.json(mockData.mapping);
});
// POST /api/mapping
app.post("/api/mapping", (req, res) => {
const { id, name } = req.body;
if (!id || !name || !name.first || !name.last) {
return res.status(400).json({ error: "Invalid request body" });
}
// Check if ID already exists
const existing = mappings.find((entry) => entry[0] === id);
if (existing) {
return res.status(409).json({ error: "ID already exists" });
}
// Add new mapping
mockData.mappings.push([id, name]);
res.status(201).send("");
});
app.get("/api/day", (req, res) => {
let day;
if (req.query.day) {
day = parseInt(req.query.day, 10);
}else if (req.query.timestamp) {
let ts = parseInt(req.query.timestamp, 10);
day = ts / SECS_IN_DAY;
}else {
return res.status(400).json({ error: "Missing or invalid 'day' parameter" });
}
if (isNaN(day)) {
return res.status(400).json({ error: "Missing or invalid 'day' parameter" });
}
let foundDay = mockData.days.find(e => e.date == day);
if (!foundDay) {
return res.status(404).send("Not found");
}
res.status(200).json(foundDay);
});
app.get("/api/days", (req,res) => {
let qFrom = parseInt(req.query.from) / SECS_IN_DAY;
let qTo = parseInt(req.query.to) / SECS_IN_DAY;
let days = mockData.days.filter(e => e.date >= qFrom && e.date <= qTo).map(e => e.date);
res.status(200).json(days);
});
// SSE route: /api/idevent
app.get("/api/idevent", (req, res) => {
// Set headers for SSE
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders(); // flush the headers to establish SSE connection
// Send initial event
const sendEvent = () => {
const id = generateRandomId();
res.write(`data: ${id}\n\n`);
};
// Send immediately and then every 10 seconds
sendEvent();
const interval = setInterval(sendEvent, 10000);
// When client closes connection, stop interval
req.on("close", () => {
clearInterval(interval);
res.end();
});
});
// Start the server
app.listen(port, () => {
console.log(`Mock API server running at http://localhost:${port}`);
});

887
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,9 @@
"svelte": "^5.28.1", "svelte": "^5.28.1",
"svelte-check": "^4.1.6", "svelte-check": "^4.1.6",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"vite": "^6.3.5" "vite": "^6.4.1",
"body-parser": "^2.2.0",
"express": "^5.1.0"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",

View File

@@ -3,18 +3,24 @@
import IDTable from "./lib/IDTable.svelte"; import IDTable from "./lib/IDTable.svelte";
import LastId from "./lib/LastID.svelte"; import LastId from "./lib/LastID.svelte";
import AddIDModal from "./lib/AddIDModal.svelte"; import AddIDModal from "./lib/AddIDModal.svelte";
import ExportModal from "./lib/ExportModal.svelte";
import { generateCSVFile } from "./lib/exporting";
import { fetchMapping, type IDMap } from "./lib/IDMapping";
import { downloadBlob } from "./lib/downloadBlob";
let lastID: string = $state(""); let lastID: string = $state("");
let mapping: IDMap | null = $state(null);
let addModal: AddIDModal; let addModal: AddIDModal;
let idTable: IDTable; let exportModal: ExportModal;
onMount(async () => {
mapping = await fetchMapping();
onMount(() => {
let sse = new EventSource("/api/idevent"); let sse = new EventSource("/api/idevent");
sse.addEventListener("msg", function (e) {
sse.onmessage = (e) => {
lastID = e.data; lastID = e.data;
}; });
}); });
</script> </script>
@@ -25,13 +31,14 @@
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1> <h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
</div> </div>
<a <button
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition" class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
href="/api/csv" onclick={() => {
download="anwesenheit.csv" exportModal.open();
}}
> >
Download CSV Export CSV
</a> </button>
<div class="pt-3 pb-2"> <div class="pt-3 pb-2">
<LastId <LastId
@@ -42,15 +49,35 @@
/> />
</div> </div>
<div> <div>
<IDTable bind:this={idTable} onEdit={(id,firstName,lastName)=>{ {#if mapping}
addModal.open(id,firstName,lastName); <IDTable
}}/> data={mapping}
onEdit={(id, firstName, lastName) => {
addModal.open(id, firstName, lastName);
}}
/>
<span>Gesammmte einträge: { Object.keys(mapping).length}</span>
{/if}
</div> </div>
<AddIDModal <AddIDModal
bind:this={addModal} bind:this={addModal}
onSubmitted={() => { onSubmitted={async () => {
idTable.reloadData(); mapping = await fetchMapping();
}}
/>
<ExportModal
bind:this={exportModal}
onSubmitted={async (from, to) => {
if (!mapping) {
return;
}
let csvFile = await generateCSVFile(from, to, mapping);
downloadBlob("export.csv", csvFile, "text/csv");
}} }}
/> />
</main> </main>

31
web/src/lib/Day.ts Normal file
View File

@@ -0,0 +1,31 @@
export type Day = number;
export interface AttendanceDay {
date: Day,
ids: string[],
}
export function dayToDate(day: Day): Date {
const SEC_PER_DAY = 86_400;
return new Date(day * SEC_PER_DAY * 1000);
}
export async function fetchDay(day: Day): Promise<AttendanceDay> {
let res = await fetch("/api/day?" + (new URLSearchParams({ day: day.toString() }).toString()));
let json = await res.json();
return json;
}
export async function fetchDays(from: Date, to: Date): Promise<Day[]> {
let q = new URLSearchParams({ from: (from.getTime() / 1000).toString(), to: (to.getTime() / 1000).toString() });
let res = await fetch("/api/days?" + q);
let json = await res.json();
return json;
}

View File

@@ -0,0 +1,142 @@
<script lang="ts">
import Modal from "./Modal.svelte";
let { onSubmitted }: { onSubmitted?: (from: Date, to: Date) => void } =
$props();
let modal: Modal;
let fromDate: string | undefined = $state();
let toDate: string | undefined = $state();
let selectedYear: number = $state(new Date().getFullYear());
let selectedTab = $state(0);
export function open() {
modal.open();
}
function generateYears() {
const currentYear = new Date().getFullYear();
const startingYear = 2020;
return Array.from(
new Array(currentYear + 1 - startingYear),
(_, i) => i + startingYear,
);
}
function onsubmit(e: SubmitEvent) {
let from: Date;
let to: Date;
switch (selectedTab) {
case 0:
if (!fromDate || !toDate) {
e.preventDefault();
return;
}
from = new Date(fromDate);
to = new Date(toDate);
break;
case 1:
from = new Date(selectedYear, 0);
to = new Date(selectedYear + 1, 0);
break;
default:
console.error("Invalid tab");
return;
}
onSubmitted?.(from, to);
}
</script>
<Modal bind:this={modal}>
<div class="flex">
<button
onclick={() => {
selectedTab = 0;
}}
class="tab {selectedTab === 0 ? 'tab-active' : ''}"
>
Datum
</button>
<button
onclick={() => {
selectedTab = 1;
}}
class="tab {selectedTab === 1 ? 'tab-active' : ''}"
>
Jahr
</button>
</div>
<form method="dialog" {onsubmit} class="flex flex-col">
{#if selectedTab === 0}
<div>
<label class="form-row">
<span>Von:</span>
<input type="date" class="form-input" bind:value={fromDate} />
</label>
<label class="form-row">
<span>Bis:</span>
<input type="date" class="form-input" bind:value={toDate} />
</label>
</div>
{/if}
{#if selectedTab === 1}
<div>
<label class="form-row">
<span>Kalendar Jahr:</span>
<select class="form-input" bind:value={selectedYear}>
{#each generateYears() as year}
<option value={year}>{year}</option>
{/each}
</select>
</label>
</div>
{/if}
<div class="flex justify-end mt-3">
<button
type="reset"
class="mr-5 px-2 py-1 bg-red-500 rounded-2xl shadow-md"
onclick={() => {
modal.close();
fromDate = undefined;
toDate = undefined;
}}>Abbrechen</button
>
<button
type="submit"
class="px-2 py-1 bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
>Export CSV</button
>
</div>
</form>
</Modal>
<style scoped>
@reference "../app.css";
.tab {
@apply px-4 py-2 rounded-t-lg bg-indigo-600 hover:bg-indigo-700 font-medium border-b-2 border-transparent cursor-pointer transition-colors duration-200;
}
.tab-active {
@apply px-4 py-2 bg-indigo-500 font-semibold border-b-2 border-blue-600 shadow-sm cursor-pointer;
}
.form-row {
@apply flex justify-between my-1;
}
.form-input {
@apply ml-20;
}
</style>

View File

@@ -1,7 +1,3 @@
export interface IDMapping {
id_map: IDMap
}
export interface IDMap { export interface IDMap {
[name: string]: Name [name: string]: Name
} }
@@ -11,6 +7,23 @@ export interface Name {
last: string, last: string,
} }
function stupidSerdeFix(pairs: [string, Name][]): IDMap {
const map: IDMap = {};
for (const [key, value] of pairs) {
map[key] = value;
}
return map;
}
export async function fetchMapping(): Promise<IDMap> {
let res = await fetch("/api/mapping");
let data = await res.json();
return stupidSerdeFix(data);
}
export async function addMapping(id: string, firstName: string, lastName: string) { export async function addMapping(id: string, firstName: string, lastName: string) {
let req = await fetch("/api/mapping", { let req = await fetch("/api/mapping", {
method: "POST", method: "POST",

View File

@@ -1,19 +1,17 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { type IDMap } from "./IDMapping";
import type { IDMapping } from "./IDMapping";
let data: IDMapping | undefined = $state();
let { onEdit }: { onEdit?: (string,string,string) => void } = $props(); let {
onEdit,
export async function reloadData() { data,
let res = await fetch("/api/mapping"); }: {
onEdit?: (id: string, firstName: string, lastName: string) => void;
data = await res.json(); data: IDMap;
} } = $props();
let rows = $derived( let rows = $derived(
data data
? Object.entries(data.id_map).map(([id, value]) => ({ ? Object.entries(data).map(([id, value]) => ({
id, id,
...value, ...value,
})) }))
@@ -43,70 +41,64 @@
if (sortKey !== key) return ""; if (sortKey !== key) return "";
return sortDirection === "asc" ? "▲" : "▼"; return sortDirection === "asc" ? "▲" : "▼";
} }
onMount(async () => {
await reloadData();
});
</script> </script>
{#if data == null} <div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
Loading... <table class="px-10">
{:else} <thead>
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto"> <tr>
<table class="px-10"> <th
<thead> class="text-left pr-5 pl-2 cursor-pointer select-none"
<tr> onclick={() => {
<th handleSortClick("id");
class="text-left pr-5 pl-2 cursor-pointer select-none" }}
onclick={() => { >
handleSortClick("id"); ID
}} <span class="indicator">{indicator("id")}</span>
> </th>
ID <th
<span class="indicator">{indicator("id")}</span> class="text-left pr-5 cursor-pointer select-none"
</th> onclick={() => {
<th handleSortClick("last");
class="text-left pr-5 cursor-pointer select-none" }}
onclick={() => { >
handleSortClick("last"); Nachname
}} <span class="indicator">{indicator("last")}</span>
> </th>
Nachname <th
<span class="indicator">{indicator("last")}</span> class="text-left pr-5 cursor-pointer select-none"
</th> onclick={() => {
<th handleSortClick("first");
class="text-left pr-5 cursor-pointer select-none" }}
onclick={() => { >Vorname
handleSortClick("first");
}}
>Vorname
<span class="indicator">{indicator("first")}</span> <span class="indicator">{indicator("first")}</span>
</th> </th>
<th> <th> </th>
</th> </tr>
</thead>
<tbody>
{#each rowsSorted as row}
<tr class="even:bg-indigo-600">
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
<td class="whitespace-nowrap pr-5">{row.last}</td>
<td class="whitespace-nowrap pr-5">{row.first}</td>
<td class="pr-5"
><button
onclick={() => {
onEdit && onEdit(row.id, row.first, row.last);
}}
class="cursor-pointer">🔧</button
></td
>
</tr> </tr>
</thead> {/each}
<tbody> </tbody>
{#each rowsSorted as row} </table>
<tr class="even:bg-indigo-600"> </div>
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
<td class="whitespace-nowrap pr-5">{row.last}</td>
<td class="whitespace-nowrap pr-5">{row.first}</td>
<td class="pr-5" ><button onclick={()=>{
onEdit && onEdit(row.id,row.first,row.last);
}} class="cursor-pointer">🔧</button></td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<style lang="css" scoped> <style lang="css" scoped>
@reference "../app.css"; @reference "../app.css";
.indicator { .indicator {
@apply ml-1 w-4 inline-block; @apply ml-1 w-4 inline-block;
} }

103
web/src/lib/csv.ts Normal file
View File

@@ -0,0 +1,103 @@
interface CSVOptions {
delimiter?: string;
headerOrder?: string[];
eol?: string;
includeBOM?: boolean;
nullString?: string;
}
type RowObject = Record<string, any>;
type InputRows = RowObject[];
export function generateCSVString(input: InputRows, opts: CSVOptions = {}): string {
const {
delimiter = ";",
headerOrder,
eol = "\r\n",
includeBOM = true,
nullString = "",
} = opts;
// Check if the value need quoting
// Usually not needed in our use case but still in case
const needsQuoting = (s: string) =>
s.includes(delimiter) || s.includes('"') || s.includes("\n") || s.includes("\r") || /^\s|\s$/.test(s);
// Transform the value of a cell in a SAFE string
const escapeCell = (raw: any): string => {
if (raw === null || raw === undefined) return nullString;
let s = stringify(raw);
// Replace quotes
if (s.includes('"')) s = s.replace(/"/g, '""');
return needsQuoting(s) ? `"${s}"` : s;
};
// Transform the value of a cell into a string
function stringify(v: any): string {
if (v === null || v === undefined) return nullString;
if (v instanceof Date) return v.toLocaleDateString();
if (typeof v === "boolean") return v ? "X" : "";
if (typeof v === "object") {
// CHeck if array and join with "|"
if (Array.isArray(v)) return v.map(item => (item === null || item === undefined ? nullString : String(item))).join("|");
// If all fails parse it via json
// Should also not happen in our use case
try { return JSON.stringify(v); } catch { return String(v); }
}
return String(v);
}
const objs = (input as RowObject[]) || [];
// Derive headers in the order keys are first encountered (fixed: no reduce)
const seen = new Set<string>();
const derivedOrder: string[] = [];
for (const obj of objs) {
if (!obj || typeof obj !== "object") continue;
for (const k of Object.keys(obj)) {
if (!seen.has(k)) {
seen.add(k);
derivedOrder.push(k);
}
}
}
// Apply headerOrder if provided: put listed columns first (in the order provided),
// then append the remaining derived headers in their derived order.
let finalHeaders = derivedOrder.slice();
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
const headSet = new Set(headerOrder);
const first = headerOrder.filter(h => seen.has(h)); // keep only headers that actually exist
const rest = derivedOrder.filter(h => !headSet.has(h));
finalHeaders = first.concat(rest);
}
const rowsOut: string[] = [];
if (finalHeaders.length > 0) {
rowsOut.push(finalHeaders.map(h => escapeCell(h)).join(delimiter));
}
// Parse every row
for (const obj of objs) {
// Check if obj is null or somthing else we can't convert
if (!obj || typeof obj !== "object") {
// produce empty row with same number of columns
rowsOut.push(finalHeaders.map(() => escapeCell(null)).join(delimiter));
continue;
}
// For every header check if a value on our row exist and print it
const row = finalHeaders.map(col => escapeCell(col in obj ? (obj as any)[col] : null)).join(delimiter);
rowsOut.push(row);
}
return (includeBOM ? "\uFEFF" : "") + rowsOut.join(eol);
}

Some files were not shown because too many files have changed in this diff Show More