Compare commits

..

67 Commits

Author SHA1 Message Date
Philipp_EndevourOS
cab2533fab deleted some imports 2025-10-27 15:19:51 +01:00
Philipp_EndevourOS
5f65cc7a73 Merge remote-tracking branch 'origin/main' into feature/newlib 2025-10-27 15:11:04 +01:00
Philipp_EndevourOS
03e6a9036f implemented LED for new lib !esp-hhal-smartled is not released! 2025-10-27 15:08:22 +01:00
16ea1db55f Merge branch 'feature/newlib' 2025-10-27 14:34:31 +01:00
a0ed04a560 moved LED and feedback to new lib 2025-10-27 14:25:26 +01:00
Philipp_EndevourOS
4e988e8f01 changed LED amount to 1 2025-10-27 14:16:36 +01:00
009f6cbb2e changed to new ESP libs WIP 2025-10-27 13:56:13 +01:00
Philipp_EndevourOS
967da9fc30 feedback ack turns LED off after lighting green 2025-10-25 15:04:05 +02:00
00cb7efedb unwraps now give a warning 2025-10-24 13:24:44 +02:00
ebbec7885e added missing persist mapping 2025-10-23 17:55:29 +02:00
7ecd2052d8 handle error in sse event route 2025-10-23 17:37:46 +02:00
96512c8a12 added wifi password set at compiletime 2025-10-23 16:55:16 +02:00
c3eaff03d9 implemented year select for export csv 2025-10-23 16:33:15 +02:00
4bf89626b9 changed defaults in csv generation 2025-10-23 16:32:55 +02:00
7c0c0699b5 added umlaut user to mock data 2025-10-23 16:32:38 +02:00
1ea70e4993 improved panic restart 2025-10-20 15:13:16 +02:00
770dca5b0f enabled download of exported csv 2025-10-20 14:30:22 +02:00
2e75ba2908 simplified csv generator 2025-10-20 14:30:22 +02:00
141c1aa9cb added downloadBlob function 2025-10-20 14:30:22 +02:00
Philipp_EndevourOS
4abbd844d2 added software resett after 10min when panic 2025-10-20 13:35:14 +02:00
7346b47816 added csv exporting logic 2025-10-17 13:10:23 +02:00
cd63dd3ee4 added ExportModal 2025-10-17 13:09:23 +02:00
f5d4ae1e05 added new routes & more mock data to server mock 2025-10-17 13:08:51 +02:00
bd3f6731fd renamed vars in main 2025-10-15 16:03:36 +02:00
6fdcf7679f Revert "updated panic handler"
This reverts commit c4d6ed45f1.
2025-10-15 15:50:26 +02:00
Philipp_EndevourOS
c4d6ed45f1 updated panic handler 2025-10-14 14:35:25 +02:00
Philipp_EndevourOS
41adf7353d delete old FRAM driver 2025-10-14 14:17:40 +02:00
6421074931 added routes for listing days & getting days
/api/days?from=...&to=...
/api/day?day=...
2025-10-13 16:32:28 +02:00
a34dc18381 implemented load_day & list_day_in_timespan in IDStore 2025-10-13 16:31:15 +02:00
252e63c607 fixed RFID reader outputting un-aligned IDs in buffer 2025-10-11 14:48:01 +02:00
99848f0e6d added TryFrom<[u8;12]> to TallyID 2025-10-11 14:47:29 +02:00
f46cdc4d29 implemented Display for TallyID 2025-10-11 14:31:26 +02:00
a8d64f6af5 fixed list_day for sd_card 2025-10-11 14:16:28 +02:00
8fb6bac651 added Ord to Day 2025-10-11 14:16:12 +02:00
7eb18376e1 fixed changed /api/mapping structure in frontend 2025-10-10 16:09:53 +02:00
b8bba28bda added mock server for frontedn dev 2025-10-10 16:09:17 +02:00
5c0ad18b94 re-enabled mapping loading 2025-10-10 02:02:00 +02:00
75130e2d20 implemented missing load & save mapping functions 2025-10-10 01:58:25 +02:00
6b2c56f3e5 added Deserialize to IDMapping 2025-10-10 01:57:48 +02:00
2980d34394 flatten IDMapping for serde 2025-10-10 01:50:04 +02:00
9b926f7a34 propper mapping of day to filename 2025-10-10 01:49:32 +02:00
f1b471c6d8 changed Day implementation 2025-10-10 01:12:46 +02:00
030a372949 updated cargo lock 2025-10-09 01:47:34 +02:00
211961a770 fixed RFID read id parser 2025-10-08 16:21:16 +02:00
dfe5197ab8 changed TallyID to a struct instead of a type alias 2025-10-08 16:14:10 +02:00
0f5ca88ae4 fixed many warning by removing unused imports
Removed a lot of imports — believe me, so many imports were totally
unnecessary. Nobody’s seen imports like these. Cleaned up the code, made
it faster, smarter, the best code. People are talking about it!
Tremendous work by me. Some say i am the best at it. It may be true.
2025-10-08 01:54:51 +02:00
9dd2f88cbc implemented SSE 2025-10-08 01:44:32 +02:00
aa91d69f0b explicit type of tallyid channels 2025-10-08 01:43:48 +02:00
b13ae76bc5 moved TallyID str function to right module 2025-10-08 01:42:25 +02:00
4a9ff47dcc 12 chars of hex are 6 Bytes not 12 2025-10-07 23:02:44 +02:00
92c7fec283 changed NET_STACK_SIZE & WEB_TASK_SIZE 2025-10-07 22:48:17 +02:00
082f1faba9 fixed webserver not working
- shared store in main
- multiple tasks for webserver
2025-10-07 17:29:14 +02:00
8cbdf834a1 make network stack size a constant 2025-10-07 17:28:12 +02:00
3eefcdd35a pined picoserve to current git version 2025-10-07 17:27:44 +02:00
4531ef72ae added Deserialize to id mapping Name 2025-10-07 17:27:17 +02:00
Psenfft
2078a3bab0 Update README with images and remove setup details
Removed setup instructions and added images.
2025-10-04 16:17:44 +02:00
Philipp_EndevourOS
7e59d836a1 added get date method for rtc 2025-10-04 15:46:30 +02:00
Philipp_EndevourOS
09f21403ec changed Date to u8 array 2025-10-04 15:45:56 +02:00
Philipp_EndevourOS
db7e22f45d new file will be created when sd card is empty 2025-10-01 18:58:08 +02:00
Philipp_EndevourOS
c91f290c31 removed dummy data and pass read tally id 2025-10-01 18:56:37 +02:00
becdd43738 connect RFID reader with IDStore 2025-10-01 18:00:45 +02:00
Philipp
453b653ac5 updated enclousure top 3mf 2025-10-01 17:56:30 +02:00
cc3605b75d return sdcard from hardware init 2025-10-01 17:54:54 +02:00
57ccc0cc8b fixed missing await 2025-10-01 17:51:51 +02:00
Philipp_EndevourOS
d90376121e added kicad backups to gitignore 2025-09-30 16:05:29 +02:00
2a81499f7c added pm3 write script & already used ids 2025-09-29 16:20:08 +02:00
Philipp
4ff8ff0f77 added some descriptions on pcb layout 2025-09-21 23:32:27 +02:00
47 changed files with 3448 additions and 1126 deletions

