mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-04-30 18:49:09 +00:00
Compare commits
122 Commits
v1.0
...
02798d90c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 02798d90c4 | |||
| 07d51264f9 | |||
| 6cc6df6298 | |||
| 7d78aa8c4c | |||
|
|
fb8d98da28 | ||
| 8100748f8a | |||
|
|
f22eb91b67 | ||
|
|
a04400a3a0 | ||
|
|
6f7561a295 | ||
|
|
279e2f7454 | ||
|
|
610840311f | ||
| 56d2dbfa7c | |||
| 35f12a4c45 | |||
| 7346fb42bd | |||
| c0bf8399a3 | |||
| 9852534dc6 | |||
| d63e9e964d | |||
| 16ea1db55f | |||
| a0ed04a560 | |||
|
|
4e988e8f01 | ||
| 009f6cbb2e | |||
|
|
967da9fc30 | ||
| 00cb7efedb | |||
| ebbec7885e | |||
| 7ecd2052d8 | |||
| 96512c8a12 | |||
| c3eaff03d9 | |||
| 4bf89626b9 | |||
| 7c0c0699b5 | |||
| 1ea70e4993 | |||
| 770dca5b0f | |||
| 2e75ba2908 | |||
| 141c1aa9cb | |||
|
|
4abbd844d2 | ||
| 7346b47816 | |||
| cd63dd3ee4 | |||
| f5d4ae1e05 | |||
| bd3f6731fd | |||
| 6fdcf7679f | |||
|
|
c4d6ed45f1 | ||
|
|
41adf7353d | ||
| 6421074931 | |||
| a34dc18381 | |||
| 252e63c607 | |||
| 99848f0e6d | |||
| f46cdc4d29 | |||
| a8d64f6af5 | |||
| 8fb6bac651 | |||
| 7eb18376e1 | |||
| b8bba28bda | |||
| 5c0ad18b94 | |||
| 75130e2d20 | |||
| 6b2c56f3e5 | |||
| 2980d34394 | |||
| 9b926f7a34 | |||
| f1b471c6d8 | |||
| 030a372949 | |||
| 211961a770 | |||
| dfe5197ab8 | |||
| 0f5ca88ae4 | |||
| 9dd2f88cbc | |||
| aa91d69f0b | |||
| b13ae76bc5 | |||
| 4a9ff47dcc | |||
| 92c7fec283 | |||
| 082f1faba9 | |||
| 8cbdf834a1 | |||
| 3eefcdd35a | |||
| 4531ef72ae | |||
|
|
2078a3bab0 | ||
|
|
7e59d836a1 | ||
|
|
09f21403ec | ||
|
|
db7e22f45d | ||
|
|
c91f290c31 | ||
| becdd43738 | |||
|
|
453b653ac5 | ||
| cc3605b75d | |||
| 57ccc0cc8b | |||
|
|
d90376121e | ||
| 2a81499f7c | |||
|
|
4ff8ff0f77 | ||
|
|
781d27ae48 | ||
|
|
671fb0cbdd | ||
|
|
99d9cf306e | ||
|
|
b551f4521f | ||
|
|
adcbe87bd7 | ||
|
|
d96b3ed11a | ||
|
|
dcb4b14854 | ||
| fe90ca9aa9 | |||
|
|
b031a47e85 | ||
|
|
bf59b6eed3 | ||
|
|
59d87eb199 | ||
|
|
630fc4aaf9 | ||
| 21480cef4f | |||
|
|
fabb14de86 | ||
|
|
6a2d448f86 | ||
|
|
fc7bd8b089 | ||
|
|
3117c55b1c | ||
|
|
593d98df74 | ||
|
|
fa6d1f024c | ||
|
|
36dc52f464 | ||
|
|
6831d7776c | ||
| a015d6b983 | |||
|
|
1ae5250449 | ||
|
|
2f502e908e | ||
|
|
5950279dc4 | ||
| fe6540ca3d | |||
|
|
161ebf9bd2 | ||
|
|
c1b54920ff | ||
|
|
5a2beb1fb3 | ||
|
|
d5c20bf348 | ||
|
|
49027fed99 | ||
|
|
4dda9548d3 | ||
|
|
46e207bd2a | ||
|
|
8cb118e0ee | ||
|
|
9b4df77112 | ||
| 23bb1126a6 | |||
| a97e9c8080 | |||
| 4b39529a65 | |||
| c91d2f070f | |||
| 2e6094ea11 | |||
| 43e964b5a0 |
18
.cargo/config.toml
Normal file
18
.cargo/config.toml
Normal 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
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
/target
|
||||
/build
|
||||
|
||||
pcb/fw-anwesenheit-backups
|
||||
Binary file not shown.
Binary file not shown.
BIN
3d_print/LWL.3mf
BIN
3d_print/LWL.3mf
Binary file not shown.
Binary file not shown.
BIN
3d_print/enclouseure_printfile.3mf
Normal file
BIN
3d_print/enclouseure_printfile.3mf
Normal file
Binary file not shown.
BIN
3d_print/enclousure_top_ffw.3mf
Normal file
BIN
3d_print/enclousure_top_ffw.3mf
Normal file
Binary file not shown.
BIN
3d_print/printfiles/fw-anwesenheit-enclousure-top.3mf
Normal file
BIN
3d_print/printfiles/fw-anwesenheit-enclousure-top.3mf
Normal file
Binary file not shown.
2695
Cargo.lock
generated
2695
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
77
Cargo.toml
77
Cargo.toml
@@ -3,24 +3,65 @@ name = "fw-anwesenheit"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
mock_pi = [] # Enable mocking of the rpi hardware
|
||||
[[bin]]
|
||||
name = "fw-anwesenheit"
|
||||
path = "./src/main.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.40", features = ["serde"] }
|
||||
gpio = "0.4.1"
|
||||
regex = "1.11.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
rust-embed = "8.7.0"
|
||||
log = "0.4.27"
|
||||
simplelog = "0.12.2"
|
||||
rppal = { version = "0.22.1", features = ["hal"] }
|
||||
smart-leds = "0.3"
|
||||
ws2812-spi = "0.3"
|
||||
rgb = "0.8.50"
|
||||
anyhow = "1.0.98"
|
||||
esp-bootloader-esp-idf = "0.1.0"
|
||||
esp-hal = { version = "1.0.0-rc.1", features = ["esp32c6", "unstable"] }
|
||||
esp-alloc = "0.9.0"
|
||||
esp-println = { version = "0.16.0", features = ["esp32c6", "log-04"] }
|
||||
esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "log-04", "smoltcp","unstable"]}
|
||||
esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
|
||||
|
||||
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
|
||||
|
||||
61
Makefile
61
Makefile
@@ -1,61 +0,0 @@
|
||||
PACKAGE_NAME := fwa
|
||||
VERSION := 1.0
|
||||
ARCH := armhf
|
||||
BUILD_DIR := build
|
||||
DEB_DIR := $(BUILD_DIR)/$(PACKAGE_NAME)-$(VERSION)
|
||||
BIN_DIR := $(DEB_DIR)/usr/local/bin
|
||||
SERVICE_DIR := $(DEB_DIR)/lib/systemd/system
|
||||
CONFIG_DIR := $(DEB_DIR)/etc
|
||||
PM3_DIR := $(DEB_DIR)/usr/share/pm3
|
||||
|
||||
.PHONY: all build clean package prepare_package
|
||||
|
||||
all: package
|
||||
|
||||
build: $(BUILD_DIR)/fwa
|
||||
|
||||
$(BUILD_DIR)/fwa: web/dist
|
||||
cross build --release --target arm-unknown-linux-gnueabihf
|
||||
cp ./target/arm-unknown-linux-gnueabihf/release/fw-anwesenheit $@
|
||||
|
||||
prepare_package: $(DEB_DIR)/DEBIAN $(BIN_DIR)/fwa
|
||||
mkdir -p $(SERVICE_DIR)
|
||||
cp ./service/fwa.service $(SERVICE_DIR)/
|
||||
cp ./service/fwa-fail.service $(SERVICE_DIR)/
|
||||
|
||||
mkdir -p $(CONFIG_DIR)
|
||||
cp ./service/fwa.env $(CONFIG_DIR)/
|
||||
|
||||
mkdir -p $(PM3_DIR)
|
||||
cp -r ./pre-compiled/* $(PM3_DIR)/
|
||||
|
||||
mkdir -p $(DEB_DIR)/var/lib/fwa/
|
||||
|
||||
$(BIN_DIR)/fwa: $(BUILD_DIR)/fwa
|
||||
mkdir -p $(BIN_DIR)
|
||||
cp $< $@
|
||||
|
||||
|
||||
$(DEB_DIR)/DEBIAN:
|
||||
mkdir -p $(DEB_DIR)/DEBIAN
|
||||
echo "Package: $(PACKAGE_NAME)" > $(DEB_DIR)/DEBIAN/control
|
||||
echo "Version: $(VERSION)" >> $(DEB_DIR)/DEBIAN/control
|
||||
echo "Section: utils" >> $(DEB_DIR)/DEBIAN/control
|
||||
echo "Priority: optional" >> $(DEB_DIR)/DEBIAN/control
|
||||
echo "Architecture: $(ARCH)" >> $(DEB_DIR)/DEBIAN/control
|
||||
echo "Depends: libc6 (>= 2.28)" >> $(DEB_DIR)/DEBIAN/control
|
||||
echo "Maintainer: Niklas Kapelle <niklas@kapelle.org>" >> $(DEB_DIR)/DEBIAN/control
|
||||
echo "Description: Feuerwehr anwesenheit" >> $(DEB_DIR)/DEBIAN/control
|
||||
|
||||
echo "/etc/fwa.env" > $(DEB_DIR)/DEBIAN/conffiles
|
||||
|
||||
web/dist:
|
||||
(cd web && npm run build)
|
||||
|
||||
package: prepare_package
|
||||
dpkg-deb --build $(DEB_DIR)
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
rm -rf web/dist
|
||||
rm -rf $(BUILD_DIR)
|
||||
30
README.md
30
README.md
@@ -1,32 +1,6 @@
|
||||
# fw-anwesenheit
|
||||
|
||||
# Setup
|
||||

|
||||
|
||||
In order to use the LED we need to enable the SPI interface on the Rpi.
|
||||
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
66
build.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
fn main() {
|
||||
linker_be_nice();
|
||||
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
||||
println!("cargo:rustc-link-arg=-Tlinkall.x");
|
||||
save_build_time();
|
||||
}
|
||||
|
||||
fn save_build_time() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("build_time.rs");
|
||||
let system_time = std::time::SystemTime::now();
|
||||
let unix_time = system_time
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
println!("cargo:rustc-env=BUILD_TIME={}", unix_time);
|
||||
let content = format!(
|
||||
"/// compile time as UNIX-Timestamp (seconds since 1970-01-01)
|
||||
pub const BUILD_UNIX_TIME: u64 = {};",
|
||||
unix_time
|
||||
);
|
||||
let mut f = File::create(dest_path).unwrap();
|
||||
f.write_all(content.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
fn linker_be_nice() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 {
|
||||
let kind = &args[1];
|
||||
let what = &args[2];
|
||||
|
||||
match kind.as_str() {
|
||||
"undefined-symbol" => match what.as_str() {
|
||||
"_defmt_timestamp" => {
|
||||
eprintln!();
|
||||
eprintln!(
|
||||
"💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"
|
||||
);
|
||||
eprintln!();
|
||||
}
|
||||
"_stack_start" => {
|
||||
eprintln!();
|
||||
eprintln!("💡 Is the linker script `linkall.x` missing?");
|
||||
eprintln!();
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
// we don't have anything helpful for "missing-lib" yet
|
||||
_ => {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
println!(
|
||||
"cargo:rustc-link-arg=--error-handling-script={}",
|
||||
std::env::current_exe().unwrap().display()
|
||||
);
|
||||
}
|
||||
24
buzzer.py
24
buzzer.py
@@ -1,24 +0,0 @@
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
BUZZER_PIN = 26
|
||||
|
||||
def beep(frequency, duration):
|
||||
pwm = GPIO.PWM(BUZZER_PIN, frequency)
|
||||
pwm.start(50) # 50 % duty cycle
|
||||
time.sleep(duration)
|
||||
pwm.stop()
|
||||
|
||||
def main():
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(BUZZER_PIN, GPIO.OUT)
|
||||
|
||||
try:
|
||||
beep(523, 0.3) # C5
|
||||
beep(659, 0.3) # E5
|
||||
beep(784, 0.3) # G5
|
||||
finally:
|
||||
GPIO.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,34 +0,0 @@
|
||||
@startuml
|
||||
actor user
|
||||
|
||||
|
||||
main -> pm3 :start
|
||||
loop
|
||||
pm3 -> pm3 : look for HITAG
|
||||
end
|
||||
|
||||
user -> pm3 :show HITAG
|
||||
|
||||
pm3 -> parser : parse ID
|
||||
parser -> pm3 : return ID
|
||||
|
||||
pm3 --> main : send ID
|
||||
|
||||
loop
|
||||
main -> main : look for IDs
|
||||
end
|
||||
|
||||
main -> idstore : send ID
|
||||
idstore -> System : ask for day
|
||||
alt
|
||||
System -> idstore : return attendence_day
|
||||
else
|
||||
create collections attendence_day
|
||||
idstore -> attendence_day : create new attendence_day
|
||||
|
||||
end
|
||||
|
||||
idstore -> attendence_day : add ID
|
||||
attendence_day -> system : save json
|
||||
|
||||
@enduml
|
||||
61
em4100_write.lua
Normal file
61
em4100_write.lua
Normal 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
4709
pcb/bom/ibom.html
Normal file
File diff suppressed because one or more lines are too long
1
pcb/fabrication-toolkit-options.json
Normal file
1
pcb/fabrication-toolkit-options.json
Normal 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
105757
pcb/fp-info-cache
Normal file
File diff suppressed because it is too large
Load Diff
BIN
pcb/fw-anwesenheit-backups/fw-anwesenheit-2025-09-21_232810.zip
Normal file
BIN
pcb/fw-anwesenheit-backups/fw-anwesenheit-2025-09-21_232810.zip
Normal file
Binary file not shown.
27885
pcb/fw-anwesenheit.kicad_pcb
Normal file
27885
pcb/fw-anwesenheit.kicad_pcb
Normal file
File diff suppressed because it is too large
Load Diff
147
pcb/fw-anwesenheit.kicad_prl
Normal file
147
pcb/fw-anwesenheit.kicad_prl
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
633
pcb/fw-anwesenheit.kicad_pro
Normal file
633
pcb/fw-anwesenheit.kicad_pro
Normal 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
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
122063
pcb/fw-anwesenheit.step
Normal file
File diff suppressed because it is too large
Load Diff
374733
pcb/fw-anwesenheit.stl
Normal file
374733
pcb/fw-anwesenheit.stl
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
2
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pcb
Normal file
2
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pcb
Normal file
@@ -0,0 +1,2 @@
|
||||
(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0")
|
||||
)
|
||||
98
pcb/fw-anwesenheit/fw-anwesenheit.kicad_prl
Normal file
98
pcb/fw-anwesenheit/fw-anwesenheit.kicad_prl
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
413
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pro
Normal file
413
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pro
Normal 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": {}
|
||||
}
|
||||
14
pcb/fw-anwesenheit/fw-anwesenheit.kicad_sch
Normal file
14
pcb/fw-anwesenheit/fw-anwesenheit.kicad_sch
Normal 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)
|
||||
)
|
||||
BIN
pcb/production/backups/fw-anwesenheit_2025-09-01_15-16-16.zip
Normal file
BIN
pcb/production/backups/fw-anwesenheit_2025-09-01_15-16-16.zip
Normal file
Binary file not shown.
BIN
pcb/production/backups/fw-anwesenheit_2025-09-01_15-58-06.zip
Normal file
BIN
pcb/production/backups/fw-anwesenheit_2025-09-01_15-58-06.zip
Normal file
Binary file not shown.
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_03-36-00.zip
Normal file
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_03-36-00.zip
Normal file
Binary file not shown.
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_23-31-28.zip
Normal file
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_23-31-28.zip
Normal file
Binary file not shown.
35
pcb/production/bom.csv
Normal file
35
pcb/production/bom.csv
Normal 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,
|
||||
|
47
pcb/production/designators.csv
Normal file
47
pcb/production/designators.csv
Normal 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
|
||||
|
BIN
pcb/production/fw-anwesenheit.zip
Normal file
BIN
pcb/production/fw-anwesenheit.zip
Normal file
Binary file not shown.
354
pcb/production/netlist.ipc
Normal file
354
pcb/production/netlist.ipc
Normal 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
|
||||
29
pcb/production/positions.csv
Normal file
29
pcb/production/positions.csv
Normal 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
|
||||
|
23
pm3_mock.sh
23
pm3_mock.sh
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
generate_random_hex() {
|
||||
openssl rand -hex 4 | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
output_hotspot_id() {
|
||||
echo "[+] UID.... C1532B57"
|
||||
}
|
||||
|
||||
trap 'output_hotspot_id' SIGUSR1
|
||||
|
||||
echo "Proxmark 3 mock script"
|
||||
echo "Outputs a random ID every 1 to 5 seconds"
|
||||
|
||||
while true; do
|
||||
random_id=$(generate_random_hex)
|
||||
echo "[+] UID.... $random_id"
|
||||
if (( RANDOM % 2 == 0 )); then
|
||||
echo "[+] UID.... $random_id"
|
||||
fi
|
||||
sleep "$((RANDOM % 5 + 1))"
|
||||
done
|
||||
Binary file not shown.
Binary file not shown.
550
pre-compiled/pm3
550
pre-compiled/pm3
@@ -1,550 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Usage: run option -h to get help
|
||||
|
||||
# BT auto detection
|
||||
# Shall we look for white HC-06-USB dongle ?
|
||||
FINDBTDONGLE=true
|
||||
# Shall we look for rfcomm interface ?
|
||||
FINDBTRFCOMM=true
|
||||
# Shall we look for registered BT device ? (Linux only)
|
||||
FINDBTDIRECT=true
|
||||
|
||||
PM3PATH=$(dirname "$0")
|
||||
EVALENV=""
|
||||
FULLIMAGE="fullimage.elf"
|
||||
BOOTIMAGE="bootrom.elf"
|
||||
|
||||
#Skip check if --list is used
|
||||
if [ ! "$1" == "--list" ]; then
|
||||
# try pm3 dirs in current repo workdir
|
||||
if [ -d "$PM3PATH/client/" ]; then
|
||||
if [ -x "$PM3PATH/client/proxmark3" ]; then
|
||||
CLIENT="$PM3PATH/client/proxmark3"
|
||||
elif [ -x "$PM3PATH/client/build/proxmark3" ]; then
|
||||
CLIENT="$PM3PATH/client/build/proxmark3"
|
||||
else
|
||||
echo >&2 "[!!] In devel workdir but no executable found, did you compile it?"
|
||||
exit 1
|
||||
fi
|
||||
# try install dir
|
||||
elif [ -x "$PM3PATH/proxmark3" ]; then
|
||||
CLIENT="$PM3PATH/proxmark3"
|
||||
else
|
||||
# hope it's installed somehow, still not sure where fw images and pm3.py are...
|
||||
CLIENT="proxmark3"
|
||||
fi
|
||||
fi
|
||||
|
||||
# LeakSanitizer suppressions
|
||||
if [ -e .lsan_suppressions ]; then
|
||||
EVALENV+=" LSAN_OPTIONS=suppressions=.lsan_suppressions"
|
||||
fi
|
||||
if [ "$EVALENV" != "" ]; then
|
||||
EVALENV="export $EVALENV"
|
||||
fi
|
||||
PM3LIST=()
|
||||
SHOWLIST=false
|
||||
|
||||
function get_pm3_list_Linux {
|
||||
N=$1
|
||||
PM3LIST=()
|
||||
if [ ! -c "/dev/tty0" ]; then
|
||||
echo >&2 "[!!] Script cannot access /dev/ttyXXX files, insufficient privileges"
|
||||
exit 1
|
||||
fi
|
||||
for DEV in $(find /dev/ttyACM* 2>/dev/null); do
|
||||
if command -v udevadm >/dev/null; then
|
||||
# WSL1 detection
|
||||
if udevadm info -q property -n "$DEV" | grep -q "ID_VENDOR=proxmark.org"; then
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# WSL2 with usbipd detection - doesn't report same things as WSL1
|
||||
if grep -q "proxmark.org" "/sys/class/tty/${DEV#/dev/}/../../../manufacturer" 2>/dev/null; then
|
||||
if echo "${PM3LIST[*]}" | grep -qv "${DEV}"; then
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if $FINDBTDONGLE; then
|
||||
# check if the HC-06-USB white dongle is present (still, that doesn't tell us if it's paired with a Proxmark3...)
|
||||
for DEV in $(find /dev/ttyUSB* 2>/dev/null); do
|
||||
if command -v udevadm >/dev/null; then
|
||||
if udevadm info -q property -n "$DEV" | grep -q "ID_MODEL=CP2104_USB_to_UART_Bridge_Controller"; then
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if grep -q "DRIVER=cp210x" "/sys/class/tty/${DEV#/dev/}/../../uevent" 2>/dev/null; then
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if $FINDBTRFCOMM; then
|
||||
# check if the MAC of a Proxmark3 was bound to a local rfcomm interface
|
||||
# (on OSes without deprecated rfcomm and hcitool, the loop will be simply skipped)
|
||||
for DEVMAC in $(rfcomm -a 2>/dev/null | grep " 20:19:0[45]" | sed 's/^\(.*\): \([0-9:]*\) .*/\1@\2/'); do
|
||||
DEV=${DEVMAC/@*/}
|
||||
MAC=${DEVMAC/*@/}
|
||||
# check which are Proxmark3 and, side-effect, if they're actually present
|
||||
if hcitool name "$MAC" | grep -q "PM3"; then
|
||||
PM3LIST+=("/dev/$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if $FINDBTDIRECT; then
|
||||
# check if the MAC of a Proxmark3 was registered in the known devices
|
||||
for MAC in $(dbus-send --system --print-reply --type=method_call --dest='org.bluez' '/' org.freedesktop.DBus.ObjectManager.GetManagedObjects 2>/dev/null|\
|
||||
awk '/"Address"/{getline;gsub(/"/,"",$3);a=$3}/Name/{getline;if (/PM3_RDV4/ || /Proxmark3 SE/) print a}'); do
|
||||
PM3LIST+=("bt:$MAC")
|
||||
done
|
||||
# we don't probe the device so there is no guarantee the device is actually present
|
||||
fi
|
||||
}
|
||||
|
||||
function get_pm3_list_macOS {
|
||||
N=$1
|
||||
PM3LIST=()
|
||||
for DEV in $(ioreg -r -c "IOUSBHostDevice" -l | awk -F '"' '
|
||||
$2=="USB Vendor Name"{b=($4=="proxmark.org")}
|
||||
b==1 && $2=="IODialinDevice"{print $4}'); do
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function get_pm3_list_Windows {
|
||||
N=$1
|
||||
PM3LIST=()
|
||||
|
||||
# Normal SERIAL PORTS (COM)
|
||||
for DEV in $(wmic /locale:ms_409 path Win32_SerialPort Where "PNPDeviceID LIKE '%VID_9AC4&PID_4B8F%' Or PNPDeviceID LIKE '%VID_2D2D&PID_504D%'" Get DeviceID 2>/dev/null | awk -b '/^COM/{print $1}'); do
|
||||
DEV=${DEV/ */}
|
||||
#prevent soft bricking when using pm3-flash-all on an outdated bootloader
|
||||
if [ $(basename -- "$0") = "pm3-flash-all" ]; then
|
||||
line=$(wmic /locale:ms_409 path Win32_SerialPort Where "DeviceID='$DEV'" Get PNPDeviceID 2>/dev/null | awk -b '/^USB/{print $1}');
|
||||
if [[ ! $line =~ ^"USB\VID_9AC4&PID_4B8F\ICEMAN" ]]; then
|
||||
echo -e "\033[0;31m[!] Using pm3-flash-all on an oudated bootloader, use pm3-flash-bootrom first!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
#BT direct SERIAL PORTS (COM)
|
||||
if $FINDBTRFCOMM; then
|
||||
for DEV in $(wmic /locale:ms_409 path Win32_PnPEntity Where "Caption LIKE '%Bluetooth%(COM%'" Get Name 2> /dev/null | awk -b 'match($0,/(COM[0-9]+)/,m){print m[1]}'); do
|
||||
DEV=${DEV/ */}
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
#white BT dongle SERIAL PORTS (COM)
|
||||
if $FINDBTDONGLE; then
|
||||
for DEV in $(wmic /locale:ms_409 path Win32_SerialPort Where "PNPDeviceID LIKE '%VID_10C4&PID_EA60%'" Get DeviceID 2>/dev/null | awk -b '/^COM/{print $1}'); do
|
||||
DEV=${DEV/ */}
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
function get_pm3_list_WSL {
|
||||
N=$1
|
||||
PM3LIST=()
|
||||
|
||||
# Normal SERIAL PORTS (COM)
|
||||
for DEV in $($PSHEXE -command "Get-CimInstance -ClassName Win32_serialport | Where-Object {\$_.PNPDeviceID -like '*VID_9AC4&PID_4B8F*' -or \$_.PNPDeviceID -like '*VID_2D2D&PID_504D*'} | Select -expandproperty DeviceID" 2>/dev/null); do
|
||||
DEV=$(echo $DEV | tr -dc '[:print:]')
|
||||
_comport=$DEV
|
||||
DEV=$(echo $DEV | sed -nr 's#^COM([0-9]+)\b#/dev/ttyS\1#p')
|
||||
# ttyS counterpart takes some more time to appear
|
||||
if [ -e "$DEV" ]; then
|
||||
|
||||
#prevent soft bricking when using pm3-flash-all on an outdated bootloader
|
||||
if [ $(basename -- "$0") = "pm3-flash-all" ]; then
|
||||
line=$($PSHEXE -command "Get-CimInstance -ClassName Win32_serialport | Where-Object {\$_.DeviceID -eq '$_comport'} | Select -expandproperty PNPDeviceID" 2>/dev/null | tr -dc '[:print:]');
|
||||
if [[ ! $line =~ ^"USB\VID_9AC4&PID_4B8F\ICEMAN" ]]; then
|
||||
echo -e "\033[0;31m[!] Using pm3-flash-all on an oudated bootloader, use pm3-flash-bootrom first!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ! -w "$DEV" ]; then
|
||||
echo "[!] Let's give users read/write access to $DEV"
|
||||
sudo chmod 666 "$DEV"
|
||||
fi
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
#BT direct SERIAL PORTS (COM)
|
||||
if $FINDBTRFCOMM; then
|
||||
for DEV in $($PSHEXE -command "Get-CimInstance -ClassName Win32_PnPEntity | Where-Object Caption -like 'Standard Serial over Bluetooth link (COM*' | Select Name" 2> /dev/null | sed -nr 's#.*\bCOM([0-9]+)\b.*#/dev/ttyS\1#p'); do
|
||||
# ttyS counterpart takes some more time to appear
|
||||
if [ -e "$DEV" ]; then
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ! -w "$DEV" ]; then
|
||||
echo "[!] Let's give users read/write access to $DEV"
|
||||
sudo chmod 666 "$DEV"
|
||||
fi
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
done
|
||||
fi
|
||||
|
||||
#white BT dongle SERIAL PORTS (COM)
|
||||
if $FINDBTDONGLE; then
|
||||
for DEV in $($PSHEXE -command "Get-CimInstance -ClassName Win32_serialport | Where-Object PNPDeviceID -like '*VID_10C4&PID_EA60*' | Select DeviceID" 2>/dev/null | sed -nr 's#^COM([0-9]+)\b#/dev/ttyS\1#p'); do
|
||||
# ttyS counterpart takes some more time to appear
|
||||
if [ -e "$DEV" ]; then
|
||||
PM3LIST+=("$DEV")
|
||||
if [ ! -w "$DEV" ]; then
|
||||
echo "[!] Let's give users read/write access to $DEV"
|
||||
sudo chmod 666 "$DEV"
|
||||
fi
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
SCRIPT=$(basename -- "$0")
|
||||
|
||||
if [ "$SCRIPT" = "pm3" ]; then
|
||||
CMD() { eval "$EVALENV"; $CLIENT "$@"; }
|
||||
HELP() {
|
||||
cat << EOF
|
||||
|
||||
Quick helper script for proxmark3 client when working with a Proxmark3 device
|
||||
|
||||
Description:
|
||||
The usage is the same as for the proxmark3 client, with the following differences:
|
||||
* the correct port name will be automatically guessed;
|
||||
* the script will wait for a Proxmark3 to be connected (same as option -w of the client).
|
||||
You can also specify a first option -n N to access the Nth Proxmark3 connected.
|
||||
To see a list of available ports, use --list.
|
||||
|
||||
Usage:
|
||||
$SCRIPT [-n <N>] [<any other proxmark3 client option>]
|
||||
$SCRIPT [--list] [-h|--help] [-hh|--helpclient]
|
||||
$SCRIPT [-o|--offline]
|
||||
|
||||
|
||||
Arguments:
|
||||
-h/--help this help
|
||||
-hh/--helpclient proxmark3 client help (the script will forward these options)
|
||||
--list list all detected com ports
|
||||
-n <N> connect device referred to the N:th number on the --list output
|
||||
-o/--offline shortcut to use directly the proxmark3 client without guessing ports
|
||||
|
||||
Samples:
|
||||
./$SCRIPT -- Auto detect/ select com port in the following order BT, USB/CDC, BT DONGLE
|
||||
./$SCRIPT -p /dev/ttyACM0 -- connect to port /dev/ttyACM0
|
||||
./$SCRIPT -n 2 -- use second item from the --list output
|
||||
./$SCRIPT -c 'lf search' -i -- run command and stay in client once completed
|
||||
|
||||
|
||||
EOF
|
||||
}
|
||||
elif [ "$SCRIPT" = "pm3-flash" ]; then
|
||||
FINDBTDONGLE=false
|
||||
FINDBTRFCOMM=false
|
||||
FINDBTDIRECT=false
|
||||
CMD() {
|
||||
ARGS=("--port" "$1" "--flash")
|
||||
shift;
|
||||
while [ "$1" != "" ]; do
|
||||
if [ "$1" == "-b" ]; then
|
||||
ARGS+=("--unlock-bootloader")
|
||||
elif [ "$1" == "--force" ]; then
|
||||
ARGS+=("--force")
|
||||
else
|
||||
ARGS+=("--image" "$1")
|
||||
fi
|
||||
shift;
|
||||
done
|
||||
$CLIENT "${ARGS[@]}";
|
||||
}
|
||||
HELP() {
|
||||
cat << EOF
|
||||
Quick helper script for flashing a Proxmark3 device via USB
|
||||
|
||||
Description:
|
||||
The usage is similar to the old proxmark3-flasher binary, except that the correct port name will be automatically guessed.
|
||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
||||
To see a list of available ports, use --list.
|
||||
|
||||
Usage:
|
||||
$SCRIPT [-n <N>] [-b] image.elf [image.elf...]
|
||||
$SCRIPT --list
|
||||
|
||||
Options:
|
||||
-b Enable flashing of bootloader area (DANGEROUS)
|
||||
|
||||
Example:
|
||||
$SCRIPT -b bootrom.elf fullimage.elf
|
||||
EOF
|
||||
}
|
||||
elif [ "$SCRIPT" = "pm3-flash-all" ]; then
|
||||
FINDBTDONGLE=false
|
||||
FINDBTRFCOMM=false
|
||||
FINDBTDIRECT=false
|
||||
|
||||
|
||||
CMD() {
|
||||
ARGS=("--port" "$1" "--flash" "--unlock-bootloader" "--image" "$BOOTIMAGE" "--image" "$FULLIMAGE")
|
||||
shift;
|
||||
while [ "$1" != "" ]; do
|
||||
if [ "$1" == "--force" ]; then
|
||||
ARGS+=("--force")
|
||||
fi
|
||||
shift;
|
||||
done
|
||||
$CLIENT "${ARGS[@]}";
|
||||
}
|
||||
HELP() {
|
||||
cat << EOF
|
||||
Quick helper script for flashing a Proxmark3 device via USB
|
||||
|
||||
Description:
|
||||
The correct port name will be automatically guessed and the stock bootloader and firmware image will be flashed.
|
||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
||||
To see a list of available ports, use --list.
|
||||
|
||||
Usage:
|
||||
$SCRIPT [-n <N>]
|
||||
$SCRIPT --list
|
||||
EOF
|
||||
}
|
||||
elif [ "$SCRIPT" = "pm3-flash-fullimage" ]; then
|
||||
FINDBTDONGLE=false
|
||||
FINDBTRFCOMM=false
|
||||
FINDBTDIRECT=false
|
||||
CMD() {
|
||||
ARGS=("--port" "$1" "--flash" "--image" "$FULLIMAGE")
|
||||
shift;
|
||||
while [ "$1" != "" ]; do
|
||||
if [ "$1" == "--force" ]; then
|
||||
ARGS+=("--force")
|
||||
fi
|
||||
shift;
|
||||
done
|
||||
$CLIENT "${ARGS[@]}";
|
||||
}
|
||||
HELP() {
|
||||
cat << EOF
|
||||
Quick helper script for flashing a Proxmark3 device via USB
|
||||
|
||||
Description:
|
||||
The correct port name will be automatically guessed and the stock firmware image will be flashed.
|
||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
||||
To see a list of available ports, use --list.
|
||||
|
||||
Usage:
|
||||
$SCRIPT [-n <N>]
|
||||
$SCRIPT --list
|
||||
EOF
|
||||
}
|
||||
elif [ "$SCRIPT" = "pm3-flash-bootrom" ]; then
|
||||
FINDBTDONGLE=false
|
||||
FINDBTRFCOMM=false
|
||||
FINDBTDIRECT=false
|
||||
CMD() {
|
||||
ARGS=("--port" "$1" "--flash" "--unlock-bootloader" "--image" "$BOOTIMAGE")
|
||||
shift;
|
||||
while [ "$1" != "" ]; do
|
||||
if [ "$1" == "--force" ]; then
|
||||
ARGS+=("--force")
|
||||
fi
|
||||
shift;
|
||||
done
|
||||
$CLIENT "${ARGS[@]}";
|
||||
}
|
||||
HELP() {
|
||||
cat << EOF
|
||||
Quick helper script for flashing a Proxmark3 device via USB
|
||||
|
||||
Description:
|
||||
The correct port name will be automatically guessed and the stock bootloader will be flashed.
|
||||
You can also specify a first option -n N to access the Nth Proxmark3 connected on USB.
|
||||
If this doesn't work, you'll have to use manually the proxmark3 client, see "$CLIENT -h".
|
||||
To see a list of available ports, use --list.
|
||||
|
||||
Usage:
|
||||
$SCRIPT [-n <N>]
|
||||
$SCRIPT --list
|
||||
EOF
|
||||
}
|
||||
else
|
||||
echo >&2 "[!!] Script ran under unknown name, abort: $SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# priority to the help options
|
||||
for ARG; do
|
||||
if [ "$ARG" == "-h" ] || [ "$ARG" == "--help" ]; then
|
||||
HELP
|
||||
exit 0
|
||||
fi
|
||||
if [ "$ARG" == "-hh" ] || [ "$ARG" == "--helpclient" ]; then
|
||||
CMD "-h"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
# if offline, bypass the script and forward all other args
|
||||
for ARG; do
|
||||
shift
|
||||
if [ "$ARG" == "-o" ] || [ "$ARG" == "--offline" ]; then
|
||||
CMD "$@"
|
||||
exit $?
|
||||
fi
|
||||
set -- "$@" "$ARG"
|
||||
done
|
||||
|
||||
# if a port is already provided, let's just run the command as such
|
||||
for ARG; do
|
||||
shift
|
||||
if [ "$ARG" == "-p" ]; then
|
||||
CMD "$@"
|
||||
exit $?
|
||||
fi
|
||||
set -- "$@" "$ARG"
|
||||
done
|
||||
|
||||
if [ "$1" == "--list" ]; then
|
||||
shift
|
||||
if [ "$1" != "" ]; then
|
||||
echo >&2 "[!!] Option --list must be used alone"
|
||||
exit 1
|
||||
fi
|
||||
SHOWLIST=true
|
||||
fi
|
||||
|
||||
# Number of the Proxmark3 we're interested in
|
||||
N=1
|
||||
if [ "$1" == "-n" ]; then
|
||||
shift
|
||||
if [ "$1" -ge 1 ] && [ "$1" -lt 10 ]; then
|
||||
N=$1
|
||||
shift
|
||||
else
|
||||
echo >&2 "[!!] Option -n requires a number between 1 and 9, got \"$1\""
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
HOSTOS=$(uname | awk '{print toupper($0)}')
|
||||
if [ "$HOSTOS" = "LINUX" ]; then
|
||||
# Detect when running under WSL1 (but exclude WSL2)
|
||||
if uname -a | grep -qi Microsoft && uname -a | grep -qvi WSL2; then
|
||||
# First try finding it using the PATH environment variable
|
||||
PSHEXE=$(command -v powershell.exe 2>/dev/null)
|
||||
|
||||
# If it fails (such as if WSLENV is not set), try using the default installation path
|
||||
if [ -z "$PSHEXE" ]; then
|
||||
PSHEXE=/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe
|
||||
fi
|
||||
|
||||
# Finally test if PowerShell is working
|
||||
if ! "$PSHEXE" exit >/dev/null 2>&1; then
|
||||
echo >&2 "[!!] Cannot run powershell.exe, are you sure your WSL is authorized to run Windows processes? (cf WSL interop flag)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GETPM3LIST=get_pm3_list_WSL
|
||||
else
|
||||
GETPM3LIST=get_pm3_list_Linux
|
||||
fi
|
||||
elif [ "$HOSTOS" = "DARWIN" ]; then
|
||||
GETPM3LIST=get_pm3_list_macOS
|
||||
elif [[ "$HOSTOS" =~ MINGW(32|64)_NT* ]]; then
|
||||
GETPM3LIST=get_pm3_list_Windows
|
||||
else
|
||||
echo >&2 "[!!] Host OS not recognized, abort: $HOSTOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $SHOWLIST; then
|
||||
# Probe for up to 9 devs
|
||||
$GETPM3LIST 9
|
||||
if [ ${#PM3LIST} -lt 1 ]; then
|
||||
echo >&2 "[!!] No port found"
|
||||
exit 1
|
||||
fi
|
||||
n=1
|
||||
for DEV in "${PM3LIST[@]}"
|
||||
do
|
||||
echo "$n: $DEV"
|
||||
n=$((n+1))
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait till we get at least N Proxmark3 devices
|
||||
$GETPM3LIST "$N"
|
||||
if [ ${#PM3LIST} -lt "$N" ]; then
|
||||
echo >&2 "[=] Waiting for Proxmark3 to appear..."
|
||||
fi
|
||||
while true; do
|
||||
if [ ${#PM3LIST[*]} -ge "$N" ]; then
|
||||
break
|
||||
fi
|
||||
sleep .1
|
||||
$GETPM3LIST "$N"
|
||||
done
|
||||
|
||||
if [ ${#PM3LIST} -lt "$N" ]; then
|
||||
HELP() {
|
||||
cat << EOF
|
||||
[!!] No port found, abort
|
||||
|
||||
[?] Hint: try '$SCRIPT --list' to see list of available ports, and use the -n command like below
|
||||
[?] $SCRIPT [-n <N>]
|
||||
|
||||
EOF
|
||||
}
|
||||
HELP
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CMD "${PM3LIST[$((N-1))]}" "$@"
|
||||
exit $?
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PM3PATH=$(dirname "$0")
|
||||
. "$PM3PATH/pm3"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PM3PATH=$(dirname "$0")
|
||||
. "$PM3PATH/pm3"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PM3PATH=$(dirname "$0")
|
||||
. "$PM3PATH/pm3"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PM3PATH=$(dirname "$0")
|
||||
. "$PM3PATH/pm3"
|
||||
Binary file not shown.
4
rust-toolchain.toml
Normal file
4
rust-toolchain.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src"]
|
||||
targets = ["riscv32imac-unknown-none-elf"]
|
||||
@@ -1,19 +0,0 @@
|
||||
[Unit]
|
||||
Description=Failure state for fwa.service
|
||||
Requires=local-fs.target
|
||||
After=local-fs.target
|
||||
StartLimitIntervalSec=500
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/fwa --error
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/var/lib/fwa
|
||||
EnvironmentFile=/etc/fwa.env
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,5 +0,0 @@
|
||||
PM3_BIN=/usr/share/pm3/pm3
|
||||
LOG_LEVEL=warn
|
||||
HOTSPOT_IDS=578B5DF2;c1532b57
|
||||
HOTSPOT_SSID=fwa
|
||||
HOTSPOT_PW=a9LG2kUVrsRRVUo1
|
||||
@@ -1,20 +0,0 @@
|
||||
[Unit]
|
||||
Description=Feuerwehr Anwesenheit Service
|
||||
Requires=local-fs.target
|
||||
After=local-fs.target
|
||||
StartLimitIntervalSec=500
|
||||
StartLimitBurst=5
|
||||
OnFailure= fwa-fail.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/fwa
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/var/lib/fwa
|
||||
EnvironmentFile=/etc/fwa.env
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
2
src/drivers.rs
Normal file
2
src/drivers.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod nfc_reader;
|
||||
pub mod rtc;
|
||||
68
src/drivers/nfc_reader.rs
Normal file
68
src/drivers/nfc_reader.rs
Normal 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
|
||||
}
|
||||
90
src/drivers/rtc.rs
Normal file
90
src/drivers/rtc.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
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::Enabled,
|
||||
};
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
info!("RTC time is: {:?}", rtc.datetime().await.unwrap());
|
||||
|
||||
rtc
|
||||
}
|
||||
304
src/feedback.rs
304
src/feedback.rs
@@ -1,181 +1,135 @@
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use rgb::RGB8;
|
||||
use smart_leds::colors::{GREEN, RED};
|
||||
use std::time::Duration;
|
||||
use tokio::{join, time::sleep};
|
||||
use embassy_time::{Duration, Timer};
|
||||
use esp_hal::gpio::Output;
|
||||
use esp_hal_smartled::SmartLedsAdapterAsync;
|
||||
use log::debug;
|
||||
use smart_leds::SmartLedsWriteAsync;
|
||||
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"))]
|
||||
use crate::{hardware::GPIOBuzzer, hardware::SpiLed};
|
||||
|
||||
#[cfg(feature = "mock_pi")]
|
||||
use crate::hardware::{MockBuzzer, MockLed};
|
||||
|
||||
const LED_BLINK_DURATION: Duration = Duration::from_secs(1);
|
||||
|
||||
pub enum DeviceStatus {
|
||||
NotReady,
|
||||
Ready,
|
||||
HotspotEnabled,
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum FeedbackState {
|
||||
Ack,
|
||||
Nack,
|
||||
Error,
|
||||
Startup,
|
||||
WIFI,
|
||||
Idle,
|
||||
}
|
||||
|
||||
impl DeviceStatus {
|
||||
pub fn color(&self) -> RGB8 {
|
||||
match self {
|
||||
Self::NotReady => RGB8::new(0, 0, 0),
|
||||
Self::Ready => RGB8::new(0, 50, 0),
|
||||
Self::HotspotEnabled => RGB8::new(0, 0, 50),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct Feedback<B: Buzzer, L: StatusLed> {
|
||||
device_status: DeviceStatus,
|
||||
buzzer: B,
|
||||
led: L,
|
||||
}
|
||||
|
||||
impl<B: Buzzer, L: StatusLed> Feedback<B, L> {
|
||||
pub async fn success(&mut self) {
|
||||
let buzzer_handle = Self::beep_ack(&mut self.buzzer);
|
||||
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, LED_BLINK_DURATION);
|
||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
||||
|
||||
buzzer_result.unwrap_or_else(|err| {
|
||||
error!("Failed to buzz: {err}");
|
||||
});
|
||||
|
||||
let _ = self.led_to_status();
|
||||
}
|
||||
|
||||
pub async fn failure(&mut self) {
|
||||
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
|
||||
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
|
||||
|
||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
||||
|
||||
buzzer_result.unwrap_or_else(|err| {
|
||||
error!("Failed to buzz: {err}");
|
||||
});
|
||||
|
||||
let _ = self.led_to_status();
|
||||
}
|
||||
|
||||
pub async fn activate_error_state(&mut self) -> Result<()> {
|
||||
self.led.turn_on(RED)?;
|
||||
Self::beep_nak(&mut self.buzzer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn startup(&mut self){
|
||||
self.device_status = DeviceStatus::Ready;
|
||||
|
||||
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, Duration::from_secs(1));
|
||||
let buzzer_handle = Self::beep_startup(&mut self.buzzer);
|
||||
|
||||
let (buzzer_result, led_result) = join!(buzzer_handle, led_handle);
|
||||
|
||||
buzzer_result.unwrap_or_else(|err| {
|
||||
error!("Failed to buzz: {err}");
|
||||
});
|
||||
|
||||
led_result.unwrap_or_else(|err| {
|
||||
error!("Failed to blink led: {err}");
|
||||
});
|
||||
|
||||
let _ = self.led_to_status();
|
||||
}
|
||||
|
||||
pub fn set_device_status(&mut self, status: DeviceStatus){
|
||||
self.device_status = status;
|
||||
let _ = self.led_to_status();
|
||||
}
|
||||
|
||||
fn led_to_status(&mut self) -> Result<()> {
|
||||
self.led.turn_on(self.device_status.color())
|
||||
}
|
||||
|
||||
async fn flash_led_for_duration(led: &mut L, color: RGB8, duration: Duration) -> Result<()> {
|
||||
led.turn_on(color)?;
|
||||
|
||||
sleep(duration).await;
|
||||
|
||||
led.turn_off()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn beep_ack(buzzer: &mut B) -> Result<()> {
|
||||
buzzer
|
||||
.modulated_tone(1200.0, Duration::from_millis(100))
|
||||
.await?;
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
buzzer
|
||||
.modulated_tone(2000.0, Duration::from_millis(50))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn beep_nak(buzzer: &mut B) -> Result<()> {
|
||||
buzzer
|
||||
.modulated_tone(600.0, Duration::from_millis(150))
|
||||
.await?;
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
buzzer
|
||||
.modulated_tone(600.0, Duration::from_millis(150))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn beep_startup(buzzer: &mut B) -> Result<()> {
|
||||
buzzer
|
||||
.modulated_tone(523.0, Duration::from_millis(150))
|
||||
.await?;
|
||||
buzzer
|
||||
.modulated_tone(659.0, Duration::from_millis(150))
|
||||
.await?;
|
||||
buzzer
|
||||
.modulated_tone(784.0, Duration::from_millis(150))
|
||||
.await?;
|
||||
buzzer
|
||||
.modulated_tone(1046.0, Duration::from_millis(200))
|
||||
.await?;
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
buzzer
|
||||
.modulated_tone(784.0, Duration::from_millis(100))
|
||||
.await?;
|
||||
buzzer
|
||||
.modulated_tone(880.0, Duration::from_millis(200))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[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()?,
|
||||
})
|
||||
}
|
||||
const LED_LEVEL: u8 = 255;
|
||||
|
||||
//TODO ERROR STATE: 1 Blink = unknows error, 3 Blink = no sd card
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn feedback_task(
|
||||
mut led: SmartLedsAdapterAsync<'static, { hardware::LED_BUFFER_SIZE }>,
|
||||
mut buzzer: Output<'static>,
|
||||
) {
|
||||
debug!("Starting feedback task");
|
||||
loop {
|
||||
let feedback_state = FEEDBACK_STATE.wait().await;
|
||||
match feedback_state {
|
||||
FeedbackState::Ack => {
|
||||
led.write(brightness(
|
||||
[GREEN; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
buzzer.set_low();
|
||||
Timer::after(Duration::from_millis(50)).await;
|
||||
led.write(brightness(
|
||||
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
FeedbackState::Nack => {
|
||||
led.write(brightness(
|
||||
[YELLOW; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
buzzer.set_low();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
buzzer.set_low();
|
||||
led.write(brightness(
|
||||
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
FeedbackState::Error => {
|
||||
led.write(brightness(
|
||||
[RED; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
buzzer.set_low();
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
buzzer.set_low();
|
||||
}
|
||||
FeedbackState::Startup => {
|
||||
led.write(brightness(
|
||||
[GREEN; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(10)).await;
|
||||
buzzer.set_low();
|
||||
Timer::after(Duration::from_millis(10)).await;
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(10)).await;
|
||||
buzzer.set_low();
|
||||
Timer::after(Duration::from_millis(50)).await;
|
||||
buzzer.set_high();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
buzzer.set_low();
|
||||
|
||||
Timer::after(Duration::from_secs(2)).await;
|
||||
led.write(brightness(
|
||||
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
FeedbackState::WIFI => {
|
||||
led.write(brightness(
|
||||
[BLUE; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
FeedbackState::Idle => {
|
||||
led.write(brightness(
|
||||
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||
LED_LEVEL,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
debug!("Feedback state: {:?}", feedback_state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use rppal::pwm::{Channel, Polarity, Pwm};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::hardware::Buzzer;
|
||||
|
||||
const DEFAULT_PWM_CHANNEL_BUZZER: Channel = Channel::Pwm0; //PWM0 = GPIO18/Physical pin 12
|
||||
|
||||
pub struct GPIOBuzzer {
|
||||
pwm: Pwm,
|
||||
}
|
||||
|
||||
impl GPIOBuzzer {
|
||||
pub fn new_from_channel(channel: Channel) -> Result<Self, rppal::pwm::Error> {
|
||||
// Enable with dummy values; we'll set frequency/duty in the tone method
|
||||
let duty_cycle: f64 = 0.5;
|
||||
let pwm = Pwm::with_frequency(channel, 1000.0, duty_cycle, Polarity::Normal, true)?;
|
||||
pwm.disable()?;
|
||||
|
||||
Ok(GPIOBuzzer { pwm })
|
||||
}
|
||||
|
||||
pub fn new_default() -> Result<Self, rppal::pwm::Error> {
|
||||
Self::new_from_channel(DEFAULT_PWM_CHANNEL_BUZZER)
|
||||
}
|
||||
}
|
||||
|
||||
impl Buzzer for GPIOBuzzer {
|
||||
async fn modulated_tone(&mut self, frequency_hz: f64, duration: Duration) -> Result<()> {
|
||||
self.pwm.set_frequency(frequency_hz, 0.5)?; // 50% duty cycle (square wave)
|
||||
self.pwm.enable()?;
|
||||
sleep(duration).await;
|
||||
self.pwm.disable()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use log::{trace, warn};
|
||||
use std::env;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::hardware::Hotspot;
|
||||
|
||||
const SSID: &str = "fwa";
|
||||
const CON_NAME: &str = "fwa-hotspot";
|
||||
const PASSWORD: &str = "a9LG2kUVrsRRVUo1";
|
||||
const IPV4_ADDRES: &str = "192.168.4.1/24";
|
||||
|
||||
/// NetworkManager Hotspot
|
||||
pub struct NMHotspot {
|
||||
ssid: String,
|
||||
con_name: String,
|
||||
password: String,
|
||||
ipv4: String,
|
||||
}
|
||||
|
||||
impl NMHotspot {
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let ssid = env::var("HOTSPOT_SSID").unwrap_or(SSID.to_owned());
|
||||
let password = env::var("HOTSPOT_PW").unwrap_or_else(|_| {
|
||||
warn!("HOTSPOT_PW not set. Using default password");
|
||||
PASSWORD.to_owned()
|
||||
});
|
||||
|
||||
if password.len() < 8 {
|
||||
return Err(anyhow!("Hotspot password to short"));
|
||||
}
|
||||
|
||||
Ok(NMHotspot {
|
||||
ssid,
|
||||
con_name: CON_NAME.to_owned(),
|
||||
password,
|
||||
ipv4: IPV4_ADDRES.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_hotspot(&self) -> Result<()> {
|
||||
let cmd = Command::new("nmcli")
|
||||
.args(["device", "wifi", "hotspot"])
|
||||
.arg("con-name")
|
||||
.arg(&self.con_name)
|
||||
.arg("ssid")
|
||||
.arg(&self.ssid)
|
||||
.arg("password")
|
||||
.arg(&self.password)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
||||
|
||||
if !cmd.status.success() {
|
||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
||||
}
|
||||
|
||||
let cmd = Command::new("nmcli")
|
||||
.arg("connection")
|
||||
.arg("modify")
|
||||
.arg(&self.con_name)
|
||||
.arg("ipv4.method")
|
||||
.arg("shared")
|
||||
.arg("ipv4.addresses")
|
||||
.arg(&self.ipv4)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !cmd.status.success() {
|
||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the connection already exists
|
||||
async fn exists(&self) -> Result<bool> {
|
||||
let cmd = Command::new("nmcli")
|
||||
.args(["connection", "show"])
|
||||
.arg(&self.con_name)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
||||
|
||||
Ok(cmd.status.success())
|
||||
}
|
||||
}
|
||||
|
||||
impl Hotspot for NMHotspot {
|
||||
async fn enable_hotspot(&self) -> Result<()> {
|
||||
if !self.exists().await? {
|
||||
self.create_hotspot().await?;
|
||||
}
|
||||
|
||||
let cmd = Command::new("nmcli")
|
||||
.args(["connection", "up"])
|
||||
.arg(&self.con_name)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
||||
|
||||
if !cmd.status.success() {
|
||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disable_hotspot(&self) -> Result<()> {
|
||||
let cmd = Command::new("nmcli")
|
||||
.args(["connection", "down"])
|
||||
.arg(&self.con_name)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout));
|
||||
trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr));
|
||||
|
||||
if !cmd.status.success() {
|
||||
return Err(anyhow!("nmcli command had non-zero exit code"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,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(())
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use std::time::Duration;
|
||||
|
||||
mod gpio_buzzer;
|
||||
mod hotspot;
|
||||
mod mock;
|
||||
mod spi_led;
|
||||
|
||||
pub use gpio_buzzer::GPIOBuzzer;
|
||||
pub use mock::{MockBuzzer, MockHotspot, MockLed};
|
||||
pub use spi_led::SpiLed;
|
||||
|
||||
pub trait StatusLed {
|
||||
fn turn_off(&mut self) -> Result<()>;
|
||||
|
||||
fn turn_on(&mut self, color: rgb::RGB8) -> Result<()>;
|
||||
}
|
||||
|
||||
pub trait Buzzer {
|
||||
fn modulated_tone(
|
||||
&mut self,
|
||||
frequency_hz: f64,
|
||||
duration: Duration,
|
||||
) -> impl Future<Output = Result<()>> + std::marker::Send;
|
||||
}
|
||||
|
||||
pub trait Hotspot {
|
||||
fn enable_hotspot(&self) -> impl std::future::Future<Output = Result<()>> + std::marker::Send;
|
||||
|
||||
fn disable_hotspot(&self) -> impl std::future::Future<Output = Result<()>> + std::marker::Send;
|
||||
}
|
||||
|
||||
/// Create a struct to manage the hotspot
|
||||
/// Respects the `mock_pi` flag.
|
||||
pub fn create_hotspot() -> Result<impl Hotspot> {
|
||||
#[cfg(feature = "mock_pi")]
|
||||
{
|
||||
Ok(mock::MockHotspot {})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock_pi"))]
|
||||
{
|
||||
hotspot::NMHotspot::new_from_env()
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use rppal::spi::{Bus, Mode, SlaveSelect, Spi};
|
||||
use smart_leds::SmartLedsWrite;
|
||||
use ws2812_spi::Ws2812;
|
||||
|
||||
use crate::hardware::StatusLed;
|
||||
|
||||
const SPI_CLOCK_SPEED: u32 = 3_800_000;
|
||||
|
||||
pub struct SpiLed {
|
||||
controller: Ws2812<Spi>,
|
||||
}
|
||||
|
||||
impl SpiLed {
|
||||
pub fn new() -> Result<Self, rppal::spi::Error> {
|
||||
let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, SPI_CLOCK_SPEED, Mode::Mode0)?;
|
||||
let controller = Ws2812::new(spi);
|
||||
Ok(SpiLed { controller })
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusLed for SpiLed {
|
||||
fn turn_off(&mut self) -> Result<()> {
|
||||
self.controller
|
||||
.write(vec![rgb::RGB8::new(0, 0, 0)].into_iter())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn turn_on(&mut self, color: rgb::RGB8) -> Result<()> {
|
||||
self.controller.write(vec![color].into_iter())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
4
src/init.rs
Normal file
4
src/init.rs
Normal 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
220
src/init/hardware.rs
Normal 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
69
src/init/network.rs
Normal 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;
|
||||
}
|
||||
275
src/init/sd_card.rs
Normal file
275
src/init/sd_card.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
use alloc::{string::ToString, vec::Vec};
|
||||
use core::str::from_utf8;
|
||||
use embassy_time::Delay;
|
||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||
use embedded_sdmmc::{
|
||||
SdCard, SdCardError, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager,
|
||||
};
|
||||
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::store::{
|
||||
AttendanceDay, day::Day, mapping_loader::Name, persistence::Persistence, tally_id::TallyID,
|
||||
};
|
||||
|
||||
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_DIRNAME: &'static str = "MAPPINGS";
|
||||
|
||||
fn generate_filename_for_day(day: Day) -> Result<ShortFileName, PersistenceError> {
|
||||
let basename = day.to_string();
|
||||
let mut filename: heapless::String<11> = heapless::String::new();
|
||||
filename
|
||||
.push_str(&basename)
|
||||
.map_err(|_| PersistenceError::DayFilename)?;
|
||||
filename
|
||||
.push_str(".JS")
|
||||
.map_err(|_| PersistenceError::DayFilename)?;
|
||||
|
||||
ShortFileName::create_from_str(&filename).map_err(|_| PersistenceError::DayFilename)
|
||||
}
|
||||
|
||||
fn generate_path_for_id(
|
||||
id: TallyID,
|
||||
) -> Result<(ShortFileName, ShortFileName), PersistenceError> {
|
||||
let basename: heapless::String<12> = id.into();
|
||||
let (dir, file) = basename.split_at(6);
|
||||
|
||||
let mut filename: heapless::String<11> = heapless::String::new();
|
||||
filename
|
||||
.push_str(file)
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
filename
|
||||
.push_str(".JS")
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
let mut dirname: heapless::String<11> = heapless::String::new();
|
||||
dirname
|
||||
.push_str(dir)
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
Ok((
|
||||
ShortFileName::create_from_str(&dirname).map_err(|_| PersistenceError::IDFilename)?,
|
||||
ShortFileName::create_from_str(&filename).map_err(|_| PersistenceError::IDFilename)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_tallyid_from_path(
|
||||
dirname: &ShortFileName,
|
||||
filename: &ShortFileName,
|
||||
) -> Result<TallyID, PersistenceError> {
|
||||
let mut id_str: heapless::String<12> = heapless::String::new();
|
||||
|
||||
id_str
|
||||
.push_str(&dirname.to_string())
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
id_str
|
||||
.push_str(from_utf8(filename.base_name()).map_err(|_| PersistenceError::IDFilename)?)
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
let id: TallyID = id_str
|
||||
.try_into()
|
||||
.map_err(|_| PersistenceError::IDFilename)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PersistenceError {
|
||||
#[error("Failed to interact with SD card")]
|
||||
SdCard(embedded_sdmmc::Error<SdCardError>),
|
||||
|
||||
#[error("Failed to parse data")]
|
||||
Parseing(#[from] serde_json::Error),
|
||||
|
||||
#[error("Failed to parse Day and Filename")]
|
||||
DayFilename,
|
||||
|
||||
#[error("Failed to parse TallyID for file path")]
|
||||
IDFilename,
|
||||
|
||||
#[error("Item not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl From<embedded_sdmmc::Error<SdCardError>> for PersistenceError {
|
||||
fn from(err: embedded_sdmmc::Error<SdCardError>) -> Self {
|
||||
PersistenceError::SdCard(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl Persistence for SDCardPersistence {
|
||||
type Error = PersistenceError;
|
||||
|
||||
async fn load_day(&mut self, day: Day) -> Result<AttendanceDay, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
|
||||
let filename = Self::generate_filename_for_day(day)?;
|
||||
let file = root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly);
|
||||
|
||||
if file.is_err() {
|
||||
return Err(PersistenceError::NotFound);
|
||||
}
|
||||
|
||||
let mut open_file = file?;
|
||||
|
||||
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||
let read = open_file.read(&mut read_buffer)?;
|
||||
open_file.close()?;
|
||||
|
||||
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read])?;
|
||||
|
||||
Ok(day)
|
||||
}
|
||||
|
||||
async fn save_day(&mut self, day: Day, data: &AttendanceDay) -> Result<(), Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
|
||||
let filename = Self::generate_filename_for_day(day)?;
|
||||
|
||||
let mut file =
|
||||
root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)?;
|
||||
|
||||
file.write(&serde_json::to_vec(data)?)?;
|
||||
|
||||
file.flush()?;
|
||||
file.close()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_days(&mut self) -> Result<Vec<Day>, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
|
||||
let mut days_dir = root_dir.open_dir(".")?;
|
||||
|
||||
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);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(days)
|
||||
}
|
||||
|
||||
async fn load_mapping_for_id(
|
||||
&mut self,
|
||||
id: crate::store::tally_id::TallyID,
|
||||
) -> Result<crate::store::mapping_loader::Name, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME)?;
|
||||
|
||||
let (dirname, filename) = Self::generate_path_for_id(id)?;
|
||||
|
||||
let mut dir = mapping_dir.open_dir(dirname)?;
|
||||
let mut file = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?;
|
||||
|
||||
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||
let read_bytes = file.read(&mut read_buffer)?;
|
||||
file.close()?;
|
||||
|
||||
let mapping: Name = serde_json::from_slice(&read_buffer[..read_bytes])?;
|
||||
|
||||
Ok(mapping)
|
||||
}
|
||||
|
||||
async fn save_mapping_for_id(
|
||||
&mut self,
|
||||
id: crate::store::tally_id::TallyID,
|
||||
name: crate::store::mapping_loader::Name,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME)?;
|
||||
|
||||
let (dirname, filename) = Self::generate_path_for_id(id)?;
|
||||
|
||||
let mut dir = if let Ok(dir) = mapping_dir.open_dir(&dirname) {
|
||||
dir
|
||||
} else {
|
||||
mapping_dir.make_dir_in_dir(&dirname)?;
|
||||
mapping_dir.open_dir(&dirname)?
|
||||
};
|
||||
|
||||
let mut file =
|
||||
dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)?;
|
||||
|
||||
file.write(&serde_json::to_vec(&name)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_mappings(&mut self) -> Result<Vec<TallyID>, Self::Error> {
|
||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0))?;
|
||||
let mut root_dir = vol_0.open_root_dir()?;
|
||||
let mut mapping_dir = root_dir.open_dir(Self::MAPPING_DIRNAME)?;
|
||||
let mut ids: Vec<TallyID> = Vec::new();
|
||||
|
||||
let mut dir_names = Vec::new();
|
||||
mapping_dir.iterate_dir(|entry| {
|
||||
if entry.attributes.is_directory()
|
||||
&& entry.name.to_string() != "."
|
||||
&& entry.name.to_string() != ".."
|
||||
{
|
||||
dir_names.push(entry.name.clone());
|
||||
}
|
||||
})?;
|
||||
|
||||
for dirname in dir_names {
|
||||
if let Ok(mut subdir) = mapping_dir.open_dir(&dirname) {
|
||||
let mut file_names = Vec::new();
|
||||
subdir.iterate_dir(|file_entry| {
|
||||
if !file_entry.attributes.is_directory() {
|
||||
file_names.push(file_entry.name.clone());
|
||||
}
|
||||
})?;
|
||||
|
||||
for filename in file_names {
|
||||
let id = Self::get_tallyid_from_path(&dirname, &filename)?;
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
77
src/init/wifi.rs
Normal file
77
src/init/wifi.rs
Normal 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
1
src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
#![no_std]
|
||||
@@ -1,25 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use log::LevelFilter;
|
||||
use simplelog::{ConfigBuilder, SimpleLogger};
|
||||
|
||||
pub fn setup_logger() {
|
||||
let log_level = env::var("LOG_LEVEL")
|
||||
.ok()
|
||||
.and_then(|level| level.parse::<LevelFilter>().ok())
|
||||
.unwrap_or({
|
||||
if cfg!(debug_assertions) {
|
||||
LevelFilter::Debug
|
||||
} else {
|
||||
LevelFilter::Warn
|
||||
}
|
||||
});
|
||||
|
||||
let config = ConfigBuilder::new()
|
||||
.set_target_level(LevelFilter::Off)
|
||||
.set_location_level(LevelFilter::Off)
|
||||
.set_thread_level(LevelFilter::Off)
|
||||
.build();
|
||||
|
||||
let _ = SimpleLogger::init(log_level, config);
|
||||
}
|
||||
288
src/main.rs
288
src/main.rs
@@ -1,192 +1,152 @@
|
||||
#![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 feedback::{Feedback, FeedbackImpl};
|
||||
use log::{error, info, warn};
|
||||
use std::{
|
||||
env::{self, args},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tally_id::TallyID;
|
||||
use tokio::{
|
||||
fs,
|
||||
signal::unix::{SignalKind, signal},
|
||||
sync::{
|
||||
Mutex,
|
||||
broadcast::{self, Receiver, Sender},
|
||||
use alloc::rc::Rc;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_net::Stack;
|
||||
use embassy_sync::{
|
||||
blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex},
|
||||
mutex::Mutex,
|
||||
pubsub::{
|
||||
PubSubChannel, Publisher, Subscriber,
|
||||
WaitResult::{Lagged, Message},
|
||||
},
|
||||
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, mapping_loader::MappingLoader, tally_id::TallyID},
|
||||
webserver::start_webserver,
|
||||
};
|
||||
|
||||
mod drivers;
|
||||
mod feedback;
|
||||
mod hardware;
|
||||
mod pm3;
|
||||
mod logger;
|
||||
mod tally_id;
|
||||
mod webserver;
|
||||
mod init;
|
||||
mod store;
|
||||
mod webserver;
|
||||
|
||||
const STORE_PATH: &str = "./data.json";
|
||||
static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
|
||||
|
||||
async fn run_webserver<H>(
|
||||
store: Arc<Mutex<IDStore>>,
|
||||
id_channel: Sender<String>,
|
||||
hotspot: Arc<Mutex<H>>,
|
||||
user_feedback: Arc<Mutex<FeedbackImpl>>,
|
||||
) -> Result<()>
|
||||
where
|
||||
H: Hotspot + Send + Sync + 'static,
|
||||
{
|
||||
let activity_channel = spawn_idle_watcher(Duration::from_secs(60 * 30), move || {
|
||||
info!("No activity on webserver. Disabling hotspot");
|
||||
let cloned_hotspot = hotspot.clone();
|
||||
let cloned_user_feedback = user_feedback.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = cloned_hotspot.lock().await.disable_hotspot().await;
|
||||
cloned_user_feedback
|
||||
.lock()
|
||||
.await
|
||||
.set_device_status(feedback::DeviceStatus::Ready);
|
||||
});
|
||||
});
|
||||
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
|
||||
type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
|
||||
type TallySubscriber = Subscriber<'static, NoopRawMutex, TallyID, 8, 2, 1>;
|
||||
type UsedStore = IDStore<SDCardPersistence>;
|
||||
|
||||
let notifier = ActivityNotifier {
|
||||
sender: activity_channel,
|
||||
};
|
||||
static CHAN: StaticCell<TallyChannel> = StaticCell::new();
|
||||
|
||||
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> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
let mut rtc = drivers::rtc::RTCClock::new(app_hardware.i2c).await;
|
||||
|
||||
fn get_hotspot_enable_ids() -> Vec<TallyID> {
|
||||
let hotspot_ids: Vec<TallyID> = env::var("HOTSPOT_IDS")
|
||||
.map(|ids| ids.split(";").map(|id| TallyID(id.to_owned())).collect())
|
||||
.unwrap_or_default();
|
||||
let current_day: Day = rtc.get_time().await.into();
|
||||
|
||||
if hotspot_ids.is_empty() {
|
||||
warn!(
|
||||
"HOTSPOT_IDS is not set or empty. You will not be able to activate the hotspot via a tally!"
|
||||
);
|
||||
}
|
||||
let shared_sdcard: Rc<Mutex<CriticalSectionRawMutex, SDCardPersistence>> =
|
||||
Rc::new(Mutex::new(app_hardware.sdcard));
|
||||
|
||||
hotspot_ids
|
||||
}
|
||||
let store: UsedStore = IDStore::new_from_storage(shared_sdcard.clone(), current_day).await;
|
||||
let shared_store = Rc::new(Mutex::new(store));
|
||||
|
||||
async fn handle_ids_loop(
|
||||
mut id_channel: Receiver<String>,
|
||||
hotspot_enable_ids: Vec<TallyID>,
|
||||
id_store: Arc<Mutex<IDStore>>,
|
||||
hotspot: Arc<Mutex<impl Hotspot>>,
|
||||
user_feedback: Arc<Mutex<FeedbackImpl>>,
|
||||
) -> Result<()> {
|
||||
while let Ok(tally_id_string) = id_channel.recv().await {
|
||||
let tally_id = TallyID(tally_id_string);
|
||||
let mapping_loader = MappingLoader::new(shared_sdcard.clone());
|
||||
|
||||
if hotspot_enable_ids.contains(&tally_id) {
|
||||
info!("Enableing hotspot");
|
||||
let hotspot_enable_result = hotspot.lock().await.enable_hotspot().await;
|
||||
let chan: &'static mut TallyChannel = CHAN.init(PubSubChannel::new());
|
||||
let publisher: TallyPublisher = chan.publisher().unwrap();
|
||||
let mut sub: TallySubscriber = chan.subscriber().unwrap();
|
||||
|
||||
match hotspot_enable_result {
|
||||
Ok(_) => {
|
||||
user_feedback
|
||||
.lock()
|
||||
.await
|
||||
.set_device_status(feedback::DeviceStatus::HotspotEnabled);
|
||||
wait_for_stack_up(app_hardware.network_stack).await;
|
||||
|
||||
start_webserver(
|
||||
spawner,
|
||||
app_hardware.network_stack,
|
||||
shared_store.clone(),
|
||||
chan,
|
||||
mapping_loader,
|
||||
);
|
||||
|
||||
/****************************** Spawning tasks ***********************************/
|
||||
debug!("spawing NFC reader task...");
|
||||
spawner.must_spawn(drivers::nfc_reader::rfid_reader_task(
|
||||
app_hardware.uart,
|
||||
publisher,
|
||||
));
|
||||
|
||||
debug!("spawing feedback task..");
|
||||
spawner.must_spawn(feedback::feedback_task(
|
||||
app_hardware.led,
|
||||
app_hardware.buzzer,
|
||||
));
|
||||
|
||||
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>>) {
|
||||
let _ = feedback.lock().await.activate_error_state().await;
|
||||
let _ = hotspot.lock().await.enable_hotspot().await;
|
||||
#[embassy_executor::task]
|
||||
async fn sd_detect_task(sd_det_gpio: AnyPin<'static>) {
|
||||
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();
|
||||
sigterm.recv().await;
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
logger::setup_logger();
|
||||
|
||||
info!("Starting application");
|
||||
|
||||
let user_feedback = Arc::new(Mutex::new(Feedback::new()?));
|
||||
let hotspot = Arc::new(Mutex::new(create_hotspot()?));
|
||||
|
||||
let error_flag_set = args().any(|e| e == "--error" || e == "-e");
|
||||
if error_flag_set {
|
||||
error!("Error flag set. Entering error state");
|
||||
enter_error_state(user_feedback.clone(), hotspot).await;
|
||||
return Ok(());
|
||||
loop {
|
||||
sd_det.wait_for_any_edge().await;
|
||||
{
|
||||
if sd_det.is_high() {
|
||||
FEEDBACK_STATE.signal(feedback::FeedbackState::Ack);
|
||||
debug!("card insert");
|
||||
}
|
||||
//card is not insert on low
|
||||
else {
|
||||
FEEDBACK_STATE.signal(feedback::FeedbackState::Nack);
|
||||
debug!("card removed");
|
||||
}
|
||||
}
|
||||
//debounce time
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
mod runner;
|
||||
mod parser;
|
||||
|
||||
pub use runner::run_pm3;
|
||||
@@ -1,10 +0,0 @@
|
||||
use regex::Regex;
|
||||
|
||||
/// Parses the output of PM3 finds the read IDs
|
||||
/// Example input: `[+] UID.... 3112B710`
|
||||
pub fn parse_line(line: &str) -> Option<String> {
|
||||
let regex = Regex::new(r"(?m)^\[\+\] UID.... (.*)$").unwrap();
|
||||
let result = regex.captures(line);
|
||||
|
||||
result.map(|c| c.get(1).unwrap().as_str().to_owned())
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use log::{debug, info, trace, warn};
|
||||
use std::env;
|
||||
use std::process::Stdio;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tokio::select;
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
/// Runs the pm3 binary and monitors it's output
|
||||
/// The pm3 binary is ether set in the env var PM3_BIN or found in the path
|
||||
/// The ouput is parsed and send via the `tx` channel
|
||||
pub async fn run_pm3(tx: broadcast::Sender<String>) -> Result<()> {
|
||||
kill_orphans().await;
|
||||
|
||||
let pm3_path = match env::var("PM3_BIN") {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
info!("PM3_BIN not set. Using default value");
|
||||
"pm3".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmd = Command::new("stdbuf")
|
||||
.arg("-oL")
|
||||
.arg(pm3_path)
|
||||
.arg("-c")
|
||||
.arg("lf hitag reader -@")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdout = cmd.stdout.take().ok_or(anyhow!("Failed to get stdout"))?;
|
||||
let mut stdin = cmd.stdin.take().ok_or(anyhow!("Failed to get stdin"))?;
|
||||
|
||||
let mut reader = BufReader::new(stdout).lines();
|
||||
|
||||
let mut sigterm = signal(SignalKind::terminate())?;
|
||||
|
||||
let child_handle = tokio::spawn(async move {
|
||||
let mut last_id: String = "".to_owned();
|
||||
while let Some(line) = reader.next_line().await.unwrap_or(None) {
|
||||
trace!("PM3: {line}");
|
||||
if let Some(uid) = super::parser::parse_line(&line) {
|
||||
if last_id == uid {
|
||||
let _ = tx.send(uid.clone());
|
||||
}
|
||||
last_id = uid;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
select! {
|
||||
_ = child_handle => {}
|
||||
_ = sigterm.recv() => {
|
||||
debug!("Graceful shutdown of PM3");
|
||||
let _ = stdin.write_all(b"\n").await;
|
||||
let _ = stdin.flush().await;
|
||||
}
|
||||
};
|
||||
|
||||
let status = cmd.wait().await?;
|
||||
|
||||
// We use the exit code here because status.success() is false if the child was terminated by a
|
||||
// signal
|
||||
let code = status.code().unwrap_or(0);
|
||||
|
||||
if code == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("PM3 exited with a non-zero exit code: {code}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Kills any open pm3 instances
|
||||
/// Also funny name. hehehe.
|
||||
async fn kill_orphans() {
|
||||
let kill_result = Command::new("pkill")
|
||||
.arg("-KILL")
|
||||
.arg("-x")
|
||||
.arg("proxmark3")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match kill_result {
|
||||
Ok(_) => {
|
||||
debug!("Successfully killed orphaned pm3 instances");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to kill pm3 orphans: {e} Continuing anyway");
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/store/day.rs
Normal file
63
src/store/day.rs
Normal 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(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use crate::tally_id::TallyID;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Name {
|
||||
pub first: String,
|
||||
pub last: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct IDMapping {
|
||||
id_map: HashMap<TallyID, Name>,
|
||||
}
|
||||
|
||||
impl IDMapping {
|
||||
pub fn new() -> Self {
|
||||
IDMapping {
|
||||
id_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map(&self, id: &TallyID) -> Option<&Name> {
|
||||
self.id_map.get(id)
|
||||
}
|
||||
|
||||
pub fn add_mapping(&mut self, id: TallyID, name: Name) {
|
||||
self.id_map.insert(id, name);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let mut map = IDMapping::new();
|
||||
let id1 = TallyID("A2Fb44".to_owned());
|
||||
let name1 = Name {
|
||||
first: "Max".to_owned(),
|
||||
last: "Mustermann".to_owned(),
|
||||
};
|
||||
|
||||
map.add_mapping(id1.clone(), name1.clone());
|
||||
|
||||
let res = map.map(&id1);
|
||||
|
||||
assert_eq!(res, Some(&name1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple() {
|
||||
let mut map = IDMapping::new();
|
||||
let id1 = TallyID("A2Fb44".to_owned());
|
||||
let name1 = Name {
|
||||
first: "Max".to_owned(),
|
||||
last: "Mustermann".to_owned(),
|
||||
};
|
||||
|
||||
let id2 = TallyID("7D3DF5B5".to_owned());
|
||||
let name2 = Name {
|
||||
first: "First".to_owned(),
|
||||
last: "Last".to_owned(),
|
||||
};
|
||||
|
||||
map.add_mapping(id1.clone(), name1.clone());
|
||||
map.add_mapping(id2.clone(), name2.clone());
|
||||
|
||||
let res = map.map(&id1);
|
||||
assert_eq!(res, Some(&name1));
|
||||
|
||||
let res = map.map(&id2);
|
||||
assert_eq!(res, Some(&name2));
|
||||
}
|
||||
}
|
||||
@@ -1,123 +1,24 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::fs;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::vec::Vec;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{store::IDMapping, tally_id::TallyID};
|
||||
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(Deserialize, Serialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct AttendanceDay {
|
||||
date: String,
|
||||
date: Day,
|
||||
ids: Vec<TallyID>,
|
||||
}
|
||||
|
||||
/// Stores all the days
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct IDStore {
|
||||
days: HashMap<String, AttendanceDay>,
|
||||
pub mapping: IDMapping,
|
||||
}
|
||||
|
||||
impl IDStore {
|
||||
pub fn new() -> Self {
|
||||
IDStore {
|
||||
days: HashMap::new(),
|
||||
mapping: IDMapping::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creats a new `IDStore` from a json file
|
||||
pub async fn new_from_json(filepath: &str) -> Result<Self> {
|
||||
let read_string = fs::read_to_string(filepath).await?;
|
||||
Ok(serde_json::from_str(&read_string)?)
|
||||
}
|
||||
|
||||
/// Add a new id for the current day
|
||||
/// Returns false if ID is already present at the current day.
|
||||
pub fn add_id(&mut self, id: TallyID) -> bool {
|
||||
self.get_current_day().add_id(id)
|
||||
}
|
||||
|
||||
/// Get the `AttendanceDay` of the current day
|
||||
/// Creates a new if not exists
|
||||
pub fn get_current_day(&mut self) -> &mut AttendanceDay {
|
||||
let current_day = get_day_str();
|
||||
|
||||
if self.days.contains_key(¤t_day) {
|
||||
return self.days.get_mut(¤t_day).unwrap();
|
||||
}
|
||||
|
||||
self.days.insert(
|
||||
current_day.clone(),
|
||||
AttendanceDay::new(¤t_day.clone()),
|
||||
);
|
||||
|
||||
self.days.get_mut(¤t_day.clone()).unwrap()
|
||||
}
|
||||
|
||||
/// Writes the store to a json file
|
||||
pub async fn export_json(&self, filepath: &str) -> Result<()> {
|
||||
fs::write(filepath, serde_json::to_string(&self)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export the store to a csv file.
|
||||
/// With days in the rows and IDs in the collum.
|
||||
pub fn export_csv(&self) -> Result<String> {
|
||||
let mut csv = String::new();
|
||||
let seperator = ";";
|
||||
let mut user_ids: HashSet<TallyID> = HashSet::new();
|
||||
|
||||
for day in self.days.values() {
|
||||
for id in day.ids.iter() {
|
||||
user_ids.insert(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut user_ids: Vec<TallyID> = user_ids.into_iter().collect();
|
||||
user_ids.sort();
|
||||
|
||||
let mut days: Vec<String> = self.days.keys().cloned().collect();
|
||||
days.sort();
|
||||
|
||||
let header = days.join(seperator);
|
||||
csv.push_str(&format!(
|
||||
"ID{seperator}Nachname{seperator}Vorname{seperator}{header}\n"
|
||||
));
|
||||
|
||||
for user_id in user_ids.iter() {
|
||||
let id = &user_id.0.to_string();
|
||||
let name = self.mapping.map(user_id);
|
||||
|
||||
let firstname = name.map(|e| e.first.clone()).unwrap_or("".to_owned());
|
||||
let lastname = name.map(|e| e.last.clone()).unwrap_or("".to_owned());
|
||||
|
||||
csv.push_str(&format!("{id}{seperator}{lastname}{seperator}{firstname}"));
|
||||
for day in days.iter() {
|
||||
let was_there: bool = self
|
||||
.days
|
||||
.get(day)
|
||||
.ok_or(anyhow!("Failed to access day"))?
|
||||
.ids
|
||||
.contains(user_id);
|
||||
|
||||
if was_there {
|
||||
csv.push_str(&format!("{seperator}x"));
|
||||
} else {
|
||||
csv.push_str(seperator);
|
||||
}
|
||||
}
|
||||
csv.push('\n');
|
||||
}
|
||||
Ok(csv)
|
||||
}
|
||||
}
|
||||
|
||||
impl AttendanceDay {
|
||||
fn new(day: &str) -> Self {
|
||||
pub fn new(date: Day) -> Self {
|
||||
Self {
|
||||
date: day.to_owned(),
|
||||
date,
|
||||
ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -133,7 +34,79 @@ impl AttendanceDay {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_day_str() -> String {
|
||||
let now = chrono::offset::Local::now();
|
||||
now.format("%Y-%m-%d").to_string()
|
||||
#[derive(Clone)]
|
||||
pub struct IDStore<T: Persistence> {
|
||||
current_day: AttendanceDay,
|
||||
persistence_layer: Rc<Mutex<CriticalSectionRawMutex, T>>,
|
||||
}
|
||||
|
||||
impl<T: Persistence> IDStore<T> {
|
||||
pub async fn new_from_storage(
|
||||
persistence_layer: Rc<Mutex<CriticalSectionRawMutex, T>>,
|
||||
current_date: Day,
|
||||
) -> Self {
|
||||
let day = persistence_layer
|
||||
.lock()
|
||||
.await
|
||||
.load_day(current_date)
|
||||
.await
|
||||
.unwrap_or(AttendanceDay::new(current_date));
|
||||
|
||||
Self {
|
||||
current_day: day,
|
||||
persistence_layer,
|
||||
}
|
||||
}
|
||||
|
||||
async fn persist_day(&mut self) -> Result<(), T::Error> {
|
||||
self.persistence_layer
|
||||
.lock()
|
||||
.await
|
||||
.save_day(self.current_day.date, &self.current_day)
|
||||
.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) -> Result<AttendanceDay, T::Error> {
|
||||
if day == self.current_day.date {
|
||||
return Ok(self.current_day.clone());
|
||||
}
|
||||
|
||||
self.persistence_layer.lock().await.load_day(day).await
|
||||
}
|
||||
|
||||
pub async fn list_days_in_timespan(
|
||||
&mut self,
|
||||
from: Day,
|
||||
to: Day,
|
||||
) -> Result<Vec<Day>, T::Error> {
|
||||
let all_days = self.persistence_layer.lock().await.list_days().await?;
|
||||
|
||||
Ok(all_days
|
||||
.into_iter()
|
||||
.filter(|e| *e >= from)
|
||||
.filter(|e| *e <= to)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
31
src/store/mapping_loader.rs
Normal file
31
src/store/mapping_loader.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use alloc::{rc::Rc, string::String, vec::Vec};
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::store::{persistence::Persistence, tally_id::TallyID};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Name {
|
||||
pub first: String,
|
||||
pub last: String,
|
||||
}
|
||||
|
||||
pub struct MappingLoader<T: Persistence>(Rc<Mutex<CriticalSectionRawMutex, T>>);
|
||||
|
||||
impl<T: Persistence> MappingLoader<T> {
|
||||
pub fn new(persistence_layer: Rc<Mutex<CriticalSectionRawMutex, T>>) -> Self {
|
||||
Self(persistence_layer)
|
||||
}
|
||||
|
||||
pub async fn get_mapping(&self, id: TallyID) -> Result<Name, T::Error> {
|
||||
self.0.lock().await.load_mapping_for_id(id).await
|
||||
}
|
||||
|
||||
pub async fn set_mapping(&self, id: TallyID, name: Name) -> Result<(), T::Error> {
|
||||
self.0.lock().await.save_mapping_for_id(id, name).await
|
||||
}
|
||||
|
||||
pub async fn list_mappings(&self) -> Result<Vec<TallyID>,T::Error> {
|
||||
self.0.lock().await.list_mappings().await
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod id_store;
|
||||
mod id_mapping;
|
||||
pub use id_store::{IDStore,AttendanceDay};
|
||||
|
||||
pub use id_store::IDStore;
|
||||
pub use id_mapping::{IDMapping,Name};
|
||||
pub mod persistence;
|
||||
mod id_store;
|
||||
pub mod tally_id;
|
||||
pub mod day;
|
||||
pub mod mapping_loader;
|
||||
|
||||
15
src/store/persistence.rs
Normal file
15
src/store/persistence.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::store::{day::Day, id_store::AttendanceDay, mapping_loader::Name, tally_id::TallyID};
|
||||
|
||||
pub trait Persistence {
|
||||
type Error: core::error::Error;
|
||||
|
||||
async fn load_day(&mut self, day: Day) -> Result<AttendanceDay, Self::Error>;
|
||||
async fn save_day(&mut self, day: Day, data: &AttendanceDay) -> Result<(), Self::Error>;
|
||||
async fn list_days(&mut self) -> Result<Vec<Day>, Self::Error>;
|
||||
|
||||
async fn load_mapping_for_id(&mut self, id: TallyID) -> Result<Name, Self::Error>;
|
||||
async fn save_mapping_for_id(&mut self, id: TallyID, name: Name) -> Result<(), Self::Error>;
|
||||
async fn list_mappings(&mut self) -> Result<Vec<TallyID>, Self::Error>;
|
||||
}
|
||||
109
src/store/tally_id.rs
Normal file
109
src/store/tally_id.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use alloc::string::String;
|
||||
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 = <String>::deserialize(deserializer)?;
|
||||
TallyID::from_str(&s).map_err(|_| de::Error::custom("Failed to parse Tally ID"))
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::Display,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents the ID that is stored on the Tally
|
||||
/// Is case-insensitive.
|
||||
/// While any string can be a ID, most IDs are going to be a hex string.
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct TallyID(pub String);
|
||||
|
||||
impl PartialEq for TallyID {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.eq_ignore_ascii_case(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for TallyID {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.to_uppercase().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TallyID {}
|
||||
|
||||
impl Ord for TallyID {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.to_uppercase().cmp(&other.0.to_uppercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for TallyID {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TallyID {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0.to_uppercase())
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use log::error;
|
||||
use rocket::{
|
||||
Data, Request,
|
||||
fairing::{Fairing, Info, Kind},
|
||||
};
|
||||
use tokio::{sync::mpsc, time::timeout};
|
||||
|
||||
pub struct ActivityNotifier {
|
||||
pub sender: mpsc::Sender<()>,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl Fairing for ActivityNotifier {
|
||||
fn info(&self) -> Info {
|
||||
Info {
|
||||
name: "Keeps track of time since the last request",
|
||||
kind: Kind::Request | Kind::Response,
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_request(&self, _: &mut Request<'_>, _: &mut Data<'_>) {
|
||||
error!("on_request");
|
||||
let _ = self.sender.try_send(());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_idle_watcher<F>(idle_duration: Duration, mut on_idle: F) -> mpsc::Sender<()>
|
||||
where
|
||||
F: FnMut() + Send + 'static,
|
||||
{
|
||||
let (tx, mut rx) = mpsc::channel::<()>(100);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let idle = timeout(idle_duration, rx.recv()).await;
|
||||
if idle.is_err() {
|
||||
// No activity received in the duration
|
||||
on_idle();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tx
|
||||
}
|
||||
136
src/webserver/api.rs
Normal file
136
src/webserver/api.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use log::error;
|
||||
use picoserve::{
|
||||
extract::{Json, Query, State},
|
||||
response::{self, IntoResponse},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
store::{day::Day, mapping_loader::Name, 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>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct QueryMapping {
|
||||
id: TallyID,
|
||||
}
|
||||
|
||||
// GET /api/mappings
|
||||
pub async fn get_mappings(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||
let loader = state.mapping_loader.lock().await;
|
||||
|
||||
match loader.list_mappings().await {
|
||||
Ok(ids) => Ok(response::Json(ids)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/mapping
|
||||
pub async fn get_mapping(
|
||||
State(state): State<AppState>,
|
||||
Query(QueryMapping { id }): Query<QueryMapping>,
|
||||
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||
let loader = state.mapping_loader.lock().await;
|
||||
|
||||
match loader.get_mapping(id).await {
|
||||
Ok(name) => Ok(response::Json(name)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/mapping
|
||||
pub async fn add_mapping(
|
||||
State(state): State<AppState>,
|
||||
Json(data): Json<NewMapping>,
|
||||
) -> impl IntoResponse {
|
||||
let loader = state.mapping_loader.lock().await;
|
||||
match loader.set_mapping(data.id, data.name).await {
|
||||
Ok(_) => (response::StatusCode::CREATED, ""),
|
||||
Err(_) => (
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
match store.list_days_in_timespan(from_day, to_day).await {
|
||||
Ok(days) => Ok(response::Json(days)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Ok(att_day) => Ok(response::Json(att_day)),
|
||||
Err(_) => Err((
|
||||
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error",
|
||||
)),
|
||||
}
|
||||
}
|
||||
36
src/webserver/app.rs
Normal file
36
src/webserver/app.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use alloc::rc::Rc;
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||
use picoserve::{AppWithStateBuilder, routing::get};
|
||||
|
||||
use crate::{
|
||||
TallyChannel, UsedStore,
|
||||
init::sd_card::SDCardPersistence,
|
||||
store::mapping_loader::MappingLoader,
|
||||
webserver::{
|
||||
api::{add_mapping, get_day, get_days, get_idevent, get_mapping, get_mappings},
|
||||
assets::Assets,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||
pub chan: &'static TallyChannel,
|
||||
pub mapping_loader: Rc<Mutex<CriticalSectionRawMutex, MappingLoader<SDCardPersistence>>>,
|
||||
}
|
||||
|
||||
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/mappings", get(get_mappings))
|
||||
.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
74
src/webserver/assets.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,67 @@
|
||||
mod server;
|
||||
mod activity_fairing;
|
||||
use alloc::rc::Rc;
|
||||
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};
|
||||
pub use server::start_webserver;
|
||||
use crate::{
|
||||
TallyChannel, UsedStore,
|
||||
init::sd_card::SDCardPersistence,
|
||||
store::mapping_loader::{self, MappingLoader},
|
||||
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,
|
||||
mapping_loader: MappingLoader<SDCardPersistence>,
|
||||
) {
|
||||
let app = make_static!(AppProps.build_app());
|
||||
|
||||
let shared_mapping_loader = Rc::new(Mutex::new(mapping_loader));
|
||||
let state = make_static!(AppState {
|
||||
store,
|
||||
chan,
|
||||
mapping_loader: shared_mapping_loader
|
||||
});
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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
32
src/webserver/sse.rs
Normal 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
88
used_ids.txt
Normal 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
108
web/mock/data.json
Normal 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
111
web/mock/server.js
Normal 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
887
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,9 @@
|
||||
"svelte": "^5.28.1",
|
||||
"svelte-check": "^4.1.6",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^6.4.1",
|
||||
"body-parser": "^2.2.0",
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
|
||||
@@ -3,18 +3,24 @@
|
||||
import IDTable from "./lib/IDTable.svelte";
|
||||
import LastId from "./lib/LastID.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 mapping: IDMap | null = $state(null);
|
||||
|
||||
let addModal: AddIDModal;
|
||||
let idTable: IDTable;
|
||||
let exportModal: ExportModal;
|
||||
|
||||
onMount(async () => {
|
||||
mapping = await fetchMapping();
|
||||
|
||||
onMount(() => {
|
||||
let sse = new EventSource("/api/idevent");
|
||||
|
||||
sse.onmessage = (e) => {
|
||||
sse.addEventListener("msg", function (e) {
|
||||
lastID = e.data;
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -25,13 +31,14 @@
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
||||
</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"
|
||||
href="/api/csv"
|
||||
download="anwesenheit.csv"
|
||||
onclick={() => {
|
||||
exportModal.open();
|
||||
}}
|
||||
>
|
||||
Download CSV
|
||||
</a>
|
||||
Export CSV
|
||||
</button>
|
||||
|
||||
<div class="pt-3 pb-2">
|
||||
<LastId
|
||||
@@ -42,15 +49,35 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IDTable bind:this={idTable} onEdit={(id,firstName,lastName)=>{
|
||||
addModal.open(id,firstName,lastName);
|
||||
}}/>
|
||||
{#if mapping}
|
||||
<IDTable
|
||||
data={mapping}
|
||||
onEdit={(id, firstName, lastName) => {
|
||||
addModal.open(id, firstName, lastName);
|
||||
}}
|
||||
/>
|
||||
<span>Gesammmte einträge: { Object.keys(mapping).length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<AddIDModal
|
||||
bind:this={addModal}
|
||||
onSubmitted={() => {
|
||||
idTable.reloadData();
|
||||
onSubmitted={async () => {
|
||||
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>
|
||||
|
||||
31
web/src/lib/Day.ts
Normal file
31
web/src/lib/Day.ts
Normal 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;
|
||||
}
|
||||
|
||||
142
web/src/lib/ExportModal.svelte
Normal file
142
web/src/lib/ExportModal.svelte
Normal 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>
|
||||
@@ -1,7 +1,3 @@
|
||||
export interface IDMapping {
|
||||
id_map: IDMap
|
||||
}
|
||||
|
||||
export interface IDMap {
|
||||
[name: string]: Name
|
||||
}
|
||||
@@ -11,6 +7,31 @@ export interface Name {
|
||||
last: string,
|
||||
}
|
||||
|
||||
async function fetchIDList(): Promise<string[]> {
|
||||
let res = await fetch("/api/mappings");
|
||||
let data = await res.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchID(id: string): Promise<Name> {
|
||||
let res = await fetch("/api/mapping?id=" + id);
|
||||
let data = await res.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchMapping(): Promise<IDMap> {
|
||||
let ids = await fetchIDList();
|
||||
|
||||
let map: IDMap = {};
|
||||
|
||||
for (const id of ids) {
|
||||
let id_name = await fetchID(id);
|
||||
map[id] = id_name;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export async function addMapping(id: string, firstName: string, lastName: string) {
|
||||
let req = await fetch("/api/mapping", {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { IDMapping } from "./IDMapping";
|
||||
let data: IDMapping | undefined = $state();
|
||||
import { type IDMap } from "./IDMapping";
|
||||
|
||||
let { onEdit }: { onEdit?: (string,string,string) => void } = $props();
|
||||
|
||||
export async function reloadData() {
|
||||
let res = await fetch("/api/mapping");
|
||||
|
||||
data = await res.json();
|
||||
}
|
||||
let {
|
||||
onEdit,
|
||||
data,
|
||||
}: {
|
||||
onEdit?: (id: string, firstName: string, lastName: string) => void;
|
||||
data: IDMap;
|
||||
} = $props();
|
||||
|
||||
let rows = $derived(
|
||||
data
|
||||
? Object.entries(data.id_map).map(([id, value]) => ({
|
||||
? Object.entries(data).map(([id, value]) => ({
|
||||
id,
|
||||
...value,
|
||||
}))
|
||||
@@ -43,70 +41,64 @@
|
||||
if (sortKey !== key) return "";
|
||||
return sortDirection === "asc" ? "▲" : "▼";
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await reloadData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if data == null}
|
||||
Loading...
|
||||
{:else}
|
||||
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
||||
<table class="px-10">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="text-left pr-5 pl-2 cursor-pointer select-none"
|
||||
onclick={() => {
|
||||
handleSortClick("id");
|
||||
}}
|
||||
>
|
||||
ID
|
||||
<span class="indicator">{indicator("id")}</span>
|
||||
</th>
|
||||
<th
|
||||
class="text-left pr-5 cursor-pointer select-none"
|
||||
onclick={() => {
|
||||
handleSortClick("last");
|
||||
}}
|
||||
>
|
||||
Nachname
|
||||
<span class="indicator">{indicator("last")}</span>
|
||||
</th>
|
||||
<th
|
||||
class="text-left pr-5 cursor-pointer select-none"
|
||||
onclick={() => {
|
||||
handleSortClick("first");
|
||||
}}
|
||||
>Vorname
|
||||
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
||||
<table class="px-10">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="text-left pr-5 pl-2 cursor-pointer select-none"
|
||||
onclick={() => {
|
||||
handleSortClick("id");
|
||||
}}
|
||||
>
|
||||
ID
|
||||
<span class="indicator">{indicator("id")}</span>
|
||||
</th>
|
||||
<th
|
||||
class="text-left pr-5 cursor-pointer select-none"
|
||||
onclick={() => {
|
||||
handleSortClick("last");
|
||||
}}
|
||||
>
|
||||
Nachname
|
||||
<span class="indicator">{indicator("last")}</span>
|
||||
</th>
|
||||
<th
|
||||
class="text-left pr-5 cursor-pointer select-none"
|
||||
onclick={() => {
|
||||
handleSortClick("first");
|
||||
}}
|
||||
>Vorname
|
||||
|
||||
<span class="indicator">{indicator("first")}</span>
|
||||
</th>
|
||||
<th>
|
||||
</th>
|
||||
<span class="indicator">{indicator("first")}</span>
|
||||
</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>
|
||||
</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>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style lang="css" scoped>
|
||||
@reference "../app.css";
|
||||
|
||||
.indicator {
|
||||
@apply ml-1 w-4 inline-block;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user