Compare commits

...

69 Commits

Author SHA1 Message Date
tdfischer fb35f88c14 tasks: usb_power: build++ 2026-03-27 22:55:02 +01:00
tdfischer d01cddb423 tasks: wifi: expose cloud sensor state through a regular ping of nextcloud 2026-03-27 22:54:26 +01:00
tdfischer a5a2fdc1c7 tasks: ui: make the notification blink faster 2026-03-27 22:53:58 +01:00
tdfischer c747a0fbf1 tasks: mpu: rewrite to drop usage of backoff 2026-03-27 22:53:37 +01:00
tdfischer fc9bd0b7a7 ego: engine: move all the async timeouts into the engine's commit method 2026-03-27 22:53:09 +01:00
tdfischer 4977746af1 simulation: rewrite the storage and simulation stack 2026-03-27 22:45:54 +01:00
tdfischer 8a3b2303a2 tasks: motion: give warning if recording bus is stalled 2026-03-27 22:39:42 +01:00
tdfischer 8ff29caa6f graphics: rewrite the DisplayControls to use a write-ack mechanism between the renderer and ui tasks 2026-03-27 22:38:49 +01:00
tdfischer 3fff4fdfd1 main: prettify the sensor status line 2026-03-24 12:49:00 +01:00
tdfischer f803b3c2d4 tasks: gps: drop complicated usage of Backoff 2026-03-24 12:44:26 +01:00
tdfischer ee5e2e2a4b tasks: oled: cleanup 2026-03-24 12:43:31 +01:00
tdfischer db65fbafd8 simdata: refactor RmpData trait to accept generic RmpRead/Write traits 2026-03-24 12:42:54 +01:00
tdfischer 00d3df315b simdata: add an error code for not enough space while writing 2026-03-24 12:41:41 +01:00
tdfischer 73e0773942 animation: fix crash with zero-duration animations 2026-03-24 12:40:01 +01:00
tdfischer e18425e0ca tasks: ui: use const animators 2026-03-24 12:37:53 +01:00
tdfischer 38f49513f3 tasks: ble: drop unnessicary Backoff usage 2026-03-24 12:36:25 +01:00
tdfischer 6779881523 tasks: ble: use const init of client predictions pipe 2026-03-24 12:35:23 +01:00
tdfischer cda0f424b1 logging: cleanup 2026-03-24 12:33:23 +01:00
tdfischer fb037a6aac graphics: warnings-- 2026-03-24 12:32:39 +01:00
tdfischer 3a4ce6344e tasks: ui: simplify matches 2026-03-14 23:51:49 +01:00
tdfischer bf2d1c6077 tasks: oled_render: reduce fps to 15 2026-03-14 14:17:50 +01:00
tdfischer e5f96bc87c cargo: bump 2026-03-14 12:21:54 +01:00
tdfischer a0580b08e6 board: last minute tweaks before manufacturing 2026-03-14 12:21:30 +01:00
tdfischer b031e4c64b main: update storage api 2026-03-14 12:21:03 +01:00
tdfischer f64dbae4e5 tasks: oled: re-enable oled slow frame warning 2026-03-14 12:20:35 +01:00
tdfischer 2213c56ddb storage: build++ 2026-03-14 12:19:51 +01:00
tdfischer aa69464258 graphics: display: set default power to 500mA 2026-03-14 12:19:36 +01:00
tdfischer f8e53e85a7 tasks: implement a task for writing measurement straems to the SD card 2026-03-14 12:19:24 +01:00
tdfischer 462de0af99 graphics: display: also add power setting to the sync version 2026-03-14 12:15:38 +01:00
tdfischer 040a419a2f tasks: usb_power: reimplement tusb320 API using embedded_hal_async traits, and implement a task for managing the power status 2026-03-14 12:15:12 +01:00
tdfischer 2ad22a2632 display: implement power scaling into the display APIs 2026-03-14 12:06:03 +01:00
tdfischer 3f87e22585 cargo: replace the 'motion' features with an i2c on/off flag, add a flag to switch between the mpu6050 and the next iteration board with the LSM6DS chip 2026-03-11 14:37:16 +01:00
tdfischer e09649592b tasks: first implementation of protocol for the TUSB320 USB-PD chip. also untested!!! 2026-03-11 14:01:40 +01:00
tdfischer ccb2680954 main: first implementation of a way to handle interrupts from sensors. untested!! 2026-03-11 14:00:25 +01:00
tdfischer 94144ab4b3 board: update some pins to match current devkits 2026-03-11 13:40:25 +01:00
tdfischer 25a6122d53 main: lots changed, I lost track 2026-03-09 10:31:37 +01:00
tdfischer 4e5f053c18 animation: move a few more steps towards a real generic animation framework 2026-03-09 10:29:40 +01:00
tdfischer 49c201add7 cargo: config: re-enable SPI in ram, disable USB PHY 2026-03-09 10:28:31 +01:00
tdfischer 4eafbde5a9 build.rs: use the streamindex to write the correct data, instead of hand-crafted binary bits 2026-03-09 10:27:58 +01:00
tdfischer d942561900 cargo: bump 2026-03-09 10:27:34 +01:00
tdfischer 45d84a91dc tasks: safetyui: re-add waiting for the renderer before starting up 2026-03-09 10:25:09 +01:00
tdfischer 74d753e0b3 tasks: wifi: tweak the hardware config for more reliability where possible 2026-03-09 10:24:21 +01:00
tdfischer 9ecfd11982 tasks: motion: implement a sensor recording layer 2026-03-09 10:22:30 +01:00
tdfischer 96e128ac67 storage: implement storage writing 2026-03-09 10:20:00 +01:00
tdfischer caefdfe131 simdata: implement bundle streams 2026-03-09 10:17:44 +01:00
tdfischer f0d7968843 graphics: display: implement dynamic target fps 2026-03-09 10:16:02 +01:00
tdfischer c77ecc9a19 tasks: wifi: report cloud connectivity status in the http telemetry task 2026-03-09 10:14:20 +01:00
tdfischer 561fef0ed1 tasks: safetyui: move one more animator into a global const 2026-03-09 10:12:36 +01:00
tdfischer 6c43d9f2b7 tasks: motion: improve error messages 2026-03-09 10:11:24 +01:00
tdfischer 3479fd1bf8 tasks: display: docs++ 2026-03-09 10:10:30 +01:00
tdfischer c9518498f2 tasks: ble: warning-- 2026-03-09 10:09:52 +01:00
tdfischer f650cfa644 tasks: gps: refine the gps sensor state reporting, and start handling GPS time 2026-03-09 10:08:40 +01:00
tdfischer 18458bbf27 tracing: first implementation of rtos-trace glue 2026-03-09 10:07:13 +01:00
tdfischer 030575f395 tasks: oled: update to new figments crates 2026-03-09 10:05:45 +01:00
tdfischer e2a26dab5a logging: add timestamp 2026-03-09 10:03:59 +01:00
tdfischer 99a3c61f20 board: add first revision of the new board 2026-03-09 10:02:18 +01:00
tdfischer c23764c6ef tasks: ui: docs++ 2026-03-09 10:01:54 +01:00
tdfischer a62aefda77 tasks: motion: add hard timeouts to the motion engine, to catch deadlocks 2026-02-28 17:18:34 +01:00
tdfischer 0d8254501b tasks: ble: cleanup 2026-02-28 17:16:46 +01:00
tdfischer 3b763fcf99 graphics: display: figments++ 2026-02-28 17:16:13 +01:00
tdfischer 315fa633c1 ego: engine: move some of the system constants out into real const values 2026-02-28 17:15:32 +01:00
tdfischer 54ebbbaea7 tasks: demo: build++ 2026-02-28 17:00:30 +01:00
tdfischer 816cc20087 tasks: use new animations api 2026-02-28 16:59:29 +01:00
tdfischer e8a7a18539 graphics: more figments updates 2026-02-28 16:58:11 +01:00
tdfischer 22d1b4d077 tasks: more Fract8 update 2026-02-28 16:54:44 +01:00
tdfischer 935f30d968 events: combine gravity and forwards fusion sensors into a shared motionframe 2026-02-28 16:52:00 +01:00
tdfischer f2b2674158 tasks: render: switch to using the new figments DMA driver 2026-02-28 16:49:26 +01:00
tdfischer 319c812a61 graphics: update to use figments Fract8 type 2026-02-28 16:47:25 +01:00
tdfischer 95ebc7820a logging: implement a way to set the logging levels at runtime 2026-02-28 16:43:25 +01:00
65 changed files with 35195 additions and 1199 deletions
+5 -5
View File
@@ -2,11 +2,11 @@
runner = "espflash flash --monitor --partition-table ./partitions.csv" runner = "espflash flash --monitor --partition-table ./partitions.csv"
[env] [env]
ESP_LOG="info" ESP_HAL_CONFIG_PLACE_SPI_MASTER_DRIVER_IN_RAM="true"
ESP_HAL_CONFIG_PLACE_RMT_DRIVER_IN_RAM="true" ESP_RTOS_CONFIG_SW_TASK_OVERFLOW_DETECTION="true"
ESP_HAL_EMBASSY_CONFIG_TIMER_QUEUE="multiple-integrated" ESP_RTOS_CONFIG_HW_TASK_OVERFLOW_DETECTION="true"
ESP_WIFI_CONFIG_COUNTRY_CODE="DE" REASSEMBLY_BUFFER_COUNT="5"
ESP_RTOS_CONFIG_TICK_RATE_HZ="50" ESP_PHY_CONFIG_PHY_ENABLE_USB="false"
[build] [build]
rustflags = [ rustflags = [
Generated
+446 -423
View File
File diff suppressed because it is too large Load Diff
+31 -29
View File
@@ -1,68 +1,62 @@
[package] [package]
edition = "2021" edition = "2024"
name = "renderbug-embassy" name = "renderbug-bike"
rust-version = "1.86" rust-version = "1.92"
version = "0.1.0" version = "0.1.0"
default-run = "renderbug-embassy" default-run = "renderbug-bike"
[[bin]] [[bin]]
name = "renderbug-embassy" name = "renderbug-bike"
path = "./src/bin/main.rs" path = "./src/bin/main.rs"
[features] [features]
default = ["real-output"] default = ["radio", "i2c", "mpu6050", "oled"]
real-output = []
dual-core = [] dual-core = []
simulation = ["dep:rmp"] simulation = []
radio = [ radio = [
"dep:esp-radio", "dep:esp-radio",
"dep:reqwless", "dep:reqwless",
"dep:trouble-host", "dep:trouble-host",
"esp-rtos/esp-radio" "esp-rtos/esp-radio"
] ]
motion = ["mpu", "gps"] i2c = ["dep:nmea"]
mpu6050 = ["i2c", "dep:mpu6050-dmp"]
max-usb-power = [] max-usb-power = []
wokwi = ["max-usb-power"] wokwi = ["max-usb-power"]
mpu = ["dep:mpu6050-dmp"] oled = ["dep:ssd1306", "dep:display-interface"]
gps = ["dep:nmea"]
oled = ["dep:ssd1306"]
rtt = ["dep:rtt-target"] rtt = ["dep:rtt-target"]
demo = [] demo = []
[dependencies] [dependencies]
# The basic requirements for all features # The basic requirements for all features
figments = { path = "../figments/figments/", features = ["alloc", "embedded-graphics"] } figments = { path = "../figments/figments/", features = ["alloc", "embedded-graphics", "log-04"] }
figments-render = { path = "../figments/figments-render/", features = ["smart-leds", "micromath"], default-features = false } figments-render = { path = "../figments/figments-render/", features = ["smart-leds", "micromath", "log-04"], default-features = false }
figments-esp32-ws2812-dma = { path = "../figments/figments-esp32-ws2812-dma/", features = ["esp32s3"] }
esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32s3"] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32s3"] }
esp-hal = { version = "1.0.0", features = [ esp-hal = { version = "1.0.0", features = [
"esp32s3", "esp32s3",
"log-04",
"unstable", "unstable",
"log-04"
] } ] }
esp-alloc = "0.9.0" esp-alloc = { version = "0.9.0", default-features = false, features = ["internal-heap-stats"] }
esp-backtrace = { version = "0.18", features = [ esp-backtrace = { version = "0.18", features = ["esp32s3", "halt-cores", "panic-handler", "println"] }
"esp32s3",
"panic-handler",
"println",
] }
esp-println = { version = "0.16", features = ["esp32s3", "log-04"] } esp-println = { version = "0.16", features = ["esp32s3", "log-04"] }
# for more networking protocol support see https://crates.io/crates/edge-net # for more networking protocol support see https://crates.io/crates/edge-net
critical-section = "1.2.0" critical-section = "1.2.0"
embassy-executor = { version = "0.9.0", features = [ embassy-executor = { version = "0.9.0", default-features = false, features = [
"log", "log",
# "task-arena-size-98304",
] } ] }
embassy-time = { version = "0.5.0", features = ["log"] } embassy-time = { version = "0.5.0", default-features = false, features = ["log"] }
esp-rtos = { version = "0.2.0", features = [ esp-rtos = { version = "0.2.0", features = [
"embassy", "embassy",
"esp-alloc", "esp-alloc",
"esp32s3", "esp32s3",
# "rtos-trace",
"log-04" "log-04"
] } ] }
log = "0.4" log = "0.4"
static_cell = "2.1.1" static_cell = "2.1.1"
rgb = "0.8.52" rgb = "0.8.52"
esp-hal-smartled = { version = "0.17.0", features = ["esp32s3"] }
smart-leds = "0.4.0" smart-leds = "0.4.0"
embassy-sync = "0.7.2" embassy-sync = "0.7.2"
embassy-embedded-hal = "0.5.0" embassy-embedded-hal = "0.5.0"
@@ -74,6 +68,9 @@ enumset = "1.1.10"
enum-map = "2.7.3" enum-map = "2.7.3"
portable-atomic = { version = "1.11", features = ["critical-section"] } portable-atomic = { version = "1.11", features = ["critical-section"] }
embassy-futures = { version = "0.1.2", features = ["log"] } embassy-futures = { version = "0.1.2", features = ["log"] }
embedded-graphics = { version = "0.8.1", features = ["nalgebra_support"] }
ssd1306 = { version = "0.10.0", features = ["async"], optional = true }
display-interface = { version = "0.5.0", optional = true }
# Telemetry outputs # Telemetry outputs
esp-radio = { version = "*", optional = true, features = [ esp-radio = { version = "*", optional = true, features = [
@@ -99,6 +96,7 @@ nmea = { version = "0.7.0", optional = true, default-features = false, features
"GLL", "GLL",
"GST", "GST",
"all-sentences"
] } ] }
mpu6050-dmp = { version = "0.6.1", features = ["async"], optional = true } mpu6050-dmp = { version = "0.6.1", features = ["async"], optional = true }
@@ -108,10 +106,14 @@ rtt-target = { version = "0.6.2", optional = true }
# Simulation # Simulation
esp-storage = { version = "0.7.0", features = ["esp32s3", "critical-section"] } esp-storage = { version = "0.7.0", features = ["esp32s3", "critical-section"] }
embedded-storage = "0.3.1" embedded-storage = "0.3.1"
rmp = { path = "../msgpack-rust/rmp/", optional = true, default-features = false } rmp = { path = "../msgpack-rust/rmp/", default-features = false }
display-interface = "0.5.0" heapless = { version = "0.9.1", features = ["portable-atomic"] }
embassy-net = { version = "0.7.1", features = ["alloc", "dns", "medium-ethernet", "proto-ipv4", "tcp", "udp", "dhcpv4"] } num-traits = { version = "0.2.19", default-features = false }
reqwless = { version = "0.13.0", optional = true } rtos-trace = { version = "0.2.1", default-features = false, features = ["trace_impl"] }
embedded-sdmmc = "0.9.0"
embedded-hal-bus = "0.3.0"
embedded-hal = "1.0.0"
embedded-io = "0.6.1"
[profile.dev] [profile.dev]
# Rust debug is too slow. # Rust debug is too slow.
+40
View File
@@ -0,0 +1,40 @@
# For PCBs designed using KiCad: https://www.kicad.org/
# Format documentation: https://kicad.org/help/file-formats/
# Temporary files
*.000
*.bak
*.bck
*.kicad_pcb-bak
*.kicad_sch-bak
*-backups
*-cache*
*-bak
*-bak*
*~
~*
_autosave-*
\#auto_saved_files\#
*.tmp
*-save.pro
*-save.kicad_pcb
fp-info-cache
~*.lck
\#auto_saved_files#
# Netlist files (exported from Eeschema)
*.net
# Autorouter files (exported from Pcbnew)
*.dsn
*.ses
# Exported BOM files
*.xml
*.csv
# Archived Backups (KiCad 6.0)
**/*-backups/*.zip
# Local project settings
*.kicad_prl
@@ -0,0 +1,9 @@
EESchema-DOCLIB Version 2.0
#
$CMP L6982N50DR
D 38 V, 2 A synchronous step-down converter with 20 uA quiescent current
K
F https://www.st.com/resource/en/datasheet/l6982.pdf
$ENDCMP
#
#End Doc Library
@@ -0,0 +1,78 @@
(kicad_symbol_lib (version 20211014) (generator SamacSys_ECAD_Model)
(symbol "L6982N50DR" (in_bom yes) (on_board yes)
(property "Reference" "IC" (at 34.29 7.62 0)
(effects (font (size 1.27 1.27)) (justify left top))
)
(property "Value" "L6982N50DR" (at 34.29 5.08 0)
(effects (font (size 1.27 1.27)) (justify left top))
)
(property "Footprint" "SOIC127P600X175-8N" (at 34.29 -94.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Datasheet" "https://www.st.com/resource/en/datasheet/l6982.pdf" (at 34.29 -194.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "ki_description" "38 V, 2 A synchronous step-down converter with 20 uA quiescent current" (at 34.29 -294.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Height" "1.75" (at 34.29 -394.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Manufacturer_Name" "STMicroelectronics" (at 34.29 -494.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Manufacturer_Part_Number" "L6982N50DR" (at 34.29 -594.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Mouser Part Number" "511-L6982N50DR" (at 34.29 -694.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Mouser Price/Stock" "https://www.mouser.co.uk/ProductDetail/STMicroelectronics/L6982N50DR?qs=rQFj71Wb1eViugxt34b%2FVA%3D%3D" (at 34.29 -794.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Arrow Part Number" "L6982N50DR" (at 34.29 -894.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Arrow Price/Stock" "https://www.arrow.com/en/products/l6982n50dr/stmicroelectronics?utm_currency=USD&region=nac" (at 34.29 -994.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(rectangle
(start 5.08 2.54)
(end 33.02 -10.16)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin passive line (at 0 0 0) (length 5.08)
(name "SW" (effects (font (size 1.27 1.27))))
(number "1" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 0 -2.54 0) (length 5.08)
(name "BOOT" (effects (font (size 1.27 1.27))))
(number "2" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 0 -5.08 0) (length 5.08)
(name "VCC" (effects (font (size 1.27 1.27))))
(number "3" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 0 -7.62 0) (length 5.08)
(name "VOUT/FB" (effects (font (size 1.27 1.27))))
(number "4" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 38.1 0 180) (length 5.08)
(name "PGND" (effects (font (size 1.27 1.27))))
(number "8" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 38.1 -2.54 180) (length 5.08)
(name "VIN" (effects (font (size 1.27 1.27))))
(number "7" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 38.1 -5.08 180) (length 5.08)
(name "AGND" (effects (font (size 1.27 1.27))))
(number "6" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 38.1 -7.62 180) (length 5.08)
(name "EN/CLKIN" (effects (font (size 1.27 1.27))))
(number "5" (effects (font (size 1.27 1.27))))
)
)
)
@@ -0,0 +1,31 @@
EESchema-LIBRARY Version 2.3
#encoding utf-8
#SamacSys ECAD Model L6982N50DR
#/17168867/1957227/2.50/8/2/Integrated Circuit
DEF L6982N50DR IC 0 30 Y Y 1 F N
F0 "IC" 1350 300 50 H V L CNN
F1 "L6982N50DR" 1350 200 50 H V L CNN
F2 "SOIC127P600X175-8N" 1350 100 50 H I L CNN
F3 "https://www.st.com/resource/en/datasheet/l6982.pdf" 1350 0 50 H I L CNN
F4 "38 V, 2 A synchronous step-down converter with 20 uA quiescent current" 1350 -100 50 H I L CNN "Description"
F5 "1.75" 1350 -200 50 H I L CNN "Height"
F6 "STMicroelectronics" 1350 -300 50 H I L CNN "Manufacturer_Name"
F7 "L6982N50DR" 1350 -400 50 H I L CNN "Manufacturer_Part_Number"
F8 "511-L6982N50DR" 1350 -500 50 H I L CNN "Mouser Part Number"
F9 "https://www.mouser.co.uk/ProductDetail/STMicroelectronics/L6982N50DR?qs=rQFj71Wb1eViugxt34b%2FVA%3D%3D" 1350 -600 50 H I L CNN "Mouser Price/Stock"
F10 "L6982N50DR" 1350 -700 50 H I L CNN "Arrow Part Number"
F11 "https://www.arrow.com/en/products/l6982n50dr/stmicroelectronics?utm_currency=USD&region=nac" 1350 -800 50 H I L CNN "Arrow Price/Stock"
DRAW
X SW 1 0 0 200 R 50 50 0 0 P
X BOOT 2 0 -100 200 R 50 50 0 0 P
X VCC 3 0 -200 200 R 50 50 0 0 P
X VOUT/FB 4 0 -300 200 R 50 50 0 0 P
X PGND 8 1500 0 200 L 50 50 0 0 P
X VIN 7 1500 -100 200 L 50 50 0 0 P
X AGND 6 1500 -200 200 L 50 50 0 0 P
X EN/CLKIN 5 1500 -300 200 L 50 50 0 0 P
P 5 0 1 6 200 100 1300 100 1300 -400 200 -400 200 100 N
ENDDRAW
ENDDEF
#
#End Library
@@ -0,0 +1,81 @@
PCBNEW-LibModule-V1 2026-03-07 11:39:32
# encoding utf-8
Units mm
$INDEX
SOIC127P600X175-8N
$EndINDEX
$MODULE SOIC127P600X175-8N
Po 0 0 0 15 69ac0e74 00000000 ~~
Li SOIC127P600X175-8N
Cd SO 8L
Kw Integrated Circuit
Sc 0
At SMD
AR
Op 0 0 0
T0 0 0 1.27 1.27 0 0.254 N V 21 N "IC**"
T1 0 0 1.27 1.27 0 0.254 N I 21 N "SOIC127P600X175-8N"
DS -3.725 -2.75 3.725 -2.75 0.05 24
DS 3.725 -2.75 3.725 2.75 0.05 24
DS 3.725 2.75 -3.725 2.75 0.05 24
DS -3.725 2.75 -3.725 -2.75 0.05 24
DS -1.95 -2.45 1.95 -2.45 0.1 24
DS 1.95 -2.45 1.95 2.45 0.1 24
DS 1.95 2.45 -1.95 2.45 0.1 24
DS -1.95 2.45 -1.95 -2.45 0.1 24
DS -1.95 -1.18 -0.68 -2.45 0.1 24
DS -1.6 -2.45 1.6 -2.45 0.2 21
DS 1.6 -2.45 1.6 2.45 0.2 21
DS 1.6 2.45 -1.6 2.45 0.2 21
DS -1.6 2.45 -1.6 -2.45 0.2 21
DS -3.475 -2.605 -1.95 -2.605 0.2 21
$PAD
Po -2.712 -1.905
Sh "1" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -2.712 -0.635
Sh "2" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -2.712 0.635
Sh "3" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -2.712 1.905
Sh "4" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 2.712 1.905
Sh "5" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 2.712 0.635
Sh "6" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 2.712 -0.635
Sh "7" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 2.712 -1.905
Sh "8" R 0.7 1.525 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$EndMODULE SOIC127P600X175-8N
$EndLIBRARY
@@ -0,0 +1,45 @@
(module "LSM6DSV16BXTR" (layer F.Cu)
(descr "LGA-14L 2.5 x 3.0 x 0.74 mm XDM)")
(tags "Integrated Circuit")
(attr smd)
(fp_text reference IC** (at 0.000 -0) (layer F.SilkS)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text user %R (at 0.000 -0) (layer F.Fab)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text value "LSM6DSV16BXTR" (at 0.000 -0) (layer F.SilkS) hide
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_line (start -1.5 -1.25) (end 1.5 -1.25) (layer F.Fab) (width 0.1))
(fp_line (start 1.5 -1.25) (end 1.5 1.25) (layer F.Fab) (width 0.1))
(fp_line (start 1.5 1.25) (end -1.5 1.25) (layer F.Fab) (width 0.1))
(fp_line (start -1.5 1.25) (end -1.5 -1.25) (layer F.Fab) (width 0.1))
(fp_line (start -2.3 -2.05) (end 2.3 -2.05) (layer F.CrtYd) (width 0.1))
(fp_line (start 2.3 -2.05) (end 2.3 2.05) (layer F.CrtYd) (width 0.1))
(fp_line (start 2.3 2.05) (end -2.3 2.05) (layer F.CrtYd) (width 0.1))
(fp_line (start -2.3 2.05) (end -2.3 -2.05) (layer F.CrtYd) (width 0.1))
(fp_line (start -1.9 -0.75) (end -1.9 -0.75) (layer F.SilkS) (width 0.1))
(fp_line (start -2 -0.75) (end -2 -0.75) (layer F.SilkS) (width 0.1))
(fp_arc (start -1.95 -0.75) (end -1.900 -0.75) (angle -180) (layer F.SilkS) (width 0.1))
(fp_arc (start -1.95 -0.75) (end -2.000 -0.75) (angle -180) (layer F.SilkS) (width 0.1))
(pad 1 smd rect (at -1.150 -0.75 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 2 smd rect (at -1.150 -0.25 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 3 smd rect (at -1.150 0.25 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 4 smd rect (at -1.150 0.75 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 5 smd rect (at -0.500 0.9 0) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 6 smd rect (at 0.000 0.9 0) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 7 smd rect (at 0.500 0.9 0) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 8 smd rect (at 1.150 0.75 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 9 smd rect (at 1.150 0.25 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 10 smd rect (at 1.150 -0.25 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 11 smd rect (at 1.150 -0.75 90) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 12 smd rect (at 0.500 -0.9 0) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 13 smd rect (at 0.000 -0.9 0) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(pad 14 smd rect (at -0.500 -0.9 0) (size 0.300 0.600) (layers F.Cu F.Paste F.Mask))
(model LSM6DSV80XTR.stp
(at (xyz 0 0 0))
(scale (xyz 1 1 1))
(rotate (xyz 0 0 0))
)
)
@@ -0,0 +1,9 @@
EESchema-DOCLIB Version 2.0
#
$CMP LSM6DSV80XTR
D 6-axis IMU (inertial measurement unit) with high-g accelerometer, embedded AI, and sensor fusion for wearables and sport trackers
K
F https://www.st.com/resource/en/datasheet/lsm6dsv80x.pdf
$ENDCMP
#
#End Doc Library
@@ -0,0 +1,102 @@
(kicad_symbol_lib (version 20211014) (generator SamacSys_ECAD_Model)
(symbol "LSM6DSV80XTR" (in_bom yes) (on_board yes)
(property "Reference" "IC" (at 31.75 15.24 0)
(effects (font (size 1.27 1.27)) (justify left top))
)
(property "Value" "LSM6DSV80XTR" (at 31.75 12.7 0)
(effects (font (size 1.27 1.27)) (justify left top))
)
(property "Footprint" "LSM6DSV16BXTR" (at 31.75 -87.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Datasheet" "https://www.st.com/resource/en/datasheet/lsm6dsv80x.pdf" (at 31.75 -187.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "ki_description" "6-axis IMU (inertial measurement unit) with high-g accelerometer, embedded AI, and sensor fusion for wearables and sport trackers" (at 31.75 -287.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Height" "0.74" (at 31.75 -387.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Manufacturer_Name" "STMicroelectronics" (at 31.75 -487.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Manufacturer_Part_Number" "LSM6DSV80XTR" (at 31.75 -587.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Mouser Part Number" "" (at 31.75 -687.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Mouser Price/Stock" "" (at 31.75 -787.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Arrow Part Number" "LSM6DSV80XTR" (at 31.75 -887.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Arrow Price/Stock" "https://www.arrow.com/en/products/lsm6dsv80xtr/stmicroelectronics?utm_currency=USD&region=nac" (at 31.75 -987.3 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(rectangle
(start 5.08 10.16)
(end 30.48 -20.32)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin passive line (at 0 0 0) (length 5.08)
(name "SDO/SA0" (effects (font (size 1.27 1.27))))
(number "1" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 0 -2.54 0) (length 5.08)
(name "SDX" (effects (font (size 1.27 1.27))))
(number "2" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 0 -5.08 0) (length 5.08)
(name "SCX" (effects (font (size 1.27 1.27))))
(number "3" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 0 -7.62 0) (length 5.08)
(name "INT1" (effects (font (size 1.27 1.27))))
(number "4" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 15.24 -25.4 90) (length 5.08)
(name "VDDIO" (effects (font (size 1.27 1.27))))
(number "5" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 17.78 -25.4 90) (length 5.08)
(name "GND_1" (effects (font (size 1.27 1.27))))
(number "6" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 20.32 -25.4 90) (length 5.08)
(name "GND_2" (effects (font (size 1.27 1.27))))
(number "7" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 35.56 -7.62 180) (length 5.08)
(name "VDD" (effects (font (size 1.27 1.27))))
(number "8" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 35.56 -5.08 180) (length 5.08)
(name "INT2" (effects (font (size 1.27 1.27))))
(number "9" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 35.56 -2.54 180) (length 5.08)
(name "NC_1" (effects (font (size 1.27 1.27))))
(number "10" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 35.56 0 180) (length 5.08)
(name "NC_2" (effects (font (size 1.27 1.27))))
(number "11" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 20.32 15.24 270) (length 5.08)
(name "CS" (effects (font (size 1.27 1.27))))
(number "12" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 17.78 15.24 270) (length 5.08)
(name "SCL" (effects (font (size 1.27 1.27))))
(number "13" (effects (font (size 1.27 1.27))))
)
(pin passive line (at 15.24 15.24 270) (length 5.08)
(name "SDA" (effects (font (size 1.27 1.27))))
(number "14" (effects (font (size 1.27 1.27))))
)
)
)
@@ -0,0 +1,37 @@
EESchema-LIBRARY Version 2.3
#encoding utf-8
#SamacSys ECAD Model LSM6DSV80XTR
#/20519981/1957227/2.50/14/2/Integrated Circuit
DEF LSM6DSV80XTR IC 0 30 Y Y 1 F N
F0 "IC" 1250 600 50 H V L CNN
F1 "LSM6DSV80XTR" 1250 500 50 H V L CNN
F2 "LSM6DSV16BXTR" 1250 400 50 H I L CNN
F3 "https://www.st.com/resource/en/datasheet/lsm6dsv80x.pdf" 1250 300 50 H I L CNN
F4 "6-axis IMU (inertial measurement unit) with high-g accelerometer, embedded AI, and sensor fusion for wearables and sport trackers" 1250 200 50 H I L CNN "Description"
F5 "0.74" 1250 100 50 H I L CNN "Height"
F6 "STMicroelectronics" 1250 0 50 H I L CNN "Manufacturer_Name"
F7 "LSM6DSV80XTR" 1250 -100 50 H I L CNN "Manufacturer_Part_Number"
F8 "" 1250 -200 50 H I L CNN "Mouser Part Number"
F9 "" 1250 -300 50 H I L CNN "Mouser Price/Stock"
F10 "LSM6DSV80XTR" 1250 -400 50 H I L CNN "Arrow Part Number"
F11 "https://www.arrow.com/en/products/lsm6dsv80xtr/stmicroelectronics?utm_currency=USD&region=nac" 1250 -500 50 H I L CNN "Arrow Price/Stock"
DRAW
X SDO/SA0 1 0 0 200 R 50 50 0 0 P
X SDX 2 0 -100 200 R 50 50 0 0 P
X SCX 3 0 -200 200 R 50 50 0 0 P
X INT1 4 0 -300 200 R 50 50 0 0 P
X VDDIO 5 600 -1000 200 U 50 50 0 0 P
X GND_1 6 700 -1000 200 U 50 50 0 0 P
X GND_2 7 800 -1000 200 U 50 50 0 0 P
X VDD 8 1400 -300 200 L 50 50 0 0 P
X INT2 9 1400 -200 200 L 50 50 0 0 P
X NC_1 10 1400 -100 200 L 50 50 0 0 P
X NC_2 11 1400 0 200 L 50 50 0 0 P
X CS 12 800 600 200 D 50 50 0 0 P
X SCL 13 700 600 200 D 50 50 0 0 P
X SDA 14 600 600 200 D 50 50 0 0 P
P 5 0 1 6 200 400 1200 400 1200 -800 200 -800 200 400 N
ENDDRAW
ENDDEF
#
#End Library
@@ -0,0 +1,115 @@
PCBNEW-LibModule-V1 2026-03-08 11:22:49
# encoding utf-8
Units mm
$INDEX
LSM6DSV16BXTR
$EndINDEX
$MODULE LSM6DSV16BXTR
Po 0 0 0 15 69ad5c09 00000000 ~~
Li LSM6DSV16BXTR
Cd LGA-14L 2.5 x 3.0 x 0.74 mm XDM)
Kw Integrated Circuit
Sc 0
At SMD
AR
Op 0 0 0
T0 0.000 -0 1.27 1.27 0 0.254 N V 21 N "IC**"
T1 0.000 -0 1.27 1.27 0 0.254 N I 21 N "LSM6DSV16BXTR"
DS -1.5 -1.25 1.5 -1.25 0.1 24
DS 1.5 -1.25 1.5 1.25 0.1 24
DS 1.5 1.25 -1.5 1.25 0.1 24
DS -1.5 1.25 -1.5 -1.25 0.1 24
DS -2.3 -2.05 2.3 -2.05 0.1 24
DS 2.3 -2.05 2.3 2.05 0.1 24
DS 2.3 2.05 -2.3 2.05 0.1 24
DS -2.3 2.05 -2.3 -2.05 0.1 24
DS -1.9 -0.75 -1.9 -0.75 0.1 21
DS -2 -0.75 -2 -0.75 0.1 21
DA -1.95 -0.75 -1.900 -0.75 -1800 0.1 21
DA -1.95 -0.75 -2.000 -0.75 -1800 0.1 21
$PAD
Po -1.150 -0.75
Sh "1" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -1.150 -0.25
Sh "2" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -1.150 0.25
Sh "3" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -1.150 0.75
Sh "4" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -0.500 0.9
Sh "5" R 0.300 0.600 0 0 0
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 0.000 0.9
Sh "6" R 0.300 0.600 0 0 0
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 0.500 0.9
Sh "7" R 0.300 0.600 0 0 0
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 1.150 0.75
Sh "8" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 1.150 0.25
Sh "9" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 1.150 -0.25
Sh "10" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 1.150 -0.75
Sh "11" R 0.300 0.600 0 0 900
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 0.500 -0.9
Sh "12" R 0.300 0.600 0 0 0
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po 0.000 -0.9
Sh "13" R 0.300 0.600 0 0 0
At SMD N 00888000
Ne 0 ""
$EndPAD
$PAD
Po -0.500 -0.9
Sh "14" R 0.300 0.600 0 0 0
At SMD N 00888000
Ne 0 ""
$EndPAD
$EndMODULE LSM6DSV16BXTR
$EndLIBRARY
+9
View File
@@ -0,0 +1,9 @@
EESchema-DOCLIB Version 2.0
#
$CMP W3213
D Antennas GPS patch antenna 13x13 mm
K
F https://www.arrow.com/en/products/w3213/pulse-electronics-corporation
$ENDCMP
#
#End Doc Library
@@ -0,0 +1,31 @@
(module "W3213" (layer F.Cu)
(descr "W3213-1")
(tags "Antenna")
(fp_text reference ANT** (at 0.000 -0.7) (layer F.SilkS)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text user %R (at 0.000 -0.7) (layer F.Fab)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text value "W3213" (at 0.000 -0.7) (layer F.SilkS) hide
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_line (start -6.5 -7.2) (end 6.5 -7.2) (layer F.Fab) (width 0.2))
(fp_line (start 6.5 -7.2) (end 6.5 5.8) (layer F.Fab) (width 0.2))
(fp_line (start 6.5 5.8) (end -6.5 5.8) (layer F.Fab) (width 0.2))
(fp_line (start -6.5 5.8) (end -6.5 -7.2) (layer F.Fab) (width 0.2))
(fp_line (start -6.5 -7.2) (end 6.5 -7.2) (layer F.SilkS) (width 0.1))
(fp_line (start 6.5 -7.2) (end 6.5 5.8) (layer F.SilkS) (width 0.1))
(fp_line (start 6.5 5.8) (end -6.5 5.8) (layer F.SilkS) (width 0.1))
(fp_line (start -6.5 5.8) (end -6.5 -7.2) (layer F.SilkS) (width 0.1))
(fp_line (start -7.5 -8.2) (end 7.5 -8.2) (layer F.CrtYd) (width 0.1))
(fp_line (start 7.5 -8.2) (end 7.5 6.8) (layer F.CrtYd) (width 0.1))
(fp_line (start 7.5 6.8) (end -7.5 6.8) (layer F.CrtYd) (width 0.1))
(fp_line (start -7.5 6.8) (end -7.5 -8.2) (layer F.CrtYd) (width 0.1))
(pad 1 thru_hole circle (at 0.000 -0) (size 1.575 1.575) (drill 1.05) (layers *.Cu *.Mask))
(model W3213.stp
(at (xyz 0 0 0))
(scale (xyz 1 1 1))
(rotate (xyz 0 0 0))
)
)
@@ -0,0 +1,50 @@
(kicad_symbol_lib (version 20211014) (generator SamacSys_ECAD_Model)
(symbol "W3213" (in_bom yes) (on_board yes)
(property "Reference" "ANT" (at 16.51 7.62 0)
(effects (font (size 1.27 1.27)) (justify left top))
)
(property "Value" "W3213" (at 16.51 5.08 0)
(effects (font (size 1.27 1.27)) (justify left top))
)
(property "Footprint" "W3213" (at 16.51 -94.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Datasheet" "https://www.arrow.com/en/products/w3213/pulse-electronics-corporation" (at 16.51 -194.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "ki_description" "Antennas GPS patch antenna 13x13 mm" (at 16.51 -294.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Height" "4.83" (at 16.51 -394.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Manufacturer_Name" "Yageo Group" (at 16.51 -494.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Manufacturer_Part_Number" "W3213" (at 16.51 -594.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Mouser Part Number" "673-W3213" (at 16.51 -694.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Mouser Price/Stock" "https://www.mouser.co.uk/ProductDetail/Pulse-Electronics/W3213?qs=tNd8r9m29y7JOJNw2gSjhQ%3D%3D" (at 16.51 -794.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Arrow Part Number" "W3213" (at 16.51 -894.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(property "Arrow Price/Stock" "https://www.arrow.com/en/products/w3213/pulse-electronics-corporation" (at 16.51 -994.92 0)
(effects (font (size 1.27 1.27)) (justify left top) hide)
)
(rectangle
(start 5.08 2.54)
(end 15.24 -2.54)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin passive line (at 0 0 0) (length 5.08)
(name "1" (effects (font (size 1.27 1.27))))
(number "1" (effects (font (size 1.27 1.27))))
)
)
)
+24
View File
@@ -0,0 +1,24 @@
EESchema-LIBRARY Version 2.3
#encoding utf-8
#SamacSys ECAD Model W3213
#/1938663/1957227/2.50/1/0/Antenna
DEF W3213 ANT 0 30 Y Y 1 F N
F0 "ANT" 650 300 50 H V L CNN
F1 "W3213" 650 200 50 H V L CNN
F2 "W3213" 650 100 50 H I L CNN
F3 "https://www.arrow.com/en/products/w3213/pulse-electronics-corporation" 650 0 50 H I L CNN
F4 "Antennas GPS patch antenna 13x13 mm" 650 -100 50 H I L CNN "Description"
F5 "4.83" 650 -200 50 H I L CNN "Height"
F6 "Yageo Group" 650 -300 50 H I L CNN "Manufacturer_Name"
F7 "W3213" 650 -400 50 H I L CNN "Manufacturer_Part_Number"
F8 "673-W3213" 650 -500 50 H I L CNN "Mouser Part Number"
F9 "https://www.mouser.co.uk/ProductDetail/Pulse-Electronics/W3213?qs=tNd8r9m29y7JOJNw2gSjhQ%3D%3D" 650 -600 50 H I L CNN "Mouser Price/Stock"
F10 "W3213" 650 -700 50 H I L CNN "Arrow Part Number"
F11 "https://www.arrow.com/en/products/w3213/pulse-electronics-corporation" 650 -800 50 H I L CNN "Arrow Price/Stock"
DRAW
X 1 1 0 0 200 R 50 50 0 0 P
P 5 0 1 6 200 100 600 100 600 -100 200 -100 200 100 N
ENDDRAW
ENDDEF
#
#End Library
+38
View File
@@ -0,0 +1,38 @@
PCBNEW-LibModule-V1 2026-03-07 19:18:26
# encoding utf-8
Units mm
$INDEX
W3213
$EndINDEX
$MODULE W3213
Po 0 0 0 15 69ac7a02 00000000 ~~
Li W3213
Cd W3213-1
Kw Antenna
Sc 0
At STD
AR
Op 0 0 0
T0 0.000 -0.7 1.27 1.27 0 0.254 N V 21 N "ANT**"
T1 0.000 -0.7 1.27 1.27 0 0.254 N I 21 N "W3213"
DS -6.5 -7.2 6.5 -7.2 0.2 24
DS 6.5 -7.2 6.5 5.8 0.2 24
DS 6.5 5.8 -6.5 5.8 0.2 24
DS -6.5 5.8 -6.5 -7.2 0.2 24
DS -6.5 -7.2 6.5 -7.2 0.1 21
DS 6.5 -7.2 6.5 5.8 0.1 21
DS 6.5 5.8 -6.5 5.8 0.1 21
DS -6.5 5.8 -6.5 -7.2 0.1 21
DS -7.5 -8.2 7.5 -8.2 0.1 24
DS 7.5 -8.2 7.5 6.8 0.1 24
DS 7.5 6.8 -7.5 6.8 0.1 24
DS -7.5 6.8 -7.5 -8.2 0.1 24
$PAD
Po 0.000 -0
Sh "1" C 1.575 1.575 0 0 900
Dr 1.05 0 0
At STD N 00E0FFFF
Ne 0 ""
$EndPAD
$EndMODULE W3213
$EndLIBRARY
+16
View File
@@ -0,0 +1,16 @@
[x] double-check resistors and capacitors - can I do with fewer unique values?
[x] double check sd card keep-out areas with datasheet
[x] swap sda/scl i2c test points
[x] Do we even need any pullup/down resistors?? can't the esp32 do this internally? (update: it can)
[x] i2c Pins need reassigned so they match what is on the dev boards, which use 17/18 for the OLED display. Or we somehow also squeeze an OLED into the dev boards. Or maybe replace the OLED entirely with just some LEDs to indicate sensor state?
R2:
[ ] some kind of power consumption measuring chip? INA233?
[ ] protection diodes on usb lines?
[ ] pull-down resistor before LED strip?
[ ] CS line on SD card
R3:
[ ] MP2662 lipoly charger chip on a 3.7v battery at 2500mAh
[ ] redesign power supply to use a buck booster from the 3.7v battery to 5v for the display, and a simple divider directly off the battery for 3.3v for system power
[ ] soldering ports to accept 3W at 6VAC to charge the battery from the bike dynamo
+13060
View File
File diff suppressed because it is too large Load Diff
+694
View File
@@ -0,0 +1,694 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.3,
"height": 0.6,
"width": 0.6
},
"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.15
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"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.0,
"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.25,
"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.3,
"min_track_width": 0.0,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
}
],
"zones_allow_external_fillets": false
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "board.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "/tmp/",
"pos_files": "",
"specctra_dsn": "",
"step": "board.stl",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": false
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": false
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
},
{
"group_by": false,
"label": "Arrow Price/Stock",
"name": "Arrow Price/Stock",
"show": false
},
{
"group_by": false,
"label": "Height",
"name": "Height",
"show": false
},
{
"group_by": false,
"label": "Manufacturer_Name",
"name": "Manufacturer_Name",
"show": false
},
{
"group_by": false,
"label": "Manufacturer_Part_Number",
"name": "Manufacturer_Part_Number",
"show": false
},
{
"group_by": false,
"label": "Mouser Part Number",
"name": "Mouser Part Number",
"show": true
},
{
"group_by": false,
"label": "Mouser Price/Stock",
"name": "Mouser Price/Stock",
"show": true
},
{
"group_by": false,
"label": "Arrow Part Number",
"name": "Arrow Part Number",
"show": false
},
{
"group_by": false,
"label": "Description",
"name": "Description",
"show": false
},
{
"group_by": false,
"label": "#",
"name": "${ITEM_NUMBER}",
"show": false
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "",
"sort_asc": true,
"sort_field": "Reference"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"ngspice": {
"fix_include_paths": true,
"meta": {
"version": 0
},
"model_mode": 4,
"workbook_filename": ""
},
"page_layout_descr_file": "",
"plot_directory": "",
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [
[
"51d2b720-4c26-4e02-a385-8df43b74d31d",
"Root"
]
],
"text_variables": {}
}
+12093
View File
File diff suppressed because it is too large Load Diff
+4709
View File
File diff suppressed because one or more lines are too long
+720
View File
@@ -0,0 +1,720 @@
(kicad_symbol_lib
(version 20241209)
(generator "kicad_symbol_editor")
(generator_version "9.0")
(symbol "HUSB238"
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(property "Reference" "U"
(at 2.54 -6.35 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(symbol "HUSB238_0_1"
(rectangle
(start -5.08 2.54)
(end 10.16 -15.24)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
)
(symbol "HUSB238_1_1"
(pin input line
(at -7.62 0 0)
(length 2.54)
(name "VIN"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at -7.62 -5.08 0)
(length 2.54)
(name "D+"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at -7.62 -7.62 0)
(length 2.54)
(name "D-"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at -7.62 -11.43 0)
(length 2.54)
(name "CC1"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at -7.62 -13.97 0)
(length 2.54)
(name "CC2"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 2.54 -17.78 90)
(length 2.54)
(name "GND"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 12.7 0 180)
(length 2.54)
(name "GATE"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 12.7 -5.08 180)
(length 2.54)
(name "SCL"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 12.7 -7.62 180)
(length 2.54)
(name "SDA"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 12.7 -11.43 180)
(length 2.54)
(name "VSET"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 12.7 -13.97 180)
(length 2.54)
(name "ISET"
(effects
(font
(size 1.27 1.27)
)
)
)
(number ""
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "LED_STRIP"
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(property "Reference" "SOCKET"
(at -10.414 -10.414 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Value" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(symbol "LED_STRIP_0_1"
(rectangle
(start -1.27 -1.27)
(end -13.97 -8.89)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
)
(symbol "LED_STRIP_1_1"
(pin power_in line
(at 0 -2.54 180)
(length 2.54)
(name "LED_5V"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 0 -5.08 180)
(length 2.54)
(name "LED_DATA"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 0 -7.62 180)
(length 2.54)
(name "LED_GND"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "3"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "PA1010D"
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(property "Reference" "U"
(at 10.16 -3.048 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(symbol "PA1010D_0_1"
(rectangle
(start -1.27 8.89)
(end 21.59 -10.16)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
)
(symbol "PA1010D_1_1"
(text "PA1010D"
(at 10.16 0 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(pin bidirectional line
(at -3.81 6.35 0)
(length 2.54)
(name "I2C_SDA"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at -3.81 3.81 0)
(length 2.54)
(name "I2C_SCK"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin output line
(at -3.81 -2.54 0)
(length 2.54)
(name "1PPS"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "3"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin output line
(at -3.81 -5.08 0)
(length 2.54)
(name "TX"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "4"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at -3.81 -7.62 0)
(length 2.54)
(name "RX"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "5"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_in line
(at 24.13 6.35 180)
(length 2.54)
(name "VCC"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "10"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_in line
(at 24.13 3.81 180)
(length 2.54)
(name "VBACKUP"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "9"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 24.13 -2.54 180)
(length 2.54)
(name "WAKE-UP"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "8"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 24.13 -5.08 180)
(length 2.54)
(name "GND"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "7"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin input line
(at 24.13 -7.62 180)
(length 2.54)
(name "NRESET"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "6"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "W3213"
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(property "Reference" "U"
(at 3.048 0.762 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(symbol "W3213_0_1"
(rectangle
(start 0 0)
(end 6.35 -2.54)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
)
(symbol "W3213_1_1"
(pin bidirectional line
(at -2.54 -1.27 0)
(length 2.54)
(name "FEED"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
)
+108
View File
@@ -0,0 +1,108 @@
(footprint "3_PIN_WIRE"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at 2.5 -4 0)
(unlocked yes)
(layer "F.SilkS")
(uuid "ed9aed21-178b-495d-b9e1-929f192a36e4")
(effects
(font
(size 1 1)
(thickness 0.1)
)
)
)
(property "Value" "3_PIN_WIRE"
(at 2.5 -2.5 0)
(unlocked yes)
(layer "F.Fab")
(uuid "a81b8f00-4fb7-411f-ad2b-943cd5e16966")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "874c5579-03b1-4f97-8e19-7e491326f2e9")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "ac906119-cf33-42dd-9c1a-5d2fb67ab974")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(attr smd)
(fp_rect
(start -1.5 -1.5)
(end 6.5 1.5)
(stroke
(width 0.1)
(type solid)
)
(fill no)
(layer "F.SilkS")
(uuid "3c534895-8b86-4446-afd7-676a7a56e959")
)
(fp_text user "${REFERENCE}"
(at 2.5 3 0)
(unlocked yes)
(layer "F.Fab")
(uuid "0f18bd78-61ca-451a-9eeb-2263cb5f43f2")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "1" thru_hole circle
(at 0 0)
(size 1.75 1.75)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(solder_mask_margin 0)
(uuid "ebc9015d-9cc2-4111-aef3-b60d76b89373")
)
(pad "2" thru_hole circle
(at 2.5 0)
(size 1.75 1.75)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(solder_mask_margin 0)
(uuid "173044b5-a2c0-4c4f-bb3f-f9fd466a356c")
)
(pad "3" thru_hole circle
(at 5 0)
(size 1.75 1.75)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(solder_mask_margin 0)
(uuid "f499520f-f554-4d82-83ea-fa2da2309ab8")
)
(embedded_fonts no)
)
+173
View File
@@ -0,0 +1,173 @@
(footprint "PA1010D"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at 5.5 -1 0)
(unlocked yes)
(layer "F.SilkS")
(uuid "130f3771-d8e2-4352-9988-833559a1de18")
(effects
(font
(size 1 1)
(thickness 0.1)
)
)
)
(property "Value" "PA1010D"
(at 5 8.5 0)
(unlocked yes)
(layer "F.Fab")
(uuid "41b10efd-006f-40fa-bbac-f8ded5a8fb94")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "b21a5dda-2f72-4e25-aec7-67c44f8948ee")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "cc6957af-7fef-407d-948e-2e72ed2fa122")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(attr smd)
(fp_rect
(start 0 0)
(end 10 10)
(stroke
(width 0.1)
(type default)
)
(fill no)
(layer "F.SilkS")
(uuid "d57ec54c-ee0b-47c8-a7f8-dc75c8a9b37a")
)
(fp_circle
(center 1.5 1)
(end 1.5 1.5)
(stroke
(width 0.1)
(type solid)
)
(fill yes)
(layer "F.SilkS")
(uuid "edc44674-6cfd-4316-bad3-efc6a576b3ca")
)
(fp_circle
(center 5 5)
(end 3.5 5)
(stroke
(width 0.1)
(type solid)
)
(fill yes)
(layer "Edge.Cuts")
(uuid "c34f8951-a402-414f-8c4a-f636b56f109c")
)
(fp_text user "${REFERENCE}"
(at 5 2 0)
(unlocked yes)
(layer "F.Fab")
(uuid "722d4a1d-7d11-4a0d-8121-59126244e839")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "1" smd roundrect
(at 0 1.15 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "dd90346c-26e8-425a-9a22-9aa95ea9966c")
)
(pad "2" smd roundrect
(at 0 2.75 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "3ed9afe3-fc3d-4c35-8d7a-cc9f3faca37b")
)
(pad "3" smd roundrect
(at 0 5.65 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "baf6929d-7350-437e-b05a-f2d9bce91360")
)
(pad "4" smd roundrect
(at 0 7.25 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "6ec8d1d5-e58f-4b90-9380-ff6ca42c02dd")
)
(pad "5" smd roundrect
(at 0 8.85 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "6f769a41-6d17-4d4a-8d0a-1c53033bec90")
)
(pad "6" smd roundrect
(at 10 8.85 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "8377c02f-ce7c-4edb-af17-6113f49103f3")
)
(pad "7" smd roundrect
(at 10 7.25 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "1b5dcfd8-2351-4f21-91e5-80eb16f68d64")
)
(pad "8" smd roundrect
(at 10 5.65 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "af994bfc-2691-4dd3-a5a6-1b7939aef85f")
)
(pad "9" smd roundrect
(at 10 2.75 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "de6b9b35-3982-456a-8dad-8c11afd9d2a6")
)
(pad "10" smd roundrect
(at 10 1.15 90)
(size 0.8 1.5)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15)
(uuid "ed9d7d62-f653-4f37-8d08-2969d1579eea")
)
(embedded_fonts no)
)
@@ -0,0 +1,41 @@
(module "SOIC127P600X175-8N" (layer F.Cu)
(descr "SO 8L")
(tags "Integrated Circuit")
(attr smd)
(fp_text reference IC** (at 0 0) (layer F.SilkS)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text user %R (at 0 0) (layer F.Fab)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text value "SOIC127P600X175-8N" (at 0 0) (layer F.SilkS) hide
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_line (start -3.725 -2.75) (end 3.725 -2.75) (layer F.CrtYd) (width 0.05))
(fp_line (start 3.725 -2.75) (end 3.725 2.75) (layer F.CrtYd) (width 0.05))
(fp_line (start 3.725 2.75) (end -3.725 2.75) (layer F.CrtYd) (width 0.05))
(fp_line (start -3.725 2.75) (end -3.725 -2.75) (layer F.CrtYd) (width 0.05))
(fp_line (start -1.95 -2.45) (end 1.95 -2.45) (layer F.Fab) (width 0.1))
(fp_line (start 1.95 -2.45) (end 1.95 2.45) (layer F.Fab) (width 0.1))
(fp_line (start 1.95 2.45) (end -1.95 2.45) (layer F.Fab) (width 0.1))
(fp_line (start -1.95 2.45) (end -1.95 -2.45) (layer F.Fab) (width 0.1))
(fp_line (start -1.95 -1.18) (end -0.68 -2.45) (layer F.Fab) (width 0.1))
(fp_line (start -1.6 -2.45) (end 1.6 -2.45) (layer F.SilkS) (width 0.2))
(fp_line (start 1.6 -2.45) (end 1.6 2.45) (layer F.SilkS) (width 0.2))
(fp_line (start 1.6 2.45) (end -1.6 2.45) (layer F.SilkS) (width 0.2))
(fp_line (start -1.6 2.45) (end -1.6 -2.45) (layer F.SilkS) (width 0.2))
(fp_line (start -3.475 -2.605) (end -1.95 -2.605) (layer F.SilkS) (width 0.2))
(pad 1 smd rect (at -2.712 -1.905 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(pad 2 smd rect (at -2.712 -0.635 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(pad 3 smd rect (at -2.712 0.635 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(pad 4 smd rect (at -2.712 1.905 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(pad 5 smd rect (at 2.712 1.905 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(pad 6 smd rect (at 2.712 0.635 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(pad 7 smd rect (at 2.712 -0.635 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(pad 8 smd rect (at 2.712 -1.905 90) (size 0.7 1.525) (layers F.Cu F.Paste F.Mask))
(model L6982N50DR.stp
(at (xyz 0 0 0))
(scale (xyz 1 1 1))
(rotate (xyz 0 0 0))
)
)
+138
View File
@@ -0,0 +1,138 @@
(footprint "W3213"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at 6.5 -10 0)
(unlocked yes)
(layer "F.SilkS")
(uuid "f24b340a-fab5-4a19-89d5-142c860a7fc0")
(effects
(font
(size 1 1)
(thickness 0.1)
)
)
)
(property "Value" "W3213"
(at 6.5 -2.5 0)
(unlocked yes)
(layer "F.Fab")
(uuid "04dd6c12-8251-4781-a9f5-1b11f920ff3a")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "b99134c4-fb24-4473-86ca-b34478a5cd2d")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "cd3e3cb1-5955-400e-9a61-613d452727a7")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(attr smd)
(fp_rect
(start 0 -13)
(end 13 0)
(stroke
(width 0.1)
(type default)
)
(fill no)
(layer "F.SilkS")
(uuid "c78c6db4-7e41-4ecc-83d0-30df1c12f03d")
)
(fp_rect
(start -1 -14)
(end 14 1)
(stroke
(width 0.1)
(type solid)
)
(fill no)
(layer "F.CrtYd")
(uuid "4539cb55-422e-48bf-a735-c6db2ea37043")
)
(fp_text user "${REFERENCE}"
(at 6.5 -1 0)
(unlocked yes)
(layer "F.Fab")
(uuid "4e027ed7-f943-4ac4-b64e-958cf24df4e4")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "1" thru_hole circle
(at 6.5 -5.8)
(size 3.6 3.6)
(drill 2.5)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "e16d3e96-37cd-42db-8a30-66cca71459e4")
)
(zone
(net 0)
(net_name "")
(layers "F.Cu" "B.Cu" "In1.Cu" "In2.Cu" "In3.Cu" "In4.Cu" "In5.Cu" "In6.Cu"
"In7.Cu" "In8.Cu" "In9.Cu" "In10.Cu" "In11.Cu" "In12.Cu" "In13.Cu" "In14.Cu"
"In15.Cu" "In16.Cu" "In17.Cu" "In18.Cu" "In19.Cu" "In20.Cu" "In21.Cu"
"In22.Cu" "In23.Cu" "In24.Cu" "In25.Cu" "In26.Cu" "In27.Cu" "In28.Cu"
"In29.Cu" "In30.Cu"
)
(uuid "4534681b-5cec-434f-b51a-fe95ff081411")
(name "KEEP-OUT")
(hatch edge 0.5)
(connect_pads
(clearance 0)
)
(min_thickness 0.25)
(filled_areas_thickness no)
(keepout
(tracks allowed)
(vias not_allowed)
(pads not_allowed)
(copperpour not_allowed)
(footprints not_allowed)
)
(placement
(enabled no)
(sheetname "")
)
(fill
(thermal_gap 0.5)
(thermal_bridge_width 0.5)
)
(polygon
(pts
(xy 0 0.141405) (xy 0 -12.858595) (xy 13 -12.858595) (xy 13 0.141405)
)
)
)
(embedded_fonts no)
)
+6
View File
@@ -0,0 +1,6 @@
(fp_lib_table
(version 7)
(lib (name "chips")(type "KiCad")(uri "${KIPRJMOD}/chips.pretty")(options "")(descr ""))
(lib (name "KiCad")(type "KiCad")(uri "${KIPRJMOD}/LIB_W3213/W3213/KiCad")(options "")(descr ""))
(lib (name "LSM6DSV80XTR")(type "KiCad")(uri "${KIPRJMOD}/LIB_LSM6DSV80XTR/LSM6DSV80XTR/KiCad")(options "")(descr ""))
)
+7
View File
@@ -0,0 +1,7 @@
(sym_lib_table
(version 7)
(lib (name "chips")(type "KiCad")(uri "${KIPRJMOD}/chips.kicad_sym")(options "")(descr ""))
(lib (name "L6982N50DR")(type "KiCad")(uri "${KIPRJMOD}/LIB_L6982N50DR/L6982N50DR/KiCad/L6982N50DR.kicad_sym")(options "")(descr ""))
(lib (name "W3213")(type "KiCad")(uri "${KIPRJMOD}/LIB_W3213/W3213/KiCad/W3213.kicad_sym")(options "")(descr ""))
(lib (name "LSM6DSV80XTR")(type "KiCad")(uri "${KIPRJMOD}/LIB_LSM6DSV80XTR/LSM6DSV80XTR/KiCad/LSM6DSV80XTR.kicad_sym")(options "")(descr ""))
)
+3 -2
View File
@@ -205,12 +205,13 @@ fn write_sim_data() {
let segments = [(StreamType::IMU, motion_output), (StreamType::GPS, gps_output), (StreamType::Annotations, annotation_output)]; let segments = [(StreamType::IMU, motion_output), (StreamType::GPS, gps_output), (StreamType::Annotations, annotation_output)];
// Write out the stream index header // Write out the stream index header
rmp::encode::write_array_len(&mut unified_fd, segments.len() as u32).unwrap(); StreamIndex { count: segments.len() }.write_rmp(&mut unified_fd).unwrap();
// Then the streams // Then the streams
for (stream_type, stream_path) in segments { for (stream_type, stream_path) in segments {
let mut fd = File::open(stream_path).unwrap(); let mut fd = File::open(stream_path).unwrap();
rmp::encode::write_ext_meta(&mut unified_fd, fd.metadata().unwrap().len() as u32, stream_type.into()).unwrap(); // FIXME: Replace this with the actual rmp types in simdata
StreamHeader { id: stream_type, size: fd.metadata().unwrap().len() as usize }.write_rmp(&mut unified_fd).unwrap();
let mut buf = Vec::new(); let mut buf = Vec::new();
fd.read_to_end(&mut buf).unwrap(); fd.read_to_end(&mut buf).unwrap();
unified_fd.write_all(buf.as_slice()).unwrap(); unified_fd.write_all(buf.as_slice()).unwrap();
+104 -61
View File
@@ -1,8 +1,7 @@
use embassy_time::{Duration, Instant, Timer}; use embassy_time::{Duration, Instant, Timer};
use esp_rtos::CurrentThreadHandle;
use figments::{liber8tion::interpolate::Fract8, surface::Surface}; use figments::{liber8tion::interpolate::Fract8, surface::Surface};
use figments_render::output::Brightness; use figments_render::output::Brightness;
use core::{fmt::Debug, mem::MaybeUninit, ops::{Deref, DerefMut}}; use core::{fmt::Debug, ops::{Deref, DerefMut}};
use log::*; use log::*;
use crate::graphics::display::DisplayControls; use crate::graphics::display::DisplayControls;
@@ -30,9 +29,9 @@ impl<S: Surface> AnimationActor<Fract8> for S {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct AnimDisplay<'a>(pub &'a mut DisplayControls); pub struct AnimDisplay<'a, 'b>(pub &'a mut DisplayControls<'b>);
impl<'a> AnimationActor<Fract8> for AnimDisplay<'a> { impl<'a> AnimationActor<Fract8> for AnimDisplay<'a, '_> {
fn get_value(&self) -> Fract8 { fn get_value(&self) -> Fract8 {
self.0.brightness() self.0.brightness()
} }
@@ -53,6 +52,10 @@ impl<S: Surface> AnimationActor<Fract8> for AnimatedSurface<S> {
} }
} }
trait Tickable {
fn tick(&mut self) -> TickResult;
}
struct Slot<'a, T> { struct Slot<'a, T> {
from: T, from: T,
to: T, to: T,
@@ -62,6 +65,38 @@ struct Slot<'a, T> {
target: &'a mut dyn AnimationActor<T> target: &'a mut dyn AnimationActor<T>
} }
enum TickResult {
Finished,
Continue
}
impl<'a, T> Slot<'a, T> {
fn is_valid(&self) -> bool {
self.step_time.as_ticks() != 0
}
}
impl<'a> Tickable for Slot<'a, Fract8> {
/// Advances the animation by one tick, and then returns whether or not the animation should continue or not
fn tick(&mut self) -> TickResult {
self.next_update += self.step_time;
self.cur_step = if self.to > self.from {
self.cur_step + Fract8::from_raw(1)
} else {
self.cur_step - Fract8::from_raw(1)
};
self.target.set_value(self.cur_step);
if (self.to > self.from && self.cur_step >= self.to) ||
(self.to <= self.from && self.cur_step <= self.to) {
TickResult::Finished
} else {
TickResult::Continue
}
}
}
impl<'a, T: Debug> core::fmt::Debug for Slot<'a, T> { impl<'a, T: Debug> core::fmt::Debug for Slot<'a, T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Slot") f.debug_struct("Slot")
@@ -73,7 +108,14 @@ impl<'a, T: Debug> core::fmt::Debug for Slot<'a, T> {
} }
} }
impl Animation<Fract8> { struct Animator<'a, T, const ACTOR_COUNT: usize> {
animators: [Slot<'a, T>; ACTOR_COUNT]
}
impl<'a, T, const ACTOR_COUNT: usize> Animator<'a, T, ACTOR_COUNT> {
}
impl<T: core::fmt::Debug> Animation<T> {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
from: None, from: None,
@@ -82,6 +124,53 @@ impl Animation<Fract8> {
} }
} }
async fn execute<'a, const ACTOR_COUNT: usize>(mut animators: [Slot<'a, T>; ACTOR_COUNT]) where Slot<'a, T>: Tickable {
let mut now: Instant = Instant::now();
loop {
// Find the next shortest delay
let mut next_keyframe_time = Instant::MAX;
let mut finished = false;
let mut has_valid = false;
for animator in &mut animators {
if !animator.is_valid() {
continue;
}
has_valid = true;
if animator.next_update <= now {
finished = match animator.tick() {
TickResult::Finished => true,
TickResult::Continue => finished
}
}
if next_keyframe_time <= now || animator.next_update < next_keyframe_time {
next_keyframe_time = animator.next_update;
}
}
// If there are no valid animators, or if all animators are finished, then we're done
finished |= !has_valid;
if finished {
break;
}
assert!(next_keyframe_time > now, "Weird times: {next_keyframe_time:?} is earlier than {now:?} animators={animators:?}");
let keyframe_delay = next_keyframe_time - now;
trace!("delay {:?}", keyframe_delay.as_millis());
Timer::after(keyframe_delay).await;
now += keyframe_delay;
}
}
}
impl Animation<Fract8> {
pub const fn duration(self, duration: Duration) -> Self {
Self {
duration,
..self
}
}
pub const fn from(self, from: Fract8) -> Self { pub const fn from(self, from: Fract8) -> Self {
Self { Self {
from: Some(from), from: Some(from),
@@ -95,23 +184,18 @@ impl Animation<Fract8> {
..self ..self
} }
} }
pub const fn duration(self, duration: Duration) -> Self {
Self {
duration,
..self
}
}
} }
const MIN_ANIMATION_RATE: Duration = Duration::from_millis(5);
impl Animation<Fract8> { impl Animation<Fract8> {
pub async fn apply<const ACTOR_COUNT: usize>(&self, actors: [&mut dyn AnimationActor<Fract8>; ACTOR_COUNT]) { pub async fn apply<const ACTOR_COUNT: usize>(&self, actors: [&mut dyn AnimationActor<Fract8>; ACTOR_COUNT]) {
let mut now = Instant::now(); let now = Instant::now();
trace!("start now={now:?} ACTOR_COUNT={ACTOR_COUNT}"); trace!("start now={now:?} ACTOR_COUNT={ACTOR_COUNT}");
let mut actors = actors.into_iter(); let mut actors = actors.into_iter();
let mut animators: [Slot<Fract8>; ACTOR_COUNT] = core::array::from_fn(|_| { let animators: [Slot<Fract8>; ACTOR_COUNT] = core::array::from_fn(|_| {
let target = actors.next().unwrap(); let target = actors.next().unwrap();
let from = if let Some(val) = self.from { let from = if let Some(val) = self.from {
val val
@@ -123,12 +207,14 @@ impl Animation<Fract8> {
} else { } else {
target.get_value() target.get_value()
}; };
let steps = to.abs_diff(from);
let step_time = if steps == Fract8::MIN { let step_time = if to == from {
// Zero ticks is an 'invalid' animator that shouldn't get processed because start == end already
Duration::from_ticks(0) Duration::from_ticks(0)
} else { } else {
(self.duration / steps.to_raw().into()).max(Duration::from_millis(1)) let steps = to.abs_diff(from);
// FIXME: if the resulting duration is less than the animation rate, we also need to re-scale the number added to animator.cur_step further down below. Otherwise a 0-255 animation with a 100ms duration actually ends up running for 255ms
(self.duration / steps.to_raw().into()).max(MIN_ANIMATION_RATE)
}; };
Slot { Slot {
from, from,
@@ -142,50 +228,7 @@ impl Animation<Fract8> {
trace!("animators={animators:?}"); trace!("animators={animators:?}");
loop { Self::execute(animators).await;
// Find the next shortest delay
let mut next_keyframe_time = animators[0].next_update;
let mut finished = false;
for animator in &mut animators {
if animator.step_time.as_ticks() == 0 {
continue;
}
if animator.next_update <= now {
animator.next_update += animator.step_time;
animator.cur_step = if animator.to > animator.from {
animator.cur_step + Fract8::from_raw(1)
} else {
animator.cur_step - Fract8::from_raw(1)
};
if (animator.to > animator.from && animator.cur_step >= animator.to) ||
(animator.to <= animator.from && animator.cur_step <= animator.to) {
finished = true;
}
/*if animator.cur_step == animator.from || animator.cur_step == animator.to {
finished = true;
}*/
animator.target.set_value(animator.cur_step);
}
if next_keyframe_time <= now || animator.next_update < next_keyframe_time {
next_keyframe_time = animator.next_update;
}
}
if finished {
break;
}
let keyframe_delay = next_keyframe_time - now;
trace!("delay {keyframe_delay:?}");
Timer::after(keyframe_delay).await;
now += keyframe_delay;
}
trace!("finished animators={animators:?}");
} }
} }
+242 -76
View File
@@ -7,34 +7,29 @@
)] )]
use core::{num::{self, Wrapping}, ptr::addr_of_mut};
use alloc::{string::String, sync::Arc}; use alloc::{string::String, sync::Arc};
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_time::{Instant, Timer}; use embassy_time::{Duration, Instant, Timer, WithTimeout};
use esp_hal::{gpio::{Output, OutputConfig, Pin}, time::Rate, xtensa_lx::debug_break}; use enum_map::EnumMap;
use esp_hal::{gpio::{Event, Input, InputConfig, Output, OutputConfig, Pin}, handler, time::Rate};
use esp_hal::{ use esp_hal::{
clock::CpuClock, system::{AppCoreGuard, CpuControl, Stack}, timer::{systimer::SystemTimer, timg::{TimerGroup, Wdt}} clock::CpuClock, timer::{systimer::SystemTimer, timg::{TimerGroup, Wdt}}
}; };
use embassy_sync::{ use embassy_sync::{
pubsub::PubSubChannel, blocking_mutex::raw::NoopRawMutex, channel::DynamicReceiver, once_lock::OnceLock, pubsub::PubSubChannel
blocking_mutex::raw::NoopRawMutex
}; };
use static_cell::ConstStaticCell; use esp_storage::FlashStorage;
use log::*; use log::*;
use renderbug_embassy::{events::Prediction, graphics::display::DisplayControls, logging::RenderbugLogger, tasks::{oled::{OledUI, OledUiSurfacePool, oled_ui}, safetyui::{SafetyUi, safety_ui_main}, ui::UiSurfacePool}}; use renderbug_bike::{events::{Prediction, SensorSource, SensorState}, gpio_interrupt::{InterruptDispatch, PinInterrupt}, graphics::display::DisplayControls, logging::RenderbugLogger, simdata::IMUReading, storage::{SharedFlash, SimDataRecorder, StorageRange}, tasks::{oled::{OledUI, OledUiSurfacePool, oled_ui}, safetyui::{SafetyUi, safety_ui_main}, ui::UiSurfacePool}, tracing::Tracer};
use renderbug_embassy::events::Measurement; use renderbug_bike::events::Measurement;
use static_cell::StaticCell; use static_cell::StaticCell;
use esp_backtrace as _; use esp_backtrace as _;
use esp_rtos::embassy::{Executor, InterruptExecutor}; use esp_hal::spi::master::any::Degrade;
use esp_hal::interrupt::software::SoftwareInterruptControl;
use renderbug_embassy::tasks::{ use renderbug_bike::tasks::ui::{Ui, ui_main};
motion::motion_task, use esp_hal::dma::DmaChannelConvert;
ui::{Ui, ui_main}
};
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pubsub::DynSubscriber}; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pubsub::DynSubscriber};
use embassy_sync::channel::Channel; use embassy_sync::channel::Channel;
@@ -48,27 +43,50 @@ esp_bootloader_esp_idf::esp_app_desc!();
#[cfg(feature="radio")] #[cfg(feature="radio")]
static WIFI_INIT: StaticCell<esp_radio::Controller<'static>> = StaticCell::new(); static WIFI_INIT: StaticCell<esp_radio::Controller<'static>> = StaticCell::new();
rtos_trace::global_trace!(Tracer);
static INTERRUPTS: OnceLock<InterruptDispatch<'static, 4>> = OnceLock::new();
#[handler]
fn gpio_interrupt_handler() {
INTERRUPTS.try_get().unwrap().process_interrupts();
}
static MOTION_BUS: ConstStaticCell<Channel<CriticalSectionRawMutex,Measurement,5> > = ConstStaticCell::new(Channel::new());
static RECORDING_BUS: ConstStaticCell<PubSubChannel<CriticalSectionRawMutex,Measurement,10, 4, 1> > = ConstStaticCell::new(PubSubChannel::new());
static PREDICTIONS: ConstStaticCell<PubSubChannel<NoopRawMutex, Prediction, 30, 7, 1>> = ConstStaticCell::new(PubSubChannel::new());
#[esp_rtos::main] #[esp_rtos::main]
async fn main(spawner: Spawner) { async fn main(spawner: Spawner) {
esp_alloc::heap_allocator!(size: 128 * 1024); // If we aren't using the second CPU, we can use the bootloader space for the heap instead
if cfg!(not(feature="dual-core")) {
esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);
esp_alloc::heap_allocator!(size: 32 * 1024);
} else {
esp_alloc::heap_allocator!(size: 100000);
}
RenderbugLogger::init_logger(); RenderbugLogger::init_logger();
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);
info!("Boot memory stats: {}", esp_alloc::HEAP.stats());
let sys_timer = SystemTimer::new(peripherals.SYSTIMER); let sys_timer = SystemTimer::new(peripherals.SYSTIMER);
esp_rtos::start(sys_timer.alarm0); esp_rtos::start(sys_timer.alarm0);
info!("Embassy initialized!"); info!("Embassy initialized!");
static MOTION_BUS: StaticCell<Channel<CriticalSectionRawMutex,Measurement,5> > = StaticCell::new(); let motion_bus = MOTION_BUS.take();
let motion_bus = MOTION_BUS.init_with(|| { Channel::new() }); let recording_bus = RECORDING_BUS.take();
info!("Setting up rendering pipeline"); info!("Setting up rendering pipeline");
let mut surfaces = UiSurfacePool::default(); let mut surfaces = UiSurfacePool::default();
let ui = Ui::new(&mut surfaces); let ui = Ui::new(&mut surfaces);
let display_controls = DisplayControls::default(); static MAIN_DISPLAY_SWITCH: ConstStaticCell<DisplayControlResources> = ConstStaticCell::new(DisplayControlResources::new());
let oled_controls = DisplayControls::default(); static OLED_DISPLAY_SWITCH: ConstStaticCell<DisplayControlResources> = ConstStaticCell::new(DisplayControlResources::new());
let display_controls = DisplayControls::new(MAIN_DISPLAY_SWITCH.take());
let oled_controls = DisplayControls::new(OLED_DISPLAY_SWITCH.take());
let mut oled_surfaces = OledUiSurfacePool::default(); let mut oled_surfaces = OledUiSurfacePool::default();
let oled_uniforms = Default::default(); let oled_uniforms = Default::default();
@@ -83,12 +101,33 @@ async fn main(spawner: Spawner) {
wdt.enable(); wdt.enable();
// Spawn the rendering task as soon as possible so it can start pushing pixels // Spawn the rendering task as soon as possible so it can start pushing pixels
spawner.must_spawn(renderbug_embassy::tasks::render::render(peripherals.RMT, peripherals.GPIO5.degrade(), surfaces, safety_surfaces, display_controls, wdt)); spawner.must_spawn(renderbug_bike::tasks::render::render(peripherals.SPI2.degrade(), peripherals.DMA_CH2.degrade(), peripherals.GPIO5.degrade(), surfaces, safety_surfaces, display_controls.clone(), wdt));
// Wait one scheduler tick for the rendering task to get initialized let imu_interrupt = PinInterrupt::new(Input::new(peripherals.GPIO36.degrade(), InputConfig::default()), Event::RisingEdge);
Timer::after_ticks(1).await; let pd_interrupt = PinInterrupt::new(Input::new(peripherals.GPIO16.degrade(), InputConfig::default()), Event::RisingEdge);
let sd_detect_interrupt = PinInterrupt::new(Input::new(peripherals.GPIO42.degrade(), InputConfig::default()), Event::RisingEdge);
let gps_pulse_interrupt = PinInterrupt::new(Input::new(peripherals.GPIO45.degrade(), InputConfig::default()), Event::RisingEdge);
#[cfg(feature="motion")] INTERRUPTS.init(InterruptDispatch::new([
imu_interrupt.clone(),
pd_interrupt.clone(),
sd_detect_interrupt.clone(),
gps_pulse_interrupt.clone()
])).ok();
let mut io = esp_hal::gpio::Io::new(peripherals.IO_MUX);
io.set_interrupt_handler(gpio_interrupt_handler);
spawner.must_spawn(renderbug_bike::tasks::sd_card::sdcard_task(
peripherals.SPI3.degrade(),
peripherals.GPIO41.degrade(),
peripherals.GPIO2.degrade(),
peripherals.GPIO1.degrade(),
Output::new(peripherals.GPIO40.degrade(), esp_hal::gpio::Level::High, OutputConfig::default()),
sd_detect_interrupt,
recording_bus.dyn_subscriber().unwrap()
));
#[cfg(feature="i2c")]
{ {
use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice;
use esp_hal::{i2c::master::{Config, I2c}, Async}; use esp_hal::{i2c::master::{Config, I2c}, Async};
@@ -101,16 +140,17 @@ async fn main(spawner: Spawner) {
let scl = peripherals.GPIO3; let scl = peripherals.GPIO3;
let i2c = I2c::new(peripherals.I2C1, Config::default()).unwrap().with_scl(scl).with_sda(sda).into_async(); let i2c = I2c::new(peripherals.I2C1, Config::default()).unwrap().with_scl(scl).with_sda(sda).into_async();
let i2c_bus = I2C_BUS.init(Mutex::new(i2c)); let i2c_bus = I2C_BUS.init(Mutex::new(i2c));
#[cfg(feature="mpu")] #[cfg(feature="mpu6050")]
spawner.must_spawn(renderbug_embassy::tasks::mpu::mpu_task(motion_bus.dyn_sender(), I2cDevice::new(i2c_bus))); spawner.must_spawn(renderbug_bike::tasks::mpu::mpu_task(motion_bus.dyn_sender(), I2cDevice::new(i2c_bus), imu_interrupt));
#[cfg(feature="gps")]
spawner.must_spawn(renderbug_embassy::tasks::gps::gps_task(motion_bus.dyn_sender(), I2cDevice::new(i2c_bus))); spawner.must_spawn(renderbug_bike::tasks::gps::gps_task(motion_bus.dyn_sender(), I2cDevice::new(i2c_bus)));
spawner.must_spawn(renderbug_bike::tasks::usb_power::usb_task(motion_bus.dyn_sender(), I2cDevice::new(i2c_bus), pd_interrupt, display_controls.clone()));
} }
#[cfg(feature="oled")] #[cfg(feature="oled")]
{ {
use esp_hal::i2c::master::{Config, I2c}; use esp_hal::i2c::master::{Config, I2c};
use renderbug_embassy::graphics::ssd1306::SsdOutput; use renderbug_bike::graphics::ssd1306::SsdOutput;
let rst = Output::new(peripherals.GPIO21, esp_hal::gpio::Level::Low, OutputConfig::default()); let rst = Output::new(peripherals.GPIO21, esp_hal::gpio::Level::Low, OutputConfig::default());
let i2c = I2c::new( let i2c = I2c::new(
@@ -118,23 +158,7 @@ async fn main(spawner: Spawner) {
Config::default().with_frequency(Rate::from_khz(400)) Config::default().with_frequency(Rate::from_khz(400))
).unwrap().with_scl(peripherals.GPIO18).with_sda(peripherals.GPIO17).into_async(); ).unwrap().with_scl(peripherals.GPIO18).with_sda(peripherals.GPIO17).into_async();
let output = SsdOutput::new(i2c, rst, oled_controls).await; let output = SsdOutput::new(i2c, rst, oled_controls).await;
spawner.must_spawn(renderbug_embassy::tasks::oled_render::oled_render(output, oled_surfaces, oled_uniforms)); spawner.must_spawn(renderbug_bike::tasks::oled_render::oled_render(output, oled_surfaces, oled_uniforms));
}
#[cfg(feature="simulation")]
{
use esp_storage::FlashStorage;
use renderbug_embassy::tasks::simulation::{SharedFlash, SimDataTable};
let mut storage = SharedFlash::new(FlashStorage::new());
let mut buf = [8; 1024];
let partitions = esp_bootloader_esp_idf::partitions::read_partition_table(&mut storage, &mut buf).unwrap();
for sim_data in SimDataTable::open(storage, partitions).expect("Could not find sim data!") {
let srcid = sim_data.srcid();
info!("Found simulation data for {srcid:?}");
if spawner.spawn(renderbug_embassy::tasks::simulation::simulation_task(sim_data, motion_bus.dyn_sender())).is_err() {
error!("Unable to spawn simulation task for {srcid:?}! Increase the task pool size.");
}
}
} }
#[cfg(feature="radio")] #[cfg(feature="radio")]
@@ -145,7 +169,16 @@ async fn main(spawner: Spawner) {
let ble = esp_radio::ble::controller::BleConnector::new(wifi_init, peripherals.BT, esp_radio::ble::Config::default()).unwrap(); let ble = esp_radio::ble::controller::BleConnector::new(wifi_init, peripherals.BT, esp_radio::ble::Config::default()).unwrap();
let (wifi, interfaces) = esp_radio::wifi::new(wifi_init, peripherals.WIFI, esp_radio::wifi::Config::default()) let wifi_config = esp_radio::wifi::Config::default()
.with_rx_queue_size(64)
.with_tx_queue_size(256)
.with_static_rx_buf_num(32)
.with_static_tx_buf_num(32)
.with_dynamic_rx_buf_num(128)
.with_dynamic_tx_buf_num(128)
.with_rx_ba_win(7)
.with_power_save_mode(esp_radio::wifi::PowerSaveMode::Minimum);
let (wifi, interfaces) = esp_radio::wifi::new(wifi_init, peripherals.WIFI, wifi_config)
.expect("Failed to initialize WIFI!"); .expect("Failed to initialize WIFI!");
(wifi, interfaces.sta, ble) (wifi, interfaces.sta, ble)
@@ -153,20 +186,18 @@ async fn main(spawner: Spawner) {
let core2_main = |spawner: Spawner| { let core2_main = |spawner: Spawner| {
info!("Starting application tasks"); info!("Starting application tasks");
let predictions = PREDICTIONS.take();
static PREDICTIONS: StaticCell<PubSubChannel<NoopRawMutex, Prediction, 15, 6, 1>> = StaticCell::new();
let predictions = PREDICTIONS.init(PubSubChannel::new());
#[cfg(not(feature="demo"))] #[cfg(not(feature="demo"))]
{ {
info!("Launching motion engine"); info!("Launching motion engine");
spawner.must_spawn(motion_task(motion_bus.dyn_receiver(), predictions.dyn_publisher().unwrap())); spawner.must_spawn(renderbug_bike::tasks::motion::motion_task(motion_bus.dyn_receiver(), predictions.dyn_publisher().unwrap(), recording_bus.dyn_publisher().unwrap()));
} }
#[cfg(feature="demo")] #[cfg(feature="demo")]
{ {
warn!("Launching with demo sequencer"); warn!("Launching with demo sequencer");
spawner.must_spawn(renderbug_embassy::tasks::demo::demo_task(predictions.dyn_publisher().unwrap())); spawner.must_spawn(renderbug_bike::tasks::demo::demo_task(predictions.dyn_publisher().unwrap()));
} }
info!("Launching Safety UI"); info!("Launching Safety UI");
@@ -188,39 +219,96 @@ async fn main(spawner: Spawner) {
let seed = Rng::new().random() as i32; let seed = Rng::new().random() as i32;
let (stack, runner) = embassy_net::new(network_device, config, RESOURCES.take(), seed as u64); let (stack, runner) = embassy_net::new(network_device, config, RESOURCES.take(), seed as u64);
info!("Launching network services"); info!("Launching network services");
//spawner.must_spawn(renderbug_embassy::tasks::wifi::net_task(runner)); spawner.must_spawn(renderbug_bike::tasks::wifi::net_task(runner));
info!("Starting connectivity task"); info!("Starting connectivity task");
//spawner.must_spawn(renderbug_embassy::tasks::wifi::wifi_connect_task(wifi, stack, motion_bus.dyn_sender())); spawner.must_spawn(renderbug_bike::tasks::wifi::wifi_connect_task(wifi, motion_bus.dyn_sender()));
info!("Starting location sampler");
static SAMPLER: ConstStaticCell<Watch<NoopRawMutex, Prediction, 1>> = ConstStaticCell::new(Watch::new());
let sampler = SAMPLER.take();
spawner.must_spawn(renderbug_bike::tasks::wifi::location_sampler(predictions.dyn_subscriber().unwrap(), sampler.dyn_sender()));
info!("Launching HTTP telemetry"); info!("Launching HTTP telemetry");
//spawner.must_spawn(renderbug_embassy::tasks::wifi::http_telemetry_task(predictions.dyn_subscriber().unwrap(), stack)); spawner.must_spawn(renderbug_bike::tasks::wifi::http_telemetry_task(sampler.dyn_receiver().unwrap(), stack, motion_bus.dyn_sender()));
info!("Starting BLE services"); info!("Starting BLE services");
spawner.must_spawn(renderbug_embassy::tasks::ble::ble_task(ble, predictions.dyn_subscriber().unwrap(), spawner)); spawner.must_spawn(renderbug_bike::tasks::ble::ble_task(ble, predictions.dyn_subscriber().unwrap(), spawner));
} }
#[cfg(feature="dual-core")] spawner.must_spawn(print_sensor_status(predictions.dyn_subscriber().unwrap()));
let mut storage = SharedFlash::new(esp_storage::FlashStorage::new(peripherals.FLASH).multicore_auto_park());
let mut partition_buf = [8; 1024];
let partitions = esp_bootloader_esp_idf::partitions::read_partition_table(&mut storage, &mut partition_buf).unwrap();
#[cfg(any(feature="flash-recording", feature="simulation"))]
{ {
info!("Launching core 2 watchdog"); use renderbug_bike::tasks::simulation::{SimDataStream, SimDataTable};
let timer1 = TimerGroup::new(peripherals.TIMG1); let sim_partition = SimDataTable::find_partition(storage, partitions).expect("Could not find sim data partition!");
let mut ui_wdt = timer1.wdt; info!("Got partition: {sim_partition:?}");
ui_wdt.set_timeout(esp_hal::timer::timg::MwdtStage::Stage0, esp_hal::time::Duration::from_secs(10)); let mut sim_table = match SimDataTable::open(sim_partition.clone()) {
#[cfg(feature="dual-core")] Ok(table) => table,
ui_wdt.enable(); Err(SimDataError::StreamIndexMissing) => {
spawner.must_spawn(wdt_task(ui_wdt)); info!("Sim data partition not formatted, creating new stream table with {sim_partition:?}");
SimDataTable::create(sim_partition).expect("Unable to create sim data stream table in partition!")
},
Err(e) => panic!("Error opening sim data stream table: {e:?}")
};
loop {
match sim_table.next() {
Some((header, reader)) => {
let srcid = header.id;
info!("Found simulation data for {srcid:?} at {:#x}", reader.abs_start());
let stream = SimDataStream::open(reader, srcid);
if cfg!(feature="simulation") {
if spawner.spawn(renderbug_bike::tasks::simulation::simulation_task(stream, motion_bus.dyn_sender())).is_err() {
error!("Unable to spawn simulation task for {srcid:?}! Increase the task pool size.");
}
} else if cfg!(feature="flash-recording") && srcid == StreamType::Bundle {
info!("Continuing recording stream");
spawner.spawn(record_telemetry(recording_bus.dyn_subscriber().unwrap(), stream, motion_bus.dyn_sender())).unwrap();
break;
}
},
None if cfg!(feature="flash-recording") => {
// If we already found a recording stream, we break; out of the loop above
let reader = sim_table.append_new_stream(StreamType::Bundle).expect("Unable to create a new recording stream in the sim data partition! Is there enough free space?");
warn!("Starting new recording stream at {:#x}", reader.abs_start());
let recorder = SimDataStream::create(reader, StreamType::Bundle);
spawner.spawn(record_telemetry(recording_bus.dyn_subscriber().unwrap(), recorder, motion_bus.dyn_sender())).unwrap();
break;
},
_ => ()
}
}
} }
spawner.must_spawn(print_telemetry(predictions.dyn_subscriber().unwrap())); spawner.must_spawn(wdt_task(rtc, predictions.dyn_subscriber().unwrap(), oled_controls, display_controls));
info!("System is ready in {}ms", Instant::now().as_millis()); //info!("Final memory stats: {}", esp_alloc::HEAP.stats());
info!("Ready to rock and roll in {}ms", Instant::now().as_millis());
}; };
#[allow(static_mut_refs)]
#[cfg(feature="dual-core")] #[cfg(feature="dual-core")]
{ {
static mut CORE2_STACK: Stack<16384> = Stack::new(); use core::mem::MaybeUninit;
use esp_hal::interrupt::software::SoftwareInterruptControl;
use esp_hal::system::Stack;
use esp_rtos::embassy::Executor;
// We can be sneaky and stick the stack for the second core into the bootloader ram
#[esp_hal::ram(reclaimed)]
static mut CORE2_STACK: MaybeUninit<Stack<73744>> = MaybeUninit::uninit();
let swi = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); let swi = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start_second_core(peripherals.CPU_CTRL, swi.software_interrupt0, swi.software_interrupt1, unsafe { &mut *addr_of_mut!(CORE2_STACK) }, || { // SAFETY: The internal implementation of Stack is itself MaybeUninit
esp_rtos::start_second_core(peripherals.CPU_CTRL, swi.software_interrupt0, swi.software_interrupt1, unsafe { CORE2_STACK.assume_init_mut() }, || {
info!("Second CPU core started"); info!("Second CPU core started");
static CORE2_EXEC: StaticCell<Executor> = StaticCell::new(); static CORE2_EXEC: StaticCell<Executor> = StaticCell::new();
let exec = CORE2_EXEC.init_with(|| { Executor::new() }); let exec = CORE2_EXEC.init_with(|| { Executor::new() });
@@ -231,7 +319,10 @@ async fn main(spawner: Spawner) {
#[cfg(not(feature="dual-core"))] #[cfg(not(feature="dual-core"))]
core2_main(spawner); core2_main(spawner);
info!("Ready to rock and roll"); loop {
//info!("Memory stats: {}", esp_alloc::HEAP.stats());
Timer::after_secs(1).await;
}
} }
#[embassy_executor::task] #[embassy_executor::task]
@@ -246,12 +337,87 @@ async fn wdt_task(mut wdt: Wdt<esp_hal::peripherals::TIMG1<'static>>) {
} }
#[embassy_executor::task] #[embassy_executor::task]
async fn print_telemetry(mut events: DynSubscriber<'static, Prediction>) { async fn record_telemetry(mut firehose: DynSubscriber<'static, Measurement>, mut storage: SimDataStream<StorageRange<SharedFlash<FlashStorage<'static>>>>, motion: DynamicSender<'static, Measurement>) {
info!("telemetry ready"); let mut skipped_events = 0;
let mut num_events = Wrapping(0usize); while let Ok(Some((_, evt))) = storage.read_next() {
trace!("Skipping event {evt:?}");
skipped_events += 1;
}
info!("Skipped {} events to catch up to the end of the recording stream", skipped_events);
motion.send(Measurement::SensorHardwareStatus(SensorSource::FlashRecording, SensorState::Online)).await;
storage.write_next(AnnotationReading { buf: *b"Telemetry recording started " }).unwrap();
loop { loop {
let next = events.next_message_pure().await; match firehose.next_message_pure().await {
trace!("idx={} predict={next:?}", num_events.0); Measurement::IMU { accel, gyro } => {
num_events += 1; let reading = IMUReading {
accel_x: accel.x as f64,
accel_y: accel.y as f64,
accel_z: accel.z as f64,
gyro_x: gyro.x as f64,
gyro_y: gyro.y as f64,
gyro_z: gyro.z as f64
};
//storage.write_next(reading).unwrap();
trace!("Wrote IMU to flash");
},
Measurement::GPS(Some(pos)) => {
storage.write_next(GPSReading {
lat: pos.x,
lon: pos.y,
}).unwrap();
trace!("Wrote GPS to flash");
},
Measurement::SensorHardwareStatus(sensor, status) => {
let annotation = alloc::format!("{:?}={:?}", sensor, status);
let mut buf = [0; 32];
let bytes = annotation.as_bytes();
let copy_len = bytes.len().min(buf.len());
buf[..copy_len].copy_from_slice(&bytes[..copy_len]);
if copy_len < buf.len() {
buf[copy_len..].fill(b' ');
}
storage.write_next(AnnotationReading { buf }).unwrap();
trace!("Wrote sensor status update to flash");
}
_ => ()
}
}
}
#[embassy_executor::task]
async fn print_sensor_status(mut events: DynSubscriber<'static, Prediction>) {
let mut sensor_states: EnumMap<SensorSource, SensorState> = EnumMap::default();
loop {
let next = events.next_message_pure().with_timeout(Duration::from_secs(5)).await;
match next {
Ok(Prediction::SensorStatus(sensor, status)) => {
sensor_states[sensor] = status;
let mut report_str = String::new();
for (sensor, state) in &sensor_states {
let state_icon = match state {
SensorState::AcquiringFix => "",
SensorState::Degraded => "!",
SensorState::Offline => "",
SensorState::Online => ""
};
report_str += alloc::format!("{sensor:?}={state_icon} ").as_str();
}
info!("{report_str}");
},
Err(_) => {
let mut report_str = String::new();
for (sensor, state) in &sensor_states {
let state_icon = match state {
SensorState::AcquiringFix => "",
SensorState::Degraded => "!",
SensorState::Offline => "",
SensorState::Online => ""
};
report_str += alloc::format!("{sensor:?}={state_icon} ").as_str();
}
info!("{report_str}");
},
_ => ()
}
} }
} }
+59
View File
@@ -0,0 +1,59 @@
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use esp_hal::clock::CpuClock;
use esp_hal::timer::systimer::SystemTimer;
use log::*;
use esp_backtrace as _;
use renderbug_bike::logging::RenderbugLogger;
use renderbug_bike::tasks::simulation::{SimDataStream, SimDataTable};
use renderbug_bike::simdata::*;
esp_bootloader_esp_idf::esp_app_desc!();
#[esp_rtos::main]
async fn main(spawner: Spawner) {
esp_alloc::heap_allocator!(size: 100000);
RenderbugLogger::init_logger();
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let sys_timer = SystemTimer::new(peripherals.SYSTIMER);
esp_rtos::start(sys_timer.alarm0);
let mut storage = renderbug_bike::storage::SharedFlash::new(esp_storage::FlashStorage::new(peripherals.FLASH).multicore_auto_park());
let mut partition_buf = [8; 1024];
let partitions = esp_bootloader_esp_idf::partitions::read_partition_table(&mut storage, &mut partition_buf).unwrap();
let sim_partition = SimDataTable::find_partition(storage, partitions).expect("Could not find sim data partition!");
info!("Got partition: {sim_partition:?}");
let sim_table = SimDataTable::open(sim_partition).expect("Sim data partition not found");
for (header, reader) in sim_table {
info!("Found stream {header:?}");
if header.id == StreamType::Bundle {
let mut stream = SimDataStream::open(reader, header.id);
let mut timestamp = 0;
loop {
match stream.read_next() {
Ok(Some((timecode, next_evt))) => {
timestamp += timecode.as_millis();
esp_println::println!("{timestamp} {next_evt:?}");
},
Ok(None) => {
warn!("End of simulation data stream");
break
},
Err(err) => {
warn!("Error during sensor stream: {err:?}");
break
}
}
}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use esp_hal::clock::CpuClock;
use esp_hal::timer::systimer::SystemTimer;
use log::*;
use esp_backtrace as _;
use renderbug_bike::logging::RenderbugLogger;
use renderbug_bike::tasks::simulation::{SimDataStream, SimDataTable};
use renderbug_bike::simdata::*;
esp_bootloader_esp_idf::esp_app_desc!();
#[esp_rtos::main]
async fn main(spawner: Spawner) {
esp_alloc::heap_allocator!(size: 100000);
RenderbugLogger::init_logger();
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let sys_timer = SystemTimer::new(peripherals.SYSTIMER);
esp_rtos::start(sys_timer.alarm0);
let mut storage = renderbug_bike::storage::SharedFlash::new(esp_storage::FlashStorage::new(peripherals.FLASH));
let mut partition_buf = [8; 1024];
let partitions = esp_bootloader_esp_idf::partitions::read_partition_table(&mut storage, &mut partition_buf).unwrap();
let sim_partition = SimDataTable::find_partition(storage, partitions).expect("Could not find sim data partition!");
info!("Got partition: {sim_partition:?}");
SimDataTable::create(sim_partition).expect("Could not write empty sim partition");
error!("Overwrote sim data partition with a blank stream table");
}
+61 -47
View File
@@ -1,5 +1,5 @@
use embassy_sync::pubsub::DynPublisher; use embassy_sync::pubsub::DynPublisher;
use embassy_time::{Duration, Instant}; use embassy_time::{Duration, Instant, WithTimeout};
use nalgebra::{Rotation3, Vector2, Vector3, ComplexField, RealField}; use nalgebra::{Rotation3, Vector2, Vector3, ComplexField, RealField};
use log::*; use log::*;
@@ -28,8 +28,7 @@ pub struct BikeStates {
reference_fix: Option<Vector2<f64>>, // The first GPS value, which is used to make sense out of the EKF output which is in meters offset from this point reference_fix: Option<Vector2<f64>>, // The first GPS value, which is used to make sense out of the EKF output which is in meters offset from this point
// State switches // State switches
has_down: Breaker<bool>, has_motion_frame: Breaker<bool>,
has_forwards: Breaker<bool>,
motion_state: Breaker<MotionState>, motion_state: Breaker<MotionState>,
acquiring_data: Breaker<bool>, acquiring_data: Breaker<bool>,
// FIXME: pub // FIXME: pub
@@ -43,10 +42,35 @@ pub struct BikeStates {
sleep_timer: IdleClock, sleep_timer: IdleClock,
} }
/// Above this speed, the system should consider waking up from sleep mode
const BUMP_SPEED_NOISE_GATE: f32 = 0.1;
/// Above this speed, the system should be fully awake and ready to start moving around with purpose
const WAKEUP_SPEED_NOISE_GATE: f32 = 0.5;
/// Above this average speed, we are considered to be moving around with purpose
const MOVING_SPEED_NOISE_GATE: f32 = 1.0;
/// IMU readings of at least this value are considered valid motion. Below this value, the signal is considered noise while stationary
const MOTION_NOISE_GATE: f32 = 0.8;
/// If the system's calculated speed is increasing or decreasing by this value or more, we are accelerating/decelerating
const ACCELERATION_NOISE_GATE: f32 = 1.0;
/// When we get a heading via GPS, this determines how much weight that value has when blended into the system.
const GPS_HEADING_ALPHA_CORRECTION: f32 = 0.9;
const TIMEOUT: Duration = Duration::from_millis(3);
macro_rules! publish {
($predictions:expr, $prediction:expr) => {
$predictions.publish($prediction).with_timeout(TIMEOUT).await.expect("Could not commit IMU data in time. Is the prediction bus stalled?")
}
}
impl Debug for BikeStates { impl Debug for BikeStates {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BikeStates") f.debug_struct("BikeStates")
.field("has_down", &self.orientation.has_down())
.field("has_orientation", &self.orientation.is_ready()) .field("has_orientation", &self.orientation.is_ready())
.field("heading", &self.heading.heading()) .field("heading", &self.heading.heading())
.field("motion_state", &self.motion_state) .field("motion_state", &self.motion_state)
@@ -71,7 +95,7 @@ impl BikeStates {
Some(coords) => { Some(coords) => {
if self.last_fix != coords { if self.last_fix != coords {
let gps_heading = self.last_fix.angle(&coords); let gps_heading = self.last_fix.angle(&coords);
self.heading.correct(gps_heading as f32, 0.9); self.heading.correct(gps_heading as f32, GPS_HEADING_ALPHA_CORRECTION);
} }
let delta = gps_to_local_meters_haversine(&coords, &gps_pos); let delta = gps_to_local_meters_haversine(&coords, &gps_pos);
@@ -99,17 +123,15 @@ impl BikeStates {
let heading_rotation = Rotation3::from_axis_angle(&Vector3::z_axis(), self.heading.heading()); let heading_rotation = Rotation3::from_axis_angle(&Vector3::z_axis(), self.heading.heading());
let enu_rotated = heading_rotation * body_accel; let enu_rotated = heading_rotation * body_accel;
if body_accel.xy().magnitude() >= 0.8 { if body_accel.xy().magnitude() >= MOTION_NOISE_GATE {
self.kf.predict(enu_rotated.xy(), body_gyro.z, dt); self.kf.predict(enu_rotated.xy(), body_gyro.z, dt);
} else { } else {
// Otherwise, we are standing stationary and should insert accel=0 data into the model // Otherwise, we are standing stationary and should insert accel=0 data into the model
self.kf.update_zupt(); self.kf.update_zupt();
} }
self.has_down.set(true);
if self.orientation.is_ready() { if self.orientation.is_ready() {
self.has_forwards.set(true); self.has_motion_frame.set(true);
} }
} }
} }
@@ -118,23 +140,18 @@ impl BikeStates {
let last_motion = self.motion_state.value; let last_motion = self.motion_state.value;
if let Some(true) = self.acquiring_data.read_tripped() { if let Some(true) = self.acquiring_data.read_tripped() {
predictions.publish(Prediction::SensorStatus(SensorSource::ForwardsReference, SensorState::AcquiringFix)).await; publish!(predictions, Prediction::SensorStatus(SensorSource::MotionFrame, SensorState::AcquiringFix));
predictions.publish(Prediction::SensorStatus(SensorSource::GravityReference, SensorState::AcquiringFix)).await; publish!(predictions, Prediction::SensorStatus(SensorSource::Location, SensorState::AcquiringFix));
predictions.publish(Prediction::SensorStatus(SensorSource::Location, SensorState::AcquiringFix)).await;
} }
if let Some(true) = self.has_down.read_tripped() { if let Some(true) = self.has_motion_frame.read_tripped() {
predictions.publish(Prediction::SensorStatus(SensorSource::GravityReference, SensorState::Online)).await publish!(predictions, Prediction::SensorStatus(SensorSource::MotionFrame, SensorState::Online));
}
if let Some(true) = self.has_forwards.read_tripped() {
predictions.publish(Prediction::SensorStatus(SensorSource::ForwardsReference, SensorState::Online)).await
} }
match self.has_gps_fix.read_tripped() { match self.has_gps_fix.read_tripped() {
None => (), None => (),
Some(true) => predictions.publish(Prediction::SensorStatus(SensorSource::Location, SensorState::Online)).await, Some(true) => publish!(predictions, Prediction::SensorStatus(SensorSource::Location, SensorState::Online)),
Some(false) => predictions.publish(Prediction::SensorStatus(SensorSource::Location, SensorState::Degraded)).await, Some(false) => publish!(predictions, Prediction::SensorStatus(SensorSource::Location, SensorState::Degraded)),
} }
let est = self.kf.x; let est = self.kf.x;
@@ -146,7 +163,7 @@ impl BikeStates {
if let Some(pos) = position { if let Some(pos) = position {
self.predicted_location.set(pos); self.predicted_location.set(pos);
if let Some(pos) = self.predicted_location.read_tripped() { if let Some(pos) = self.predicted_location.read_tripped() {
predictions.publish(Prediction::Location(pos)).await; publish!(predictions, Prediction::Location(pos));
} }
} }
@@ -157,45 +174,43 @@ impl BikeStates {
self.speedo.insert(current_prediction); self.speedo.insert(current_prediction);
// If the model has enough samples to report useful data, we can start analyzing the motion trends // If the model has enough samples to report useful data, we can start analyzing the motion trends
if self.speedo.is_filled() { if self.speedo.is_filled() {
let threshold = 1.0;
// Calculate if the velocity is increasing, decreasing, or mostly the same // Calculate if the velocity is increasing, decreasing, or mostly the same
let trend = self.speedo.data().windows(2).map(|n| { let trend = self.speedo.data().windows(2).map(|n| {
n[1] - n[0] n[1] - n[0]
}).sum::<f32>(); }).sum::<f32>();
// Also grab the average velocity of the last few sample periods // Also grab the average velocity of the last few sample periods
let mean = self.speedo.mean(); let average_speed = self.speedo.mean();
trace!("prediction={current_prediction:?} mean={average_speed}");
// Reported velocity is kept only to the first decimal, so we aren't spamming the system with floating point noise // Reported velocity is kept only to the first decimal, so we aren't spamming the system with floating point noise
self.reported_velocity.set((mean * 10.0).trunc() / 10.0); self.reported_velocity.set((average_speed * 10.0).trunc() / 10.0);
if let Some(reported) = self.reported_velocity.read_tripped() { if let Some(reported) = self.reported_velocity.read_tripped() {
predictions.publish(Prediction::Velocity(reported)).await; publish!(predictions, Prediction::Velocity(reported));
} }
// We only want to wake up from sleep if our current velocity is obviously intentional, eg not a quick bump // We only want to wake up from sleep if our current velocity is something like a bump
if mean > 0.5 { if average_speed > BUMP_SPEED_NOISE_GATE && self.sleep_timer.wake() {
if self.sleep_timer.wake() { warn!("Waking from sleep into idle mode");
warn!("Waking from sleep into idle mode"); publish!(predictions, Prediction::SetPersonality(Personality::Waking));
predictions.publish(Prediction::SetPersonality(Personality::Waking)).await; publish!(predictions, Prediction::SetPersonality(Personality::Parked));
predictions.publish(Prediction::SetPersonality(Personality::Parked)).await; }
} // Here, we additionally release the parking brake if we are currently parked and reading substantial movement
// Here, we additionally release the parking brake if we are currently parked and reading some kind of significant movement if average_speed > WAKEUP_SPEED_NOISE_GATE && self.parking_timer.wake() {
if self.parking_timer.wake() { warn!("Disengaging parking brake");
warn!("Disengaging parking brake"); publish!(predictions, Prediction::SetPersonality(Personality::Active));
predictions.publish(Prediction::SetPersonality(Personality::Active)).await;
}
} }
// If the total slope is more upwards than not, we are accelerating. // If the total slope is more upwards than not, we are accelerating.
if trend >= threshold { if trend >= ACCELERATION_NOISE_GATE {
self.motion_state.set(MotionState::Accelerating); self.motion_state.set(MotionState::Accelerating);
self.steady_timer.wake(); self.steady_timer.wake();
} else if trend <= -threshold { } else if trend <= -ACCELERATION_NOISE_GATE {
self.steady_timer.wake(); self.steady_timer.wake();
self.motion_state.set(MotionState::Decelerating); self.motion_state.set(MotionState::Decelerating);
} else if self.steady_timer.check() && mean > 1.0 { } else if self.steady_timer.check() && average_speed > MOVING_SPEED_NOISE_GATE {
// If we haven't changed our acceleration for a while, and we still have speed, we are moving at a steady pace // If we haven't changed our acceleration for a while, and we still have speed, we are moving at a steady pace
self.motion_state.set(MotionState::Steady); self.motion_state.set(MotionState::Steady);
} else if current_prediction <= 1.0 && mean <= 1.0 { } else if current_prediction <= 1.0 && average_speed <= MOVING_SPEED_NOISE_GATE {
// If the average and instantaneous speed is rather low, we are probably stationary! // If the average and instantaneous speed is rather low, we are probably stationary!
self.motion_state.set(MotionState::Stationary); self.motion_state.set(MotionState::Stationary);
} }
@@ -206,19 +221,19 @@ impl BikeStates {
//debug!("state={state:?} trend={trend} mean={mean} v={v}"); //debug!("state={state:?} trend={trend} mean={mean} v={v}");
//if state != MotionState::Stationary { //if state != MotionState::Stationary {
// warn!("Active due to motion"); // warn!("Active due to motion");
// predictions.publish(Prediction::SetPersonality(Personality::Active)).await; // publish!(predictions, Prediction::SetPersonality(Personality::Active));
//} //}
predictions.publish(Prediction::Motion { prev: last_motion, next: state }).await; publish!(predictions, Prediction::Motion { prev: last_motion, next: state });
} else if self.motion_state.value == MotionState::Stationary { } else if self.motion_state.value == MotionState::Stationary {
// Finally, if we are stationary, check our parking and sleep timers // Finally, if we are stationary, check our parking and sleep timers
if self.parking_timer.check() { if self.parking_timer.check() {
warn!("Engaging parking brake"); warn!("Engaging parking brake");
predictions.publish(Prediction::SetPersonality(Personality::Parked)).await; publish!(predictions, Prediction::SetPersonality(Personality::Parked));
} }
if self.sleep_timer.check() { if self.sleep_timer.check() {
warn!("Sleeping!"); warn!("Sleeping!");
predictions.publish(Prediction::SetPersonality(Personality::Sleeping)).await; publish!(predictions, Prediction::SetPersonality(Personality::Sleeping));
} }
} }
} }
@@ -232,8 +247,7 @@ impl Default for BikeStates {
last_stamp: Instant::now(), last_stamp: Instant::now(),
speedo: Default::default(), speedo: Default::default(),
heading: Default::default(), heading: Default::default(),
has_down: Default::default(), has_motion_frame: Default::default(),
has_forwards: Default::default(),
kf: Default::default(), kf: Default::default(),
steady_timer: IdleClock::new(Duration::from_secs(3)), steady_timer: IdleClock::new(Duration::from_secs(3)),
last_fix: Default::default(), last_fix: Default::default(),
+1 -1
View File
@@ -66,7 +66,7 @@ impl OrientationEstimator {
self.forward = Some(Unit::new_unchecked(-unit_forward.into_inner())); self.forward = Some(Unit::new_unchecked(-unit_forward.into_inner()));
} }
} else { } else {
info!("Found forwards: {unit_forward:?} avg.norm={}", horiz.norm()); info!("Motion frame established with forwards={unit_forward:?} avg.norm={} down={:?} bias={:?}", horiz.norm(), self.down, self.sensor_bias);
self.forward = Some(unit_forward); self.forward = Some(unit_forward);
} }
+11 -4
View File
@@ -3,6 +3,7 @@ use embassy_time::Duration;
use enum_map::Enum; use enum_map::Enum;
use enumset::EnumSetType; use enumset::EnumSetType;
use figments::liber8tion::interpolate::Fract8; use figments::liber8tion::interpolate::Fract8;
use figments_render::power::Milliwatts;
use nalgebra::{Vector2, Vector3}; use nalgebra::{Vector2, Vector3};
use crate::ego::engine::MotionState; use crate::ego::engine::MotionState;
@@ -38,13 +39,15 @@ pub enum Measurement {
GPS(Option<Vector2<f64>>), GPS(Option<Vector2<f64>>),
// Accelerometer values in body frame where x=forwards // Accelerometer values in body frame where x=forwards
IMU { accel: Vector3<f32>, gyro: Vector3<f32> }, IMU { accel: Vector3<f32>, gyro: Vector3<f32> },
// Power status
ExternalPower(Milliwatts),
// Hardware status updates // Hardware status updates
SensorHardwareStatus(SensorSource, SensorState), SensorHardwareStatus(SensorSource, SensorState),
// Simulation metadata updates // Simulation metadata updates
SimulationProgress(SensorSource, Duration, Fract8), SimulationProgress(SensorSource, Duration, Fract8),
Annotation Annotation([u8; 32])
} }
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -73,13 +76,16 @@ pub enum SensorSource {
// Real hardware // Real hardware
IMU, IMU,
GPS, GPS,
ExternalPower,
// Connectivity related // Connectivity related
Wifi, Wifi,
// Data processing/logging
FlashRecording,
// Fusion outputs // Fusion outputs
GravityReference, MotionFrame,
ForwardsReference,
Location, Location,
Cloud, Cloud,
@@ -97,7 +103,8 @@ impl From<StreamType> for SensorSource {
match value { match value {
StreamType::Annotations => Self::Annotations, StreamType::Annotations => Self::Annotations,
StreamType::GPS => Self::GPS, StreamType::GPS => Self::GPS,
StreamType::IMU => Self::IMU StreamType::IMU => Self::IMU,
StreamType::Bundle => unimplemented!()
} }
} }
} }
+62
View File
@@ -0,0 +1,62 @@
use core::cell::RefCell;
use alloc::sync::Arc;
use critical_section::Mutex;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
use esp_hal::gpio::Input;
pub struct InterruptDispatch<'a, const COUNT: usize> {
interrupts: [PinInterrupt<'a>; COUNT]
}
impl<'a, const COUNT: usize> InterruptDispatch<'a, COUNT> {
pub fn new(interrupts: [PinInterrupt<'a>; COUNT]) -> Self {
Self { interrupts }
}
pub fn process_interrupts(&self) {
for interrupt in &self.interrupts {
interrupt.handle_interrupt();
}
}
}
#[derive(Clone)]
pub struct PinInterrupt<'a> {
pin: Arc<Mutex<RefCell<Input<'a>>>>,
signal: Arc<Signal<CriticalSectionRawMutex, esp_hal::gpio::Level>>,
event: esp_hal::gpio::Event
}
impl<'a> PinInterrupt<'a> {
pub fn new(pin: Input<'a>, event: esp_hal::gpio::Event) -> Self {
Self {
pin: Arc::new(Mutex::new(RefCell::new(pin))),
signal: Arc::new(Signal::new()),
event
}
}
pub fn handle_interrupt(&self) {
critical_section::with(|cs| {
let locked = self.pin.borrow(cs);
let mut pin = locked.borrow_mut();
if pin.is_interrupt_set() {
pin.clear_interrupt();
self.signal.signal(pin.level());
}
});
}
pub fn listen(&self) {
critical_section::with(|cs| {
let locked = self.pin.borrow(cs);
let mut pin = locked.borrow_mut();
pin.listen(self.event);
});
}
pub async fn wait_for_interrupt(&self) -> esp_hal::gpio::Level {
self.signal.wait().await
}
}
+104 -60
View File
@@ -1,11 +1,11 @@
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal, watch::{Receiver, Watch}}; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal, watch::{Receiver, Watch}};
use figments::prelude::*; use figments::{liber8tion::interpolate::Fract8, prelude::*};
use core::{fmt::Debug, sync::atomic::{AtomicBool, AtomicU8}}; use portable_atomic::AtomicU32;
use core::{cell::RefCell, fmt::Debug, ops::Mul, sync::atomic::{AtomicBool, AtomicU8}};
use alloc::sync::Arc; use alloc::sync::Arc;
//use super::{Output};
use figments_render::{ use figments_render::{
gamma::{GammaCurve, WithGamma}, output::{Brightness, GammaCorrected, Output, OutputAsync}, power::AsMilliwatts, smart_leds::PowerManagedWriter gamma::{GammaCurve, WithGamma}, output::{Brightness, GammaCorrected, Output, OutputAsync}, power::{AsMilliwatts, Milliwatts}, smart_leds::PowerManagedWriter
}; };
use smart_leds::{SmartLedsWrite, SmartLedsWriteAsync}; use smart_leds::{SmartLedsWrite, SmartLedsWriteAsync};
@@ -14,7 +14,7 @@ pub const NUM_PIXELS: usize = 178;
pub struct BikeOutput<T, Color> { pub struct BikeOutput<T, Color> {
pixbuf: [Color; NUM_PIXELS], pixbuf: [Color; NUM_PIXELS],
writer: PowerManagedWriter<T>, writer: PowerManagedWriter<T>,
controls: DisplayControls controls: DisplayControls<'static>
} }
impl<T, Color> GammaCorrected for BikeOutput<T, Color> { impl<T, Color> GammaCorrected for BikeOutput<T, Color> {
@@ -24,10 +24,10 @@ impl<T, Color> GammaCorrected for BikeOutput<T, Color> {
} }
impl<T, Color: Default + Copy> BikeOutput<T, Color> { impl<T, Color: Default + Copy> BikeOutput<T, Color> {
pub fn new(target: T, max_mw: u32, controls: DisplayControls) -> Self { pub fn new(target: T, controls: DisplayControls<'static>) -> Self {
Self { Self {
pixbuf: [Default::default(); NUM_PIXELS], pixbuf: [Default::default(); NUM_PIXELS],
writer: PowerManagedWriter::new(target, max_mw), writer: PowerManagedWriter::new(target, controls.max_power()),
controls controls
} }
} }
@@ -37,38 +37,39 @@ impl<T, Color: Default + Copy> BikeOutput<T, Color> {
} }
} }
impl<'a, T: SmartLedsWrite + 'a> Output<'a, SegmentSpace> for BikeOutput<T, T::Color> where T::Color: AsMilliwatts + Copy + Fract8Ops + WithGamma + Default + Debug + 'a + 'static { impl<'a, T: SmartLedsWrite + 'a> Output<'a, SegmentSpace> for BikeOutput<T, T::Color> where T::Color: AsMilliwatts + Copy + Mul<Fract8, Output = T::Color> + WithGamma + Default + Debug + 'a + 'static {
type Error = T::Error; type Error = T::Error;
type Controls = DisplayControls; type Controls = DisplayControls<'static>;
fn commit(&mut self) -> Result<(), Self::Error> { fn commit(&mut self) -> Result<(), Self::Error> {
self.writer.controls().set_brightness(self.controls.brightness()); self.writer.controls().set_brightness(self.controls.brightness());
self.writer.controls().set_on(self.controls.is_on()); self.writer.controls().set_on(self.controls.is_on());
self.writer.controls().set_max_power(self.controls.max_power());
critical_section::with(|_| { critical_section::with(|_| {
self.writer.write(&self.pixbuf) self.writer.write(&self.pixbuf)
}) })
} }
fn controls(&self) -> Option<&Self::Controls> { fn controls(&mut self) -> Option<&mut Self::Controls> {
Some(&self.controls) Some(&mut self.controls)
} }
} }
impl<'a, T: SmartLedsWriteAsync + 'a> OutputAsync<'a, SegmentSpace> for BikeOutput<T, T::Color> where T::Color: 'static + Copy + Fract8Ops + WithGamma + Default + Debug + AsMilliwatts + 'a { impl<'a, T: SmartLedsWriteAsync + 'a> OutputAsync<'a, SegmentSpace> for BikeOutput<T, T::Color> where T::Color: 'static + Copy + Mul<Fract8, Output = T::Color> + WithGamma + Default + Debug + AsMilliwatts + 'a {
async fn commit_async(&mut self) -> Result<(), T::Error> where T: SmartLedsWriteAsync { async fn commit_async(&mut self) -> Result<(), T::Error> where T: SmartLedsWriteAsync {
self.writer.controls().set_brightness(self.controls.brightness()); self.writer.controls().set_brightness(self.controls.brightness());
self.writer.controls().set_on(self.controls.is_on()); self.writer.controls().set_on(self.controls.is_on());
self.writer.controls().set_max_power(self.controls.max_power());
// TODO: We should grab the power used here and somehow feed it back into the telemetry layer, probably just via another atomic u32 // TODO: We should grab the power used here and somehow feed it back into the telemetry layer, probably just via another atomic u32
self.writer.write_async(&self.pixbuf).await self.writer.write_async(&self.pixbuf).await
} }
//type HardwarePixel = T::Color;
type Error = T::Error; type Error = T::Error;
type Controls = DisplayControls; type Controls = DisplayControls<'static>;
fn controls(&self) -> Option<&Self::Controls> { fn controls(&mut self) -> Option<&mut Self::Controls> {
Some(&self.controls) Some(&mut self.controls)
} }
} }
@@ -109,7 +110,6 @@ impl<'a, T> Sample<'a, SegmentSpace> for [T; NUM_PIXELS] where T: 'static {
} }
} }
#[derive(Clone, Copy, Default, Debug)] #[derive(Clone, Copy, Default, Debug)]
pub struct SegmentSpace {} pub struct SegmentSpace {}
impl CoordinateSpace for SegmentSpace { impl CoordinateSpace for SegmentSpace {
@@ -162,99 +162,143 @@ pub struct Uniforms {
pub primary_color: Hsv pub primary_color: Hsv
} }
pub const DEFAULT_FPS: u8 = 60;
pub const LOW_POWER_FPS: u8 = 16;
struct ControlData { struct ControlData {
on: AtomicBool, on: AtomicBool,
brightness: AtomicU8 brightness: AtomicU8,
fps: AtomicU8,
max_power_mw: AtomicU32
} }
impl Default for ControlData { impl Default for ControlData {
fn default() -> Self { fn default() -> Self {
Self { Self {
on: AtomicBool::new(true), on: AtomicBool::new(true),
brightness: AtomicU8::new(255) brightness: AtomicU8::new(255),
fps: AtomicU8::new(DEFAULT_FPS),
// We default to 500mA, because that's what we get over USB by default
max_power_mw: AtomicU32::new(500)
} }
} }
} }
// A watch that indicates whether or not the rendering engine is running. If the display is off or sleeping, this will be false. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
static RENDER_IS_RUNNING: Watch<CriticalSectionRawMutex, bool, 7> = Watch::new(); pub enum RenderState {
On,
// TODO: Implement something similar for a system-wide sleep mechanism Off
pub struct DisplayControls {
data: Arc<ControlData>,
display_is_on: Arc<Signal<CriticalSectionRawMutex, bool>>,
render_run_receiver: Receiver<'static, CriticalSectionRawMutex, bool, 7>
} }
impl Clone for DisplayControls { pub struct DisplayControlResources {
state_rx: Watch<CriticalSectionRawMutex, RenderState, 7>,
state_ack: Watch<CriticalSectionRawMutex, RenderState, 7>
}
impl DisplayControlResources {
pub const fn new() -> Self {
Self {
state_rx: Watch::new_with(RenderState::On),
state_ack: Watch::new_with(RenderState::On)
}
}
}
// TODO: Implement something similar for a system-wide sleep mechanism
pub struct DisplayControls<'a> {
data: Arc<ControlData>,
render_run_receiver: Receiver<'a, CriticalSectionRawMutex, RenderState, 7>,
render_ack_receiver: Receiver<'a, CriticalSectionRawMutex, RenderState, 7>,
resources: &'a DisplayControlResources,
}
impl Clone for DisplayControls<'_> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
data: Arc::clone(&self.data), data: Arc::clone(&self.data),
display_is_on: Arc::clone(&self.display_is_on), render_run_receiver: self.resources.state_rx.receiver().expect("Could not create enough render running receivers"),
render_run_receiver: RENDER_IS_RUNNING.receiver().expect("Could not create enough render running receivers") resources: self.resources,
render_ack_receiver: self.resources.state_ack.receiver().expect("Could not create enough render ack receivers"),
} }
} }
} }
impl DisplayControls {
impl<'a> DisplayControls<'a> {
pub fn new(resources: &'a DisplayControlResources) -> Self {
Self {
data: Default::default(),
render_run_receiver: resources.state_rx.receiver().expect("Could not create enough render running receivers"),
render_ack_receiver: resources.state_ack.receiver().expect("Could not create enough render ack receivers"),
resources
}
}
pub fn is_on(&self) -> bool { pub fn is_on(&self) -> bool {
self.data.on.load(core::sync::atomic::Ordering::Relaxed) self.data.on.load(core::sync::atomic::Ordering::Relaxed)
} }
pub fn brightness(&self) -> u8 { pub fn brightness(&self) -> Fract8 {
self.data.brightness.load(core::sync::atomic::Ordering::Relaxed) Fract8::from_raw(self.data.brightness.load(core::sync::atomic::Ordering::Relaxed))
} }
pub async fn wait_until_display_is_on(&self) { pub fn fps(&self) -> u8 {
while !self.display_is_on.wait().await { log::info!("wait for display") } self.data.fps.load(core::sync::atomic::Ordering::Relaxed)
log::trace!("display says on!");
} }
pub fn notify_render_is_running(&mut self, value: bool) { pub fn set_fps(&self, value: u8) {
log::trace!("render is running!"); self.data.fps.store(value, core::sync::atomic::Ordering::Relaxed);
RENDER_IS_RUNNING.sender().send(value);
} }
pub async fn wait_until_render_is_running(&mut self) { pub fn set_max_power(&mut self, mw: Milliwatts) {
while !self.render_run_receiver.changed().await { log::info!("wait for render run") } self.data.max_power_mw.store(mw.0, core::sync::atomic::Ordering::Relaxed);
log::trace!("render says run!"); }
pub fn max_power(&self) -> Milliwatts {
Milliwatts(self.data.max_power_mw.load(core::sync::atomic::Ordering::Relaxed))
}
pub async fn wait_for_state(&mut self, state: RenderState) {
self.render_run_receiver.get_and(|cur| { *cur == state }).await;
}
pub async fn wait_for_ack(&mut self, state: RenderState) {
self.render_ack_receiver.get_and(|cur| { *cur == state }).await;
}
/// Indicates the current state has been applied to the hardware, which can wake up tasks waiting for the power to turn on/off.
pub async fn ack(&mut self) {
let next_state = self.render_run_receiver.get().await;
let is_on = next_state == RenderState::On;
self.data.on.store(is_on, core::sync::atomic::Ordering::Relaxed);
self.resources.state_ack.sender().send(if self.is_on() { RenderState::On } else { RenderState::Off });
} }
} }
impl GammaCorrected for DisplayControls { impl GammaCorrected for DisplayControls<'_> {
fn set_gamma(&mut self, _gamma: GammaCurve) { fn set_gamma(&mut self, _gamma: GammaCurve) {
todo!() todo!()
} }
} }
impl Brightness for DisplayControls { impl Brightness for DisplayControls<'_> {
fn set_brightness(&mut self, brightness: u8) { fn set_brightness(&mut self, brightness: Fract8) {
self.data.brightness.store(brightness, core::sync::atomic::Ordering::Relaxed); self.data.brightness.store(brightness.to_raw(), core::sync::atomic::Ordering::Relaxed);
} }
fn set_on(&mut self, is_on: bool) { fn set_on(&mut self, is_on: bool) {
self.data.on.store(is_on, core::sync::atomic::Ordering::Relaxed); self.resources.state_rx.sender().send(if is_on { RenderState::On } else { RenderState::Off });
log::trace!("display is on {is_on}"); log::trace!("display is on {is_on}");
self.display_is_on.signal(is_on);
} }
} }
impl core::fmt::Debug for DisplayControls { impl core::fmt::Debug for DisplayControls<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
f.debug_struct("DisplayControls") f.debug_struct("DisplayControls")
.field("on", &self.data.on) .field("on", &self.data.on)
.field("brightness", &self.data.brightness) .field("brightness", &self.data.brightness)
.field("render_pause_signaled", &self.display_is_on.signaled()) .field("fps", &self.data.fps)
.field("max_power_mw", &self.data.max_power_mw)
.finish() .finish()
} }
} }
impl Default for DisplayControls {
fn default() -> Self {
Self {
data: Default::default(),
display_is_on: Default::default(),
render_run_receiver: RENDER_IS_RUNNING.receiver().unwrap()
}
}
}
+2
View File
@@ -4,6 +4,8 @@ pub mod shaders;
pub mod oled_ui; pub mod oled_ui;
mod images { mod images {
#![allow(unused)]
use embedded_graphics::{ use embedded_graphics::{
image::ImageRaw, image::ImageRaw,
pixelcolor::BinaryColor pixelcolor::BinaryColor
+5 -5
View File
@@ -10,9 +10,9 @@ use embedded_graphics::primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder,
use embedded_graphics::text::{Alignment, Text}; use embedded_graphics::text::{Alignment, Text};
use embedded_graphics::{image::Image, prelude::Point, Drawable}; use embedded_graphics::{image::Image, prelude::Point, Drawable};
use enum_map::EnumMap; use enum_map::EnumMap;
use figments::liber8tion::trig::sin8; use figments::liber8tion::trig::Trig8;
use figments::mappings::embedded_graphics::Matrix2DSpace; use figments::mappings::embedded_graphics::Matrix2DSpace;
use figments::{liber8tion::trig::cos8, mappings::embedded_graphics::EmbeddedGraphicsSampler}; use figments::{mappings::embedded_graphics::EmbeddedGraphicsSampler};
use figments::prelude::*; use figments::prelude::*;
use nalgebra::Vector2; use nalgebra::Vector2;
use micromath::F32Ext; use micromath::F32Ext;
@@ -108,7 +108,7 @@ const SENSOR_IMAGES: &[SensorImage] = &[
on: &images::IMU_ON, off: &images::IMU_OFF, on: &images::IMU_ON, off: &images::IMU_OFF,
}, },
SensorImage { SensorImage {
source: SensorSource::GravityReference, source: SensorSource::MotionFrame,
on: &images::GRAVITY_LOCATED, off: &images::GRAVITY_MISSING, on: &images::GRAVITY_LOCATED, off: &images::GRAVITY_MISSING,
}, },
SensorImage { SensorImage {
@@ -145,9 +145,9 @@ impl Screen {
Image::new(&images::BOOT_LOGO, Point::zero()).draw(sampler).unwrap(); Image::new(&images::BOOT_LOGO, Point::zero()).draw(sampler).unwrap();
const SPARKLE_COUNT: i32 = 8; const SPARKLE_COUNT: i32 = 8;
for n in 0..SPARKLE_COUNT { for n in 0..SPARKLE_COUNT {
let sparkle_center = Point::new(128u8.scale8(cos8(state.frame.wrapping_mul(n as usize))) as i32, 64u8.scale8(sin8(state.frame.wrapping_mul(n as usize) as u8)) as i32); let sparkle_center = Point::new((128u8 * state.frame.wrapping_mul(n as usize).cos8()) as i32, (64u8 * state.frame.wrapping_mul(n as usize).sin8()) as i32);
let offset = (state.frame / 2 % 32) as i32 - 16; let offset = (state.frame / 2 % 32) as i32 - 16;
let rotation = PI * 2.0 * (sin8(state.frame) as f32 / 255.0); let rotation = PI * 2.0 * state.frame.sin8();
let normal = Point::new((rotation.cos() * offset as f32) as i32, (rotation.sin() * offset as f32) as i32); let normal = Point::new((rotation.cos() * offset as f32) as i32, (rotation.sin() * offset as f32) as i32);
let cross_normal = Point::new((rotation.sin() * offset as f32) as i32, (rotation.cos() * offset as f32) as i32); let cross_normal = Point::new((rotation.sin() * offset as f32) as i32, (rotation.cos() * offset as f32) as i32);
// Draw horizontal // Draw horizontal
+19 -18
View File
@@ -1,6 +1,7 @@
use core::cmp::max; use core::cmp::max;
use figments::{liber8tion::{interpolate::{ease_in_out_quad}, noise::inoise8, trig::{cos8, sin8}}, prelude::*}; use figments::{liber8tion::{interpolate::{Fract8, ease_in_out_quad}, noise::inoise8, trig::Trig8}, prelude::*};
use num_traits::WrappingAdd;
use rgb::Rgba; use rgb::Rgba;
use crate::graphics::display::{SegmentSpace, Uniforms}; use crate::graphics::display::{SegmentSpace, Uniforms};
@@ -25,8 +26,8 @@ impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Movement {
} else { } else {
uniforms.frame.wrapping_sub(surface_coords.x) uniforms.frame.wrapping_sub(surface_coords.x)
}; };
let idx = sin8(offset).wrapping_add(uniforms.primary_color.hue); let idx = offset.sin8().wrapping_add(&Fract8::from_raw(uniforms.primary_color.hue));
Rgba::new(idx, idx.wrapping_mul(2), idx.wrapping_div(2), 128) Rgba::new(idx.to_raw(), idx.to_raw().wrapping_mul(2), idx.to_raw() / 2, 128)
} }
} }
@@ -45,12 +46,12 @@ impl Background {
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Background { impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Background {
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> { fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
let noise_x = sin8(uniforms.frame % 255) as i16; let noise_x = (uniforms.frame % 255).sin8().to_raw() as i16;
let noise_y = cos8(uniforms.frame % 255) as i16; let noise_y = (uniforms.frame % 255).cos8().to_raw() as i16;
let brightness = inoise8(noise_x.wrapping_add(coords.x as i16), noise_y.wrapping_add(coords.y as i16)); let brightness = inoise8(noise_x.wrapping_add(coords.x as i16), noise_y.wrapping_add(coords.y as i16));
let saturation = inoise8(noise_y.wrapping_add(coords.y as i16), noise_x.wrapping_add(coords.x as i16)); let saturation = inoise8(noise_y.wrapping_add(coords.y as i16), noise_x.wrapping_add(coords.x as i16));
let rgb: Rgb<u8> = match self.color { let rgb: Rgb<u8> = match self.color {
None => Hsv::new(uniforms.primary_color.hue, max(128, saturation), brightness).into(), None => Hsv::new(uniforms.primary_color.hue, max(128, saturation.to_raw()), brightness.to_raw()).into(),
Some(c) => c Some(c) => c
}; };
@@ -62,9 +63,9 @@ impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Background {
pub struct Tail {} pub struct Tail {}
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Tail { impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Tail {
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> { fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
let hue_offset: u8 = 32.scale8(sin8(uniforms.frame.wrapping_sub(coords.x))); let hue_offset: u8 = 32u8 * Fract8::from_raw(uniforms.frame.wrapping_sub(coords.x) as u8);
let value = max(30, inoise8(coords.x.wrapping_add(uniforms.frame) as i16, coords.y.wrapping_add(uniforms.frame) as i16)); let value = max(30, inoise8(coords.x.wrapping_add(uniforms.frame) as i16, coords.y.wrapping_add(uniforms.frame) as i16).to_raw());
Hsv::new(uniforms.primary_color.hue.wrapping_sub(hue_offset), max(210, sin8(uniforms.frame.wrapping_add(coords.x))), value).into() Hsv::new(uniforms.primary_color.hue.wrapping_sub(hue_offset), max(210, uniforms.frame.wrapping_add(coords.x).sin8().to_raw()), value).into()
} }
} }
@@ -96,10 +97,10 @@ impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Brakelight {
let distance_from_end = Self::end() - coords.x; let distance_from_end = Self::end() - coords.x;
if distance_from_end < Self::safety_length() { if distance_from_end < Self::safety_length() {
Rgba::new(max(128, sin8(uniforms.frame.wrapping_sub(coords.x))), 0, 0, 255) Rgba::new(max(128, uniforms.frame.wrapping_sub(coords.x).sin8().to_raw()), 0, 0, 255)
} else { } else {
let pct = (distance_from_end as f32 / Self::length() as f32) * 255f32; let pct = (distance_from_end as f32 / Self::length() as f32) * 255f32;
Rgba::new(max(100, ease_in_out_quad((pct as u8).wrapping_add(uniforms.frame as u8))), 0, 0, ease_in_out_quad(255 - pct as u8)) Rgba::new(max(100, ease_in_out_quad(Fract8::from_raw(pct as u8).wrapping_add(&Fract8::from_raw(uniforms.frame as u8))).to_raw()), 0, 0, ease_in_out_quad(Fract8::from_raw(255 - pct as u8)).to_raw())
} }
} }
} }
@@ -108,7 +109,7 @@ impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Brakelight {
pub struct Headlight {} pub struct Headlight {}
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Headlight { impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Headlight {
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> { fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
Hsv::new(0, 0, max(130, ease_in_out_quad(sin8(uniforms.frame.wrapping_sub(coords.x))))).into() Hsv::new(0, 0, max(130, ease_in_out_quad(uniforms.frame.wrapping_sub(coords.x).sin8()).to_raw())).into()
} }
} }
@@ -116,14 +117,14 @@ impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Headlight {
pub struct Panel {} pub struct Panel {}
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Panel { impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Panel {
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> { fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
let noise_offset = max(180, inoise8(coords.x.wrapping_add(uniforms.frame) as i16, coords.y.wrapping_add(uniforms.frame) as i16)); let noise_offset = max(180, inoise8(coords.x.wrapping_add(uniforms.frame) as i16, coords.y.wrapping_add(uniforms.frame) as i16).to_raw());
let pct = (coords.x as f32 / 18f32) * 255f32; let pct = (coords.x as f32 / 18f32) * 255f32;
let shift = match coords.y { let shift = match coords.y {
1..=2 => 106, // 150 degrees 1..=2 => 106, // 150 degrees
3..=4 => 148, // 210 degrees 3..=4 => 148, // 210 degrees
_ => 0 _ => 0
}; };
Hsv::new(uniforms.primary_color.hue.wrapping_add(shift), noise_offset, max(100, sin8(pct as u8).wrapping_add(uniforms.frame as u8))).into() Hsv::new(uniforms.primary_color.hue.wrapping_add(shift), noise_offset, max(100, pct.sin8().wrapping_add(&Fract8::from_raw(uniforms.frame as u8)).to_raw())).into()
} }
} }
@@ -134,15 +135,15 @@ impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Thinking {
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> { fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
//let noise_x = sin8(sin8((frame % 255) as u8).wrapping_add(coords.x)); //let noise_x = sin8(sin8((frame % 255) as u8).wrapping_add(coords.x));
//let noise_y = cos8(cos8((frame % 255) as u8).wrapping_add(coords.y)); //let noise_y = cos8(cos8((frame % 255) as u8).wrapping_add(coords.y));
let offset_x = sin8(uniforms.frame.wrapping_add(coords.x)); let offset_x = uniforms.frame.wrapping_add(coords.x).sin8();
let offset_y = cos8(uniforms.frame.wrapping_add(coords.y)); let offset_y = uniforms.frame.wrapping_add(coords.y).cos8();
let noise_x = offset_x / 2; let noise_x = offset_x / 2;
let noise_y = offset_y / 2; let noise_y = offset_y / 2;
//let noise_x = coords.x.wrapping_add(offset_x); //let noise_x = coords.x.wrapping_add(offset_x);
//let noise_y = coords.y.wrapping_add(offset_y); //let noise_y = coords.y.wrapping_add(offset_y);
Hsv::new( Hsv::new(
inoise8(offset_x as i16, offset_y as i16), inoise8(offset_x.to_raw() as i16, offset_y.to_raw() as i16).to_raw(),
128_u8.saturating_add(inoise8(noise_y.into(), noise_x.into())), 128_u8.saturating_add(inoise8(noise_y.to_raw().into(), noise_x.to_raw().into()).to_raw()),
255 255
).into() ).into()
} }
+12 -12
View File
@@ -4,7 +4,7 @@ use core::{cmp::min, ops::{BitAnd, Shr}};
use display_interface::DisplayError; use display_interface::DisplayError;
use embedded_graphics::prelude::*; use embedded_graphics::prelude::*;
use esp_hal::{gpio::Output, i2c::master::I2c, Async}; use esp_hal::{gpio::Output, i2c::master::I2c, Async};
use figments::{liber8tion::{interpolate::Fract8, noise}, mappings::embedded_graphics::Matrix2DSpace, prelude::*}; use figments::{liber8tion::{interpolate::Fract8}, mappings::embedded_graphics::Matrix2DSpace, prelude::*};
use embedded_graphics::pixelcolor::BinaryColor; use embedded_graphics::pixelcolor::BinaryColor;
use figments::pixels::AdditivePixelSink; use figments::pixels::AdditivePixelSink;
use figments_render::output::OutputAsync; use figments_render::output::OutputAsync;
@@ -59,10 +59,10 @@ const DITHER_MAP: [u16;15] = [
impl AdditivePixelSink<BinaryColor> for SsdPixel { impl AdditivePixelSink<BinaryColor> for SsdPixel {
fn add(&mut self, pixel: BinaryColor, opacity: Fract8) { fn add(&mut self, pixel: BinaryColor, opacity: Fract8) {
match opacity { match opacity {
0 => (), Fract8::MIN => (),
255 => self.set_pixel(pixel), Fract8::MAX => self.set_pixel(pixel),
_ => { _ => {
let dither_value = DITHER_MAP[opacity as usize / 17]; let dither_value = DITHER_MAP[opacity.to_raw() as usize / 17];
let dither_x = self.coords.x % 4; let dither_x = self.coords.x % 4;
let dither_y = self.coords.y % 4; let dither_y = self.coords.y % 4;
if dither_value.shr(dither_x).shr(dither_y * 4).bitand(0x01) == 1 { if dither_value.shr(dither_x).shr(dither_y * 4).bitand(0x01) == 1 {
@@ -106,9 +106,9 @@ impl<'a> Iterator for SsdSampler<'a> {
pub struct SsdOutput { pub struct SsdOutput {
pixbuf: [u8; 128 * 64 / 8], pixbuf: [u8; 128 * 64 / 8],
target: ssd1306::Ssd1306Async<ssd1306::prelude::I2CInterface<esp_hal::i2c::master::I2c<'static, esp_hal::Async>>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>, target: ssd1306::Ssd1306Async<ssd1306::prelude::I2CInterface<esp_hal::i2c::master::I2c<'static, esp_hal::Async>>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>,
controls: DisplayControls, controls: DisplayControls<'static>,
is_on: bool, is_on: bool,
last_brightness: u8, last_brightness: Fract8,
reset_pin: Output<'static> reset_pin: Output<'static>
} }
@@ -124,7 +124,7 @@ impl SsdOutput {
Some(SsdPixel { byte: pixref, bit, coords }) Some(SsdPixel { byte: pixref, bit, coords })
} }
pub async fn new(i2c: I2c<'static, Async>, reset_pin: Output<'static>, controls: DisplayControls) -> Self { pub async fn new(i2c: I2c<'static, Async>, reset_pin: Output<'static>, controls: DisplayControls<'static>) -> Self {
let interface = I2CDisplayInterface::new(i2c); let interface = I2CDisplayInterface::new(i2c);
let target = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0); let target = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0);
@@ -132,7 +132,7 @@ impl SsdOutput {
pixbuf: [0; 128 * 64 / 8], pixbuf: [0; 128 * 64 / 8],
target, target,
controls, controls,
last_brightness: 255, last_brightness: Fract8::MAX,
is_on: true, is_on: true,
reset_pin reset_pin
} }
@@ -221,7 +221,7 @@ impl OriginDimensions for SsdOutput {
impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput { impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput {
type Error = DisplayError; type Error = DisplayError;
type Controls = DisplayControls; type Controls = DisplayControls<'static>;
async fn commit_async(&mut self) -> Result<(), Self::Error> { async fn commit_async(&mut self) -> Result<(), Self::Error> {
let new_brightness = self.controls.brightness(); let new_brightness = self.controls.brightness();
@@ -231,14 +231,14 @@ impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput {
self.is_on = new_power; self.is_on = new_power;
} }
if self.last_brightness != new_brightness { if self.last_brightness != new_brightness {
self.target.set_brightness(ssd1306::prelude::Brightness::custom(1, new_brightness)).await.unwrap(); self.target.set_brightness(ssd1306::prelude::Brightness::custom(1, new_brightness.to_raw())).await.unwrap();
self.last_brightness = new_brightness; self.last_brightness = new_brightness;
} }
self.target.draw(&self.pixbuf).await self.target.draw(&self.pixbuf).await
} }
fn controls(&self) -> Option<&Self::Controls> { fn controls(&mut self) -> Option<&mut Self::Controls> {
Some(&self.controls) Some(&mut self.controls)
} }
} }
+3 -4
View File
@@ -8,12 +8,11 @@ pub mod animation;
pub mod idle; pub mod idle;
pub mod logging; pub mod logging;
pub mod graphics; pub mod graphics;
pub mod tracing;
#[cfg(feature="simulation")]
pub mod storage; pub mod storage;
#[cfg(feature="simulation")]
pub mod simdata; pub mod simdata;
pub mod gpio_interrupt;
pub mod tusb320;
extern crate alloc; extern crate alloc;
+29 -4
View File
@@ -1,11 +1,14 @@
#![allow(static_mut_refs)]
use embassy_sync::blocking_mutex::{raw::CriticalSectionRawMutex, Mutex}; use embassy_sync::blocking_mutex::{raw::CriticalSectionRawMutex, Mutex};
use esp_hal::time::Instant;
use esp_println::println; use esp_println::println;
use log::{LevelFilter, Metadata, Record}; use log::{LevelFilter, Metadata, Record};
use static_cell::StaticCell;
// Provides a threadsafe serial logger // Provides a threadsafe serial logger
pub struct RenderbugLogger { pub struct RenderbugLogger {
lock: Mutex<CriticalSectionRawMutex, ()>, lock: Mutex<CriticalSectionRawMutex, ()>,
current_level: LevelFilter,
default_level: LevelFilter default_level: LevelFilter
} }
@@ -13,12 +16,13 @@ impl Default for RenderbugLogger {
fn default() -> Self { fn default() -> Self {
Self { Self {
lock: Mutex::new(()), lock: Mutex::new(()),
current_level: LevelFilter::Info,
default_level: LevelFilter::Info default_level: LevelFilter::Info
} }
} }
} }
static LOGGER: StaticCell<RenderbugLogger> = StaticCell::new(); static mut LOGGER: Option<RenderbugLogger> = None;
impl RenderbugLogger { impl RenderbugLogger {
pub fn init_logger() { pub fn init_logger() {
@@ -33,7 +37,8 @@ impl RenderbugLogger {
_ => LevelFilter::Info _ => LevelFilter::Info
}; };
let logger = LOGGER.init(RenderbugLogger { default_level, ..Default::default() }); unsafe { LOGGER.replace(RenderbugLogger { default_level, current_level: default_level, ..Default::default() }) };
let logger = unsafe { LOGGER.as_mut().unwrap() };
unsafe { unsafe {
critical_section::with(|_| { critical_section::with(|_| {
log::set_logger_racy(logger).ok(); log::set_logger_racy(logger).ok();
@@ -41,6 +46,25 @@ impl RenderbugLogger {
}); });
}; };
} }
pub fn set_level(level: LevelFilter) {
unsafe {
critical_section::with(|_| {
log::set_max_level_racy(level);
LOGGER.as_mut().unwrap().current_level = level;
})
}
}
pub fn reset_level() {
unsafe {
critical_section::with(|_| {
let logger = LOGGER.as_mut().unwrap();
log::set_max_level_racy(logger.default_level);
logger.current_level = logger.default_level;
})
}
}
} }
impl log::Log for RenderbugLogger { impl log::Log for RenderbugLogger {
@@ -69,13 +93,14 @@ impl log::Log for RenderbugLogger {
let filename = record.file().map_or("???", |f| {f}); let filename = record.file().map_or("???", |f| {f});
let crate_name = record.module_path_static().unwrap(); let crate_name = record.module_path_static().unwrap();
let timestamp = Instant::now().duration_since_epoch().as_micros();
for enabled in option_env!("LOG_CRATES").unwrap_or("").split(",") { for enabled in option_env!("LOG_CRATES").unwrap_or("").split(",") {
if crate_name.starts_with(enabled) { if crate_name.starts_with(enabled) {
self.lock.lock(|_| { self.lock.lock(|_| {
#[cfg(feature = "rtt")] #[cfg(feature = "rtt")]
rtt_target::rprintln!("{}{}\t{}{}:{}{}\t{}{}", color, record.level(), GREY, filename, record.line().map_or(0, |f| {f}), RESET, record.args(), RESET); rtt_target::rprintln!("{}{}\t{}{}:{}{}\t{}{}", color, record.level(), GREY, filename, record.line().map_or(0, |f| {f}), RESET, record.args(), RESET);
println!("{color}{}\t{GREY}{} {filename}:{}{RESET}\t{}{RESET}", record.level(), record.module_path_static().unwrap(), record.line().map_or(0, |f| {f}), record.args()); println!("{color}{}\t{GREY}{timestamp}\t{} {filename}:{}{RESET}\t{}{RESET}", record.level(), record.module_path_static().unwrap(), record.line().map_or(0, |f| {f}), record.args());
}); });
return; return;
} }
+81 -27
View File
@@ -1,38 +1,48 @@
use rmp::{Marker, decode::{ExtMeta, RmpRead, ValueReadError}, encode::{RmpWrite, ValueWriteError}}; use rmp::{decode::{ExtMeta, RmpRead, RmpReadErr, ValueReadError}, encode::{RmpWrite, RmpWriteErr, ValueWriteError}};
pub trait RmpData: Sized { pub trait RmpData: Sized {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>>; fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>>;
fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>>; fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>>;
} }
pub trait EventRecord: RmpData { pub trait EventRecord: RmpData {
fn stream_id() -> StreamType;
fn field_count() -> usize; fn field_count() -> usize;
} }
#[derive(Debug)] #[derive(Debug)]
pub enum SimDataError<E> { pub enum SimDataError<E: RmpReadErr + RmpWriteErr> {
StreamIndexMissing, StreamIndexMissing,
InvalidChunkSize { expected: usize, found: usize }, InvalidChunkSize { expected: usize, found: usize },
MissingTimecode, MissingTimecode,
BadString, BadString,
DecodeError(E), DecodeError(ValueReadError<E>),
EncodeError(ValueWriteError<E>),
EndOfStream, EndOfStream,
UnsupportedStreamType(ExtMeta), UnsupportedStreamType(ExtMeta),
EventHeaderMissing, EventHeaderMissing,
PartitionNotFound PartitionNotFound,
NotEnoughSpace
} }
impl<E> From<E> for SimDataError<E> { impl<E: RmpReadErr + RmpWriteErr> From<ValueReadError<E>> for SimDataError<E> {
fn from(value: E) -> Self { fn from(value: ValueReadError<E>) -> Self {
SimDataError::DecodeError(value) SimDataError::DecodeError(value)
} }
} }
#[derive(Debug)] impl<E: RmpReadErr + RmpWriteErr> From<ValueWriteError<E>> for SimDataError<E> {
fn from(value: ValueWriteError<E>) -> Self {
SimDataError::EncodeError(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamType { pub enum StreamType {
IMU, IMU,
GPS, GPS,
Annotations Annotations,
Bundle
} }
impl TryFrom<i8> for StreamType { impl TryFrom<i8> for StreamType {
@@ -43,6 +53,7 @@ impl TryFrom<i8> for StreamType {
1 => Ok(StreamType::IMU), 1 => Ok(StreamType::IMU),
2 => Ok(StreamType::GPS), 2 => Ok(StreamType::GPS),
3 => Ok(StreamType::Annotations), 3 => Ok(StreamType::Annotations),
4 => Ok(StreamType::Bundle),
_ => Err(()) _ => Err(())
} }
} }
@@ -53,7 +64,8 @@ impl From<StreamType> for i8 {
match value { match value {
StreamType::IMU => 1, StreamType::IMU => 1,
StreamType::GPS => 2, StreamType::GPS => 2,
StreamType::Annotations => 3 StreamType::Annotations => 3,
StreamType::Bundle => 4
} }
} }
} }
@@ -64,20 +76,49 @@ pub struct StreamIndex {
} }
impl RmpData for StreamIndex { impl RmpData for StreamIndex {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>> { fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
rmp::decode::read_array_len(reader).map(|count| { if rmp::decode::read_u16(reader)? != 0xDA1A {
Self { Err(SimDataError::StreamIndexMissing)
count: count as usize } else {
} rmp::decode::read_u64(reader).map(|count| {
}).map_err(|_| { SimDataError::StreamIndexMissing }) Self {
count: count as usize
}
}).map_err(|err| { SimDataError::DecodeError(err) })
}
} }
fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>> { fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>> {
rmp::encode::write_array_len(writer, self.count as u32)?; rmp::encode::write_u16(writer, 0xDA1A)?;
rmp::encode::write_u64(writer, self.count as u64)?;
Ok(()) Ok(())
} }
} }
#[derive(Debug)]
pub struct BundleEventHeader {
pub id: StreamType,
}
impl RmpData for BundleEventHeader {
fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
let meta = rmp::decode::read_i8(reader)?;
if let Ok(id) = meta.try_into() {
Ok(Self {
id
})
} else {
Err(SimDataError::EventHeaderMissing)
}
}
fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>> {
rmp::encode::write_i8(writer, self.id.into())
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct StreamHeader { pub struct StreamHeader {
pub id: StreamType, pub id: StreamType,
@@ -85,7 +126,7 @@ pub struct StreamHeader {
} }
impl RmpData for StreamHeader { impl RmpData for StreamHeader {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>> { fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
let meta = rmp::decode::read_ext_meta(reader)?; let meta = rmp::decode::read_ext_meta(reader)?;
if let Ok(id) = meta.typeid.try_into() { if let Ok(id) = meta.typeid.try_into() {
Ok(Self { Ok(Self {
@@ -97,8 +138,9 @@ impl RmpData for StreamHeader {
} }
} }
fn write_rmp<Writer: RmpWrite>(&self, _writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>> { fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>> {
todo!() rmp::encode::write_ext_meta(writer, self.size as u32, self.id.into())?;
Ok(())
} }
} }
@@ -108,14 +150,14 @@ pub struct EventStreamHeader {
} }
impl RmpData for EventStreamHeader { impl RmpData for EventStreamHeader {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>> { fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
Ok(Self { Ok(Self {
count: rmp::decode::read_array_len(reader)? as usize count: rmp::decode::read_u32(reader)? as usize
}) })
} }
fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>> { fn write_rmp<Writer: RmpWrite>(&self, writer: &mut Writer) -> Result<(), ValueWriteError<Writer::Error>> {
rmp::encode::write_array_len(writer, self.count as u32)?; rmp::encode::write_u32(writer, self.count as u32)?;
Ok(()) Ok(())
} }
} }
@@ -127,7 +169,7 @@ pub struct StreamEvent<Event: EventRecord> {
} }
impl<Event: EventRecord> RmpData for StreamEvent<Event> { impl<Event: EventRecord> RmpData for StreamEvent<Event> {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>> { fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
let chunk_len = rmp::decode::read_array_len(reader).map_err(|_| { SimDataError::EventHeaderMissing })? as usize; let chunk_len = rmp::decode::read_array_len(reader).map_err(|_| { SimDataError::EventHeaderMissing })? as usize;
// Add 1 to the field count for the timestamp // Add 1 to the field count for the timestamp
if chunk_len != Event::field_count() + 1 { if chunk_len != Event::field_count() + 1 {
@@ -158,16 +200,24 @@ impl EventRecord for GPSReading {
fn field_count() -> usize { fn field_count() -> usize {
2 2
} }
fn stream_id() -> StreamType {
StreamType::GPS
}
} }
impl EventRecord for IMUReading { impl EventRecord for IMUReading {
fn field_count() -> usize { fn field_count() -> usize {
6 6
} }
fn stream_id() -> StreamType {
StreamType::IMU
}
} }
impl RmpData for GPSReading { impl RmpData for GPSReading {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>> { fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
Ok(Self { Ok(Self {
lat: rmp::decode::read_f64(reader)?, lat: rmp::decode::read_f64(reader)?,
lon: rmp::decode::read_f64(reader)? lon: rmp::decode::read_f64(reader)?
@@ -191,7 +241,7 @@ pub struct IMUReading {
} }
impl RmpData for IMUReading { impl RmpData for IMUReading {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>> { fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
Ok(Self { Ok(Self {
accel_x: rmp::decode::read_f64(reader)?, accel_x: rmp::decode::read_f64(reader)?,
accel_y: rmp::decode::read_f64(reader)?, accel_y: rmp::decode::read_f64(reader)?,
@@ -221,7 +271,7 @@ pub struct AnnotationReading {
} }
impl RmpData for AnnotationReading { impl RmpData for AnnotationReading {
fn from_rmp<Reader: RmpRead>(reader: &mut Reader) -> Result<Self, SimDataError<ValueReadError<Reader::Error>>> { fn from_rmp<Reader: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr>(reader: &mut Reader) -> Result<Self, SimDataError<E>> {
let mut buf = [0; 32]; let mut buf = [0; 32];
rmp::decode::read_str(reader, &mut buf).map_err(|_| { SimDataError::BadString })?; rmp::decode::read_str(reader, &mut buf).map_err(|_| { SimDataError::BadString })?;
Ok(Self { Ok(Self {
@@ -239,4 +289,8 @@ impl EventRecord for AnnotationReading {
fn field_count() -> usize { fn field_count() -> usize {
1 1
} }
fn stream_id() -> StreamType {
StreamType::Annotations
}
} }
+151 -20
View File
@@ -1,9 +1,14 @@
use core::{cell::RefCell, fmt::Formatter}; use core::{cell::RefCell, fmt::Formatter};
use alloc::rc::Rc; use alloc::rc::Rc;
use embedded_io::{ErrorKind, ErrorType, Read, Write};
use embedded_storage::{ReadStorage, Storage}; use embedded_storage::{ReadStorage, Storage};
use esp_bootloader_esp_idf::partitions::PartitionTable;
use esp_hal::time::Instant;
use log::*; use log::*;
use rmp::decode::{RmpRead, RmpReadErr}; use rmp::{decode::{RmpRead, RmpReadErr}, encode::{RmpWrite, RmpWriteErr, ValueWriteError}};
use crate::{simdata::{BundleEventHeader, EventRecord, EventStreamHeader, RmpData, SimDataError, StreamEvent, StreamHeader, StreamIndex, StreamType}, tasks::simulation::Checkpoint};
#[derive(Debug)] #[derive(Debug)]
pub struct SharedFlash<S> { pub struct SharedFlash<S> {
@@ -45,69 +50,174 @@ impl<S: ReadStorage> ReadStorage for SharedFlash<S> {
} }
#[derive(Debug)] #[derive(Debug)]
pub enum RangeReadError<E> { pub enum StorageRangeError<E> {
OutOfData, OutOfData,
Storage(E) Storage(E)
} }
impl<E: core::fmt::Debug + 'static> RmpReadErr for RangeReadError<E> {} impl<E: core::fmt::Debug + 'static> RmpReadErr for StorageRangeError<E> {}
impl<E: core::fmt::Debug + 'static> RmpWriteErr for StorageRangeError<E> {}
impl<E> core::fmt::Display for RangeReadError<E> { impl<E> core::fmt::Display for StorageRangeError<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.write_str("RmpErr") f.write_str("RmpErr")
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct RangeReader<S> { pub struct StorageRange<S> {
storage: S, storage: S,
start: usize, start: usize,
end: usize, end: usize,
offset: usize offset: usize,
reset_pos: usize
} }
impl<S: ReadStorage> RangeReader<S> { impl<S> Checkpoint for StorageRange<S> {
fn checkpoint(&mut self) {
self.reset_pos = self.offset;
}
fn rollback(&mut self) {
self.offset = self.reset_pos;
}
}
impl<S> StorageRange<S> {
pub fn inner(&self) -> &S {
&self.storage
}
pub fn inner_mut(&mut self) -> &mut S {
&mut self.storage
}
pub fn abs_start(&self) -> usize {
self.start
}
pub fn abs_end(&self) -> usize {
self.end
}
}
impl<E: core::fmt::Debug> embedded_io::Error for StorageRangeError<E> {
fn kind(&self) -> embedded_io::ErrorKind {
ErrorKind::Other
}
}
impl<S: ReadStorage<Error = E>, E: core::fmt::Debug> ErrorType for StorageRange<S> {
type Error = StorageRangeError<E>;
}
impl<S: Storage<Error = E> + ReadStorage<Error = E>, E: core::fmt::Debug> Write for StorageRange<S> {
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
let pos = self.start + self.offset;
trace!("write {:#02x}:{:#02x} -> {:#02x}:{:#02x}", self.start, self.end, pos, pos + buf.len());
if pos > self.end {
Err(StorageRangeError::OutOfData)
} else {
assert!(pos + buf.len() <= self.end);
match self.storage.write(pos as u32, buf) {
Ok(_) => {
self.offset += buf.len();
Ok(buf.len())
},
Err(err) => Err(StorageRangeError::Storage(err))
}
}
}
fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
impl<S: ReadStorage<Error = E>, E: core::fmt::Debug> Read for StorageRange<S> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
let pos = self.start + self.offset;
trace!("read_exact_buf {:#02x}:{:#02x} -> {:#02x}:{:#02x}", self.start, self.end, pos, pos + buf.len());
if pos > self.end {
Err(StorageRangeError::OutOfData)
} else {
let remaining = self.end - pos;
let max_read = buf.len().min(remaining);
if max_read == 0 {
return Err(StorageRangeError::OutOfData)
}
assert!(pos + buf.len() <= self.end);
match self.storage.read(pos as u32, &mut buf[..max_read]) {
Ok(_) => {
self.offset += max_read;
Ok(max_read)
},
Err(err) => Err(StorageRangeError::Storage(err))
}
}
}
}
impl<S: ReadStorage> StorageRange<S> {
pub const fn new(storage: S, start: usize, end: usize) -> Self { pub const fn new(storage: S, start: usize, end: usize) -> Self {
assert!(start <= end); assert!(start <= end);
// TODO: Should add bounds checking since we will know the size of the chunk already
Self { Self {
storage, storage,
start, start,
end, end,
offset: 0 offset: 0,
reset_pos: 0
} }
} }
pub fn seek(&mut self, offset: usize) -> Result<(), RangeReadError<S::Error>> { pub const fn capacity(&self) -> usize {
self.end - self.start
}
pub const fn pos(&self) -> usize {
self.offset
}
pub fn seek(&mut self, offset: usize) -> Result<(), StorageRangeError<S::Error>> {
self.offset += offset; self.offset += offset;
if self.offset > self.end { if self.offset > self.end {
Err(RangeReadError::OutOfData) Err(StorageRangeError::OutOfData)
} else { } else {
Ok(()) Ok(())
} }
} }
pub fn subset(&self, size: usize) -> Result<Self, RangeReadError<S::Error>> where S: Clone + core::fmt::Debug { pub fn seek_abs(&mut self, pos: usize) -> Result<(), StorageRangeError<S::Error>> {
self.offset = pos;
if self.offset > self.end {
Err(StorageRangeError::OutOfData)
} else {
Ok(())
}
}
pub fn subset(&self, size: usize) -> Result<Self, StorageRangeError<S::Error>> where S: Clone + core::fmt::Debug {
trace!("subset {:#02x}:{:#02x} -> {:#02x}:{:#02x}", self.start, self.end, self.start + self.offset, self.start + self.offset + size); trace!("subset {:#02x}:{:#02x} -> {:#02x}:{:#02x}", self.start, self.end, self.start + self.offset, self.start + self.offset + size);
if self.start + self.offset + size > self.end { if self.start + self.offset + size > self.end {
Err(RangeReadError::OutOfData) Err(StorageRangeError::OutOfData)
} else { } else {
Ok(Self { Ok(Self {
storage: self.storage.clone(), storage: self.storage.clone(),
start: self.offset + self.start, start: self.offset + self.start,
end: self.start + self.offset + size, end: self.start + self.offset + size,
offset: 0 offset: 0,
reset_pos: 0
}) })
} }
} }
} }
impl<S: ReadStorage> RmpRead for RangeReader<S> where S::Error: core::fmt::Debug + 'static { impl<S: ReadStorage> RmpRead for StorageRange<S> where S::Error: core::fmt::Debug + 'static {
type Error = RangeReadError<S::Error>; type Error = StorageRangeError<S::Error>;
fn read_exact_buf(&mut self, buf: &mut [u8]) -> Result<(), RangeReadError<S::Error>> { fn read_exact_buf(&mut self, buf: &mut [u8]) -> Result<(), StorageRangeError<S::Error>> {
let pos = self.start + self.offset; let pos = self.start + self.offset;
if pos > self.end { if pos > self.end {
Err(RangeReadError::OutOfData) Err(StorageRangeError::OutOfData)
} else { } else {
assert!(pos + buf.len() <= self.end); assert!(pos + buf.len() <= self.end);
match self.storage.read(pos as u32, buf) { match self.storage.read(pos as u32, buf) {
@@ -115,7 +225,28 @@ impl<S: ReadStorage> RmpRead for RangeReader<S> where S::Error: core::fmt::Debug
self.offset += buf.len(); self.offset += buf.len();
Ok(()) Ok(())
}, },
Err(err) => Err(RangeReadError::Storage(err)) Err(err) => Err(StorageRangeError::Storage(err))
}
}
}
}
impl<S: Storage> RmpWrite for StorageRange<S> where S::Error: core::fmt::Debug + 'static {
type Error = StorageRangeError<S::Error>;
fn write_bytes(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
let pos = self.start + self.offset;
if pos > self.end {
Err(StorageRangeError::OutOfData)
} else {
assert!(pos + buf.len() <= self.end);
match self.storage.write(pos as u32, buf) {
Ok(_) => {
self.offset += buf.len();
Ok(())
},
Err(err) => Err(StorageRangeError::Storage(err))
} }
} }
} }
+43 -47
View File
@@ -1,15 +1,13 @@
use core::ptr::slice_from_raw_parts;
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_futures::select::Either; use embassy_futures::select::Either;
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, pubsub::{DynSubscriber, PubSubChannel, Publisher}}; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, pubsub::{DynSubscriber, PubSubChannel, Publisher}};
use esp_radio::ble::controller::BleConnector; use esp_radio::ble::controller::BleConnector;
use static_cell::{ConstStaticCell, StaticCell}; use static_cell::{ConstStaticCell, StaticCell};
use trouble_host::{attribute, prelude::*, types::gatt_traits::FromGattError}; use trouble_host::{prelude::*, types::gatt_traits::FromGattError};
use log::*; use log::*;
use crate::{backoff::Backoff, events::Prediction}; use crate::events::Prediction;
#[gatt_server] #[gatt_server]
struct SerialServer { struct SerialServer {
@@ -55,7 +53,7 @@ struct LocationData {
impl FixedGattValue for LocationData { impl FixedGattValue for LocationData {
fn as_gatt(&self) -> &[u8] { fn as_gatt(&self) -> &[u8] {
let databuf = [0;Self::SIZE]; //let databuf = [0;Self::SIZE];
unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::SIZE) } unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::SIZE) }
} }
@@ -91,24 +89,24 @@ struct SerialService {
async fn client_prediction_task(mut src: DynSubscriber<'static, Prediction>, sink: Publisher<'static, NoopRawMutex, Prediction, 5, 1, 1>) { async fn client_prediction_task(mut src: DynSubscriber<'static, Prediction>, sink: Publisher<'static, NoopRawMutex, Prediction, 5, 1, 1>) {
debug!("Started BLE client prediction stream"); debug!("Started BLE client prediction stream");
loop { loop {
sink.publish(src.next_message_pure().await).await; let _ = sink.try_publish(src.next_message_pure().await);
} }
} }
static STATIC_RESOURCES: StaticCell<HostResources<DefaultPacketPool, 1, 1>> = StaticCell::new(); static STATIC_RESOURCES: StaticCell<HostResources<DefaultPacketPool, 1, 1>> = StaticCell::new();
static STATIC_STACK: StaticCell<Stack<'static, ExternalController<BleConnector<'static>, 1>, DefaultPacketPool>> = StaticCell::new(); static STATIC_STACK: StaticCell<Stack<'static, ExternalController<BleConnector<'static>, 1>, DefaultPacketPool>> = StaticCell::new();
static STATIC_SERVER: StaticCell<SerialServer> = StaticCell::new(); static STATIC_SERVER: StaticCell<SerialServer> = StaticCell::new();
static STATIC_CLIENT_PREDICTIONS: StaticCell<PubSubChannel<NoopRawMutex, Prediction, 5, 1, 1>> = StaticCell::new(); static STATIC_CLIENT_PREDICTIONS: ConstStaticCell<PubSubChannel<NoopRawMutex, Prediction, 5, 1, 1>> = ConstStaticCell::new(PubSubChannel::new());
#[embassy_executor::task] #[embassy_executor::task]
pub async fn ble_task(ble: BleConnector<'static>, predictions: DynSubscriber<'static, Prediction>, spawner: Spawner) { pub async fn ble_task(ble: BleConnector<'static>, predictions: DynSubscriber<'static, Prediction>, spawner: Spawner) {
info!("Starting BLE stack"); info!("Starting BLE stack");
let server = STATIC_SERVER.init(SerialServer::new_with_config(GapConfig::Peripheral(PeripheralConfig { name: "Renderbug", appearance: &appearance::light_source::LED_ARRAY })).unwrap()); let server = STATIC_SERVER.init_with(|| { SerialServer::new_with_config(GapConfig::Peripheral(PeripheralConfig { name: "Renderbug", appearance: &appearance::light_source::LED_ARRAY })).unwrap() });
let control: ExternalController<esp_radio::ble::controller::BleConnector<'_>, 1> = ExternalController::new(ble); let control: ExternalController<esp_radio::ble::controller::BleConnector<'_>, 1> = ExternalController::new(ble);
let stack = STATIC_STACK.init(trouble_host::new(control, STATIC_RESOURCES.init(HostResources::new()))); let stack = STATIC_STACK.init_with(|| { trouble_host::new(control, STATIC_RESOURCES.init_with(|| { HostResources::new() })) });
let Host { mut peripheral, mut runner, .. } = stack.build(); let Host { mut peripheral, mut runner, .. } = stack.build();
let client_predictions = STATIC_CLIENT_PREDICTIONS.init(PubSubChannel::new()); let client_predictions = STATIC_CLIENT_PREDICTIONS.take();
spawner.must_spawn(client_prediction_task(predictions, client_predictions.publisher().unwrap())); spawner.must_spawn(client_prediction_task(predictions, client_predictions.publisher().unwrap()));
// The host task must be started and ticking beore we can start advertising, so we use a join() here instead of a separate task // The host task must be started and ticking beore we can start advertising, so we use a join() here instead of a separate task
@@ -116,43 +114,41 @@ pub async fn ble_task(ble: BleConnector<'static>, predictions: DynSubscriber<'st
runner.run(), runner.run(),
async { async {
loop { loop {
let advertiser = Backoff::from_secs(5).forever().attempt(async || { info!("Starting BLE advertising");
info!("Starting BLE advertising"); let mut adv_data = [0; 64];
let mut adv_data = [0; 64]; let len = AdStructure::encode_slice(
let len = AdStructure::encode_slice( &[
&[ AdStructure::CompleteLocalName("Renderbug".as_bytes()),
AdStructure::CompleteLocalName("Renderbug".as_bytes()), AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), AdStructure::ServiceUuids128(&[
AdStructure::ServiceUuids128(&[ [ // Serial service
[ // Serial service 0x6E, 0x40, 0x00, 0x01,
0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3,
0xB5, 0xA3, 0xF3, 0x93,
0xF3, 0x93, 0xE0, 0xA9,
0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E
0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E //0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5,
//0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, //0x01, 0x00, 0x40, 0x6E,
//0x01, 0x00, 0x40, 0x6E, ],
], [ // Renderbug mesh service
[ // Renderbug mesh service 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xf4, 0x3a,
0xf4, 0x3a, 0x48, 0xc4,
0x48, 0xc4, 0x84, 0x11,
0x84, 0x11, 0xef, 0xe6, 0xa3, 0x8a, 0x5d, 0x75
0xef, 0xe6, 0xa3, 0x8a, 0x5d, 0x75 ]
] ]),
]), AdStructure::ServiceUuids16(&[
AdStructure::ServiceUuids16(&[ // Location and navigation
// Location and navigation [0x18, 0x19]
[0x18, 0x19] ])
]) ],
], &mut adv_data[..],
&mut adv_data[..], ).unwrap();
).unwrap(); let advertiser = peripheral.advertise(
peripheral.advertise( &Default::default(),
&Default::default(), Advertisement::ConnectableScannableUndirected { adv_data: &adv_data[..len], scan_data: &[] }
Advertisement::ConnectableScannableUndirected { adv_data: &adv_data[..len], scan_data: &[] } ).await.unwrap();
).await
}).await.unwrap();
info!("Waiting for connection"); info!("Waiting for connection");
match advertiser.accept().await.and_then(|raw| { raw.with_attribute_server(server) }) { match advertiser.accept().await.and_then(|raw| { raw.with_attribute_server(server) }) {
Ok(conn) => { Ok(conn) => {
+1 -1
View File
@@ -17,7 +17,7 @@ pub async fn demo_task(ui: DynPublisher<'static, Prediction>) {
for state in [SensorState::Offline, SensorState::AcquiringFix, SensorState::Degraded, SensorState::Offline] { for state in [SensorState::Offline, SensorState::AcquiringFix, SensorState::Degraded, SensorState::Offline] {
for motion in [MotionState::Accelerating, MotionState::Steady, MotionState::Decelerating, MotionState::Stationary] { for motion in [MotionState::Accelerating, MotionState::Steady, MotionState::Decelerating, MotionState::Stationary] {
ui.publish(Prediction::Motion { prev: motion, next: motion }).await; ui.publish(Prediction::Motion { prev: motion, next: motion }).await;
for sensor in [SensorSource::ForwardsReference, SensorSource::GPS, SensorSource::GravityReference, SensorSource::IMU, SensorSource::Location] { for sensor in [SensorSource::MotionFrame, SensorSource::GPS, SensorSource::IMU, SensorSource::Location] {
ui.publish(Prediction::SensorStatus(sensor, state)).await; ui.publish(Prediction::SensorStatus(sensor, state)).await;
} }
Timer::after_secs(1).await; Timer::after_secs(1).await;
+81 -76
View File
@@ -1,96 +1,101 @@
use alloc::string::String; use alloc::string::String;
use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::{I2cDeviceError, asynch::i2c::I2cDevice};
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::DynamicSender}; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::DynamicSender};
use embassy_time::Timer; use embassy_time::Timer;
use embedded_hal_async::i2c::I2c as _; use embedded_hal_async::i2c::I2c as _;
use esp_hal::{i2c::master::I2c, Async}; use esp_hal::{i2c::master::I2c, Async};
use log::*; use log::*;
use nalgebra::Vector2; use nalgebra::Vector2;
use nmea::Nmea; use nmea::{Nmea, sentences::FixType};
use crate::{backoff::Backoff, events::{Measurement, SensorSource, SensorState}}; use crate::{events::{Measurement, SensorSource, SensorState}};
async fn init_gps(i2c_bus: &mut I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>) -> Result<(), I2cDeviceError<esp_hal::i2c::master::Error>> {
info!("Initializing GPS");
// Enable a bunch of data? idk
let bytes = "$PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n";
i2c_bus.write(0x10, bytes.as_bytes()).await?;
// 1hz updates
let bytes = "$PMTK220,1000*1F\r\n";
i2c_bus.write(0x10, bytes.as_bytes()).await?;
// 1hz position fix
let bytes = "$PMTK300,1000,0,0,0,0*1C\r\n";
i2c_bus.write(0x10, bytes.as_bytes()).await?;
// Antenna updates
let bytes = "$PGCMD,33,1*6C\r\n";
i2c_bus.write(0x10, bytes.as_bytes()).await
}
// FIXME: We need a way to put the GPS to sleep when the system goes to sleep // FIXME: We need a way to put the GPS to sleep when the system goes to sleep
#[embassy_executor::task] #[embassy_executor::task]
pub async fn gps_task(events: DynamicSender<'static, Measurement>, mut i2c_bus: I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>) { pub async fn gps_task(events: DynamicSender<'static, Measurement>, mut i2c_bus: I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>) {
Backoff::from_secs(5).forever().attempt::<_, (), ()>(async || { events.send(Measurement::SensorHardwareStatus(SensorSource::GPS, SensorState::Offline)).await;
events.send(Measurement::SensorHardwareStatus(SensorSource::GPS, SensorState::Offline)).await; if let Err(e) = init_gps(&mut i2c_bus).await {
Backoff::from_secs(5).forever().attempt(async || { error!("Failed to initialize GPS: {e:?}");
info!("Initializing GPS"); return;
events.send(Measurement::SensorHardwareStatus(SensorSource::GPS, SensorState::AcquiringFix)).await; }
// Enable a bunch of data? idk
let bytes = "$PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n";
i2c_bus.write(0x10, bytes.as_bytes()).await?;
// 1hz updates let mut strbuf = String::new();
let bytes = "$PMTK220,1000*1F\r\n";
i2c_bus.write(0x10, bytes.as_bytes()).await?;
// 1hz position fix let mut parser = Nmea::default();
let bytes = "$PMTK300,1000,0,0,0,0*1C\r\n"; let mut parsing = false;
i2c_bus.write(0x10, bytes.as_bytes()).await?; let mut has_lock = false;
events.send(Measurement::SensorHardwareStatus(SensorSource::GPS, SensorState::AcquiringFix)).await;
// Antenna updates info!("GPS is ready!");
let bytes = "$PGCMD,33,1*6C\r\n"; loop {
i2c_bus.write(0x10, bytes.as_bytes()).await let mut buf = [0; 1];
}).await.unwrap(); i2c_bus.read(0x10, &mut buf).await.map_err(|_| { Err::<(), ()>(()) }).ok();
if (buf[0] as char == '\n' || buf[0] as char == '\r') && !strbuf.is_empty() {
let mut strbuf = String::new(); match parser.parse_for_fix(&strbuf) {
Ok(FixType::Invalid) if has_lock => {
let mut parser = Nmea::default(); // TODO: Send a Measurement::SensorOffline(SensorSource::GPS) here instead
let mut parsing = false; events.send(Measurement::GPS(None)).await;
let mut has_lock = false; events.send(Measurement::SensorHardwareStatus(SensorSource::GPS, SensorState::Degraded)).await;
events.send(Measurement::SensorHardwareStatus(SensorSource::GPS, SensorState::Online)).await; has_lock = false
info!("GPS is ready!"); },
loop { Ok(FixType::Invalid) => {
let mut buf = [0; 1]; debug!("Waiting for fix {parser:?}");
i2c_bus.read(0x10, &mut buf).await.map_err(|_| { Err::<(), ()>(()) }).ok(); },
if (buf[0] as char == '\n' || buf[0] as char == '\r') && !strbuf.is_empty() { Ok(fix_type) => {
if let Ok(result) = parser.parse(&strbuf) { if !has_lock {
match parser.fix_type { has_lock = true;
None if has_lock => { info!("Got a fix of type {fix_type:?}");
// TODO: Send a Measurement::SensorOffline(SensorSource::GPS) here instead events.send(Measurement::SensorHardwareStatus(SensorSource::GPS, SensorState::Online)).await;
events.send(Measurement::GPS(None)).await;
has_lock = false
},
None => (),
Some(_) => {
if !has_lock {
has_lock = true;
}
// TODO: Send a Measurement::SensorOnline(SensorSource::GPS) here instead
//TODO: 4 satellites seems to be "Some" fix, 6 is a perfect fix
//TODO: Only send updates when we get the correct nmea sentence
if let (Some(lat), Some(lng)) = (parser.latitude, parser.longitude) {
events.send(Measurement::GPS(Some(Vector2::new(lat, lng)))).await;
}
}
} }
log::trace!("nmea={result:?} raw={strbuf:?}");
log::trace!("nmea={parser:?}"); if let (Some(lat), Some(lng)) = (parser.latitude, parser.longitude) {
log::trace!("speed={:?} altitude={:?} lat={:?} lng={:?} fix={:?}", parser.speed_over_ground, parser.altitude, parser.latitude, parser.longitude, parser.fix_type); events.send(Measurement::GPS(Some(Vector2::new(lat, lng)))).await;
for sat in parser.satellites() {
trace!("\t{} snr={:?} prn={:?}", sat.gnss_type(), sat.snr(), sat.prn())
} }
} else {
log::warn!("Unhandled NMEA {strbuf:?}"); if let (Some(date), Some(time)) = (parser.fix_date, parser.fix_time) {
let now = date.and_time(time).and_utc();
info!("GPS time is {now}");
}
},
Err(nmea::Error::ParsingError(_)) => {
debug!("NMEA could not parse: {strbuf}");
},
Err(err) => {
error!("NMEA error on {strbuf} {err:?}");
} }
strbuf = String::new();
parsing = false;
// Update frequency is 1hz, so we should never get an update faster than once per second
Timer::after_secs(1).await;
} else if strbuf.is_empty() && (buf[0] as char == '$' || buf[0] as char == '!') {
parsing = true;
strbuf.push(buf[0] as char);
Timer::after_millis(10).await;
} else if parsing {
strbuf.push(buf[0] as char);
Timer::after_millis(10).await;
} else {
// If there is no data ready for some reason, wait 500ms, which should place us at least somewhere after the next data frame is ready to read.
Timer::after_millis(500).await;
} }
strbuf = String::new();
parsing = false;
// Update frequency is 1hz, so we should never get an update faster than once per second
Timer::after_secs(1).await;
} else if strbuf.is_empty() && (buf[0] as char == '$' || buf[0] as char == '!') {
parsing = true;
strbuf.push(buf[0] as char);
Timer::after_millis(10).await;
} else if parsing {
strbuf.push(buf[0] as char);
Timer::after_millis(10).await;
} else {
// If there is no data ready for some reason, wait 500ms, which should place us at least somewhere after the next data frame is ready to read.
Timer::after_millis(500).await;
} }
}).await.ok(); }
} }
+5 -3
View File
@@ -1,18 +1,20 @@
#[cfg(feature="mpu")] #[cfg(feature="mpu6050")]
pub mod mpu; pub mod mpu;
#[cfg(feature="gps")]
pub mod gps; pub mod gps;
#[cfg(feature="radio")] #[cfg(feature="radio")]
pub mod wifi; pub mod wifi;
#[cfg(feature="radio")] #[cfg(feature="radio")]
pub mod ble; pub mod ble;
#[cfg(feature="simulation")]
pub mod simulation; pub mod simulation;
#[cfg(feature="demo")] #[cfg(feature="demo")]
pub mod demo; pub mod demo;
#[cfg(feature="oled")] #[cfg(feature="oled")]
pub mod oled_render; pub mod oled_render;
pub mod usb_power;
pub mod sd_card;
// Prediction engines // Prediction engines
pub mod motion; pub mod motion;
+12 -3
View File
@@ -1,10 +1,11 @@
use embassy_sync::{channel::DynamicReceiver, pubsub::DynPublisher}; use embassy_sync::{channel::DynamicReceiver, pubsub::DynPublisher};
use log::*; use log::*;
use embassy_time::{Duration, WithTimeout};
use crate::{ego::engine::BikeStates, events::{Measurement, Prediction}}; use crate::{ego::engine::BikeStates, events::{Measurement, Prediction}};
#[embassy_executor::task] #[embassy_executor::task]
pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, prediction_sink: DynPublisher<'static, Prediction>) { pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, prediction_sink: DynPublisher<'static, Prediction>, recording_sink: DynPublisher<'static, Measurement>) {
let mut states = BikeStates::default(); let mut states = BikeStates::default();
loop { loop {
@@ -25,11 +26,19 @@ pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, prediction_
}, },
// FIXME: This needs harmonized with the automatic data timeout from above, somehow? // FIXME: This needs harmonized with the automatic data timeout from above, somehow?
Measurement::SensorHardwareStatus(source, state) => { Measurement::SensorHardwareStatus(source, state) => {
warn!("Sensor {source:?} reports {state:?}!"); debug!("Sensor {source:?} reports {state:?}!");
prediction_sink.publish(Prediction::SensorStatus(source, state)).await; prediction_sink.publish(Prediction::SensorStatus(source, state)).await;
}, },
Measurement::ExternalPower(mw) => {
info!("Got external power change to {mw:?}");
},
Measurement::SimulationProgress(source, duration, pct) => debug!("{source:?} simulation time: {} {} / 255", duration.as_secs(), pct), Measurement::SimulationProgress(source, duration, pct) => debug!("{source:?} simulation time: {} {} / 255", duration.as_secs(), pct),
Measurement::Annotation => () Measurement::Annotation(msg) => {
info!("Annotation: {}", str::from_utf8(&msg).unwrap_or("<non-utf8-data>"));
}
}
if recording_sink.try_publish(next_measurement).is_err() {
warn!("Could not publish measurement to recording bus. Is the recording bus stalled?");
} }
} }
} }
+39 -34
View File
@@ -1,5 +1,3 @@
use core::cell::RefCell;
use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::channel::DynamicSender; use embassy_sync::channel::DynamicSender;
@@ -12,52 +10,53 @@ use mpu6050_dmp::{address::Address, error_async::Error, sensor_async::Mpu6050};
use nalgebra::Vector3; use nalgebra::Vector3;
use crate::events::SensorSource; use crate::events::SensorSource;
use crate::{backoff::Backoff, events::Measurement}; use crate::gpio_interrupt::PinInterrupt;
use crate::events::Measurement;
const G: f32 = 9.80665; const G: f32 = 9.80665;
const GYRO_SCALE: GyroFullScale = GyroFullScale::Deg2000; const GYRO_SCALE: GyroFullScale = GyroFullScale::Deg2000;
const ACCEL_SCALE: AccelFullScale = AccelFullScale::G2; const ACCEL_SCALE: AccelFullScale = AccelFullScale::G2;
#[embassy_executor::task] #[embassy_executor::task]
pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>) { pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>, _interrupt: PinInterrupt<'static>) {
let backoff = Backoff::from_millis(5); warn!("Initializing connection to MPU");
let busref = RefCell::new(Some(bus)); events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::AcquiringFix)).await;
backoff.forever().attempt::<_, (), ()>(async || { let mut sensor = match Mpu6050::new(bus, Address::default()).await {
events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::Offline)).await; Err(err) => {
let mut sensor = backoff.forever().attempt(async || { error!("Could not connect to MPU: {err:?}");
warn!("Initializing connection to MPU"); events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::Offline)).await;
match Mpu6050::new(busref.replace(None).unwrap(), Address::default()).await.map_err(|e| { e.i2c }) { return;
Err(i2c) => { },
busref.replace(Some(i2c)); Ok(sensor) => sensor
Err(()) };
},
Ok(mut sensor) => {
events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::AcquiringFix)).await;
match backoff.attempt(async || { mpu_init(&mut sensor).await }).await {
Err(_) => {
busref.replace(Some(sensor.release()));
Err(())
},
Ok(_) => Ok(sensor)
}
}
}
}).await?;
let sensor_ref = &mut sensor; loop {
if let Err(err) = mpu_init(&mut sensor).await {
error!("Could not initialize MPU: {err:?}");
events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::Degraded)).await;
continue;
}
events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::Online)).await; events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::Online)).await;
//TODO: Need to read in a constant buffer of accelerometer measurements, which we can then use to determine where "forward" points in the body frame when converting from the sensor frame. //TODO: Need to read in a constant buffer of accelerometer measurements, which we can then use to determine where "forward" points in the body frame when converting from the sensor frame.
// From there, we can rotate the body frame into the world frame using gps headings to generate a compass north // From there, we can rotate the body frame into the world frame using gps headings to generate a compass north
fn lowpass(prev: f32, current: f32, alpha: f32) -> f32 { fn lowpass(prev: f32, current: f32, alpha: f32) -> f32 {
prev + alpha * (current - prev) // alpha in (0,1), small alpha = heavy smoothing prev + alpha * (current - prev) // alpha in (0,1), small alpha = heavy smoothing
} }
let mut prev_accel = Vector3::default(); let mut prev_accel = Vector3::default();
let mut degraded = false;
loop { loop {
match backoff.attempt(async || { sensor_ref.motion6().await }).await { match sensor.motion6().await {
Ok((accel_data, gyro_data)) => { Ok((accel_data, gyro_data)) => {
if degraded {
warn!("Reconnected to MPU");
events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::Online)).await;
degraded = false;
}
// Apply the initial alignment correction to put it into the body frame where X is forward // Apply the initial alignment correction to put it into the body frame where X is forward
let scaled = accel_data.scaled(ACCEL_SCALE); let scaled = accel_data.scaled(ACCEL_SCALE);
let adjusted = Vector3::new( let adjusted = Vector3::new(
@@ -90,12 +89,19 @@ pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevic
}, },
Err(e) => { Err(e) => {
error!("Failed to read MPU motion data! {e:?}"); error!("Failed to read MPU motion data! {e:?}");
busref.replace(Some(sensor.release())); if !degraded {
return Err(()); events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::Degraded)).await;
degraded = true;
} else {
error!("MPU connection is degraded, and failed to read data again. Attempting to re-initialize connection...");
events.send(Measurement::SensorHardwareStatus(SensorSource::IMU, crate::events::SensorState::AcquiringFix)).await;
break;
}
Timer::after_millis(10).await;
} }
}; };
} }
}).await.ok(); }
} }
async fn mpu_init(sensor: &mut Mpu6050<I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>>) -> Result<(), Error<I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>>> { async fn mpu_init(sensor: &mut Mpu6050<I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>>) -> Result<(), Error<I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>>> {
@@ -104,10 +110,9 @@ async fn mpu_init(sensor: &mut Mpu6050<I2cDevice<'static, NoopRawMutex, I2c<'sta
Ok(()) Ok(())
} else { } else {
let mut delay = Delay; let mut delay = Delay;
let backoff = Backoff::from_millis(10);
info!("Initializing DMP"); info!("Initializing DMP");
let start = Instant::now(); let start = Instant::now();
backoff.attempt(async || { sensor.initialize_dmp(&mut delay).await }).await?; sensor.initialize_dmp(&mut delay).await?;
info!("DMP initialized in {}ms", start.as_millis()); info!("DMP initialized in {}ms", start.as_millis());
Ok(()) Ok(())
} }
+11 -13
View File
@@ -2,25 +2,25 @@ use alloc::sync::Arc;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, pubsub::DynSubscriber}; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, pubsub::DynSubscriber};
use embassy_time::{Duration, Timer}; use embassy_time::{Duration, Timer};
use embedded_graphics::pixelcolor::BinaryColor; use embedded_graphics::pixelcolor::BinaryColor;
use figments::{mappings::embedded_graphics::Matrix2DSpace, prelude::{Coordinates, Rectangle}, render::Shader, surface::{BufferedSurfacePool, NullBufferPool, Surface, SurfaceBuilder, Surfaces}}; use figments::{liber8tion::interpolate::Fract8, mappings::embedded_graphics::Matrix2DSpace, prelude::{Coordinates, Rectangle}, render::Shader, surface::{Surface, SurfaceBuilder, Surfaces}};
use figments_render::output::Brightness; use figments_render::output::Brightness;
use log::*; use log::*;
use crate::{animation::Animation, events::{Personality, Prediction}, graphics::{display::DisplayControls, oled_ui::{OledUniforms, Screen}}}; use crate::{animation::Animation, events::{Personality, Prediction}, graphics::{display::DisplayControls, oled_ui::{OledUniforms, Screen}}};
#[cfg(feature="oled")] #[cfg(feature="oled")]
pub type OledUiSurfacePool = BufferedSurfacePool<OledUniforms, Matrix2DSpace, BinaryColor>; pub type OledUiSurfacePool = figments::surfaces::buffered::BufferedSurfacePool<OledUniforms, Matrix2DSpace, BinaryColor>;
#[cfg(not(feature="oled"))] #[cfg(not(feature="oled"))]
pub type OledUiSurfacePool = NullBufferPool<OledUniforms, Matrix2DSpace, BinaryColor>; pub type OledUiSurfacePool = figments::surfaces::null::NullBufferPool<OledUniforms, Matrix2DSpace, BinaryColor>;
type OledSurface = <OledUiSurfacePool as Surfaces<Matrix2DSpace>>::Surface; type OledSurface = <OledUiSurfacePool as Surfaces>::Surface;
pub type LockedUniforms = Arc<Mutex<CriticalSectionRawMutex, OledUniforms>>; pub type LockedUniforms = Arc<Mutex<CriticalSectionRawMutex, OledUniforms>>;
pub struct OledUI<S: Surface + core::fmt::Debug> { pub struct OledUI<S: Surface + core::fmt::Debug> {
overlay: S, overlay: S,
controls: DisplayControls, controls: DisplayControls<'static>,
uniforms: LockedUniforms uniforms: LockedUniforms
} }
@@ -32,12 +32,12 @@ impl Shader<OledUniforms, Matrix2DSpace, BinaryColor> for OverlayShader {
} }
impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = BinaryColor, Uniforms = OledUniforms>> OledUI<S> { impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = BinaryColor, Uniforms = OledUniforms>> OledUI<S> {
pub fn new<SS: Surfaces<Matrix2DSpace, Surface = S>>(surfaces: &mut SS, controls: DisplayControls, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug { pub fn new<SS: Surfaces<Surface = S>>(surfaces: &mut SS, controls: DisplayControls<'static>, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug {
Self { Self {
overlay: SurfaceBuilder::build(surfaces) overlay: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything()) .rect(Rectangle::everything())
.shader(OverlayShader{}) .shader(OverlayShader{})
.opacity(0) .opacity(Fract8::MIN)
.finish().unwrap(), .finish().unwrap(),
controls, controls,
uniforms uniforms
@@ -45,15 +45,15 @@ impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = Bina
} }
pub async fn screen_transition(&mut self, next_screen: Screen) { pub async fn screen_transition(&mut self, next_screen: Screen) {
const FADE_IN: Animation = Animation::new().from(0).to(255).duration(Duration::from_millis(300)); const FADE_IN: Animation<Fract8> = Animation::new().from(Fract8::MIN).to(Fract8::MAX).duration(Duration::from_millis(300));
const FADE_OUT: Animation = Animation::new().from(255).to(0).duration(Duration::from_millis(300)); const FADE_OUT: Animation<Fract8> = Animation::new().from(Fract8::MAX).to(Fract8::MIN).duration(Duration::from_millis(300));
info!("Fading in to screen {next_screen:?}"); info!("Fading in to screen {next_screen:?}");
FADE_IN.apply(&mut self.overlay).await; FADE_IN.apply([&mut self.overlay]).await;
{ {
let mut locked = self.uniforms.lock().await; let mut locked = self.uniforms.lock().await;
locked.current_screen = next_screen; locked.current_screen = next_screen;
} }
FADE_OUT.apply(&mut self.overlay).await; FADE_OUT.apply([&mut self.overlay]).await;
} }
pub async fn on_event(&mut self, event: Prediction) { pub async fn on_event(&mut self, event: Prediction) {
@@ -94,7 +94,6 @@ impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = Bina
#[embassy_executor::task] #[embassy_executor::task]
pub async fn oled_ui(mut events: DynSubscriber<'static, Prediction>, mut ui: OledUI<OledSurface>) { pub async fn oled_ui(mut events: DynSubscriber<'static, Prediction>, mut ui: OledUI<OledSurface>) {
ui.screen_transition(Screen::Bootsplash).await; ui.screen_transition(Screen::Bootsplash).await;
Timer::after_secs(3).await; Timer::after_secs(3).await;
ui.screen_transition(Screen::Home).await; ui.screen_transition(Screen::Home).await;
@@ -102,5 +101,4 @@ pub async fn oled_ui(mut events: DynSubscriber<'static, Prediction>, mut ui: Ole
loop { loop {
ui.on_event(events.next_message_pure().await).await; ui.on_event(events.next_message_pure().await).await;
} }
} }
+2 -2
View File
@@ -12,7 +12,7 @@ use crate::{backoff::Backoff, graphics::ssd1306::SsdOutput, tasks::oled::{Locked
pub async fn oled_render(mut output: SsdOutput, mut surfaces: OledUiSurfacePool, uniforms: LockedUniforms) { pub async fn oled_render(mut output: SsdOutput, mut surfaces: OledUiSurfacePool, uniforms: LockedUniforms) {
warn!("Starting OLED rendering task"); warn!("Starting OLED rendering task");
Backoff::from_secs(1).forever().attempt::<_, (), DisplayError>(async || { Backoff::from_secs(1).forever().attempt::<_, (), DisplayError>(async || {
const FPS: u64 = 30; const FPS: u64 = 15;
const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS); const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS);
const ANIMATION_TPS: u64 = 30; const ANIMATION_TPS: u64 = 30;
@@ -35,7 +35,7 @@ pub async fn oled_render(mut output: SsdOutput, mut surfaces: OledUiSurfacePool,
if frame_time < RENDER_BUDGET { if frame_time < RENDER_BUDGET {
Timer::after(RENDER_BUDGET - frame_time).await; Timer::after(RENDER_BUDGET - frame_time).await;
} else { } else {
//warn!("OLED Frame took too long to render! {}ms", frame_time.as_millis()); warn!("OLED Frame took too long to render! {}ms", frame_time.as_millis());
} }
} }
}).await.unwrap(); }).await.unwrap();
+37 -32
View File
@@ -1,34 +1,35 @@
use embassy_time::{Duration, Instant, Timer}; use embassy_time::{Duration, Instant, Timer};
use esp_hal::{gpio::AnyPin, rmt::Rmt, time::Rate, timer::timg::Wdt}; use esp_hal::spi::Mode;
use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync}; use esp_hal::spi::master::{AnySpi, Config, Spi};
use esp_hal::{gpio::AnyPin, time::Rate, timer::timg::Wdt};
use esp_hal::dma::{AnyGdmaChannel, DmaTxBuf};
use figments::prelude::*; use figments::prelude::*;
use figments_esp32_ws2812_dma::{DmaBuffers, Esp32Ws2812SpiDmaWriter};
use figments_render::gamma::GammaCurve; use figments_render::gamma::GammaCurve;
use figments_render::output::{GammaCorrected, OutputAsync}; use figments_render::output::{GammaCorrected, OutputAsync};
use log::{info, warn}; use log::*;
use crate::graphics::display::NUM_PIXELS; use crate::graphics::display::{NUM_PIXELS, RenderState};
use crate::{graphics::display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool}; use crate::{graphics::display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool};
static SPI_BUFFERS: static_cell::ConstStaticCell<DmaBuffers<u8, {NUM_PIXELS * 12 + 140}>> = static_cell::ConstStaticCell::new(DmaBuffers::new(0));
#[embassy_executor::task] #[embassy_executor::task]
pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'static>, mut surfaces: UiSurfacePool, mut safety_surfaces: UiSurfacePool, mut controls: DisplayControls, mut wdt: Wdt<esp_hal::peripherals::TIMG0<'static>>) { pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: AnyPin<'static>, mut surfaces: UiSurfacePool, mut safety_surfaces: UiSurfacePool, mut controls: DisplayControls<'static>, mut wdt: Wdt<esp_hal::peripherals::TIMG0<'static>>) {
let frequency: Rate = Rate::from_mhz(80); info!("Starting rendering task");
let rmt = Rmt::new(rmt, frequency)
.expect("Failed to initialize RMT").into_async();
let rmt_channel = rmt.channel0;
// FIXME: I don't know why we need to add an extra bunch of pixels, but otherwise the buffer is too small? let buffers = SPI_BUFFERS.take();
let mut rmt_buffer = esp_hal_smartled::smart_led_buffer!(NUM_PIXELS + 25);
// FIXME: The strip is GRB, we need to fix the pixel swizzling deep within the traits let tx_buf = DmaTxBuf::new(&mut buffers.tx_descriptors, &mut buffers.tx_buffer).unwrap();
let target = SmartLedsAdapterAsync::new_with_color(rmt_channel, gpio, &mut rmt_buffer); let conf = Config::default()
.with_frequency(Rate::from_khz(3_800))
.with_mode(Mode::_0);
// Change this to adjust the power available; the USB spec says 500ma is the standard limit, but sometimes you can draw more from a power brick let driver = Spi::new(spi, conf).unwrap()
const POWER_MA : u32 = 500; .with_mosi(gpio)
.with_dma(dma).into_async();
// You probably don't need to change these values, unless your LED strip is somehow not 5 volts let target = Esp32Ws2812SpiDmaWriter::new(driver, tx_buf);
const POWER_VOLTS : u32 = 5;
const MAX_POWER_MW : u32 = if cfg!(feature="max-usb-power") { u32::MAX } else { POWER_VOLTS * POWER_MA };
// This value is used as the 'seed' for rendering each frame, allowing us to do things like run the animation backwards, frames for double FPS, or even use system uptime for more human-paced animations // This value is used as the 'seed' for rendering each frame, allowing us to do things like run the animation backwards, frames for double FPS, or even use system uptime for more human-paced animations
let mut uniforms = Uniforms { let mut uniforms = Uniforms {
@@ -36,21 +37,18 @@ pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'stati
..Uniforms::default() ..Uniforms::default()
}; };
let mut output = BikeOutput::new(target, MAX_POWER_MW, controls.clone()); let mut output = BikeOutput::new(target, controls.clone());
output.set_gamma(GammaCurve::new(1.3)); output.set_gamma(GammaCurve::new(1.3));
info!("Rendering started! {}ms since boot", Instant::now().as_millis()); info!("Rendering started! {}ms since boot", Instant::now().as_millis());
controls.notify_render_is_running(true);
// TODO: The prediction engine should be able to scale the display FPS down for power saving sometimes let mut requested_fps= controls.fps() as u64;
const FPS: u64 = 80; let mut render_budget = Duration::from_millis(1000 / requested_fps);
const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS);
const ANIMATION_TPS: u64 = 120; const ANIMATION_TPS: u64 = 120;
const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS); const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS);
loop { loop {
// FIXME: need to put the rendering loop into a deep sleep when the display is off
let start = Instant::now(); let start = Instant::now();
output.blank(); output.blank();
@@ -59,7 +57,6 @@ pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'stati
surfaces.commit(); surfaces.commit();
safety_surfaces.commit(); safety_surfaces.commit();
surfaces.render_to(&mut output, &uniforms); surfaces.render_to(&mut output, &uniforms);
// TODO: We should split up the safety layers so they always have full power
safety_surfaces.render_to(&mut output, &uniforms); safety_surfaces.render_to(&mut output, &uniforms);
// Finally, write out the rendered frame // Finally, write out the rendered frame
@@ -67,27 +64,35 @@ pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'stati
let render_duration = Instant::now() - start; let render_duration = Instant::now() - start;
let next_fps = controls.fps() as u64;
if next_fps != requested_fps {
requested_fps = next_fps;
render_budget = Duration::from_millis(1000 / requested_fps);
info!("FPS changed to {requested_fps}");
}
if !controls.is_on() { if !controls.is_on() {
warn!("Renderer is sleeping zzzz"); warn!("Renderer is sleeping zzzz");
controls.notify_render_is_running(false);
output.blank(); output.blank();
output.commit_async().await.expect("Failed to commit low power frame"); output.commit_async().await.expect("Failed to commit low power frame");
wdt.disable(); wdt.disable();
controls.wait_until_display_is_on().await; controls.ack().await;
controls.wait_for_state(RenderState::On).await;
controls.ack().await;
wdt.feed(); wdt.feed();
wdt.enable(); wdt.enable();
warn!("Renderer is awake !!!!"); warn!("Renderer is awake !!!!");
controls.notify_render_is_running(true);
} }
// Apply the FPS cap where we sleep if we are rendering fast enough // Apply the FPS cap where we sleep if we are rendering fast enough
if render_duration < RENDER_BUDGET { if render_duration < render_budget {
let remaining_budget = RENDER_BUDGET - render_duration; let remaining_budget = render_budget - render_duration;
Timer::after(remaining_budget).await; Timer::after(remaining_budget).await;
} else { } else {
// Otherwise, we have a problem // Otherwise, we have a problem
// TODO: Automatically scale FPS back when this happens
warn!("Render stall! Frame took {}ms", render_duration.as_millis()); warn!("Render stall! Frame took {}ms", render_duration.as_millis());
// Kick the scheduler since it took so long
Timer::after_ticks(1).await;
} }
// TODO: Need a way to let the UI layers configure this // TODO: Need a way to let the UI layers configure this
+31 -39
View File
@@ -1,12 +1,12 @@
use embassy_sync::pubsub::DynSubscriber; use embassy_sync::pubsub::DynSubscriber;
use embassy_time::Duration; use embassy_time::Duration;
use figments::prelude::*; use figments::{liber8tion::interpolate::Fract8, prelude::*};
use figments_render::output::Brightness; use figments_render::output::Brightness;
use rgb::Rgba; use rgb::Rgba;
use core::fmt::Debug; use core::fmt::Debug;
use log::*; use log::*;
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, events::{Personality, Prediction}, graphics::{display::{DisplayControls, SegmentSpace, Uniforms}, shaders::*}, tasks::ui::UiSurfacePool}; use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, events::{Personality, Prediction}, graphics::{display::{DEFAULT_FPS, DisplayControls, LOW_POWER_FPS, SegmentSpace, Uniforms}, shaders::*}, tasks::ui::UiSurfacePool};
#[derive(Debug)] #[derive(Debug)]
pub struct SafetyUi<S: Surface> { pub struct SafetyUi<S: Surface> {
@@ -16,11 +16,11 @@ pub struct SafetyUi<S: Surface> {
// The overlay covers everything and is used to implement a power-on and power-off animation. // The overlay covers everything and is used to implement a power-on and power-off animation.
overlay: AnimatedSurface<S>, overlay: AnimatedSurface<S>,
display: DisplayControls display: DisplayControls<'static>
} }
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> SafetyUi<S> { impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> SafetyUi<S> {
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(surfaces: &mut SS, display: DisplayControls) -> Self where SS::Error: Debug { pub fn new<SS: Surfaces<Surface = S>>(surfaces: &mut SS, display: DisplayControls<'static>) -> Self where SS::Error: Debug {
let ret = Self { let ret = Self {
overlay: SurfaceBuilder::build(surfaces) overlay: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything()) .rect(Rectangle::everything())
@@ -31,13 +31,13 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
.rect(Rectangle::new_from_coordinates(0, 0, 255, 0)) .rect(Rectangle::new_from_coordinates(0, 0, 255, 0))
.shader(Headlight::default()) .shader(Headlight::default())
.visible(false) .visible(false)
.opacity(0) .opacity(Fract8::MIN)
.finish().unwrap().into(), .finish().unwrap().into(),
brakelight: SurfaceBuilder::build(surfaces) brakelight: SurfaceBuilder::build(surfaces)
.rect(Brakelight::rectangle()) .rect(Brakelight::rectangle())
.shader(Brakelight::default()) .shader(Brakelight::default())
.visible(false) .visible(false)
.opacity(0) .opacity(Fract8::MIN)
.finish().unwrap().into(), .finish().unwrap().into(),
display display
}; };
@@ -48,7 +48,7 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
pub async fn sleep(&mut self) { pub async fn sleep(&mut self) {
info!("Running sleep sequence"); info!("Running sleep sequence");
let mut disp_anim = AnimDisplay(&mut self.display); let mut disp_anim = AnimDisplay(&mut self.display);
TURN_OFF.apply(&mut disp_anim).await; TURN_OFF_FAST.apply([&mut disp_anim]).await;
warn!("Resetting safety lights"); warn!("Resetting safety lights");
self.brakelight.set_visible(false); self.brakelight.set_visible(false);
@@ -57,8 +57,7 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
warn!("Turning off display"); warn!("Turning off display");
self.display.set_on(false); self.display.set_on(false);
// Wait for the display hardware to actually turn off, before we return to process the next event, which could cause funky behaviors. // Wait for the display hardware to actually turn off, before we return to process the next event, which could cause funky behaviors.
// FIXME: also deadlocks :( self.display.wait_until_render_is_running().await;
//self.display.render_is_running.wait().await;
} }
pub async fn wake(&mut self) { pub async fn wake(&mut self) {
@@ -66,30 +65,25 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
info!("Turning on display"); info!("Turning on display");
// Turn on the display hardware // Turn on the display hardware
self.display.set_brightness(0); self.display.set_brightness(Fract8::MIN);
self.display.set_on(true); self.display.set_on(true);
// Wait for the renderer to start running again // Wait for the renderer to start running again
// FIXME: This deadlocks :( self.display.wait_for_ack(RenderState::On).await;
//self.display.render_is_running.wait().await;
let fade_in = Animation::default().duration(Duration::from_secs(3)).from(0).to(255);
trace!("Fading in brightness with overlay={:?}", self.overlay); trace!("Fading in brightness with overlay={:?}", self.overlay);
self.overlay.set_opacity(255); self.overlay.set_opacity(Fract8::MAX);
self.overlay.set_visible(true); self.overlay.set_visible(true);
fade_in.apply(&mut AnimDisplay(&mut self.display)).await; TURN_ON_SLOW.apply([&mut AnimDisplay(&mut self.display)]).await;
warn!("Turning on safety lights"); warn!("Turning on safety lights");
self.headlight.set_opacity(0); self.headlight.set_opacity(Fract8::MIN);
self.headlight.set_visible(true); self.headlight.set_visible(true);
self.brakelight.set_opacity(0); self.brakelight.set_opacity(Fract8::MIN);
self.brakelight.set_visible(true); self.brakelight.set_visible(true);
embassy_futures::join::join( TURN_ON_SLOW.apply([&mut self.headlight, &mut self.brakelight]).await;
fade_in.apply(&mut self.headlight),
fade_in.apply(&mut self.brakelight)
).await;
info!("Fade out overlay"); info!("Fade out overlay");
TURN_OFF.apply(&mut self.overlay).await; TURN_OFF_FAST.apply([&mut self.overlay]).await;
self.overlay.set_visible(false); self.overlay.set_visible(false);
info!("Wakeup complete!"); info!("Wakeup complete!");
} }
@@ -98,18 +92,20 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
if let Prediction::SetPersonality(personality) = event { match personality { if let Prediction::SetPersonality(personality) = event { match personality {
Personality::Active => { Personality::Active => {
// FIXME: These should be a Off/Low/High enum, so the stopping brake looks different from the dayrunning brake. // FIXME: These should be a Off/Low/High enum, so the stopping brake looks different from the dayrunning brake.
self.display.set_fps(DEFAULT_FPS);
warn!("Active personality: Turning on safety lights"); warn!("Active personality: Turning on safety lights");
embassy_futures::join::join( TURN_ON_FAST.apply([
TURN_ON.apply(&mut self.brakelight), &mut self.brakelight,
TURN_ON.apply(&mut self.headlight) &mut self.headlight
).await; ]).await;
}, },
Personality::Parked => { Personality::Parked => {
warn!("Idle personality: Turning off safety lights"); warn!("Idle personality: Turning off safety lights");
embassy_futures::join::join( TURN_OFF_FAST.apply([
TURN_OFF.apply(&mut self.brakelight), &mut self.brakelight,
TURN_OFF.apply(&mut self.headlight) &mut self.headlight
).await; ]).await;
self.display.set_fps(LOW_POWER_FPS);
}, },
Personality::Sleeping => { Personality::Sleeping => {
warn!("Sleeping personality: Safety UI is going to sleep"); warn!("Sleeping personality: Safety UI is going to sleep");
@@ -117,23 +113,19 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
}, },
Personality::Waking => { Personality::Waking => {
warn!("Waking personality: Waking up safety UI"); warn!("Waking personality: Waking up safety UI");
self.display.set_fps(DEFAULT_FPS);
self.wake().await; self.wake().await;
}, },
} } } }
} }
} }
const TURN_ON: Animation = Animation::new().duration(Duration::from_secs(1)).from(0).to(255); const TURN_ON_FAST: Animation<Fract8> = Animation::new().duration(Duration::from_secs(1)).from(Fract8::MIN).to(Fract8::MAX);
const TURN_OFF: Animation = Animation::new().duration(Duration::from_secs(1)).from(255).to(0); const TURN_OFF_FAST: Animation<Fract8> = Animation::new().duration(Duration::from_secs(1)).from(Fract8::MAX).to(Fract8::MIN);
const TURN_ON_SLOW: Animation<Fract8> = Animation::new().duration(Duration::from_secs(3)).from(Fract8::MIN).to(Fract8::MAX);
#[embassy_executor::task] #[embassy_executor::task]
pub async fn safety_ui_main(mut events: DynSubscriber<'static, Prediction>, mut ui: SafetyUi<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) { pub async fn safety_ui_main(mut events: DynSubscriber<'static, Prediction>, mut ui: SafetyUi<<UiSurfacePool as Surfaces>::Surface>) {
// Wait for the renderer to start running
//ui.display.render_is_running.wait().await;
trace!("spooling until render starts ui={ui:?}");
ui.display.wait_until_render_is_running().await;
trace!("spooling wait task ui={ui:?}");
// Run the wake sequence, and turn on the lights // Run the wake sequence, and turn on the lights
ui.wake().await; ui.wake().await;
+154
View File
@@ -0,0 +1,154 @@
use core::cell::RefCell;
use embassy_sync::pubsub::DynSubscriber;
use embedded_hal_bus::spi::RefCellDevice;
use embedded_sdmmc::{Mode, SdCard, TimeSource, VolumeIdx, VolumeManager};
use esp_hal::{delay::Delay, gpio::{AnyPin, Level, Output}, spi::{master::{AnySpi, Config, Spi}}, time::Rate};
use rmp::encode::{RmpWrite, RmpWriteErr};
use crate::{events::Measurement, gpio_interrupt::PinInterrupt, simdata::IMUReading, storage::SimDataRecorder};
use log::*;
struct SdTimeSource;
// TODSO: We need a whole time keeping stack that can sync from GPS or NTP
impl TimeSource for SdTimeSource {
fn get_timestamp(&self) -> embedded_sdmmc::Timestamp {
embedded_sdmmc::Timestamp::from_fat(0, 0)
}
}
#[allow(clippy::single_match)]
#[embassy_executor::task]
pub async fn sdcard_task(
spi: AnySpi<'static>,
sck: AnyPin<'static>,
mosi: AnyPin<'static>,
miso: AnyPin<'static>,
cs: Output<'static>,
sd_detect_interrupt: PinInterrupt<'static>,
mut recording_stream: DynSubscriber<'static, Measurement>
) {
let sd_spi = Spi::new(spi, Config::default().with_frequency(Rate::from_khz(400)).with_mode(esp_hal::spi::Mode::_0))
.unwrap()
.with_sck(sck)
.with_mosi(mosi)
.with_miso(miso)
.into_async();
let sd_ref = RefCell::new(sd_spi);
let sd_device = RefCellDevice::new(&sd_ref, cs, Delay::new()).unwrap();
let mut sd_card = SdCard::new(sd_device, Delay::new());
loop {
match sd_detect_interrupt.wait_for_interrupt().await {
Level::High => {
info!("SD card inserted!");
sd_detect_interrupt.listen();
},
Level::Low => {
info!("SD card removed!");
sd_detect_interrupt.listen();
continue;
}
}
sd_card.mark_card_uninit();
match sd_card.get_card_type() {
Some(card_type) => info!("SD card detected: {:?}", card_type),
None => {
error!("Failed to determine SD card type!");
continue;
}
}
let volumes = VolumeManager::new(sd_card, SdTimeSource);
let vol = volumes.open_volume(VolumeIdx(0)).unwrap();
let rootfs = vol.open_root_dir().unwrap();
let stream = rootfs.open_file_in_dir("renderbug-sensors.rmp",Mode::ReadWriteCreateOrTruncate).unwrap();
let rmp_stream = EmbeddedRmp(stream);
/*let mut recorder = SimDataStream::open(rmp_stream, StreamType::Bundle);
loop {
// FIXME: THis chunk could really go into an impl From<Measurement> for EventRecord
match recording_stream.next_message_pure().await {
Measurement::IMU { accel, gyro } => {
let reading = IMUReading {
accel_x: accel.x as f64,
accel_y: accel.y as f64,
accel_z: accel.z as f64,
gyro_x: gyro.x as f64,
gyro_y: gyro.y as f64,
gyro_z: gyro.z as f64
};
/*if recorder.write_next(reading).is_err() {
error!("Failed to write IMU reading to SD card");
break;
}*/
info!("Wrote IMU to SD card");
},
_ => ()
}
}
drop(recorder);*/
drop(rmp_stream);
drop(rootfs);
drop(vol);
sd_card = volumes.free().0;
}
}
struct EmbeddedRmp<W>(W);
#[derive(Debug)]
struct EmbeddedRmpError<T>(T);
impl<T: core::fmt::Debug + 'static> RmpWriteErr for EmbeddedRmpError<T> {
}
impl<T: core::fmt::Debug + 'static> RmpReadErr for EmbeddedRmpError<T> {
}
impl<T> core::fmt::Display for EmbeddedRmpError<T> where T: core::fmt::Debug {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl<W: embedded_io::Write> RmpWrite for EmbeddedRmp<W> where W::Error: core::fmt::Debug + 'static {
type Error = EmbeddedRmpError<W::Error>;
fn write_bytes(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
self.0.write(buf).map_err(|e| { EmbeddedRmpError(e)})?;
Ok(())
}
}
impl<W: embedded_io::Read> RmpRead for EmbeddedRmp<W> where W::Error: core::fmt::Debug + 'static {
type Error = EmbeddedRmpError<ReadExactError<W::Error>>;
fn read_exact_buf(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
self.0.read_exact(buf).map_err(|e| { EmbeddedRmpError(e) })?;
Ok(())
}
}
impl<W: embedded_io::Write> embedded_io::ErrorType for EmbeddedRmp<W> {
type Error = W::Error;
}
impl<W: embedded_io::Write> embedded_io::Write for EmbeddedRmp<W> {
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
self.0.write(buf)
}
fn flush(&mut self) -> Result<(), Self::Error> {
self.0.flush()
}
}
+165 -55
View File
@@ -1,23 +1,54 @@
use core::usize;
use embassy_sync::channel::DynamicSender; use embassy_sync::channel::DynamicSender;
use embassy_time::{Duration, Timer}; use embassy_time::{Duration, Timer};
use embedded_storage::ReadStorage; use embedded_storage::{ReadStorage, Storage};
use esp_bootloader_esp_idf::partitions::PartitionTable; use esp_bootloader_esp_idf::partitions::PartitionTable;
use esp_hal::time::Instant;
use esp_storage::FlashStorage; use esp_storage::FlashStorage;
use figments::liber8tion::interpolate::Fract8;
use nalgebra::{Vector2, Vector3}; use nalgebra::{Vector2, Vector3};
use log::*; use log::*;
use rmp::decode::ValueReadError; use rmp::{decode::{RmpRead, RmpReadErr, ValueReadError}, encode::{RmpWrite, RmpWriteErr, ValueWriteError}};
use crate::{Breaker, events::{Measurement, SensorSource, SensorState}, simdata::{AnnotationReading, EventRecord, EventStreamHeader, GPSReading, IMUReading, RmpData, SimDataError, StreamEvent, StreamHeader, StreamIndex, StreamType}, storage::{RangeReadError, RangeReader, SharedFlash}}; use crate::{Breaker, events::{Measurement, SensorSource, SensorState}, simdata::{AnnotationReading, BundleEventHeader, EventRecord, EventStreamHeader, GPSReading, IMUReading, RmpData, SimDataError, StreamEvent, StreamHeader, StreamIndex, StreamType}, storage::{SharedFlash, StorageRange, StorageRangeError}};
pub struct SimDataTable<S> { pub struct SimDataTable<S> {
reader: RangeReader<S>, reader: S,
count: usize, count: usize,
index: usize index: usize
} }
impl<S: ReadStorage + Clone> SimDataTable<S> where S::Error: core::fmt::Debug + 'static { impl<S: RmpRead<Error = E>, E: RmpReadErr + RmpWriteErr> SimDataTable<S> {
pub fn open(mut reader: S) -> Result<Self, SimDataError<E>> {
if let Ok(index) = StreamIndex::from_rmp(&mut reader) {
info!("Found stream index: {index:?}");
Ok(Self {
reader,
count: index.count,
index: 0
})
} else {
Err(SimDataError::StreamIndexMissing)
}
}
}
pub fn open(storage: S, partitions: PartitionTable<'_>) -> Result<Self, SimDataError<S>> { impl<S: RmpWrite<Error = E>, E: RmpReadErr + RmpWriteErr> SimDataTable<S> {
pub fn create(mut writer: S) -> Result<Self, SimDataError<E>> {
// FIXME: The stream header should use a fixed size integer for the count, instead of the dynamic array length type
info!("Writing new stream index");
StreamIndex { count: 0 }.write_rmp(&mut writer)?;
Ok(Self {
reader: writer,
count: 0,
index: 0
})
}
}
impl<S: ReadStorage<Error = E>, E: core::fmt::Debug + 'static> SimDataTable<StorageRange<S>> {
pub fn find_partition(storage: S, partitions: PartitionTable<'_>) -> Result<StorageRange<S>, SimDataError<StorageRangeError<E>>> {
let partition_type = esp_bootloader_esp_idf::partitions::PartitionType::Data( let partition_type = esp_bootloader_esp_idf::partitions::PartitionType::Data(
esp_bootloader_esp_idf::partitions::DataPartitionSubType::Undefined, esp_bootloader_esp_idf::partitions::DataPartitionSubType::Undefined,
); );
@@ -29,39 +60,53 @@ impl<S: ReadStorage + Clone> SimDataTable<S> where S::Error: core::fmt::Debug +
let start = data_partition.offset() as usize; let start = data_partition.offset() as usize;
let end = data_partition.len() as usize + start; let end = data_partition.len() as usize + start;
info!("Opening simulation data at {start:#02x}:{end:#02x}"); info!("Opening simulation data at {start:#02x}:{end:#02x}");
let mut reader = RangeReader::new(storage.clone(), start, end); Ok(StorageRange::new(storage, start, end))
if let Ok(index) = StreamIndex::from_rmp(&mut reader) {
info!("Found stream index: {index:?}");
Ok(Self {
reader,
count: index.count,
index: 0
})
} else {
error!("Stream index is missing! Have you flashed the partition yet?");
Err(SimDataError::StreamIndexMissing)
}
} }
} }
impl<S: ReadStorage + Clone + core::fmt::Debug> Iterator for SimDataTable<S> where S::Error: core::fmt::Debug + 'static { impl<S: Storage<Error = E> + Clone + core::fmt::Debug, E: core::fmt::Debug + 'static> SimDataTable<StorageRange<S>> {
type Item = SimDataReader<S>; pub fn append_new_stream(&mut self, streamid: StreamType) -> Result<StorageRange<S>, SimDataError<StorageRangeError<E>>> {
let stream_size = self.reader.capacity() - self.reader.pos() - 6;
if stream_size < 1024 {
error!("Not enough space left in the simulation data partition to create a new stream!");
return Err(SimDataError::NotEnoughSpace);
}
StreamHeader { id: streamid, size: stream_size }.write_rmp(&mut self.reader)?;
self.count += 1;
self.index += 1;
let reset_pos = self.reader.pos();
self.reader.seek_abs(0).unwrap();
StreamIndex { count: self.count }.write_rmp(&mut self.reader)?;
self.reader.seek_abs(reset_pos).unwrap();
info!("Creating new stream id={streamid:?} capacity={stream_size:#02x} range={:#02x}:{:#02x}", self.reader.abs_start() + self.reader.pos(), self.reader.abs_start() + stream_size);
Ok(self.reader.subset(stream_size).unwrap())
}
}
impl<S: ReadStorage<Error = E> + Clone + core::fmt::Debug, E: core::fmt::Debug + 'static> Iterator for SimDataTable<StorageRange<S>> {
type Item = (StreamHeader, StorageRange<S>);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.count { if self.index >= self.count {
None None
} else { } else {
loop { loop {
let reset_pos = self.reader.pos();
match StreamHeader::from_rmp(&mut self.reader) { match StreamHeader::from_rmp(&mut self.reader) {
Ok(header) => { Ok(header) => {
match self.reader.subset(header.size) { info!("Found stream header: {header:?}");
let stream_size = if header.size == 0 { self.reader.capacity() - self.reader.pos() } else { header.size };
match self.reader.subset(stream_size) {
Ok(sensor_reader) => { Ok(sensor_reader) => {
self.reader.seek(header.size).unwrap_or_else(|err| { self.reader.seek(stream_size).unwrap_or_else(|err| {
error!("Simulation data appears bigger than the storage capacity: {err:?}") error!("Simulation data appears bigger than the storage capacity: {err:?}")
}); });
self.index += 1; self.index += 1;
debug!("Found header={header:?}"); debug!("Found header={header:?}");
return Some(SimDataReader::open(sensor_reader, header.id)); return Some((header, sensor_reader));
}, },
err => { err => {
error!("Could not open the next simulation data chunk: {err:?}"); error!("Could not open the next simulation data chunk: {err:?}");
@@ -78,6 +123,8 @@ impl<S: ReadStorage + Clone + core::fmt::Debug> Iterator for SimDataTable<S> whe
} }
Err(err) => { Err(err) => {
error!("Read error while reading next chunk in simulation stream table {err:?}"); error!("Read error while reading next chunk in simulation stream table {err:?}");
// Reset back to right before we tried reading the stream header, so that append_new_stream() places it correctly.
self.reader.seek_abs(reset_pos).unwrap();
return None; return None;
} }
} }
@@ -86,10 +133,11 @@ impl<S: ReadStorage + Clone + core::fmt::Debug> Iterator for SimDataTable<S> whe
} }
} }
pub struct SimDataReader<S> { pub struct SimDataStream<S> {
reader: RangeReader<S>, reader: S,
srcid: SensorSource, srcid: StreamType,
runtime: Duration, last_stamp: Instant,
//runtime: Duration,
event_count: usize, event_count: usize,
index: usize index: usize
} }
@@ -111,79 +159,141 @@ impl From<GPSReading> for Measurement {
impl From<AnnotationReading> for Measurement { impl From<AnnotationReading> for Measurement {
fn from(value: AnnotationReading) -> Self { fn from(value: AnnotationReading) -> Self {
warn!("ANNOTATION: {}", core::str::from_utf8(&value.buf).unwrap()); Measurement::Annotation(value.buf)
Measurement::Annotation
} }
} }
impl<S: ReadStorage> SimDataReader<S> where S::Error: core::fmt::Debug + 'static { impl<S> SimDataStream<S> {
pub fn open(mut reader: RangeReader<S>, stream_type: StreamType) -> Self { pub fn srcid(&self) -> StreamType {
debug!("Opening {stream_type:?} sim data chunk"); self.srcid
}
}
pub trait Checkpoint {
fn checkpoint(&mut self);
fn rollback(&mut self);
}
impl<S: Checkpoint + RmpRead<Error = E>, E: RmpWriteErr + RmpReadErr> SimDataStream<S> {
pub fn open(mut reader: S, stream_type: StreamType) -> Self {
info!("Opening {stream_type:?} sim data chunk");
let event_count = EventStreamHeader::from_rmp(&mut reader).unwrap().count; let event_count = EventStreamHeader::from_rmp(&mut reader).unwrap().count;
debug!("Found {event_count} events!"); info!("Found {event_count} events!");
Self { Self {
reader, reader,
srcid: stream_type.into(), srcid: stream_type,
runtime: Default::default(), //runtime: Default::default(),
last_stamp: Instant::now(),
event_count, event_count,
index: 0 index: 0
} }
} }
pub fn srcid(&self) -> SensorSource { fn read_next_event<T: EventRecord + Into<Measurement>>(&mut self) -> Result<(Duration, Measurement), SimDataError<E>> {
self.srcid
}
async fn read_next_event<T: EventRecord + Into<Measurement>>(&mut self) -> Result<Measurement, SimDataError<ValueReadError<RangeReadError<S::Error>>>> {
let event = StreamEvent::<T>::from_rmp(&mut self.reader)?; let event = StreamEvent::<T>::from_rmp(&mut self.reader)?;
let delay = embassy_time::Duration::from_millis((event.timecode * 1000.0) as u64); let delay = embassy_time::Duration::from_millis((event.timecode * 1000.0) as u64);
self.runtime += delay; Ok((delay, event.data.into()))
Timer::after(delay).await;
Ok(event.data.into())
} }
pub async fn read_next(&mut self) -> Result<Option<Measurement>, SimDataError<ValueReadError<RangeReadError<S::Error>>>> { pub fn read_next(&mut self) -> Result<Option<(Duration, Measurement)>, SimDataError<E>> {
if self.index < self.event_count { if self.index < self.event_count {
self.index += 1; self.index += 1;
self.reader.checkpoint();
let next_id = match self.srcid {
StreamType::Bundle => BundleEventHeader::from_rmp(&mut self.reader).inspect_err(|_| { self.reader.rollback() })?.id,
_ => self.srcid
};
// The read_* functions can only ever return a valid result, or a data/reading error, so we map them into a Some() // The read_* functions can only ever return a valid result, or a data/reading error, so we map them into a Some()
Ok(Some(match self.srcid { let (delay, evt) = match next_id {
SensorSource::IMU => self.read_next_event::<IMUReading>().await?, StreamType::IMU => self.read_next_event::<IMUReading>().inspect_err(|_| { self.reader.rollback() })?,
SensorSource::GPS => self.read_next_event::<GPSReading>().await?, StreamType::GPS => self.read_next_event::<GPSReading>().inspect_err(|_| { self.reader.rollback() })?,
SensorSource::Annotations => self.read_next_event::<AnnotationReading>().await?, StreamType::Annotations => self.read_next_event::<AnnotationReading>().inspect_err(|_| { self.reader.rollback() })?,
srcid => unimplemented!("{srcid:?} is not a simulatable sensor yet!") srcid => unimplemented!("{srcid:?} is not a simulatable sensor yet!")
})) };
Ok(Some((delay, evt)))
} else {
Ok(None)
}
}
pub async fn read_next_and_wait(&mut self) -> Result<Option<Measurement>, SimDataError<E>> {
if let Some((delay, evt)) = self.read_next()? {
trace!("waiting {delay}");
Timer::after(delay).await;
self.last_stamp = Instant::now();
Ok(Some(evt))
} else { } else {
Ok(None) Ok(None)
} }
} }
} }
impl<S: Storage<Error = SE>, SE: core::fmt::Debug + 'static> SimDataStream<StorageRange<S>> {
pub fn write_next<T: EventRecord>(&mut self, event: T) -> Result<(), SimDataError<StorageRangeError<SE>>> {
BundleEventHeader { id: T::stream_id() }.write_rmp(&mut self.reader)?;
let now = Instant::now();
let event = StreamEvent {
data: event,
timecode: (now - self.last_stamp).as_millis() as f64 / 1000.0
};
event.write_rmp(&mut self.reader)?;
self.last_stamp = now;
self.event_count = self.event_count.checked_add(1).ok_or(SimDataError::NotEnoughSpace)?;
self.write_header()?;
Ok(())
}
fn write_header(&mut self) -> Result<(), SimDataError<StorageRangeError<SE>>> {
let reset_pos = self.reader.pos();
self.reader.seek_abs(0).unwrap();
trace!("Writing event stream header with count={} to {:#x}", self.event_count, self.reader.abs_start());
EventStreamHeader { count: self.event_count }.write_rmp(&mut self.reader)?;
self.reader.seek_abs(reset_pos).unwrap();
Ok(())
}
pub fn create(mut storage: StorageRange<S>, stream_type: StreamType) -> Self {
storage.seek_abs(0).unwrap();
EventStreamHeader { count: 0 }.write_rmp(&mut storage).unwrap();
Self {
reader: storage,
srcid: stream_type,
//runtime: Default::default(),
last_stamp: Instant::now(),
event_count: 0,
index: 0
}
}
}
#[embassy_executor::task(pool_size = 3)] #[embassy_executor::task(pool_size = 3)]
pub async fn simulation_task(mut reader: SimDataReader<SharedFlash<FlashStorage>>, events: DynamicSender<'static, Measurement>) { pub async fn simulation_task(mut reader: SimDataStream<StorageRange<SharedFlash<FlashStorage<'static>>>>, events: DynamicSender<'static, Measurement>) {
warn!("Starting simulation for {:?}", reader.srcid()); warn!("Starting simulation for {:?}", reader.srcid());
events.send(Measurement::SensorHardwareStatus(SensorSource::Simulation, SensorState::AcquiringFix)).await; events.send(Measurement::SensorHardwareStatus(SensorSource::Simulation, SensorState::AcquiringFix)).await;
events.send(Measurement::SensorHardwareStatus(reader.srcid(), SensorState::Online)).await; //events.send(Measurement::SensorHardwareStatus(reader.srcid().into(), SensorState::Online)).await;
let mut idx = 0; let mut idx = 0;
let mut idx_breaker = Breaker::default(); let mut idx_breaker = Breaker::default();
// TODO: SimulationProgress updates // TODO: SimulationProgress updates
loop { loop {
match reader.read_next().await { match reader.read_next_and_wait().await {
Ok(Some(next_evt)) => { Ok(Some(next_evt)) => {
events.send(next_evt).await; events.send(next_evt).await;
let pct = (idx as f32) / (reader.event_count as f32); let pct = (idx as f32) / (reader.event_count as f32);
idx_breaker.set((pct * 255.0) as u8); idx_breaker.set((pct * 255.0) as u8);
if let Some(pct) = idx_breaker.read_tripped() { if let Some(pct) = idx_breaker.read_tripped() {
events.send(Measurement::SimulationProgress(reader.srcid(), reader.runtime, pct)).await; //events.send(Measurement::SimulationProgress(reader.srcid().into(), reader.runtime, Fract8::from_raw(pct))).await;
} }
idx += 1; idx += 1;
}, },
Ok(None) => { Ok(None) => {
warn!("End of simulation data stream"); warn!("End of simulation data stream");
break break
} },
Err(err) => { Err(err) => {
warn!("Error during sensor stream: {err:?}"); warn!("Error during sensor stream: {err:?}");
break break
@@ -191,7 +301,7 @@ pub async fn simulation_task(mut reader: SimDataReader<SharedFlash<FlashStorage>
} }
} }
events.send(Measurement::SensorHardwareStatus(reader.srcid(), SensorState::Offline)).await; //events.send(Measurement::SensorHardwareStatus(reader.srcid().into(), SensorState::Offline)).await;
events.send(Measurement::SensorHardwareStatus(SensorSource::Simulation, SensorState::Degraded)).await; events.send(Measurement::SensorHardwareStatus(SensorSource::Simulation, SensorState::Degraded)).await;
warn!("End of simulation for {:?}", reader.srcid()); warn!("End of simulation for {:?}", reader.srcid());
+33 -42
View File
@@ -1,13 +1,25 @@
use embassy_sync::pubsub::DynSubscriber; use embassy_sync::pubsub::DynSubscriber;
use embassy_time::{Duration, Timer}; use embassy_time::{Duration, Timer};
use figments::prelude::*; use figments::{liber8tion::interpolate::Fract8, prelude::*};
use rgb::{Rgb, Rgba}; use rgb::{Rgb, Rgba};
use core::fmt::Debug; use core::fmt::Debug;
use futures::join;
use log::*; use log::*;
use crate::{animation::{AnimatedSurface, Animation}, ego::engine::MotionState, events::{Personality, Prediction, Scene, SensorSource, SensorState}, graphics::{display::{SegmentSpace, Uniforms}, shaders::*}}; use crate::{animation::{AnimatedSurface, Animation}, ego::engine::MotionState, events::{Personality, Prediction, Scene, SensorSource, SensorState}, graphics::{display::{SegmentSpace, Uniforms}, shaders::*}};
const NOTIFY_FADE_IN: Animation<Fract8> = Animation::new().duration(Duration::from_millis(30)) .from(Fract8::MIN).to(Fract8::MAX);
const NOTIFY_PULSE_OUT: Animation<Fract8> = Animation::new().duration(Duration::from_millis(10)).from(Fract8::MAX).to(Fract8::from_raw(60));
const NOTIFY_PULSE_IN: Animation<Fract8> = Animation::new().duration(Duration::from_millis(10)).from(Fract8::MIN).to(Fract8::MAX);
const NOTIFY_FADE_OUT: Animation<Fract8> = Animation::new().duration(Duration::from_millis(750)) .from(Fract8::MAX).to(Fract8::MIN);
const FG_FADE_OUT: Animation<Fract8> = Animation::new().duration(Duration::from_millis(300)).to(Fract8::MIN);
const BG_FADE_IN: Animation<Fract8> = Animation::new().duration(Duration::from_millis(300)).to(Fract8::from_raw(128));
const TAIL: Animation<Fract8> = Animation::new().duration(Duration::from_millis(300)).to(Fract8::from_raw(96));
const PANELS: Animation<Fract8> = Animation::new().duration(Duration::from_millis(300)).to(Fract8::from_raw(128));
const BG: Animation<Fract8> = Animation::new().duration(Duration::from_millis(300)).to(Fract8::from_raw(32));
const MOTION: Animation<Fract8> = Animation::new().duration(Duration::from_secs(1)) .to(Fract8::MIN);
pub struct Ui<S: Surface> { pub struct Ui<S: Surface> {
// Background layer provides an always-running background for everything to draw on // Background layer provides an always-running background for everything to draw on
background: AnimatedSurface<S>, background: AnimatedSurface<S>,
@@ -23,7 +35,7 @@ pub struct Ui<S: Surface> {
} }
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> Ui<S> { impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> Ui<S> {
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(surfaces: &mut SS) -> Self where SS::Error: Debug { pub fn new<SS: Surfaces<Surface = S>>(surfaces: &mut SS) -> Self where SS::Error: Debug {
Self { Self {
background: SurfaceBuilder::build(surfaces) background: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything()) .rect(Rectangle::everything())
@@ -54,25 +66,21 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
} }
pub async fn flash_notification_color(&mut self, color: Rgb<u8>) { pub async fn flash_notification_color(&mut self, color: Rgb<u8>) {
let fade_in = Animation::default().from(0).to(255).duration(Duration::from_millis(30));
let pulse_out = Animation::default().from(255).to(60).duration(Duration::from_millis(100));
let pulse_in = Animation::default().from(0).to(255).duration(Duration::from_millis(100));
let fade_out = Animation::default().from(255).to(0).duration(Duration::from_secs(2));
info!("Flashing notification {color}"); info!("Flashing notification {color}");
self.notification.set_visible(true); self.notification.set_visible(true);
self.notification.set_shader(Background::from_color(color)); self.notification.set_shader(Background::from_color(color));
fade_in.apply(&mut self.notification).await; NOTIFY_FADE_IN.apply([&mut self.notification]).await;
// Pulse quickly 5 times // Pulse quickly 5 times
for _ in 0..5 { for _ in 0..5 {
pulse_out.apply(&mut self.notification).await; NOTIFY_PULSE_OUT.apply([&mut self.notification]).await;
pulse_in.apply(&mut self.notification).await; NOTIFY_PULSE_IN.apply([&mut self.notification]).await;
} }
fade_out.apply(&mut self.notification).await; NOTIFY_FADE_OUT.apply([&mut self.notification]).await;
self.notification.set_visible(false); self.notification.set_visible(false);
} }
@@ -90,56 +98,42 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
self.as_slice().set_visible(true); self.as_slice().set_visible(true);
} }
// TODO: Brakelight should only be toggled when actually braking or stationary
pub async fn apply_scene(&mut self, next_scene: Scene) { pub async fn apply_scene(&mut self, next_scene: Scene) {
info!("Activating scene {next_scene:?}"); info!("Activating scene {next_scene:?}");
match next_scene { match next_scene {
Scene::Ready => { Scene::Ready => {
let tail = Animation::default().duration(Duration::from_millis(300)).to(96);
let panels = Animation::default().duration(Duration::from_millis(300)).to(128);
let bg = Animation::default().duration(Duration::from_millis(300)).to(32);
let motion = Animation::default().duration(Duration::from_secs(1)).to(0);
embassy_futures::join::join4( embassy_futures::join::join4(
tail.apply(&mut self.tail), TAIL.apply([&mut self.tail]),
panels.apply(&mut self.panels), PANELS.apply([&mut self.panels]),
bg.apply(&mut self.background), BG.apply([&mut self.background]),
motion.apply(&mut self.motion) MOTION.apply([&mut self.motion])
).await; ).await;
self.background.set_shader(Background::default()); self.background.set_shader(Background::default());
}, },
Scene::Idle => { Scene::Idle => {
// FIXME: The safety UI task should handle setting the display brightness to 50% here
self.background.set_shader(Thinking::default()); self.background.set_shader(Thinking::default());
let fg_fade = Animation::default().duration(Duration::from_millis(300)).to(0);
let bg_fade = Animation::default().duration(Duration::from_millis(300)).to(128);
// FIXME: The scenes shouldn't be touching the brake/headlights at all here. In fact, they should be dealt with in a whole separate task from the main UI, maybe running on the motion prediction executor embassy_futures::join::join(
embassy_futures::join::join4( FG_FADE_OUT.apply([&mut self.tail, &mut self.panels, &mut self.motion]),
fg_fade.apply(&mut self.tail), BG_FADE_IN.apply([&mut self.background])
fg_fade.apply(&mut self.panels),
bg_fade.apply(&mut self.background),
fg_fade.apply(&mut self.motion)
).await; ).await;
}, },
Scene::Accelerating => { Scene::Accelerating => {
self.motion.set_shader(Movement::default()); self.motion.set_shader(Movement::default());
Animation::default().duration(Duration::from_secs(1)).to(255).apply(&mut self.motion).await; Animation::default().duration(Duration::from_secs(1)).to(Fract8::MAX).apply([&mut self.motion]).await;
}, },
Scene::Decelerating => { Scene::Decelerating => {
self.motion.set_shader(Movement::default().reversed()); self.motion.set_shader(Movement::default().reversed());
Animation::default().duration(Duration::from_secs(1)).to(255).apply(&mut self.motion).await; Animation::default().duration(Duration::from_secs(1)).to(Fract8::MAX).apply([&mut self.motion]).await;
} }
} }
} }
pub async fn on_event(&mut self, event: Prediction) { pub async fn on_event(&mut self, event: Prediction) {
match event { match event {
Prediction::SetPersonality(personality) => match personality { Prediction::SetPersonality(Personality::Active) => self.apply_scene(Scene::Ready).await,
Personality::Active => self.apply_scene(Scene::Ready).await, Prediction::SetPersonality(Personality::Parked) => self.apply_scene(Scene::Idle).await,
Personality::Parked => self.apply_scene(Scene::Idle).await, Prediction::SetPersonality(Personality::Waking) => self.show().await,
Personality::Waking => self.show().await,
_ => ()
},
Prediction::Motion { prev: _, next: MotionState::Accelerating } => self.apply_scene(Scene::Accelerating).await, Prediction::Motion { prev: _, next: MotionState::Accelerating } => self.apply_scene(Scene::Accelerating).await,
Prediction::Motion { prev: _, next: MotionState::Decelerating } => self.apply_scene(Scene::Decelerating).await, Prediction::Motion { prev: _, next: MotionState::Decelerating } => self.apply_scene(Scene::Decelerating).await,
Prediction::Motion { prev: _, next: MotionState::Steady } => self.apply_scene(Scene::Ready).await, Prediction::Motion { prev: _, next: MotionState::Steady } => self.apply_scene(Scene::Ready).await,
@@ -180,7 +174,7 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
self.as_slice().set_rect(rect); self.as_slice().set_rect(rect);
} }
fn set_opacity(&mut self, opacity: u8) { fn set_opacity(&mut self, opacity: Fract8) {
self.as_slice().set_opacity(opacity); self.as_slice().set_opacity(opacity);
} }
@@ -193,13 +187,10 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
} }
} }
#[cfg(not(feature="real-output"))]
pub type UiSurfacePool = NullBufferPool<Uniforms, SegmentSpace, Rgba<u8>>;
#[cfg(feature="real-output")]
pub type UiSurfacePool = BufferedSurfacePool<Uniforms, SegmentSpace, Rgba<u8>>; pub type UiSurfacePool = BufferedSurfacePool<Uniforms, SegmentSpace, Rgba<u8>>;
#[embassy_executor::task] #[embassy_executor::task]
pub async fn ui_main(mut events: DynSubscriber<'static, Prediction>, mut ui: Ui<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) { pub async fn ui_main(mut events: DynSubscriber<'static, Prediction>, mut ui: Ui<<UiSurfacePool as Surfaces>::Surface>) {
// FIXME: This should instead wait on some kind of flag set by the safety UI, or else we risk painting before we even have a display up and running // FIXME: This should instead wait on some kind of flag set by the safety UI, or else we risk painting before we even have a display up and running
Timer::after_secs(3).await; Timer::after_secs(3).await;
ui.show().await; ui.show().await;
+52
View File
@@ -0,0 +1,52 @@
use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice;
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::DynamicSender};
use esp_hal::Async;
use figments_render::power::Milliwatts;
use log::*;
use crate::{events::{Measurement, SensorSource, SensorState}, gpio_interrupt::PinInterrupt, graphics::display::DisplayControls, tusb320::{ActiveCableConnected, DEVICE_ID, InterruptStatus, TUSB320}};
#[embassy_executor::task]
pub async fn usb_task(motion: DynamicSender<'static, Measurement>, bus: I2cDevice<'static, NoopRawMutex, esp_hal::i2c::master::I2c<'static, Async>>, interrupt_pin: PinInterrupt<'static>, mut display_control: DisplayControls<'static>) {
let mut tusb = TUSB320::new(bus);
motion.send(Measurement::SensorHardwareStatus(SensorSource::ExternalPower, SensorState::AcquiringFix)).await;
match tusb.get_device_id().await {
Ok(DEVICE_ID) => {
info!("TUSB320 connected");
motion.send(Measurement::SensorHardwareStatus(SensorSource::ExternalPower, SensorState::Degraded)).await;
},
Ok(val) => {
error!("Unexpected TUSB320 Device ID: {val:?}! Power management is disabled!!");
motion.send(Measurement::SensorHardwareStatus(SensorSource::ExternalPower, SensorState::Offline)).await;
return;
},
Err(e) => {
error!("Failed to read from TUSB320! Power management is disabled!! {e:?}");
motion.send(Measurement::SensorHardwareStatus(SensorSource::ExternalPower, SensorState::Offline)).await;
return;
}
}
loop {
match tusb.active_cable_connected().read().await.unwrap() {
ActiveCableConnected::Connected => {
let current_mode = tusb.current_mode_detect().read().await.unwrap();
let cur_mw = Milliwatts(current_mode.milliwatts());
info!("USB reports current mode: {current_mode:?}, {cur_mw:?} mW");
display_control.set_max_power(cur_mw);
motion.send(Measurement::ExternalPower(cur_mw)).await;
motion.send(Measurement::SensorHardwareStatus(SensorSource::ExternalPower, SensorState::Online)).await;
},
ActiveCableConnected::Disconnected => {
warn!("USB power is disconnected! Setting display power to zero.");
// FIXME: We should also send an event through the system to report that the power is down, and the renderer should stop
display_control.set_max_power(Milliwatts(0));
motion.send(Measurement::ExternalPower(Milliwatts(0))).await;
motion.send(Measurement::SensorHardwareStatus(SensorSource::ExternalPower, SensorState::Degraded)).await;
}
}
interrupt_pin.wait_for_interrupt().await;
tusb.interrupt_status().write(InterruptStatus::Clear).await.ok();
}
}
+89 -52
View File
@@ -1,7 +1,8 @@
use alloc::string::ToString; use alloc::string::ToString;
use embassy_sync::channel::DynamicSender; use embassy_sync::channel::DynamicSender;
use embassy_sync::pubsub::DynSubscriber; use embassy_sync::pubsub::DynSubscriber;
use embassy_time::{Duration, Instant, WithTimeout}; use embassy_sync::watch::{DynReceiver, DynSender};
use embassy_time::{Duration, Instant, Timer, WithTimeout};
use esp_hal::rng::Rng; use esp_hal::rng::Rng;
use esp_radio::wifi::{ClientConfig, ScanConfig, WifiController, WifiDevice, WifiEvent}; use esp_radio::wifi::{ClientConfig, ScanConfig, WifiController, WifiDevice, WifiEvent};
use log::*; use log::*;
@@ -23,58 +24,62 @@ pub async fn net_task(mut runner: embassy_net::Runner<'static, WifiDevice<'stati
runner.run().await runner.run().await
} }
pub fn config_for_network(name: &str) -> Option<ClientConfig> {
Some(match name {
"The Frequency" => Some(ClientConfig::default().with_password("thepasswordkenneth".to_string())),
"The Frozen Throne" => Some(ClientConfig::default().with_password("IronEmpress420".to_string())),
_ => None
}?.with_ssid(name.to_string()))
}
#[embassy_executor::task] #[embassy_executor::task]
pub async fn wifi_connect_task(mut wifi: WifiController<'static>, stack: Stack<'static>, motion: DynamicSender<'static, Measurement>) { pub async fn wifi_connect_task(mut wifi: WifiController<'static>, motion: DynamicSender<'static, Measurement>) {
wifi.set_config(&esp_radio::wifi::ModeConfig::Client(
ClientConfig::default()
.with_ssid("The Frequency".to_string())
.with_auth_method(esp_radio::wifi::AuthMethod::Wpa2Personal)
.with_password("thepasswordkenneth".to_string())
)).unwrap();
wifi.set_mode(esp_radio::wifi::WifiMode::Sta).unwrap(); wifi.set_mode(esp_radio::wifi::WifiMode::Sta).unwrap();
wifi.set_power_saving(esp_radio::wifi::PowerSaveMode::Maximum).unwrap(); wifi.start_async().with_timeout(Duration::from_secs(30)).await.unwrap().unwrap();
Timer::after_secs(2).await;
// TODO: need a way to stop wifi after, say, 30 seconds of being unable to connect, or the system goes to sleep
loop { loop {
Backoff::from_secs(3).forever().attempt(async || { info!("Connecting to wifi...");
info!("Connecting to wifi..."); motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, crate::events::SensorState::AcquiringFix)).await;
wifi.start_async().await.unwrap(); let networks = wifi.scan_with_config_async(ScanConfig::default()).await.unwrap();
motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, crate::events::SensorState::AcquiringFix)).await; for network in networks {
let networks = wifi.scan_with_config_async(ScanConfig::default().with_show_hidden(true)).await.unwrap(); info!("wifi: {} @ {}db", network.ssid, network.signal_strength);
for network in networks { if let Some(cfg) = config_for_network(&network.ssid) {
info!("wifi: {} @ {}db", network.ssid, network.signal_strength); wifi.set_config(&esp_radio::wifi::ModeConfig::Client(cfg)).unwrap();
} if wifi.connect_async().await.is_err() {
match wifi.connect_async().await { error!("Unable to connect to wifi {network:?}");
Ok(_) => Ok(()),
Err(e) => {
error!("Unable to connect to wifi: {e:?}");
wifi.stop_async().await.unwrap();
Err(())
} }
break;
} }
}).await.unwrap();
info!("Waiting for DHCP...");
motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, SensorState::Degraded)).await;
if stack.wait_config_up().with_timeout(Duration::from_secs(5)).await.is_ok() {
info!("Online!");
motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, SensorState::Online)).await;
let ip_cfg = stack.config_v4().unwrap();
info!("ip={ip_cfg:?}");
wifi.wait_for_event(WifiEvent::ApStaDisconnected).await;
info!("Wifi disconnected!");
} else {
warn!("DHCP timed out after 5 seconds. Disconnecting wifi");
wifi.disconnect_async().await.unwrap();
} }
warn!("Stopping wifi device");
wifi.stop_async().await.unwrap(); if wifi.is_connected().unwrap() {
motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, SensorState::Offline)).await; info!("Wifi is online!");
motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, crate::events::SensorState::Online)).await;
wifi.wait_for_event(WifiEvent::StaDisconnected).await;
error!("Wifi disconnected");
} else {
info!("Wifi is offline.");
motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, crate::events::SensorState::Offline)).await;
Timer::after_secs(30).await;
}
}
}
#[embassy_executor::task]
pub async fn location_sampler(mut predictions: DynSubscriber<'static, Prediction>, tx: DynSender<'static, Prediction>) {
loop {
if let Prediction::Location(coords) = predictions.next_message_pure().await {
debug!("New location prediction: {coords}");
tx.send(Prediction::Location(coords));
}
} }
} }
// TODO: Wifi task needs to know when there is data to upload, so it only connects when needed. // TODO: Wifi task needs to know when there is data to upload, so it only connects when needed.
#[embassy_executor::task] #[embassy_executor::task]
pub async fn http_telemetry_task(mut predictions: DynSubscriber<'static, Prediction>, stack: Stack<'static>) { pub async fn http_telemetry_task(mut latest_prediction: DynReceiver<'static, Prediction>, stack: Stack<'static>, motion: DynamicSender<'static, Measurement>) {
// TODO: should wait for wifi disconnect event somehow and use that to restart sending the wifi around // TODO: should wait for wifi disconnect event somehow and use that to restart sending the wifi around
let seed = Rng::new().random() as i32; let seed = Rng::new().random() as i32;
@@ -97,18 +102,30 @@ pub async fn http_telemetry_task(mut predictions: DynSubscriber<'static, Predict
let mut last_push = Instant::from_ticks(0); let mut last_push = Instant::from_ticks(0);
loop { loop {
if let Prediction::Location(coords) = predictions.next_message_pure().await { motion.send(Measurement::SensorHardwareStatus(SensorSource::Cloud, SensorState::Offline)).await;
stack.wait_config_up().await;
info!("Network is up, connecting to nextcloud...");
motion.send(Measurement::SensorHardwareStatus(SensorSource::Cloud, SensorState::AcquiringFix)).await;
if stack.is_config_up() { if let Err(err) = ping_nextcloud(&mut client).await {
// Only push to HTTP if we have an ip config etc error!("Could not ping nextcloud: {err:?}");
if last_push.elapsed().as_secs() >= 5 || gps_to_local_meters_haversine(&last_location, &coords).norm() >= 10.0 { continue;
last_location = coords; }
last_push = Instant::now();
if let Err(e) = Backoff::from_secs(3).attempt(async || { motion.send(Measurement::SensorHardwareStatus(SensorSource::Cloud, SensorState::Online)).await;
push_location(&mut client, coords, Instant::now().as_millis()).await
}).await { if let Prediction::Location(coords) = latest_prediction.get().await {
warn!("Could not submit location! {e:?}"); // Only push to HTTP if we have an ip config etc
} if last_push.elapsed().as_secs() >= 5 || gps_to_local_meters_haversine(&last_location, &coords).norm() >= 10.0 {
last_location = coords;
last_push = Instant::now();
if let Err(e) = Backoff::from_secs(3).attempt(async || {
push_location(&mut client, coords, Instant::now().as_millis()).await
}).await {
motion.send(Measurement::SensorHardwareStatus(SensorSource::Cloud, SensorState::Offline)).await;
warn!("Could not submit location! {e:?}");
} else {
info!("Location published");
} }
} }
} }
@@ -134,3 +151,23 @@ async fn push_location(client: &mut HttpClient<'_, TcpClient<'_, 1, 4096, 4096>,
debug!("HTTP response: {content}"); debug!("HTTP response: {content}");
Ok(()) Ok(())
} }
async fn ping_nextcloud(client: &mut HttpClient<'_, TcpClient<'_, 1, 4096, 4096>, DnsSocket<'_>>) -> Result<bool, reqwless::Error> {
let mut buffer = [0u8; 4096];
let url = "https://nextcloud.malloc.hackerbots.net/nextcloud/index.php/login";
info!("Pinging via {url}");
let mut http_req = client
.request(
reqwless::request::Method::HEAD,
url,
)
.await?;
let response = http_req.send(&mut buffer).await?;
if !response.status.is_client_error() && !response.status.is_server_error() {
info!("Nextcloud is online!");
Ok(true)
} else {
warn!("Nextcloud ping returned error status: {:?}", response.status);
Ok(false)
}
}
+177
View File
@@ -0,0 +1,177 @@
#![allow(static_mut_refs)]
use log::*;
use crate::logging::RenderbugLogger;
#[derive(Debug)]
struct TracedTask {
id: u32,
name: &'static str,
priority: u32,
running: bool,
ready: bool
}
impl TracedTask {
fn new(id: u32) -> Self {
Self {
id,
name: "<unnamed task>",
priority: 0,
running: false,
ready: false
}
}
}
pub struct Tracer {}
struct TracerState {
tasks: [core::mem::MaybeUninit<TracedTask>; 16],
num_tasks: usize,
}
static mut TRACER_STATE: TracerState = TracerState::new();
impl TracerState {
const fn new() -> Self {
Self {
tasks: [const { core::mem::MaybeUninit::uninit() }; 16],
num_tasks: 0,
}
}
fn get_task(&mut self, id: u32) -> &mut TracedTask {
for i in 0..self.num_tasks {
let info = unsafe { &mut *self.tasks[i].as_mut_ptr() };
if info.id == id {
return info;
}
}
// Otherwise, we register a new task
if self.num_tasks < self.tasks.len() {
self.tasks[self.num_tasks].write(TracedTask::new(id));
self.num_tasks += 1;
} else {
panic!("too many tasks registered");
}
unsafe { &mut *self.tasks[self.num_tasks - 1].as_mut_ptr() }
}
}
impl rtos_trace::RtosTrace for Tracer {
fn start() {
warn!("rtos start");
}
fn stop() {
warn!("rtos stop");
}
fn task_new(id: u32) {
let task = unsafe { TRACER_STATE.get_task(id) };
warn!("rtos new task {task:?}");
}
fn task_send_info(id: u32, info: rtos_trace::TaskInfo) {
let task = unsafe { TRACER_STATE.get_task(id) };
task.name = info.name;
task.priority = info.priority;
warn!("rtos task info for task {task:?}");
}
fn task_new_stackless(id: u32, name: &'static str, priority: u32) {
let task = unsafe { TRACER_STATE.get_task(id) };
task.name = name;
task.priority = priority;
warn!("rtos new task {task:?} {name}");
}
fn task_terminate(id: u32) {
let task = unsafe { TRACER_STATE.get_task(id) };
warn!("rtos terminate {task:?}")
}
fn task_exec_begin(id: u32) {
let task = unsafe { TRACER_STATE.get_task(id) };
task.running = true;
warn!("rtos exec {task:?}")
}
fn task_exec_end() {
warn!("rtos exec end");
}
fn task_ready_begin(id: u32) {
let task = unsafe { TRACER_STATE.get_task(id) };
task.ready = true;
warn!("rtos ready {task:?}")
}
fn task_ready_end(id: u32) {
let task = unsafe { TRACER_STATE.get_task(id) };
task.ready = false;
warn!("rtos suspend {task:?}")
}
fn system_idle() {
warn!("rtos system idle");
}
fn isr_enter() {
warn!("rtos isr enter");
}
fn isr_exit() {
warn!("rtos isr exit");
}
fn isr_exit_to_scheduler() {
warn!("rtos isr exit to scheduler");
}
fn name_marker(id: u32, name: &'static str) {
warn!("rtos name marker {id}: {name}");
}
fn marker(id: u32) {
warn!("rtos marker {}", Self::name_for_marker(id));
}
fn marker_begin(id: u32) {
match id {
//1..2 => trace!("rtos marker begin {}", Self::name_for_marker(id)),
_ => {
warn!("rtos marker begin {}", Self::name_for_marker(id));
RenderbugLogger::set_level(LevelFilter::Trace);
}
}
}
fn marker_end(id: u32) {
match id {
//1..2 => trace!("rtos marker end {}", Self::name_for_marker(id)),
_ => {
warn!("rtos marker end {}", Self::name_for_marker(id));
RenderbugLogger::reset_level();
},
}
}
}
impl Tracer {
const fn name_for_marker(id: u32) -> &'static str {
match id {
0 => "run scheduler",
1 => "yield task",
2 => "timer tick",
3 => "process timer queue",
4 => "process embassy timer queue",
_ => "unknown",
}
}
}
+330
View File
@@ -0,0 +1,330 @@
use core::marker::PhantomData;
use embedded_hal_async::i2c::I2c as AsyncI2c;
#[derive(Clone, Copy, Debug)]
pub enum RegisterAddress {
DeviceID = 0x00,
DeviceID1 = 0x01,
DeviceID2 = 0x02,
DeviceID3 = 0x03,
DeviceID4 = 0x04,
DeviceID5 = 0x05,
DeviceID6 = 0x06,
DeviceID7 = 0x07,
Status1 = 0x08,
Status2 = 0x09,
Status3 = 0x0a,
DisableRDRP = 0x45
}
pub trait RegisterField {
fn mask() -> u8;
fn shift() -> u8;
fn address() -> RegisterAddress;
}
macro_rules! define_register_field {
($name:tt address=$address:tt offset=$shift:tt mask=$bitmask:tt $($value:tt = $valuemask:tt),+) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum $name {
$($value = $valuemask),+
}
impl From<u8> for $name {
fn from(value: u8) -> Self {
match value & 0b11 {
$($valuemask => $name::$value),+,
_ => unreachable!()
}
}
}
impl From<$name> for u8 {
fn from(value: $name) -> Self {
value as u8
}
}
impl RegisterField for $name {
fn mask() -> u8 {
$bitmask
}
fn shift() -> u8 {
$shift
}
fn address() -> RegisterAddress {
RegisterAddress::$address
}
}
};
}
define_register_field!{
CurrentAdvertisement
address=Status1
offset=6
mask=0b11
Default = 0b00,
// 1.5A
Medium = 0b01,
// 3A
High = 0b10
}
define_register_field!{
CurrentModeDetect
address=Status1
offset=4
mask=0b11
// 500mA
Default = 0b00,
// 1.5A
Medium = 0b01,
// 500mA
ThroughAccessory = 0b10,
// 3A
High = 0b11
}
impl CurrentModeDetect {
pub fn milliwatts(&self) -> u32 {
match self {
CurrentModeDetect::Default => 500,
CurrentModeDetect::Medium => 1500,
CurrentModeDetect::ThroughAccessory => 500,
CurrentModeDetect::High => 3000
}
}
}
define_register_field!{
AccessoryConnected
address=Status1
offset=1
mask=0b111
None = 0b000,
Audio = 0b100,
ThroughAccessory = 0b101,
DebugAccessory = 0b110
// All other patterns are 'reserved' and should not be possible
}
define_register_field!{
ActiveCableConnected
address=Status1
offset=0
mask=0b1
Disconnected = 0,
Connected = 1
}
define_register_field!{
AttachedState
address=Status2
offset=6
mask=0b11
Unattached = 0b00,
Source = 0b01,
Sink = 0b10,
Accessory = 0b11
}
define_register_field! {
CableOrientation
address=Status2
offset=5
mask=0b1
Normal = 0,
Flipped = 1
}
define_register_field! {
InterruptStatus
address=Status2
offset=4
mask=0b1
Clear = 0,
Set = 1
}
define_register_field! {
DrpDutyCycle
address=Status2
offset=1
mask=0b11
// 30%
Default = 0b00,
// 40%
Fast = 0b01,
// 50%
Faster = 0b10,
// 60%
Fastest = 0b11
}
define_register_field! {
Debounce
address=Status3
offset=6
mask=0b11
// 133ms (default)
D133 = 0b00,
// 116ms
D116 = 0b01,
// 151ms
D151 = 0b10,
// 168ms
D168 = 0b11
}
define_register_field! {
ModeSelect
address=Status3
offset=4
mask=0b11
UsePortPin = 0b00,
UFP = 0b01,
DFP = 0b10,
DRP = 0b11
}
define_register_field! {
DisableRDRP
address=DisableRDRP
offset=2
mask=0b1
Enabled = 0,
Disabled = 1
}
pub const DEVICE_ID: [u8; 8] = *b"TUSB320A";
const USB_ADDR_WRITE: u8 = 0b1100000;
const USB_ADDR_READ: u8 = 0b1100001;
pub struct TUSB320<T> {
bus: T
}
impl<T: AsyncI2c> TUSB320<T> {
pub const fn new(bus: T) -> Self {
Self { bus }
}
pub async fn soft_reset(&mut self) -> Result<(), T::Error> {
let mut val = self.read_address(RegisterAddress::Status3).await?;
val |= 0b1 << 3;
self.write_address(RegisterAddress::Status3, val).await
}
pub fn register<'a, R: RegisterField>(&'a mut self) -> UpdateRegister<'a, T, R> {
UpdateRegister::new(R::address(), self)
}
pub fn current_advertisement<'a>(&'a mut self) -> UpdateRegister<'a, T, CurrentAdvertisement> {
self.register()
}
pub fn current_mode_detect<'a>(&'a mut self) -> UpdateRegister<'a, T, CurrentModeDetect> {
self.register()
}
pub fn accessory_connected<'a>(&'a mut self) -> UpdateRegister<'a, T, AccessoryConnected> {
self.register()
}
pub fn active_cable_connected<'a>(&'a mut self) -> UpdateRegister<'a, T, ActiveCableConnected> {
self.register()
}
pub fn cable_orientation<'a>(&'a mut self) -> UpdateRegister<'a, T, CableOrientation> {
self.register()
}
pub fn interrupt_status<'a>(&'a mut self) -> UpdateRegister<'a, T, InterruptStatus> {
self.register()
}
pub fn drp_duty_cycle<'a>(&'a mut self) -> UpdateRegister<'a, T, DrpDutyCycle> {
self.register()
}
pub fn debounce<'a>(&'a mut self) -> UpdateRegister<'a, T, Debounce> {
self.register()
}
pub fn mode_select<'a>(&'a mut self) -> UpdateRegister<'a, T, ModeSelect> {
self.register()
}
pub fn disable_rdrp<'a>(&'a mut self) -> UpdateRegister<'a, T, DisableRDRP> {
self.register()
}
pub async fn get_device_id(&mut self) -> Result<[u8;8], T::Error> {
Ok([
self.read_address(RegisterAddress::DeviceID).await?,
self.read_address(RegisterAddress::DeviceID1).await?,
self.read_address(RegisterAddress::DeviceID2).await?,
self.read_address(RegisterAddress::DeviceID3).await?,
self.read_address(RegisterAddress::DeviceID4).await?,
self.read_address(RegisterAddress::DeviceID5).await?,
self.read_address(RegisterAddress::DeviceID6).await?,
self.read_address(RegisterAddress::DeviceID7).await?
])
}
async fn read_address(&mut self, reg: RegisterAddress) -> Result<u8, T::Error> {
let mut response = [0; 1];
self.bus.write_read(USB_ADDR_READ, &[reg as u8], &mut response).await?;
Ok(response[0])
}
async fn write_address(&mut self, reg: RegisterAddress, value: u8) -> Result<(), T::Error> {
// FIXME: I think this needs to use self.bus.transaction()
self.bus.write(USB_ADDR_WRITE, &[reg as u8]).await?;
self.bus.write(USB_ADDR_WRITE, &[value]).await?;
Ok(())
}
}
pub struct UpdateRegister<'a, I2C: AsyncI2c, T> {
reg: RegisterAddress,
raw: u8,
dev: &'a mut TUSB320<I2C>,
_type: PhantomData<T>
}
impl<'a, I2C: AsyncI2c, T> UpdateRegister<'a, I2C, T> {
const fn new(reg: RegisterAddress, dev: &'a mut TUSB320<I2C>) -> Self {
Self { reg, raw: 0, dev, _type: PhantomData }
}
pub async fn refresh(&mut self) -> Result<(), I2C::Error> {
self.raw = self.dev.read_address(self.reg).await?;
Ok(())
}
}
impl<'a, I2C: AsyncI2c, T: RegisterField + Into<u8>> UpdateRegister<'a, I2C, T> where u8: Into<T> {
pub async fn write(&mut self, value: T) -> Result<(), I2C::Error> {
let mask = T::mask();
let v: u8 = value.into();
self.refresh().await?;
self.raw &= !(mask | (v << T::shift()));
self.dev.write_address(self.reg, self.raw).await?;
Ok(())
}
pub async fn read(&mut self) -> Result<T, I2C::Error> {
self.refresh().await?;
Ok(self.value())
}
pub fn value(&mut self) -> T {
(self.raw >> T::shift()).into()
}
}