View File

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

2
.gitignore vendored
View File

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

Binary file not shown.

941
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,63 +12,44 @@ bench = false
[dependencies]
esp-bootloader-esp-idf = "0.1.0"
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",
] }
esp-hal = { version = "1.0.0-rc.1", features = ["esp32c6", "unstable"] }
esp-alloc = "0.9.0"
esp-println = { version = "0.16.0", features = ["esp32c6", "log-04"] }
esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "log-04", "smoltcp","unstable"]}
esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
critical-section = "1.2.0"
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 }
static_cell = { version = "2.1.0", features = ["nightly"] }
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
log = { version = "0.4" }
static_cell = { version = "2.1.1", features = ["nightly"] }
heapless = { version = "0.8.0", default-features = false }
chrono = { version = "0.4.41", default-features = false }
embedded-hal = "1.0.0"
embedded-io = "0.7.1"
embedded-io-async = "0.7.0"
embassy-executor = { version = "0.9.0", features = [] }
embassy-time = { version = "0.5.0", features = [] }
embassy-futures = { version = "0.1.2", features = ["log"] }
embassy-sync = { version = "0.7.2", features = ["log"] }
embassy-net = { version = "0.7.0", features = [ "dhcpv4", "medium-ethernet", "tcp", "udp" ] }
smoltcp = { version = "0.12.0", default-features = false, features = [ "medium-ethernet", "multicast", "proto-dhcpv4", "proto-dns", "proto-ipv4", "socket-dns", "socket-icmp", "socket-raw", "socket-tcp", "socket-udp" ] }
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "async", "macros" ] }
edge-dhcp = { version = "0.6.0", features = ["log"] }
edge-nal = "0.5.0"
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
picoserve = { version = "0.16.0", features = ["embassy", "log"] }
embassy-sync = { version = "0.7.0", features = ["log"] }
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
chrono = { version = "0.4.41", default-features = false }
picoserve = { git = "https://github.com/sammhicks/picoserve.git", rev = "400df53f61137e1bb2883ec610fc191bfe551a3a", features = ["embassy", "log", "json"] }
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"] }
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"]}
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
esp-hal-smartled = { git = "https://github.com/esp-rs/esp-hal-community.git", rev = "ab4316534d90e3a12785907f043f6899faee0f20", package = "esp-hal-smartled", features = ["esp32c6"]}
smart-leds = "0.4.0"
embedded-sdmmc = "0.8.0"
embedded-hal-bus = "0.3.0"
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"]}
[profile.dev]
# Rust debug is too slow.

View File

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

61
em4100_write.lua Normal file
View File

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

View File

