mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-04-30 18:49:09 +00:00
Compare commits
72 Commits
d96b3ed11a
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cab2533fab | ||
|
|
5f65cc7a73 | ||
|
|
03e6a9036f | ||
| 16ea1db55f | |||
| a0ed04a560 | |||
|
|
4e988e8f01 | ||
| 009f6cbb2e | |||
|
|
967da9fc30 | ||
| 00cb7efedb | |||
| ebbec7885e | |||
| 7ecd2052d8 | |||
| 96512c8a12 | |||
| c3eaff03d9 | |||
| 4bf89626b9 | |||
| 7c0c0699b5 | |||
| 1ea70e4993 | |||
| 770dca5b0f | |||
| 2e75ba2908 | |||
| 141c1aa9cb | |||
|
|
4abbd844d2 | ||
| 7346b47816 | |||
| cd63dd3ee4 | |||
| f5d4ae1e05 | |||
| bd3f6731fd | |||
| 6fdcf7679f | |||
|
|
c4d6ed45f1 | ||
|
|
41adf7353d | ||
| 6421074931 | |||
| a34dc18381 | |||
| 252e63c607 | |||
| 99848f0e6d | |||
| f46cdc4d29 | |||
| a8d64f6af5 | |||
| 8fb6bac651 | |||
| 7eb18376e1 | |||
| b8bba28bda | |||
| 5c0ad18b94 | |||
| 75130e2d20 | |||
| 6b2c56f3e5 | |||
| 2980d34394 | |||
| 9b926f7a34 | |||
| f1b471c6d8 | |||
| 030a372949 | |||
| 211961a770 | |||
| dfe5197ab8 | |||
| 0f5ca88ae4 | |||
| 9dd2f88cbc | |||
| aa91d69f0b | |||
| b13ae76bc5 | |||
| 4a9ff47dcc | |||
| 92c7fec283 | |||
| 082f1faba9 | |||
| 8cbdf834a1 | |||
| 3eefcdd35a | |||
| 4531ef72ae | |||
|
|
2078a3bab0 | ||
|
|
7e59d836a1 | ||
|
|
09f21403ec | ||
|
|
db7e22f45d | ||
|
|
c91f290c31 | ||
| becdd43738 | |||
|
|
453b653ac5 | ||
| cc3605b75d | |||
| 57ccc0cc8b | |||
|
|
d90376121e | ||
| 2a81499f7c | |||
|
|
4ff8ff0f77 | ||
|
|
781d27ae48 | ||
|
|
671fb0cbdd | ||
|
|
99d9cf306e | ||
|
|
b551f4521f | ||
|
|
adcbe87bd7 |
@@ -12,3 +12,7 @@ target = "riscv32imac-unknown-none-elf"
|
|||||||
|
|
||||||
[unstable]
|
[unstable]
|
||||||
build-std = ["alloc", "core"]
|
build-std = ["alloc", "core"]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
WIFI_PASSWD = "hunter22"
|
||||||
|
WIFI_SSID = "fwa"
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
pcb/fw-anwesenheit-backups
|
||||||
BIN
3d_print/enclouseure_printfile.3mf
Normal file
BIN
3d_print/enclouseure_printfile.3mf
Normal file
Binary file not shown.
BIN
3d_print/enclousure_top_ffw.3mf
Normal file
BIN
3d_print/enclousure_top_ffw.3mf
Normal file
Binary file not shown.
BIN
3d_print/printfiles/fw-anwesenheit-enclousure-top.3mf
Normal file
BIN
3d_print/printfiles/fw-anwesenheit-enclousure-top.3mf
Normal file
Binary file not shown.
941
Cargo.lock
generated
941
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
77
Cargo.toml
77
Cargo.toml
@@ -12,63 +12,44 @@ bench = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
esp-bootloader-esp-idf = "0.1.0"
|
esp-bootloader-esp-idf = "0.1.0"
|
||||||
embassy-net = { version = "0.7.0", features = [
|
esp-hal = { version = "1.0.0-rc.1", features = ["esp32c6", "unstable"] }
|
||||||
"dhcpv4",
|
esp-alloc = "0.9.0"
|
||||||
"medium-ethernet",
|
esp-println = { version = "0.16.0", features = ["esp32c6", "log-04"] }
|
||||||
"tcp",
|
esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "log-04", "smoltcp","unstable"]}
|
||||||
"udp",
|
esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
|
||||||
] }
|
|
||||||
embedded-hal = "=1.0.0"
|
|
||||||
embedded-io = "0.6.1"
|
|
||||||
embedded-io-async = "0.6.1"
|
|
||||||
esp-alloc = "0.8.0"
|
|
||||||
esp-hal = { version = "1.0.0-beta.1", features = ["esp32c6", "unstable"] }
|
|
||||||
smoltcp = { version = "0.12.0", default-features = false, features = [
|
|
||||||
"medium-ethernet",
|
|
||||||
"multicast",
|
|
||||||
"proto-dhcpv4",
|
|
||||||
"proto-dns",
|
|
||||||
"proto-ipv4",
|
|
||||||
"socket-dns",
|
|
||||||
"socket-icmp",
|
|
||||||
"socket-raw",
|
|
||||||
"socket-tcp",
|
|
||||||
"socket-udp",
|
|
||||||
] }
|
|
||||||
# for more networking protocol support see https://crates.io/crates/edge-net
|
|
||||||
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [
|
|
||||||
"async",
|
|
||||||
"macros",
|
|
||||||
] }
|
|
||||||
critical-section = "1.2.0"
|
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" }
|
log = { version = "0.4" }
|
||||||
|
static_cell = { version = "2.1.1", features = ["nightly"] }
|
||||||
|
heapless = { version = "0.8.0", default-features = false }
|
||||||
|
chrono = { version = "0.4.41", default-features = false }
|
||||||
|
|
||||||
|
embedded-hal = "1.0.0"
|
||||||
|
embedded-io = "0.7.1"
|
||||||
|
embedded-io-async = "0.7.0"
|
||||||
|
embassy-executor = { version = "0.9.0", features = [] }
|
||||||
|
embassy-time = { version = "0.5.0", features = [] }
|
||||||
|
embassy-futures = { version = "0.1.2", features = ["log"] }
|
||||||
|
embassy-sync = { version = "0.7.2", features = ["log"] }
|
||||||
|
|
||||||
|
embassy-net = { version = "0.7.0", features = [ "dhcpv4", "medium-ethernet", "tcp", "udp" ] }
|
||||||
|
smoltcp = { version = "0.12.0", default-features = false, features = [ "medium-ethernet", "multicast", "proto-dhcpv4", "proto-dns", "proto-ipv4", "socket-dns", "socket-icmp", "socket-raw", "socket-tcp", "socket-udp" ] }
|
||||||
|
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "async", "macros" ] }
|
||||||
edge-dhcp = { version = "0.6.0", features = ["log"] }
|
edge-dhcp = { version = "0.6.0", features = ["log"] }
|
||||||
edge-nal = "0.5.0"
|
edge-nal = "0.5.0"
|
||||||
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
|
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
|
||||||
picoserve = { version = "0.16.0", features = ["embassy", "log"] }
|
|
||||||
embassy-sync = { version = "0.7.0", features = ["log"] }
|
picoserve = { git = "https://github.com/sammhicks/picoserve.git", rev = "400df53f61137e1bb2883ec610fc191bfe551a3a", features = ["embassy", "log", "json"] }
|
||||||
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
|
|
||||||
chrono = { version = "0.4.41", default-features = false }
|
|
||||||
dir-embed = "0.3.0"
|
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 = { 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-sdmmc = "0.8.0"
|
||||||
embedded-hal-bus = "0.3.0"
|
embedded-hal-bus = "0.3.0"
|
||||||
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"]}
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
# Rust debug is too slow.
|
# Rust debug is too slow.
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -1,32 +1,6 @@
|
|||||||
# fw-anwesenheit
|
# fw-anwesenheit
|
||||||
|
|
||||||
# Setup
|

|
||||||
|
|
||||||
In order to use the LED we need to enable the SPI interface on the Rpi.
|

|
||||||
You can enable it by running `sudo raspi-config`, or by manually adding `dtparam=spi=on` to `/boot/firmware/config.txt`.
|
|
||||||
Enable PWM -> add dtoverlay=pwm to /boot/config.txt
|
|
||||||
I²C fpr RTC `sudo raspi-config` -> interface -> enable I²C
|
|
||||||
|
|
||||||
# Config
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
|
|
||||||
`--error` or `-e`: Enters error state. The LED turns red and the hotspot is activated. This state gets called from systemd if the service is in a failure state.
|
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
|
|
||||||
- `PM3_BIN`: Path to the pm3 binary. Seach in path if not set. Can also be set to the `pm3_mock.sh` for testing.
|
|
||||||
- `LOG_LEVEL`: Can be set to either "debug","warn","error","trace" or "info". Defaults to "warn" in production.
|
|
||||||
- `HTTP_PORT`: What port to listen on. Defaults to 80.
|
|
||||||
- `HOTSPOT_IDS`: A semicolon seperated list of ids to activate the hotspot with e.g. `578B5DF2;c1532b57`.
|
|
||||||
- `HOTSPOT_SSID`: Set the hotspot ssid. Defaults to "fwa".
|
|
||||||
- `HOTSPOT_PW`: Set the hotspot password. Default to "a9LG2kUVrsRRVUo1". Recommended to change.
|
|
||||||
|
|
||||||
Systemd:
|
|
||||||
|
|
||||||
The service is run as a systemd service. There are two service `fwa.service` and `fwa-fail.service`. They read their config
|
|
||||||
from a env file located at `/etc/fwa.env`. See example [env file](service/fwa.env).
|
|
||||||
|
|
||||||
# Building
|
|
||||||
|
|
||||||
Run `make package` to create `.deb` file. [Cross](https://github.com/cross-rs/cross) is used for building the rust code.
|
|
||||||
|
|||||||
61
em4100_write.lua
Normal file
61
em4100_write.lua
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
local used_ids = {}
|
||||||
|
local id_file_path = "used_ids.txt"
|
||||||
|
|
||||||
|
local function load_ids()
|
||||||
|
local file = io.open(id_file_path, "r")
|
||||||
|
if not file then return end
|
||||||
|
for line in file:lines() do
|
||||||
|
used_ids[line:lower()] = true
|
||||||
|
end
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function save_id(id)
|
||||||
|
local file = io.open(id_file_path, "a")
|
||||||
|
if file then
|
||||||
|
file:write(id:lower() .. "\n")
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gen_id()
|
||||||
|
local id = ""
|
||||||
|
for i = 1, 10 do
|
||||||
|
id = id .. string.format("%x", math.random(0, 15))
|
||||||
|
end
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_new_id()
|
||||||
|
local tries = 0
|
||||||
|
while tries < 10000 do
|
||||||
|
local id = gen_id()
|
||||||
|
if not used_ids[id:lower()] then
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
tries = tries + 1
|
||||||
|
end
|
||||||
|
error("Could not generate a new unused ID after 10000 tries")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_new_card()
|
||||||
|
local id = get_new_id()
|
||||||
|
local cmd = string.format("lf em 410x clone --id %s", id)
|
||||||
|
core.console(cmd)
|
||||||
|
used_ids[id:lower()] = true
|
||||||
|
save_id(id)
|
||||||
|
print("Wrote new EM4100 card with ID:", id)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_new_card(id)
|
||||||
|
local cmd = string.format("lf em 410x clone --id %s", id)
|
||||||
|
core.console(cmd)
|
||||||
|
used_ids[id:lower()] = true
|
||||||
|
save_id(id)
|
||||||
|
print("Wrote new EM4100 card with ID:", id)
|
||||||
|
end
|
||||||
|
|
||||||
|
math.randomseed(os.time())
|
||||||
|
load_ids()
|
||||||
|
local id = get_new_id()
|
||||||
|
write_new_card(id)
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
0
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
pcb/fw-anwesenheit-backups/fw-anwesenheit-2025-09-21_232810.zip
Normal file
BIN
pcb/fw-anwesenheit-backups/fw-anwesenheit-2025-09-21_232810.zip
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"board": {
|
"board": {
|
||||||
"active_layer": 2,
|
"active_layer": 7,
|
||||||
"active_layer_preset": "",
|
"active_layer_preset": "",
|
||||||
"auto_track_width": true,
|
"auto_track_width": true,
|
||||||
"hidden_netclasses": [],
|
"hidden_netclasses": [],
|
||||||
@@ -49,7 +49,11 @@
|
|||||||
"conflict_shadows",
|
"conflict_shadows",
|
||||||
"shapes"
|
"shapes"
|
||||||
],
|
],
|
||||||
"visible_layers": "00000000_00000000_0fffffff_fffffaaa",
|
<<<<<<< HEAD
|
||||||
|
"visible_layers": "00000000_00000000_0fffffff_fffff8aa",
|
||||||
|
=======
|
||||||
|
"visible_layers": "00000000_00000000_0fffffff_fffff8ab",
|
||||||
|
>>>>>>> 15c64e4 (updated enclousure top 3mf)
|
||||||
"zone_display_mode": 0
|
"zone_display_mode": 0
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
|
|||||||
@@ -504,7 +504,7 @@
|
|||||||
"plot": "production/",
|
"plot": "production/",
|
||||||
"pos_files": "",
|
"pos_files": "",
|
||||||
"specctra_dsn": "",
|
"specctra_dsn": "",
|
||||||
"step": "fw-anwesenheit.stl",
|
"step": "fw-anwesenheit.step",
|
||||||
"svg": "",
|
"svg": "",
|
||||||
"vrml": ""
|
"vrml": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9586,7 +9586,7 @@
|
|||||||
(justify right)
|
(justify right)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "100 nF"
|
(property "Value" "100nF"
|
||||||
(at 105.41 166.3699 0)
|
(at 105.41 166.3699 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
@@ -11154,7 +11154,7 @@
|
|||||||
(justify right)
|
(justify right)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "100 nF"
|
(property "Value" "100nF"
|
||||||
(at 200.66 135.8899 0)
|
(at 200.66 135.8899 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
@@ -11620,7 +11620,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "100R"
|
(property "Value" "150R"
|
||||||
(at 140.97 91.44 90)
|
(at 140.97 91.44 90)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
@@ -11698,7 +11698,7 @@
|
|||||||
(justify left)
|
(justify left)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Footprint" "Connector_PinSocket_2.00mm:PinSocket_1x02_P2.00mm_Vertical"
|
(property "Footprint" "Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Vertical"
|
||||||
(at 203.835 21.59 90)
|
(at 203.835 21.59 90)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
|
|||||||
122063
pcb/fw-anwesenheit.step
Normal file
122063
pcb/fw-anwesenheit.step
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
2
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pcb
Normal file
2
pcb/fw-anwesenheit/fw-anwesenheit.kicad_pcb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0")
|
||||||
|
)
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
"board": {
|
"board": {
|
||||||
"active_layer": 0,
|
"active_layer": 0,
|
||||||
"active_layer_preset": "",
|
"active_layer_preset": "",
|
||||||
"auto_track_width": false,
|
"auto_track_width": true,
|
||||||
"hidden_netclasses": [],
|
"hidden_netclasses": [],
|
||||||
"hidden_nets": [],
|
"hidden_nets": [],
|
||||||
"high_contrast_mode": 0,
|
"high_contrast_mode": 0,
|
||||||
"net_color_mode": 1,
|
"net_color_mode": 1,
|
||||||
"opacity": {
|
"opacity": {
|
||||||
"images": 1.0,
|
"images": 0.6,
|
||||||
"pads": 1.0,
|
"pads": 1.0,
|
||||||
"shapes": 1.0,
|
"shapes": 1.0,
|
||||||
"tracks": 1.0,
|
"tracks": 1.0,
|
||||||
"vias": 1.0,
|
"vias": 1.0,
|
||||||
"zones": 1.0
|
"zones": 0.6
|
||||||
},
|
},
|
||||||
"selection_filter": {
|
"selection_filter": {
|
||||||
"dimensions": true,
|
"dimensions": true,
|
||||||
@@ -29,70 +29,43 @@
|
|||||||
"zones": true
|
"zones": true
|
||||||
},
|
},
|
||||||
"visible_items": [
|
"visible_items": [
|
||||||
12
|
"vias",
|
||||||
|
"footprint_text",
|
||||||
|
"footprint_anchors",
|
||||||
|
"ratsnest",
|
||||||
|
"grid",
|
||||||
|
"footprints_front",
|
||||||
|
"footprints_back",
|
||||||
|
"footprint_values",
|
||||||
|
"footprint_references",
|
||||||
|
"tracks",
|
||||||
|
"drc_errors",
|
||||||
|
"drawing_sheet",
|
||||||
|
"bitmaps",
|
||||||
|
"pads",
|
||||||
|
"zones",
|
||||||
|
"drc_warnings",
|
||||||
|
"drc_exclusions",
|
||||||
|
"locked_item_shadows",
|
||||||
|
"conflict_shadows",
|
||||||
|
"shapes"
|
||||||
],
|
],
|
||||||
"visible_layers": "fffffff_ffffffff",
|
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
|
||||||
"zone_display_mode": 0
|
"zone_display_mode": 0
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"repo_password": "",
|
|
||||||
"repo_type": "",
|
"repo_type": "",
|
||||||
"repo_username": "",
|
"repo_username": "",
|
||||||
"ssh_key": ""
|
"ssh_key": ""
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"filename": "fw-anwesenheit.kicad_prl",
|
"filename": "fw-anwesenheit.kicad_prl",
|
||||||
"version": 3
|
"version": 5
|
||||||
},
|
},
|
||||||
"net_inspector_panel": {
|
"net_inspector_panel": {
|
||||||
"col_hidden": [
|
"col_hidden": [],
|
||||||
false,
|
"col_order": [],
|
||||||
false,
|
"col_widths": [],
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"col_order": [
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
7,
|
|
||||||
8,
|
|
||||||
9,
|
|
||||||
10,
|
|
||||||
11,
|
|
||||||
12,
|
|
||||||
13
|
|
||||||
],
|
|
||||||
"col_widths": [
|
|
||||||
162,
|
|
||||||
147,
|
|
||||||
91,
|
|
||||||
72,
|
|
||||||
91,
|
|
||||||
100,
|
|
||||||
91,
|
|
||||||
76,
|
|
||||||
91,
|
|
||||||
91,
|
|
||||||
91,
|
|
||||||
91,
|
|
||||||
91,
|
|
||||||
91
|
|
||||||
],
|
|
||||||
"custom_group_rules": [],
|
"custom_group_rules": [],
|
||||||
"expanded_rows": [],
|
"expanded_rows": [],
|
||||||
"filter_by_net_name": true,
|
"filter_by_net_name": true,
|
||||||
@@ -103,7 +76,7 @@
|
|||||||
"show_unconnected_nets": false,
|
"show_unconnected_nets": false,
|
||||||
"show_zero_pad_nets": false,
|
"show_zero_pad_nets": false,
|
||||||
"sort_ascending": true,
|
"sort_ascending": true,
|
||||||
"sorting_column": 0
|
"sorting_column": -1
|
||||||
},
|
},
|
||||||
"open_jobsets": [],
|
"open_jobsets": [],
|
||||||
"project": {
|
"project": {
|
||||||
@@ -2,226 +2,12 @@
|
|||||||
"board": {
|
"board": {
|
||||||
"3dviewports": [],
|
"3dviewports": [],
|
||||||
"design_settings": {
|
"design_settings": {
|
||||||
"defaults": {
|
"defaults": {},
|
||||||
"apply_defaults_to_fp_fields": false,
|
"diff_pair_dimensions": [],
|
||||||
"apply_defaults_to_fp_shapes": false,
|
|
||||||
"apply_defaults_to_fp_text": false,
|
|
||||||
"board_outline_line_width": 0.05,
|
|
||||||
"copper_line_width": 0.2,
|
|
||||||
"copper_text_italic": false,
|
|
||||||
"copper_text_size_h": 1.5,
|
|
||||||
"copper_text_size_v": 1.5,
|
|
||||||
"copper_text_thickness": 0.3,
|
|
||||||
"copper_text_upright": false,
|
|
||||||
"courtyard_line_width": 0.05,
|
|
||||||
"dimension_precision": 4,
|
|
||||||
"dimension_units": 3,
|
|
||||||
"dimensions": {
|
|
||||||
"arrow_length": 1270000,
|
|
||||||
"extension_offset": 500000,
|
|
||||||
"keep_text_aligned": true,
|
|
||||||
"suppress_zeroes": true,
|
|
||||||
"text_position": 0,
|
|
||||||
"units_format": 0
|
|
||||||
},
|
|
||||||
"fab_line_width": 0.1,
|
|
||||||
"fab_text_italic": false,
|
|
||||||
"fab_text_size_h": 1.0,
|
|
||||||
"fab_text_size_v": 1.0,
|
|
||||||
"fab_text_thickness": 0.15,
|
|
||||||
"fab_text_upright": false,
|
|
||||||
"other_line_width": 0.1,
|
|
||||||
"other_text_italic": false,
|
|
||||||
"other_text_size_h": 1.0,
|
|
||||||
"other_text_size_v": 1.0,
|
|
||||||
"other_text_thickness": 0.15,
|
|
||||||
"other_text_upright": false,
|
|
||||||
"pads": {
|
|
||||||
"drill": 0.0,
|
|
||||||
"height": 1.7,
|
|
||||||
"width": 1.7
|
|
||||||
},
|
|
||||||
"silk_line_width": 0.1,
|
|
||||||
"silk_text_italic": false,
|
|
||||||
"silk_text_size_h": 1.0,
|
|
||||||
"silk_text_size_v": 1.0,
|
|
||||||
"silk_text_thickness": 0.1,
|
|
||||||
"silk_text_upright": false,
|
|
||||||
"zones": {
|
|
||||||
"min_clearance": 0.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"diff_pair_dimensions": [
|
|
||||||
{
|
|
||||||
"gap": 0.0,
|
|
||||||
"via_gap": 0.0,
|
|
||||||
"width": 0.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"drc_exclusions": [],
|
"drc_exclusions": [],
|
||||||
"meta": {
|
"rules": {},
|
||||||
"version": 2
|
"track_widths": [],
|
||||||
},
|
"via_dimensions": []
|
||||||
"rule_severities": {
|
|
||||||
"annular_width": "error",
|
|
||||||
"clearance": "error",
|
|
||||||
"connection_width": "warning",
|
|
||||||
"copper_edge_clearance": "error",
|
|
||||||
"copper_sliver": "warning",
|
|
||||||
"courtyards_overlap": "error",
|
|
||||||
"creepage": "error",
|
|
||||||
"diff_pair_gap_out_of_range": "error",
|
|
||||||
"diff_pair_uncoupled_length_too_long": "error",
|
|
||||||
"drill_out_of_range": "error",
|
|
||||||
"duplicate_footprints": "warning",
|
|
||||||
"extra_footprint": "warning",
|
|
||||||
"footprint": "error",
|
|
||||||
"footprint_filters_mismatch": "ignore",
|
|
||||||
"footprint_symbol_mismatch": "warning",
|
|
||||||
"footprint_type_mismatch": "ignore",
|
|
||||||
"hole_clearance": "error",
|
|
||||||
"hole_near_hole": "error",
|
|
||||||
"hole_to_hole": "error",
|
|
||||||
"holes_co_located": "warning",
|
|
||||||
"invalid_outline": "error",
|
|
||||||
"isolated_copper": "warning",
|
|
||||||
"item_on_disabled_layer": "error",
|
|
||||||
"items_not_allowed": "error",
|
|
||||||
"length_out_of_range": "error",
|
|
||||||
"lib_footprint_issues": "warning",
|
|
||||||
"lib_footprint_mismatch": "warning",
|
|
||||||
"malformed_courtyard": "error",
|
|
||||||
"microvia_drill_out_of_range": "error",
|
|
||||||
"mirrored_text_on_front_layer": "warning",
|
|
||||||
"missing_courtyard": "ignore",
|
|
||||||
"missing_footprint": "warning",
|
|
||||||
"net_conflict": "warning",
|
|
||||||
"nonmirrored_text_on_back_layer": "warning",
|
|
||||||
"npth_inside_courtyard": "ignore",
|
|
||||||
"padstack": "warning",
|
|
||||||
"pth_inside_courtyard": "ignore",
|
|
||||||
"shorting_items": "error",
|
|
||||||
"silk_edge_clearance": "warning",
|
|
||||||
"silk_over_copper": "warning",
|
|
||||||
"silk_overlap": "warning",
|
|
||||||
"skew_out_of_range": "error",
|
|
||||||
"solder_mask_bridge": "error",
|
|
||||||
"starved_thermal": "error",
|
|
||||||
"text_height": "warning",
|
|
||||||
"text_on_edge_cuts": "error",
|
|
||||||
"text_thickness": "warning",
|
|
||||||
"through_hole_pad_without_hole": "error",
|
|
||||||
"too_many_vias": "error",
|
|
||||||
"track_angle": "error",
|
|
||||||
"track_dangling": "warning",
|
|
||||||
"track_segment_length": "error",
|
|
||||||
"track_width": "error",
|
|
||||||
"tracks_crossing": "error",
|
|
||||||
"unconnected_items": "error",
|
|
||||||
"unresolved_variable": "error",
|
|
||||||
"via_dangling": "warning",
|
|
||||||
"zones_intersect": "error"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"max_error": 0.005,
|
|
||||||
"min_clearance": 0.2,
|
|
||||||
"min_connection": 0.0,
|
|
||||||
"min_copper_edge_clearance": 0.5,
|
|
||||||
"min_groove_width": 0.0,
|
|
||||||
"min_hole_clearance": 0.25,
|
|
||||||
"min_hole_to_hole": 0.2,
|
|
||||||
"min_microvia_diameter": 0.2,
|
|
||||||
"min_microvia_drill": 0.1,
|
|
||||||
"min_resolved_spokes": 2,
|
|
||||||
"min_silk_clearance": 0.0,
|
|
||||||
"min_text_height": 0.8,
|
|
||||||
"min_text_thickness": 0.08,
|
|
||||||
"min_through_hole_diameter": 0.1,
|
|
||||||
"min_track_width": 0.1,
|
|
||||||
"min_via_annular_width": 0.1,
|
|
||||||
"min_via_diameter": 0.3,
|
|
||||||
"solder_mask_to_copper_clearance": 0.0,
|
|
||||||
"use_height_for_length_calcs": true
|
|
||||||
},
|
|
||||||
"teardrop_options": [
|
|
||||||
{
|
|
||||||
"td_onpadsmd": true,
|
|
||||||
"td_onroundshapesonly": false,
|
|
||||||
"td_ontrackend": false,
|
|
||||||
"td_onviapad": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"teardrop_parameters": [
|
|
||||||
{
|
|
||||||
"td_allow_use_two_tracks": true,
|
|
||||||
"td_curve_segcount": 0,
|
|
||||||
"td_height_ratio": 1.0,
|
|
||||||
"td_length_ratio": 0.5,
|
|
||||||
"td_maxheight": 2.0,
|
|
||||||
"td_maxlen": 1.0,
|
|
||||||
"td_on_pad_in_zone": false,
|
|
||||||
"td_target_name": "td_round_shape",
|
|
||||||
"td_width_to_size_filter_ratio": 0.9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"td_allow_use_two_tracks": true,
|
|
||||||
"td_curve_segcount": 0,
|
|
||||||
"td_height_ratio": 1.0,
|
|
||||||
"td_length_ratio": 0.5,
|
|
||||||
"td_maxheight": 2.0,
|
|
||||||
"td_maxlen": 1.0,
|
|
||||||
"td_on_pad_in_zone": false,
|
|
||||||
"td_target_name": "td_rect_shape",
|
|
||||||
"td_width_to_size_filter_ratio": 0.9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"td_allow_use_two_tracks": true,
|
|
||||||
"td_curve_segcount": 0,
|
|
||||||
"td_height_ratio": 1.0,
|
|
||||||
"td_length_ratio": 0.5,
|
|
||||||
"td_maxheight": 2.0,
|
|
||||||
"td_maxlen": 1.0,
|
|
||||||
"td_on_pad_in_zone": false,
|
|
||||||
"td_target_name": "td_track_end",
|
|
||||||
"td_width_to_size_filter_ratio": 0.9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"track_widths": [
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"tuning_pattern_settings": {
|
|
||||||
"diff_pair_defaults": {
|
|
||||||
"corner_radius_percentage": 80,
|
|
||||||
"corner_style": 1,
|
|
||||||
"max_amplitude": 1.0,
|
|
||||||
"min_amplitude": 0.2,
|
|
||||||
"single_sided": false,
|
|
||||||
"spacing": 1.0
|
|
||||||
},
|
|
||||||
"diff_pair_skew_defaults": {
|
|
||||||
"corner_radius_percentage": 80,
|
|
||||||
"corner_style": 1,
|
|
||||||
"max_amplitude": 1.0,
|
|
||||||
"min_amplitude": 0.2,
|
|
||||||
"single_sided": false,
|
|
||||||
"spacing": 0.6
|
|
||||||
},
|
|
||||||
"single_track_defaults": {
|
|
||||||
"corner_radius_percentage": 80,
|
|
||||||
"corner_style": 1,
|
|
||||||
"max_amplitude": 1.0,
|
|
||||||
"min_amplitude": 0.2,
|
|
||||||
"single_sided": false,
|
|
||||||
"spacing": 0.6
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"via_dimensions": [
|
|
||||||
{
|
|
||||||
"diameter": 0.0,
|
|
||||||
"drill": 0.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"zones_allow_external_fillets": false
|
|
||||||
},
|
},
|
||||||
"ipc2581": {
|
"ipc2581": {
|
||||||
"dist": "",
|
"dist": "",
|
||||||
@@ -453,6 +239,7 @@
|
|||||||
"single_global_label": "ignore",
|
"single_global_label": "ignore",
|
||||||
"unannotated": "error",
|
"unannotated": "error",
|
||||||
"unconnected_wire_endpoint": "warning",
|
"unconnected_wire_endpoint": "warning",
|
||||||
|
"undefined_netclass": "error",
|
||||||
"unit_value_mismatch": "error",
|
"unit_value_mismatch": "error",
|
||||||
"unresolved_variable": "error",
|
"unresolved_variable": "error",
|
||||||
"wire_dangling": "error"
|
"wire_dangling": "error"
|
||||||
@@ -464,7 +251,7 @@
|
|||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"filename": "fw-anwesenheit.kicad_pro",
|
"filename": "fw-anwesenheit.kicad_pro",
|
||||||
"version": 1
|
"version": 3
|
||||||
},
|
},
|
||||||
"net_settings": {
|
"net_settings": {
|
||||||
"classes": [
|
"classes": [
|
||||||
@@ -479,6 +266,7 @@
|
|||||||
"microvia_drill": 0.1,
|
"microvia_drill": 0.1,
|
||||||
"name": "Default",
|
"name": "Default",
|
||||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||||
|
"priority": 2147483647,
|
||||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||||
"track_width": 0.2,
|
"track_width": 0.2,
|
||||||
"via_diameter": 0.6,
|
"via_diameter": 0.6,
|
||||||
@@ -487,7 +275,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"meta": {
|
"meta": {
|
||||||
"version": 3
|
"version": 4
|
||||||
},
|
},
|
||||||
"net_colors": null,
|
"net_colors": null,
|
||||||
"netclass_assignments": null,
|
"netclass_assignments": null,
|
||||||
@@ -498,7 +286,7 @@
|
|||||||
"gencad": "",
|
"gencad": "",
|
||||||
"idf": "",
|
"idf": "",
|
||||||
"netlist": "",
|
"netlist": "",
|
||||||
"plot": "production/",
|
"plot": "",
|
||||||
"pos_files": "",
|
"pos_files": "",
|
||||||
"specctra_dsn": "",
|
"specctra_dsn": "",
|
||||||
"step": "",
|
"step": "",
|
||||||
@@ -578,7 +366,7 @@
|
|||||||
"include_excluded_from_bom": true,
|
"include_excluded_from_bom": true,
|
||||||
"name": "Default Editing",
|
"name": "Default Editing",
|
||||||
"sort_asc": true,
|
"sort_asc": true,
|
||||||
"sort_field": "Referenz"
|
"sort_field": "Reference"
|
||||||
},
|
},
|
||||||
"connection_grid_size": 50.0,
|
"connection_grid_size": 50.0,
|
||||||
"drawing": {
|
"drawing": {
|
||||||
@@ -620,11 +408,6 @@
|
|||||||
"subpart_first_id": 65,
|
"subpart_first_id": 65,
|
||||||
"subpart_id_separator": 0
|
"subpart_id_separator": 0
|
||||||
},
|
},
|
||||||
"sheets": [
|
"sheets": [],
|
||||||
[
|
|
||||||
"ccbf1fda-befd-42da-bcb2-5d3829184012",
|
|
||||||
"Root"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"text_variables": {}
|
"text_variables": {}
|
||||||
}
|
}
|
||||||
14
pcb/fw-anwesenheit/fw-anwesenheit.kicad_sch
Normal file
14
pcb/fw-anwesenheit/fw-anwesenheit.kicad_sch
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
(kicad_sch
|
||||||
|
(version 20250114)
|
||||||
|
(generator "eeschema")
|
||||||
|
(generator_version "9.0")
|
||||||
|
(uuid 35cd442a-c7c9-4bc2-bfa5-9414b343d8e4)
|
||||||
|
(paper "A4")
|
||||||
|
(lib_symbols)
|
||||||
|
(sheet_instances
|
||||||
|
(path "/"
|
||||||
|
(page "1")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(embedded_fonts no)
|
||||||
|
)
|
||||||
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_03-36-00.zip
Normal file
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_03-36-00.zip
Normal file
Binary file not shown.
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_23-31-28.zip
Normal file
BIN
pcb/production/backups/fw-anwesenheit_2025-09-21_23-31-28.zip
Normal file
Binary file not shown.
@@ -1,16 +1,16 @@
|
|||||||
Designator,Footprint,Quantity,Value,LCSC Part #
|
Designator,Footprint,Quantity,Value,LCSC Part #
|
||||||
BT1,Battery_Panasonic_CR2032-HFN_Horizontal_CircularHoles,1,Battery_Cell,
|
BT1,Battery_Panasonic_CR2032-HFN_Horizontal_CircularHoles,1,Battery_Cell,
|
||||||
BZ1,PinSocket_1x02_P2.00mm_Vertical,1,Buzzer,
|
BZ1,PinSocket_1x02_P2.54mm_Vertical,1,Buzzer,
|
||||||
"C1, C4",0603,2,10µF,
|
"C1, C4",0603,2,10µF,
|
||||||
"C2, C3",0603,2,100 nF,
|
"C2, C3, C5",0603,3,100nF,
|
||||||
C5,0603,1,100nF,
|
|
||||||
D2,0603,1,LED,
|
D2,0603,1,LED,
|
||||||
J1,PinHeader_1x02_P2.54mm_Vertical,1,Conn_01x02_Pin,
|
J1,PinHeader_1x02_P2.54mm_Vertical,1,Conn_01x02_Pin,
|
||||||
J2,PinHeader_1x04_P2.54mm_Vertical,1,I2C,
|
J2,PinHeader_1x04_P2.54mm_Vertical,1,I2C,
|
||||||
J3,PinHeader_1x03_P2.54mm_Vertical,1,LED,
|
J3,PinHeader_1x03_P2.54mm_Vertical,1,LED,
|
||||||
J4,WURTH_693071020811,1,MicroSD,
|
J4,WURTH_693071020811,1,MicroSD,
|
||||||
"R1, R2",0603,2,100R,
|
R1,0603,1,150R,
|
||||||
"R10, R11, R12, R13, R14, R15, R9",0603,7,47k,
|
"R10, R11, R12, R13, R14, R15, R9",0603,7,47k,
|
||||||
|
R2,0603,1,100R,
|
||||||
R3,0603,1,10K,
|
R3,0603,1,10K,
|
||||||
R4,0603,1,20k,
|
R4,0603,1,20k,
|
||||||
"R5, R6",0603,2,4k7,
|
"R5, R6",0603,2,4k7,
|
||||||
@@ -27,7 +27,7 @@ TP3,TestPoint_Pad_D1.5mm,1,GND,
|
|||||||
TP4,TestPoint_Pad_D1.5mm,1,"3,3V",
|
TP4,TestPoint_Pad_D1.5mm,1,"3,3V",
|
||||||
TP5_2,TestPoint_Pad_D1.5mm,1,Din,
|
TP5_2,TestPoint_Pad_D1.5mm,1,Din,
|
||||||
TP5,TestPoint_Pad_D1.5mm,1,CS,
|
TP5,TestPoint_Pad_D1.5mm,1,CS,
|
||||||
TP6,TestPoint_Pad_D1.5mm,1,DECT,
|
TP6,TestPoint_Pad_D1.5mm,1,SD_DECT,
|
||||||
TP7,TestPoint_Pad_D1.5mm,1,UART_RX,
|
TP7,TestPoint_Pad_D1.5mm,1,UART_RX,
|
||||||
TP8,TestPoint_Pad_D1.5mm,1,UART_TX,
|
TP8,TestPoint_Pad_D1.5mm,1,UART_TX,
|
||||||
U1,XIAO-ESP32C6-SMD,1,XIAO-ESP32-S3-SMD,
|
U1,XIAO-ESP32C6-SMD,1,XIAO-ESP32-S3-SMD,
|
||||||
|
|||||||
|
Binary file not shown.
@@ -1,225 +1,225 @@
|
|||||||
P CODE 00
|
P CODE 00
|
||||||
P UNITS CUST 0
|
P UNITS CUST 0
|
||||||
P arrayDim N
|
P arrayDim N
|
||||||
317GND VIA MD0118PA00X+040120Y-047880X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+035433Y-045241X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+041640Y-047740X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+047750Y-050550X0177Y0000R000S-2119174445
|
||||||
317-GPIO16_D6_TX) VIA MD0118PA00X+045700Y-046650X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046550Y-043700X0177Y0000R000S-2119174445
|
||||||
317-GPIO16_D6_TX) VIA MD0118PA00X+036450Y-047200X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046950Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+035433Y-045241X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+044450Y-033050X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+047750Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043600Y-040300X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046550Y-043700X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050950Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046950Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038800Y-048850X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+044450Y-033050X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+034150Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043600Y-040300X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+040550Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050950Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+044550Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038800Y-048850X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+040120Y-047880X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+034150Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048850Y-037100X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+040550Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+049500Y-036550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+044550Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+036500Y-038600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048850Y-037100X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+042300Y-042500X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+049500Y-036550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+040900Y-042500X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+036500Y-038600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048750Y-038400X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+042300Y-042500X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+041150Y-049250X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+040900Y-042500X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038800Y-049100X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048750Y-038400X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+034850Y-040600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+041150Y-049250X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+051100Y-045600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038800Y-049100X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+047150Y-036350X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+034850Y-040600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048100Y-050150X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+051100Y-045600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+034950Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+047150Y-036350X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+033051Y-044472X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048100Y-050150X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043600Y-041800X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+034950Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+044650Y-036950X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+033051Y-044472X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046340Y-045450X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043600Y-041800X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038450Y-048600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+044650Y-036950X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+047750Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046340Y-045450X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+049600Y-045500X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038450Y-048600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050787Y-045776X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+047750Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+034050Y-039300X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+049600Y-045500X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+049350Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050787Y-045776X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038950Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+034050Y-039300X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+042850Y-047400X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+049350Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038600Y-041300X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038950Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050500Y-045950X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+042850Y-047400X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048100Y-049850X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038600Y-041300X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050950Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050500Y-045950X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+037200Y-048900X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048100Y-049850X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+035750Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050950Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038600Y-041800X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+037200Y-048900X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+033200Y-049950X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+035750Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+049950Y-038400X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038600Y-041800X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050150Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+033200Y-049950X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+033350Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+049950Y-038400X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+044550Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050150Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043600Y-039800X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+033350Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043600Y-041300X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+044550Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+042950Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043600Y-039800X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+047250Y-039150X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043600Y-041300X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046340Y-044709X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+042950Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+041350Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+047250Y-039150X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+041200Y-038450X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046340Y-044709X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048400Y-049850X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+041350Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+037350Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+041200Y-038450X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+037350Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048400Y-049850X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050750Y-036850X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+037350Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+032550Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+037350Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+037550Y-046600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050750Y-036850X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+045450Y-036150X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+032550Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046950Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+037550Y-046600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048420Y-050150X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+045450Y-036150X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038950Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046950Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043850Y-042500X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048420Y-050150X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038150Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038950Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+045350Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043850Y-042500X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046150Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038150Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038100Y-048600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+045350Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046650Y-039000X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046150Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043750Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038100Y-048600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048550Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046650Y-039000X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+045350Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043750Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043750Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048550Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050150Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+045350Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+051100Y-045950X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043750Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+046150Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050150Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038800Y-048600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+051100Y-045950X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048050Y-039000X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+046150Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043500Y-045350X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038800Y-048600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048550Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048050Y-039000X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+040550Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043500Y-045350X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+036550Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048550Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+049350Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+040550Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050650Y-033050X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+036550Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+041350Y-044650X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+049350Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+042150Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050650Y-033050X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038600Y-040300X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+041350Y-044650X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+043600Y-040800X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+042150Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038100Y-048850X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038600Y-040300X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038150Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+043600Y-040800X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+041350Y-044300X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038100Y-048850X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+047250Y-046950X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038150Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+042950Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+041350Y-044300X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+041526Y-041339X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+047250Y-046950X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+050500Y-045600X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+042950Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048850Y-037400X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+041526Y-041339X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+039750Y-031750X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+050500Y-045600X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+037645Y-038800X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048850Y-037400X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+039750Y-050550X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+039750Y-031750X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038450Y-049100X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+037645Y-038800X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038600Y-040800X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+039750Y-050550X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+041640Y-047740X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038450Y-049100X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038450Y-048850X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038600Y-040800X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+048228Y-046885X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038450Y-048850X0177Y0000R000S2107636099
|
317GND VIA MD0118PA00X+038100Y-049100X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+048228Y-046885X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+050480Y-044960X0177Y0000R000S-2119174445
|
||||||
317GND VIA MD0118PA00X+038100Y-049100X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038820Y-050080X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+050480Y-044960X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+050480Y-044600X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038820Y-050080X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038460Y-050080X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+050480Y-044600X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+051040Y-044620X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038460Y-050080X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+047360Y-044720X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+051040Y-044620X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038160Y-049860X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+047360Y-044720X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+037200Y-049500X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038160Y-049860X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038820Y-049640X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+037200Y-049500X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+034451Y-049801X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038820Y-049640X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+051040Y-044980X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+034451Y-049801X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038820Y-049860X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+051040Y-044980X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+047150Y-043700X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038820Y-049860X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038460Y-049640X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+047150Y-043700X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038460Y-049860X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038460Y-049640X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+049600Y-044900X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038460Y-049860X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+050780Y-044780X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+049600Y-044900X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038160Y-050080X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+050780Y-044780X0177Y0000R000S2107636099
|
317+5V VIA MD0118PA00X+038160Y-049640X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038160Y-050080X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+041050Y-047400X0177Y0000R000S-2119174445
|
||||||
317+5V VIA MD0118PA00X+038160Y-049640X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+032874Y-036304X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+041050Y-047400X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+049606Y-040320X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+032874Y-036304X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038255Y-038800X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+049606Y-040320X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+040400Y-037150X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038255Y-038800X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+047244Y-047638X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+040400Y-037150X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+035595Y-038976X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+047244Y-047638X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048000Y-037450X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+035595Y-038976X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+046457Y-040320X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048000Y-037450X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+043350Y-048650X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+046457Y-040320X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+040450Y-048550X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+043350Y-048650X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038800Y-037150X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+040450Y-048550X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+046350Y-048650X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038800Y-037150X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048150Y-044050X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+046350Y-048650X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+041300Y-047400X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048150Y-044050X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048031Y-040320X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+041300Y-047400X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038450Y-047600X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048031Y-040320X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048000Y-037100X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038450Y-047600X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+041300Y-047150X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048000Y-037100X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+046600Y-048650X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+041300Y-047150X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038800Y-047850X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+046600Y-048650X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+041050Y-047150X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038800Y-047850X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048100Y-038400X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+041050Y-047150X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+050394Y-040354X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048100Y-038400X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+043650Y-048650X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+050394Y-040354X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038450Y-048100X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+043650Y-048650X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038450Y-047850X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038450Y-048100X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038800Y-048100X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038450Y-047850X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+045669Y-040320X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038800Y-048100X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+040350Y-038800X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+045669Y-040320X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+032750Y-043500X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+040350Y-038800X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+035595Y-038189X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+032750Y-043500X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048819Y-040320X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+035595Y-038189X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+047244Y-040320X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048819Y-040320X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038800Y-047600X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+047244Y-040320X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038100Y-048100X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038800Y-047600X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048228Y-047603X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038100Y-048100X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038100Y-047850X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048228Y-047603X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+036100Y-050350X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038100Y-047850X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+038100Y-047600X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+036100Y-050350X0177Y0000R000S2107636099
|
317+3.3V VIA MD0118PA00X+048150Y-045550X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+038100Y-047600X0177Y0000R000S2107636099
|
317NET-(R3-PAD1) VIA MD0118PA00X+035433Y-042948X0177Y0000R000S-2119174445
|
||||||
317+3.3V VIA MD0118PA00X+048150Y-045550X0177Y0000R000S2107636099
|
317/SD_DECT VIA MD0118PA00X+050138Y-037264X0177Y0000R000S-2119174445
|
||||||
317NET-(R3-PAD1) VIA MD0118PA00X+035433Y-042948X0177Y0000R000S2107636099
|
317/SD_DECT VIA MD0118PA00X+044800Y-049900X0177Y0000R000S-2119174445
|
||||||
317/SD_DECT VIA MD0118PA00X+050138Y-037264X0177Y0000R000S2107636099
|
317/MISO VIA MD0118PA00X+038450Y-045800X0177Y0000R000S-2119174445
|
||||||
317/SD_DECT VIA MD0118PA00X+044800Y-049900X0177Y0000R000S2107636099
|
317/MISO VIA MD0118PA00X+049272Y-037264X0177Y0000R000S-2119174445
|
||||||
317/MISO VIA MD0118PA00X+038450Y-045800X0177Y0000R000S2107636099
|
317/SPI_SCL VIA MD0118PA00X+048400Y-037264X0177Y0000R000S-2119174445
|
||||||
317/MISO VIA MD0118PA00X+049272Y-037264X0177Y0000R000S2107636099
|
317/SPI_SCL VIA MD0118PA00X+038450Y-044900X0177Y0000R000S-2119174445
|
||||||
317/SPI_SCL VIA MD0118PA00X+048400Y-037264X0177Y0000R000S2107636099
|
317/SPI_SCL VIA MD0118PA00X+048031Y-039601X0177Y0000R000S-2119174445
|
||||||
317/SPI_SCL VIA MD0118PA00X+038450Y-044900X0177Y0000R000S2107636099
|
317/MOSI VIA MD0118PA00X+037500Y-044550X0177Y0000R000S-2119174445
|
||||||
317/SPI_SCL VIA MD0118PA00X+048031Y-039601X0177Y0000R000S2107636099
|
317/MOSI VIA MD0118PA00X+047539Y-037264X0177Y0000R000S-2119174445
|
||||||
317/MOSI VIA MD0118PA00X+037500Y-044550X0177Y0000R000S2107636099
|
317/SPI_CS VIA MD0118PA00X+046250Y-046000X0177Y0000R000S-2119174445
|
||||||
317/MOSI VIA MD0118PA00X+047539Y-037264X0177Y0000R000S2107636099
|
317/SPI_CS VIA MD0118PA00X+047106Y-037264X0177Y0000R000S-2119174445
|
||||||
317/SPI_CS VIA MD0118PA00X+046250Y-046000X0177Y0000R000S2107636099
|
317/BUZZER VIA MD0118PA00X+044450Y-046650X0177Y0000R000S-2119174445
|
||||||
317/SPI_CS VIA MD0118PA00X+047106Y-037264X0177Y0000R000S2107636099
|
317-GPIO16_D6_TX) VIA MD0118PA00X+036480Y-047320X0177Y0000R000S-2119174445
|
||||||
317/BUZZER VIA MD0118PA00X+044450Y-046650X0177Y0000R000S2107636099
|
317-GPIO16_D6_TX) VIA MD0118PA00X+045700Y-046650X0177Y0000R000S-2119174445
|
||||||
317/I2C_SCL VIA MD0118PA00X+043000Y-038300X0177Y0000R000S2107636099
|
317/I2C_SCL VIA MD0118PA00X+043000Y-038300X0177Y0000R000S-2119174445
|
||||||
317/I2C_SCL VIA MD0118PA00X+045600Y-046100X0177Y0000R000S2107636099
|
317/I2C_SCL VIA MD0118PA00X+045600Y-046100X0177Y0000R000S-2119174445
|
||||||
317/I2C_SCL VIA MD0118PA00X+043225Y-043575X0177Y0000R000S2107636099
|
317/I2C_SCL VIA MD0118PA00X+043225Y-043575X0177Y0000R000S-2119174445
|
||||||
317/I2C_SCL VIA MD0118PA00X+034650Y-046250X0177Y0000R000S2107636099
|
317/I2C_SCL VIA MD0118PA00X+034680Y-046240X0177Y0000R000S-2119174445
|
||||||
317/I2C_SDA VIA MD0118PA00X+042900Y-044100X0177Y0000R000S2107636099
|
317/I2C_SDA VIA MD0118PA00X+042900Y-044100X0177Y0000R000S-2119174445
|
||||||
317/I2C_SDA VIA MD0118PA00X+042900Y-038800X0177Y0000R000S2107636099
|
317/I2C_SDA VIA MD0118PA00X+042900Y-038800X0177Y0000R000S-2119174445
|
||||||
317/I2C_SDA VIA MD0118PA00X+044812Y-045842X0177Y0000R000S2107636099
|
317/I2C_SDA VIA MD0118PA00X+044812Y-045842X0177Y0000R000S-2119174445
|
||||||
317/I2C_SDA VIA MD0118PA00X+033300Y-046050X0177Y0000R000S2107636099
|
317/I2C_SDA VIA MD0118PA00X+033300Y-046050X0177Y0000R000S-2119174445
|
||||||
317/DAT2 VIA MD0118PA00X+046673Y-037264X0177Y0000R000S2107636099
|
317/DAT2 VIA MD0118PA00X+046673Y-037264X0177Y0000R000S-2119174445
|
||||||
317/DAT1 VIA MD0118PA00X+049705Y-037264X0177Y0000R000S2107636099
|
317/DAT1 VIA MD0118PA00X+049705Y-037264X0177Y0000R000S-2119174445
|
||||||
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-049800X0177Y0000R000S2107636099
|
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-049800X0177Y0000R000S-2119174445
|
||||||
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050000X0177Y0000R000S2107636099
|
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050000X0177Y0000R000S-2119174445
|
||||||
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050200X0177Y0000R000S2107636099
|
317NET-(J1-PIN_1) VIA MD0118PA00X+047100Y-050200X0177Y0000R000S-2119174445
|
||||||
317ET-(U3-~{RST}) VIA MD0118PA00X+033850Y-038500X0177Y0000R000S2107636099
|
317ET-(U3-~{RST}) VIA MD0118PA00X+033850Y-038500X0177Y0000R000S-2119174445
|
||||||
317ET-(U3-~{RST}) VIA MD0118PA00X+037750Y-039800X0177Y0000R000S2107636099
|
317ET-(U3-~{RST}) VIA MD0118PA00X+037750Y-039800X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+042450Y-045000X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+042450Y-045000X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+045800Y-049600X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+045800Y-049600X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+042250Y-045000X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+042250Y-045000X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044750X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044750X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044450X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044450X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+046050Y-049350X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+046050Y-049350X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044450X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+042250Y-044450X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+045850Y-049400X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+045850Y-049400X0177Y0000R000S-2119174445
|
||||||
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044750X0177Y0000R000S2107636099
|
317NET-(JP1-A) VIA MD0118PA00X+042450Y-044750X0177Y0000R000S-2119174445
|
||||||
327GND C5 -1 A01X+048730Y-038386X0354Y0374R180S2
|
327GND C5 -1 A01X+048730Y-038386X0354Y0374R180S2
|
||||||
327+3.3V C5 -2 A01X+048120Y-038386X0354Y0374R180S2
|
327+3.3V C5 -2 A01X+048120Y-038386X0354Y0374R180S2
|
||||||
327/LED_DIN J3 -1 A01X+050700Y-043800X0984Y0669R000S2
|
327/LED_DIN J3 -1 A01X+050700Y-043800X0984Y0669R000S2
|
||||||
327+5V J3 -2 A01X+050700Y-044800X0984Y0669R000S2
|
327+5V J3 -2 A01X+050700Y-044800X0984Y0669R000S2
|
||||||
327GND J3 -3 A01X+050700Y-045800X0984Y0669R000S2
|
327GND J3 -3 A01X+050700Y-045800X0984Y0669R000S2
|
||||||
327-GPIO16_D6_TX) U1 -7 A01X+044812Y-043842X1083Y0787R180S2
|
|
||||||
327/SD_DECT U1 -1 A01X+044812Y-049842X1083Y0787R180S2
|
327/SD_DECT U1 -1 A01X+044812Y-049842X1083Y0787R180S2
|
||||||
327/LED_DRIVER U1 -2 A01X+044812Y-048842X1083Y0787R180S2
|
327/LED_DRIVER U1 -2 A01X+044812Y-048842X1083Y0787R180S2
|
||||||
327/SPI_CS U1 -3 A01X+044812Y-047842X1083Y0787R180S2
|
327/SPI_CS U1 -3 A01X+044812Y-047842X1083Y0787R180S2
|
||||||
327/BUZZER U1 -4 A01X+044812Y-046842X1083Y0787R180S2
|
327/BUZZER U1 -4 A01X+044812Y-046842X1083Y0787R180S2
|
||||||
327/I2C_SDA U1 -5 A01X+044812Y-045842X1083Y0787R180S2
|
327/I2C_SDA U1 -5 A01X+044812Y-045842X1083Y0787R180S2
|
||||||
327/I2C_SCL U1 -6 A01X+044812Y-044842X1083Y0787R180S2
|
327/I2C_SCL U1 -6 A01X+044812Y-044842X1083Y0787R180S2
|
||||||
|
327-GPIO16_D6_TX) U1 -7 A01X+044812Y-043842X1083Y0787R180S2
|
||||||
327/UART_RX U1 -8 A01X+038447Y-043842X1083Y0787R180S2
|
327/UART_RX U1 -8 A01X+038447Y-043842X1083Y0787R180S2
|
||||||
327/SPI_SCL U1 -9 A01X+038447Y-044842X1083Y0787R180S2
|
327/SPI_SCL U1 -9 A01X+038447Y-044842X1083Y0787R180S2
|
||||||
327/MISO U1 -10 A01X+038447Y-045842X1083Y0787R180S2
|
327/MISO U1 -10 A01X+038447Y-045842X1083Y0787R180S2
|
||||||
@@ -265,8 +265,8 @@ P arrayDim N
|
|||||||
327/BATTERY_CELL U3 -14 A01X+042981Y-039300X0807Y0236R000S2
|
327/BATTERY_CELL U3 -14 A01X+042981Y-039300X0807Y0236R000S2
|
||||||
327/I2C_SDA U3 -15 A01X+042981Y-038800X0807Y0236R000S2
|
327/I2C_SDA U3 -15 A01X+042981Y-038800X0807Y0236R000S2
|
||||||
327/I2C_SCL U3 -16 A01X+042981Y-038300X0807Y0236R000S2
|
327/I2C_SCL U3 -16 A01X+042981Y-038300X0807Y0236R000S2
|
||||||
317/BUZZER BZ1 -1 D0315PA00X+035400Y-046806X0531Y0531R000S0
|
317/BUZZER BZ1 -1 D0394PA00X+035400Y-046806X0669Y0669R000S0
|
||||||
317GND BZ1 -2 D0315PA00X+035400Y-047594X0531Y0000R000S0
|
317GND BZ1 -2 D0394PA00X+035400Y-047806X0669Y0000R000S0
|
||||||
327+3.3V R15 -1 A01X+045669Y-040320X0384Y0374R270S2
|
327+3.3V R15 -1 A01X+045669Y-040320X0384Y0374R270S2
|
||||||
327/DAT2 R15 -2 A01X+045669Y-039601X0384Y0374R270S2
|
327/DAT2 R15 -2 A01X+045669Y-039601X0384Y0374R270S2
|
||||||
327-GPIO16_D6_TX) TP8 -1 A01X+033927Y-046604X0591Y0000R000S2
|
327-GPIO16_D6_TX) TP8 -1 A01X+033927Y-046604X0591Y0000R000S2
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Designator,Mid X,Mid Y,Rotation,Layer
|
Designator,Mid X,Mid Y,Rotation,Layer
|
||||||
BT1,103.161,-103.378,0.0,bottom
|
BT1,103.161,-103.378,0.0,bottom
|
||||||
BZ1,89.916,-119.888,0.0,top
|
BZ1,89.916,-120.158,0.0,top
|
||||||
C1,94.5,-125.0,270.0,top
|
C1,94.5,-125.0,270.0,top
|
||||||
C2,96.393,-98.552,0.0,top
|
C2,96.393,-98.552,0.0,top
|
||||||
C3,119.0,-111.0,0.0,top
|
C3,119.0,-111.0,0.0,top
|
||||||
|
|||||||
|
@@ -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
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::{Async, uart::Uart};
|
use esp_hal::{Async, uart::Uart};
|
||||||
use log::{debug, info};
|
use log::{debug, info, warn};
|
||||||
|
|
||||||
use crate::TallyPublisher;
|
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();
|
core::fmt::Write::write_fmt(&mut hex_str, format_args!("{:02X} ", byte)).ok();
|
||||||
}
|
}
|
||||||
info!("Read {n} bytes from UART: {hex_str}");
|
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) => {
|
Err(e) => {
|
||||||
log::error!("Error reading from UART: {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;
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use chrono::{TimeZone, Utc};
|
||||||
use ds3231::{
|
use ds3231::{
|
||||||
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
|
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
|
||||||
TimeRepresentation,
|
TimeRepresentation,
|
||||||
@@ -9,12 +10,15 @@ use esp_hal::{
|
|||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
|
|
||||||
use crate::{FEEDBACK_STATE, drivers, feedback};
|
use crate::{FEEDBACK_STATE, drivers, feedback};
|
||||||
use chrono::{TimeZone, Utc};
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
|
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
|
||||||
|
|
||||||
const RTC_ADDRESS: u8 = 0x68;
|
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 {
|
pub struct RTCClock {
|
||||||
dev: DS3231<I2c<'static, Async>>,
|
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>> {
|
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 mut rtc: DS3231<I2c<'static, Async>> = DS3231::new(i2c, RTC_ADDRESS);
|
||||||
let naive_dt = Utc
|
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 {
|
match rtc.configure(&rtc_config).await {
|
||||||
Ok(_) => info!("DS3231 configured successfully"),
|
Ok(_) => info!("DS3231 configured successfully"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Failed to configure DS3231: {:?}", e);
|
error!("Failed to configure DS3231: {:?}", e);
|
||||||
panic!("DS3231 configuration failed");
|
error!("DS3231 configuration failed");
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
197
src/feedback.rs
197
src/feedback.rs
@@ -1,18 +1,19 @@
|
|||||||
use embassy_time::{Delay, Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::{delay, gpio::Output, peripherals, rmt::ConstChannelAccess};
|
use esp_hal::rmt::Rmt;
|
||||||
use esp_hal_smartled::SmartLedsAdapterAsync;
|
use esp_hal::peripherals;
|
||||||
use log::{debug, error, info};
|
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
||||||
use init::hardware;
|
use log::debug;
|
||||||
|
use smart_leds::SmartLedsWriteAsync;
|
||||||
use smart_leds::colors::{BLACK, GREEN, RED, YELLOW};
|
use smart_leds::colors::{BLACK, GREEN, RED, YELLOW};
|
||||||
use smart_leds::{brightness, colors::BLUE};
|
use smart_leds::{brightness, colors::BLUE};
|
||||||
use smart_leds::SmartLedsWriteAsync;
|
|
||||||
|
|
||||||
|
use crate::init::hardware;
|
||||||
use crate::{FEEDBACK_STATE, init};
|
use crate::{FEEDBACK_STATE, init};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum FeedbackState {
|
pub enum FeedbackState {
|
||||||
Ack,
|
Ack,
|
||||||
Nak,
|
Nack,
|
||||||
Error,
|
Error,
|
||||||
Startup,
|
Startup,
|
||||||
WIFI,
|
WIFI,
|
||||||
@@ -24,21 +25,47 @@ const LED_LEVEL: u8 = 255;
|
|||||||
//TODO ERROR STATE: 1 Blink = unknows error, 3 Blink = no sd card
|
//TODO ERROR STATE: 1 Blink = unknows error, 3 Blink = no sd card
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[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>) {
|
pub async fn feedback_task(
|
||||||
|
rmt: Rmt<'static, esp_hal::Async>,
|
||||||
|
led_gpio: peripherals::GPIO1<'static>,
|
||||||
|
buzzer_gpio: peripherals::GPIO21<'static>,
|
||||||
|
) {
|
||||||
debug!("Starting feedback task");
|
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 {
|
loop {
|
||||||
let feedback_state = FEEDBACK_STATE.wait().await;
|
let feedback_state = FEEDBACK_STATE.wait().await;
|
||||||
match feedback_state {
|
match feedback_state {
|
||||||
FeedbackState::Ack => {
|
FeedbackState::Ack => {
|
||||||
led.write(brightness([GREEN; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
led.write(brightness(
|
||||||
|
[GREEN; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
buzzer.set_high();
|
buzzer.set_high();
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
Timer::after(Duration::from_millis(50)).await;
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
|
led.write(brightness(
|
||||||
|
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
FeedbackState::Nak => {
|
FeedbackState::Nack => {
|
||||||
led.write(brightness([YELLOW; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
led.write(brightness(
|
||||||
|
[YELLOW; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
buzzer.set_high();
|
buzzer.set_high();
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
@@ -46,10 +73,20 @@ pub async fn feedback_task(mut led: SmartLedsAdapterAsync<ConstChannelAccess<esp
|
|||||||
buzzer.set_high();
|
buzzer.set_high();
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
led.write(brightness([BLACK; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
led.write(brightness(
|
||||||
|
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
FeedbackState::Error => {
|
FeedbackState::Error => {
|
||||||
led.write(brightness([RED; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
led.write(brightness(
|
||||||
|
[RED; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
buzzer.set_high();
|
buzzer.set_high();
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
@@ -59,7 +96,12 @@ pub async fn feedback_task(mut led: SmartLedsAdapterAsync<ConstChannelAccess<esp
|
|||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
}
|
}
|
||||||
FeedbackState::Startup => {
|
FeedbackState::Startup => {
|
||||||
led.write(brightness([GREEN; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
led.write(brightness(
|
||||||
|
[GREEN; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
buzzer.set_high();
|
buzzer.set_high();
|
||||||
Timer::after(Duration::from_millis(10)).await;
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
@@ -71,119 +113,30 @@ pub async fn feedback_task(mut led: SmartLedsAdapterAsync<ConstChannelAccess<esp
|
|||||||
buzzer.set_high();
|
buzzer.set_high();
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
led.write(brightness([BLACK; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
led.write(brightness(
|
||||||
|
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
FeedbackState::WIFI => {
|
FeedbackState::WIFI => {
|
||||||
led.write(brightness([BLUE; init::hardware::NUM_LEDS].into_iter(), LED_LEVEL)).await.unwrap();
|
led.write(brightness(
|
||||||
|
[BLUE; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
FeedbackState::Idle => {
|
FeedbackState::Idle => {
|
||||||
// Do nothing
|
led.write(brightness(
|
||||||
|
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
debug!("Feedback state: {:?}", feedback_state);
|
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
use bleps::att::Att;
|
use core::cell::RefCell;
|
||||||
|
use critical_section::Mutex;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_net::Stack;
|
use embassy_net::Stack;
|
||||||
|
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_hal::Blocking;
|
||||||
|
use esp_hal::delay::Delay;
|
||||||
|
use esp_hal::gpio::Input;
|
||||||
use esp_hal::i2c::master::Config;
|
use esp_hal::i2c::master::Config;
|
||||||
use esp_hal::peripherals::{
|
use esp_hal::peripherals::{
|
||||||
GPIO0, GPIO1, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, GPIO22, GPIO23, I2C0, RMT, SPI2,
|
GPIO0, GPIO1, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, GPIO22, GPIO23, I2C0, RMT, SPI2,
|
||||||
UART1,
|
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::spi::master::{Config as Spi_config, Spi};
|
||||||
|
use esp_hal::system::software_reset;
|
||||||
use esp_hal::Blocking;
|
|
||||||
use esp_hal::time::Rate;
|
use esp_hal::time::Rate;
|
||||||
use esp_hal::timer::timg::TimerGroup;
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
use esp_hal::{
|
use esp_hal::{
|
||||||
@@ -23,15 +25,12 @@ use esp_hal::{
|
|||||||
uart::Uart,
|
uart::Uart,
|
||||||
};
|
};
|
||||||
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
||||||
use esp_println::dbg;
|
|
||||||
use esp_println::logger::init_logger;
|
use esp_println::logger::init_logger;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error};
|
||||||
|
|
||||||
use crate::init::network;
|
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::init::wifi;
|
||||||
use crate::store::AttendanceDay;
|
|
||||||
use crate::store::persistence::Persistence;
|
|
||||||
|
|
||||||
/*************************************************
|
/*************************************************
|
||||||
* GPIO Pinout Xiao Esp32c6
|
* GPIO Pinout Xiao Esp32c6
|
||||||
@@ -50,52 +49,64 @@ use crate::store::persistence::Persistence;
|
|||||||
*
|
*
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
pub const NUM_LEDS: usize = 66;
|
pub const NUM_LEDS: usize = 1;
|
||||||
pub const LED_BUFFER_SIZE: usize = NUM_LEDS * 25;
|
|
||||||
|
static SD_DET: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
fn panic(info: &core::panic::PanicInfo) -> ! {
|
fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||||
loop {
|
let delay = Delay::new();
|
||||||
error!("PANIC: {info}");
|
error!("PANIC: {info}");
|
||||||
}
|
delay.delay(esp_hal::time::Duration::from_secs(30));
|
||||||
|
software_reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_bootloader_esp_idf::esp_app_desc!();
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
|
|
||||||
pub async fn hardware_init(
|
pub async fn hardware_init(
|
||||||
spawner: &mut Spawner,
|
spawner: Spawner,
|
||||||
) -> (
|
) -> (
|
||||||
Uart<'static, Async>,
|
Uart<'static, Async>,
|
||||||
Stack<'static>,
|
Stack<'static>,
|
||||||
I2c<'static, Async>,
|
I2c<'static, Async>,
|
||||||
SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE>,
|
Rmt<'static, esp_hal::Async>,
|
||||||
|
GPIO1<'static>,
|
||||||
GPIO21<'static>,
|
GPIO21<'static>,
|
||||||
|
GPIO0<'static>,
|
||||||
|
SmartLedsAdapterAsync<'static, LED_BUFFER_SIZE>,
|
||||||
|
SDCardPersistence,
|
||||||
) {
|
) {
|
||||||
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
||||||
let peripherals = esp_hal::init(config);
|
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);
|
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||||
esp_hal_embassy::init(timer0.alarm0);
|
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);
|
init_logger(log::LevelFilter::Debug);
|
||||||
|
|
||||||
let timer1 = TimerGroup::new(peripherals.TIMG0);
|
let rng = esp_hal::rng::Rng::new();
|
||||||
let mut rng = esp_hal::rng::Rng::new(peripherals.RNG);
|
|
||||||
let network_seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
let network_seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
||||||
|
|
||||||
wifi::set_antenna_mode(peripherals.GPIO3, peripherals.GPIO14).await;
|
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);
|
let stack = network::setup_network(network_seed, interfaces.ap, spawner);
|
||||||
|
|
||||||
Timer::after(Duration::from_millis(1)).await;
|
Timer::after(Duration::from_millis(1)).await;
|
||||||
init_lvl_shifter(peripherals.GPIO0);
|
|
||||||
|
|
||||||
let uart_device = setup_uart(peripherals.UART1, peripherals.GPIO16, peripherals.GPIO17);
|
let uart_device = setup_uart(peripherals.UART1, peripherals.GPIO16, peripherals.GPIO17);
|
||||||
|
|
||||||
let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23);
|
let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23);
|
||||||
|
|
||||||
|
let sd_det_gpio = peripherals.GPIO0;
|
||||||
|
|
||||||
|
let rmt: Rmt<'_, esp_hal::Async> = {let frequency: Rate = Rate::from_mhz(80);
|
||||||
|
Rmt::new(peripherals.RMT, frequency)} .expect("Failed to initialize RMT")
|
||||||
|
.into_async();
|
||||||
|
|
||||||
let spi_bus = setup_spi(
|
let spi_bus = setup_spi(
|
||||||
peripherals.SPI2,
|
peripherals.SPI2,
|
||||||
peripherals.GPIO19,
|
peripherals.GPIO19,
|
||||||
@@ -109,29 +120,28 @@ pub async fn hardware_init(
|
|||||||
OutputConfig::default(),
|
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;
|
let buzzer_gpio = peripherals.GPIO21;
|
||||||
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
|
|
||||||
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
|
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
|
||||||
|
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
debug!("hardware init done");
|
debug!("hardware init done");
|
||||||
|
|
||||||
(uart_device, stack, i2c_device, led, buzzer_gpio)
|
(
|
||||||
}
|
uart_device,
|
||||||
|
stack,
|
||||||
// Initialize the level shifter for the NFC reader and LED (output-enable (OE) input is low, all outputs are placed in the high-impedance (Hi-Z) state)
|
i2c_device,
|
||||||
fn init_lvl_shifter(oe_pin: GPIO0<'static>) {
|
rmt,
|
||||||
let mut oe_lvl_shifter = Output::new(
|
led_gpio,
|
||||||
oe_pin,
|
buzzer_gpio,
|
||||||
esp_hal::gpio::Level::Low,
|
sd_det_gpio,
|
||||||
OutputConfig::default()
|
led,
|
||||||
.with_drive_mode(esp_hal::gpio::DriveMode::PushPull)
|
vol_mgr,
|
||||||
.with_drive_strength(esp_hal::gpio::DriveStrength::_10mA),
|
)
|
||||||
);
|
|
||||||
oe_lvl_shifter.set_high();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_uart(
|
fn setup_uart(
|
||||||
@@ -188,11 +198,10 @@ pub fn setup_buzzer(buzzer_gpio: GPIO21<'static>) -> Output<'static> {
|
|||||||
buzzer
|
buzzer
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_led(
|
fn setup_led<'a>(
|
||||||
rmt: RMT<'static>,
|
rmt: RMT<'a>,
|
||||||
led_gpio: GPIO1<'static>,
|
led_gpio: GPIO1<'a>,
|
||||||
) -> SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE> {
|
) -> esp_hal_smartled::SmartLedsAdapterAsync<'a, LED_BUFFER_SIZE> {
|
||||||
debug!("setup led");
|
|
||||||
let rmt: Rmt<'_, esp_hal::Async> = {
|
let rmt: Rmt<'_, esp_hal::Async> = {
|
||||||
let frequency: Rate = Rate::from_mhz(80);
|
let frequency: Rate = Rate::from_mhz(80);
|
||||||
Rmt::new(rmt, frequency)
|
Rmt::new(rmt, frequency)
|
||||||
@@ -201,10 +210,7 @@ fn setup_led(
|
|||||||
.into_async();
|
.into_async();
|
||||||
|
|
||||||
let rmt_channel = rmt.channel0;
|
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)
|
||||||
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
|
|
||||||
|
|
||||||
led
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use core::{net::Ipv4Addr, str::FromStr};
|
use core::{net::Ipv4Addr, str::FromStr};
|
||||||
|
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_wifi::wifi::WifiDevice;
|
use esp_radio::wifi::WifiDevice;
|
||||||
use static_cell::make_static;
|
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_str = "192.168.2.1";
|
||||||
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
|
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 {
|
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(),
|
dns_servers: Default::default(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let (stack, runner) =
|
let nw_stack: &'static mut StackResources<NETWORK_STACK_SIZE> =
|
||||||
embassy_net::new(wifi, config, make_static!(StackResources::<3>::new()), seed);
|
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(net_task(runner));
|
||||||
spawner.must_spawn(run_dhcp(stack, gw_ip_addr_str));
|
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>>) {
|
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
||||||
runner.run().await;
|
runner.run().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use embassy_time::Delay;
|
use embassy_time::Delay;
|
||||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
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 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;
|
pub struct DummyTimesource;
|
||||||
|
|
||||||
@@ -38,52 +38,111 @@ pub struct SDCardPersistence {
|
|||||||
vol_mgr: VolMgr,
|
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 {
|
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 vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
let mut root_dir = vol_0.open_root_dir().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)
|
let filename = Self::generate_filename(day);
|
||||||
.unwrap();
|
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 mut read_buffer: [u8; 1024] = [0; 1024];
|
||||||
let read = file.read(&mut read_buffer).unwrap();
|
let read = open_file.read(&mut read_buffer).unwrap();
|
||||||
file.close().unwrap();
|
open_file.close().unwrap();
|
||||||
|
|
||||||
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read]).unwrap();
|
let day: AttendanceDay = serde_json::from_slice(&read_buffer[..read]).unwrap();
|
||||||
|
|
||||||
Some(day)
|
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 vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
let mut file = root_dir
|
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();
|
.unwrap();
|
||||||
|
|
||||||
file.write(&serde_json::to_vec(data).unwrap()).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> {
|
async fn list_days(&mut self) -> Vec<Day> {
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_days(&mut self) -> Vec<Date> {
|
|
||||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
let mut root_dir = vol_0.open_root_dir().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
|
days_dir
|
||||||
.iterate_dir(|e| {
|
.iterate_dir(|e| {
|
||||||
days.push(1);
|
let filename = e.name.clone();
|
||||||
|
|
||||||
|
if let Ok(day) = filename.try_into() {
|
||||||
|
days.push(day);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ use embassy_executor::Spawner;
|
|||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::gpio::{Output, OutputConfig};
|
use esp_hal::gpio::{Output, OutputConfig};
|
||||||
use esp_hal::peripherals::{GPIO3, GPIO14, WIFI};
|
use esp_hal::peripherals::{GPIO3, GPIO14, WIFI};
|
||||||
use esp_wifi::wifi::{AccessPointConfiguration, Configuration, WifiController, WifiEvent, WifiState};
|
use esp_radio::Controller;
|
||||||
use esp_wifi::{EspWifiRngSource, EspWifiTimerSource, wifi::Interfaces};
|
use esp_radio::wifi::{
|
||||||
use static_cell::make_static;
|
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>) {
|
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());
|
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();
|
antenna_mode.set_low();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_wifi<'d: 'static>(
|
pub fn setup_wifi<'d: 'static>(wifi: WIFI<'static>, spawner: Spawner) -> Interfaces<'d> {
|
||||||
timer: impl EspWifiTimerSource + 'd,
|
let esp_wifi_ctrl = ESP_WIFI_CTRL.init(esp_radio::init().unwrap());
|
||||||
rng: impl EspWifiRngSource + 'd,
|
|
||||||
wifi: WIFI<'static>,
|
|
||||||
spawner: &mut Spawner,
|
|
||||||
) -> Interfaces<'d> {
|
|
||||||
let esp_wifi_ctrl = make_static!(esp_wifi::init(timer, rng).unwrap());
|
|
||||||
|
|
||||||
let (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));
|
spawner.must_spawn(connection(controller));
|
||||||
|
|
||||||
interfaces
|
interfaces
|
||||||
}
|
}
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
async fn connection(mut controller: WifiController<'static>) {
|
async fn connection(mut controller: WifiController<'static>) {
|
||||||
|
debug!("start connection task");
|
||||||
|
debug!("Device capabilities: {:?}", controller.capabilities());
|
||||||
loop {
|
loop {
|
||||||
match esp_wifi::wifi::wifi_state() {
|
match esp_radio::wifi::ap_state() {
|
||||||
WifiState::ApStarted => {
|
WifiApState::Started => {
|
||||||
// wait until we're no longer connected
|
// wait until we're no longer connected
|
||||||
controller.wait_for_event(WifiEvent::ApStop).await;
|
controller.wait_for_event(WifiEvent::ApStop).await;
|
||||||
Timer::after(Duration::from_millis(5000)).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)) {
|
if !matches!(controller.is_started(), Ok(true)) {
|
||||||
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
|
let client_config = ModeConfig::AccessPoint(
|
||||||
ssid: "esp-wifi".try_into().unwrap(),
|
AccessPointConfig::default()
|
||||||
..Default::default()
|
.with_ssid(env!("WIFI_SSID").try_into().unwrap())
|
||||||
});
|
.with_password(env!("WIFI_PASSWD").try_into().unwrap())
|
||||||
controller.set_configuration(&client_config).unwrap();
|
.with_auth_method(esp_radio::wifi::AuthMethod::Wpa2Personal),
|
||||||
|
);
|
||||||
|
controller.set_config(&client_config).unwrap();
|
||||||
|
debug!("Starting wifi");
|
||||||
controller.start_async().await.unwrap();
|
controller.start_async().await.unwrap();
|
||||||
|
debug!("Wifi started!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/main.rs
101
src/main.rs
@@ -2,50 +2,68 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
#![feature(type_alias_impl_trait)]
|
#![feature(type_alias_impl_trait)]
|
||||||
#![feature(impl_trait_in_assoc_type)]
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
|
#![warn(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use alloc::rc::Rc;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_net::Stack;
|
use embassy_net::Stack;
|
||||||
use embassy_sync::{
|
use embassy_sync::{
|
||||||
blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, channel::Channel, pubsub::{
|
blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex},
|
||||||
PubSubChannel, Publisher,
|
mutex::Mutex,
|
||||||
|
pubsub::{
|
||||||
|
PubSubChannel, Publisher, Subscriber,
|
||||||
WaitResult::{Lagged, Message},
|
WaitResult::{Lagged, Message},
|
||||||
}, signal::Signal
|
},
|
||||||
|
signal::Signal,
|
||||||
};
|
};
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_hal::gpio::Input;
|
||||||
|
use esp_hal::{gpio::InputConfig, peripherals};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use static_cell::make_static;
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
use crate::{store::TallyID};
|
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
init::sd_card::SDCardPersistence,
|
||||||
|
store::{IDStore, day::Day, tally_id::TallyID},
|
||||||
|
webserver::start_webserver,
|
||||||
|
};
|
||||||
|
|
||||||
mod drivers;
|
mod drivers;
|
||||||
mod feedback;
|
mod feedback;
|
||||||
mod init;
|
mod init;
|
||||||
mod store;
|
mod store;
|
||||||
//mod webserver;
|
mod webserver;
|
||||||
|
|
||||||
static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
|
static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState> = Signal::new();
|
||||||
|
|
||||||
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
|
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
type TallyPublisher = Publisher<'static, 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]
|
static CHAN: StaticCell<TallyChannel> = StaticCell::new();
|
||||||
async fn main(mut spawner: Spawner) {
|
|
||||||
let (uart_device, stack, _i2c, _led, buzzer_gpio) =
|
|
||||||
init::hardware::hardware_init(&mut spawner).await;
|
|
||||||
|
|
||||||
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...");
|
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 ***********************************/
|
/****************************** Spawning tasks ***********************************/
|
||||||
debug!("spawing NFC reader task...");
|
debug!("spawing NFC reader task...");
|
||||||
@@ -55,24 +73,53 @@ async fn main(mut spawner: Spawner) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
debug!("spawing feedback task..");
|
debug!("spawing feedback task..");
|
||||||
spawner.must_spawn(feedback::feedback_task(_led, buzzer_gpio));
|
spawner.must_spawn(feedback::feedback_task(rmt, led_gpio, buzzer_gpio));
|
||||||
/******************************************************************************/
|
|
||||||
|
|
||||||
let mut sub = chan.subscriber().unwrap();
|
debug!("spawn sd detect task");
|
||||||
|
spawner.must_spawn(sd_detect_task(sd_det_gpio));
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
debug!("everything spawned");
|
debug!("everything spawned");
|
||||||
FEEDBACK_STATE.signal(feedback::FeedbackState::Startup);
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Startup);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
rtc.get_time().await;
|
let wait_result = sub.next_message().await;
|
||||||
info!("Current RTC time: {}", rtc.get_time().await);
|
match wait_result {
|
||||||
Timer::after(Duration::from_millis(1000)).await;
|
Lagged(_) => debug!("Lagged"),
|
||||||
|
Message(msg) => {
|
||||||
|
debug!("Got message: {msg:?}");
|
||||||
|
|
||||||
// let wait_result = sub.next_message().await;
|
let day: Day = rtc.get_time().await.into();
|
||||||
// match wait_result {
|
let added = shared_store.lock().await.add_id(msg, day).await;
|
||||||
// Lagged(_) => debug!("Lagged"),
|
|
||||||
// Message(msg) => debug!("Got message: {msg:?}"),
|
if added {
|
||||||
// }
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Ack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn sd_detect_task(sd_det_gpio: peripherals::GPIO0<'static>) {
|
||||||
|
let mut sd_det = Input::new(sd_det_gpio, InputConfig::default());
|
||||||
|
sd_det.wait_for(esp_hal::gpio::Event::AnyEdge).await;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
sd_det.wait_for_any_edge().await;
|
||||||
|
{
|
||||||
|
if sd_det.is_high() {
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Ack);
|
||||||
|
debug!("card insert");
|
||||||
|
}
|
||||||
|
//card is not insert on low
|
||||||
|
else {
|
||||||
|
FEEDBACK_STATE.signal(feedback::FeedbackState::Nack);
|
||||||
|
debug!("card removed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//debounce time
|
||||||
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
src/store/day.rs
Normal file
63
src/store/day.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use embedded_sdmmc::ShortFileName;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Day(u32);
|
||||||
|
|
||||||
|
impl Day {
|
||||||
|
const SECONDS_PER_DAY: u64 = 86_400;
|
||||||
|
|
||||||
|
pub fn new(daystamp: u32) -> Self {
|
||||||
|
Day(daystamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_from_timestamp(time: u64) -> Self {
|
||||||
|
let day = time / Self::SECONDS_PER_DAY;
|
||||||
|
|
||||||
|
if day > u32::MAX as u64 {
|
||||||
|
// TBH this would only happen if about 11 million years have passed
|
||||||
|
// I sure hope i don't have to work on this project any more then
|
||||||
|
// So we just cap it at this
|
||||||
|
Day(u32::MAX)
|
||||||
|
} else {
|
||||||
|
Day(day as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_timestamp(self) -> u64 {
|
||||||
|
(self.0 as u64) * Self::SECONDS_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(self) -> heapless::String<8> {
|
||||||
|
let mut s: heapless::String<8> = heapless::String::new();
|
||||||
|
write!(s, "{:08X}", self.0).unwrap();
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hex_str(s: &str) -> Result<Self, &'static str> {
|
||||||
|
if s.len() > 8 {
|
||||||
|
return Err("hex string too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
u32::from_str_radix(s, 16)
|
||||||
|
.map_err(|_| "invalid hex string")
|
||||||
|
.map(Day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for Day {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self::new_from_timestamp(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ShortFileName> for Day {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: ShortFileName) -> Result<Self, Self::Error> {
|
||||||
|
let name = core::str::from_utf8(value.base_name()).map_err(|_| ())?;
|
||||||
|
Self::from_hex_str(name).map_err(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
use super::TallyID;
|
|
||||||
use alloc::collections::BTreeMap;
|
use alloc::collections::BTreeMap;
|
||||||
use alloc::string::String;
|
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 struct Name {
|
||||||
pub first: String,
|
pub first: String,
|
||||||
pub last: String,
|
pub last: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct IDMapping {
|
pub struct IDMapping {
|
||||||
|
#[serde(flatten)]
|
||||||
id_map: BTreeMap<TallyID, Name>,
|
id_map: BTreeMap<TallyID, Name>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
use crate::store::persistence::Persistence;
|
|
||||||
|
|
||||||
use super::Date;
|
|
||||||
use super::IDMapping;
|
|
||||||
use super::TallyID;
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
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)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct AttendanceDay {
|
pub struct AttendanceDay {
|
||||||
date: Date,
|
date: Day,
|
||||||
ids: Vec<TallyID>,
|
ids: Vec<TallyID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttendanceDay {
|
impl AttendanceDay {
|
||||||
pub fn new(date: Date) -> Self {
|
pub fn new(date: Day) -> Self {
|
||||||
Self {
|
Self {
|
||||||
date,
|
date,
|
||||||
ids: Vec::new(),
|
ids: Vec::new(),
|
||||||
@@ -46,7 +46,7 @@ impl<T: Persistence> IDStore<T> {
|
|||||||
None => IDMapping::new(),
|
None => IDMapping::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let current_date: Date = 1;
|
let current_date: Day = Day::new(1);
|
||||||
|
|
||||||
let day = persistence_layer
|
let day = persistence_layer
|
||||||
.load_day(current_date)
|
.load_day(current_date)
|
||||||
@@ -66,15 +66,13 @@ impl<T: Persistence> IDStore<T> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn persist_mapping(&mut self) {
|
pub async fn persist_mapping(&mut self) {
|
||||||
self.persistence_layer.save_mapping(&self.mapping).await
|
self.persistence_layer.save_mapping(&self.mapping).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new id for the current day
|
/// Add a new id for the current day
|
||||||
/// Returns false if ID is already present at the current day.
|
/// Returns false if ID is already present at the current day.
|
||||||
pub async fn add_id(&mut self, id: TallyID) -> bool {
|
pub async fn add_id(&mut self, id: TallyID, current_date: Day) -> bool {
|
||||||
let current_date: Date = 1;
|
|
||||||
|
|
||||||
if self.current_day.date == current_date {
|
if self.current_day.date == current_date {
|
||||||
let changed = self.current_day.add_id(id);
|
let changed = self.current_day.add_id(id);
|
||||||
if changed {
|
if changed {
|
||||||
@@ -92,4 +90,23 @@ impl<T: Persistence> IDStore<T> {
|
|||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load and return a AttendanceDay. Nothing more. Nothing less.
|
||||||
|
pub async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
|
||||||
|
if day == self.current_day.date {
|
||||||
|
return Some(self.current_day.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.persistence_layer.load_day(day).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_days_in_timespan(&mut self, from: Day, to: Day) -> Vec<Day> {
|
||||||
|
let all_days = self.persistence_layer.list_days().await;
|
||||||
|
|
||||||
|
all_days
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| *e >= from)
|
||||||
|
.filter(|e| *e <= to)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
mod id_mapping;
|
|
||||||
pub mod persistence;
|
|
||||||
mod id_store;
|
|
||||||
|
|
||||||
pub use id_mapping::{IDMapping, Name};
|
pub use id_mapping::{IDMapping, Name};
|
||||||
pub use id_store::{IDStore,AttendanceDay};
|
pub use id_store::{IDStore,AttendanceDay};
|
||||||
|
|
||||||
pub type TallyID = [u8; 8];
|
mod id_mapping;
|
||||||
pub type Date = u64;
|
pub mod persistence;
|
||||||
|
mod id_store;
|
||||||
|
pub mod tally_id;
|
||||||
|
pub mod day;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use crate::store::{Date, IDMapping, id_store::AttendanceDay};
|
use crate::store::{IDMapping, day::Day, id_store::AttendanceDay};
|
||||||
|
|
||||||
pub trait Persistence {
|
pub trait Persistence {
|
||||||
async fn load_day(&mut self, day: Date) -> Option<AttendanceDay>;
|
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay>;
|
||||||
async fn save_day(&mut self, day: Date, data: &AttendanceDay);
|
async fn save_day(&mut self, day: Day, data: &AttendanceDay);
|
||||||
async fn list_days(&mut self) -> Vec<Date>;
|
async fn list_days(&mut self) -> Vec<Day>;
|
||||||
|
|
||||||
async fn load_mapping(&mut self) -> Option<IDMapping>;
|
async fn load_mapping(&mut self) -> Option<IDMapping>;
|
||||||
async fn save_mapping(&mut self, data: &IDMapping);
|
async fn save_mapping(&mut self, data: &IDMapping);
|
||||||
|
|||||||
108
src/store/tally_id.rs
Normal file
108
src/store/tally_id.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use core::{fmt::Display, str::FromStr};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct TallyID([u8; 6]);
|
||||||
|
|
||||||
|
impl FromStr for TallyID {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
s.as_bytes().try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<heapless::String<12>> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: heapless::String<12>) -> Result<Self, Self::Error> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
|
||||||
|
let mut out: [u8; 6] = [0; 6];
|
||||||
|
for i in 0..6 {
|
||||||
|
let hi = hex_val(bytes[2 * i])?;
|
||||||
|
let lo = hex_val(bytes[2 * i + 1])?;
|
||||||
|
out[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TallyID(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_val(b: u8) -> Result<u8, ()> {
|
||||||
|
match b {
|
||||||
|
b'0'..=b'9' => Ok(b - b'0'),
|
||||||
|
b'a'..=b'f' => Ok(b - b'a' + 10),
|
||||||
|
b'A'..=b'F' => Ok(b - b'A' + 10),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TallyID> for heapless::String<12> {
|
||||||
|
fn from(value: TallyID) -> Self {
|
||||||
|
const HEX_CHARS: &[u8; 16] = b"0123456789ABCDEF";
|
||||||
|
let mut s: Self = Self::new();
|
||||||
|
|
||||||
|
for &b in &value.0 {
|
||||||
|
// Should be safe to unwrap since the string is already long enough
|
||||||
|
s.push(HEX_CHARS[(b >> 4) as usize] as char).unwrap();
|
||||||
|
s.push(HEX_CHARS[(b & 0x0F) as usize] as char).unwrap();
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a array of hex chars
|
||||||
|
impl TryFrom<&[u8]> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != 12 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: [u8; 6] = [0; 6];
|
||||||
|
for i in 0..6 {
|
||||||
|
let hi = hex_val(value[2 * i])?;
|
||||||
|
let lo = hex_val(value[2 * i + 1])?;
|
||||||
|
out[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TallyID(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<[u8; 12]> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: [u8; 12]) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(&value as &[u8])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TallyID {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let s: heapless::String<12> = (*self).into();
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for TallyID {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let s: heapless::String<12> = (*self).into();
|
||||||
|
serializer.serialize_str(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for TallyID {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = <&str>::deserialize(deserializer)?;
|
||||||
|
TallyID::from_str(s).map_err(|_| de::Error::custom("Failed to parse Tally ID"))
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/webserver/api.rs
Normal file
94
src/webserver/api.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use log::error;
|
||||||
|
use picoserve::{
|
||||||
|
extract::{Json, Query, State},
|
||||||
|
response::{self, IntoResponse},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
store::{Name, day::Day, tally_id::TallyID},
|
||||||
|
webserver::{app::AppState, sse::IDEvents},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NewMapping {
|
||||||
|
id: TallyID,
|
||||||
|
name: Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct QueryTimespan {
|
||||||
|
from: u64,
|
||||||
|
to: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct QueryDay {
|
||||||
|
timestamp: Option<u64>,
|
||||||
|
day: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/mapping
|
||||||
|
pub async fn get_mapping(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let store = state.store.lock().await;
|
||||||
|
response::Json(store.mapping.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/mapping
|
||||||
|
pub async fn add_mapping(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(data): Json<NewMapping>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
store.mapping.add_mapping(data.id, data.name);
|
||||||
|
store.persist_mapping().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE /api/idevent
|
||||||
|
pub async fn get_idevent(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||||
|
match state.chan.subscriber() {
|
||||||
|
Ok(chan) => Ok(response::EventStream(IDEvents(chan))),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create SSE: {:?}", e);
|
||||||
|
Err((
|
||||||
|
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Internal server error",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/days
|
||||||
|
pub async fn get_days(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(QueryTimespan { from, to }): Query<QueryTimespan>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let from_day = Day::new_from_timestamp(from);
|
||||||
|
let to_day = Day::new_from_timestamp(to);
|
||||||
|
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
|
||||||
|
let days = store.list_days_in_timespan(from_day, to_day).await;
|
||||||
|
|
||||||
|
response::Json(days)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/day
|
||||||
|
pub async fn get_day(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(QueryDay { timestamp, day }): Query<QueryDay>,
|
||||||
|
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||||
|
let parsed_day = timestamp
|
||||||
|
.map(Day::new_from_timestamp)
|
||||||
|
.or_else(|| day.map(Day::new))
|
||||||
|
.ok_or((response::StatusCode::NOT_FOUND, "Not found"))?;
|
||||||
|
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
|
||||||
|
match store.load_day(parsed_day).await {
|
||||||
|
Some(att_day) => Ok(response::Json(att_day)),
|
||||||
|
None => Err((response::StatusCode::NOT_FOUND, "Not found")),
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/webserver/app.rs
Normal file
32
src/webserver/app.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use alloc::rc::Rc;
|
||||||
|
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||||
|
use picoserve::{AppWithStateBuilder, routing::get};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
TallyChannel, UsedStore,
|
||||||
|
webserver::{
|
||||||
|
api::{add_mapping, get_day, get_days, get_idevent, get_mapping},
|
||||||
|
assets::Assets,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||||
|
pub chan: &'static TallyChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppProps;
|
||||||
|
|
||||||
|
impl AppWithStateBuilder for AppProps {
|
||||||
|
type State = AppState;
|
||||||
|
type PathRouter = impl picoserve::routing::PathRouter<AppState>;
|
||||||
|
|
||||||
|
fn build_app(self) -> picoserve::Router<Self::PathRouter, AppState> {
|
||||||
|
picoserve::Router::from_service(Assets)
|
||||||
|
.route("/api/mapping", get(get_mapping).post(add_mapping))
|
||||||
|
.route("/api/idevent", get(get_idevent))
|
||||||
|
.route("/api/days", get(get_days))
|
||||||
|
.route("/api/day", get(get_day))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ impl<State, CurrentPathParameters>
|
|||||||
);
|
);
|
||||||
|
|
||||||
response_writer
|
response_writer
|
||||||
.write_response(request.body_connection.finalize().await.unwrap(), response)
|
.write_response(request.body_connection.finalize().await?, response)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -68,10 +68,7 @@ impl Content for StaticAsset {
|
|||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_content<W: embedded_io_async::Write>(
|
async fn write_content<W: edge_nal::io::Write>(self, mut writer: W) -> Result<(), W::Error> {
|
||||||
self,
|
|
||||||
mut writer: W,
|
|
||||||
) -> Result<(), W::Error> {
|
|
||||||
writer.write_all(self.0).await
|
writer.write_all(self.0).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,59 @@
|
|||||||
|
use alloc::rc::Rc;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_net::Stack;
|
use embassy_net::Stack;
|
||||||
|
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||||
use embassy_time::Duration;
|
use embassy_time::Duration;
|
||||||
use picoserve::{AppBuilder, AppRouter, routing::get};
|
use picoserve::{AppRouter, AppWithStateBuilder};
|
||||||
use static_cell::make_static;
|
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 app = make_static!(AppProps.build_app());
|
||||||
|
|
||||||
|
let state = make_static!(AppState { store, chan });
|
||||||
|
|
||||||
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
||||||
start_read_request: Some(Duration::from_secs(5)),
|
start_read_request: Some(Duration::from_secs(5)),
|
||||||
persistent_start_read_request: Some(Duration::from_secs(1)),
|
persistent_start_read_request: Some(Duration::from_secs(5)),
|
||||||
read_request: Some(Duration::from_secs(1)),
|
read_request: Some(Duration::from_secs(5)),
|
||||||
write: Some(Duration::from_secs(1)),
|
write: Some(Duration::from_secs(5)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let _ = spawner.spawn(webserver_task(0, stack, app, config));
|
for task_id in 0..WEB_TAKS_SIZE {
|
||||||
}
|
spawner.must_spawn(webserver_task(task_id, stack, app, config, state));
|
||||||
|
|
||||||
struct AppProps;
|
|
||||||
|
|
||||||
impl AppBuilder for AppProps {
|
|
||||||
type PathRouter = impl picoserve::routing::PathRouter;
|
|
||||||
|
|
||||||
fn build_app(self) -> picoserve::Router<Self::PathRouter> {
|
|
||||||
picoserve::Router::from_service(assets::Assets).route("/api/a", get(async move || "Hello"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task(pool_size = WEB_TAKS_SIZE)]
|
||||||
async fn webserver_task(
|
async fn webserver_task(
|
||||||
id: usize,
|
task_id: usize,
|
||||||
stack: embassy_net::Stack<'static>,
|
stack: embassy_net::Stack<'static>,
|
||||||
app: &'static AppRouter<AppProps>,
|
app: &'static AppRouter<AppProps>,
|
||||||
config: &'static picoserve::Config<Duration>,
|
config: &'static picoserve::Config<Duration>,
|
||||||
|
state: &'static AppState,
|
||||||
) -> ! {
|
) -> ! {
|
||||||
let mut tcp_rx_buffer = [0u8; 1024];
|
let mut tcp_rx_buffer = [0u8; 1024];
|
||||||
let mut tcp_tx_buffer = [0u8; 1024];
|
let mut tcp_tx_buffer = [0u8; 1024];
|
||||||
let mut http_buffer = [0u8; 2048];
|
let mut http_buffer = [0u8; 2048];
|
||||||
|
|
||||||
picoserve::listen_and_serve(
|
picoserve::Server::new(&app.shared().with_state(state), config, &mut http_buffer)
|
||||||
id,
|
.listen_and_serve(task_id, stack, 80, &mut tcp_rx_buffer, &mut tcp_tx_buffer)
|
||||||
app,
|
.await
|
||||||
config,
|
.into_never()
|
||||||
stack,
|
|
||||||
80,
|
|
||||||
&mut tcp_rx_buffer,
|
|
||||||
&mut tcp_tx_buffer,
|
|
||||||
&mut http_buffer,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/webserver/sse.rs
Normal file
32
src/webserver/sse.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use log::warn;
|
||||||
|
use picoserve::response;
|
||||||
|
|
||||||
|
use crate::TallySubscriber;
|
||||||
|
|
||||||
|
pub struct IDEvents(pub TallySubscriber);
|
||||||
|
|
||||||
|
impl response::sse::EventSource for IDEvents {
|
||||||
|
async fn write_events<W: picoserve::io::Write>(
|
||||||
|
mut self,
|
||||||
|
mut writer: response::sse::EventWriter<W>,
|
||||||
|
) -> Result<(), W::Error> {
|
||||||
|
loop {
|
||||||
|
let timeout = Timer::after(Duration::from_secs(15));
|
||||||
|
let sel = embassy_futures::select::select(self.0.next_message(), timeout);
|
||||||
|
|
||||||
|
match sel.await {
|
||||||
|
embassy_futures::select::Either::First(msg) => match msg {
|
||||||
|
embassy_sync::pubsub::WaitResult::Message(id) => {
|
||||||
|
let id_str: heapless::String<12> = id.into();
|
||||||
|
writer.write_event("msg", id_str.as_str()).await?
|
||||||
|
}
|
||||||
|
embassy_sync::pubsub::WaitResult::Lagged(_) => {
|
||||||
|
warn!("SSE subscriber got lagged");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
embassy_futures::select::Either::Second(_) => writer.write_keepalive().await?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
used_ids.txt
Normal file
88
used_ids.txt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
8f801f988c
|
||||||
|
6fc90dd450
|
||||||
|
edf3f0793a
|
||||||
|
df39cf9566
|
||||||
|
b9f998924d
|
||||||
|
f7686ed090
|
||||||
|
c4dc09d5fa
|
||||||
|
78908e3baa
|
||||||
|
da874600cd
|
||||||
|
66beb3dfad
|
||||||
|
b5758c2ada
|
||||||
|
aee543f099
|
||||||
|
ea912de917
|
||||||
|
d309e7a0da
|
||||||
|
54d1fbd27a
|
||||||
|
a2f95c6f50
|
||||||
|
b663118bfd
|
||||||
|
8d639f381c
|
||||||
|
25ec58dace
|
||||||
|
a0ecef7443
|
||||||
|
cab4672699
|
||||||
|
f8a021b691
|
||||||
|
5314cc63d8
|
||||||
|
ad4a8a3882
|
||||||
|
927ead1dec
|
||||||
|
743a8e8162
|
||||||
|
f54694666a
|
||||||
|
38f8ff49c4
|
||||||
|
6da025be13
|
||||||
|
afe671009f
|
||||||
|
8a18526cc5
|
||||||
|
fe6ead39e7
|
||||||
|
07b0391c5b
|
||||||
|
aaf6d9cef5
|
||||||
|
ee12fe5bbf
|
||||||
|
96b833ccc2
|
||||||
|
690eec798e
|
||||||
|
142c3cb709
|
||||||
|
cc585eea85
|
||||||
|
1b426bf077
|
||||||
|
3df69e83bc
|
||||||
|
8fba107bbc
|
||||||
|
79e24823d2
|
||||||
|
3ec6e52678
|
||||||
|
e1e0d87659
|
||||||
|
4c12460af8
|
||||||
|
7d506534de
|
||||||
|
c4946d9a72
|
||||||
|
80d2b13291
|
||||||
|
0c36d4a7a7
|
||||||
|
776cef50a2
|
||||||
|
1cc64b5158
|
||||||
|
ee78890172
|
||||||
|
63fa57ad63
|
||||||
|
9072b8fad8
|
||||||
|
b3c407a858
|
||||||
|
833c54a7a5
|
||||||
|
99d2c32c35
|
||||||
|
4f5e5357e2
|
||||||
|
82cea5924d
|
||||||
|
fec8fa57ef
|
||||||
|
11b49b1b2b
|
||||||
|
e40a8e6e3f
|
||||||
|
2fbe63bb85
|
||||||
|
f76830b226
|
||||||
|
76544233e5
|
||||||
|
2fc8c544ef
|
||||||
|
2a4cc77d6b
|
||||||
|
f52eebdd85
|
||||||
|
508e07aca5
|
||||||
|
936aed7997
|
||||||
|
0fbf70c4c6
|
||||||
|
bf47dfd6b7
|
||||||
|
81e7d42454
|
||||||
|
96b701ef5d
|
||||||
|
11d3ecfa1b
|
||||||
|
e0bd39c427
|
||||||
|
6fa914114e
|
||||||
|
d7a3b89055
|
||||||
|
e417131533
|
||||||
|
1fef16c2ce
|
||||||
|
1af12ecd77
|
||||||
|
37e1a8f1dc
|
||||||
|
65e4521004
|
||||||
|
a0be6cc4fa
|
||||||
|
90bcf9dbaa
|
||||||
|
8f169642c4
|
||||||
|
ac5b109c5b
|
||||||
108
web/mock/data.json
Normal file
108
web/mock/data.json
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"mapping": [
|
||||||
|
[
|
||||||
|
"123456789ABC",
|
||||||
|
{
|
||||||
|
"first": "Feuerwehrman",
|
||||||
|
"last": "Sam"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"A1B2C3D4E5F6",
|
||||||
|
{
|
||||||
|
"first": "Luna",
|
||||||
|
"last": "Starforge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0F1E2D3C4B5A",
|
||||||
|
{
|
||||||
|
"first": "Gareth",
|
||||||
|
"last": "Ironwill"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ABCDEF123456",
|
||||||
|
{
|
||||||
|
"first": "Nina",
|
||||||
|
"last": "Skylark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"654321FEDCBA",
|
||||||
|
{
|
||||||
|
"first": "Tobias",
|
||||||
|
"last": "Marrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"DEADBEEFCAFE",
|
||||||
|
{
|
||||||
|
"first": "Astra",
|
||||||
|
"last": "Vale"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"BADA55C0FFEE",
|
||||||
|
{
|
||||||
|
"first": "Rowan",
|
||||||
|
"last": "Tempest"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"C001D00D1337",
|
||||||
|
{
|
||||||
|
"first": "Juniper",
|
||||||
|
"last": "Voss"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"A0B1DB0D133B",
|
||||||
|
{
|
||||||
|
"first": "Öäü",
|
||||||
|
"last": "ßẞ"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"days": [
|
||||||
|
{
|
||||||
|
"date": 20372,
|
||||||
|
"ids": [
|
||||||
|
"123456789ABC",
|
||||||
|
"A1B2C3D4E5F6",
|
||||||
|
"A0B1DB0D133B"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20373,
|
||||||
|
"ids": [
|
||||||
|
"0F1E2D3C4B5A",
|
||||||
|
"ABCDEF123456"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20374,
|
||||||
|
"ids": [
|
||||||
|
"654321FEDCBA",
|
||||||
|
"DEADBEEFCAFE",
|
||||||
|
"BADA55C0FFEE",
|
||||||
|
"A0B1DB0D133B"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20375,
|
||||||
|
"ids": [
|
||||||
|
"C001D00D1337",
|
||||||
|
"A1B2C3D4E5F6",
|
||||||
|
"123456789ABC"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20376,
|
||||||
|
"ids": [
|
||||||
|
"N0T3X1ST1D0",
|
||||||
|
"654321FEDCBA"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
111
web/mock/server.js
Normal file
111
web/mock/server.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import express from "express";
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
|
||||||
|
import mockData from "./data.json" with {type: "json"};
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
const SECS_IN_DAY = 86_400;
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
function generateRandomId() {
|
||||||
|
const chars = "ABCDEF0123456789";
|
||||||
|
let id = "";
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/mapping
|
||||||
|
app.get("/api/mapping", (req, res) => {
|
||||||
|
res.json(mockData.mapping);
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mapping
|
||||||
|
app.post("/api/mapping", (req, res) => {
|
||||||
|
const { id, name } = req.body;
|
||||||
|
|
||||||
|
if (!id || !name || !name.first || !name.last) {
|
||||||
|
return res.status(400).json({ error: "Invalid request body" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ID already exists
|
||||||
|
const existing = mappings.find((entry) => entry[0] === id);
|
||||||
|
if (existing) {
|
||||||
|
return res.status(409).json({ error: "ID already exists" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new mapping
|
||||||
|
mockData.mappings.push([id, name]);
|
||||||
|
|
||||||
|
res.status(201).send("");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/day", (req, res) => {
|
||||||
|
let day;
|
||||||
|
|
||||||
|
if (req.query.day) {
|
||||||
|
day = parseInt(req.query.day, 10);
|
||||||
|
}else if (req.query.timestamp) {
|
||||||
|
let ts = parseInt(req.query.timestamp, 10);
|
||||||
|
day = ts / SECS_IN_DAY;
|
||||||
|
}else {
|
||||||
|
return res.status(400).json({ error: "Missing or invalid 'day' parameter" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(day)) {
|
||||||
|
return res.status(400).json({ error: "Missing or invalid 'day' parameter" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundDay = mockData.days.find(e => e.date == day);
|
||||||
|
|
||||||
|
if (!foundDay) {
|
||||||
|
return res.status(404).send("Not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(foundDay);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/days", (req,res) => {
|
||||||
|
|
||||||
|
let qFrom = parseInt(req.query.from) / SECS_IN_DAY;
|
||||||
|
let qTo = parseInt(req.query.to) / SECS_IN_DAY;
|
||||||
|
|
||||||
|
let days = mockData.days.filter(e => e.date >= qFrom && e.date <= qTo).map(e => e.date);
|
||||||
|
|
||||||
|
res.status(200).json(days);
|
||||||
|
});
|
||||||
|
|
||||||
|
// SSE route: /api/idevent
|
||||||
|
app.get("/api/idevent", (req, res) => {
|
||||||
|
// Set headers for SSE
|
||||||
|
res.setHeader("Content-Type", "text/event-stream");
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
res.setHeader("Connection", "keep-alive");
|
||||||
|
|
||||||
|
res.flushHeaders(); // flush the headers to establish SSE connection
|
||||||
|
|
||||||
|
// Send initial event
|
||||||
|
const sendEvent = () => {
|
||||||
|
const id = generateRandomId();
|
||||||
|
res.write(`data: ${id}\n\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send immediately and then every 10 seconds
|
||||||
|
sendEvent();
|
||||||
|
const interval = setInterval(sendEvent, 10000);
|
||||||
|
|
||||||
|
// When client closes connection, stop interval
|
||||||
|
req.on("close", () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Mock API server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
871
web/package-lock.json
generated
871
web/package-lock.json
generated
@@ -14,6 +14,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
"svelte": "^5.28.1",
|
"svelte": "^5.28.1",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.1.6",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
@@ -1078,6 +1080,20 @@
|
|||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
@@ -1111,6 +1127,68 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"raw-body": "^3.0.0",
|
||||||
|
"type-is": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@@ -1146,6 +1224,49 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -1174,6 +1295,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
@@ -1183,6 +1314,38 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.1",
|
"version": "5.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||||
@@ -1196,6 +1359,39 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||||
@@ -1236,6 +1432,13 @@
|
|||||||
"@esbuild/win32-x64": "0.25.4"
|
"@esbuild/win32-x64": "0.25.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esm-env": {
|
"node_modules/esm-env": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
@@ -1253,6 +1456,59 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.4.4",
|
"version": "6.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||||
@@ -1267,6 +1523,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1281,12 +1575,164 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"toidentifier": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors/node_modules/statuses": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
@@ -1560,6 +2006,62 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@@ -1631,6 +2133,73 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1677,6 +2246,79 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"iconv-lite": "0.7.0",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body/node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -1730,6 +2372,23 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
@@ -1743,6 +2402,156 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.5",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.1",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1752,6 +2561,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.30.1",
|
"version": "5.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz",
|
||||||
@@ -1850,6 +2669,31 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
@@ -1864,6 +2708,26 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
@@ -1957,6 +2821,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"svelte": "^5.28.1",
|
"svelte": "^5.28.1",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.1.6",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"express": "^5.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
|
|||||||
@@ -3,15 +3,21 @@
|
|||||||
import IDTable from "./lib/IDTable.svelte";
|
import IDTable from "./lib/IDTable.svelte";
|
||||||
import LastId from "./lib/LastID.svelte";
|
import LastId from "./lib/LastID.svelte";
|
||||||
import AddIDModal from "./lib/AddIDModal.svelte";
|
import AddIDModal from "./lib/AddIDModal.svelte";
|
||||||
|
import ExportModal from "./lib/ExportModal.svelte";
|
||||||
|
import { generateCSVFile } from "./lib/exporting";
|
||||||
|
import { fetchMapping, type IDMap } from "./lib/IDMapping";
|
||||||
|
import { downloadBlob } from "./lib/downloadBlob";
|
||||||
|
|
||||||
let lastID: string = $state("");
|
let lastID: string = $state("");
|
||||||
|
let mapping: IDMap | null = $state(null);
|
||||||
|
|
||||||
let addModal: AddIDModal;
|
let addModal: AddIDModal;
|
||||||
let idTable: IDTable;
|
let exportModal: ExportModal;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
mapping = await fetchMapping();
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
let sse = new EventSource("/api/idevent");
|
let sse = new EventSource("/api/idevent");
|
||||||
|
|
||||||
sse.onmessage = (e) => {
|
sse.onmessage = (e) => {
|
||||||
lastID = e.data;
|
lastID = e.data;
|
||||||
};
|
};
|
||||||
@@ -25,13 +31,14 @@
|
|||||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<button
|
||||||
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
||||||
href="/api/csv"
|
onclick={() => {
|
||||||
download="anwesenheit.csv"
|
exportModal.open();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Download CSV
|
Export CSV
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<div class="pt-3 pb-2">
|
<div class="pt-3 pb-2">
|
||||||
<LastId
|
<LastId
|
||||||
@@ -42,15 +49,32 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IDTable bind:this={idTable} onEdit={(id,firstName,lastName)=>{
|
{#if mapping}
|
||||||
addModal.open(id,firstName,lastName);
|
<IDTable
|
||||||
}}/>
|
data={mapping}
|
||||||
|
onEdit={(id, firstName, lastName) => {
|
||||||
|
addModal.open(id, firstName, lastName);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddIDModal
|
<AddIDModal
|
||||||
bind:this={addModal}
|
bind:this={addModal}
|
||||||
onSubmitted={() => {
|
onSubmitted={async () => {
|
||||||
idTable.reloadData();
|
mapping = await fetchMapping();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExportModal
|
||||||
|
bind:this={exportModal}
|
||||||
|
onSubmitted={async (from, to) => {
|
||||||
|
if (!mapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let csvFile = await generateCSVFile(from, to, mapping);
|
||||||
|
|
||||||
|
downloadBlob("export.csv",csvFile,"text/csv");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
31
web/src/lib/Day.ts
Normal file
31
web/src/lib/Day.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export type Day = number;
|
||||||
|
|
||||||
|
export interface AttendanceDay {
|
||||||
|
date: Day,
|
||||||
|
ids: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dayToDate(day: Day): Date {
|
||||||
|
const SEC_PER_DAY = 86_400;
|
||||||
|
|
||||||
|
return new Date(day * SEC_PER_DAY * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDay(day: Day): Promise<AttendanceDay> {
|
||||||
|
let res = await fetch("/api/day?" + (new URLSearchParams({ day: day.toString() }).toString()));
|
||||||
|
|
||||||
|
let json = await res.json();
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDays(from: Date, to: Date): Promise<Day[]> {
|
||||||
|
let q = new URLSearchParams({ from: (from.getTime() / 1000).toString(), to: (to.getTime() / 1000).toString() });
|
||||||
|
|
||||||
|
let res = await fetch("/api/days?" + q);
|
||||||
|
|
||||||
|
let json = await res.json();
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
142
web/src/lib/ExportModal.svelte
Normal file
142
web/src/lib/ExportModal.svelte
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "./Modal.svelte";
|
||||||
|
|
||||||
|
let { onSubmitted }: { onSubmitted?: (from: Date, to: Date) => void } =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
let modal: Modal;
|
||||||
|
|
||||||
|
let fromDate: string | undefined = $state();
|
||||||
|
let toDate: string | undefined = $state();
|
||||||
|
|
||||||
|
let selectedYear: number = $state(new Date().getFullYear());
|
||||||
|
|
||||||
|
let selectedTab = $state(0);
|
||||||
|
|
||||||
|
export function open() {
|
||||||
|
modal.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateYears() {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const startingYear = 2020;
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
new Array(currentYear + 1 - startingYear),
|
||||||
|
(_, i) => i + startingYear,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onsubmit(e: SubmitEvent) {
|
||||||
|
let from: Date;
|
||||||
|
let to: Date;
|
||||||
|
|
||||||
|
switch (selectedTab) {
|
||||||
|
case 0:
|
||||||
|
if (!fromDate || !toDate) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
from = new Date(fromDate);
|
||||||
|
to = new Date(toDate);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
from = new Date(selectedYear, 0);
|
||||||
|
to = new Date(selectedYear + 1, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Invalid tab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitted?.(from, to);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<div class="flex">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
selectedTab = 0;
|
||||||
|
}}
|
||||||
|
class="tab {selectedTab === 0 ? 'tab-active' : ''}"
|
||||||
|
>
|
||||||
|
Datum
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
selectedTab = 1;
|
||||||
|
}}
|
||||||
|
class="tab {selectedTab === 1 ? 'tab-active' : ''}"
|
||||||
|
>
|
||||||
|
Jahr
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" {onsubmit} class="flex flex-col">
|
||||||
|
{#if selectedTab === 0}
|
||||||
|
<div>
|
||||||
|
<label class="form-row">
|
||||||
|
<span>Von:</span>
|
||||||
|
<input type="date" class="form-input" bind:value={fromDate} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-row">
|
||||||
|
<span>Bis:</span>
|
||||||
|
<input type="date" class="form-input" bind:value={toDate} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedTab === 1}
|
||||||
|
<div>
|
||||||
|
<label class="form-row">
|
||||||
|
<span>Kalendar Jahr:</span>
|
||||||
|
<select class="form-input" bind:value={selectedYear}>
|
||||||
|
{#each generateYears() as year}
|
||||||
|
<option value={year}>{year}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-3">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
class="mr-5 px-2 py-1 bg-red-500 rounded-2xl shadow-md"
|
||||||
|
onclick={() => {
|
||||||
|
modal.close();
|
||||||
|
|
||||||
|
fromDate = undefined;
|
||||||
|
toDate = undefined;
|
||||||
|
}}>Abbrechen</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-2 py-1 bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
||||||
|
>Export CSV</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "../app.css";
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
@apply px-4 py-2 rounded-t-lg bg-indigo-600 hover:bg-indigo-700 font-medium border-b-2 border-transparent cursor-pointer transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-active {
|
||||||
|
@apply px-4 py-2 bg-indigo-500 font-semibold border-b-2 border-blue-600 shadow-sm cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
@apply flex justify-between my-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
@apply ml-20;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
export interface IDMapping {
|
|
||||||
id_map: IDMap
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDMap {
|
export interface IDMap {
|
||||||
[name: string]: Name
|
[name: string]: Name
|
||||||
}
|
}
|
||||||
@@ -11,6 +7,23 @@ export interface Name {
|
|||||||
last: string,
|
last: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stupidSerdeFix(pairs: [string, Name][]): IDMap {
|
||||||
|
const map: IDMap = {};
|
||||||
|
for (const [key, value] of pairs) {
|
||||||
|
map[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMapping(): Promise<IDMap> {
|
||||||
|
let res = await fetch("/api/mapping");
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
return stupidSerdeFix(data);
|
||||||
|
}
|
||||||
|
|
||||||
export async function addMapping(id: string, firstName: string, lastName: string) {
|
export async function addMapping(id: string, firstName: string, lastName: string) {
|
||||||
let req = await fetch("/api/mapping", {
|
let req = await fetch("/api/mapping", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { type IDMap } from "./IDMapping";
|
||||||
import type { IDMapping } from "./IDMapping";
|
|
||||||
let data: IDMapping | undefined = $state();
|
|
||||||
|
|
||||||
let { onEdit }: { onEdit?: (string,string,string) => void } = $props();
|
let {
|
||||||
|
onEdit,
|
||||||
export async function reloadData() {
|
data,
|
||||||
let res = await fetch("/api/mapping");
|
}: {
|
||||||
|
onEdit?: (id: string, firstName: string, lastName: string) => void;
|
||||||
data = await res.json();
|
data: IDMap;
|
||||||
}
|
} = $props();
|
||||||
|
|
||||||
let rows = $derived(
|
let rows = $derived(
|
||||||
data
|
data
|
||||||
? Object.entries(data.id_map).map(([id, value]) => ({
|
? Object.entries(data).map(([id, value]) => ({
|
||||||
id,
|
id,
|
||||||
...value,
|
...value,
|
||||||
}))
|
}))
|
||||||
@@ -43,70 +41,64 @@
|
|||||||
if (sortKey !== key) return "";
|
if (sortKey !== key) return "";
|
||||||
return sortDirection === "asc" ? "▲" : "▼";
|
return sortDirection === "asc" ? "▲" : "▼";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await reloadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data == null}
|
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
||||||
Loading...
|
<table class="px-10">
|
||||||
{:else}
|
<thead>
|
||||||
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
<tr>
|
||||||
<table class="px-10">
|
<th
|
||||||
<thead>
|
class="text-left pr-5 pl-2 cursor-pointer select-none"
|
||||||
<tr>
|
onclick={() => {
|
||||||
<th
|
handleSortClick("id");
|
||||||
class="text-left pr-5 pl-2 cursor-pointer select-none"
|
}}
|
||||||
onclick={() => {
|
>
|
||||||
handleSortClick("id");
|
ID
|
||||||
}}
|
<span class="indicator">{indicator("id")}</span>
|
||||||
>
|
</th>
|
||||||
ID
|
<th
|
||||||
<span class="indicator">{indicator("id")}</span>
|
class="text-left pr-5 cursor-pointer select-none"
|
||||||
</th>
|
onclick={() => {
|
||||||
<th
|
handleSortClick("last");
|
||||||
class="text-left pr-5 cursor-pointer select-none"
|
}}
|
||||||
onclick={() => {
|
>
|
||||||
handleSortClick("last");
|
Nachname
|
||||||
}}
|
<span class="indicator">{indicator("last")}</span>
|
||||||
>
|
</th>
|
||||||
Nachname
|
<th
|
||||||
<span class="indicator">{indicator("last")}</span>
|
class="text-left pr-5 cursor-pointer select-none"
|
||||||
</th>
|
onclick={() => {
|
||||||
<th
|
handleSortClick("first");
|
||||||
class="text-left pr-5 cursor-pointer select-none"
|
}}
|
||||||
onclick={() => {
|
>Vorname
|
||||||
handleSortClick("first");
|
|
||||||
}}
|
|
||||||
>Vorname
|
|
||||||
|
|
||||||
<span class="indicator">{indicator("first")}</span>
|
<span class="indicator">{indicator("first")}</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th> </th>
|
||||||
</th>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each rowsSorted as row}
|
||||||
|
<tr class="even:bg-indigo-600">
|
||||||
|
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
|
||||||
|
<td class="whitespace-nowrap pr-5">{row.last}</td>
|
||||||
|
<td class="whitespace-nowrap pr-5">{row.first}</td>
|
||||||
|
<td class="pr-5"
|
||||||
|
><button
|
||||||
|
onclick={() => {
|
||||||
|
onEdit && onEdit(row.id, row.first, row.last);
|
||||||
|
}}
|
||||||
|
class="cursor-pointer">🔧</button
|
||||||
|
></td
|
||||||
|
>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{/each}
|
||||||
<tbody>
|
</tbody>
|
||||||
{#each rowsSorted as row}
|
</table>
|
||||||
<tr class="even:bg-indigo-600">
|
</div>
|
||||||
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
|
|
||||||
<td class="whitespace-nowrap pr-5">{row.last}</td>
|
|
||||||
<td class="whitespace-nowrap pr-5">{row.first}</td>
|
|
||||||
<td class="pr-5" ><button onclick={()=>{
|
|
||||||
onEdit && onEdit(row.id,row.first,row.last);
|
|
||||||
}} class="cursor-pointer">🔧</button></td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
@reference "../app.css";
|
@reference "../app.css";
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
@apply ml-1 w-4 inline-block;
|
@apply ml-1 w-4 inline-block;
|
||||||
}
|
}
|
||||||
|
|||||||
103
web/src/lib/csv.ts
Normal file
103
web/src/lib/csv.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
interface CSVOptions {
|
||||||
|
delimiter?: string;
|
||||||
|
headerOrder?: string[];
|
||||||
|
eol?: string;
|
||||||
|
includeBOM?: boolean;
|
||||||
|
nullString?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowObject = Record<string, any>;
|
||||||
|
type InputRows = RowObject[];
|
||||||
|
|
||||||
|
export function generateCSVString(input: InputRows, opts: CSVOptions = {}): string {
|
||||||
|
const {
|
||||||
|
delimiter = ";",
|
||||||
|
headerOrder,
|
||||||
|
eol = "\r\n",
|
||||||
|
includeBOM = true,
|
||||||
|
nullString = "",
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
// Check if the value need quoting
|
||||||
|
// Usually not needed in our use case but still in case
|
||||||
|
const needsQuoting = (s: string) =>
|
||||||
|
s.includes(delimiter) || s.includes('"') || s.includes("\n") || s.includes("\r") || /^\s|\s$/.test(s);
|
||||||
|
|
||||||
|
// Transform the value of a cell in a SAFE string
|
||||||
|
const escapeCell = (raw: any): string => {
|
||||||
|
if (raw === null || raw === undefined) return nullString;
|
||||||
|
|
||||||
|
let s = stringify(raw);
|
||||||
|
|
||||||
|
// Replace quotes
|
||||||
|
if (s.includes('"')) s = s.replace(/"/g, '""');
|
||||||
|
|
||||||
|
return needsQuoting(s) ? `"${s}"` : s;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform the value of a cell into a string
|
||||||
|
function stringify(v: any): string {
|
||||||
|
if (v === null || v === undefined) return nullString;
|
||||||
|
if (v instanceof Date) return v.toLocaleDateString();
|
||||||
|
if (typeof v === "boolean") return v ? "X" : "";
|
||||||
|
|
||||||
|
if (typeof v === "object") {
|
||||||
|
// CHeck if array and join with "|"
|
||||||
|
if (Array.isArray(v)) return v.map(item => (item === null || item === undefined ? nullString : String(item))).join("|");
|
||||||
|
|
||||||
|
// If all fails parse it via json
|
||||||
|
// Should also not happen in our use case
|
||||||
|
try { return JSON.stringify(v); } catch { return String(v); }
|
||||||
|
}
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objs = (input as RowObject[]) || [];
|
||||||
|
|
||||||
|
// Derive headers in the order keys are first encountered (fixed: no reduce)
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const derivedOrder: string[] = [];
|
||||||
|
for (const obj of objs) {
|
||||||
|
if (!obj || typeof obj !== "object") continue;
|
||||||
|
for (const k of Object.keys(obj)) {
|
||||||
|
if (!seen.has(k)) {
|
||||||
|
seen.add(k);
|
||||||
|
derivedOrder.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply headerOrder if provided: put listed columns first (in the order provided),
|
||||||
|
// then append the remaining derived headers in their derived order.
|
||||||
|
let finalHeaders = derivedOrder.slice();
|
||||||
|
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
|
||||||
|
const headSet = new Set(headerOrder);
|
||||||
|
const first = headerOrder.filter(h => seen.has(h)); // keep only headers that actually exist
|
||||||
|
const rest = derivedOrder.filter(h => !headSet.has(h));
|
||||||
|
finalHeaders = first.concat(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsOut: string[] = [];
|
||||||
|
|
||||||
|
if (finalHeaders.length > 0) {
|
||||||
|
rowsOut.push(finalHeaders.map(h => escapeCell(h)).join(delimiter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse every row
|
||||||
|
for (const obj of objs) {
|
||||||
|
|
||||||
|
// Check if obj is null or somthing else we can't convert
|
||||||
|
if (!obj || typeof obj !== "object") {
|
||||||
|
|
||||||
|
// produce empty row with same number of columns
|
||||||
|
rowsOut.push(finalHeaders.map(() => escapeCell(null)).join(delimiter));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For every header check if a value on our row exist and print it
|
||||||
|
const row = finalHeaders.map(col => escapeCell(col in obj ? (obj as any)[col] : null)).join(delimiter);
|
||||||
|
rowsOut.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (includeBOM ? "\uFEFF" : "") + rowsOut.join(eol);
|
||||||
|
}
|
||||||
8
web/src/lib/downloadBlob.ts
Normal file
8
web/src/lib/downloadBlob.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function downloadBlob(filename: string, content: string, mimeType = "text/plain") {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
53
web/src/lib/exporting.ts
Normal file
53
web/src/lib/exporting.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { generateCSVString } from "./csv";
|
||||||
|
import { dayToDate, fetchDay, fetchDays, type AttendanceDay, type Day } from "./Day";
|
||||||
|
import type { IDMap } from "./IDMapping";
|
||||||
|
|
||||||
|
interface CSVRow {
|
||||||
|
ID: string
|
||||||
|
Vorname: string
|
||||||
|
Nachname: string
|
||||||
|
[key: string]: string | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareRows(mapping: IDMap, days: AttendanceDay[]): CSVRow[] {
|
||||||
|
let csvData: CSVRow[] = [];
|
||||||
|
|
||||||
|
const allIDs = Object.keys(mapping);
|
||||||
|
|
||||||
|
for (const id of allIDs) {
|
||||||
|
const name = mapping[id];
|
||||||
|
const row: CSVRow = {
|
||||||
|
ID: id,
|
||||||
|
Vorname: name.first,
|
||||||
|
Nachname: name.last,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const day of days) {
|
||||||
|
const dayKey = dayToDate(day.date).toLocaleDateString();
|
||||||
|
row[dayKey] = day.ids.includes(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
csvData.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return csvData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDays(from: Date, to: Date): Promise<AttendanceDay[]> {
|
||||||
|
const recordedDays: Day[] = await fetchDays(from, to);
|
||||||
|
let days: AttendanceDay[] = [];
|
||||||
|
|
||||||
|
for (const day of recordedDays) {
|
||||||
|
days.push(await fetchDay(day))
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateCSVFile(from: Date, to: Date, mapping: IDMap): Promise<string> {
|
||||||
|
const days = await getDays(from, to);
|
||||||
|
const rows = prepareRows(mapping, days);
|
||||||
|
const csvString = generateCSVString(rows);
|
||||||
|
|
||||||
|
return csvString;
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:3000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user