Compare commits

..

1 Commits

Author SHA1 Message Date
Philipp
bc4dc20012 fixed error with LED 2025-09-30 18:51:32 +02:00
48 changed files with 1690 additions and 3558 deletions

View File

@@ -12,7 +12,3 @@ target = "riscv32imac-unknown-none-elf"
[unstable]
build-std = ["alloc", "core"]
[env]
WIFI_PASSWD = "hunter22"
WIFI_SSID = "fwa"

941
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,44 +12,63 @@ bench = false
[dependencies]
esp-bootloader-esp-idf = "0.1.0"
esp-hal = { version = "1.0.0-rc.1", features = ["esp32c6", "unstable"] }
esp-alloc = "0.9.0"
esp-println = { version = "0.16.0", features = ["esp32c6", "log-04"] }
esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "log-04", "smoltcp","unstable"]}
esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
embassy-net = { version = "0.7.0", features = [
"dhcpv4",
"medium-ethernet",
"tcp",
"udp",
] }
embedded-hal = "=1.0.0"
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"
log = { version = "0.4" }
static_cell = { version = "2.1.1", features = ["nightly"] }
embassy-executor = { version = "0.7.0", features = ["nightly"] }
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 }
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" ] }
static_cell = { version = "2.1.0", features = ["nightly"] }
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
log = { version = "0.4" }
edge-dhcp = { version = "0.6.0", features = ["log"] }
edge-nal = "0.5.0"
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
picoserve = { 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"]}
picoserve = { version = "0.16.0", features = ["embassy", "log"] }
embassy-sync = { version = "0.7.0", features = ["log"] }
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
esp-hal-smartled = { git = "https://github.com/esp-rs/esp-hal-community.git", rev = "ab4316534d90e3a12785907f043f6899faee0f20", package = "esp-hal-smartled", features = ["esp32c6"]}
chrono = { version = "0.4.41", default-features = false }
dir-embed = "0.3.0"
esp-hal-smartled = { git = "https://github.com/esp-rs/esp-hal-community.git", package = "esp-hal-smartled", branch = "main", features = ["esp32c6"]}
smart-leds = "0.4.0"
serde = { version = "1.0.219", default-features = false, features = ["derive", "alloc"] }
embedded-sdmmc = "0.8.0"
embedded-hal-bus = "0.3.0"
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"]}
[profile.dev]
# Rust debug is too slow.

View File

@@ -1,6 +1,32 @@
# fw-anwesenheit
![PXL_20251004_141110955 MP (1)](https://github.com/user-attachments/assets/afffd664-507d-439a-a428-2477b2fe4de2)
# Setup
![PXL_20251001_103957322 MP~2](https://github.com/user-attachments/assets/8bc182df-a93d-4923-b8d8-88b56d1aa441)
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.

0
pcb/FETCH_HEAD Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"board": {
"active_layer": 7,
"active_layer": 6,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
@@ -49,11 +49,7 @@
"conflict_shadows",
"shapes"
],
<<<<<<< HEAD
"visible_layers": "00000000_00000000_0fffffff_fffff8aa",
=======
"visible_layers": "00000000_00000000_0fffffff_fffff8ab",
>>>>>>> 15c64e4 (updated enclousure top 3mf)
"visible_layers": "00000000_00000000_0fffffff_fffff8ef",
"zone_display_mode": 0
},
"git": {

View File

@@ -6157,7 +6157,7 @@
)
)
(symbol
(lib_id "power:+3.3V")
(lib_id "power:+5V")
(at 158.75 95.25 270)
(unit 1)
(exclude_from_sim no)
@@ -6165,8 +6165,8 @@
(on_board yes)
(dnp no)
(fields_autoplaced yes)
(uuid "2ae7a711-ee06-4c71-80b4-273d6f6d7ce6")
(property "Reference" "#PWR015"
(uuid "2964c836-6055-454e-b889-14ec73d51d79")
(property "Reference" "#PWR06"
(at 154.94 95.25 0)
(effects
(font
@@ -6175,7 +6175,7 @@
(hide yes)
)
)
(property "Value" "+3.3V"
(property "Value" "+5V"
(at 162.56 95.2499 90)
(effects
(font
@@ -6202,7 +6202,7 @@
(hide yes)
)
)
(property "Description" "Power symbol creates a global label with name \"+3.3V\""
(property "Description" "Power symbol creates a global label with name \"+5V\""
(at 158.75 95.25 0)
(effects
(font
@@ -6212,12 +6212,12 @@
)
)
(pin "1"
(uuid "04891225-ec31-465b-8d9c-5b738e413865")
(uuid "4da695c4-c5ff-46c5-b130-7514e0da8e01")
)
(instances
(project "fw-anwesenheit"
(project ""
(path "/ccbf1fda-befd-42da-bcb2-5d3829184012"
(reference "#PWR015")
(reference "#PWR06")
(unit 1)
)
)

Binary file not shown.

View File

@@ -1,213 +1,213 @@
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
317GND VIA MD0118PA00X+035433Y-045241X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+047750Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046550Y-043700X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046950Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+044450Y-033050X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043600Y-040300X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050950Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038800Y-048850X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+034150Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+040550Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+044550Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+040120Y-047880X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048850Y-037100X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+049500Y-036550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+036500Y-038600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+042300Y-042500X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+040900Y-042500X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048750Y-038400X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+041150Y-049250X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038800Y-049100X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+034850Y-040600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+051100Y-045600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+047150Y-036350X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048100Y-050150X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+034950Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+033051Y-044472X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043600Y-041800X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+044650Y-036950X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046340Y-045450X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038450Y-048600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+047750Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+049600Y-045500X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050787Y-045776X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+034050Y-039300X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+049350Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038950Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+042850Y-047400X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038600Y-041300X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050500Y-045950X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048100Y-049850X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050950Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+037200Y-048900X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+035750Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038600Y-041800X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+033200Y-049950X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+049950Y-038400X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050150Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+033350Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+044550Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043600Y-039800X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043600Y-041300X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+042950Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+047250Y-039150X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046340Y-044709X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+041350Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+041200Y-038450X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048400Y-049850X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+037350Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+037350Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050750Y-036850X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+032550Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+037550Y-046600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+045450Y-036150X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046950Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048420Y-050150X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038950Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043850Y-042500X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038150Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+045350Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046150Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038100Y-048600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046650Y-039000X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043750Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048550Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+045350Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043750Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050150Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+051100Y-045950X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+046150Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038800Y-048600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048050Y-039000X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043500Y-045350X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048550Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+040550Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+036550Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+049350Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050650Y-033050X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+041350Y-044650X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+042150Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038600Y-040300X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+043600Y-040800X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038100Y-048850X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038150Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+041350Y-044300X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+047250Y-046950X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+042950Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+041526Y-041339X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+050500Y-045600X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048850Y-037400X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+039750Y-031750X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+037645Y-038800X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+039750Y-050550X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038450Y-049100X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038600Y-040800X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+041640Y-047740X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038450Y-048850X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+048228Y-046885X0177Y0000R000S1628436659
317GND VIA MD0118PA00X+038100Y-049100X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+050480Y-044960X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038820Y-050080X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+050480Y-044600X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038460Y-050080X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+051040Y-044620X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+047360Y-044720X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038160Y-049860X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+037200Y-049500X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038820Y-049640X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+034451Y-049801X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+051040Y-044980X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038820Y-049860X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+047150Y-043700X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038460Y-049640X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038460Y-049860X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+049600Y-044900X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+050780Y-044780X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038160Y-050080X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+038160Y-049640X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+041050Y-047400X0177Y0000R000S1628436659
317+5V VIA MD0118PA00X+032874Y-036304X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+049606Y-040320X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038255Y-038800X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+040400Y-037150X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+047244Y-047638X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+035595Y-038976X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048000Y-037450X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+046457Y-040320X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+043350Y-048650X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+040450Y-048550X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038800Y-037150X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+046350Y-048650X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048150Y-044050X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+041300Y-047400X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048031Y-040320X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038450Y-047600X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048000Y-037100X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+041300Y-047150X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+046600Y-048650X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038800Y-047850X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+041050Y-047150X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048100Y-038400X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+050394Y-040354X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+043650Y-048650X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038450Y-048100X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038450Y-047850X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038800Y-048100X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+045669Y-040320X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+040350Y-038800X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+032750Y-043500X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+035595Y-038189X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048819Y-040320X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+047244Y-040320X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038800Y-047600X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038100Y-048100X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048228Y-047603X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038100Y-047850X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+036100Y-050350X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+038100Y-047600X0177Y0000R000S1628436659
317+3.3V VIA MD0118PA00X+048150Y-045550X0177Y0000R000S1628436659
317NET-(R3-PAD1) VIA MD0118PA00X+035433Y-042948X0177Y0000R000S1628436659
317/SD_DECT VIA MD0118PA00X+050138Y-037264X0177Y0000R000S1628436659
317/SD_DECT VIA MD0118PA00X+044800Y-049900X0177Y0000R000S1628436659
317/MISO VIA MD0118PA00X+038450Y-045800X0177Y0000R000S1628436659
317/MISO VIA MD0118PA00X+049272Y-037264X0177Y0000R000S1628436659
317/SPI_SCL VIA MD0118PA00X+048400Y-037264X0177Y0000R000S1628436659
317/SPI_SCL VIA MD0118PA00X+038450Y-044900X0177Y0000R000S1628436659
317/SPI_SCL VIA MD0118PA00X+048031Y-039601X0177Y0000R000S1628436659
317/MOSI VIA MD0118PA00X+037500Y-044550X0177Y0000R000S1628436659
317/MOSI VIA MD0118PA00X+047539Y-037264X0177Y0000R000S1628436659
317/SPI_CS VIA MD0118PA00X+046250Y-046000X0177Y0000R000S1628436659
317/SPI_CS VIA MD0118PA00X+047106Y-037264X0177Y0000R000S1628436659
317/BUZZER VIA MD0118PA00X+044450Y-046650X0177Y0000R000S1628436659
317-GPIO16_D6_TX) VIA MD0118PA00X+036480Y-047320X0177Y0000R000S1628436659
317-GPIO16_D6_TX) VIA MD0118PA00X+045700Y-046650X0177Y0000R000S1628436659
317/I2C_SCL VIA MD0118PA00X+043000Y-038300X0177Y0000R000S1628436659
317/I2C_SCL VIA MD0118PA00X+045600Y-046100X0177Y0000R000S1628436659
317/I2C_SCL VIA MD0118PA00X+043225Y-043575X0177Y0000R000S1628436659
317/I2C_SCL VIA MD0118PA00X+034680Y-046240X0177Y0000R000S1628436659
317/I2C_SDA VIA MD0118PA00X+042900Y-044100X0177Y0000R000S1628436659
317/I2C_SDA VIA MD0118PA00X+042900Y-038800X0177Y0000R000S1628436659
317/I2C_SDA VIA MD0118PA00X+044812Y-045842X0177Y0000R000S1628436659
317/I2C_SDA VIA MD0118PA00X+033300Y-046050X0177Y0000R000S1628436659
317/DAT2 VIA MD0118PA00X+046673Y-037264X0177Y0000R000S1628436659
317/DAT1 VIA MD0118PA00X+049705Y-037264X0177Y0000R000S1628436659
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-049800X0177Y0000R000S1628436659
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050000X0177Y0000R000S1628436659
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050200X0177Y0000R000S1628436659
317ET-(U3-~{RST}) VIA MD0118PA00X+033850Y-038500X0177Y0000R000S1628436659
317ET-(U3-~{RST}) VIA MD0118PA00X+037750Y-039800X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+042450Y-045000X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+045800Y-049600X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+042250Y-045000X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044750X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044450X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+046050Y-049350X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044450X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+045850Y-049400X0177Y0000R000S1628436659
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044750X0177Y0000R000S1628436659
327GND C5 -1 A01X+048730Y-038386X0354Y0374R180S2
327+3.3V C5 -2 A01X+048120Y-038386X0354Y0374R180S2
327/LED_DIN J3 -1 A01X+050700Y-043800X0984Y0669R000S2
@@ -239,7 +239,7 @@ P arrayDim N
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+5V 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

9
src/drivers/fram.rs Normal file
View File

@@ -0,0 +1,9 @@
const DEVICE_TYPE_CODE: u8 = 0b10100000;
const DEVICE_ADDRESS_CODE: u8 = 0b000000; // 3 bits for device address | default A0 = 0 A1 = 0 A2 = 0
const WRITE_CODE: u8 = 0b00000000; // 0 for write
const READ_CODE: u8 = 0b00000001; // 1 for read
const DEVICE_ADDRESS_WRITE: u8 = DEVICE_TYPE_CODE | DEVICE_ADDRESS_CODE | WRITE_CODE; // I2C address write for FRAM
const DEVICE_ADDRESS_READ: u8 = DEVICE_TYPE_CODE | DEVICE_ADDRESS_CODE | READ_CODE; // I2C address read for FRAM

View File

@@ -1,6 +1,6 @@
use embassy_time::{Duration, Timer};
use esp_hal::{Async, uart::Uart};
use log::{debug, info, warn};
use log::{debug, info};
use crate::TallyPublisher;
@@ -17,15 +17,7 @@ pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: Tally
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");
}
};
chan.publish([1, 0, 2, 5, 0, 8, 12, 15]).await;
}
Err(e) => {
log::error!("Error reading from UART: {e}");
@@ -34,35 +26,3 @@ pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: Tally
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
}

View File

@@ -1,4 +1,3 @@
use chrono::{TimeZone, Utc};
use ds3231::{
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
TimeRepresentation,
@@ -10,15 +9,12 @@ use esp_hal::{
use log::{debug, error, info};
use crate::{FEEDBACK_STATE, drivers, feedback};
use chrono::{TimeZone, Utc};
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>>,
}
@@ -47,33 +43,6 @@ impl RTCClock {
}
}
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
@@ -93,9 +62,8 @@ pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>>
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);
info!("Failed to configure DS3231: {:?}", e);
panic!("DS3231 configuration failed");
}
}

View File

@@ -1,13 +1,12 @@
use embassy_time::{Duration, Timer};
use esp_hal::rmt::Rmt;
use esp_hal::peripherals;
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
use log::debug;
use embassy_time::{Delay, Duration, Timer};
use esp_hal::{delay, gpio::Output, peripherals, rmt::ConstChannelAccess};
use esp_hal_smartled::SmartLedsAdapterAsync;
use init::hardware;
use log::{debug, error, info};
use smart_leds::SmartLedsWriteAsync;
use smart_leds::colors::{BLACK, GREEN, RED, YELLOW};
use smart_leds::{brightness, colors::BLUE};
use crate::init::hardware;
use crate::{FEEDBACK_STATE, init};
#[derive(Copy, Clone, Debug)]
@@ -26,18 +25,14 @@ const LED_LEVEL: u8 = 255;
#[embassy_executor::task]
pub async fn feedback_task(
rmt: Rmt<'static, esp_hal::Async>,
led_gpio: peripherals::GPIO1<'static>,
buzzer_gpio: peripherals::GPIO21<'static>,
mut led: SmartLedsAdapterAsync<
ConstChannelAccess<esp_hal::rmt::Tx, 0>,
{ init::hardware::LED_BUFFER_SIZE },
>,
buzzer: peripherals::GPIO21<'static>,
) {
debug!("Starting feedback task");
let rmt_channel = rmt.channel0;
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); buffer_size_async(hardware::NUM_LEDS)];
let mut led = SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
let mut buzzer = init::hardware::setup_buzzer(buzzer_gpio);
let mut buzzer = init::hardware::setup_buzzer(buzzer);
loop {
let feedback_state = FEEDBACK_STATE.wait().await;
match feedback_state {
@@ -52,12 +47,6 @@ pub async fn feedback_task(
Timer::after(Duration::from_millis(100)).await;
buzzer.set_low();
Timer::after(Duration::from_millis(50)).await;
led.write(brightness(
[BLACK; init::hardware::NUM_LEDS].into_iter(),
LED_LEVEL,
))
.await
.unwrap();
}
FeedbackState::Nack => {
led.write(brightness(
@@ -130,7 +119,7 @@ pub async fn feedback_task(
}
FeedbackState::Idle => {
led.write(brightness(
[BLACK; init::hardware::NUM_LEDS].into_iter(),
[GREEN; init::hardware::NUM_LEDS].into_iter(),
LED_LEVEL,
))
.await
@@ -140,3 +129,106 @@ pub async fn feedback_task(
debug!("Feedback state: {:?}", feedback_state);
}
}
// async fn beep_ack() {
// buzzer.set_high();
// buzzer.set_low();
// //Timer::after(Duration::from_millis(100)).await;
// }
/* pub async fn failure(&mut self) {
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
buzzer_result.unwrap_or_else(|err| { error!("Failed to buzz: {err}");
});
let _ = self.led_to_status();
}
pub async fn activate_error_state(&mut self) -> Result<()> {
self.led.turn_on(RED)?;
Self::beep_nak(&mut self.buzzer).await?;
Ok(())
}
pub async fn startup(&mut self){
self.device_status = DeviceStatus::Ready;
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, Duration::from_secs(1));
let buzzer_handle = Self::beep_startup(&mut self.buzzer);
let (buzzer_result, led_result) = join!(buzzer_handle, led_handle);
buzzer_result.unwrap_or_else(|err| {
error!("Failed to buzz: {err}");
});
led_result.unwrap_or_else(|err| {
error!("Failed to blink led: {err}");
});
let _ = self.led_to_status();
}
async fn flash_led_for_duration(led: &mut L, color: RGB8, duration: Duration) -> Result<()> {
led.turn_on(color)?;
sleep(duration).await;
led.turn_off()?;
Ok(())
}
async fn beep_ack(buzzer: &mut B) -> Result<()> {
buzzer
.modulated_tone(1200.0, Duration::from_millis(100))
.await?;
sleep(Duration::from_millis(10)).await;
buzzer
.modulated_tone(2000.0, Duration::from_millis(50))
.await?;
Ok(())
}
async fn beep_nak(buzzer: &mut B) -> Result<()> {
buzzer
.modulated_tone(600.0, Duration::from_millis(150))
.await?;
sleep(Duration::from_millis(100)).await;
buzzer
.modulated_tone(600.0, Duration::from_millis(150))
.await?;
Ok(())
}
async fn beep_startup(buzzer: &mut B) -> Result<()> {
buzzer
.modulated_tone(523.0, Duration::from_millis(150))
.await?;
buzzer
.modulated_tone(659.0, Duration::from_millis(150))
.await?;
buzzer
.modulated_tone(784.0, Duration::from_millis(150))
.await?;
buzzer
.modulated_tone(1046.0, Duration::from_millis(200))
.await?;
sleep(Duration::from_millis(100)).await;
buzzer
.modulated_tone(784.0, Duration::from_millis(100))
.await?;
buzzer
.modulated_tone(880.0, Duration::from_millis(200))
.await?;
Ok(())
}
*/

View File

@@ -1,19 +1,22 @@
use core::cell::RefCell;
use bleps::att::Att;
use critical_section::Mutex;
use ds3231::InterruptControl;
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::gpio::{Input, InputConfig};
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::rmt::{ConstChannelAccess, Rmt};
use esp_hal::spi::master::{Config as Spi_config, Spi};
use esp_hal::system::software_reset;
use esp_hal::Blocking;
use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::{
@@ -25,12 +28,16 @@ use esp_hal::{
uart::Uart,
};
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
use esp_println::dbg;
use esp_println::logger::init_logger;
use log::{debug, error};
use log::{debug, error, info};
use crate::FEEDBACK_STATE;
use crate::init::network;
use crate::init::sd_card::{SDCardPersistence, setup_sdcard};
use crate::init::sd_card::setup_sdcard;
use crate::init::wifi;
use crate::store::AttendanceDay;
use crate::store::persistence::Persistence;
/*************************************************
* GPIO Pinout Xiao Esp32c6
@@ -49,50 +56,46 @@ use crate::init::wifi;
*
*************************************************/
pub const NUM_LEDS: usize = 1;
pub const NUM_LEDS: usize = 66;
pub const LED_BUFFER_SIZE: usize = NUM_LEDS * 25;
static SD_DET: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
let delay = Delay::new();
loop {
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,
spawner: &mut Spawner,
) -> (
Uart<'static, Async>,
Stack<'static>,
I2c<'static, Async>,
Rmt<'static, esp_hal::Async>,
GPIO1<'static>,
SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE>,
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);
esp_alloc::heap_allocator!(size: 72 * 1024);
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);
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(timer0.alarm0);
init_logger(log::LevelFilter::Debug);
let rng = esp_hal::rng::Rng::new();
let timer1 = TimerGroup::new(peripherals.TIMG0);
let mut rng = esp_hal::rng::Rng::new(peripherals.RNG);
let network_seed = (rng.random() as u64) << 32 | rng.random() as u64;
wifi::set_antenna_mode(peripherals.GPIO3, peripherals.GPIO14).await;
let interfaces = wifi::setup_wifi(peripherals.WIFI, spawner);
let interfaces = wifi::setup_wifi(timer1.timer0, rng, peripherals.WIFI, spawner);
let stack = network::setup_network(network_seed, interfaces.ap, spawner);
Timer::after(Duration::from_millis(1)).await;
@@ -101,11 +104,7 @@ pub async fn hardware_init(
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 mut sd_det_gpio = peripherals.GPIO0;
let spi_bus = setup_spi(
peripherals.SPI2,
@@ -120,27 +119,23 @@ pub async fn hardware_init(
OutputConfig::default(),
);
let vol_mgr = setup_sdcard(spi_bus, sd_cs_pin);
let mut 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;
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
debug!("hardware init done");
(
uart_device,
stack,
i2c_device,
rmt,
led_gpio,
led,
buzzer_gpio,
sd_det_gpio,
led,
vol_mgr,
)
}
@@ -198,10 +193,11 @@ pub fn setup_buzzer(buzzer_gpio: GPIO21<'static>) -> Output<'static> {
buzzer
}
fn setup_led<'a>(
rmt: RMT<'a>,
led_gpio: GPIO1<'a>,
) -> esp_hal_smartled::SmartLedsAdapterAsync<'a, LED_BUFFER_SIZE> {
fn setup_led(
rmt: RMT<'static>,
led_gpio: GPIO1<'static>,
) -> SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE> {
debug!("setup led");
let rmt: Rmt<'_, esp_hal::Async> = {
let frequency: Rate = Rate::from_mhz(80);
Rmt::new(rmt, frequency)
@@ -210,7 +206,10 @@ fn setup_led<'a>(
.into_async();
let rmt_channel = rmt.channel0;
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); LED_BUFFER_SIZE];
let rmt_buffer = [0_u32; buffer_size_async(NUM_LEDS)];
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer)
let led: SmartLedsAdapterAsync<_, LED_BUFFER_SIZE> =
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
led
}

View File

@@ -1,16 +1,13 @@
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 esp_wifi::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> {
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: &mut Spawner) -> Stack<'a> {
let gw_ip_addr_str = "192.168.2.1";
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
@@ -19,10 +16,8 @@ pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: Spawner)
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);
let (stack, runner) =
embassy_net::new(wifi, config, make_static!(StackResources::<3>::new()), seed);
spawner.must_spawn(net_task(runner));
spawner.must_spawn(run_dhcp(stack, gw_ip_addr_str));
@@ -74,3 +69,4 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await;
}

View File

@@ -1,10 +1,10 @@
use alloc::vec::Vec;
use embassy_time::Delay;
use embedded_hal_bus::spi::ExclusiveDevice;
use embedded_sdmmc::{SdCard, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager};
use embedded_sdmmc::{SdCard, TimeSource, Timestamp, VolumeIdx, VolumeManager};
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
use crate::store::{AttendanceDay, IDMapping, day::Day, persistence::Persistence};
use crate::store::{AttendanceDay, Date, persistence::Persistence};
pub struct DummyTimesource;
@@ -38,111 +38,52 @@ 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> {
async fn load_day(&mut self, day: crate::store::Date) -> 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 file = root_dir
.open_file_in_dir("day.jsn", embedded_sdmmc::Mode::ReadOnly)
.unwrap();
let mut read_buffer: [u8; 1024] = [0; 1024];
let read = open_file.read(&mut read_buffer).unwrap();
open_file.close().unwrap();
let read = file.read(&mut read_buffer).unwrap();
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) {
async fn save_day(&mut self, day: Date, 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)
.open_file_in_dir("day.jsn", embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
.unwrap();
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
file.flush().unwrap();
file.close().unwrap();
file.flush();
file.close();
}
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)
todo!()
}
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();
todo!()
}
async fn list_days(&mut self) -> Vec<Day> {
async fn list_days(&mut self) -> Vec<Date> {
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("days").unwrap();
let mut days_dir = root_dir.open_dir(".").unwrap();
let mut days: Vec<Day> = Vec::new();
let mut days = Vec::new();
days_dir
.iterate_dir(|e| {
let filename = e.name.clone();
if let Ok(day) = filename.try_into() {
days.push(day);
}
days.push(1);
})
.unwrap();

View File

@@ -2,14 +2,9 @@ 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();
use esp_wifi::wifi::{AccessPointConfiguration, Configuration, WifiController, WifiEvent, WifiState};
use esp_wifi::{EspWifiRngSource, EspWifiTimerSource, wifi::Interfaces};
use static_cell::make_static;
pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
let mut rf_switch = Output::new(gpio3, esp_hal::gpio::Level::Low, OutputConfig::default());
@@ -23,23 +18,26 @@ pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
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());
pub fn setup_wifi<'d: 'static>(
timer: impl EspWifiTimerSource + 'd,
rng: impl EspWifiRngSource + 'd,
wifi: WIFI<'static>,
spawner: &mut Spawner,
) -> Interfaces<'d> {
let esp_wifi_ctrl = make_static!(esp_wifi::init(timer, rng).unwrap());
let config = esp_radio::wifi::Config::default();
let (controller, interfaces) = esp_radio::wifi::new(esp_wifi_ctrl, wifi, config).unwrap();
let (controller, interfaces) = esp_wifi::wifi::new(esp_wifi_ctrl, wifi).unwrap();
spawner.must_spawn(connection(controller));
interfaces
}
#[embassy_executor::task]
async fn connection(mut controller: WifiController<'static>) {
debug!("start connection task");
debug!("Device capabilities: {:?}", controller.capabilities());
loop {
match esp_radio::wifi::ap_state() {
WifiApState::Started => {
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
@@ -47,16 +45,12 @@ async fn connection(mut controller: WifiController<'static>) {
_ => {}
}
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");
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
ssid: "esp-wifi".try_into().unwrap(),
..Default::default()
});
controller.set_configuration(&client_config).unwrap();
controller.start_async().await.unwrap();
debug!("Wifi started!");
}
}
}