@@ -11071,23 +11071,10 @@
)
)
)
(gr_text "+"
(at 88.773 117.602 0)
(layer "F.SilkS" knockout)
(uuid "a5e7ec92-48c6-446d-8a96-c2f07b7968e4")
(effects
(font
(size 1.5 1.5)
(thickness 0.3)
(bold yes)
)
(justify left bottom)
)
)
(gr_text "Buzzer"
(at 91.313 117.221 270)
(layer "F.SilkS")
(uuid "abfada97-32de-4b88-ab88-f42d72ba44e0")
(uuid "6b1723a4-eff4-46d3-9f40-823ffd913ff8")
(effects
(font
(face "Arial Black")
@@ -11177,6 +11164,32 @@
)
)
)
(gr_text "+"
(at 88.773 117.602 0)
(layer "F.SilkS" knockout)
(uuid "98085a16-de6b-4ebd-a839-66d3a77983c4")
(effects
(font
(size 1.5 1.5)
(thickness 0.3)
(bold yes)
)
(justify left bottom)
)
)
(gr_text "+"
(at 88.773 117.602 0)
(layer "F.SilkS" knockout)
(uuid "a5e7ec92-48c6-446d-8a96-c2f07b7968e4")
(effects
(font
(size 1.5 1.5)
(thickness 0.3)
(bold yes)
)
(justify left bottom)
)
)
(gr_text "RTC"
(at 86 99 270)
(layer "F.SilkS" knockout)
@@ -11309,6 +11322,50 @@
)
)
)
(gr_text "LED"
(at 128.778 108.966 0)
(layer "B.SilkS" knockout)
(uuid "47710ce0-7741-4329-bc8b-f955a1ec342c")
(effects
(font
(face "Arial Black")
(size 1 1)
(thickness 0.15)
)
(justify mirror)
)
(render_cache "LED" 0
(polygon
(pts
(xy 130.191311 108.380581) (xy 129.881855 108.380581) (xy 129.881855 109.134803) (xy 129.398865 109.134803)
(xy 129.398865 109.381) (xy 130.191311 109.381)
)
)
(polygon
(pts
(xy 129.258975 108.380581) (xy 128.429648 108.380581) (xy 128.429648 108.595515) (xy 128.948847 108.595515)
(xy 128.948847 108.755738) (xy 128.467261 108.755738) (xy 128.467261 108.958948) (xy 128.948847 108.958948)
(xy 128.948847 109.154342) (xy 128.414627 109.154342) (xy 128.414627 109.381) (xy 129.258975 109.381)
)
)
(polygon
(pts
(xy 128.244023 109.381) (xy 127.784297 109.381) (xy 127.699762 109.374602) (xy 127.599833 109.353705)
(xy 127.552233 109.336525) (xy 127.50594 109.310337) (xy 127.460492 109.274204) (xy 127.420799 109.231739)
(xy 127.387025 109.182634) (xy 127.359071 109.126132) (xy 127.340244 109.065861) (xy 127.32735 108.984655)
(xy 127.322788 108.883843) (xy 127.633294 108.883843) (xy 127.636583 108.964835) (xy 127.644965 109.020735)
(xy 127.656558 109.057683) (xy 127.674863 109.091119) (xy 127.696224 109.115675) (xy 127.720733 109.132849)
(xy 127.749268 109.143523) (xy 127.793442 109.151277) (xy 127.85873 109.154342) (xy 127.934568 109.154342)
(xy 127.934568 108.607238) (xy 127.857387 108.607238) (xy 127.796553 108.611661) (xy 127.749338 108.623664)
(xy 127.712956 108.641925) (xy 127.685257 108.665979) (xy 127.665189 108.696126) (xy 127.64889 108.739621)
(xy 127.637599 108.800561) (xy 127.633294 108.883843) (xy 127.322788 108.883843) (xy 127.322495 108.877371)
(xy 127.328764 108.776846) (xy 127.347103 108.683625) (xy 127.367158 108.624393) (xy 127.393578 108.571144)
(xy 127.426359 108.523219) (xy 127.465839 108.480989) (xy 127.511703 108.445842) (xy 127.564662 108.417461)
(xy 127.62169 108.398105) (xy 127.693835 108.385286) (xy 127.784297 108.380581) (xy 128.244023 108.380581)
)
)
)
)
(gr_text "BAT"
(at 122.682 123.968 0)
(layer "B.SilkS")
@@ -11359,6 +11416,213 @@
)
)
)
(gr_text "DIN\n5V\nGND"
(at 126.8476 117.348 0)
(layer "B.SilkS" knockout)
(uuid "95b529a5-4fce-4586-9423-1ab5032a1cef")
(effects
(font
(face "Arial Black")
(size 1.5 1.5)
(thickness 0.3)
(bold yes)
)
(justify left bottom mirror)
)
(render_cache "DIN\n5V\nGND" 0
(polygon
(pts
(xy 126.687773 112.053) (xy 125.998184 112.053) (xy 125.871381 112.043403) (xy 125.721487 112.012058)
(xy 125.650087 111.986287) (xy 125.580648 111.947006) (xy 125.512477 111.892807) (xy 125.452936 111.829108)
(xy 125.402275 111.755451) (xy 125.360344 111.670698) (xy 125.332104 111.580292) (xy 125.312762 111.458482)
(xy 125.30592 111.307265) (xy 125.771679 111.307265) (xy 125.776612 111.428753) (xy 125.789185 111.512603)
(xy 125.806575 111.568025) (xy 125.834033 111.618179) (xy 125.866074 111.655013) (xy 125.902838 111.680773)
(xy 125.945641 111.696784) (xy 126.011901 111.708415) (xy 126.109833 111.713013) (xy 126.22359 111.713013)
(xy 126.22359 110.892357) (xy 126.107818 110.892357) (xy 126.016568 110.898991) (xy 125.945745 110.916997)
(xy 125.891171 110.944388) (xy 125.849623 110.980468) (xy 125.819521 111.025689) (xy 125.795073 111.090932)
(xy 125.778136 111.182342) (xy 125.771679 111.307265) (xy 125.30592 111.307265) (xy 125.305481 111.297556)
(xy 125.314885 111.146769) (xy 125.342392 111.006938) (xy 125.372476 110.918089) (xy 125.412105 110.838217)
(xy 125.461277 110.766328) (xy 125.520496 110.702983) (xy 125.589293 110.650263) (xy 125.668731 110.607692)
(xy 125.754273 110.578658) (xy 125.86249 110.559429) (xy 125.998184 110.552371) (xy 126.687773 110.552371)
)
)
(polygon
(pts
(xy 125.042889 110.552371) (xy 124.577698 110.552371) (xy 124.577698 112.053) (xy 125.042889 112.053)
)
)
(polygon
(pts
(xy 124.242017 110.552371) (xy 123.808608 110.552371) (xy 123.242941 111.38237) (xy 123.242941 110.552371)
(xy 122.805411 110.552371) (xy 122.805411 112.053) (xy 123.242941 112.053) (xy 123.805494 111.227672)
(xy 123.805494 112.053) (xy 124.242017 112.053)
)
)
(polygon
(pts
(xy 126.592427 113.072371) (xy 125.601595 113.072371) (xy 125.601595 113.412357) (xy 126.272774 113.412357)
(xy 126.308586 113.636114) (xy 126.238466 113.607159) (xy 126.170742 113.587022) (xy 126.10257 113.574675)
(xy 126.036011 113.570627) (xy 125.924816 113.579714) (xy 125.828433 113.605688) (xy 125.744244 113.647576)
(xy 125.670288 113.705724) (xy 125.608847 113.777669) (xy 125.565452 113.857201) (xy 125.539012 113.945893)
(xy 125.529879 114.045984) (xy 125.537815 114.140317) (xy 125.561667 114.23234) (xy 125.602144 114.323231)
(xy 125.657269 114.405293) (xy 125.725156 114.472564) (xy 125.807033 114.52638) (xy 125.898204 114.563628)
(xy 126.009998 114.587734) (xy 126.146745 114.596447) (xy 126.289769 114.588892) (xy 126.401368 114.568512)
(xy 126.501325 114.532855) (xy 126.581711 114.485255) (xy 126.649675 114.42541) (xy 126.702061 114.359775)
(xy 126.742988 114.283967) (xy 126.778906 114.184928) (xy 126.356763 114.139224) (xy 126.340925 114.203854)
(xy 126.31679 114.253856) (xy 126.285047 114.29209) (xy 126.244976 114.321214) (xy 126.200723 114.338491)
(xy 126.150866 114.344388) (xy 126.095745 114.337026) (xy 126.048417 114.31557) (xy 126.006885 114.279175)
(xy 125.976972 114.232277) (xy 125.95732 114.16902) (xy 125.950007 114.084453) (xy 125.957532 113.997794)
(xy 125.977476 113.93504) (xy 126.007435 113.89028) (xy 126.049177 113.856395) (xy 126.099215 113.835791)
(xy 126.160025 113.828547) (xy 126.219093 113.835786) (xy 126.276804 113.857856) (xy 126.318045 113.885375)
(xy 126.369036 113.934793) (xy 126.724593 113.884143)
)
)
(polygon
(pts
(xy 125.442959 113.072371) (xy 124.957251 113.072371) (xy 124.619097 114.152322) (xy 124.286072 113.072371)
(xy 123.814744 113.072371) (xy 124.372168 114.573) (xy 124.875277 114.573)
)
)
(polygon
(pts
(xy 125.938741 116.547849) (xy 125.938741 116.237173) (xy 125.2214 116.237173) (xy 125.2214 116.875104)
(xy 125.362568 116.963849) (xy 125.483058 117.025917) (xy 125.585749 117.065889) (xy 125.692421 117.092942)
(xy 125.816712 117.110283) (xy 125.961181 117.116447) (xy 126.09707 117.109958) (xy 126.213333 117.091719)
(xy 126.312611 117.063248) (xy 126.397246 117.025588) (xy 126.476431 116.974873) (xy 126.54595 116.913459)
(xy 126.606555 116.840609) (xy 126.658556 116.75512) (xy 126.709172 116.632213) (xy 126.740417 116.495585)
(xy 126.751246 116.342685) (xy 126.744456 116.220155) (xy 126.724835 116.109225) (xy 126.693184 116.008434)
(xy 126.649854 115.916512) (xy 126.593382 115.831582) (xy 126.525637 115.757911) (xy 126.445827 115.694625)
(xy 126.352641 115.641373) (xy 126.248156 115.603503) (xy 126.113488 115.578254) (xy 125.941764 115.568924)
(xy 125.774258 115.575114) (xy 125.653491 115.591186) (xy 125.569263 115.613895) (xy 125.490835 115.649775)
(xy 125.422939 115.696116) (xy 125.364374 115.753297) (xy 125.315897 115.819666) (xy 125.274941 115.898857)
(xy 125.241916 115.992808) (xy 125.689705 116.073041) (xy 125.713044 116.019486) (xy 125.744219 115.976466)
(xy 125.783494 115.942433) (xy 125.82956 115.918081) (xy 125.88501 115.902682) (xy 125.952022 115.897187)
(xy 126.026528 115.904192) (xy 126.090672 115.924253) (xy 126.146494 115.956886) (xy 126.19538 116.002883)
(xy 126.231931 116.057275) (xy 126.260281 116.128296) (xy 126.279107 116.219959) (xy 126.286055 116.337098)
(xy 126.278855 116.462337) (xy 126.259553 116.558173) (xy 126.230867 116.63048) (xy 126.194372 116.684137)
(xy 126.145179 116.728825) (xy 126.08743 116.761014) (xy 126.01942 116.781093) (xy 125.938741 116.788184)
(xy 125.862175 116.782506) (xy 125.790181 116.765745) (xy 125.718163 116.737089) (xy 125.628248 116.688991)
(xy 125.628248 116.547849)
)
)
(polygon
(pts
(xy 124.942505 115.592371) (xy 124.509096 115.592371) (xy 123.94343 116.42237) (xy 123.94343 115.592371)
(xy 123.505899 115.592371) (xy 123.505899 117.093) (xy 123.94343 117.093) (xy 124.505982 116.267672)
(xy 124.505982 117.093) (xy 124.942505 117.093)
)
)
(polygon
(pts
(xy 123.191192 117.093) (xy 122.501603 117.093) (xy 122.3748 117.083403) (xy 122.224907 117.052058)
(xy 122.153507 117.026287) (xy 122.084067 116.987006) (xy 122.015896 116.932807) (xy 121.956356 116.869108)
(xy 121.905694 116.795451) (xy 121.863763 116.710698) (xy 121.835523 116.620292) (xy 121.816182 116.498482)
(xy 121.809339 116.347265) (xy 122.275099 116.347265) (xy 122.280032 116.468753) (xy 122.292605 116.552603)
(xy 122.309995 116.608025) (xy 122.337452 116.658179) (xy 122.369493 116.695013) (xy 122.406257 116.720773)
(xy 122.44906 116.736784) (xy 122.51532 116.748415) (xy 122.613253 116.753013) (xy 122.727009 116.753013)
(xy 122.727009 115.932357) (xy 122.611238 115.932357) (xy 122.519987 115.938991) (xy 122.449164 115.956997)
(xy 122.394591 115.984388) (xy 122.353043 116.020468) (xy 122.322941 116.065689) (xy 122.298492 116.130932)
(xy 122.281555 116.222342) (xy 122.275099 116.347265) (xy 121.809339 116.347265) (xy 121.8089 116.337556)
(xy 121.818304 116.186769) (xy 121.845811 116.046938) (xy 121.875895 115.958089) (xy 121.915525 115.878217)
(xy 121.964696 115.806328) (xy 122.023915 115.742983) (xy 122.092713 115.690263) (xy 122.17215 115.647692)
(xy 122.257692 115.618658) (xy 122.36591 115.599429) (xy 122.501603 115.592371) (xy 123.191192 115.592371)
)
)
)
)
(gr_text "Buzzer"
(at 91.313 117.221 270)
(layer "B.SilkS")
(uuid "abfada97-32de-4b88-ab88-f42d72ba44e0")
(effects
(font
(face "Arial Black")
(size 1 1)
(thickness 0.1)
)
(justify left bottom)
)
(render_cache "Buzzer" 270
(polygon
(pts
(xy 92.483418 117.903414) (xy 92.478292 117.976154) (xy 92.464024 118.036027) (xy 92.441765 118.085307)
(xy 92.411916 118.125797) (xy 92.373641 118.160033) (xy 92.331965 118.183954) (xy 92.286118 118.198391)
(xy 92.234962 118.203344) (xy 92.178019 118.197029) (xy 92.127952 118.178657) (xy 92.083165 118.148023)
(xy 92.058127 118.12116) (xy 92.035802 118.085745) (xy 92.016426 118.040068) (xy 91.993439 118.109093)
(xy 91.963486 118.16089) (xy 91.927155 118.198826) (xy 91.882539 118.226579) (xy 91.830193 118.243721)
(xy 91.768153 118.24975) (xy 91.7172 118.245602) (xy 91.670749 118.23349) (xy 91.627958 118.213541)
(xy 91.589323 118.186407) (xy 91.556666 118.153562) (xy 91.529528 118.114501) (xy 91.511267 118.069998)
(xy 91.496677 117.99702) (xy 91.483 117.858351) (xy 91.483 117.794115) (xy 91.697933 117.794115)
(xy 91.701852 117.846999) (xy 91.711905 117.883056) (xy 91.726326 117.906833) (xy 91.747181 117.9249)
(xy 91.772176 117.935802) (xy 91.802591 117.939623) (xy 91.830744 117.935894) (xy 91.854196 117.925158)
(xy 91.874093 117.907139) (xy 91.88773 117.883682) (xy 91.897359 117.847408) (xy 91.901143 117.793444)
(xy 91.901143 117.770912) (xy 92.09263 117.770912) (xy 92.096078 117.818953) (xy 92.104836 117.851053)
(xy 92.117237 117.871662) (xy 92.135462 117.887014) (xy 92.158683 117.89656) (xy 92.188434 117.899995)
(xy 92.215914 117.896639) (xy 92.237983 117.88717) (xy 92.255906 117.871662) (xy 92.268201 117.851115)
(xy 92.276829 117.819585) (xy 92.280208 117.772927) (xy 92.280208 117.636334) (xy 92.09263 117.636334)
(xy 92.09263 117.770912) (xy 91.901143 117.770912) (xy 91.901143 117.636334) (xy 91.697933 117.636334)
(xy 91.697933 117.794115) (xy 91.483 117.794115) (xy 91.483 117.324131) (xy 92.483418 117.324131)
)
)
(polygon
(pts
(xy 91.483 119.157784) (xy 91.483 118.898215) (xy 91.601885 118.898215) (xy 91.555728 118.857063)
(xy 91.521845 118.818128) (xy 91.498265 118.78104) (xy 91.48167 118.740502) (xy 91.471121 118.692598)
(xy 91.467368 118.635898) (xy 91.472082 118.579152) (xy 91.485414 118.531232) (xy 91.506703 118.490521)
(xy 91.536061 118.45583) (xy 91.571961 118.429077) (xy 91.617624 118.40875) (xy 91.675278 118.395468)
(xy 91.747698 118.390617) (xy 92.209866 118.390617) (xy 92.209866 118.670031) (xy 91.811689 118.670031)
(xy 91.766673 118.673485) (xy 91.73563 118.682401) (xy 91.714847 118.69531) (xy 91.699256 118.713911)
(xy 91.689642 118.737136) (xy 91.68621 118.766385) (xy 91.690517 118.797824) (xy 91.703059 118.824619)
(xy 91.724372 118.847962) (xy 91.752072 118.863911) (xy 91.795687 118.875278) (xy 91.861454 118.879775)
(xy 92.209866 118.879775) (xy 92.209866 119.157784)
)
)
(polygon
(pts
(xy 92.209866 119.294927) (xy 92.209866 119.967816) (xy 92.049277 119.967816) (xy 91.670578 119.601696)
(xy 91.670578 119.990347) (xy 91.483 119.990347) (xy 91.483 119.267632) (xy 91.661114 119.267632)
(xy 92.022288 119.61464) (xy 92.022288 119.294927)
)
)
(polygon
(pts
(xy 92.209866 120.072596) (xy 92.209866 120.745485) (xy 92.049277 120.745485) (xy 91.670578 120.379365)
(xy 91.670578 120.768016) (xy 91.483 120.768016) (xy 91.483 120.045302) (xy 91.661114 120.045302)
(xy 92.022288 120.39231) (xy 92.022288 120.072596)
)
)
(polygon
(pts
(xy 91.928569 120.84049) (xy 92.00099 120.861291) (xy 92.064405 120.895006) (xy 92.12029 120.942161)
(xy 92.15692 120.987231) (xy 92.185889 121.038777) (xy 92.20729 121.097744) (xy 92.220761 121.165356)
(xy 92.225498 121.243069) (xy 92.219449 121.346488) (xy 92.203111 121.426978) (xy 92.178603 121.48896)
(xy 92.142502 121.544547) (xy 92.097522 121.590004) (xy 92.042804 121.626286) (xy 91.981648 121.651241)
(xy 91.90555 121.667509) (xy 91.811262 121.673424) (xy 91.779999 121.673424) (xy 91.779999 121.390652)
(xy 91.912867 121.390652) (xy 91.964796 121.381126) (xy 92.001597 121.366281) (xy 92.026928 121.347238)
(xy 92.045741 121.321911) (xy 92.05729 121.291532) (xy 92.061367 121.25467) (xy 92.055472 121.213061)
(xy 92.038402 121.17835) (xy 92.009404 121.148791) (xy 91.971902 121.129093) (xy 91.912867 121.116673)
(xy 91.912867 121.390652) (xy 91.779999 121.390652) (xy 91.779999 121.116001) (xy 91.735682 121.124168)
(xy 91.702916 121.136513) (xy 91.679188 121.15221) (xy 91.652687 121.181823) (xy 91.636946 121.216355)
(xy 91.631499 121.257418) (xy 91.636589 121.297234) (xy 91.652016 121.335331) (xy 91.668601 121.357193)
(xy 91.697933 121.383814) (xy 91.674486 121.657732) (xy 91.605045 121.610937) (xy 91.552902 121.560453)
(xy 91.515484 121.506057) (xy 91.490617 121.444747) (xy 91.473724 121.361757) (xy 91.467368 121.251312)
(xy 91.472641 121.155392) (xy 91.486828 121.081175) (xy 91.507973 121.024471) (xy 91.539751 120.973365)
(xy 91.582318 120.927853) (xy 91.637056 120.887512) (xy 91.698129 120.857858) (xy 91.766923 120.839582)
(xy 91.84509 120.833229)
)
)
(polygon
(pts
(xy 92.209866 121.803789) (xy 92.209866 122.06403) (xy 92.091103 122.06403) (xy 92.140903 122.091893)
(xy 92.174871 122.117669) (xy 92.196616 122.141577) (xy 92.212328 122.169529) (xy 92.222073 122.202078)
(xy 92.225498 122.240312) (xy 92.221718 122.280528) (xy 92.209689 122.325061) (xy 92.188006 122.37489)
(xy 91.989987 122.288794) (xy 92.006228 122.241599) (xy 92.010564 122.210942) (xy 92.005431 122.17692)
(xy 91.99062 122.149223) (xy 91.965318 122.126251) (xy 91.919836 122.105157) (xy 91.844479 122.089519)
(xy 91.726937 122.083203) (xy 91.483 122.083203) (xy 91.483 121.803789)
)
)
)
)
(gr_text "+"
(at 121.0578 123.8076 180)
(layer "B.SilkS" knockout)

View File

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

Binary file not shown.

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
use embassy_time::{Duration, Timer};
use esp_hal::{Async, uart::Uart};
use log::{debug, info};
use log::{debug, info, warn};
use crate::TallyPublisher;
@@ -17,7 +17,15 @@ 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}");
chan.publish([1, 0, 2, 5, 0, 8, 12, 15]).await;
match extract_id(&uart_buffer) {
Some(read) => {
chan.publish(read.try_into().unwrap()).await;
}
None => {
warn!("Invalid read from the RFID reader");
}
};
}
Err(e) => {
log::error!("Error reading from UART: {e}");
@@ -26,3 +34,35 @@ 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,3 +1,4 @@
use chrono::{TimeZone, Utc};
use ds3231::{
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
TimeRepresentation,
@@ -9,12 +10,15 @@ 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>>,
}
@@ -43,6 +47,33 @@ 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
@@ -62,8 +93,9 @@ 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) => {
info!("Failed to configure DS3231: {:?}", e);
panic!("DS3231 configuration failed");
error!("Failed to configure DS3231: {:?}", e);
error!("DS3231 configuration failed");
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
}
}

