mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-05-01 02:59:09 +00:00
Compare commits
106 Commits
2e6094ea11
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cab2533fab | ||
|
|
5f65cc7a73 | ||
|
|
03e6a9036f | ||
| 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 |
@@ -12,3 +12,7 @@ target = "riscv32imac-unknown-none-elf"
|
|||||||
|
|
||||||
[unstable]
|
[unstable]
|
||||||
build-std = ["alloc", "core"]
|
build-std = ["alloc", "core"]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
WIFI_PASSWD = "hunter22"
|
||||||
|
WIFI_SSID = "fwa"
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
pcb/fw-anwesenheit-backups
|
||||||
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.
1142
Cargo.lock
generated
1142
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
75
Cargo.toml
75
Cargo.toml
@@ -5,60 +5,51 @@ edition = "2024"
|
|||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "fw-anwesenheit"
|
name = "fw-anwesenheit"
|
||||||
path = "./src/bin/main.rs"
|
path = "./src/main.rs"
|
||||||
test = false
|
test = false
|
||||||
doctest = false
|
doctest = false
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
esp-bootloader-esp-idf = "0.1.0"
|
esp-bootloader-esp-idf = "0.1.0"
|
||||||
embassy-net = { version = "0.7.0", features = [
|
esp-hal = { version = "1.0.0-rc.1", features = ["esp32c6", "unstable"] }
|
||||||
"dhcpv4",
|
esp-alloc = "0.9.0"
|
||||||
"medium-ethernet",
|
esp-println = { version = "0.16.0", features = ["esp32c6", "log-04"] }
|
||||||
"tcp",
|
esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "log-04", "smoltcp","unstable"]}
|
||||||
"udp",
|
esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
|
||||||
] }
|
|
||||||
embedded-io = "0.6.1"
|
|
||||||
embedded-io-async = "0.6.1"
|
|
||||||
esp-alloc = "0.8.0"
|
|
||||||
esp-hal = { version = "1.0.0-beta.1", features = ["esp32c6", "unstable"] }
|
|
||||||
smoltcp = { version = "0.12.0", default-features = false, features = [
|
|
||||||
"medium-ethernet",
|
|
||||||
"multicast",
|
|
||||||
"proto-dhcpv4",
|
|
||||||
"proto-dns",
|
|
||||||
"proto-ipv4",
|
|
||||||
"socket-dns",
|
|
||||||
"socket-icmp",
|
|
||||||
"socket-raw",
|
|
||||||
"socket-tcp",
|
|
||||||
"socket-udp",
|
|
||||||
] }
|
|
||||||
# for more networking protocol support see https://crates.io/crates/edge-net
|
|
||||||
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [
|
|
||||||
"async",
|
|
||||||
"macros",
|
|
||||||
] }
|
|
||||||
critical-section = "1.2.0"
|
critical-section = "1.2.0"
|
||||||
embassy-executor = { version = "0.7.0", features = ["task-arena-size-20480"] }
|
|
||||||
embassy-time = { version = "0.4.0", features = ["generic-queue-8"] }
|
|
||||||
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6"] }
|
|
||||||
esp-wifi = { version = "0.15.0", features = [
|
|
||||||
"wifi",
|
|
||||||
"builtin-scheduler",
|
|
||||||
"esp-alloc",
|
|
||||||
"esp32c6",
|
|
||||||
"log-04",
|
|
||||||
] }
|
|
||||||
heapless = { version = "0.8.0", default-features = false }
|
|
||||||
static_cell = { version = "2.1.0", features = ["nightly"] }
|
|
||||||
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
|
|
||||||
log = { version = "0.4" }
|
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-dhcp = { version = "0.6.0", features = ["log"] }
|
||||||
edge-nal = "0.5.0"
|
edge-nal = "0.5.0"
|
||||||
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
|
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
|
||||||
picoserve = { version = "0.16.0", features = ["embassy", "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", rev = "ab4316534d90e3a12785907f043f6899faee0f20", package = "esp-hal-smartled", features = ["esp32c6"]}
|
||||||
|
smart-leds = "0.4.0"
|
||||||
|
|
||||||
|
embedded-sdmmc = "0.8.0"
|
||||||
|
embedded-hal-bus = "0.3.0"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
# Rust debug is too slow.
|
# Rust debug is too slow.
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -1,32 +1,6 @@
|
|||||||
# fw-anwesenheit
|
# 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.
|
|
||||||
|
|||||||
28
build.rs
28
build.rs
@@ -1,7 +1,31 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
linker_be_nice();
|
linker_be_nice();
|
||||||
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
||||||
println!("cargo:rustc-link-arg=-Tlinkall.x");
|
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() {
|
fn linker_be_nice() {
|
||||||
@@ -14,7 +38,9 @@ fn linker_be_nice() {
|
|||||||
"undefined-symbol" => match what.as_str() {
|
"undefined-symbol" => match what.as_str() {
|
||||||
"_defmt_timestamp" => {
|
"_defmt_timestamp" => {
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`");
|
eprintln!(
|
||||||
|
"💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"
|
||||||
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
}
|
}
|
||||||
"_stack_start" => {
|
"_stack_start" => {
|
||||||
|
|||||||
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
|
||||||
|
277
src/bin/main.rs
277
src/bin/main.rs
@@ -1,277 +0,0 @@
|
|||||||
#![no_std]
|
|
||||||
#![no_main]
|
|
||||||
#![feature(type_alias_impl_trait)]
|
|
||||||
#![feature(impl_trait_in_assoc_type)]
|
|
||||||
|
|
||||||
use core::net::Ipv4Addr;
|
|
||||||
use core::str::FromStr;
|
|
||||||
|
|
||||||
use embassy_executor::Spawner;
|
|
||||||
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
|
||||||
use embassy_time::{Duration, Timer};
|
|
||||||
use esp_hal::clock::CpuClock;
|
|
||||||
use esp_hal::gpio::{Output, OutputConfig};
|
|
||||||
use esp_hal::peripherals::{GPIO1, GPIO2, UART1};
|
|
||||||
use esp_hal::timer::systimer::SystemTimer;
|
|
||||||
use esp_hal::timer::timg::TimerGroup;
|
|
||||||
use esp_hal::uart::{Config, Uart};
|
|
||||||
use esp_println::logger::init_logger;
|
|
||||||
use esp_wifi::wifi::{
|
|
||||||
AccessPointConfiguration, Configuration, WifiController, WifiDevice, WifiEvent, WifiState,
|
|
||||||
};
|
|
||||||
use log::{debug, info};
|
|
||||||
use picoserve::routing::get;
|
|
||||||
use picoserve::{AppBuilder, AppRouter};
|
|
||||||
use static_cell::make_static;
|
|
||||||
|
|
||||||
#[panic_handler]
|
|
||||||
fn panic(_: &core::panic::PanicInfo) -> ! {
|
|
||||||
loop {}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
esp_bootloader_esp_idf::esp_app_desc!();
|
|
||||||
|
|
||||||
#[esp_hal_embassy::main]
|
|
||||||
async fn main(spawner: Spawner) {
|
|
||||||
// ------------------- init ---------------------------
|
|
||||||
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
|
||||||
let peripherals = esp_hal::init(config);
|
|
||||||
|
|
||||||
info!("starting up...");
|
|
||||||
|
|
||||||
esp_alloc::heap_allocator!(size: 72 * 1024);
|
|
||||||
|
|
||||||
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
|
|
||||||
esp_hal_embassy::init(timer0.alarm0);
|
|
||||||
|
|
||||||
init_logger(log::LevelFilter::Debug);
|
|
||||||
|
|
||||||
let timer1 = TimerGroup::new(peripherals.TIMG0);
|
|
||||||
let mut rng = esp_hal::rng::Rng::new(peripherals.RNG);
|
|
||||||
|
|
||||||
debug!("set wlan antenna..");
|
|
||||||
let mut rf_switch = Output::new(
|
|
||||||
peripherals.GPIO3,
|
|
||||||
esp_hal::gpio::Level::Low,
|
|
||||||
OutputConfig::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
rf_switch.set_low();
|
|
||||||
|
|
||||||
Timer::after_secs(1).await;
|
|
||||||
|
|
||||||
let mut antenna_mode = Output::new(
|
|
||||||
peripherals.GPIO14,
|
|
||||||
esp_hal::gpio::Level::Low,
|
|
||||||
OutputConfig::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
antenna_mode.set_low();
|
|
||||||
|
|
||||||
Timer::after_secs(1).await;
|
|
||||||
|
|
||||||
// Setup wifi deivce
|
|
||||||
debug!("setup wifi..");
|
|
||||||
let esp_wifi_ctrl =
|
|
||||||
make_static!(esp_wifi::init(timer1.timer0, rng).unwrap());
|
|
||||||
let (controller, interfaces) = esp_wifi::wifi::new(esp_wifi_ctrl, peripherals.WIFI).unwrap();
|
|
||||||
// let wifi_interface = interfaces.sta;
|
|
||||||
let wifi_ap = interfaces.ap;
|
|
||||||
|
|
||||||
let gw_ip_addr_str = "192.168.2.1";
|
|
||||||
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
|
|
||||||
|
|
||||||
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
|
||||||
address: Ipv4Cidr::new(gw_ip_addr, 24),
|
|
||||||
gateway: Some(gw_ip_addr),
|
|
||||||
dns_servers: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
|
||||||
|
|
||||||
// Init network stack
|
|
||||||
let (stack, runner) = embassy_net::new(
|
|
||||||
wifi_ap,
|
|
||||||
config,
|
|
||||||
make_static!(StackResources::<3>::new()),
|
|
||||||
seed,
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!("Setup complete. Running network tasks");
|
|
||||||
|
|
||||||
spawner.spawn(connection(controller)).ok();
|
|
||||||
spawner.spawn(net_task(runner)).ok();
|
|
||||||
spawner.spawn(run_dhcp(stack, gw_ip_addr_str)).ok();
|
|
||||||
spawner
|
|
||||||
.spawn(rfid_reader_task(
|
|
||||||
peripherals.UART1,
|
|
||||||
peripherals.GPIO1,
|
|
||||||
peripherals.GPIO2,
|
|
||||||
))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Starting webserver");
|
|
||||||
|
|
||||||
let app = make_static!(AppProps.build_app());
|
|
||||||
|
|
||||||
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
|
||||||
start_read_request: Some(Duration::from_secs(5)),
|
|
||||||
persistent_start_read_request: Some(Duration::from_secs(1)),
|
|
||||||
read_request: Some(Duration::from_secs(1)),
|
|
||||||
write: Some(Duration::from_secs(1)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let _ = spawner.spawn(webserver_task(0, stack, app, config));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AppProps;
|
|
||||||
|
|
||||||
impl AppBuilder for AppProps {
|
|
||||||
type PathRouter = impl picoserve::routing::PathRouter;
|
|
||||||
|
|
||||||
fn build_app(self) -> picoserve::Router<Self::PathRouter> {
|
|
||||||
picoserve::Router::new().route("/", get(|| async move { "Hello World" }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn webserver_task(
|
|
||||||
id: usize,
|
|
||||||
stack: embassy_net::Stack<'static>,
|
|
||||||
app: &'static AppRouter<AppProps>,
|
|
||||||
config: &'static picoserve::Config<Duration>,
|
|
||||||
) -> ! {
|
|
||||||
let mut tcp_rx_buffer = [0u8; 1024];
|
|
||||||
let mut tcp_tx_buffer = [0u8; 1024];
|
|
||||||
let mut http_buffer = [0u8; 2048];
|
|
||||||
|
|
||||||
picoserve::listen_and_serve(
|
|
||||||
id,
|
|
||||||
app,
|
|
||||||
config,
|
|
||||||
stack,
|
|
||||||
80,
|
|
||||||
&mut tcp_rx_buffer,
|
|
||||||
&mut tcp_tx_buffer,
|
|
||||||
&mut http_buffer,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
|
||||||
debug!("start dhcp");
|
|
||||||
use core::net::{Ipv4Addr, SocketAddrV4};
|
|
||||||
|
|
||||||
use edge_dhcp::{
|
|
||||||
io::{self, DEFAULT_SERVER_PORT},
|
|
||||||
server::{Server, ServerOptions},
|
|
||||||
};
|
|
||||||
use edge_nal::UdpBind;
|
|
||||||
use edge_nal_embassy::{Udp, UdpBuffers};
|
|
||||||
|
|
||||||
let ip = Ipv4Addr::from_str(gw_ip_addr).expect("dhcp task failed to parse gw ip");
|
|
||||||
|
|
||||||
let mut buf = [0u8; 1500];
|
|
||||||
|
|
||||||
let mut gw_buf = [Ipv4Addr::UNSPECIFIED];
|
|
||||||
|
|
||||||
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
|
|
||||||
let unbound_socket = Udp::new(stack, &buffers);
|
|
||||||
let mut bound_socket = unbound_socket
|
|
||||||
.bind(core::net::SocketAddr::V4(SocketAddrV4::new(
|
|
||||||
Ipv4Addr::UNSPECIFIED,
|
|
||||||
DEFAULT_SERVER_PORT,
|
|
||||||
)))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
_ = io::server::run(
|
|
||||||
&mut Server::<_, 64>::new_with_et(ip),
|
|
||||||
&ServerOptions::new(ip, Some(&mut gw_buf)),
|
|
||||||
&mut bound_socket,
|
|
||||||
&mut buf,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| log::warn!("DHCP server error: {e:?}"));
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
|
||||||
runner.run().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn connection(mut controller: WifiController<'static>) {
|
|
||||||
debug!("start connection task");
|
|
||||||
debug!("Device capabilities: {:?}", controller.capabilities());
|
|
||||||
loop {
|
|
||||||
match esp_wifi::wifi::wifi_state() {
|
|
||||||
WifiState::ApStarted => {
|
|
||||||
// wait until we're no longer connected
|
|
||||||
controller.wait_for_event(WifiEvent::ApStop).await;
|
|
||||||
Timer::after(Duration::from_millis(5000)).await
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if !matches!(controller.is_started(), Ok(true)) {
|
|
||||||
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
|
|
||||||
ssid: "esp-wifi".try_into().unwrap(),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
controller.set_configuration(&client_config).unwrap();
|
|
||||||
debug!("Starting wifi");
|
|
||||||
controller.start_async().await.unwrap();
|
|
||||||
debug!("Wifi started!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn rfid_reader_task(uart1: UART1<'static>, gpio1: GPIO1<'static>, gpio2: GPIO2<'static>) {
|
|
||||||
debug!("init rfid reader..");
|
|
||||||
|
|
||||||
let uart1_block_result = Uart::new(uart1, Config::default().with_baudrate(9600));
|
|
||||||
let mut nfc_reader = match uart1_block_result {
|
|
||||||
Ok(block) => block.with_rx(gpio1).with_tx(gpio2).into_async(),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to initialize UART: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut uart_buffer = [0u8; 64];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
debug!("Looking for NFC...");
|
|
||||||
match nfc_reader.read_async(&mut uart_buffer).await {
|
|
||||||
Ok(n) => {
|
|
||||||
let mut hex_str = heapless::String::<128>::new();
|
|
||||||
for byte in &uart_buffer[..n] {
|
|
||||||
core::fmt::Write::write_fmt(&mut hex_str, format_args!("{:02X} ", byte)).ok();
|
|
||||||
}
|
|
||||||
info!("Read {} bytes from UART: {}", n, hex_str);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Error reading from UART: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(200)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
src/drivers.rs
Normal file
3
src/drivers.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod nfc_reader;
|
||||||
|
pub mod rtc;
|
||||||
|
pub mod buzzer;
|
||||||
0
src/drivers/buzzer.rs
Normal file
0
src/drivers/buzzer.rs
Normal file
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
|
||||||
|
}
|
||||||
128
src/drivers/rtc.rs
Normal file
128
src/drivers/rtc.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use ds3231::{
|
||||||
|
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
|
||||||
|
TimeRepresentation,
|
||||||
|
};
|
||||||
|
use esp_hal::{
|
||||||
|
Async,
|
||||||
|
i2c::{self, master::I2c},
|
||||||
|
};
|
||||||
|
use log::{debug, error, info};
|
||||||
|
|
||||||
|
use crate::{FEEDBACK_STATE, drivers, feedback};
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
|
||||||
|
|
||||||
|
const RTC_ADDRESS: u8 = 0x68;
|
||||||
|
|
||||||
|
const SECS_PER_DAY: u64 = 86_400;
|
||||||
|
const UNIX_OFFSET_DAYS: u64 = 719_163; // Days from 0000-03-01 to 1970-01-01
|
||||||
|
const UTC_PLUS_ONE: u64 = 3600;
|
||||||
|
|
||||||
|
pub struct RTCClock {
|
||||||
|
dev: DS3231<I2c<'static, Async>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RTCClock {
|
||||||
|
pub async fn new(i2c: i2c::master::I2c<'static, Async>) -> Self {
|
||||||
|
debug!("configuring rtc...");
|
||||||
|
let rtc = drivers::rtc::rtc_config(i2c).await;
|
||||||
|
debug!("rtc up");
|
||||||
|
|
||||||
|
RTCClock { dev: rtc }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_time(&mut self) -> u64 {
|
||||||
|
match self.dev.datetime().await {
|
||||||
|
Ok(datetime) => {
|
||||||
|
let utc_time = datetime.and_utc().timestamp() as u64;
|
||||||
|
utc_time
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
|
||||||
|
error!("Failed to read RTC datetime: {:?}", e);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_to_ymd_string(timestamp: u64) -> (u16, u8, u8) {
|
||||||
|
// Apply UTC+1 offset
|
||||||
|
let ts = timestamp + UTC_PLUS_ONE;
|
||||||
|
|
||||||
|
// Convert to total days since UNIX epoch
|
||||||
|
let days_since_epoch = ts / SECS_PER_DAY;
|
||||||
|
|
||||||
|
// Convert to proleptic Gregorian date
|
||||||
|
civil_from_days(days_since_epoch as i64 + UNIX_OFFSET_DAYS as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function returns (year, month, day).
|
||||||
|
// Based on the algorithm by Howard Hinnant.
|
||||||
|
fn civil_from_days(z: i64) -> (u16, u8, u8) {
|
||||||
|
let mut z = z;
|
||||||
|
z -= 60; // shift epoch for algorithm
|
||||||
|
let era = (z >= 0).then_some(z).unwrap_or(z - 146096) / 146097;
|
||||||
|
let doe = z - era * 146097; // [0, 146096]
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
|
||||||
|
let y = yoe + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
|
||||||
|
let mp = (5 * doy + 2) / 153; // [0, 11]
|
||||||
|
let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
|
||||||
|
let m = mp + (if mp < 10 { 3 } else { -9 }); // [1, 12]
|
||||||
|
((y + (m <= 2) as i64) as u16, m as u8, d as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>> {
|
||||||
|
let mut rtc: DS3231<I2c<'static, Async>> = DS3231::new(i2c, RTC_ADDRESS);
|
||||||
|
let naive_dt = Utc
|
||||||
|
.timestamp_opt(BUILD_UNIX_TIME as i64, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap()
|
||||||
|
.naive_utc();
|
||||||
|
|
||||||
|
let rtc_config = Config {
|
||||||
|
time_representation: TimeRepresentation::TwentyFourHour,
|
||||||
|
square_wave_frequency: SquareWaveFrequency::Hz1,
|
||||||
|
interrupt_control: InterruptControl::Interrupt, // Enable interrupt mode
|
||||||
|
battery_backed_square_wave: false,
|
||||||
|
oscillator_enable: Oscillator::Disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
match rtc.configure(&rtc_config).await {
|
||||||
|
Ok(_) => info!("DS3231 configured successfully"),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to configure DS3231: {:?}", e);
|
||||||
|
error!("DS3231 configuration failed");
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc.set_datetime(&naive_dt).await.unwrap_or_else(|e| {
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
|
||||||
|
error!("Failed to set RTC datetime: {:?}", e);
|
||||||
|
});
|
||||||
|
info!("RTC datetime set to: {}", naive_dt);
|
||||||
|
|
||||||
|
match rtc.status().await {
|
||||||
|
Ok(mut status) => {
|
||||||
|
status.set_alarm1_flag(false);
|
||||||
|
status.set_alarm2_flag(false);
|
||||||
|
match rtc.set_status(status).await {
|
||||||
|
Ok(_) => info!("Alarm flags cleared"),
|
||||||
|
Err(e) => info!("Failed to clear alarm flags: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => info!("Failed to read status: {:?}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_rtc_time<'a>(
|
||||||
|
rtc: &'a mut DS3231<I2c<'static, Async>>,
|
||||||
|
) -> Result<u64, DS3231Error<esp_hal::i2c::master::Error>> {
|
||||||
|
let timestamp_result = rtc.datetime().await?;
|
||||||
|
Ok(timestamp_result.and_utc().timestamp() as u64)
|
||||||
|
}
|
||||||
309
src/feedback.rs
309
src/feedback.rs
@@ -1,181 +1,142 @@
|
|||||||
use anyhow::Result;
|
use embassy_time::{Duration, Timer};
|
||||||
use log::error;
|
use esp_hal::rmt::Rmt;
|
||||||
use rgb::RGB8;
|
use esp_hal::peripherals;
|
||||||
use smart_leds::colors::{GREEN, RED};
|
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
||||||
use std::time::Duration;
|
use log::debug;
|
||||||
use tokio::{join, time::sleep};
|
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"))]
|
#[derive(Copy, Clone, Debug)]
|
||||||
use crate::{hardware::GPIOBuzzer, hardware::SpiLed};
|
pub enum FeedbackState {
|
||||||
|
Ack,
|
||||||
#[cfg(feature = "mock_pi")]
|
Nack,
|
||||||
use crate::hardware::{MockBuzzer, MockLed};
|
Error,
|
||||||
|
Startup,
|
||||||
const LED_BLINK_DURATION: Duration = Duration::from_secs(1);
|
WIFI,
|
||||||
|
Idle,
|
||||||
pub enum DeviceStatus {
|
|
||||||
NotReady,
|
|
||||||
Ready,
|
|
||||||
HotspotEnabled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceStatus {
|
const LED_LEVEL: u8 = 255;
|
||||||
pub fn color(&self) -> RGB8 {
|
|
||||||
match self {
|
//TODO ERROR STATE: 1 Blink = unknows error, 3 Blink = no sd card
|
||||||
Self::NotReady => RGB8::new(0, 0, 0),
|
|
||||||
Self::Ready => RGB8::new(0, 50, 0),
|
#[embassy_executor::task]
|
||||||
Self::HotspotEnabled => RGB8::new(0, 0, 50),
|
pub async fn feedback_task(
|
||||||
}
|
rmt: Rmt<'static, esp_hal::Async>,
|
||||||
}
|
led_gpio: peripherals::GPIO1<'static>,
|
||||||
}
|
buzzer_gpio: peripherals::GPIO21<'static>,
|
||||||
pub struct Feedback<B: Buzzer, L: StatusLed> {
|
) {
|
||||||
device_status: DeviceStatus,
|
debug!("Starting feedback task");
|
||||||
buzzer: B,
|
|
||||||
led: L,
|
let rmt_channel = rmt.channel0;
|
||||||
}
|
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); buffer_size_async(hardware::NUM_LEDS)];
|
||||||
|
|
||||||
impl<B: Buzzer, L: StatusLed> Feedback<B, L> {
|
let mut led = SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
|
||||||
pub async fn success(&mut self) {
|
|
||||||
let buzzer_handle = Self::beep_ack(&mut self.buzzer);
|
let mut buzzer = init::hardware::setup_buzzer(buzzer_gpio);
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, LED_BLINK_DURATION);
|
loop {
|
||||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
let feedback_state = FEEDBACK_STATE.wait().await;
|
||||||
|
match feedback_state {
|
||||||
buzzer_result.unwrap_or_else(|err| {
|
FeedbackState::Ack => {
|
||||||
error!("Failed to buzz: {err}");
|
led.write(brightness(
|
||||||
});
|
[GREEN; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
let _ = self.led_to_status();
|
))
|
||||||
}
|
.await
|
||||||
|
.unwrap();
|
||||||
pub async fn failure(&mut self) {
|
buzzer.set_high();
|
||||||
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
|
buzzer.set_low();
|
||||||
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
led.write(brightness(
|
||||||
|
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||||
buzzer_result.unwrap_or_else(|err| {
|
LED_LEVEL,
|
||||||
error!("Failed to buzz: {err}");
|
))
|
||||||
});
|
.await
|
||||||
|
.unwrap();
|
||||||
let _ = self.led_to_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
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()?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
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();
|
||||||
|
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,26 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
216
src/init/hardware.rs
Normal file
216
src/init/hardware.rs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
use core::cell::RefCell;
|
||||||
|
use critical_section::Mutex;
|
||||||
|
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::Input;
|
||||||
|
use esp_hal::i2c::master::Config;
|
||||||
|
use esp_hal::peripherals::{
|
||||||
|
GPIO0, 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,
|
||||||
|
timer::systimer::SystemTimer,
|
||||||
|
uart::Uart,
|
||||||
|
};
|
||||||
|
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
||||||
|
use esp_println::logger::init_logger;
|
||||||
|
use log::{debug, 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;
|
||||||
|
|
||||||
|
static SD_DET: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
|
#[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!();
|
||||||
|
|
||||||
|
pub async fn hardware_init(
|
||||||
|
spawner: Spawner,
|
||||||
|
) -> (
|
||||||
|
Uart<'static, Async>,
|
||||||
|
Stack<'static>,
|
||||||
|
I2c<'static, Async>,
|
||||||
|
Rmt<'static, esp_hal::Async>,
|
||||||
|
GPIO1<'static>,
|
||||||
|
GPIO21<'static>,
|
||||||
|
GPIO0<'static>,
|
||||||
|
SmartLedsAdapterAsync<'static, LED_BUFFER_SIZE>,
|
||||||
|
SDCardPersistence,
|
||||||
|
) {
|
||||||
|
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 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 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 rmt: Rmt<'_, esp_hal::Async> = {let frequency: Rate = Rate::from_mhz(80);
|
||||||
|
Rmt::new(peripherals.RMT, frequency)} .expect("Failed to initialize RMT")
|
||||||
|
.into_async();
|
||||||
|
|
||||||
|
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 led_gpio = peripherals.GPIO1;
|
||||||
|
let buzzer_gpio = peripherals.GPIO21;
|
||||||
|
|
||||||
|
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
|
||||||
|
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
debug!("hardware init done");
|
||||||
|
|
||||||
|
(
|
||||||
|
uart_device,
|
||||||
|
stack,
|
||||||
|
i2c_device,
|
||||||
|
rmt,
|
||||||
|
led_gpio,
|
||||||
|
buzzer_gpio,
|
||||||
|
sd_det_gpio,
|
||||||
|
led,
|
||||||
|
vol_mgr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_uart(
|
||||||
|
uart1: UART1<'static>,
|
||||||
|
uart_tx: GPIO16<'static>,
|
||||||
|
uart_rx: GPIO17<'static>,
|
||||||
|
) -> Uart<'static, Async> {
|
||||||
|
let uard_device = Uart::new(uart1, esp_hal::uart::Config::default().with_baudrate(9600));
|
||||||
|
|
||||||
|
match uard_device {
|
||||||
|
Ok(block) => block.with_rx(uart_rx).with_tx(uart_tx).into_async(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize UART: {e}");
|
||||||
|
panic!(); //TODO panic!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_i2c(
|
||||||
|
i2c0: I2C0<'static>,
|
||||||
|
sda: GPIO22<'static>,
|
||||||
|
scl: GPIO23<'static>,
|
||||||
|
) -> I2c<'static, Async> {
|
||||||
|
debug!("init I2C");
|
||||||
|
let config = Config::default().with_frequency(Rate::from_khz(400));
|
||||||
|
let i2c = match I2c::new(i2c0, config) {
|
||||||
|
Ok(i2c) => i2c.with_sda(sda).with_scl(scl).into_async(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize I2C: {:?}", e);
|
||||||
|
panic!(); //TODO panic!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
i2c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_spi(
|
||||||
|
spi2: SPI2<'static>,
|
||||||
|
sck: GPIO19<'static>,
|
||||||
|
miso: GPIO20<'static>,
|
||||||
|
mosi: GPIO18<'static>,
|
||||||
|
) -> Spi<'static, Blocking> {
|
||||||
|
let spi = match Spi::new(spi2, Spi_config::default()) {
|
||||||
|
Ok(spi) => spi.with_sck(sck).with_miso(miso).with_mosi(mosi),
|
||||||
|
Err(e) => panic!("Failed to initialize SPI: {:?}", e),
|
||||||
|
};
|
||||||
|
spi
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_buzzer(buzzer_gpio: GPIO21<'static>) -> Output<'static> {
|
||||||
|
let config = esp_hal::gpio::OutputConfig::default()
|
||||||
|
.with_drive_strength(esp_hal::gpio::DriveStrength::_40mA);
|
||||||
|
let buzzer = Output::new(buzzer_gpio, esp_hal::gpio::Level::Low, config);
|
||||||
|
|
||||||
|
buzzer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_led<'a>(
|
||||||
|
rmt: RMT<'a>,
|
||||||
|
led_gpio: GPIO1<'a>,
|
||||||
|
) -> esp_hal_smartled::SmartLedsAdapterAsync<'a, LED_BUFFER_SIZE> {
|
||||||
|
let rmt: Rmt<'_, esp_hal::Async> = {
|
||||||
|
let frequency: Rate = Rate::from_mhz(80);
|
||||||
|
Rmt::new(rmt, frequency)
|
||||||
|
}
|
||||||
|
.expect("Failed to initialize RMT")
|
||||||
|
.into_async();
|
||||||
|
|
||||||
|
let rmt_channel = rmt.channel0;
|
||||||
|
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); LED_BUFFER_SIZE];
|
||||||
|
|
||||||
|
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer)
|
||||||
|
}
|
||||||
76
src/init/network.rs
Normal file
76
src/init/network.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use core::{net::Ipv4Addr, str::FromStr};
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_radio::wifi::WifiDevice;
|
||||||
|
use static_cell::make_static;
|
||||||
|
|
||||||
|
use crate::webserver::WEB_TAKS_SIZE;
|
||||||
|
|
||||||
|
pub const NETWORK_STACK_SIZE: usize = WEB_TAKS_SIZE + 2; // + 2 for other network taks. Breaks
|
||||||
|
// without
|
||||||
|
|
||||||
|
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: Spawner) -> Stack<'a> {
|
||||||
|
let gw_ip_addr_str = "192.168.2.1";
|
||||||
|
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
|
||||||
|
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
||||||
|
address: Ipv4Cidr::new(gw_ip_addr, 24),
|
||||||
|
gateway: Some(gw_ip_addr),
|
||||||
|
dns_servers: Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let nw_stack: &'static mut StackResources<NETWORK_STACK_SIZE> =
|
||||||
|
make_static!(StackResources::<NETWORK_STACK_SIZE>::new());
|
||||||
|
|
||||||
|
let (stack, runner) = embassy_net::new(wifi, config, nw_stack, seed);
|
||||||
|
|
||||||
|
spawner.must_spawn(net_task(runner));
|
||||||
|
spawner.must_spawn(run_dhcp(stack, gw_ip_addr_str));
|
||||||
|
|
||||||
|
stack
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
||||||
|
use core::net::{Ipv4Addr, SocketAddrV4};
|
||||||
|
|
||||||
|
use edge_dhcp::{
|
||||||
|
io::{self, DEFAULT_SERVER_PORT},
|
||||||
|
server::{Server, ServerOptions},
|
||||||
|
};
|
||||||
|
use edge_nal::UdpBind;
|
||||||
|
use edge_nal_embassy::{Udp, UdpBuffers};
|
||||||
|
|
||||||
|
let ip = Ipv4Addr::from_str(gw_ip_addr).expect("dhcp task failed to parse gw ip");
|
||||||
|
|
||||||
|
let mut buf = [0u8; 1500];
|
||||||
|
|
||||||
|
let mut gw_buf = [Ipv4Addr::UNSPECIFIED];
|
||||||
|
|
||||||
|
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
|
||||||
|
let unbound_socket = Udp::new(stack, &buffers);
|
||||||
|
let mut bound_socket = unbound_socket
|
||||||
|
.bind(core::net::SocketAddr::V4(SocketAddrV4::new(
|
||||||
|
Ipv4Addr::UNSPECIFIED,
|
||||||
|
DEFAULT_SERVER_PORT,
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
_ = io::server::run(
|
||||||
|
&mut Server::<_, 64>::new_with_et(ip),
|
||||||
|
&ServerOptions::new(ip, Some(&mut gw_buf)),
|
||||||
|
&mut bound_socket,
|
||||||
|
&mut buf,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::warn!("DHCP server error: {e:?}"));
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
||||||
|
runner.run().await;
|
||||||
|
}
|
||||||
151
src/init/sd_card.rs
Normal file
151
src/init/sd_card.rs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
use alloc::vec::Vec;
|
||||||
|
use embassy_time::Delay;
|
||||||
|
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||||
|
use embedded_sdmmc::{SdCard, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager};
|
||||||
|
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
|
||||||
|
|
||||||
|
use crate::store::{AttendanceDay, IDMapping, day::Day, persistence::Persistence};
|
||||||
|
|
||||||
|
pub struct DummyTimesource;
|
||||||
|
|
||||||
|
impl TimeSource for DummyTimesource {
|
||||||
|
fn get_timestamp(&self) -> Timestamp {
|
||||||
|
Timestamp {
|
||||||
|
year_since_1970: 0,
|
||||||
|
zero_indexed_month: 0,
|
||||||
|
zero_indexed_day: 0,
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type VolMgr = VolumeManager<
|
||||||
|
SdCard<ExclusiveDevice<Spi<'static, Blocking>, Output<'static>, Delay>, Delay>,
|
||||||
|
DummyTimesource,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub fn setup_sdcard(spi_bus: Spi<'static, Blocking>, cs_pin: Output<'static>) -> SDCardPersistence {
|
||||||
|
let spi_device = ExclusiveDevice::new(spi_bus, cs_pin, Delay).unwrap();
|
||||||
|
let sd_card = SdCard::new(spi_device, Delay);
|
||||||
|
let vol_mgr = VolumeManager::new(sd_card, DummyTimesource);
|
||||||
|
|
||||||
|
SDCardPersistence { vol_mgr }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SDCardPersistence {
|
||||||
|
vol_mgr: VolMgr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SDCardPersistence {
|
||||||
|
const MAPPING_FILENAME: &'static str = "MAPPING.JS";
|
||||||
|
|
||||||
|
fn generate_filename(day: Day) -> ShortFileName {
|
||||||
|
let basename = day.to_string();
|
||||||
|
let mut filename: heapless::String<11> = heapless::String::new();
|
||||||
|
filename.push_str(&basename).unwrap();
|
||||||
|
filename.push_str(".js").unwrap();
|
||||||
|
|
||||||
|
ShortFileName::create_from_str(&filename).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Persistence for SDCardPersistence {
|
||||||
|
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
|
||||||
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
|
let filename = Self::generate_filename(day);
|
||||||
|
let file = root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly);
|
||||||
|
|
||||||
|
if file.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut open_file = file.unwrap();
|
||||||
|
|
||||||
|
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||||
|
let read = open_file.read(&mut read_buffer).unwrap();
|
||||||
|
open_file.close().unwrap();
|
||||||
|
|
||||||
|
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read]).unwrap();
|
||||||
|
|
||||||
|
Some(day)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_day(&mut self, day: Day, data: &AttendanceDay) {
|
||||||
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
|
let filename = Self::generate_filename(day);
|
||||||
|
|
||||||
|
let mut file = root_dir
|
||||||
|
.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
|
||||||
|
|
||||||
|
file.flush().unwrap();
|
||||||
|
file.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_mapping(&mut self) -> Option<crate::store::IDMapping> {
|
||||||
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
|
let file =
|
||||||
|
root_dir.open_file_in_dir(Self::MAPPING_FILENAME, embedded_sdmmc::Mode::ReadOnly);
|
||||||
|
|
||||||
|
if file.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut open_file = file.unwrap();
|
||||||
|
|
||||||
|
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||||
|
let read = open_file.read(&mut read_buffer).unwrap();
|
||||||
|
open_file.close().unwrap();
|
||||||
|
|
||||||
|
let mapping: IDMapping = serde_json::from_slice(&read_buffer[..read]).unwrap();
|
||||||
|
|
||||||
|
Some(mapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
|
||||||
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
|
let mut file = root_dir
|
||||||
|
.open_file_in_dir(
|
||||||
|
Self::MAPPING_FILENAME,
|
||||||
|
embedded_sdmmc::Mode::ReadWriteCreateOrTruncate,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
|
||||||
|
|
||||||
|
file.flush().unwrap();
|
||||||
|
file.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_days(&mut self) -> Vec<Day> {
|
||||||
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
|
let mut days_dir = root_dir.open_dir(".").unwrap();
|
||||||
|
|
||||||
|
let mut days: Vec<Day> = Vec::new();
|
||||||
|
days_dir
|
||||||
|
.iterate_dir(|e| {
|
||||||
|
let filename = e.name.clone();
|
||||||
|
|
||||||
|
if let Ok(day) = filename.try_into() {
|
||||||
|
days.push(day);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
days
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/init/wifi.rs
Normal file
62
src/init/wifi.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
static ESP_WIFI_CTRL: StaticCell<Controller<'static>> = StaticCell::new();
|
||||||
|
|
||||||
|
pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
|
||||||
|
let mut rf_switch = Output::new(gpio3, esp_hal::gpio::Level::Low, OutputConfig::default());
|
||||||
|
|
||||||
|
rf_switch.set_low();
|
||||||
|
|
||||||
|
Timer::after_millis(150).await;
|
||||||
|
|
||||||
|
let mut antenna_mode = Output::new(gpio14, esp_hal::gpio::Level::Low, OutputConfig::default());
|
||||||
|
|
||||||
|
antenna_mode.set_low();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_wifi<'d: 'static>(wifi: WIFI<'static>, spawner: Spawner) -> Interfaces<'d> {
|
||||||
|
let esp_wifi_ctrl = ESP_WIFI_CTRL.init(esp_radio::init().unwrap());
|
||||||
|
|
||||||
|
let config = esp_radio::wifi::Config::default();
|
||||||
|
let (controller, interfaces) = esp_radio::wifi::new(esp_wifi_ctrl, wifi, config).unwrap();
|
||||||
|
|
||||||
|
spawner.must_spawn(connection(controller));
|
||||||
|
|
||||||
|
interfaces
|
||||||
|
}
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn connection(mut controller: WifiController<'static>) {
|
||||||
|
debug!("start connection task");
|
||||||
|
debug!("Device capabilities: {:?}", controller.capabilities());
|
||||||
|
loop {
|
||||||
|
match 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/main.rs
Normal file
137
src/main.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
#![feature(type_alias_impl_trait)]
|
||||||
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
|
#![warn(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
signal::Signal,
|
||||||
|
};
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_hal::gpio::Input;
|
||||||
|
use esp_hal::{gpio::InputConfig, peripherals};
|
||||||
|
use log::{debug, info};
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
init::sd_card::SDCardPersistence,
|
||||||
|
store::{IDStore, day::Day, tally_id::TallyID},
|
||||||
|
webserver::start_webserver,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod drivers;
|
||||||
|
mod feedback;
|
||||||
|
mod init;
|
||||||
|
mod store;
|
||||||
|
mod webserver;
|
||||||
|
|
||||||
|
static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
|
||||||
|
|
||||||
|
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>;
|
||||||
|
|
||||||
|
static CHAN: StaticCell<TallyChannel> = StaticCell::new();
|
||||||
|
|
||||||
|
#[esp_rtos::main]
|
||||||
|
async fn main(spawner: Spawner) -> ! {
|
||||||
|
let (uart_device, stack, i2c, rmt, led_gpio, buzzer_gpio, sd_det_gpio, persistence_layer) =
|
||||||
|
init::hardware::hardware_init(spawner).await;
|
||||||
|
|
||||||
|
info!("Starting up...");
|
||||||
|
|
||||||
|
let mut rtc = drivers::rtc::RTCClock::new(i2c).await;
|
||||||
|
|
||||||
|
let store: UsedStore = IDStore::new_from_storage(persistence_layer).await;
|
||||||
|
let shared_store = Rc::new(Mutex::new(store));
|
||||||
|
|
||||||
|
let chan: &'static mut TallyChannel = CHAN.init(PubSubChannel::new());
|
||||||
|
let publisher: TallyPublisher = chan.publisher().unwrap();
|
||||||
|
let mut sub: TallySubscriber = chan.subscriber().unwrap();
|
||||||
|
|
||||||
|
wait_for_stack_up(stack).await;
|
||||||
|
|
||||||
|
start_webserver(spawner, stack, shared_store.clone(), chan);
|
||||||
|
|
||||||
|
/****************************** Spawning tasks ***********************************/
|
||||||
|
debug!("spawing NFC reader task...");
|
||||||
|
spawner.must_spawn(drivers::nfc_reader::rfid_reader_task(
|
||||||
|
uart_device,
|
||||||
|
publisher,
|
||||||
|
));
|
||||||
|
|
||||||
|
debug!("spawing feedback task..");
|
||||||
|
spawner.must_spawn(feedback::feedback_task(rmt, led_gpio, buzzer_gpio));
|
||||||
|
|
||||||
|
debug!("spawn sd detect task");
|
||||||
|
spawner.must_spawn(sd_detect_task(sd_det_gpio));
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn sd_detect_task(sd_det_gpio: peripherals::GPIO0<'static>) {
|
||||||
|
let mut sd_det = Input::new(sd_det_gpio, InputConfig::default());
|
||||||
|
sd_det.wait_for(esp_hal::gpio::Event::AnyEdge).await;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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,22 +1,25 @@
|
|||||||
use crate::tally_id::TallyID;
|
use alloc::collections::BTreeMap;
|
||||||
|
use alloc::string::String;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
|
use crate::store::tally_id::TallyID;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Name {
|
pub struct Name {
|
||||||
pub first: String,
|
pub first: String,
|
||||||
pub last: String,
|
pub last: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct IDMapping {
|
pub struct IDMapping {
|
||||||
id_map: HashMap<TallyID, Name>,
|
#[serde(flatten)]
|
||||||
|
id_map: BTreeMap<TallyID, Name>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IDMapping {
|
impl IDMapping {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
IDMapping {
|
IDMapping {
|
||||||
id_map: HashMap::new(),
|
id_map: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,49 +31,3 @@ impl IDMapping {
|
|||||||
self.id_map.insert(id, name);
|
self.id_map.insert(id, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic() {
|
|
||||||
let mut map = IDMapping::new();
|
|
||||||
let id1 = TallyID("A2Fb44".to_owned());
|
|
||||||
let name1 = Name {
|
|
||||||
first: "Max".to_owned(),
|
|
||||||
last: "Mustermann".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
map.add_mapping(id1.clone(), name1.clone());
|
|
||||||
|
|
||||||
let res = map.map(&id1);
|
|
||||||
|
|
||||||
assert_eq!(res, Some(&name1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multiple() {
|
|
||||||
let mut map = IDMapping::new();
|
|
||||||
let id1 = TallyID("A2Fb44".to_owned());
|
|
||||||
let name1 = Name {
|
|
||||||
first: "Max".to_owned(),
|
|
||||||
last: "Mustermann".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let id2 = TallyID("7D3DF5B5".to_owned());
|
|
||||||
let name2 = Name {
|
|
||||||
first: "First".to_owned(),
|
|
||||||
last: "Last".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
map.add_mapping(id1.clone(), name1.clone());
|
|
||||||
map.add_mapping(id2.clone(), name2.clone());
|
|
||||||
|
|
||||||
let res = map.map(&id1);
|
|
||||||
assert_eq!(res, Some(&name1));
|
|
||||||
|
|
||||||
let res = map.map(&id2);
|
|
||||||
assert_eq!(res, Some(&name2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,123 +1,22 @@
|
|||||||
use anyhow::{Result, anyhow};
|
use alloc::vec::Vec;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use std::collections::{HashMap, HashSet};
|
use serde::Serialize;
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
use crate::{store::IDMapping, tally_id::TallyID};
|
use super::IDMapping;
|
||||||
|
use crate::store::day::Day;
|
||||||
|
use crate::store::persistence::Persistence;
|
||||||
|
use crate::store::tally_id::TallyID;
|
||||||
|
|
||||||
/// Represents a single day that IDs can attend
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct AttendanceDay {
|
pub struct AttendanceDay {
|
||||||
date: String,
|
date: Day,
|
||||||
ids: Vec<TallyID>,
|
ids: Vec<TallyID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores all the days
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct IDStore {
|
|
||||||
days: HashMap<String, AttendanceDay>,
|
|
||||||
pub mapping: IDMapping,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IDStore {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
IDStore {
|
|
||||||
days: HashMap::new(),
|
|
||||||
mapping: IDMapping::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creats a new `IDStore` from a json file
|
|
||||||
pub async fn new_from_json(filepath: &str) -> Result<Self> {
|
|
||||||
let read_string = fs::read_to_string(filepath).await?;
|
|
||||||
Ok(serde_json::from_str(&read_string)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new id for the current day
|
|
||||||
/// Returns false if ID is already present at the current day.
|
|
||||||
pub fn add_id(&mut self, id: TallyID) -> bool {
|
|
||||||
self.get_current_day().add_id(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the `AttendanceDay` of the current day
|
|
||||||
/// Creates a new if not exists
|
|
||||||
pub fn get_current_day(&mut self) -> &mut AttendanceDay {
|
|
||||||
let current_day = get_day_str();
|
|
||||||
|
|
||||||
if self.days.contains_key(¤t_day) {
|
|
||||||
return self.days.get_mut(¤t_day).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.days.insert(
|
|
||||||
current_day.clone(),
|
|
||||||
AttendanceDay::new(¤t_day.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.days.get_mut(¤t_day.clone()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes the store to a json file
|
|
||||||
pub async fn export_json(&self, filepath: &str) -> Result<()> {
|
|
||||||
fs::write(filepath, serde_json::to_string(&self)?).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Export the store to a csv file.
|
|
||||||
/// With days in the rows and IDs in the collum.
|
|
||||||
pub fn export_csv(&self) -> Result<String> {
|
|
||||||
let mut csv = String::new();
|
|
||||||
let seperator = ";";
|
|
||||||
let mut user_ids: HashSet<TallyID> = HashSet::new();
|
|
||||||
|
|
||||||
for day in self.days.values() {
|
|
||||||
for id in day.ids.iter() {
|
|
||||||
user_ids.insert(id.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut user_ids: Vec<TallyID> = user_ids.into_iter().collect();
|
|
||||||
user_ids.sort();
|
|
||||||
|
|
||||||
let mut days: Vec<String> = self.days.keys().cloned().collect();
|
|
||||||
days.sort();
|
|
||||||
|
|
||||||
let header = days.join(seperator);
|
|
||||||
csv.push_str(&format!(
|
|
||||||
"ID{seperator}Nachname{seperator}Vorname{seperator}{header}\n"
|
|
||||||
));
|
|
||||||
|
|
||||||
for user_id in user_ids.iter() {
|
|
||||||
let id = &user_id.0.to_string();
|
|
||||||
let name = self.mapping.map(user_id);
|
|
||||||
|
|
||||||
let firstname = name.map(|e| e.first.clone()).unwrap_or("".to_owned());
|
|
||||||
let lastname = name.map(|e| e.last.clone()).unwrap_or("".to_owned());
|
|
||||||
|
|
||||||
csv.push_str(&format!("{id}{seperator}{lastname}{seperator}{firstname}"));
|
|
||||||
for day in days.iter() {
|
|
||||||
let was_there: bool = self
|
|
||||||
.days
|
|
||||||
.get(day)
|
|
||||||
.ok_or(anyhow!("Failed to access day"))?
|
|
||||||
.ids
|
|
||||||
.contains(user_id);
|
|
||||||
|
|
||||||
if was_there {
|
|
||||||
csv.push_str(&format!("{seperator}x"));
|
|
||||||
} else {
|
|
||||||
csv.push_str(seperator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
csv.push('\n');
|
|
||||||
}
|
|
||||||
Ok(csv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttendanceDay {
|
impl AttendanceDay {
|
||||||
fn new(day: &str) -> Self {
|
pub fn new(date: Day) -> Self {
|
||||||
Self {
|
Self {
|
||||||
date: day.to_owned(),
|
date,
|
||||||
ids: Vec::new(),
|
ids: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +32,81 @@ impl AttendanceDay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_day_str() -> String {
|
#[derive(Clone)]
|
||||||
let now = chrono::offset::Local::now();
|
pub struct IDStore<T: Persistence> {
|
||||||
now.format("%Y-%m-%d").to_string()
|
pub current_day: AttendanceDay,
|
||||||
|
pub mapping: IDMapping,
|
||||||
|
persistence_layer: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Persistence> IDStore<T> {
|
||||||
|
pub async fn new_from_storage(mut persistence_layer: T) -> Self {
|
||||||
|
let mapping = match persistence_layer.load_mapping().await {
|
||||||
|
Some(map) => map,
|
||||||
|
None => IDMapping::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_date: Day = Day::new(1);
|
||||||
|
|
||||||
|
let day = persistence_layer
|
||||||
|
.load_day(current_date)
|
||||||
|
.await
|
||||||
|
.unwrap_or(AttendanceDay::new(current_date));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
current_day: day,
|
||||||
|
mapping,
|
||||||
|
persistence_layer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_day(&mut self) {
|
||||||
|
self.persistence_layer
|
||||||
|
.save_day(self.current_day.date, &self.current_day)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn persist_mapping(&mut self) {
|
||||||
|
self.persistence_layer.save_mapping(&self.mapping).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new id for the current day
|
||||||
|
/// Returns false if ID is already present at the current day.
|
||||||
|
pub async fn add_id(&mut self, id: TallyID, current_date: Day) -> bool {
|
||||||
|
if self.current_day.date == current_date {
|
||||||
|
let changed = self.current_day.add_id(id);
|
||||||
|
if changed {
|
||||||
|
self.persist_day().await;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_day = AttendanceDay::new(current_date);
|
||||||
|
self.current_day = new_day;
|
||||||
|
|
||||||
|
let changed = self.current_day.add_id(id);
|
||||||
|
if changed {
|
||||||
|
self.persist_day().await;
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load and return a AttendanceDay. Nothing more. Nothing less.
|
||||||
|
pub async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
|
||||||
|
if day == self.current_day.date {
|
||||||
|
return Some(self.current_day.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.persistence_layer.load_day(day).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_days_in_timespan(&mut self, from: Day, to: Day) -> Vec<Day> {
|
||||||
|
let all_days = self.persistence_layer.list_days().await;
|
||||||
|
|
||||||
|
all_days
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| *e >= from)
|
||||||
|
.filter(|e| *e <= to)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
mod id_store;
|
pub use id_mapping::{IDMapping, Name};
|
||||||
mod id_mapping;
|
pub use id_store::{IDStore,AttendanceDay};
|
||||||
|
|
||||||
|
mod id_mapping;
|
||||||
|
pub mod persistence;
|
||||||
|
mod id_store;
|
||||||
|
pub mod tally_id;
|
||||||
|
pub mod day;
|
||||||
|
|
||||||
pub use id_store::IDStore;
|
|
||||||
pub use id_mapping::{IDMapping,Name};
|
|
||||||
|
|||||||
12
src/store/persistence.rs
Normal file
12
src/store/persistence.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use crate::store::{IDMapping, day::Day, id_store::AttendanceDay};
|
||||||
|
|
||||||
|
pub trait Persistence {
|
||||||
|
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay>;
|
||||||
|
async fn save_day(&mut self, day: Day, data: &AttendanceDay);
|
||||||
|
async fn list_days(&mut self) -> Vec<Day>;
|
||||||
|
|
||||||
|
async fn load_mapping(&mut self) -> Option<IDMapping>;
|
||||||
|
async fn save_mapping(&mut self, data: &IDMapping);
|
||||||
|
}
|
||||||
108
src/store/tally_id.rs
Normal file
108
src/store/tally_id.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use core::{fmt::Display, str::FromStr};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct TallyID([u8; 6]);
|
||||||
|
|
||||||
|
impl FromStr for TallyID {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
s.as_bytes().try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<heapless::String<12>> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: heapless::String<12>) -> Result<Self, Self::Error> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
|
||||||
|
let mut out: [u8; 6] = [0; 6];
|
||||||
|
for i in 0..6 {
|
||||||
|
let hi = hex_val(bytes[2 * i])?;
|
||||||
|
let lo = hex_val(bytes[2 * i + 1])?;
|
||||||
|
out[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TallyID(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_val(b: u8) -> Result<u8, ()> {
|
||||||
|
match b {
|
||||||
|
b'0'..=b'9' => Ok(b - b'0'),
|
||||||
|
b'a'..=b'f' => Ok(b - b'a' + 10),
|
||||||
|
b'A'..=b'F' => Ok(b - b'A' + 10),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TallyID> for heapless::String<12> {
|
||||||
|
fn from(value: TallyID) -> Self {
|
||||||
|
const HEX_CHARS: &[u8; 16] = b"0123456789ABCDEF";
|
||||||
|
let mut s: Self = Self::new();
|
||||||
|
|
||||||
|
for &b in &value.0 {
|
||||||
|
// Should be safe to unwrap since the string is already long enough
|
||||||
|
s.push(HEX_CHARS[(b >> 4) as usize] as char).unwrap();
|
||||||
|
s.push(HEX_CHARS[(b & 0x0F) as usize] as char).unwrap();
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a array of hex chars
|
||||||
|
impl TryFrom<&[u8]> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != 12 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: [u8; 6] = [0; 6];
|
||||||
|
for i in 0..6 {
|
||||||
|
let hi = hex_val(value[2 * i])?;
|
||||||
|
let lo = hex_val(value[2 * i + 1])?;
|
||||||
|
out[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TallyID(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<[u8; 12]> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: [u8; 12]) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(&value as &[u8])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TallyID {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let s: heapless::String<12> = (*self).into();
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for TallyID {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let s: heapless::String<12> = (*self).into();
|
||||||
|
serializer.serialize_str(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for TallyID {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = <&str>::deserialize(deserializer)?;
|
||||||
|
TallyID::from_str(s).map_err(|_| de::Error::custom("Failed to parse Tally ID"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
94
src/webserver/api.rs
Normal file
94
src/webserver/api.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use log::error;
|
||||||
|
use picoserve::{
|
||||||
|
extract::{Json, Query, State},
|
||||||
|
response::{self, IntoResponse},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
store::{Name, day::Day, tally_id::TallyID},
|
||||||
|
webserver::{app::AppState, sse::IDEvents},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NewMapping {
|
||||||
|
id: TallyID,
|
||||||
|
name: Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct QueryTimespan {
|
||||||
|
from: u64,
|
||||||
|
to: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct QueryDay {
|
||||||
|
timestamp: Option<u64>,
|
||||||
|
day: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/mapping
|
||||||
|
pub async fn get_mapping(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let store = state.store.lock().await;
|
||||||
|
response::Json(store.mapping.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/mapping
|
||||||
|
pub async fn add_mapping(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(data): Json<NewMapping>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
store.mapping.add_mapping(data.id, data.name);
|
||||||
|
store.persist_mapping().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE /api/idevent
|
||||||
|
pub async fn get_idevent(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||||
|
match state.chan.subscriber() {
|
||||||
|
Ok(chan) => Ok(response::EventStream(IDEvents(chan))),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create SSE: {:?}", e);
|
||||||
|
Err((
|
||||||
|
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Internal server error",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/days
|
||||||
|
pub async fn get_days(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(QueryTimespan { from, to }): Query<QueryTimespan>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let from_day = Day::new_from_timestamp(from);
|
||||||
|
let to_day = Day::new_from_timestamp(to);
|
||||||
|
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
|
||||||
|
let days = store.list_days_in_timespan(from_day, to_day).await;
|
||||||
|
|
||||||
|
response::Json(days)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/day
|
||||||
|
pub async fn get_day(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(QueryDay { timestamp, day }): Query<QueryDay>,
|
||||||
|
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||||
|
let parsed_day = timestamp
|
||||||
|
.map(Day::new_from_timestamp)
|
||||||
|
.or_else(|| day.map(Day::new))
|
||||||
|
.ok_or((response::StatusCode::NOT_FOUND, "Not found"))?;
|
||||||
|
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
|
||||||
|
match store.load_day(parsed_day).await {
|
||||||
|
Some(att_day) => Ok(response::Json(att_day)),
|
||||||
|
None => Err((response::StatusCode::NOT_FOUND, "Not found")),
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/webserver/app.rs
Normal file
32
src/webserver/app.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use alloc::rc::Rc;
|
||||||
|
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||||
|
use picoserve::{AppWithStateBuilder, routing::get};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
TallyChannel, UsedStore,
|
||||||
|
webserver::{
|
||||||
|
api::{add_mapping, get_day, get_days, get_idevent, get_mapping},
|
||||||
|
assets::Assets,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||||
|
pub chan: &'static TallyChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppProps;
|
||||||
|
|
||||||
|
impl AppWithStateBuilder for AppProps {
|
||||||
|
type State = AppState;
|
||||||
|
type PathRouter = impl picoserve::routing::PathRouter<AppState>;
|
||||||
|
|
||||||
|
fn build_app(self) -> picoserve::Router<Self::PathRouter, AppState> {
|
||||||
|
picoserve::Router::from_service(Assets)
|
||||||
|
.route("/api/mapping", get(get_mapping).post(add_mapping))
|
||||||
|
.route("/api/idevent", get(get_idevent))
|
||||||
|
.route("/api/days", get(get_days))
|
||||||
|
.route("/api/day", get(get_day))
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/webserver/assets.rs
Normal file
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
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/webserver/mod.rs
Normal file
59
src/webserver/mod.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
TallyChannel, UsedStore,
|
||||||
|
webserver::app::{AppProps, AppState},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod app;
|
||||||
|
mod assets;
|
||||||
|
mod sse;
|
||||||
|
|
||||||
|
pub const WEB_TAKS_SIZE: usize = 5; // Up this number if request start fail with Timeouts.
|
||||||
|
|
||||||
|
pub fn start_webserver(
|
||||||
|
spawner: Spawner,
|
||||||
|
stack: Stack<'static>,
|
||||||
|
store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||||
|
chan: &'static TallyChannel,
|
||||||
|
) {
|
||||||
|
let app = make_static!(AppProps.build_app());
|
||||||
|
|
||||||
|
let state = make_static!(AppState { store, chan });
|
||||||
|
|
||||||
|
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
||||||
|
start_read_request: Some(Duration::from_secs(5)),
|
||||||
|
persistent_start_read_request: Some(Duration::from_secs(5)),
|
||||||
|
read_request: Some(Duration::from_secs(5)),
|
||||||
|
write: Some(Duration::from_secs(5)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
for task_id in 0..WEB_TAKS_SIZE {
|
||||||
|
spawner.must_spawn(webserver_task(task_id, stack, app, config, state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task(pool_size = WEB_TAKS_SIZE)]
|
||||||
|
async fn webserver_task(
|
||||||
|
task_id: usize,
|
||||||
|
stack: embassy_net::Stack<'static>,
|
||||||
|
app: &'static AppRouter<AppProps>,
|
||||||
|
config: &'static picoserve::Config<Duration>,
|
||||||
|
state: &'static AppState,
|
||||||
|
) -> ! {
|
||||||
|
let mut tcp_rx_buffer = [0u8; 1024];
|
||||||
|
let mut tcp_tx_buffer = [0u8; 1024];
|
||||||
|
let mut http_buffer = [0u8; 2048];
|
||||||
|
|
||||||
|
picoserve::Server::new(&app.shared().with_state(state), config, &mut http_buffer)
|
||||||
|
.listen_and_serve(task_id, stack, 80, &mut tcp_rx_buffer, &mut tcp_tx_buffer)
|
||||||
|
.await
|
||||||
|
.into_never()
|
||||||
|
}
|
||||||
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}`);
|
||||||
|
});
|
||||||
871
web/package-lock.json
generated
871
web/package-lock.json
generated
@@ -14,6 +14,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
"svelte": "^5.28.1",
|
"svelte": "^5.28.1",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.1.6",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
@@ -1078,6 +1080,20 @@
|
|||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
@@ -1111,6 +1127,68 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"raw-body": "^3.0.0",
|
||||||
|
"type-is": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@@ -1146,6 +1224,49 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -1174,6 +1295,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
@@ -1183,6 +1314,38 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.1",
|
"version": "5.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||||
@@ -1196,6 +1359,39 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||||
@@ -1236,6 +1432,13 @@
|
|||||||
"@esbuild/win32-x64": "0.25.4"
|
"@esbuild/win32-x64": "0.25.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esm-env": {
|
"node_modules/esm-env": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
@@ -1253,6 +1456,59 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.4.4",
|
"version": "6.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||||
@@ -1267,6 +1523,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1281,12 +1575,164 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"toidentifier": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors/node_modules/statuses": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
@@ -1560,6 +2006,62 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@@ -1631,6 +2133,73 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1677,6 +2246,79 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"iconv-lite": "0.7.0",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body/node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -1730,6 +2372,23 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
@@ -1743,6 +2402,156 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.5",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.1",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1752,6 +2561,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.30.1",
|
"version": "5.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz",
|
||||||
@@ -1850,6 +2669,31 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
@@ -1864,6 +2708,26 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
@@ -1957,6 +2821,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"svelte": "^5.28.1",
|
"svelte": "^5.28.1",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.1.6",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"express": "^5.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
|
|||||||
@@ -3,15 +3,21 @@
|
|||||||
import IDTable from "./lib/IDTable.svelte";
|
import IDTable from "./lib/IDTable.svelte";
|
||||||
import LastId from "./lib/LastID.svelte";
|
import LastId from "./lib/LastID.svelte";
|
||||||
import AddIDModal from "./lib/AddIDModal.svelte";
|
import AddIDModal from "./lib/AddIDModal.svelte";
|
||||||
|
import ExportModal from "./lib/ExportModal.svelte";
|
||||||
|
import { generateCSVFile } from "./lib/exporting";
|
||||||
|
import { fetchMapping, type IDMap } from "./lib/IDMapping";
|
||||||
|
import { downloadBlob } from "./lib/downloadBlob";
|
||||||
|
|
||||||
let lastID: string = $state("");
|
let lastID: string = $state("");
|
||||||
|
let mapping: IDMap | null = $state(null);
|
||||||
|
|
||||||
let addModal: AddIDModal;
|
let addModal: AddIDModal;
|
||||||
let idTable: IDTable;
|
let exportModal: ExportModal;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
mapping = await fetchMapping();
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
let sse = new EventSource("/api/idevent");
|
let sse = new EventSource("/api/idevent");
|
||||||
|
|
||||||
sse.onmessage = (e) => {
|
sse.onmessage = (e) => {
|
||||||
lastID = e.data;
|
lastID = e.data;
|
||||||
};
|
};
|
||||||
@@ -25,13 +31,14 @@
|
|||||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<button
|
||||||
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
||||||
href="/api/csv"
|
onclick={() => {
|
||||||
download="anwesenheit.csv"
|
exportModal.open();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Download CSV
|
Export CSV
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<div class="pt-3 pb-2">
|
<div class="pt-3 pb-2">
|
||||||
<LastId
|
<LastId
|
||||||
@@ -42,15 +49,32 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IDTable bind:this={idTable} onEdit={(id,firstName,lastName)=>{
|
{#if mapping}
|
||||||
addModal.open(id,firstName,lastName);
|
<IDTable
|
||||||
}}/>
|
data={mapping}
|
||||||
|
onEdit={(id, firstName, lastName) => {
|
||||||
|
addModal.open(id, firstName, lastName);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddIDModal
|
<AddIDModal
|
||||||
bind:this={addModal}
|
bind:this={addModal}
|
||||||
onSubmitted={() => {
|
onSubmitted={async () => {
|
||||||
idTable.reloadData();
|
mapping = await fetchMapping();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExportModal
|
||||||
|
bind:this={exportModal}
|
||||||
|
onSubmitted={async (from, to) => {
|
||||||
|
if (!mapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let csvFile = await generateCSVFile(from, to, mapping);
|
||||||
|
|
||||||
|
downloadBlob("export.csv",csvFile,"text/csv");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
31
web/src/lib/Day.ts
Normal file
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 {
|
export interface IDMap {
|
||||||
[name: string]: Name
|
[name: string]: Name
|
||||||
}
|
}
|
||||||
@@ -11,6 +7,23 @@ export interface Name {
|
|||||||
last: string,
|
last: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stupidSerdeFix(pairs: [string, Name][]): IDMap {
|
||||||
|
const map: IDMap = {};
|
||||||
|
for (const [key, value] of pairs) {
|
||||||
|
map[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMapping(): Promise<IDMap> {
|
||||||
|
let res = await fetch("/api/mapping");
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
return stupidSerdeFix(data);
|
||||||
|
}
|
||||||
|
|
||||||
export async function addMapping(id: string, firstName: string, lastName: string) {
|
export async function addMapping(id: string, firstName: string, lastName: string) {
|
||||||
let req = await fetch("/api/mapping", {
|
let req = await fetch("/api/mapping", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { type IDMap } from "./IDMapping";
|
||||||
import type { IDMapping } from "./IDMapping";
|
|
||||||
let data: IDMapping | undefined = $state();
|
|
||||||
|
|
||||||
let { onEdit }: { onEdit?: (string,string,string) => void } = $props();
|
let {
|
||||||
|
onEdit,
|
||||||
export async function reloadData() {
|
data,
|
||||||
let res = await fetch("/api/mapping");
|
}: {
|
||||||
|
onEdit?: (id: string, firstName: string, lastName: string) => void;
|
||||||
data = await res.json();
|
data: IDMap;
|
||||||
}
|
} = $props();
|
||||||
|
|
||||||
let rows = $derived(
|
let rows = $derived(
|
||||||
data
|
data
|
||||||
? Object.entries(data.id_map).map(([id, value]) => ({
|
? Object.entries(data).map(([id, value]) => ({
|
||||||
id,
|
id,
|
||||||
...value,
|
...value,
|
||||||
}))
|
}))
|
||||||
@@ -43,17 +41,9 @@
|
|||||||
if (sortKey !== key) return "";
|
if (sortKey !== key) return "";
|
||||||
return sortDirection === "asc" ? "▲" : "▼";
|
return sortDirection === "asc" ? "▲" : "▼";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await reloadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data == null}
|
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
||||||
Loading...
|
|
||||||
{:else}
|
|
||||||
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
|
||||||
<table class="px-10">
|
<table class="px-10">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -84,8 +74,7 @@
|
|||||||
|
|
||||||
<span class="indicator">{indicator("first")}</span>
|
<span class="indicator">{indicator("first")}</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th> </th>
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -94,19 +83,22 @@
|
|||||||
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
|
<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.last}</td>
|
||||||
<td class="whitespace-nowrap pr-5">{row.first}</td>
|
<td class="whitespace-nowrap pr-5">{row.first}</td>
|
||||||
<td class="pr-5" ><button onclick={()=>{
|
<td class="pr-5"
|
||||||
onEdit && onEdit(row.id,row.first,row.last);
|
><button
|
||||||
}} class="cursor-pointer">🔧</button></td>
|
onclick={() => {
|
||||||
|
onEdit && onEdit(row.id, row.first, row.last);
|
||||||
|
}}
|
||||||
|
class="cursor-pointer">🔧</button
|
||||||
|
></td
|
||||||
|
>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
@reference "../app.css";
|
@reference "../app.css";
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
@apply ml-1 w-4 inline-block;
|
@apply ml-1 w-4 inline-block;
|
||||||
}
|
}
|
||||||
|
|||||||
103
web/src/lib/csv.ts
Normal file
103
web/src/lib/csv.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
interface CSVOptions {
|
||||||
|
delimiter?: string;
|
||||||
|
headerOrder?: string[];
|
||||||
|
eol?: string;
|
||||||
|
includeBOM?: boolean;
|
||||||
|
nullString?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowObject = Record<string, any>;
|
||||||
|
type InputRows = RowObject[];
|
||||||
|
|
||||||
|
export function generateCSVString(input: InputRows, opts: CSVOptions = {}): string {
|
||||||
|
const {
|
||||||
|
delimiter = ";",
|
||||||
|
headerOrder,
|
||||||
|
eol = "\r\n",
|
||||||
|
includeBOM = true,
|
||||||
|
nullString = "",
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
// Check if the value need quoting
|
||||||
|
// Usually not needed in our use case but still in case
|
||||||
|
const needsQuoting = (s: string) =>
|
||||||
|
s.includes(delimiter) || s.includes('"') || s.includes("\n") || s.includes("\r") || /^\s|\s$/.test(s);
|
||||||
|
|
||||||
|
// Transform the value of a cell in a SAFE string
|
||||||
|
const escapeCell = (raw: any): string => {
|
||||||
|
if (raw === null || raw === undefined) return nullString;
|
||||||
|
|
||||||
|
let s = stringify(raw);
|
||||||
|
|
||||||
|
// Replace quotes
|
||||||
|
if (s.includes('"')) s = s.replace(/"/g, '""');
|
||||||
|
|
||||||
|
return needsQuoting(s) ? `"${s}"` : s;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform the value of a cell into a string
|
||||||
|
function stringify(v: any): string {
|
||||||
|
if (v === null || v === undefined) return nullString;
|
||||||
|
if (v instanceof Date) return v.toLocaleDateString();
|
||||||
|
if (typeof v === "boolean") return v ? "X" : "";
|
||||||
|
|
||||||
|
if (typeof v === "object") {
|
||||||
|
// CHeck if array and join with "|"
|
||||||
|
if (Array.isArray(v)) return v.map(item => (item === null || item === undefined ? nullString : String(item))).join("|");
|
||||||
|
|
||||||
|
// If all fails parse it via json
|
||||||
|
// Should also not happen in our use case
|
||||||
|
try { return JSON.stringify(v); } catch { return String(v); }
|
||||||
|
}
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objs = (input as RowObject[]) || [];
|
||||||
|
|
||||||
|
// Derive headers in the order keys are first encountered (fixed: no reduce)
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const derivedOrder: string[] = [];
|
||||||
|
for (const obj of objs) {
|
||||||
|
if (!obj || typeof obj !== "object") continue;
|
||||||
|
for (const k of Object.keys(obj)) {
|
||||||
|
if (!seen.has(k)) {
|
||||||
|
seen.add(k);
|
||||||
|
derivedOrder.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply headerOrder if provided: put listed columns first (in the order provided),
|
||||||
|
// then append the remaining derived headers in their derived order.
|
||||||
|
let finalHeaders = derivedOrder.slice();
|
||||||
|
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
|
||||||
|
const headSet = new Set(headerOrder);
|
||||||
|
const first = headerOrder.filter(h => seen.has(h)); // keep only headers that actually exist
|
||||||
|
const rest = derivedOrder.filter(h => !headSet.has(h));
|
||||||
|
finalHeaders = first.concat(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsOut: string[] = [];
|
||||||
|
|
||||||
|
if (finalHeaders.length > 0) {
|
||||||
|
rowsOut.push(finalHeaders.map(h => escapeCell(h)).join(delimiter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse every row
|
||||||
|
for (const obj of objs) {
|
||||||
|
|
||||||
|
// Check if obj is null or somthing else we can't convert
|
||||||
|
if (!obj || typeof obj !== "object") {
|
||||||
|
|
||||||
|
// produce empty row with same number of columns
|
||||||
|
rowsOut.push(finalHeaders.map(() => escapeCell(null)).join(delimiter));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For every header check if a value on our row exist and print it
|
||||||
|
const row = finalHeaders.map(col => escapeCell(col in obj ? (obj as any)[col] : null)).join(delimiter);
|
||||||
|
rowsOut.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (includeBOM ? "\uFEFF" : "") + rowsOut.join(eol);
|
||||||
|
}
|
||||||
8
web/src/lib/downloadBlob.ts
Normal file
8
web/src/lib/downloadBlob.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function downloadBlob(filename: string, content: string, mimeType = "text/plain") {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
53
web/src/lib/exporting.ts
Normal file
53
web/src/lib/exporting.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { generateCSVString } from "./csv";
|
||||||
|
import { dayToDate, fetchDay, fetchDays, type AttendanceDay, type Day } from "./Day";
|
||||||
|
import type { IDMap } from "./IDMapping";
|
||||||
|
|
||||||
|
interface CSVRow {
|
||||||
|
ID: string
|
||||||
|
Vorname: string
|
||||||
|
Nachname: string
|
||||||
|
[key: string]: string | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareRows(mapping: IDMap, days: AttendanceDay[]): CSVRow[] {
|
||||||
|
let csvData: CSVRow[] = [];
|
||||||
|
|
||||||
|
const allIDs = Object.keys(mapping);
|
||||||
|
|
||||||
|
for (const id of allIDs) {
|
||||||
|
const name = mapping[id];
|
||||||
|
const row: CSVRow = {
|
||||||
|
ID: id,
|
||||||
|
Vorname: name.first,
|
||||||
|
Nachname: name.last,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const day of days) {
|
||||||
|
const dayKey = dayToDate(day.date).toLocaleDateString();
|
||||||
|
row[dayKey] = day.ids.includes(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
csvData.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return csvData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDays(from: Date, to: Date): Promise<AttendanceDay[]> {
|
||||||
|
const recordedDays: Day[] = await fetchDays(from, to);
|
||||||
|
let days: AttendanceDay[] = [];
|
||||||
|
|
||||||
|
for (const day of recordedDays) {
|
||||||
|
days.push(await fetchDay(day))
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateCSVFile(from: Date, to: Date, mapping: IDMap): Promise<string> {
|
||||||
|
const days = await getDays(from, to);
|
||||||
|
const rows = prepareRows(mapping, days);
|
||||||
|
const csvString = generateCSVString(rows);
|
||||||
|
|
||||||
|
return csvString;
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:3000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user