View File

@@ -2,16 +2,14 @@
#![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,
channel::Channel,
pubsub::{
PubSubChannel, Publisher, Subscriber,
PubSubChannel, Publisher,
WaitResult::{Lagged, Message},
},
signal::Signal,
@@ -20,50 +18,39 @@ use embassy_time::{Duration, Timer};
use esp_hal::gpio::Input;
use esp_hal::{gpio::InputConfig, peripherals};
use log::{debug, info};
use static_cell::StaticCell;
use static_cell::make_static;
use crate::store::TallyID;
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;
//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();
#[esp_hal_embassy::main]
async fn main(mut spawner: Spawner) {
let (uart_device, stack, _i2c, _led, buzzer_gpio, sd_det_gpio) =
init::hardware::hardware_init(&mut spawner).await;
wait_for_stack_up(stack).await;
start_webserver(spawner, stack, shared_store.clone(), chan);
info!("Starting up...");
let chan: &'static mut TallyChannel = make_static!(PubSubChannel::new());
//start_webserver(&mut spawner, stack);
let publisher = chan.publisher().unwrap();
let mut rtc = drivers::rtc::RTCClock::new(_i2c).await;
/****************************** Spawning tasks ***********************************/
debug!("spawing NFC reader task...");
@@ -73,37 +60,34 @@ async fn main(spawner: Spawner) -> ! {
));
debug!("spawing feedback task..");
spawner.must_spawn(feedback::feedback_task(rmt, led_gpio, buzzer_gpio));
spawner.must_spawn(feedback::feedback_task(_led, buzzer_gpio));
debug!("spawn sd detect task");
spawner.must_spawn(sd_detect_task(sd_det_gpio));
/******************************************************************************/
let mut sub = chan.subscriber().unwrap();
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:?}");
rtc.get_time().await;
info!("Current RTC time: {}", rtc.get_time().await);
Timer::after(Duration::from_millis(1000)).await;
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);
}
}
}
// let wait_result = sub.next_message().await;
// match wait_result {
// Lagged(_) => debug!("Lagged"),
// Message(msg) => debug!("Got message: {msg:?}"),
// }
}
}
#[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;
sd_det.wait_for(esp_hal::gpio::Event::AnyEdge);
loop {
sd_det.wait_for_any_edge().await;

View File

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

View File

@@ -1,18 +1,16 @@
use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::string::String;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use crate::store::tally_id::TallyID;
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize)]
pub struct Name {
pub first: String,
pub last: String,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize)]
pub struct IDMapping {
#[serde(flatten)]
id_map: BTreeMap<TallyID, Name>,
}