View File

@@ -1,12 +1,13 @@
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 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 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)]
@@ -25,14 +26,18 @@ const LED_LEVEL: u8 = 255;
#[embassy_executor::task]
pub async fn feedback_task(
mut led: SmartLedsAdapterAsync<
ConstChannelAccess<esp_hal::rmt::Tx, 0>,
{ init::hardware::LED_BUFFER_SIZE },
>,
buzzer: peripherals::GPIO21<'static>,
rmt: Rmt<'static, esp_hal::Async>,
led_gpio: peripherals::GPIO1<'static>,
buzzer_gpio: peripherals::GPIO21<'static>,
) {
debug!("Starting feedback task");
let mut buzzer = init::hardware::setup_buzzer(buzzer);
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);
loop {
let feedback_state = FEEDBACK_STATE.wait().await;
match feedback_state {
@@ -47,6 +52,12 @@ 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(
@@ -119,7 +130,7 @@ pub async fn feedback_task(
}
FeedbackState::Idle => {
led.write(brightness(
[GREEN; init::hardware::NUM_LEDS].into_iter(),
[BLACK; init::hardware::NUM_LEDS].into_iter(),
LED_LEVEL,
))
.await
@@ -129,106 +140,3 @@ 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,22 +1,19 @@
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::gpio::{Input, InputConfig};
use esp_hal::Blocking;
use esp_hal::delay::Delay;
use esp_hal::gpio::Input;
use esp_hal::i2c::master::Config;
use esp_hal::peripherals::{
GPIO0, GPIO1, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, GPIO22, GPIO23, I2C0, RMT, SPI2,
UART1,
};
use esp_hal::rmt::{ConstChannelAccess, Rmt};
use esp_hal::rmt::Rmt;
use esp_hal::spi::master::{Config as Spi_config, Spi};
use esp_hal::Blocking;
use esp_hal::system::software_reset;
use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::{
@@ -28,16 +25,12 @@ 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, info};
use log::{debug, error};
use crate::FEEDBACK_STATE;
use crate::init::network;
use crate::init::sd_card::setup_sdcard;
use crate::init::sd_card::{SDCardPersistence, setup_sdcard};
use crate::init::wifi;
use crate::store::AttendanceDay;
use crate::store::persistence::Persistence;
/*************************************************
* GPIO Pinout Xiao Esp32c6
@@ -56,46 +49,50 @@ use crate::store::persistence::Persistence;
*
*************************************************/
pub const NUM_LEDS: usize = 66;
pub const LED_BUFFER_SIZE: usize = NUM_LEDS * 25;
pub const NUM_LEDS: usize = 1;
static SD_DET: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
loop {
let delay = Delay::new();
error!("PANIC: {info}");
}
delay.delay(esp_hal::time::Duration::from_secs(30));
software_reset()
}
esp_bootloader_esp_idf::esp_app_desc!();
pub async fn hardware_init(
spawner: &mut Spawner,
spawner: Spawner,
) -> (
Uart<'static, Async>,
Stack<'static>,
I2c<'static, Async>,
SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE>,
Rmt<'static, esp_hal::Async>,
GPIO1<'static>,
GPIO21<'static>,
GPIO0<'static>,
SmartLedsAdapterAsync<'static, LED_BUFFER_SIZE>,
SDCardPersistence,
) {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(size: 72 * 1024);
esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 65536);
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(timer0.alarm0);
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt =
esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
init_logger(log::LevelFilter::Debug);
let timer1 = TimerGroup::new(peripherals.TIMG0);
let mut rng = esp_hal::rng::Rng::new(peripherals.RNG);
let rng = esp_hal::rng::Rng::new();
let network_seed = (rng.random() as u64) << 32 | rng.random() as u64;
wifi::set_antenna_mode(peripherals.GPIO3, peripherals.GPIO14).await;
let interfaces = wifi::setup_wifi(timer1.timer0, rng, peripherals.WIFI, spawner);
let interfaces = wifi::setup_wifi(peripherals.WIFI, spawner);
let stack = network::setup_network(network_seed, interfaces.ap, spawner);
Timer::after(Duration::from_millis(1)).await;
@@ -104,7 +101,11 @@ pub async fn hardware_init(
let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23);
let mut sd_det_gpio = peripherals.GPIO0;
let sd_det_gpio = peripherals.GPIO0;
let rmt: Rmt<'_, esp_hal::Async> = {let frequency: Rate = Rate::from_mhz(80);
Rmt::new(peripherals.RMT, frequency)} .expect("Failed to initialize RMT")
.into_async();
let spi_bus = setup_spi(
peripherals.SPI2,
@@ -119,23 +120,27 @@ pub async fn hardware_init(
OutputConfig::default(),
);
let mut vol_mgr = setup_sdcard(spi_bus, sd_cs_pin);
let vol_mgr = setup_sdcard(spi_bus, sd_cs_pin);
let led_gpio = peripherals.GPIO1;
let buzzer_gpio = peripherals.GPIO21;
Timer::after(Duration::from_millis(500)).await;
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
Timer::after(Duration::from_millis(500)).await;
debug!("hardware init done");
(
uart_device,
stack,
i2c_device,
led,
rmt,
led_gpio,
buzzer_gpio,
sd_det_gpio,
led,
vol_mgr,
)
}
@@ -193,11 +198,10 @@ pub fn setup_buzzer(buzzer_gpio: GPIO21<'static>) -> Output<'static> {
buzzer
}
fn setup_led(
rmt: RMT<'static>,
led_gpio: GPIO1<'static>,
) -> SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE> {
debug!("setup led");
fn setup_led<'a>(
rmt: RMT<'a>,
led_gpio: GPIO1<'a>,
) -> esp_hal_smartled::SmartLedsAdapterAsync<'a, LED_BUFFER_SIZE> {
let rmt: Rmt<'_, esp_hal::Async> = {
let frequency: Rate = Rate::from_mhz(80);
Rmt::new(rmt, frequency)
@@ -206,10 +210,7 @@ fn setup_led(
.into_async();
let rmt_channel = rmt.channel0;
let rmt_buffer = [0_u32; buffer_size_async(NUM_LEDS)];
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); LED_BUFFER_SIZE];
let led: SmartLedsAdapterAsync<_, LED_BUFFER_SIZE> =
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
led
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer)
}