View File

@@ -1,20 +1,20 @@
use crate::store::persistence::Persistence;
use super::Date;
use super::IDMapping;
use super::TallyID;
use alloc::vec::Vec;
use serde::Deserialize;
use serde::Serialize;
use super::IDMapping;
use crate::store::day::Day;
use crate::store::persistence::Persistence;
use crate::store::tally_id::TallyID;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct AttendanceDay {
date: Day,
date: Date,
ids: Vec<TallyID>,
}
impl AttendanceDay {
pub fn new(date: Day) -> Self {
pub fn new(date: Date) -> Self {
Self {
date,
ids: Vec::new(),
@@ -46,7 +46,7 @@ impl<T: Persistence> IDStore<T> {
None => IDMapping::new(),
};
let current_date: Day = Day::new(1);
let current_date: Date = 1;
let day = persistence_layer
.load_day(current_date)
@@ -66,13 +66,15 @@ impl<T: Persistence> IDStore<T> {
.await
}
pub async fn persist_mapping(&mut self) {
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 {
pub async fn add_id(&mut self, id: TallyID) -> bool {
let current_date: Date = 1;
if self.current_day.date == current_date {
let changed = self.current_day.add_id(id);
if changed {
@@ -90,23 +92,4 @@ impl<T: Persistence> IDStore<T> {
}
changed
}
/// Load and return a AttendanceDay. Nothing more. Nothing less.
pub async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
if day == self.current_day.date {
return Some(self.current_day.clone());
}
self.persistence_layer.load_day(day).await
}
pub async fn list_days_in_timespan(&mut self, from: Day, to: Day) -> Vec<Day> {
let all_days = self.persistence_layer.list_days().await;
all_days
.into_iter()
.filter(|e| *e >= from)
.filter(|e| *e <= to)
.collect()
}
}

View File

@@ -1,9 +1,9 @@
pub use id_mapping::{IDMapping, Name};
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_mapping::{IDMapping, Name};
pub use id_store::{IDStore,AttendanceDay};
pub type TallyID = [u8; 8];
pub type Date = u64;

View File

@@ -1,11 +1,11 @@
use alloc::vec::Vec;
use crate::store::{IDMapping, day::Day, id_store::AttendanceDay};
use crate::store::{Date, IDMapping, 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_day(&mut self, day: Date) -> Option<AttendanceDay>;
async fn save_day(&mut self, day: Date, data: &AttendanceDay);
async fn list_days(&mut self) -> Vec<Date>;
async fn load_mapping(&mut self) -> Option<IDMapping>;
async fn save_mapping(&mut self, data: &IDMapping);

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ impl<State, CurrentPathParameters>
);
response_writer
.write_response(request.body_connection.finalize().await?, response)
.write_response(request.body_connection.finalize().await.unwrap(), response)
.await
}
None => {
@@ -68,7 +68,10 @@ impl Content for StaticAsset {
self.0.len()
}
async fn write_content<W: edge_nal::io::Write>(self, mut writer: W) -> Result<(), W::Error> {
async fn write_content<W: embedded_io_async::Write>(
self,
mut writer: W,
) -> Result<(), W::Error> {
writer.write_all(self.0).await
}
}

View File

@@ -1,59 +1,54 @@
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 picoserve::{AppBuilder, AppRouter, routing::get};
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,
) {
pub fn start_webserver(spawner: &mut Spawner, stack: Stack<'static>) {
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)),
persistent_start_read_request: Some(Duration::from_secs(1)),
read_request: Some(Duration::from_secs(1)),
write: Some(Duration::from_secs(1)),
}));
for task_id in 0..WEB_TAKS_SIZE {
spawner.must_spawn(webserver_task(task_id, stack, app, config, state));
let _ = spawner.spawn(webserver_task(0, stack, app, config));
}
struct AppProps;
impl AppBuilder for AppProps {
type PathRouter = impl picoserve::routing::PathRouter;
fn build_app(self) -> picoserve::Router<Self::PathRouter> {
picoserve::Router::from_service(assets::Assets).route("/api/a", get(async move || "Hello"))
}
}
#[embassy_executor::task(pool_size = WEB_TAKS_SIZE)]
#[embassy_executor::task]
async fn webserver_task(
task_id: usize,
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)
picoserve::listen_and_serve(
id,
app,
config,
stack,
80,
&mut tcp_rx_buffer,
&mut tcp_tx_buffer,
&mut http_buffer,
)
.await
.into_never()
}

View File

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

View File

@@ -1,108 +0,0 @@
{
"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"
]
}
]
}

View File

@@ -1,111 +0,0 @@
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
View File

@@ -14,8 +14,6 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4",
"body-parser": "^2.2.0",
"express": "^5.1.0",
"svelte": "^5.28.1",
"svelte-check": "^4.1.6",
"typescript": "~5.8.3",
@@ -1080,20 +1078,6 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"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": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -1127,68 +1111,6 @@
"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": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1224,49 +1146,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": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -1295,16 +1174,6 @@
"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": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -1314,38 +1183,6 @@
"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": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -1359,39 +1196,6 @@
"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": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
@@ -1432,13 +1236,6 @@
"@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": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
@@ -1456,59 +1253,6 @@
"@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": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
@@ -1523,44 +1267,6 @@
}
}
},
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1575,164 +1281,12 @@
"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": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@@ -2006,62 +1560,6 @@
"@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": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -2133,73 +1631,6 @@
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2246,79 +1677,6 @@
"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": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2372,23 +1730,6 @@
"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": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -2402,156 +1743,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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2561,16 +1752,6 @@
"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": {
"version": "5.30.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz",
@@ -2669,31 +1850,6 @@
"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": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -2708,26 +1864,6 @@
"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": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
@@ -2821,13 +1957,6 @@
}
}
},
"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": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,7 @@
export interface IDMapping {
id_map: IDMap
}
export interface IDMap {
[name: string]: Name
}
@@ -7,23 +11,6 @@ export interface Name {
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) {
let req = await fetch("/api/mapping", {
method: "POST",

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
target: "http://localhost:8080",
},
},
},