View File

@@ -1,13 +1,16 @@
use core::{net::Ipv4Addr, str::FromStr};
use embassy_executor::Spawner;
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
use embassy_time::{Duration, Timer};
use esp_wifi::wifi::WifiDevice;
use esp_radio::wifi::WifiDevice;
use static_cell::make_static;
use crate::webserver::WEB_TAKS_SIZE;
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: &mut Spawner) -> Stack<'a> {
pub const NETWORK_STACK_SIZE: usize = WEB_TAKS_SIZE + 2; // + 2 for other network taks. Breaks
// without
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: Spawner) -> Stack<'a> {
let gw_ip_addr_str = "192.168.2.1";
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
@@ -16,8 +19,10 @@ pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: &mut Spa
dns_servers: Default::default(),
});
let (stack, runner) =
embassy_net::new(wifi, config, make_static!(StackResources::<3>::new()), seed);
let nw_stack: &'static mut StackResources<NETWORK_STACK_SIZE> =
make_static!(StackResources::<NETWORK_STACK_SIZE>::new());
let (stack, runner) = embassy_net::new(wifi, config, nw_stack, seed);
spawner.must_spawn(net_task(runner));
spawner.must_spawn(run_dhcp(stack, gw_ip_addr_str));
@@ -69,4 +74,3 @@ 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, TimeSource, Timestamp, VolumeIdx, VolumeManager};
use embedded_sdmmc::{SdCard, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager};
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
use crate::store::{AttendanceDay, Date, persistence::Persistence};
use crate::store::{AttendanceDay, IDMapping, day::Day, persistence::Persistence};
pub struct DummyTimesource;
@@ -38,52 +38,111 @@ 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: crate::store::Date) -> Option<AttendanceDay> {
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let mut file = root_dir
.open_file_in_dir("day.jsn", embedded_sdmmc::Mode::ReadOnly)
.unwrap();
let filename = Self::generate_filename(day);
let file = root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly);
if file.is_err() {
return None;
}
let mut open_file = file.unwrap();
let mut read_buffer: [u8; 1024] = [0; 1024];
let read = file.read(&mut read_buffer).unwrap();
file.close().unwrap();
let read = open_file.read(&mut read_buffer).unwrap();
open_file.close().unwrap();
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read]).unwrap();
Some(day)
}
async fn save_day(&mut self, day: Date, data: &AttendanceDay) {
async fn save_day(&mut self, day: Day, data: &AttendanceDay) {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let filename = Self::generate_filename(day);
let mut file = root_dir
.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
.unwrap();
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
file.flush().unwrap();
file.close().unwrap();
}
async fn load_mapping(&mut self) -> Option<crate::store::IDMapping> {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let file =
root_dir.open_file_in_dir(Self::MAPPING_FILENAME, embedded_sdmmc::Mode::ReadOnly);
if file.is_err() {
return None;
}
let mut open_file = file.unwrap();
let mut read_buffer: [u8; 1024] = [0; 1024];
let read = open_file.read(&mut read_buffer).unwrap();
open_file.close().unwrap();
let mapping: IDMapping = serde_json::from_slice(&read_buffer[..read]).unwrap();
Some(mapping)
}
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let mut file = root_dir
.open_file_in_dir("day.jsn", embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
.open_file_in_dir(
Self::MAPPING_FILENAME,
embedded_sdmmc::Mode::ReadWriteCreateOrTruncate,
)
.unwrap();
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
file.flush();
file.close();
file.flush().unwrap();
file.close().unwrap();
}
async fn load_mapping(&mut self) -> Option<crate::store::IDMapping> {
todo!()
}
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
todo!()
}
async fn list_days(&mut self) -> Vec<Date> {
async fn list_days(&mut self) -> Vec<Day> {
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
let mut root_dir = vol_0.open_root_dir().unwrap();
let mut days_dir = root_dir.open_dir("days").unwrap();
let mut days = Vec::new();
let mut days_dir = root_dir.open_dir(".").unwrap();
let mut days: Vec<Day> = Vec::new();
days_dir
.iterate_dir(|e| {
days.push(1);
let filename = e.name.clone();
if let Ok(day) = filename.try_into() {
days.push(day);
}
})
.unwrap();

View File

@@ -2,9 +2,14 @@ use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use esp_hal::gpio::{Output, OutputConfig};
use esp_hal::peripherals::{GPIO3, GPIO14, WIFI};
use esp_wifi::wifi::{AccessPointConfiguration, Configuration, WifiController, WifiEvent, WifiState};
use esp_wifi::{EspWifiRngSource, EspWifiTimerSource, wifi::Interfaces};
use static_cell::make_static;
use esp_radio::Controller;
use esp_radio::wifi::{
AccessPointConfig, Interfaces, ModeConfig, WifiApState, WifiController, WifiEvent,
};
use log::debug;
use static_cell::StaticCell;
static ESP_WIFI_CTRL: StaticCell<Controller<'static>> = StaticCell::new();
pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
let mut rf_switch = Output::new(gpio3, esp_hal::gpio::Level::Low, OutputConfig::default());
@@ -18,26 +23,23 @@ pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
antenna_mode.set_low();
}
pub fn setup_wifi<'d: 'static>(
timer: impl EspWifiTimerSource + 'd,
rng: impl EspWifiRngSource + 'd,
wifi: WIFI<'static>,
spawner: &mut Spawner,
) -> Interfaces<'d> {
let esp_wifi_ctrl = make_static!(esp_wifi::init(timer, rng).unwrap());
pub fn setup_wifi<'d: 'static>(wifi: WIFI<'static>, spawner: Spawner) -> Interfaces<'d> {
let esp_wifi_ctrl = ESP_WIFI_CTRL.init(esp_radio::init().unwrap());
let (controller, interfaces) = esp_wifi::wifi::new(esp_wifi_ctrl, wifi).unwrap();
let config = esp_radio::wifi::Config::default();
let (controller, interfaces) = esp_radio::wifi::new(esp_wifi_ctrl, wifi, config).unwrap();
spawner.must_spawn(connection(controller));
interfaces
}
#[embassy_executor::task]
async fn connection(mut controller: WifiController<'static>) {
debug!("start connection task");
debug!("Device capabilities: {:?}", controller.capabilities());
loop {
match esp_wifi::wifi::wifi_state() {
WifiState::ApStarted => {
match esp_radio::wifi::ap_state() {
WifiApState::Started => {
// wait until we're no longer connected
controller.wait_for_event(WifiEvent::ApStop).await;
Timer::after(Duration::from_millis(5000)).await
@@ -45,12 +47,16 @@ async fn connection(mut controller: WifiController<'static>) {
_ => {}
}
if !matches!(controller.is_started(), Ok(true)) {
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
ssid: "esp-wifi".try_into().unwrap(),
..Default::default()
});
controller.set_configuration(&client_config).unwrap();
let client_config = ModeConfig::AccessPoint(
AccessPointConfig::default()
.with_ssid(env!("WIFI_SSID").try_into().unwrap())
.with_password(env!("WIFI_PASSWD").try_into().unwrap())
.with_auth_method(esp_radio::wifi::AuthMethod::Wpa2Personal),
);
controller.set_config(&client_config).unwrap();
debug!("Starting wifi");
controller.start_async().await.unwrap();
debug!("Wifi started!");
}
}
}

View File

@@ -2,14 +2,16 @@
#![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},
channel::Channel,
mutex::Mutex,
pubsub::{
PubSubChannel, Publisher,
PubSubChannel, Publisher, Subscriber,
WaitResult::{Lagged, Message},
},
signal::Signal,
@@ -18,39 +20,50 @@ use embassy_time::{Duration, Timer};
use esp_hal::gpio::Input;
use esp_hal::{gpio::InputConfig, peripherals};
use log::{debug, info};
use static_cell::make_static;
use crate::store::TallyID;
use static_cell::StaticCell;
extern crate alloc;
use crate::{
init::sd_card::SDCardPersistence,
store::{IDStore, day::Day, tally_id::TallyID},
webserver::start_webserver,
};
mod drivers;
mod feedback;
mod init;
mod store;
//mod webserver;
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>;
#[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;
static CHAN: StaticCell<TallyChannel> = StaticCell::new();
wait_for_stack_up(stack).await;
#[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 chan: &'static mut TallyChannel = make_static!(PubSubChannel::new());
let mut rtc = drivers::rtc::RTCClock::new(i2c).await;
//start_webserver(&mut spawner, stack);
let store: UsedStore = IDStore::new_from_storage(persistence_layer).await;
let shared_store = Rc::new(Mutex::new(store));
let publisher = chan.publisher().unwrap();
let chan: &'static mut TallyChannel = CHAN.init(PubSubChannel::new());
let publisher: TallyPublisher = chan.publisher().unwrap();
let mut sub: TallySubscriber = chan.subscriber().unwrap();
let mut rtc = drivers::rtc::RTCClock::new(_i2c).await;
wait_for_stack_up(stack).await;
start_webserver(spawner, stack, shared_store.clone(), chan);
/****************************** Spawning tasks ***********************************/
debug!("spawing NFC reader task...");
@@ -60,34 +73,37 @@ async fn main(mut spawner: Spawner) {
));
debug!("spawing feedback task..");
spawner.must_spawn(feedback::feedback_task(_led, buzzer_gpio));
spawner.must_spawn(feedback::feedback_task(rmt, led_gpio, buzzer_gpio));
debug!("spawn sd detect task");
spawner.must_spawn(sd_detect_task(sd_det_gpio));
/******************************************************************************/
let mut sub = chan.subscriber().unwrap();
debug!("everything spawned");
FEEDBACK_STATE.signal(feedback::FeedbackState::Startup);
loop {
rtc.get_time().await;
info!("Current RTC time: {}", rtc.get_time().await);
Timer::after(Duration::from_millis(1000)).await;
let wait_result = sub.next_message().await;
match wait_result {
Lagged(_) => debug!("Lagged"),
Message(msg) => {
debug!("Got message: {msg:?}");
// let wait_result = sub.next_message().await;
// match wait_result {
// Lagged(_) => debug!("Lagged"),
// Message(msg) => debug!("Got message: {msg:?}"),
// }
let day: Day = rtc.get_time().await.into();
let added = shared_store.lock().await.add_id(msg, day).await;
if added {
FEEDBACK_STATE.signal(feedback::FeedbackState::Ack);
}
}
}
}
}
#[embassy_executor::task]
async fn sd_detect_task(sd_det_gpio: peripherals::GPIO0<'static>) {
let mut sd_det = Input::new(sd_det_gpio, InputConfig::default());
sd_det.wait_for(esp_hal::gpio::Event::AnyEdge);
sd_det.wait_for(esp_hal::gpio::Event::AnyEdge).await;
loop {
sd_det.wait_for_any_edge().await;

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

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

View File

@@ -1,16 +1,18 @@
use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::string::String;
use serde::Serialize;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize)]
use crate::store::tally_id::TallyID;
#[derive(Clone, Serialize, Deserialize)]
pub struct Name {
pub first: String,
pub last: String,
}
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, Deserialize)]
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: Date,
date: Day,
ids: Vec<TallyID>,
}
impl AttendanceDay {
pub fn new(date: Date) -> Self {
pub fn new(date: Day) -> Self {
Self {
date,
ids: Vec::new(),
@@ -46,7 +46,7 @@ impl<T: Persistence> IDStore<T> {
None => IDMapping::new(),
};
let current_date: Date = 1;
let current_date: Day = Day::new(1);
let day = persistence_layer
.load_day(current_date)
@@ -66,15 +66,13 @@ impl<T: Persistence> IDStore<T> {
.await
}
async fn persist_mapping(&mut self) {
pub async fn persist_mapping(&mut self) {
self.persistence_layer.save_mapping(&self.mapping).await
}
/// Add a new id for the current day
/// Returns false if ID is already present at the current day.
pub async fn add_id(&mut self, id: TallyID) -> bool {
let current_date: Date = 1;
pub async fn add_id(&mut self, id: TallyID, current_date: Day) -> bool {
if self.current_day.date == current_date {
let changed = self.current_day.add_id(id);
if changed {
@@ -92,4 +90,23 @@ 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 @@
mod id_mapping;
pub mod persistence;
mod id_store;
pub use id_mapping::{IDMapping, Name};
pub use id_store::{IDStore,AttendanceDay};
pub type TallyID = [u8; 8];
pub type Date = u64;
mod id_mapping;
pub mod persistence;
mod id_store;
pub mod tally_id;
pub mod day;

View File

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

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

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

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

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

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

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

View File

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

View File

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

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

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

88
used_ids.txt Normal file
View File

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

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

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

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

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

871
web/package-lock.json generated
View File

@@ -14,6 +14,8 @@
"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",
@@ -1078,6 +1080,20 @@
"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",
@@ -1111,6 +1127,68 @@
"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",
@@ -1146,6 +1224,49 @@
"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",
@@ -1174,6 +1295,16 @@
"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",
@@ -1183,6 +1314,38 @@
"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",
@@ -1196,6 +1359,39 @@
"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",
@@ -1236,6 +1432,13 @@
"@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",
@@ -1253,6 +1456,59 @@
"@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",
@@ -1267,6 +1523,44 @@
}
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1281,12 +1575,164 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_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",
@@ -1560,6 +2006,62 @@
"@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",
@@ -1631,6 +2133,73 @@
"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",
@@ -1677,6 +2246,79 @@
"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",
@@ -1730,6 +2372,23 @@
"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",
@@ -1743,6 +2402,156 @@
"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",
@@ -1752,6 +2561,16 @@
"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",
@@ -1850,6 +2669,31 @@
"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",
@@ -1864,6 +2708,26 @@
"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",
@@ -1957,6 +2821,13 @@
}
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,8 @@
export function downloadBlob(filename: string, content: string, mimeType = "text/plain") {
const blob = new Blob([content], { type: mimeType });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}

53
web/src/lib/exporting.ts Normal file
View File

@@ -0,0 +1,53 @@
import { generateCSVString } from "./csv";
import { dayToDate, fetchDay, fetchDays, type AttendanceDay, type Day } from "./Day";
import type { IDMap } from "./IDMapping";
interface CSVRow {
ID: string
Vorname: string
Nachname: string
[key: string]: string | boolean
}
function prepareRows(mapping: IDMap, days: AttendanceDay[]): CSVRow[] {
let csvData: CSVRow[] = [];
const allIDs = Object.keys(mapping);
for (const id of allIDs) {
const name = mapping[id];
const row: CSVRow = {
ID: id,
Vorname: name.first,
Nachname: name.last,
};
for (const day of days) {
const dayKey = dayToDate(day.date).toLocaleDateString();
row[dayKey] = day.ids.includes(id);
}
csvData.push(row);
}
return csvData;
}
async function getDays(from: Date, to: Date): Promise<AttendanceDay[]> {
const recordedDays: Day[] = await fetchDays(from, to);
let days: AttendanceDay[] = [];
for (const day of recordedDays) {
days.push(await fetchDay(day))
}
return days;
}
export async function generateCSVFile(from: Date, to: Date, mapping: IDMap): Promise<string> {
const days = await getDays(from, to);
const rows = prepareRows(mapping, days);
const csvString = generateCSVString(rows);
return csvString;
}

View File

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