Compare commits
139 Commits
6ffbf710d3
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| c112d133db | |||
| 95281d617f | |||
| a2abc99275 | |||
| 4b3c003996 | |||
| bba959f2a2 | |||
| c9a96f37f0 | |||
| fbf97732a4 | |||
| 6b419dba6c | |||
| 3618b3329c | |||
| f5f73723d1 | |||
| be98380ba4 | |||
| fe2d227c67 | |||
| bd5b687430 | |||
| 7679fa09dc | |||
| 7078af5713 | |||
|
32256d0c91
|
|||
|
d4a4c1b573
|
|||
|
6bf7a04024
|
|||
|
df3159aa16
|
|||
|
7866604a40
|
|||
| d989b41bdd | |||
| ac8305953a | |||
| d1076145c4 | |||
| cf32f7e05d | |||
| 5e08820276 | |||
|
d2a659638d
|
|||
|
40f99870cf
|
|||
|
ac200af7a9
|
|||
|
9d57805502
|
|||
|
bafc86681c
|
|||
|
5f9db41d65
|
|||
|
ba654a904b
|
|||
|
cd4d0cc683
|
|||
|
2cfb2607a9
|
|||
|
271c1a1383
|
|||
|
a02b84d732
|
|||
| b0f8bcc9da | |||
| 103859120c | |||
| 403517fdb4 | |||
| 11eb8713bf | |||
| d903c2bf52 | |||
| f8f76674ce | |||
| 3cc5a0d2bd | |||
| 3be585ecbf | |||
| 5b1a945ac3 | |||
| f4e050d413 | |||
| 776db785c4 | |||
| ef0ec47d92 | |||
| 0ed9d6bb57 | |||
| 4771a77686 | |||
| eef165b6de | |||
| 1ace878488 | |||
| a30d59605d | |||
| 2ee3615dcd | |||
| db0f7daa4c | |||
| 6809a37d9d | |||
| 0ca09ed498 | |||
| 542ff578bc | |||
| 2e16163b0e | |||
| 9280bbb244 | |||
| e0b8acd55c | |||
| c04109a76c | |||
| f0c9ed4e7f | |||
| 3fa8077b81 | |||
| 7f0714914f | |||
| 61806a5fa2 | |||
| 016047ab23 | |||
| eb276cfa68 | |||
| f1c85d1d74 | |||
| 097aff5360 | |||
| fc0e18da56 | |||
| 2e4eb283b5 | |||
| cc92c82ac9 | |||
| b8f01f0de9 | |||
| 79daecf97d | |||
| 6b4fd3f701 | |||
| 3157ba7e76 | |||
| 2493507304 | |||
| 0f6cb5243c | |||
| b740574c68 | |||
| 6a71ac4234 | |||
| 8ce00c9d95 | |||
| 964bdb0454 | |||
| 12405d1bef | |||
| 0e3786a588 | |||
| b26206eb96 | |||
| 95f7488fa3 | |||
| 0d7074bd89 | |||
| bc25fef5ec | |||
| 301298522b | |||
| 1da6d54d7a | |||
| 0ad7a58219 | |||
| 07aed02fe7 | |||
| aef0ffd5a1 | |||
| 4d4fcbe33b | |||
| 1fa765a5d8 | |||
| eaa65637f1 | |||
| f1dadd7e6e | |||
| 7121dd0fae | |||
| 4cf5f6d151 | |||
| 9de5236e65 | |||
| abca324a67 | |||
| 57323bad55 | |||
| 086b0cbe4e | |||
| 39e4e733f3 | |||
| 66e1fe63e0 | |||
| ce981232f0 | |||
| 07cf97fffb | |||
| cca13f51d9 | |||
| 7c128a27eb | |||
| 924a9ba228 | |||
| a069888341 | |||
| 02c9486e98 | |||
| 2d2d7d16cd | |||
| 86f29a336c | |||
| c61a586595 | |||
| af27f3b820 | |||
| 32db326266 | |||
| a4d764c4fe | |||
| 5c78495bd5 | |||
| 07ab69075a | |||
| 125b3efad3 | |||
| 9b21d505e6 | |||
| c575fc2c36 | |||
| 3128e32fb2 | |||
| 6bba9b1f27 | |||
| c909b33af0 | |||
| 979f982565 | |||
| e6f8e34f7d | |||
| ce10d084f8 | |||
| 355388aa62 | |||
| 0c0b62e2ed | |||
| 1de40085fb | |||
| 8fc2a89503 | |||
| 350820741a | |||
| d33b05e1d7 | |||
| 412a26390a | |||
| af275abf15 | |||
| ca2fd8a5e1 |
@@ -0,0 +1,62 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Scope
|
||||
These instructions apply to the entire repository unless a deeper `AGENTS.md` overrides them.
|
||||
|
||||
## Repository Overview
|
||||
`PlantCtrl` is a mixed-discipline repository with embedded firmware, shared Rust crates, hardware design files, and a Hugo-based website.
|
||||
|
||||
Top-level layout:
|
||||
- `Software/MainBoard/rust`: main embedded Rust firmware for the controller board (`plant-ctrl2`).
|
||||
- `Software/CAN_Sensor`: embedded Rust firmware for the CAN sensor / BMS board.
|
||||
- `Software/Shared/canapi`: shared Rust crate used by firmware projects.
|
||||
- `Hardware`: PCB, case, and related hardware design assets.
|
||||
- `DataSheets`: reference material; treat as source data, not generated output.
|
||||
- `website`: Hugo site based on the Blowfish theme.
|
||||
- `bin`: helper scripts and local tooling, if present.
|
||||
|
||||
## Working Rules
|
||||
- Keep changes tightly scoped to the user request; this repo spans hardware, firmware, and website code.
|
||||
- Prefer fixing the underlying cause instead of applying cosmetic workarounds.
|
||||
- Preserve existing file structure and naming unless the user explicitly asks for restructuring.
|
||||
- Avoid mass formatting or opportunistic cleanup in KiCad files, lockfiles, generated assets, or vendored dependencies.
|
||||
- Do not edit dependency directories such as `website/themes` or `Software/MainBoard/rust/src_webpack/node_modules` unless the user explicitly asks for vendor changes.
|
||||
- When touching firmware code, keep resource usage and target constraints in mind; avoid unnecessary allocations or feature creep.
|
||||
|
||||
## Firmware Guidance
|
||||
- Shared protocol or serialization changes must be checked for impact across both `Software/MainBoard/rust` and `Software/CAN_Sensor`.
|
||||
- Prefer small, explicit changes in embedded code paths; do not introduce heavyweight abstractions without a clear payoff.
|
||||
- Keep `no_std`/embedded assumptions intact unless the code clearly opts into something else.
|
||||
- Be careful with feature flags, target-specific dependencies, and boot/runtime configuration in Cargo manifests.
|
||||
|
||||
## Hardware Guidance
|
||||
- Treat hardware directories as design artifacts, not generic text files.
|
||||
- Do not reorder, normalize, or bulk-edit PCB / CAD files unless the user specifically requests those changes.
|
||||
- If a software change depends on hardware assumptions, call that out clearly in the final handoff.
|
||||
|
||||
## Website Guidance
|
||||
- The site in `website` uses Hugo with the Blowfish theme.
|
||||
- Prefer editing site content, config, or custom assets over modifying vendored theme internals.
|
||||
- Keep frontend changes consistent with the existing site structure unless the user asks for a redesign.
|
||||
|
||||
## Validation
|
||||
Use the narrowest relevant check first.
|
||||
|
||||
Useful commands:
|
||||
- `cargo check --manifest-path Software/Shared/canapi/Cargo.toml`
|
||||
- `cargo check --manifest-path Software/CAN_Sensor/Cargo.toml`
|
||||
- `cargo check --manifest-path Software/MainBoard/rust/Cargo.toml`
|
||||
- `npm run dev` from `website` for local Hugo development if the environment has the required tools installed.
|
||||
|
||||
Validation notes:
|
||||
- Embedded firmware may require target-specific toolchains or hardware-adjacent tooling that is not always available.
|
||||
- If you cannot run a meaningful validation step, say so explicitly and describe the likely prerequisite.
|
||||
|
||||
## File Hygiene
|
||||
- Read large files in chunks.
|
||||
- Prefer targeted searches (`rg`, or `find` if unavailable) over broad scans.
|
||||
- Do not commit build outputs, generated binaries, or local IDE metadata unless the user explicitly requests it.
|
||||
|
||||
## Handoff Expectations
|
||||
- Summarize what changed, where it changed, and any validation performed.
|
||||
- Call out follow-up work when a change likely affects both firmware targets, hardware assumptions, or the website.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"prototype_zone_fills": false,
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
@@ -53,6 +54,7 @@
|
||||
"zone_display_mode": 1
|
||||
},
|
||||
"git": {
|
||||
"integration_disabled": false,
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
@@ -105,6 +107,7 @@
|
||||
"filter_text": "",
|
||||
"group_by_constraint": false,
|
||||
"group_by_netclass": false,
|
||||
"show_time_domain_details": false,
|
||||
"show_unconnected_nets": false,
|
||||
"show_zero_pad_nets": false,
|
||||
"sort_ascending": true,
|
||||
@@ -115,6 +118,7 @@
|
||||
"files": []
|
||||
},
|
||||
"schematic": {
|
||||
"hierarchy_collapsed": [],
|
||||
"selection_filter": {
|
||||
"graphics": true,
|
||||
"images": true,
|
||||
@@ -122,6 +126,7 @@
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pins": true,
|
||||
"ruleAreas": true,
|
||||
"symbols": true,
|
||||
"text": true,
|
||||
"wires": true
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_barcodes": false,
|
||||
"apply_defaults_to_fp_dimensions": false,
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
@@ -82,6 +84,7 @@
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_symbol_field_mismatch": "warning",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "ignore",
|
||||
"hole_clearance": "error",
|
||||
@@ -99,6 +102,7 @@
|
||||
"mirrored_text_on_front_layer": "warning",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"missing_tuning_profile": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
@@ -118,9 +122,12 @@
|
||||
"too_many_vias": "error",
|
||||
"track_angle": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_not_centered_on_via": "ignore",
|
||||
"track_on_post_machined_layer": "error",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"tuning_profile_track_geometries": "ignore",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
@@ -235,17 +242,28 @@
|
||||
"zones_allow_external_fillets": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"bom_rev": "",
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
"mpn": "",
|
||||
"sch_revision": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"component_class_settings": {
|
||||
"assignments": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"sheet_component_classes": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
@@ -494,13 +512,14 @@
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 4
|
||||
"version": 5
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
@@ -683,6 +702,7 @@
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
@@ -721,7 +741,14 @@
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
"subpart_id_separator": 0,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "MPPT.kicad_sch",
|
||||
"name": "MPPT",
|
||||
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
@@ -729,5 +756,11 @@
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12996,7 +12996,7 @@
|
||||
(drill 1)
|
||||
(layers "*.Cu" "*.Mask")
|
||||
(remove_unused_layers no)
|
||||
(net 44 "CONFIG2")
|
||||
(net 22 "BOOT_SEL")
|
||||
(pinfunction "Pin_3")
|
||||
(pintype "passive")
|
||||
(uuid "7e1e9d83-af98-48df-9773-39ee7824a6f6")
|
||||
@@ -30814,6 +30814,14 @@
|
||||
(net 22)
|
||||
(uuid "a0e70239-5b1c-4c3c-a476-64be66d07299")
|
||||
)
|
||||
(segment
|
||||
(start 216.33 52.67)
|
||||
(end 215.255 52.67)
|
||||
(width 0.2)
|
||||
(layer "F.Cu")
|
||||
(net 22)
|
||||
(uuid "a39be493-05ee-456d-b821-95927582c5ad")
|
||||
)
|
||||
(segment
|
||||
(start 217.33 53.496)
|
||||
(end 217.5 53.496)
|
||||
@@ -30862,6 +30870,14 @@
|
||||
(net 22)
|
||||
(uuid "f1e984ca-47cc-4a62-83f5-873ad9276c13")
|
||||
)
|
||||
(segment
|
||||
(start 215.255 52.67)
|
||||
(end 215.08 52.845)
|
||||
(width 0.2)
|
||||
(layer "F.Cu")
|
||||
(net 22)
|
||||
(uuid "f6609741-7a8c-4e92-b2a7-a12cb5094a7e")
|
||||
)
|
||||
(segment
|
||||
(start 217.7 53.496)
|
||||
(end 215.731 53.496)
|
||||
@@ -30878,6 +30894,14 @@
|
||||
(net 22)
|
||||
(uuid "02ec00f7-269e-4eee-bd30-b44c72206ead")
|
||||
)
|
||||
(via
|
||||
(at 216.33 52.67)
|
||||
(size 0.8)
|
||||
(drill 0.4)
|
||||
(layers "F.Cu" "B.Cu")
|
||||
(net 22)
|
||||
(uuid "81681c98-2e02-4af5-8ed6-fab73d76574b")
|
||||
)
|
||||
(via
|
||||
(at 247.33 57.67)
|
||||
(size 0.8)
|
||||
@@ -30894,6 +30918,14 @@
|
||||
(net 22)
|
||||
(uuid "668f3e0f-6796-4544-9fff-44be590ce44b")
|
||||
)
|
||||
(segment
|
||||
(start 210.28 47.97)
|
||||
(end 211.63 47.97)
|
||||
(width 0.2)
|
||||
(layer "B.Cu")
|
||||
(net 22)
|
||||
(uuid "67850407-ea7d-4962-911e-4068f92ec668")
|
||||
)
|
||||
(segment
|
||||
(start 241.08 51.42)
|
||||
(end 247.33 57.67)
|
||||
@@ -30902,6 +30934,14 @@
|
||||
(net 22)
|
||||
(uuid "6f9fed78-f413-48d8-be42-a9dd34d1adfe")
|
||||
)
|
||||
(segment
|
||||
(start 211.63 47.97)
|
||||
(end 216.33 52.67)
|
||||
(width 0.2)
|
||||
(layer "B.Cu")
|
||||
(net 22)
|
||||
(uuid "9980ac60-b158-4195-ae96-1692b27bf812")
|
||||
)
|
||||
(segment
|
||||
(start 245.88 48.17)
|
||||
(end 246.48 48.17)
|
||||
@@ -31574,30 +31614,6 @@
|
||||
(net 44)
|
||||
(uuid "ea858ae5-b45b-4221-888a-e8a2838d0cf3")
|
||||
)
|
||||
(via
|
||||
(at 221.98 51.37)
|
||||
(size 0.8)
|
||||
(drill 0.4)
|
||||
(layers "F.Cu" "B.Cu")
|
||||
(net 44)
|
||||
(uuid "cbd287e0-ce8b-4721-9efe-d472968d1c74")
|
||||
)
|
||||
(segment
|
||||
(start 218.58 47.97)
|
||||
(end 221.98 51.37)
|
||||
(width 0.2)
|
||||
(layer "In2.Cu")
|
||||
(net 44)
|
||||
(uuid "3c7ed4a9-a06a-422f-aedb-2cd3dc699c8a")
|
||||
)
|
||||
(segment
|
||||
(start 210.28 47.97)
|
||||
(end 218.58 47.97)
|
||||
(width 0.2)
|
||||
(layer "In2.Cu")
|
||||
(net 44)
|
||||
(uuid "46e80a90-509c-4b27-bf19-e8d7851b0dd5")
|
||||
)
|
||||
(segment
|
||||
(start 186.655 113.695)
|
||||
(end 186.655 112.621)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"board": {
|
||||
"active_layer": 0,
|
||||
"active_layer": 2,
|
||||
"active_layer_preset": "All Layers",
|
||||
"auto_track_width": false,
|
||||
"hidden_netclasses": [],
|
||||
@@ -15,6 +15,7 @@
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"prototype_zone_fills": false,
|
||||
"ratsnest_display_mode": 0,
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
@@ -54,6 +55,7 @@
|
||||
"zone_display_mode": 1
|
||||
},
|
||||
"git": {
|
||||
"integration_disabled": false,
|
||||
"repo_password": "",
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
@@ -113,6 +115,7 @@
|
||||
"filter_text": "",
|
||||
"group_by_constraint": false,
|
||||
"group_by_netclass": false,
|
||||
"show_time_domain_details": false,
|
||||
"show_unconnected_nets": false,
|
||||
"show_zero_pad_nets": false,
|
||||
"sort_ascending": true,
|
||||
@@ -123,6 +126,7 @@
|
||||
"files": []
|
||||
},
|
||||
"schematic": {
|
||||
"hierarchy_collapsed": [],
|
||||
"selection_filter": {
|
||||
"graphics": true,
|
||||
"images": true,
|
||||
@@ -130,6 +134,7 @@
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pins": true,
|
||||
"ruleAreas": true,
|
||||
"symbols": true,
|
||||
"text": true,
|
||||
"wires": true
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_barcodes": false,
|
||||
"apply_defaults_to_fp_dimensions": false,
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
@@ -78,6 +80,7 @@
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_symbol_field_mismatch": "warning",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "warning",
|
||||
"hole_clearance": "error",
|
||||
@@ -96,6 +99,7 @@
|
||||
"mirrored_text_on_front_layer": "warning",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"missing_tuning_profile": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
@@ -115,9 +119,12 @@
|
||||
"too_many_vias": "error",
|
||||
"track_angle": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_not_centered_on_via": "ignore",
|
||||
"track_on_post_machined_layer": "error",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"tuning_profile_track_geometries": "ignore",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
@@ -242,17 +249,28 @@
|
||||
"zones_use_no_outline": true
|
||||
},
|
||||
"ipc2581": {
|
||||
"bom_rev": "",
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
"mpn": "",
|
||||
"sch_revision": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"component_class_settings": {
|
||||
"assignments": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"sheet_component_classes": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
@@ -443,11 +461,14 @@
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "ignore",
|
||||
"extra_units": "error",
|
||||
"field_name_whitespace": "warning",
|
||||
"footprint_filter": "ignore",
|
||||
"footprint_link_issues": "warning",
|
||||
"four_way_junction": "ignore",
|
||||
"global_label_dangling": "warning",
|
||||
"ground_pin_not_ground": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"isolated_pin_label": "warning",
|
||||
"label_dangling": "error",
|
||||
"label_multiple_wires": "warning",
|
||||
"lib_symbol_issues": "warning",
|
||||
@@ -470,6 +491,7 @@
|
||||
"similar_power": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"single_global_label": "warning",
|
||||
"stacked_pin_name": "warning",
|
||||
"unannotated": "error",
|
||||
"unconnected_wire_endpoint": "warning",
|
||||
"undefined_netclass": "error",
|
||||
@@ -502,6 +524,7 @@
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 1.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 6
|
||||
@@ -520,6 +543,7 @@
|
||||
"priority": 0,
|
||||
"schematic_color": "rgb(255, 4, 6)",
|
||||
"track_width": 1.0,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 12
|
||||
@@ -538,6 +562,7 @@
|
||||
"priority": 1,
|
||||
"schematic_color": "rgb(255, 153, 0)",
|
||||
"track_width": 0.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 12
|
||||
@@ -556,6 +581,7 @@
|
||||
"priority": 2,
|
||||
"schematic_color": "rgb(81, 255, 3)",
|
||||
"track_width": 1.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 12
|
||||
@@ -574,6 +600,7 @@
|
||||
"priority": 3,
|
||||
"schematic_color": "rgb(130, 130, 130)",
|
||||
"track_width": 1.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 12
|
||||
@@ -592,13 +619,14 @@
|
||||
"priority": 4,
|
||||
"schematic_color": "rgb(0, 0, 0)",
|
||||
"track_width": 0.5,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 12
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 4
|
||||
"version": 5
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
@@ -1077,6 +1105,10 @@
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"annotation": {
|
||||
"method": 0,
|
||||
"sort_order": 0
|
||||
},
|
||||
"bom_export_filename": "PlantCtrlESP32.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
@@ -1256,6 +1288,7 @@
|
||||
"sort_asc": true,
|
||||
"sort_field": "LCSC_PART_NUMBER"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
@@ -1263,6 +1296,7 @@
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"hop_over_size_choice": 0,
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
@@ -1295,6 +1329,7 @@
|
||||
},
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "/tmp/",
|
||||
"reuse_designators": true,
|
||||
"space_save_all_events": true,
|
||||
"spice_adjust_passive_values": false,
|
||||
"spice_current_sheet_as_root": false,
|
||||
@@ -1304,7 +1339,16 @@
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
"subpart_id_separator": 0,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "PlantCtrlESP32.kicad_sch",
|
||||
"name": "PlantCtrlESP32",
|
||||
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
],
|
||||
"used_designators": "",
|
||||
"variants": []
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
@@ -1312,5 +1356,11 @@
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8333,28 +8333,6 @@
|
||||
)
|
||||
)
|
||||
)
|
||||
(global_label "CONFIG2"
|
||||
(shape input)
|
||||
(at 142.24 44.45 180)
|
||||
(fields_autoplaced yes)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
(uuid "4aed43a7-6995-42f6-b313-69653f7d36d4")
|
||||
(property "Intersheetrefs" "${INTERSHEET_REFS}"
|
||||
(at 131.8051 44.45 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
)
|
||||
(global_label "3_3V"
|
||||
(shape input)
|
||||
(at 224.79 57.15 0)
|
||||
@@ -9147,6 +9125,28 @@
|
||||
)
|
||||
)
|
||||
)
|
||||
(global_label "BOOT_SEL"
|
||||
(shape input)
|
||||
(at 142.24 44.45 180)
|
||||
(fields_autoplaced yes)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
(uuid "8202cf4c-8ab8-4793-979c-ae5015ef80e9")
|
||||
(property "Intersheetrefs" "${INTERSHEET_REFS}"
|
||||
(at 130.6562 44.45 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
)
|
||||
(global_label "GND"
|
||||
(shape input)
|
||||
(at 113.03 123.19 270)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"prototype_zone_fills": false,
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
@@ -53,6 +54,7 @@
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"git": {
|
||||
"integration_disabled": false,
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
@@ -105,6 +107,7 @@
|
||||
"filter_text": "",
|
||||
"group_by_constraint": false,
|
||||
"group_by_netclass": false,
|
||||
"show_time_domain_details": false,
|
||||
"show_unconnected_nets": false,
|
||||
"show_zero_pad_nets": false,
|
||||
"sort_ascending": true,
|
||||
@@ -115,6 +118,7 @@
|
||||
"files": []
|
||||
},
|
||||
"schematic": {
|
||||
"hierarchy_collapsed": [],
|
||||
"selection_filter": {
|
||||
"graphics": true,
|
||||
"images": true,
|
||||
@@ -122,6 +126,7 @@
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pins": true,
|
||||
"ruleAreas": true,
|
||||
"symbols": true,
|
||||
"text": true,
|
||||
"wires": true
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_barcodes": false,
|
||||
"apply_defaults_to_fp_dimensions": false,
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
@@ -82,6 +84,7 @@
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_symbol_field_mismatch": "warning",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "warning",
|
||||
"hole_clearance": "error",
|
||||
@@ -99,6 +102,7 @@
|
||||
"mirrored_text_on_front_layer": "warning",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"missing_tuning_profile": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
@@ -118,9 +122,12 @@
|
||||
"too_many_vias": "error",
|
||||
"track_angle": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_not_centered_on_via": "ignore",
|
||||
"track_on_post_machined_layer": "error",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"tuning_profile_track_geometries": "ignore",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
@@ -236,17 +243,28 @@
|
||||
"zones_allow_external_fillets": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"bom_rev": "",
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
"mpn": "",
|
||||
"sch_revision": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"component_class_settings": {
|
||||
"assignments": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"sheet_component_classes": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
@@ -495,13 +513,14 @@
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 4
|
||||
"version": 5
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
@@ -629,6 +648,7 @@
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
@@ -667,7 +687,14 @@
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
"subpart_id_separator": 0,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "PumpOutput.kicad_sch",
|
||||
"name": "PumpOutput",
|
||||
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
@@ -675,5 +702,11 @@
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"board": {
|
||||
"active_layer": 25,
|
||||
"active_layer_preset": "",
|
||||
"auto_track_width": false,
|
||||
"hidden_netclasses": [],
|
||||
"hidden_nets": [],
|
||||
"high_contrast_mode": 0,
|
||||
"net_color_mode": 1,
|
||||
"opacity": {
|
||||
"images": 0.6,
|
||||
"pads": 1.0,
|
||||
"shapes": 1.0,
|
||||
"tracks": 1.0,
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
"graphics": true,
|
||||
"keepouts": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pads": true,
|
||||
"text": true,
|
||||
"tracks": true,
|
||||
"vias": true,
|
||||
"zones": true
|
||||
},
|
||||
"visible_items": [
|
||||
"vias",
|
||||
"footprint_text",
|
||||
"footprint_anchors",
|
||||
"ratsnest",
|
||||
"grid",
|
||||
"footprints_front",
|
||||
"footprints_back",
|
||||
"footprint_values",
|
||||
"footprint_references",
|
||||
"tracks",
|
||||
"drc_errors",
|
||||
"drawing_sheet",
|
||||
"bitmaps",
|
||||
"pads",
|
||||
"zones",
|
||||
"drc_warnings",
|
||||
"drc_exclusions",
|
||||
"locked_item_shadows",
|
||||
"conflict_shadows",
|
||||
"shapes"
|
||||
],
|
||||
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"git": {
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
},
|
||||
"meta": {
|
||||
"filename": "sensor-programmer.kicad_prl",
|
||||
"version": 5
|
||||
},
|
||||
"net_inspector_panel": {
|
||||
"col_hidden": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"col_order": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9
|
||||
],
|
||||
"col_widths": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"custom_group_rules": [],
|
||||
"expanded_rows": [],
|
||||
"filter_by_net_name": true,
|
||||
"filter_by_netclass": true,
|
||||
"filter_text": "",
|
||||
"group_by_constraint": false,
|
||||
"group_by_netclass": false,
|
||||
"show_unconnected_nets": false,
|
||||
"show_zero_pad_nets": false,
|
||||
"sort_ascending": true,
|
||||
"sorting_column": 0
|
||||
},
|
||||
"open_jobsets": [],
|
||||
"project": {
|
||||
"files": []
|
||||
},
|
||||
"schematic": {
|
||||
"selection_filter": {
|
||||
"graphics": true,
|
||||
"images": true,
|
||||
"labels": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pins": true,
|
||||
"symbols": true,
|
||||
"text": true,
|
||||
"wires": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,911 @@
|
||||
{
|
||||
"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.8,
|
||||
"height": 1.27,
|
||||
"width": 2.54
|
||||
},
|
||||
"silk_line_width": 0.1,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.1,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"min_clearance": 0.5
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [
|
||||
{
|
||||
"gap": 0.0,
|
||||
"via_gap": 0.0,
|
||||
"width": 0.0
|
||||
}
|
||||
],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "ignore",
|
||||
"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": "ignore",
|
||||
"silk_overlap": "ignore",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "error",
|
||||
"starved_thermal": "ignore",
|
||||
"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.005,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_onpthpad": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_onsmdpad": true,
|
||||
"td_ontrackend": false,
|
||||
"td_onvia": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [
|
||||
0.0,
|
||||
0.2,
|
||||
0.5,
|
||||
1.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": "ignore",
|
||||
"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": "sensor-programmer.kicad_pro",
|
||||
"version": 3
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 4
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": []
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"pos_files": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"svg": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from BOM",
|
||||
"name": "${EXCLUDE_FROM_BOM}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Board",
|
||||
"name": "${EXCLUDE_FROM_BOARD}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Actuator/Cap Color",
|
||||
"name": "Actuator/Cap Color",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Attrition Qty",
|
||||
"name": "Attrition Qty",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Capacitance",
|
||||
"name": "Capacitance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Category",
|
||||
"name": "Category",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Circuit",
|
||||
"name": "Circuit",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Class",
|
||||
"name": "Class",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Contact Current",
|
||||
"name": "Contact Current",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Diode Configuration",
|
||||
"name": "Diode Configuration",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Forward Voltage (Vf@If)",
|
||||
"name": "Forward Voltage (Vf@If)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Insulation Resistance",
|
||||
"name": "Insulation Resistance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "LCSC",
|
||||
"name": "LCSC",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Manufacturer",
|
||||
"name": "Manufacturer",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Mechanical Life",
|
||||
"name": "Mechanical Life",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Minimum Qty",
|
||||
"name": "Minimum Qty",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Mounting Style",
|
||||
"name": "Mounting Style",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Operating Force",
|
||||
"name": "Operating Force",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Operating Temperature",
|
||||
"name": "Operating Temperature",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Operating Temperature Range",
|
||||
"name": "Operating Temperature Range",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Overload Voltage (Max)",
|
||||
"name": "Overload Voltage (Max)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Part",
|
||||
"name": "Part",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Pin Style",
|
||||
"name": "Pin Style",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Power(Watts)",
|
||||
"name": "Power(Watts)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Price",
|
||||
"name": "Price",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Process",
|
||||
"name": "Process",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Rectified Current",
|
||||
"name": "Rectified Current",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Resistance",
|
||||
"name": "Resistance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reverse Leakage Current",
|
||||
"name": "Reverse Leakage Current",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reverse Voltage (Vr)",
|
||||
"name": "Reverse Voltage (Vr)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Sim.Pins",
|
||||
"name": "Sim.Pins",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Stock",
|
||||
"name": "Stock",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Strike Gundam",
|
||||
"name": "Strike Gundam",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Switch Height",
|
||||
"name": "Switch Height",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Switch Length",
|
||||
"name": "Switch Length",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Switch Width",
|
||||
"name": "Switch Width",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Temperature Coefficient",
|
||||
"name": "Temperature Coefficient",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Tolerance",
|
||||
"name": "Tolerance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Type",
|
||||
"name": "Type",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Voltage Rated",
|
||||
"name": "Voltage Rated",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Voltage Rating (Dc)",
|
||||
"name": "Voltage Rating (Dc)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "With Lamp",
|
||||
"name": "With Lamp",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Actuator Style",
|
||||
"name": "Actuator Style",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Description",
|
||||
"name": "Description",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "#",
|
||||
"name": "${ITEM_NUMBER}",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "LCSC_PART_NUMBER",
|
||||
"name": "LCSC_PART_NUMBER",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Sim.Device",
|
||||
"name": "Sim.Device",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Sim.Type",
|
||||
"name": "Sim.Type",
|
||||
"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": "",
|
||||
"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": [
|
||||
[
|
||||
"a6275404-53d1-4c49-9128-131a51db8de5",
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
|
||||
+18452
-26281
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"board": {
|
||||
"active_layer": 25,
|
||||
"active_layer": 0,
|
||||
"active_layer_preset": "",
|
||||
"auto_track_width": true,
|
||||
"auto_track_width": false,
|
||||
"hidden_netclasses": [],
|
||||
"hidden_nets": [],
|
||||
"high_contrast_mode": 0,
|
||||
@@ -15,6 +15,7 @@
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"prototype_zone_fills": false,
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
@@ -54,6 +55,7 @@
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"git": {
|
||||
"integration_disabled": false,
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
@@ -106,6 +108,7 @@
|
||||
"filter_text": "",
|
||||
"group_by_constraint": false,
|
||||
"group_by_netclass": false,
|
||||
"show_time_domain_details": false,
|
||||
"show_unconnected_nets": false,
|
||||
"show_zero_pad_nets": false,
|
||||
"sort_ascending": true,
|
||||
@@ -116,6 +119,7 @@
|
||||
"files": []
|
||||
},
|
||||
"schematic": {
|
||||
"hierarchy_collapsed": [],
|
||||
"selection_filter": {
|
||||
"graphics": true,
|
||||
"images": true,
|
||||
@@ -123,6 +127,7 @@
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pins": true,
|
||||
"ruleAreas": true,
|
||||
"symbols": true,
|
||||
"text": true,
|
||||
"wires": true
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_barcodes": false,
|
||||
"apply_defaults_to_fp_dimensions": false,
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
@@ -76,9 +78,10 @@
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_filters_mismatch": "warning",
|
||||
"footprint_symbol_field_mismatch": "warning",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "ignore",
|
||||
"footprint_type_mismatch": "warning",
|
||||
"hole_clearance": "error",
|
||||
"hole_to_hole": "warning",
|
||||
"holes_co_located": "warning",
|
||||
@@ -92,20 +95,21 @@
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"mirrored_text_on_front_layer": "warning",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_courtyard": "warning",
|
||||
"missing_footprint": "warning",
|
||||
"missing_tuning_profile": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
"npth_inside_courtyard": "warning",
|
||||
"padstack": "warning",
|
||||
"pth_inside_courtyard": "ignore",
|
||||
"pth_inside_courtyard": "warning",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "warning",
|
||||
"silk_over_copper": "ignore",
|
||||
"silk_overlap": "ignore",
|
||||
"silk_over_copper": "warning",
|
||||
"silk_overlap": "warning",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "error",
|
||||
"starved_thermal": "error",
|
||||
"starved_thermal": "warning",
|
||||
"text_height": "warning",
|
||||
"text_on_edge_cuts": "error",
|
||||
"text_thickness": "warning",
|
||||
@@ -113,9 +117,12 @@
|
||||
"too_many_vias": "error",
|
||||
"track_angle": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_not_centered_on_via": "ignore",
|
||||
"track_on_post_machined_layer": "error",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"tuning_profile_track_geometries": "ignore",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
@@ -125,7 +132,7 @@
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.0,
|
||||
"min_connection": 0.0,
|
||||
"min_copper_edge_clearance": 0.5,
|
||||
"min_copper_edge_clearance": 0.3,
|
||||
"min_groove_width": 0.0,
|
||||
"min_hole_clearance": 0.25,
|
||||
"min_hole_to_hole": 0.25,
|
||||
@@ -227,17 +234,28 @@
|
||||
"zones_allow_external_fillets": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"bom_rev": "",
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
"mpn": "",
|
||||
"sch_revision": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"component_class_settings": {
|
||||
"assignments": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"sheet_component_classes": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
@@ -486,13 +504,14 @@
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 4
|
||||
"version": 5
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
@@ -506,7 +525,7 @@
|
||||
"plot": "",
|
||||
"pos_files": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"step": "sensor.step",
|
||||
"svg": "",
|
||||
"vrml": ""
|
||||
},
|
||||
@@ -861,6 +880,7 @@
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
@@ -899,7 +919,14 @@
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
"subpart_id_separator": 0,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "sensor.kicad_sch",
|
||||
"name": "sensor",
|
||||
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
@@ -907,5 +934,11 @@
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
|
||||
+2762
-9124
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"board": {
|
||||
"active_layer": 0,
|
||||
"active_layer_preset": "",
|
||||
"auto_track_width": false,
|
||||
"hidden_netclasses": [],
|
||||
"hidden_nets": [],
|
||||
"high_contrast_mode": 0,
|
||||
"net_color_mode": 1,
|
||||
"opacity": {
|
||||
"images": 0.6,
|
||||
"pads": 1.0,
|
||||
"shapes": 1.0,
|
||||
"tracks": 1.0,
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"prototype_zone_fills": false,
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
"graphics": true,
|
||||
"keepouts": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pads": true,
|
||||
"text": true,
|
||||
"tracks": true,
|
||||
"vias": true,
|
||||
"zones": true
|
||||
},
|
||||
"visible_items": [
|
||||
"vias",
|
||||
"footprint_text",
|
||||
"footprint_anchors",
|
||||
"ratsnest",
|
||||
"grid",
|
||||
"footprints_front",
|
||||
"footprints_back",
|
||||
"footprint_values",
|
||||
"footprint_references",
|
||||
"tracks",
|
||||
"drc_errors",
|
||||
"drawing_sheet",
|
||||
"bitmaps",
|
||||
"pads",
|
||||
"zones",
|
||||
"drc_warnings",
|
||||
"drc_exclusions",
|
||||
"locked_item_shadows",
|
||||
"conflict_shadows",
|
||||
"shapes"
|
||||
],
|
||||
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"git": {
|
||||
"integration_disabled": false,
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
},
|
||||
"meta": {
|
||||
"filename": "bms.kicad_prl",
|
||||
"version": 5
|
||||
},
|
||||
"net_inspector_panel": {
|
||||
"col_hidden": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"col_order": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9
|
||||
],
|
||||
"col_widths": [],
|
||||
"custom_group_rules": [],
|
||||
"expanded_rows": [],
|
||||
"filter_by_net_name": true,
|
||||
"filter_by_netclass": true,
|
||||
"filter_text": "",
|
||||
"group_by_constraint": false,
|
||||
"group_by_netclass": false,
|
||||
"show_time_domain_details": false,
|
||||
"show_unconnected_nets": false,
|
||||
"show_zero_pad_nets": false,
|
||||
"sort_ascending": true,
|
||||
"sorting_column": -1
|
||||
},
|
||||
"open_jobsets": [],
|
||||
"project": {
|
||||
"files": []
|
||||
},
|
||||
"schematic": {
|
||||
"hierarchy_collapsed": [],
|
||||
"selection_filter": {
|
||||
"graphics": true,
|
||||
"images": true,
|
||||
"labels": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pins": true,
|
||||
"ruleAreas": true,
|
||||
"symbols": true,
|
||||
"text": true,
|
||||
"wires": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,980 @@
|
||||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_barcodes": false,
|
||||
"apply_defaults_to_fp_dimensions": false,
|
||||
"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.8,
|
||||
"height": 1.27,
|
||||
"width": 2.54
|
||||
},
|
||||
"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": {
|
||||
"border_display_style": 2,
|
||||
"border_hatch_pitch": 0.5,
|
||||
"corner_radius": 0.0,
|
||||
"corner_smoothing": 0,
|
||||
"fill_mode": 0,
|
||||
"hatch_gap": 1.5,
|
||||
"hatch_orientation": 0.0,
|
||||
"hatch_smoothing_level": 0,
|
||||
"hatch_smoothing_value": 0.1,
|
||||
"hatch_thickness": 1.0,
|
||||
"min_clearance": 0.5,
|
||||
"min_island_area": 10.0,
|
||||
"min_thickness": 0.25,
|
||||
"pad_connection": 1,
|
||||
"remove_islands": 0,
|
||||
"thermal_relief_gap": 0.5,
|
||||
"thermal_relief_spoke_width": 0.5
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [
|
||||
{
|
||||
"gap": 0.0,
|
||||
"via_gap": 0.0,
|
||||
"width": 0.0
|
||||
}
|
||||
],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "error",
|
||||
"copper_sliver": "warning",
|
||||
"courtyards_overlap": "error",
|
||||
"creepage": "error",
|
||||
"diff_pair_gap_out_of_range": "error",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_symbol_field_mismatch": "warning",
|
||||
"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",
|
||||
"missing_tuning_profile": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "error",
|
||||
"padstack": "warning",
|
||||
"pth_inside_courtyard": "error",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "ignore",
|
||||
"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_not_centered_on_via": "ignore",
|
||||
"track_on_post_machined_layer": "error",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"tuning_profile_track_geometries": "ignore",
|
||||
"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.2,
|
||||
"min_via_annular_width": 0.1,
|
||||
"min_via_diameter": 0.5,
|
||||
"solder_mask_to_copper_clearance": 0.005,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_onpthpad": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_onsmdpad": true,
|
||||
"td_ontrackend": false,
|
||||
"td_onvia": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [
|
||||
0.0,
|
||||
0.2,
|
||||
0.5,
|
||||
1.0,
|
||||
2.0,
|
||||
5.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": {
|
||||
"bom_rev": "",
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": "",
|
||||
"sch_revision": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"component_class_settings": {
|
||||
"assignments": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"sheet_component_classes": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"field_name_whitespace": "warning",
|
||||
"footprint_filter": "ignore",
|
||||
"footprint_link_issues": "warning",
|
||||
"four_way_junction": "ignore",
|
||||
"global_label_dangling": "warning",
|
||||
"ground_pin_not_ground": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"isolated_pin_label": "warning",
|
||||
"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",
|
||||
"stacked_pin_name": "warning",
|
||||
"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": "bms.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,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 5
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": []
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"pos_files": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"svg": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"annotation": {
|
||||
"method": 0,
|
||||
"sort_order": 0
|
||||
},
|
||||
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from BOM",
|
||||
"name": "${EXCLUDE_FROM_BOM}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Board",
|
||||
"name": "${EXCLUDE_FROM_BOARD}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Actuator/Cap Color",
|
||||
"name": "Actuator/Cap Color",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Attrition Qty",
|
||||
"name": "Attrition Qty",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Capacitance",
|
||||
"name": "Capacitance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Category",
|
||||
"name": "Category",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Circuit",
|
||||
"name": "Circuit",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Class",
|
||||
"name": "Class",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Contact Current",
|
||||
"name": "Contact Current",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Diode Configuration",
|
||||
"name": "Diode Configuration",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Field5",
|
||||
"name": "Field5",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Forward Voltage (Vf@If)",
|
||||
"name": "Forward Voltage (Vf@If)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Insulation Resistance",
|
||||
"name": "Insulation Resistance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "LCSC",
|
||||
"name": "LCSC",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "LCSC_PART_NUMBER",
|
||||
"name": "LCSC_PART_NUMBER",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Manufacturer",
|
||||
"name": "Manufacturer",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Mechanical Life",
|
||||
"name": "Mechanical Life",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Minimum Qty",
|
||||
"name": "Minimum Qty",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Mounting Style",
|
||||
"name": "Mounting Style",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Operating Force",
|
||||
"name": "Operating Force",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Operating Temperature",
|
||||
"name": "Operating Temperature",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Operating Temperature Range",
|
||||
"name": "Operating Temperature Range",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Overload Voltage (Max)",
|
||||
"name": "Overload Voltage (Max)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Part",
|
||||
"name": "Part",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Pin Style",
|
||||
"name": "Pin Style",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Power(Watts)",
|
||||
"name": "Power(Watts)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Price",
|
||||
"name": "Price",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Process",
|
||||
"name": "Process",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Rectified Current",
|
||||
"name": "Rectified Current",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Resistance",
|
||||
"name": "Resistance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reverse Leakage Current",
|
||||
"name": "Reverse Leakage Current",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reverse Voltage (Vr)",
|
||||
"name": "Reverse Voltage (Vr)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Sim.Device",
|
||||
"name": "Sim.Device",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Sim.Pins",
|
||||
"name": "Sim.Pins",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Sim.Type",
|
||||
"name": "Sim.Type",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Stock",
|
||||
"name": "Stock",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Strike Gundam",
|
||||
"name": "Strike Gundam",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Switch Height",
|
||||
"name": "Switch Height",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Switch Length",
|
||||
"name": "Switch Length",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Switch Width",
|
||||
"name": "Switch Width",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Temperature Coefficient",
|
||||
"name": "Temperature Coefficient",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Tolerance",
|
||||
"name": "Tolerance",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Type",
|
||||
"name": "Type",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Voltage Rated",
|
||||
"name": "Voltage Rated",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Voltage Rating (Dc)",
|
||||
"name": "Voltage Rating (Dc)",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "With Lamp",
|
||||
"name": "With Lamp",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Actuator Style",
|
||||
"name": "Actuator Style",
|
||||
"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"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"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": [],
|
||||
"hop_over_size_choice": 0,
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.375,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.15
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"reuse_designators": true,
|
||||
"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,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "bms.kicad_sch",
|
||||
"name": "bms",
|
||||
"uuid": "7972d0e7-2611-420d-b298-ef8307db6186"
|
||||
}
|
||||
],
|
||||
"used_designators": "",
|
||||
"variants": []
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"7972d0e7-2611-420d-b298-ef8307db6186",
|
||||
"bms"
|
||||
]
|
||||
],
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"ARCHIVE_NAME": "", "EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false, "OPEN BROWSER": true, "NO_BACKUP_OPT": false}
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
(footprint "AMASS_XT30UPB+DATA-M_1x02_P5.0mm_Vertical"
|
||||
(version 20240108)
|
||||
(generator "pcbnew")
|
||||
(generator_version "8.0")
|
||||
(layer "F.Cu")
|
||||
(descr "Connector XT30 Vertical PCB Male, https://www.tme.eu/en/Document/4acc913878197f8c2e30d4b8cdc47230/XT30UPB%20SPEC.pdf")
|
||||
(tags "RC Connector XT30")
|
||||
(property "Reference" "REF**"
|
||||
(at 2.5 -4 0)
|
||||
(layer "F.SilkS")
|
||||
(uuid "f7510d54-dcb1-4c3b-b842-cd250a98370c")
|
||||
(effects
|
||||
(font
|
||||
(size 1 1)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(property "Value" "AMASS_XT30UPB+DATA-M_1x02_P5.0mm_Vertical"
|
||||
(at 2.5 4 0)
|
||||
(layer "F.Fab")
|
||||
(uuid "c5a8a60c-4ea1-4401-a30c-34d36be61c07")
|
||||
(effects
|
||||
(font
|
||||
(size 1 1)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(property "Footprint" ""
|
||||
(at 0 0 0)
|
||||
(unlocked yes)
|
||||
(layer "F.Fab")
|
||||
(hide yes)
|
||||
(uuid "8fb27306-c085-4316-b554-4ba9be794054")
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(property "Datasheet" ""
|
||||
(at 0 0 0)
|
||||
(unlocked yes)
|
||||
(layer "F.Fab")
|
||||
(hide yes)
|
||||
(uuid "74b71861-05d2-4229-8e81-25952aaaef7e")
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(property "Description" ""
|
||||
(at 0 0 0)
|
||||
(unlocked yes)
|
||||
(layer "F.Fab")
|
||||
(hide yes)
|
||||
(uuid "a1d16ecc-7e64-48c4-b772-a9255380960d")
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(attr through_hole)
|
||||
(fp_line
|
||||
(start -2.71 -1.41)
|
||||
(end -2.71 1.41)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "e96c6ad2-9ca6-4df1-b35b-76e090d7ff4e")
|
||||
)
|
||||
(fp_line
|
||||
(start -2.71 -1.41)
|
||||
(end -1.01 -2.71)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "0784d204-0a48-4a2b-8085-50e1ff7a1493")
|
||||
)
|
||||
(fp_line
|
||||
(start -2.71 1.41)
|
||||
(end -1.01 2.71)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "db750970-e424-4a8e-a882-20a90baabffc")
|
||||
)
|
||||
(fp_line
|
||||
(start -1.01 -2.71)
|
||||
(end 7.71 -2.71)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "9f23420e-db87-438c-a708-676a0616966e")
|
||||
)
|
||||
(fp_line
|
||||
(start -1.01 2.71)
|
||||
(end 7.71 2.71)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "2ba574e3-9de5-4d7d-9777-e19e6fa702e7")
|
||||
)
|
||||
(fp_line
|
||||
(start 7.71 -2.71)
|
||||
(end 7.71 2.71)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "2018cc6b-6115-4763-8b7c-54b60affbda7")
|
||||
)
|
||||
(fp_rect
|
||||
(start -6.3 -2.71)
|
||||
(end 7.71 2.7)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type default)
|
||||
)
|
||||
(fill none)
|
||||
(layer "F.SilkS")
|
||||
(uuid "11aac399-c862-4b67-9828-087abeea5b1b")
|
||||
)
|
||||
(fp_line
|
||||
(start -3.1 -1.8)
|
||||
(end -3.1 1.8)
|
||||
(stroke
|
||||
(width 0.05)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.CrtYd")
|
||||
(uuid "06ae69d8-1372-4524-8b1a-27a2f062f1c5")
|
||||
)
|
||||
(fp_line
|
||||
(start -3.1 -1.8)
|
||||
(end -1.4 -3.1)
|
||||
(stroke
|
||||
(width 0.05)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.CrtYd")
|
||||
(uuid "ae869a92-c688-4f2b-82ca-0578106a035a")
|
||||
)
|
||||
(fp_line
|
||||
(start -3.1 1.8)
|
||||
(end -1.4 3.1)
|
||||
(stroke
|
||||
(width 0.05)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.CrtYd")
|
||||
(uuid "cac6d927-6ba1-4095-825b-f94ee0d7abe9")
|
||||
)
|
||||
(fp_line
|
||||
(start -1.4 -3.1)
|
||||
(end 8.1 -3.1)
|
||||
(stroke
|
||||
(width 0.05)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.CrtYd")
|
||||
(uuid "fb4fa373-5492-4717-a9fe-7b69f4c53ba0")
|
||||
)
|
||||
(fp_line
|
||||
(start -1.4 3.1)
|
||||
(end 8.1 3.1)
|
||||
(stroke
|
||||
(width 0.05)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.CrtYd")
|
||||
(uuid "70296c77-546d-44a2-b5d3-e6dc58cf713b")
|
||||
)
|
||||
(fp_line
|
||||
(start 8.1 -3.1)
|
||||
(end 8.1 3.1)
|
||||
(stroke
|
||||
(width 0.05)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.CrtYd")
|
||||
(uuid "64764a09-de32-4f35-b54a-17e44810370f")
|
||||
)
|
||||
(fp_line
|
||||
(start -2.6 -1.3)
|
||||
(end -2.6 1.3)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "8d7ee7cb-5dda-453f-aa9a-6420c87f1b8e")
|
||||
)
|
||||
(fp_line
|
||||
(start -2.6 -1.3)
|
||||
(end -0.9 -2.6)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "2fa3ad90-36bb-4374-95ed-e44e50c7e385")
|
||||
)
|
||||
(fp_line
|
||||
(start -2.6 1.3)
|
||||
(end -0.9 2.6)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "2e4f8556-ffc2-4791-91da-e68c3513337e")
|
||||
)
|
||||
(fp_line
|
||||
(start -0.9 -2.6)
|
||||
(end 7.6 -2.6)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "cb9cd8af-1997-41db-b9fe-8982960ac6db")
|
||||
)
|
||||
(fp_line
|
||||
(start -0.9 2.6)
|
||||
(end 7.6 2.6)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "ea8a6c02-e974-4677-a854-a0891c323245")
|
||||
)
|
||||
(fp_line
|
||||
(start 7.6 -2.6)
|
||||
(end 7.6 2.6)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "c66f305c-0d56-4591-bc02-23252ad20321")
|
||||
)
|
||||
(fp_text user "-"
|
||||
(at -4 0 0)
|
||||
(layer "F.SilkS")
|
||||
(uuid "c119570a-6846-48dc-9422-0b5665ab2df6")
|
||||
(effects
|
||||
(font
|
||||
(size 1.5 1.5)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(fp_text user "+"
|
||||
(at 9 0 0)
|
||||
(layer "F.SilkS")
|
||||
(uuid "d6ab678c-47f7-47e0-869b-ef3b9dbd1ba9")
|
||||
(effects
|
||||
(font
|
||||
(size 1.5 1.5)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(fp_text user "${REFERENCE}"
|
||||
(at 2.5 0 0)
|
||||
(layer "F.Fab")
|
||||
(uuid "a70efd12-1491-4664-ae98-5b2b7f52a502")
|
||||
(effects
|
||||
(font
|
||||
(size 1 1)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pad "1" thru_hole rect
|
||||
(at 0 0)
|
||||
(size 3 3)
|
||||
(drill 1.8)
|
||||
(layers "*.Cu" "*.Mask")
|
||||
(remove_unused_layers no)
|
||||
(uuid "3a0f3b23-814b-4df9-a02c-3d9fed9e23c9")
|
||||
)
|
||||
(pad "2" thru_hole circle
|
||||
(at 5 0)
|
||||
(size 3 3)
|
||||
(drill 1.8)
|
||||
(layers "*.Cu" "*.Mask")
|
||||
(remove_unused_layers no)
|
||||
(uuid "d897d74a-a13b-47cf-9806-eb8a75fe8d08")
|
||||
)
|
||||
(pad "3" thru_hole circle
|
||||
(at -3.9 -1)
|
||||
(size 1.524 1.524)
|
||||
(drill 1)
|
||||
(layers "*.Cu" "*.Mask")
|
||||
(remove_unused_layers no)
|
||||
(uuid "02a8c3fc-d75c-47a4-a907-f9191ff19e2c")
|
||||
)
|
||||
(pad "4" thru_hole circle
|
||||
(at -3.9 1)
|
||||
(size 1.524 1.524)
|
||||
(drill 1)
|
||||
(layers "*.Cu" "*.Mask")
|
||||
(remove_unused_layers no)
|
||||
(uuid "2000b5b8-f7c9-40a8-9010-65c16d2aefce")
|
||||
)
|
||||
(model "${KICAD8_3DMODEL_DIR}/Connector_AMASS.3dshapes/AMASS_XT30UPB-M_1x02_P5.0mm_Vertical.wrl"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
(scale
|
||||
(xyz 1 1 1)
|
||||
)
|
||||
(rotate
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
(fp_lib_table
|
||||
(version 7)
|
||||
(lib (name "amass") (type "KiCad") (uri "${KIPRJMOD}/footprints/amass") (options "") (descr ""))
|
||||
)
|
||||
Generated
+1
@@ -66,6 +66,7 @@ dependencies = [
|
||||
"embedded-can",
|
||||
"heapless",
|
||||
"log",
|
||||
"nb 1.1.0",
|
||||
"panic-halt",
|
||||
"qingke",
|
||||
"qingke-rt",
|
||||
|
||||
@@ -28,6 +28,7 @@ embassy-usb = { version = "0.3.0" }
|
||||
embassy-futures = { version = "0.1.0" }
|
||||
embassy-sync = { version = "0.6.0" }
|
||||
embedded-can = "0.4.1"
|
||||
nb = "1.1"
|
||||
embedded-alloc = { version = "0.6.0", default-features = false, features = ["llff"] }
|
||||
|
||||
# This is okay because we should automatically use whatever ch32-hal uses
|
||||
|
||||
@@ -8,4 +8,15 @@ fn main() {
|
||||
std::fs::write(out_dir.join("memory.x"), include_bytes!("memory.x")).unwrap();
|
||||
println!("cargo:rustc-link-search={}", out_dir.display());
|
||||
println!("cargo:rerun-if-changed=memory.x");
|
||||
|
||||
// Embed firmware build timestamp as minutes since Unix epoch (4 bytes, big-endian).
|
||||
// Dropping sub-minute precision keeps it in 4 bytes for many years.
|
||||
let build_seconds = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("System time before UNIX_EPOCH")
|
||||
.as_secs();
|
||||
let build_minutes = (build_seconds / 60) as u32;
|
||||
let bytes = build_minutes.to_be_bytes();
|
||||
std::fs::write(out_dir.join("build_minutes.bin"), bytes).unwrap();
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
+478
-181
@@ -3,29 +3,29 @@
|
||||
extern crate alloc;
|
||||
|
||||
use crate::hal::peripherals::CAN1;
|
||||
use canapi::id::{plant_id, MessageKind, IDENTIFY_CMD_OFFSET, MOISTURE_DATA_OFFSET};
|
||||
use canapi::id::{plant_id, FIRMWARE_BUILD_OFFSET, IDENTIFY_CMD_OFFSET, MOISTURE_DATA_OFFSET};
|
||||
use canapi::SensorSlot;
|
||||
use ch32_hal::adc::{Adc, SampleTime, ADC_MAX};
|
||||
use ch32_hal::can;
|
||||
use ch32_hal::{pac};
|
||||
use ch32_hal::can::{Can, CanFifo, CanFilter, CanFrame, CanMode};
|
||||
use ch32_hal::gpio::{Flex, Level, Output, Pull, Speed};
|
||||
use ch32_hal::mode::NonBlocking;
|
||||
use ch32_hal::peripherals::USBD;
|
||||
use core::fmt::Write as _;
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use embassy_executor::{task, Spawner};
|
||||
use embassy_futures::yield_now;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::channel::Channel;
|
||||
use embassy_time::{Delay, Duration, Instant, Timer};
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
|
||||
use embassy_usb::{Builder, UsbDevice};
|
||||
use embedded_alloc::LlffHeap as Heap;
|
||||
use embedded_can::nb::Can as nb_can;
|
||||
use embedded_can::{Id, StandardId};
|
||||
use hal::bind_interrupts;
|
||||
use hal::usbd::Driver;
|
||||
use {ch32_hal as hal, panic_halt as _};
|
||||
use embedded_alloc::LlffHeap as Heap;
|
||||
use embedded_can::nb::Can as nb_can;
|
||||
|
||||
|
||||
macro_rules! mk_static {
|
||||
($t:ty,$val:expr) => {{
|
||||
@@ -43,13 +43,22 @@ bind_interrupts!(struct Irqs {
|
||||
#[global_allocator]
|
||||
static HEAP: Heap = Heap::empty();
|
||||
static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new();
|
||||
static CAN_TX_CH: Channel<CriticalSectionRawMutex, CanFrame, 4> = Channel::new();
|
||||
|
||||
static BEACON: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Firmware build timestamp in minutes since Unix epoch, embedded at compile time.
|
||||
const FIRMWARE_BUILD_MINUTES: u32 =
|
||||
u32::from_be_bytes(*include_bytes!(concat!(env!("OUT_DIR"), "/build_minutes.bin")));
|
||||
|
||||
#[embassy_executor::main(entry = "qingke_rt::entry")]
|
||||
async fn main(spawner: Spawner) {
|
||||
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
|
||||
|
||||
unsafe {
|
||||
static mut HEAP_SPACE: [u8; 4096] = [0; 4096]; // 4 KiB heap, adjust as needed
|
||||
#[allow(static_mut_refs)]
|
||||
static mut HEAP_SPACE: [u8; 4096] = [0; 4096]; // 4 KiB heap
|
||||
#[allow(static_mut_refs)]
|
||||
HEAP.init(HEAP_SPACE.as_ptr() as usize, HEAP_SPACE.len());
|
||||
}
|
||||
|
||||
@@ -61,9 +70,80 @@ async fn main(spawner: Spawner) {
|
||||
// Build driver and USB stack using 'static buffers
|
||||
let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11);
|
||||
|
||||
let mut probe_gnd = Flex::new(p.PA2);
|
||||
probe_gnd.set_as_input(Pull::None);
|
||||
|
||||
// Create GPIO for 555 Q output (PB0)
|
||||
let q_out = Output::new(p.PA0, Level::Low, Speed::Low);
|
||||
let info = mk_static!(Output, Output::new(p.PA10, Level::Low, Speed::Low));
|
||||
let warn = mk_static!(Output, Output::new(p.PA9, Level::Low, Speed::Low));
|
||||
|
||||
// Read configuration switches on PB3..PB7 at startup with floating detection
|
||||
// PB3: Sensor A/B selector (Low=A, High=B)
|
||||
// PB4..PB7: address bits (1,2,4,8)
|
||||
let mut sensor_ab_pin = Flex::new(p.PA3);
|
||||
let mut sensor_address_bit_1_pin = Flex::new(p.PA4);
|
||||
let mut sensor_address_bit_2_pin = Flex::new(p.PA5);
|
||||
let mut sensor_address_bit_3_pin = Flex::new(p.PA6);
|
||||
let mut sensor_address_bit_4_pin = Flex::new(p.PA7);
|
||||
|
||||
// Validate all config pins; if any is floating, stay in an error loop until fixed
|
||||
// Try read PB3..PB7
|
||||
let sensor_ab_config = detect_stable_pin(&mut sensor_ab_pin).await;
|
||||
let sensor_address_bit_1_config = detect_stable_pin(&mut sensor_address_bit_1_pin).await;
|
||||
let sensor_address_bit_2_config = detect_stable_pin(&mut sensor_address_bit_2_pin).await;
|
||||
let sensor_address_bit_3_config = detect_stable_pin(&mut sensor_address_bit_3_pin).await;
|
||||
let sensor_address_bit_4_config = detect_stable_pin(&mut sensor_address_bit_4_pin).await;
|
||||
|
||||
let slot = if sensor_ab_config.unwrap_or(false) {
|
||||
SensorSlot::B
|
||||
} else {
|
||||
SensorSlot::A
|
||||
};
|
||||
let mut addr: u8 = 0;
|
||||
if sensor_address_bit_1_config.unwrap_or(false) {
|
||||
addr |= 1;
|
||||
}
|
||||
if sensor_address_bit_2_config.unwrap_or(false) {
|
||||
addr |= 2;
|
||||
}
|
||||
if sensor_address_bit_3_config.unwrap_or(false) {
|
||||
addr |= 4;
|
||||
}
|
||||
if sensor_address_bit_4_config.unwrap_or(false) {
|
||||
addr |= 8;
|
||||
}
|
||||
let moisture_id = plant_id(MOISTURE_DATA_OFFSET, slot, addr as u16);
|
||||
let identify_id = plant_id(IDENTIFY_CMD_OFFSET, slot, addr as u16);
|
||||
let firmware_build_id = plant_id(FIRMWARE_BUILD_OFFSET, slot, addr as u16);
|
||||
let standard_identify_id = StandardId::new(identify_id).unwrap();
|
||||
|
||||
//is any floating, or invalid addr (only 1-8 are valid)
|
||||
let invalid_config = sensor_ab_config.is_none()
|
||||
|| sensor_address_bit_1_config.is_none()
|
||||
|| sensor_address_bit_2_config.is_none()
|
||||
|| sensor_address_bit_3_config.is_none()
|
||||
|| sensor_address_bit_4_config.is_none()
|
||||
|| addr == 0
|
||||
|| addr > 8;
|
||||
|
||||
let mut config = embassy_usb::Config::new(0xC0DE, 0xCAFE);
|
||||
config.manufacturer = Some("Embassy");
|
||||
config.product = Some("USB-serial example");
|
||||
config.manufacturer = Some("Can Sensor v0.2");
|
||||
let msg = mk_static!(heapless::String<128>, heapless::String::new());
|
||||
if invalid_config {
|
||||
let _ = core::fmt::Write::write_fmt(
|
||||
msg,
|
||||
format_args!(
|
||||
"CFG err: {:?} {:?} {:?} {:?} {:?}",
|
||||
to_info(sensor_ab_config), to_info(sensor_address_bit_1_config), to_info(sensor_address_bit_2_config), to_info(sensor_address_bit_3_config), to_info(sensor_address_bit_4_config)
|
||||
),
|
||||
);
|
||||
|
||||
} else {
|
||||
let _ = core::fmt::Write::write_fmt(msg, format_args!("Sensor {:?} plant {}", slot, addr));
|
||||
}
|
||||
|
||||
config.product = Some(msg.as_str());
|
||||
config.serial_number = Some("12345678");
|
||||
config.max_power = 100;
|
||||
config.max_packet_size_0 = 64;
|
||||
@@ -91,82 +171,139 @@ async fn main(spawner: Spawner) {
|
||||
// Build USB device
|
||||
let usb = mk_static!(UsbDevice<Driver<USBD>>, builder.build());
|
||||
|
||||
// Create GPIO for 555 Q output (PB0)
|
||||
let q_out = Output::new(p.PB0, Level::Low, Speed::Low);
|
||||
// Built-in LED on PB2 mirrors Q state
|
||||
let mut info = Output::new(p.PB2, Level::Low, Speed::Low);
|
||||
|
||||
// Read configuration switches on PB3..PB7 at startup with floating detection
|
||||
// PB3: Sensor A/B selector (Low=A, High=B)
|
||||
// PB4..PB7: address bits (1,2,4,8)
|
||||
let mut pb3 = Flex::new(p.PB3);
|
||||
let mut pb4 = Flex::new(p.PB4);
|
||||
let mut pb5 = Flex::new(p.PB5);
|
||||
let mut pb6 = Flex::new(p.PB6);
|
||||
let mut pb7 = Flex::new(p.PB7);
|
||||
|
||||
// Validate all config pins; if any is floating, stay in an error loop until fixed
|
||||
// Try read PB3..PB7
|
||||
let res_pb3 = detect_stable_pin(&mut pb3).await;
|
||||
let res_pb4 = detect_stable_pin(&mut pb4).await;
|
||||
let res_pb5 = detect_stable_pin(&mut pb5).await;
|
||||
let res_pb6 = detect_stable_pin(&mut pb6).await;
|
||||
let res_pb7 = detect_stable_pin(&mut pb7).await;
|
||||
|
||||
if res_pb3.is_none() || res_pb4.is_none() || res_pb5.is_none() || res_pb6.is_none() || res_pb7.is_none() {
|
||||
// At least one floating: report and blink code for the first one found.
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let code = if res_pb4.is_none() { 1 } else if res_pb5.is_none() { 2 } else if res_pb6.is_none() { 3 } else if res_pb7.is_none() { 4 } else { 5 }; // PB3 -> 5
|
||||
let which = match code { 1 => "PB4", 2 => "PB5", 3 => "PB6", 4 => "PB7", _ => "PB3 (A/B)" };
|
||||
let _ = core::fmt::Write::write_fmt(&mut msg, format_args!("Config pin floating detected on {} -> blinking code {}. Fix jumpers.\r\n", which, code));
|
||||
log(msg);
|
||||
blink_error(&mut info, code).await;
|
||||
};
|
||||
let slot = if res_pb3.unwrap() { SensorSlot::B } else { SensorSlot::A };
|
||||
let mut addr: u8 = 0;
|
||||
if res_pb4.unwrap() { addr |= 1; }
|
||||
if res_pb5.unwrap() { addr |= 2; }
|
||||
if res_pb6.unwrap() { addr |= 4; }
|
||||
if res_pb7.unwrap() { addr |= 8; }
|
||||
|
||||
// Log startup configuration and derived CAN IDs
|
||||
let moisture_id = plant_id(MOISTURE_DATA_OFFSET, slot, addr as u16);
|
||||
let identify_id = plant_id(IDENTIFY_CMD_OFFSET, slot, addr as u16);
|
||||
{
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let slot_chr = match slot { SensorSlot::A => 'a', SensorSlot::B => 'b' };
|
||||
let _ = core::fmt::Write::write_fmt(&mut msg, format_args!(
|
||||
"Startup: slot={} addr={} moisture_id=0x{:03X} identity_id=0x{:03X}\r\n",
|
||||
slot_chr, addr, moisture_id, identify_id
|
||||
));
|
||||
log(msg);
|
||||
}
|
||||
|
||||
// Create ADC on ADC1 and use PA1 as analog input (Threshold/Trigger)
|
||||
let adc = Adc::new(p.ADC1, Default::default());
|
||||
let ain = p.PA1;
|
||||
let config = can::can::Config::default();
|
||||
let can: Can<CAN1, NonBlocking> = Can::new_nb(
|
||||
p.CAN1,
|
||||
p.PB8,
|
||||
p.PB9,
|
||||
CanFifo::Fifo0,
|
||||
CanMode::Normal,
|
||||
125_000,
|
||||
config,
|
||||
)
|
||||
.expect("Valid");
|
||||
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
|
||||
|
||||
spawner.spawn(usb_task(usb)).unwrap();
|
||||
spawner.spawn(usb_writer(class)).unwrap();
|
||||
// move Q output, LED, ADC and analog input into worker task
|
||||
spawner.spawn(worker(q_out, info, adc, ain, can, StandardId::new(moisture_id).unwrap(), StandardId::new(identify_id).unwrap())).unwrap();
|
||||
|
||||
if invalid_config {
|
||||
// At least one floating: report and blink code for the first one found.
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let code = if sensor_address_bit_1_config.is_none() {
|
||||
1
|
||||
} else if sensor_address_bit_2_config.is_none() {
|
||||
2
|
||||
} else if sensor_address_bit_3_config.is_none() {
|
||||
3
|
||||
} else if sensor_address_bit_4_config.is_none() {
|
||||
4
|
||||
} else if sensor_ab_config.is_none() {
|
||||
5
|
||||
} else {
|
||||
6 // Invalid address (0 or > 8)
|
||||
};
|
||||
let which = match code {
|
||||
1 => "PB4 (bit 1)",
|
||||
2 => "PB5 (bit 2)",
|
||||
3 => "PB6 (bit 3)",
|
||||
4 => "PB7 (bit 4)",
|
||||
5 => "PB3 (A/B)",
|
||||
_ => "Address (0 or > 8)",
|
||||
};
|
||||
if code == 6 {
|
||||
let _ = core::fmt::Write::write_fmt(
|
||||
&mut msg,
|
||||
format_args!(
|
||||
"Invalid address {} (only 1-8 allowed) -> blinking code {}. Fix jumpers.\r\n",
|
||||
addr, code
|
||||
),
|
||||
);
|
||||
} else {
|
||||
let _ = core::fmt::Write::write_fmt(
|
||||
&mut msg,
|
||||
format_args!(
|
||||
"Config pin floating detected on {} -> blinking code {}. Fix jumpers.\r\n",
|
||||
which, code
|
||||
),
|
||||
);
|
||||
}
|
||||
log(msg);
|
||||
spawner.spawn(blink_error_task(warn, info, 2, code)).unwrap();
|
||||
} else {
|
||||
// Log startup configuration and derived CAN IDs
|
||||
|
||||
{
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let slot_chr = match slot {
|
||||
SensorSlot::A => 'a',
|
||||
SensorSlot::B => 'b',
|
||||
};
|
||||
let _ = core::fmt::Write::write_fmt(
|
||||
&mut msg,
|
||||
format_args!(
|
||||
"Startup: slot={} addr={} moisture_id=0x{:03X} identity_id=0x{:03X}\r\n",
|
||||
slot_chr, addr, moisture_id, identify_id
|
||||
),
|
||||
);
|
||||
log(msg);
|
||||
}
|
||||
|
||||
// Create ADC on ADC1 and use PA1 as analog input (Threshold/Trigger)
|
||||
let adc = Adc::new(p.ADC1, Default::default());
|
||||
let ain = p.PA1;
|
||||
let config = Default::default();
|
||||
let can: Can<CAN1, NonBlocking> = Can::new_nb(
|
||||
p.CAN1,
|
||||
p.PB8,
|
||||
p.PB9,
|
||||
CanFifo::Fifo0,
|
||||
CanMode::Normal,
|
||||
20_000,
|
||||
config,
|
||||
)
|
||||
.expect("Valid");
|
||||
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
|
||||
|
||||
can.add_filter(CanFilter::accept_all());
|
||||
|
||||
// Improve CAN robustness for longer cables:
|
||||
// 1. Enable Automatic Bus-Off Management (ABOM)
|
||||
// 2. Enable Automatic Retransmission (NART) to recover from transient errors
|
||||
// 3. Enable Receive FIFO Overwrite Mode (RFLM = 0, default)
|
||||
// 4. Increase Resync Jump Width (SJW) if possible by patching BTIMR
|
||||
hal::pac::CAN1.ctlr().modify(|w| {
|
||||
w.set_abom(false);
|
||||
w.set_nart(false);
|
||||
});
|
||||
|
||||
// SJW is bits 24-25 of BTIMR. HAL sets it to 0 (SJW=1).
|
||||
// Let's try to set it to 3 (SJW=4) for better jitter tolerance.
|
||||
hal::pac::CAN1.btimr().modify(|w| {
|
||||
w.set_sjw(3); // 3 means 4TQ
|
||||
});
|
||||
|
||||
// let mut filter = CanFilter::new_id_list();
|
||||
// filter.get(0).unwrap().set(Id::Standard(standard_identify_id), Default::default());
|
||||
// can.add_filter(filter);
|
||||
let standard_moisture_id = StandardId::new(moisture_id).unwrap();
|
||||
let standard_firmware_build_id = StandardId::new(firmware_build_id).unwrap();
|
||||
spawner
|
||||
.spawn(can_task(can, info, warn, standard_identify_id, standard_moisture_id, standard_firmware_build_id))
|
||||
.unwrap();
|
||||
|
||||
// move Q output, LED, ADC and analog input into worker task
|
||||
spawner
|
||||
.spawn(worker(
|
||||
probe_gnd,
|
||||
q_out,
|
||||
adc,
|
||||
ain,
|
||||
standard_moisture_id,
|
||||
standard_identify_id,
|
||||
standard_firmware_build_id,
|
||||
slot,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
// Prevent main from exiting
|
||||
core::future::pending::<()>().await;
|
||||
}
|
||||
|
||||
fn to_info(res: Option<bool>) -> i8 {
|
||||
match res {
|
||||
Some(true) => 1,
|
||||
Some(false) => -1,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper closure: detect stable pin by comparing readings under Pull::Down and Pull::Up
|
||||
async fn detect_stable_pin(pin: &mut Flex<'static>) -> Option<bool> {
|
||||
@@ -176,118 +313,74 @@ async fn detect_stable_pin(pin: &mut Flex<'static>) -> Option<bool> {
|
||||
pin.set_as_input(Pull::Up);
|
||||
Timer::after_millis(2).await;
|
||||
let high_read = pin.is_high();
|
||||
if low_read == high_read { Some(high_read) } else { None }
|
||||
if low_read == high_read {
|
||||
Some(high_read)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
async fn blink_error(mut info_led: &mut Output<'static>, code: u8) -> !{
|
||||
loop {
|
||||
// code: 1-4 for PB4..PB7, 5 for PB3 (A/B)
|
||||
for _ in 0..code {
|
||||
async fn blink_error_loop(info_led: &mut Output<'static>, warn_led: &mut Output<'static>, c_i: u8, c_w: u8) -> ! {
|
||||
for _loop_count in 0..5 {
|
||||
// code: 1-4 for PB4..PB7, 5 for PB3 (A/B), 7 for CAN address collision
|
||||
for _ in 0..c_i {
|
||||
info_led.set_high();
|
||||
Timer::after_millis(200).await;
|
||||
info_led.set_low();
|
||||
Timer::after_millis(200).await;
|
||||
}
|
||||
for _ in 0..c_w {
|
||||
warn_led.set_high();
|
||||
Timer::after_millis(200).await;
|
||||
warn_led.set_low();
|
||||
Timer::after_millis(200).await;
|
||||
}
|
||||
// Pause between sequences
|
||||
Timer::after_secs(2).await;
|
||||
Timer::after_millis(400).await;
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
info_led.set_high();
|
||||
Timer::after_millis(50).await;
|
||||
info_led.set_low();
|
||||
Timer::after_millis(50).await;
|
||||
warn_led.set_high();
|
||||
Timer::after_millis(50).await;
|
||||
warn_led.set_low();
|
||||
Timer::after_millis(50).await;
|
||||
}
|
||||
|
||||
pac::PFIC.cfgr().modify(|w| {
|
||||
w.set_resetsys(true);
|
||||
w.set_keycode(pac::pfic::vals::Keycode::KEY3); // KEY3 is 0xBEEF, the System Reset key
|
||||
});
|
||||
loop{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
async fn blink_error_task(info_led: &'static mut Output<'static>, warn_led: &'static mut Output<'static>, c_i: u8, c_w: u8) -> ! {
|
||||
blink_error_loop(info_led, warn_led, c_i, c_w).await
|
||||
}
|
||||
|
||||
#[task]
|
||||
async fn worker(
|
||||
mut q: Output<'static>,
|
||||
mut info: Output<'static>,
|
||||
mut adc: Adc<'static, hal::peripherals::ADC1>,
|
||||
mut ain: hal::peripherals::PA1,
|
||||
async fn can_task(
|
||||
mut can: Can<'static, CAN1, NonBlocking>,
|
||||
info: &'static mut Output<'static>,
|
||||
warn: &'static mut Output<'static>,
|
||||
identify_id: StandardId,
|
||||
moisture_id: StandardId,
|
||||
identify_id: StandardId
|
||||
_firmware_build_id: StandardId,
|
||||
) {
|
||||
// 555 emulation state: Q initially Low
|
||||
let mut q_high = false;
|
||||
let low_th: u16 = (ADC_MAX as u16) / 3; // ~1/3 Vref
|
||||
let high_th: u16 = ((ADC_MAX as u32 * 2) / 3) as u16; // ~2/3 Vref
|
||||
|
||||
let mut filter = CanFilter::new_id_list();
|
||||
|
||||
filter
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.set(identify_id.into(), Default::default());
|
||||
|
||||
can.add_filter(filter);
|
||||
//can.add_filter(CanFilter::accept_all());
|
||||
// Non-blocking beacon blink timing.
|
||||
// We keep this inside the CAN task so it can't stall other tasks (like `worker`) with `await`s.
|
||||
let mut next_beacon_toggle = Instant::now();
|
||||
let beacon_period = Duration::from_millis(50);
|
||||
|
||||
loop {
|
||||
// Count rising edges of Q in a 100 ms window
|
||||
let start = Instant::now();
|
||||
let mut pulses: u32 = 0;
|
||||
let mut last_q = q_high;
|
||||
|
||||
while Instant::now()
|
||||
.checked_duration_since(start)
|
||||
.unwrap_or(Duration::from_millis(0))
|
||||
< Duration::from_millis(1000)
|
||||
{
|
||||
// Sample the analog input (Threshold/Trigger on A1)
|
||||
let val: u16 = adc.convert(&mut ain, SampleTime::CYCLES28_5);
|
||||
|
||||
// 555 core behavior:
|
||||
// - If input <= 1/3 Vref => set Q high (trigger)
|
||||
// - If input >= 2/3 Vref => set Q low (threshold)
|
||||
// - Otherwise keep previous Q state (hysteresis)
|
||||
if val <= low_th {
|
||||
q_high = true;
|
||||
} else if val >= high_th {
|
||||
q_high = false;
|
||||
}
|
||||
|
||||
// Drive output pin accordingly
|
||||
if q_high {
|
||||
q.set_high();
|
||||
} else {
|
||||
q.set_low();
|
||||
}
|
||||
|
||||
// Count rising edges
|
||||
if !last_q && q_high {
|
||||
pulses = pulses.saturating_add(1);
|
||||
}
|
||||
last_q = q_high;
|
||||
|
||||
// Yield to allow USB and other tasks to run
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
// Compute frequency from 100 ms window
|
||||
let freq_hz = pulses; // pulses per 0.1s => Hz
|
||||
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(
|
||||
&mut msg,
|
||||
"555 window=100ms pulses={} freq={} Hz (A1->Q on PB0)\r\n",
|
||||
pulses, freq_hz
|
||||
);
|
||||
log(msg);
|
||||
|
||||
let mut moisture = CanFrame::new(moisture_id, &[freq_hz as u8]).unwrap();
|
||||
match can.transmit(&mut moisture) {
|
||||
Ok(..) => {
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(&mut msg, "Send to canbus");
|
||||
log(msg);
|
||||
}
|
||||
Err(err) => {
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(&mut msg, "err {:?}", err);
|
||||
log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
yield_now().await;
|
||||
match can.receive() {
|
||||
Ok(frame) => match frame.id() {
|
||||
match can.receive() {
|
||||
Ok(frame) => {
|
||||
match frame.id() {
|
||||
Id::Standard(s_frame) => {
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(
|
||||
@@ -297,21 +390,225 @@ async fn worker(
|
||||
identify_id.as_raw()
|
||||
);
|
||||
log(msg);
|
||||
if s_frame.as_raw() == moisture_id.as_raw() {
|
||||
//trigger collision detection on other side as well
|
||||
let _ = can.transmit(&frame);
|
||||
// We should never receive moisture packets addressed to ourselves.
|
||||
// If we do, another node likely uses the same jumper configuration.
|
||||
blink_error_loop(info, warn, 1,2).await;
|
||||
}
|
||||
if s_frame.as_raw() == identify_id.as_raw() {
|
||||
for _ in 0..10 {
|
||||
Timer::after_millis(250).await;
|
||||
info.toggle();
|
||||
}
|
||||
info.set_low();
|
||||
BEACON.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Id::Extended(_) => {}
|
||||
},
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(nb::Error::WouldBlock) => {
|
||||
// No frame available
|
||||
}
|
||||
Err(nb::Error::Other(err)) => {
|
||||
for _ in 0..3 {
|
||||
warn.set_high();
|
||||
Timer::after_millis(100).await;
|
||||
warn.set_low();
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(&mut msg, "rx err {:?} \r\n", err);
|
||||
log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if BEACON.load(Ordering::Relaxed) {
|
||||
let now = Instant::now();
|
||||
if now >= next_beacon_toggle {
|
||||
info.toggle();
|
||||
// Move the schedule forward; if we fell behind, resync to "now".
|
||||
next_beacon_toggle = now + beacon_period;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
while let Ok(mut frame) = CAN_TX_CH.try_receive() {
|
||||
match can.transmit(&mut frame) {
|
||||
Ok(_ok) => {
|
||||
let status = hal::pac::CAN1.errsr().read();
|
||||
|
||||
// Check CAN error status register for bus-off condition
|
||||
if status.boff() || status.ewgf() || status.epvf() {
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(&mut msg, "canbus status {} {} {} \r\n", status.boff(), status.ewgf(), status.epvf());
|
||||
log(msg);
|
||||
for _ in 0..2 {
|
||||
warn.set_high();
|
||||
Timer::after_millis(100).await;
|
||||
warn.set_low();
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(nb::Error::WouldBlock) => {
|
||||
for _ in 0..2 {
|
||||
warn.set_high();
|
||||
Timer::after_millis(100).await;
|
||||
warn.set_low();
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(&mut msg, "canbus out buffer full \r\n");
|
||||
log(msg);
|
||||
}
|
||||
Err(nb::Error::Other(err)) => {
|
||||
for _ in 0..3 {
|
||||
warn.set_high();
|
||||
Timer::after_millis(100).await;
|
||||
warn.set_low();
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(&mut msg, "tx err {:?} \r\n", err);
|
||||
log(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
yield_now().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
async fn worker(
|
||||
mut probe_gnd: Flex<'static>,
|
||||
mut q: Output<'static>,
|
||||
mut adc: Adc<'static, hal::peripherals::ADC1>,
|
||||
mut ain: hal::peripherals::PA1,
|
||||
moisture_id: StandardId,
|
||||
identify_id: StandardId,
|
||||
firmware_build_id: StandardId,
|
||||
slot: SensorSlot,
|
||||
) {
|
||||
// 555 emulation state: Q initially Low
|
||||
let mut q_high = false;
|
||||
let low_th: u16 = (ADC_MAX as u16) / 3; // ~1/3 Vref
|
||||
let high_th: u16 = ((ADC_MAX as u32 * 2) / 3) as u16; // ~2/3 Vref
|
||||
|
||||
const AVG_WINDOWS: u32 = 4;
|
||||
const YIELD_EVERY: u32 = 64;
|
||||
let probe_duration = Duration::from_millis(100);
|
||||
let measurement_time = probe_duration.as_millis() * AVG_WINDOWS as u64; // 400ms
|
||||
let interleaving_gap = Duration::from_millis(50);
|
||||
|
||||
// Interleaving timing to ensure A and B never overlap:
|
||||
// - Sensor A: measures for 400ms
|
||||
// - Gap: 50ms
|
||||
// - Sensor B: measures for 400ms
|
||||
// - Gap: 50ms
|
||||
// Total cycle: 900ms, so each sensor measures every 900ms (~1.1 measurements/second)
|
||||
//
|
||||
// Timeline:
|
||||
// 0-400ms: A measures, B idle
|
||||
// 400-450ms: both idle (gap)
|
||||
// 450-850ms: B measures, A idle
|
||||
// 850-900ms: both idle (gap)
|
||||
// Then repeat from 0ms
|
||||
|
||||
// Initial offset: B waits for A's measurement time + one gap
|
||||
match slot {
|
||||
SensorSlot::A => {
|
||||
// A sensors start measuring immediately
|
||||
}
|
||||
SensorSlot::B => {
|
||||
// B sensors wait for A to finish measuring + gap
|
||||
Timer::after(Duration::from_millis(measurement_time + interleaving_gap.as_millis())).await;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut total_pulses: u32 = 0;
|
||||
|
||||
for _window in 0..AVG_WINDOWS {
|
||||
// Count rising edges of Q in a 100 ms window
|
||||
let start = Instant::now();
|
||||
let mut pulses: u32 = 0;
|
||||
let mut last_q = q_high;
|
||||
let mut iter_count: u32 = 0;
|
||||
|
||||
probe_gnd.set_as_output(Speed::Low);
|
||||
probe_gnd.set_low();
|
||||
while Instant::now()
|
||||
.checked_duration_since(start)
|
||||
.unwrap_or(Duration::from_millis(0))
|
||||
< probe_duration
|
||||
{
|
||||
// Sample the analog input (Threshold/Trigger on A1)
|
||||
let val: u16 = adc.convert(&mut ain, SampleTime::CYCLES28_5);
|
||||
|
||||
// 555 core behavior:
|
||||
// - If input <= 1/3 Vref => set Q high (trigger)
|
||||
// - If input >= 2/3 Vref => set Q low (threshold)
|
||||
// - Otherwise keep previous Q state (hysteresis)
|
||||
if val <= low_th {
|
||||
q_high = true;
|
||||
} else if val >= high_th {
|
||||
q_high = false;
|
||||
}
|
||||
|
||||
// Drive output pin accordingly
|
||||
if q_high {
|
||||
q.set_high();
|
||||
} else {
|
||||
q.set_low();
|
||||
}
|
||||
|
||||
// Count rising edges
|
||||
if !last_q && q_high {
|
||||
pulses = pulses.saturating_add(1);
|
||||
}
|
||||
last_q = q_high;
|
||||
|
||||
// Yield every YIELD_EVERY samples to keep USB alive without
|
||||
// disrupting per-sample timing
|
||||
iter_count += 1;
|
||||
if iter_count % YIELD_EVERY == 0 {
|
||||
yield_now().await;
|
||||
}
|
||||
}
|
||||
probe_gnd.set_as_input(Pull::None);
|
||||
total_pulses = total_pulses.saturating_add(pulses);
|
||||
}
|
||||
|
||||
let avg_pulses = total_pulses / AVG_WINDOWS;
|
||||
let freq_hz: u32 = avg_pulses * (1000 / probe_duration.as_millis()) as u32;
|
||||
|
||||
let mut msg: heapless::String<128> = heapless::String::new();
|
||||
let _ = write!(
|
||||
&mut msg,
|
||||
"555 window={}ms avg_pulses={} freq={} Hz (A1->Q on PB0) id={:?}\r\n",
|
||||
probe_duration.as_millis() * AVG_WINDOWS as u64,
|
||||
avg_pulses,
|
||||
freq_hz,
|
||||
identify_id.as_raw()
|
||||
);
|
||||
log(msg);
|
||||
|
||||
let moisture = CanFrame::new(moisture_id, &(freq_hz as u32).to_be_bytes()).unwrap();
|
||||
let delay_ms = moisture_id.as_raw() as u64 % 50;
|
||||
Timer::after(Duration::from_millis(delay_ms)).await;
|
||||
CAN_TX_CH.send(moisture).await;
|
||||
|
||||
// Send firmware build timestamp after each measurement so the controller
|
||||
// always has up-to-date build info without requiring an identify request.
|
||||
let firmware = CanFrame::new(firmware_build_id, &FIRMWARE_BUILD_MINUTES.to_be_bytes()).unwrap();
|
||||
let delay_ms = firmware_build_id.as_raw() as u64 % 50;
|
||||
Timer::after(Duration::from_millis(delay_ms)).await;
|
||||
CAN_TX_CH.send(firmware).await;
|
||||
|
||||
// Wait for the other slot to measure, plus gaps to ensure no overlap
|
||||
// After A finishes measuring: wait 50ms (gap) + 400ms (B measures) + 50ms (gap) = 500ms
|
||||
// After B finishes measuring: wait 50ms (gap) + 400ms (A measures) + 50ms (gap) = 500ms
|
||||
// This ensures the full 900ms cycle is maintained and A/B never overlap
|
||||
Timer::after(Duration::from_millis(interleaving_gap.as_millis() + measurement_time + interleaving_gap.as_millis())).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+1
@@ -3,6 +3,7 @@
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/rust/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/rust/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/rust/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
@@ -23,8 +23,6 @@ target = "riscv32imac-unknown-none-elf"
|
||||
CHRONO_TZ_TIMEZONE_FILTER = "UTC|America/New_York|America/Chicago|America/Los_Angeles|Europe/London|Europe/Berlin|Europe/Paris|Asia/Tokyo|Asia/Shanghai|Asia/Kolkata|Australia/Sydney|America/Sao_Paulo|Africa/Johannesburg|Asia/Dubai|Pacific/Auckland"
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||
ESP_LOG = "info"
|
||||
PATH = { value = "../../../bin:/usr/bin:/usr/local/bin", force = true, relative = true }
|
||||
|
||||
|
||||
|
||||
[unstable]
|
||||
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
Generated
-1
@@ -2,7 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../../../website/themes/blowfish" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+2847
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,10 @@ test = false
|
||||
bench = false
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
default = ["esp32c6"]
|
||||
esp32c6 = []
|
||||
|
||||
#this strips the bootloader, we need that tho
|
||||
#strip = true
|
||||
|
||||
@@ -42,74 +46,52 @@ canapi = { path = "../../Shared/canapi" }
|
||||
|
||||
# Platform and ESP-specific runtime/boot/runtime utilities
|
||||
log = "0.4.28"
|
||||
esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32c6", "log-04"] }
|
||||
esp-hal = { version = "1.0.0", features = ["esp32c6", "log-04"] }
|
||||
esp-rtos = { version = "0.2.0", features = ["esp32c6", "embassy", "esp-radio"] }
|
||||
esp-backtrace = { version = "0.18.1", features = ["esp32c6", "panic-handler", "println", "colors", "custom-halt"] }
|
||||
esp-println = { version = "0.16.1", features = ["esp32c6", "log-04", "auto"] }
|
||||
esp-storage = { version = "0.8.1", features = ["esp32c6"] }
|
||||
esp-radio = { version = "0.17.0", features = ["esp32c6", "log-04", "smoltcp", "wifi", "unstable"] }
|
||||
esp-alloc = { version = "0.9.0", features = ["esp32c6", "internal-heap-stats"] }
|
||||
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6", "log-04"] }
|
||||
esp-hal = { version = "1.1.0", features = ["esp32c6", "log-04"] }
|
||||
esp-rtos = { version = "0.3.0", features = ["esp32c6", "embassy", "esp-radio"] }
|
||||
esp-backtrace = { version = "0.19.0", features = ["esp32c6", "panic-handler", "println", "colors", "custom-halt"] }
|
||||
esp-println = { version = "0.17.0", features = ["esp32c6", "log-04", "auto"] }
|
||||
esp-storage = { version = "0.9.0", features = ["esp32c6"] }
|
||||
esp-radio = { version = "0.18.0", features = ["esp32c6", "log-04", "wifi", "unstable"] }
|
||||
esp-alloc = { version = "0.10.0", features = ["esp32c6", "internal-heap-stats"] }
|
||||
|
||||
# Async runtime (Embassy core)
|
||||
embassy-executor = { version = "0.9.1", features = ["log", "nightly"] }
|
||||
embassy-time = { version = "0.5.0", features = ["log"], default-features = false }
|
||||
embassy-sync = { version = "0.7.2", features = ["log"] }
|
||||
embassy-executor = { version = "0.10.0", features = ["log", "nightly"] }
|
||||
embassy-time = { version = "0.5.1", features = ["log"], default-features = false }
|
||||
embassy-sync = { version = "0.8.0", features = ["log"] }
|
||||
|
||||
# Networking and protocol stacks
|
||||
embassy-net = { version = "0.7.1", features = [
|
||||
"dhcpv4",
|
||||
"log",
|
||||
"medium-ethernet",
|
||||
"tcp",
|
||||
"udp",
|
||||
"proto-ipv4",
|
||||
"dns"
|
||||
] }
|
||||
smoltcp = { version = "0.12.0", default-features = false, features = [
|
||||
"alloc",
|
||||
"log",
|
||||
"medium-ethernet",
|
||||
"multicast",
|
||||
"proto-dhcpv4",
|
||||
"proto-ipv6",
|
||||
"proto-dns",
|
||||
"proto-ipv4",
|
||||
"socket-dns",
|
||||
"socket-icmp",
|
||||
"socket-raw",
|
||||
"socket-tcp",
|
||||
"socket-udp",
|
||||
] }
|
||||
embassy-net = { version = "0.8.0", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp", "proto-ipv4", "dns", "proto-ipv6"] }
|
||||
sntpc = { version = "0.6.1", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
|
||||
edge-dhcp = "0.6.0"
|
||||
edge-nal = "0.5.0"
|
||||
edge-nal-embassy = "0.6.0"
|
||||
edge-http = { version = "0.6.1", features = ["log"] }
|
||||
edge-dhcp = "0.7.0"
|
||||
edge-nal = "0.6.0"
|
||||
edge-nal-embassy = "0.8.1"
|
||||
edge-http = { version = "0.7.0", features = ["log"] }
|
||||
|
||||
esp32c6 = { version = "0.23.2" }
|
||||
|
||||
# Hardware abstraction traits and HAL adapters
|
||||
embedded-hal = "1.0.0"
|
||||
embedded-storage = "0.3.1"
|
||||
embassy-embedded-hal = "0.5.0"
|
||||
embassy-embedded-hal = "0.6.0"
|
||||
embedded-can = "0.4.1"
|
||||
nb = "1.1.0"
|
||||
|
||||
# Concrete hardware drivers and sensors/IO expanders
|
||||
lib-bms-protocol = { git = "https://gitea.wlandt.de/judge/ch32-bms.git", default-features = false }
|
||||
onewire = "0.4.0"
|
||||
ds323x = "0.7.0"
|
||||
eeprom24x = "0.7.2"
|
||||
pca9535 = { version = "2.0.0" }
|
||||
ina219 = { version = "0.2.0" }
|
||||
bq34z100 = { version = "0.4.0", default-features = false }
|
||||
|
||||
# Storage and filesystem
|
||||
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
|
||||
littlefs2-core = "0.1.2"
|
||||
embedded-savegame = { version = "0.3.0" }
|
||||
|
||||
# Serialization / codecs
|
||||
serde = { version = "1.0.228", features = ["derive", "alloc"], default-features = false }
|
||||
serde_json = { version = "1.0.145", default-features = false, features = ["alloc"] }
|
||||
bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
|
||||
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
|
||||
|
||||
# Time and time zones
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
|
||||
@@ -119,6 +101,7 @@ chrono-tz = { version = "0.10.4", default-features = false, features = ["filter-
|
||||
heapless = { version = "0.7.17", features = ["serde"] } # stay in sync with mcutie version
|
||||
static_cell = "2.1.1"
|
||||
portable-atomic = "1.11.1"
|
||||
critical-section = "1"
|
||||
crc = "3.3.0"
|
||||
bytemuck = { version = "1.24.0", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] }
|
||||
deranged = "0.5.5"
|
||||
@@ -127,12 +110,8 @@ unit-enum = "1.4.3"
|
||||
async-trait = "0.1.89"
|
||||
option-lock = { version = "0.3.1", default-features = false }
|
||||
measurements = "0.11.1"
|
||||
mcutie = { path = "src/mcutie_3_0_0", features = ["log"] }
|
||||
|
||||
# Project-specific
|
||||
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
|
||||
|
||||
[patch.crates-io]
|
||||
mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' }
|
||||
#bq34z100 = { path = "../../bq34z100_rust" }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
One Wire does not seem to work.
|
||||
Flow Sensor does not seem to work.
|
||||
PlantProfiles with a dry out phase needs to be implemented + Memory for this
|
||||
@@ -49,5 +49,8 @@ fn linker_be_nice() {
|
||||
|
||||
fn main() {
|
||||
linker_be_nice();
|
||||
// Non-existent path causes Cargo to always re-run this script,
|
||||
// keeping VERGEN_BUILD_TIMESTAMP fresh on every build.
|
||||
println!("cargo:rerun-if-changed=ALWAYS_REBUILD_SENTINEL");
|
||||
let _ = EmitBuilder::builder().all_git().all_build().emit();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# This file is used for clippy configuration.
|
||||
# It shouldn't contain the deny attributes, which belong to the crate root.
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
cargo espflash erase-parts otadata --partition-table partitions.csv
|
||||
@@ -1,8 +1,6 @@
|
||||
[connection]
|
||||
format = "EspIdf"
|
||||
|
||||
[[usb_device]]
|
||||
vid = "303a"
|
||||
pid = "1001"
|
||||
[idf_format_args]
|
||||
|
||||
[flash]
|
||||
size = "16MB"
|
||||
|
||||
Binary file not shown.
@@ -1,25 +1,25 @@
|
||||
use crate::hal::PLANT_COUNT;
|
||||
use crate::plant_state::PlantWateringMode;
|
||||
use alloc::string::String;
|
||||
use core::str::FromStr;
|
||||
use alloc::string::{String, ToString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct NetworkConfig {
|
||||
pub ap_ssid: heapless::String<32>,
|
||||
pub ssid: Option<heapless::String<32>>,
|
||||
pub password: Option<heapless::String<64>>,
|
||||
pub ap_ssid: String,
|
||||
pub ssid: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub mqtt_url: Option<String>,
|
||||
pub base_topic: Option<heapless::String<64>>,
|
||||
pub base_topic: Option<String>,
|
||||
pub mqtt_user: Option<String>,
|
||||
pub mqtt_password: Option<String>,
|
||||
pub max_wait: u32,
|
||||
pub retry_count: u32,
|
||||
}
|
||||
impl Default for NetworkConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ap_ssid: heapless::String::from_str("PlantCtrl Init").unwrap(),
|
||||
ap_ssid: "PlantCtrl Init".to_string(),
|
||||
ssid: None,
|
||||
password: None,
|
||||
mqtt_url: None,
|
||||
@@ -27,6 +27,7 @@ impl Default for NetworkConfig {
|
||||
mqtt_user: None,
|
||||
mqtt_password: None,
|
||||
max_wait: 10000,
|
||||
retry_count: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,14 +84,12 @@ impl Default for TankConfig {
|
||||
pub enum BatteryBoardVersion {
|
||||
#[default]
|
||||
Disabled,
|
||||
BQ34Z100G1,
|
||||
WchI2cSlave,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub enum BoardVersion {
|
||||
#[default]
|
||||
Initial,
|
||||
V3,
|
||||
#[default]
|
||||
V4,
|
||||
}
|
||||
|
||||
@@ -98,6 +97,8 @@ pub enum BoardVersion {
|
||||
pub struct BoardHardware {
|
||||
pub board: BoardVersion,
|
||||
pub battery: BatteryBoardVersion,
|
||||
#[serde(default)]
|
||||
pub pump_corrosion_protection: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
@@ -111,6 +112,14 @@ pub struct PlantControllerConfig {
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub enum SensorCombineMode {
|
||||
Min,
|
||||
Max,
|
||||
#[default]
|
||||
Avg,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct PlantConfig {
|
||||
@@ -130,6 +139,9 @@ pub struct PlantConfig {
|
||||
pub min_pump_current_ma: u16,
|
||||
pub max_pump_current_ma: u16,
|
||||
pub ignore_current_error: bool,
|
||||
pub fertilizer_s: u16,
|
||||
pub fertilizer_cooldown_min: u16,
|
||||
pub sensor_combine_mode: SensorCombineMode,
|
||||
}
|
||||
|
||||
impl Default for PlantConfig {
|
||||
@@ -151,6 +163,9 @@ impl Default for PlantConfig {
|
||||
min_pump_current_ma: 10,
|
||||
max_pump_current_ma: 3000,
|
||||
ignore_current_error: true,
|
||||
fertilizer_s: 0,
|
||||
fertilizer_cooldown_min: 1440, // 1 day default
|
||||
sensor_combine_mode: SensorCombineMode::Avg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use chrono::format::ParseErrorKind;
|
||||
use chrono_tz::ParseError;
|
||||
use core::convert::Infallible;
|
||||
use core::fmt;
|
||||
use core::fmt::Debug;
|
||||
use core::str::Utf8Error;
|
||||
use embassy_embedded_hal::shared_bus::I2cDeviceError;
|
||||
use embassy_executor::SpawnError;
|
||||
@@ -12,7 +15,7 @@ use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
|
||||
use esp_hal::twai::EspTwaiError;
|
||||
use esp_radio::wifi::WifiError;
|
||||
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
|
||||
use littlefs2_core::PathError;
|
||||
use lib_bms_protocol::BmsProtocolError;
|
||||
use onewire::Error;
|
||||
use pca9535::ExpanderError;
|
||||
|
||||
@@ -25,12 +28,6 @@ pub enum FatError {
|
||||
String {
|
||||
error: String,
|
||||
},
|
||||
LittleFSError {
|
||||
error: littlefs2_core::Error,
|
||||
},
|
||||
PathError {
|
||||
error: PathError,
|
||||
},
|
||||
TryLockError {
|
||||
error: TryLockError,
|
||||
},
|
||||
@@ -47,6 +44,7 @@ pub enum FatError {
|
||||
SpawnError {
|
||||
error: SpawnError,
|
||||
},
|
||||
OTAError,
|
||||
PartitionError {
|
||||
error: esp_bootloader_esp_idf::partitions::Error,
|
||||
},
|
||||
@@ -80,8 +78,6 @@ impl fmt::Display for FatError {
|
||||
}
|
||||
FatError::OneWireError { error } => write!(f, "OneWireError {error:?}"),
|
||||
FatError::String { error } => write!(f, "{error}"),
|
||||
FatError::LittleFSError { error } => write!(f, "LittleFSError {error:?}"),
|
||||
FatError::PathError { error } => write!(f, "PathError {error:?}"),
|
||||
FatError::TryLockError { error } => write!(f, "TryLockError {error:?}"),
|
||||
FatError::WifiError { error } => write!(f, "WifiError {error:?}"),
|
||||
FatError::SerdeError { error } => write!(f, "SerdeError {error:?}"),
|
||||
@@ -100,6 +96,9 @@ impl fmt::Display for FatError {
|
||||
write!(f, "CanBusError {error:?}")
|
||||
}
|
||||
FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"),
|
||||
FatError::OTAError => {
|
||||
write!(f, "OTA missing partition")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,23 +138,28 @@ impl<T> ContextExt<T> for Option<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ContextExt<T> for Result<T, E>
|
||||
where
|
||||
E: fmt::Debug,
|
||||
{
|
||||
fn context<C>(self, context: C) -> Result<T, FatError>
|
||||
where
|
||||
C: AsRef<str>,
|
||||
{
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(FatError::String {
|
||||
error: format!("{}: {:?}", context.as_ref(), err),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error<Infallible>> for FatError {
|
||||
fn from(error: Error<Infallible>) -> Self {
|
||||
FatError::OneWireError { error }
|
||||
}
|
||||
}
|
||||
impl From<littlefs2_core::Error> for FatError {
|
||||
fn from(value: littlefs2_core::Error) -> Self {
|
||||
FatError::LittleFSError { error: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathError> for FatError {
|
||||
fn from(value: PathError) -> Self {
|
||||
FatError::PathError { error: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TryLockError> for FatError {
|
||||
fn from(value: TryLockError) -> Self {
|
||||
FatError::TryLockError { error: value }
|
||||
@@ -194,7 +198,7 @@ impl From<Utf8Error> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: core::fmt::Debug> From<edge_http::io::Error<E>> for FatError {
|
||||
impl<E: fmt::Debug> From<edge_http::io::Error<E>> for FatError {
|
||||
fn from(value: edge_http::io::Error<E>) -> Self {
|
||||
FatError::String {
|
||||
error: format!("{value:?}"),
|
||||
@@ -202,7 +206,7 @@ impl<E: core::fmt::Debug> From<edge_http::io::Error<E>> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: core::fmt::Debug> From<ds323x::Error<E>> for FatError {
|
||||
impl<E: fmt::Debug> From<ds323x::Error<E>> for FatError {
|
||||
fn from(value: ds323x::Error<E>) -> Self {
|
||||
FatError::DS323 {
|
||||
error: format!("{value:?}"),
|
||||
@@ -210,7 +214,7 @@ impl<E: core::fmt::Debug> From<ds323x::Error<E>> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: core::fmt::Debug> From<eeprom24x::Error<E>> for FatError {
|
||||
impl<E: fmt::Debug> From<eeprom24x::Error<E>> for FatError {
|
||||
fn from(value: eeprom24x::Error<E>) -> Self {
|
||||
FatError::Eeprom24x {
|
||||
error: format!("{value:?}"),
|
||||
@@ -218,7 +222,7 @@ impl<E: core::fmt::Debug> From<eeprom24x::Error<E>> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: core::fmt::Debug> From<ExpanderError<I2cDeviceError<E>>> for FatError {
|
||||
impl<E: fmt::Debug> From<ExpanderError<I2cDeviceError<E>>> for FatError {
|
||||
fn from(value: ExpanderError<I2cDeviceError<E>>) -> Self {
|
||||
FatError::ExpanderError {
|
||||
error: format!("{value:?}"),
|
||||
@@ -226,16 +230,8 @@ impl<E: core::fmt::Debug> From<ExpanderError<I2cDeviceError<E>>> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bincode::error::DecodeError> for FatError {
|
||||
fn from(value: bincode::error::DecodeError) -> Self {
|
||||
FatError::Eeprom24x {
|
||||
error: format!("{value:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bincode::error::EncodeError> for FatError {
|
||||
fn from(value: bincode::error::EncodeError) -> Self {
|
||||
impl From<postcard::Error> for FatError {
|
||||
fn from(value: postcard::Error) -> Self {
|
||||
FatError::Eeprom24x {
|
||||
error: format!("{value:?}"),
|
||||
}
|
||||
@@ -248,7 +244,7 @@ impl From<ConfigError> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: core::fmt::Debug> From<I2cDeviceError<E>> for FatError {
|
||||
impl<E: fmt::Debug> From<I2cDeviceError<E>> for FatError {
|
||||
fn from(value: I2cDeviceError<E>) -> Self {
|
||||
FatError::String {
|
||||
error: format!("{value:?}"),
|
||||
@@ -256,14 +252,14 @@ impl<E: core::fmt::Debug> From<I2cDeviceError<E>> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: core::fmt::Debug> From<BusVoltageReadError<I2cDeviceError<E>>> for FatError {
|
||||
impl<E: fmt::Debug> From<BusVoltageReadError<I2cDeviceError<E>>> for FatError {
|
||||
fn from(value: BusVoltageReadError<I2cDeviceError<E>>) -> Self {
|
||||
FatError::String {
|
||||
error: format!("{value:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E: core::fmt::Debug> From<ShuntVoltageReadError<I2cDeviceError<E>>> for FatError {
|
||||
impl<E: fmt::Debug> From<ShuntVoltageReadError<I2cDeviceError<E>>> for FatError {
|
||||
fn from(value: ShuntVoltageReadError<I2cDeviceError<E>>) -> Self {
|
||||
FatError::String {
|
||||
error: format!("{value:?}"),
|
||||
@@ -273,7 +269,7 @@ impl<E: core::fmt::Debug> From<ShuntVoltageReadError<I2cDeviceError<E>>> for Fat
|
||||
|
||||
impl From<Infallible> for FatError {
|
||||
fn from(value: Infallible) -> Self {
|
||||
panic!("Infallible error: {:?}", value)
|
||||
match value {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,3 +312,40 @@ impl From<sntpc::Error> for FatError {
|
||||
FatError::SNTPError { error: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BmsProtocolError> for FatError {
|
||||
fn from(value: BmsProtocolError) -> Self {
|
||||
match value {
|
||||
BmsProtocolError::I2cCommunicationError => FatError::String {
|
||||
error: "I2C communication error".to_string(),
|
||||
},
|
||||
BmsProtocolError::ChecksumError => FatError::String {
|
||||
error: "BMS checksum error".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for FatError {
|
||||
fn from(value: ParseError) -> Self {
|
||||
FatError::String {
|
||||
error: format!("Parsing error: {value:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseErrorKind> for FatError {
|
||||
fn from(value: ParseErrorKind) -> Self {
|
||||
FatError::String {
|
||||
error: format!("Parsing error: {value:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<chrono::format::ParseError> for FatError {
|
||||
fn from(value: chrono::format::ParseError) -> Self {
|
||||
FatError::String {
|
||||
error: format!("Parsing error: {value:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::Box;
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use alloc::string::String;
|
||||
use async_trait::async_trait;
|
||||
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
|
||||
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use esp_hal::delay::Delay;
|
||||
use esp_hal::i2c::master::I2c;
|
||||
use esp_hal::Blocking;
|
||||
use measurements::Temperature;
|
||||
use lib_bms_protocol::{
|
||||
BatteryState as bstate, BmsReadable, Config, FirmwareVersion, ProtocolVersion,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait BatteryInteraction {
|
||||
async fn state_charge_percent(&mut self) -> FatResult<f32>;
|
||||
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>;
|
||||
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16>;
|
||||
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16>;
|
||||
async fn voltage_milli_volt(&mut self) -> FatResult<u16>;
|
||||
async fn average_current_milli_ampere(&mut self) -> FatResult<i16>;
|
||||
async fn cycle_count(&mut self) -> FatResult<u16>;
|
||||
async fn state_health_percent(&mut self) -> FatResult<u16>;
|
||||
async fn bat_temperature(&mut self) -> FatResult<u16>;
|
||||
async fn get_battery_state(&mut self) -> FatResult<BatteryState>;
|
||||
async fn get_state(&mut self) -> FatResult<BatteryState>;
|
||||
async fn get_firmware(&mut self) -> FatResult<FirmwareVersion>;
|
||||
async fn get_protocol(&mut self) -> FatResult<ProtocolVersion>;
|
||||
async fn reset(&mut self) -> FatResult<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct BatteryInfo {
|
||||
pub voltage_milli_volt: u16,
|
||||
pub average_current_milli_ampere: i16,
|
||||
pub cycle_count: u16,
|
||||
pub design_milli_ampere_hour: u16,
|
||||
pub remaining_milli_ampere_hour: u16,
|
||||
pub state_of_charge: f32,
|
||||
pub state_of_health: u16,
|
||||
pub temperature: u16,
|
||||
pub voltage_mv: Option<u32>,
|
||||
pub avg_current_ma: Option<i32>,
|
||||
pub design_mah: Option<u32>,
|
||||
pub remaining_mah: Option<u32>,
|
||||
pub soc_pct: Option<f32>,
|
||||
pub soh_pct: Option<f32>,
|
||||
pub temperature_c: Option<i32>,
|
||||
pub error: Option<BatteryError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum BatteryError {
|
||||
NoBatteryMonitor,
|
||||
CommunicationError { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -46,213 +48,63 @@ pub enum BatteryState {
|
||||
pub struct NoBatteryMonitor {}
|
||||
#[async_trait(?Send)]
|
||||
impl BatteryInteraction for NoBatteryMonitor {
|
||||
async fn state_charge_percent(&mut self) -> FatResult<f32> {
|
||||
// No monitor configured: assume full battery for lightstate logic
|
||||
Ok(100.0)
|
||||
}
|
||||
|
||||
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn cycle_count(&mut self) -> FatResult<u16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn state_health_percent(&mut self) -> FatResult<u16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn bat_temperature(&mut self) -> FatResult<u16> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
|
||||
async fn get_state(&mut self) -> FatResult<BatteryState> {
|
||||
Ok(BatteryState::Unknown)
|
||||
}
|
||||
|
||||
async fn get_firmware(&mut self) -> FatResult<FirmwareVersion> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn get_protocol(&mut self) -> FatResult<ProtocolVersion> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
|
||||
async fn reset(&mut self) -> FatResult<()> {
|
||||
Err(FatError::NoBatteryMonitor)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO implement this battery monitor kind once controller is complete
|
||||
#[allow(dead_code)]
|
||||
pub struct WchI2cSlave {}
|
||||
|
||||
pub type I2cDev = I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>;
|
||||
|
||||
pub struct BQ34Z100G1 {
|
||||
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
|
||||
pub struct WCHI2CSlave<'a> {
|
||||
pub(crate) i2c: I2cDevice<'a, CriticalSectionRawMutex, I2c<'a, Blocking>>,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl BatteryInteraction for BQ34Z100G1 {
|
||||
async fn state_charge_percent(&mut self) -> FatResult<f32> {
|
||||
self.battery_driver
|
||||
.state_of_charge()
|
||||
.map(|v| v as f32)
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
impl BatteryInteraction for WCHI2CSlave<'_> {
|
||||
async fn get_state(&mut self) -> FatResult<BatteryState> {
|
||||
let state = bstate::read_from_i2c(&mut self.i2c)?;
|
||||
let config = Config::read_from_i2c(&mut self.i2c)?;
|
||||
|
||||
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
|
||||
self.battery_driver
|
||||
.remaining_capacity()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
let state_of_charge =
|
||||
state.remaining_capacity_mah as f32 * 100. / state.lifetime_capacity_mah as f32;
|
||||
let state_of_health =
|
||||
state.lifetime_capacity_mah as f32 / config.capacity_mah as f32 * 100.;
|
||||
|
||||
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
|
||||
self.battery_driver
|
||||
.full_charge_capacity()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
|
||||
self.battery_driver
|
||||
.design_capacity()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
|
||||
self.battery_driver.voltage().map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
|
||||
self.battery_driver
|
||||
.average_current()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn cycle_count(&mut self) -> FatResult<u16> {
|
||||
self.battery_driver
|
||||
.cycle_count()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn state_health_percent(&mut self) -> FatResult<u16> {
|
||||
self.battery_driver
|
||||
.state_of_health()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn bat_temperature(&mut self) -> FatResult<u16> {
|
||||
self.battery_driver
|
||||
.temperature()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
|
||||
Ok(BatteryState::Info(BatteryInfo {
|
||||
voltage_milli_volt: self.voltage_milli_volt().await?,
|
||||
average_current_milli_ampere: self.average_current_milli_ampere().await?,
|
||||
cycle_count: self.cycle_count().await?,
|
||||
design_milli_ampere_hour: self.design_milli_ampere_hour().await?,
|
||||
remaining_milli_ampere_hour: self.remaining_milli_ampere_hour().await?,
|
||||
state_of_charge: self.state_charge_percent().await?,
|
||||
state_of_health: self.state_health_percent().await?,
|
||||
temperature: self.bat_temperature().await?,
|
||||
voltage_mv: Some(state.current_mv),
|
||||
avg_current_ma: Some(1337),
|
||||
design_mah: Some(config.capacity_mah),
|
||||
remaining_mah: Some(state.remaining_capacity_mah),
|
||||
soc_pct: Some(state_of_charge),
|
||||
soh_pct: Some(state_of_health),
|
||||
temperature_c: Some(state.temperature_celcius),
|
||||
error: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_battery_bq34z100(
|
||||
battery_driver: &mut Bq34z100g1Driver<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>, Delay>,
|
||||
) -> FatResult<()> {
|
||||
log::info!("Try communicating with battery");
|
||||
let fwversion = battery_driver.fw_version().unwrap_or_else(|e| {
|
||||
log::info!("Firmware {e:?}");
|
||||
0
|
||||
});
|
||||
log::info!("fw version is {fwversion}");
|
||||
|
||||
let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| {
|
||||
log::info!("Design capacity {e:?}");
|
||||
0
|
||||
});
|
||||
log::info!("Design Capacity {design_capacity}");
|
||||
if design_capacity == 1000 {
|
||||
log::info!("Still stock configuring battery, readouts are likely to be wrong!");
|
||||
async fn get_firmware(&mut self) -> FatResult<FirmwareVersion> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
let flags = battery_driver.get_flags_decoded().unwrap_or(Flags {
|
||||
fast_charge_allowed: false,
|
||||
full_chage: false,
|
||||
charging_not_allowed: false,
|
||||
charge_inhibit: false,
|
||||
bat_low: false,
|
||||
bat_high: false,
|
||||
over_temp_discharge: false,
|
||||
over_temp_charge: false,
|
||||
discharge: false,
|
||||
state_of_charge_f: false,
|
||||
state_of_charge_1: false,
|
||||
cf: false,
|
||||
ocv_taken: false,
|
||||
});
|
||||
log::info!("Flags {flags:?}");
|
||||
async fn get_protocol(&mut self) -> FatResult<ProtocolVersion> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
let chem_id = battery_driver.chem_id().unwrap_or_else(|e| {
|
||||
log::info!("Chemid {e:?}");
|
||||
0
|
||||
});
|
||||
|
||||
let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| {
|
||||
log::info!("Bat Temp {e:?}");
|
||||
0
|
||||
});
|
||||
let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius();
|
||||
let voltage = battery_driver.voltage().unwrap_or_else(|e| {
|
||||
log::info!("Bat volt {e:?}");
|
||||
0
|
||||
});
|
||||
let current = battery_driver.current().unwrap_or_else(|e| {
|
||||
log::info!("Bat current {e:?}");
|
||||
0
|
||||
});
|
||||
let state = battery_driver.state_of_charge().unwrap_or_else(|e| {
|
||||
log::info!("Bat Soc {e:?}");
|
||||
0
|
||||
});
|
||||
let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| {
|
||||
log::info!("Bat Charge Volt {e:?}");
|
||||
0
|
||||
});
|
||||
let charge_current = battery_driver.charge_current().unwrap_or_else(|e| {
|
||||
log::info!("Bat Charge Current {e:?}");
|
||||
0
|
||||
});
|
||||
log::info!("ChemId: {chem_id} Current voltage {voltage} and current {current} with charge {state}% and temp {temp_c} CVolt: {charge_voltage} CCur {charge_current}");
|
||||
let _ = battery_driver.unsealed();
|
||||
let _ = battery_driver.it_enable();
|
||||
Ok(())
|
||||
async fn reset(&mut self) -> FatResult<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,143 +0,0 @@
|
||||
use crate::alloc::boxed::Box;
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::esp::Esp;
|
||||
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
|
||||
use crate::hal::water::TankSensor;
|
||||
use crate::hal::{BoardInteraction, FreePeripherals, Moistures, TIME_ACCESS};
|
||||
use crate::{
|
||||
bail,
|
||||
config::PlantControllerConfig,
|
||||
hal::battery::{BatteryInteraction, NoBatteryMonitor},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||
use measurements::{Current, Voltage};
|
||||
|
||||
pub struct Initial<'a> {
|
||||
pub(crate) general_fault: Output<'a>,
|
||||
pub(crate) esp: Esp<'a>,
|
||||
pub(crate) config: PlantControllerConfig,
|
||||
pub(crate) battery: Box<dyn BatteryInteraction + Send>,
|
||||
pub rtc: Box<dyn RTCModuleInteraction + Send>,
|
||||
}
|
||||
|
||||
pub(crate) struct NoRTC {}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl RTCModuleInteraction for NoRTC {
|
||||
async fn get_backup_info(&mut self) -> Result<BackupHeader, FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn get_backup_config(&mut self, _chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn backup_config(&mut self, _offset: usize, _bytes: &[u8]) -> FatResult<()> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn backup_config_finalize(&mut self, _crc: u16, _length: usize) -> FatResult<()> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn get_rtc_time(&mut self) -> Result<DateTime<Utc>, FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn set_rtc_time(&mut self, _time: &DateTime<Utc>) -> Result<(), FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_initial_board(
|
||||
free_pins: FreePeripherals<'static>,
|
||||
config: PlantControllerConfig,
|
||||
esp: Esp<'static>,
|
||||
) -> Result<Box<dyn BoardInteraction<'static> + Send>, FatError> {
|
||||
log::info!("Start initial");
|
||||
let general_fault = Output::new(free_pins.gpio23, Level::Low, OutputConfig::default());
|
||||
let v = Initial {
|
||||
general_fault,
|
||||
config,
|
||||
esp,
|
||||
battery: Box::new(NoBatteryMonitor {}),
|
||||
rtc: Box::new(NoRTC {}),
|
||||
};
|
||||
Ok(Box::new(v))
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<'a> BoardInteraction<'a> for Initial<'a> {
|
||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
fn get_esp(&mut self) -> &mut Esp<'a> {
|
||||
&mut self.esp
|
||||
}
|
||||
|
||||
fn get_config(&mut self) -> &PlantControllerConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send> {
|
||||
&mut self.battery
|
||||
}
|
||||
|
||||
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
|
||||
&mut self.rtc
|
||||
}
|
||||
|
||||
async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
|
||||
let rtc = TIME_ACCESS.get().await.lock().await;
|
||||
self.esp.deep_sleep(duration_in_ms, rtc);
|
||||
}
|
||||
fn is_day(&self) -> bool {
|
||||
false
|
||||
}
|
||||
async fn light(&mut self, _enable: bool) -> Result<(), FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn pump(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn fault(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn general_fault(&mut self, enable: bool) {
|
||||
self.general_fault.set_level(enable.into());
|
||||
}
|
||||
|
||||
async fn test(&mut self) -> Result<(), FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
fn set_config(&mut self, config: PlantControllerConfig) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
|
||||
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
|
||||
bail!("Please configure board revision")
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use crate::hal::shared_flash::MutexFlashStorage;
|
||||
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
||||
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
||||
use littlefs2::consts::U4096 as lfsCache;
|
||||
use littlefs2::consts::U512 as lfsLookahead;
|
||||
use littlefs2::driver::Storage as lfs2Storage;
|
||||
use littlefs2::io::Error as lfs2Error;
|
||||
use littlefs2::io::Result as lfs2Result;
|
||||
use log::error;
|
||||
|
||||
pub struct LittleFs2Filesystem {
|
||||
pub(crate) storage: &'static mut FlashRegion<'static, MutexFlashStorage>,
|
||||
}
|
||||
|
||||
impl lfs2Storage for LittleFs2Filesystem {
|
||||
const READ_SIZE: usize = 4096;
|
||||
const WRITE_SIZE: usize = 4096;
|
||||
const BLOCK_SIZE: usize = 4096; //usually optimal for flash access
|
||||
const BLOCK_COUNT: usize = 8 * 1000 * 1000 / 4096; //8Mb in 4k blocks + a little space for stupid calculation errors
|
||||
const BLOCK_CYCLES: isize = 100;
|
||||
type CACHE_SIZE = lfsCache;
|
||||
type LOOKAHEAD_SIZE = lfsLookahead;
|
||||
|
||||
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
|
||||
let read_size: usize = Self::READ_SIZE;
|
||||
if off % read_size != 0 {
|
||||
error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {off} read_size: {read_size}");
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
if buf.len() % read_size != 0 {
|
||||
error!("Littlefs2Filesystem read error: length not aligned to read size length: {} read_size: {}", buf.len(), read_size);
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
match self.storage.read(off as u32, buf) {
|
||||
Ok(..) => Ok(buf.len()),
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem read error: {err:?}");
|
||||
Err(lfs2Error::IO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
|
||||
let write_size: usize = Self::WRITE_SIZE;
|
||||
if off % write_size != 0 {
|
||||
error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {off} write_size: {write_size}");
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
if data.len() % write_size != 0 {
|
||||
error!("Littlefs2Filesystem write error: length not aligned to write size length: {} write_size: {}", data.len(), write_size);
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
match self.storage.write(off as u32, data) {
|
||||
Ok(..) => Ok(data.len()),
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem write error: {err:?}");
|
||||
Err(lfs2Error::IO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
|
||||
let block_size: usize = Self::BLOCK_SIZE;
|
||||
if off % block_size != 0 {
|
||||
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {off} block_size: {block_size}");
|
||||
return lfs2Result::Err(lfs2Error::IO);
|
||||
}
|
||||
if len % block_size != 0 {
|
||||
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {len} block_size: {block_size}");
|
||||
return lfs2Result::Err(lfs2Error::IO);
|
||||
}
|
||||
|
||||
match check_erase(self.storage, off as u32, (off + len) as u32) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem check erase error: {err:?}");
|
||||
return lfs2Result::Err(lfs2Error::IO);
|
||||
}
|
||||
}
|
||||
match self.storage.erase(off as u32, (off + len) as u32) {
|
||||
Ok(..) => lfs2Result::Ok(len),
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem erase error: {err:?}");
|
||||
lfs2Result::Err(lfs2Error::IO)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
use esp_hal::uart::Config as UartConfig;
|
||||
use lib_bms_protocol::BmsReadable;
|
||||
pub(crate) mod battery;
|
||||
// mod can_api; // replaced by external canapi crate
|
||||
pub mod esp;
|
||||
mod initial_hal;
|
||||
mod little_fs2storage_adapter;
|
||||
pub(crate) mod rtc;
|
||||
pub(crate) mod savegame_manager;
|
||||
mod shared_flash;
|
||||
mod v3_hal;
|
||||
mod v3_shift_register;
|
||||
mod v4_hal;
|
||||
pub(crate) mod v4_sensor;
|
||||
mod water;
|
||||
|
||||
use crate::alloc::string::ToString;
|
||||
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
|
||||
use crate::hal::rtc::{BackupHeader, DS3231Module, RTCModuleInteraction};
|
||||
use esp_hal::peripherals::Peripherals;
|
||||
use esp_hal::peripherals::ADC1;
|
||||
use esp_hal::peripherals::GPIO0;
|
||||
@@ -37,23 +35,22 @@ use esp_hal::peripherals::GPIO6;
|
||||
use esp_hal::peripherals::GPIO7;
|
||||
use esp_hal::peripherals::GPIO8;
|
||||
use esp_hal::peripherals::TWAI0;
|
||||
use lib_bms_protocol::ProtocolVersion;
|
||||
|
||||
use crate::{
|
||||
bail,
|
||||
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
|
||||
config::{BatteryBoardVersion, PlantControllerConfig},
|
||||
hal::{
|
||||
battery::{BatteryInteraction, NoBatteryMonitor},
|
||||
esp::Esp,
|
||||
},
|
||||
log::log,
|
||||
log::LogMessage,
|
||||
BOARD_ACCESS,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bincode::{Decode, Encode};
|
||||
use bq34z100::Bq34z100g1Driver;
|
||||
use canapi::SensorSlot;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use core::cell::RefCell;
|
||||
@@ -68,56 +65,59 @@ use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::blocking_mutex::CriticalSectionMutex;
|
||||
use esp_bootloader_esp_idf::partitions::{
|
||||
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry,
|
||||
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, PartitionTable,
|
||||
PartitionType,
|
||||
};
|
||||
use esp_hal::clock::CpuClock;
|
||||
use esp_hal::gpio::{Input, InputConfig, Pull};
|
||||
use measurements::{Current, Voltage};
|
||||
|
||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||
use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1};
|
||||
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
||||
use crate::hal::battery::WCHI2CSlave;
|
||||
use crate::hal::savegame_manager::SavegameManager;
|
||||
use crate::hal::water::TankSensor;
|
||||
use crate::log::LOG_ACCESS;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_sync::once_lock::OnceLock;
|
||||
use embedded_storage::nor_flash::RmwNorFlashStorage;
|
||||
use embedded_storage::ReadStorage;
|
||||
use esp_alloc as _;
|
||||
use esp_backtrace as _;
|
||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
||||
use esp_hal::delay::Delay;
|
||||
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
|
||||
use esp_hal::i2c::master::{BusTimeout, Config, FsmTimeout, I2c};
|
||||
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
||||
use esp_hal::pcnt::unit::Unit;
|
||||
use esp_hal::pcnt::Pcnt;
|
||||
use esp_hal::rng::Rng;
|
||||
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
|
||||
use esp_hal::system::reset_reason;
|
||||
use esp_hal::time::Rate;
|
||||
use esp_hal::timer::timg::TimerGroup;
|
||||
use esp_hal::timer::timg::{MwdtStage, TimerGroup, Wdt};
|
||||
use esp_hal::uart::Uart;
|
||||
use esp_hal::Blocking;
|
||||
use esp_radio::{init, Controller};
|
||||
use esp_storage::FlashStorage;
|
||||
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
|
||||
use littlefs2::object_safe::DynStorage;
|
||||
use log::{error, info, warn};
|
||||
use log::{info, warn};
|
||||
use portable_atomic::AtomicBool;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_flash::MutexFlashStorage;
|
||||
|
||||
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
|
||||
|
||||
//Only support for 8 right now!
|
||||
pub const PLANT_COUNT: usize = 8;
|
||||
|
||||
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static WATCHDOG: OnceLock<
|
||||
embassy_sync::blocking_mutex::Mutex<
|
||||
CriticalSectionRawMutex,
|
||||
RefCell<Wdt<esp_hal::peripherals::TIMG0>>,
|
||||
>,
|
||||
> = OnceLock::new();
|
||||
|
||||
const TANK_MULTI_SAMPLE: usize = 11;
|
||||
pub static I2C_DRIVER: OnceLock<
|
||||
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Sensor {
|
||||
A,
|
||||
B,
|
||||
@@ -141,13 +141,15 @@ pub struct HAL<'a> {
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait BoardInteraction<'a> {
|
||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
|
||||
fn get_tank_sensor(&mut self) -> &mut TankSensor<'a>;
|
||||
fn get_esp(&mut self) -> &mut Esp<'a>;
|
||||
fn get_config(&mut self) -> &PlantControllerConfig;
|
||||
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
|
||||
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>;
|
||||
async fn get_time(&mut self) -> DateTime<Utc>;
|
||||
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()>;
|
||||
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>;
|
||||
async fn deep_sleep(&mut self, duration_in_ms: u64) -> !;
|
||||
async fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> !;
|
||||
|
||||
fn is_day(&self) -> bool;
|
||||
//should be multsampled
|
||||
@@ -161,15 +163,29 @@ pub trait BoardInteraction<'a> {
|
||||
fn set_config(&mut self, config: PlantControllerConfig);
|
||||
async fn get_mptt_voltage(&mut self) -> FatResult<Voltage>;
|
||||
async fn get_mptt_current(&mut self) -> FatResult<Current>;
|
||||
async fn can_power(&mut self, state: bool) -> FatResult<()>;
|
||||
async fn fertilizer_pump(&mut self, enable: bool) -> FatResult<()>;
|
||||
|
||||
async fn backup_config(&mut self, config: &PlantControllerConfig) -> FatResult<()>;
|
||||
async fn read_backup(&mut self) -> FatResult<PlantControllerConfig>;
|
||||
async fn backup_info(&mut self) -> FatResult<BackupHeader>;
|
||||
|
||||
// Return JSON string with autodetected sensors per plant. Default: not supported.
|
||||
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
|
||||
async fn detect_sensors(&mut self, _request: DetectionRequest) -> FatResult<Detection> {
|
||||
bail!("Autodetection is only available on v4 HAL with CAN bus");
|
||||
}
|
||||
|
||||
/// Return the last known firmware build timestamps per sensor, set during detect_sensors.
|
||||
fn get_sensor_build_minutes(&self) -> ([Option<u32>; PLANT_COUNT], [Option<u32>; PLANT_COUNT]) {
|
||||
([None; PLANT_COUNT], [None; PLANT_COUNT])
|
||||
}
|
||||
|
||||
async fn progress(&mut self, counter: u32) {
|
||||
// Indicate progress is active to suppress default wait_infinity blinking
|
||||
crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Feed watchdog during long-running webserver operations
|
||||
PlantHal::feed_watchdog();
|
||||
|
||||
let current = counter % PLANT_COUNT as u32;
|
||||
for led in 0..PLANT_COUNT {
|
||||
@@ -190,7 +206,7 @@ pub trait BoardInteraction<'a> {
|
||||
let _ = self.general_fault(false).await;
|
||||
|
||||
// Reset progress active flag so wait_infinity can resume blinking
|
||||
crate::hal::PROGRESS_ACTIVE.store(false, core::sync::atomic::Ordering::Relaxed);
|
||||
PROGRESS_ACTIVE.store(false, core::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,14 +241,7 @@ pub struct FreePeripherals<'a> {
|
||||
pub adc1: ADC1<'a>,
|
||||
}
|
||||
|
||||
macro_rules! mk_static {
|
||||
($t:ty,$val:expr) => {{
|
||||
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
|
||||
#[deny(unused_attributes)]
|
||||
let x = STATIC_CELL.uninit().write($val);
|
||||
x
|
||||
}};
|
||||
}
|
||||
use crate::util::mk_static;
|
||||
|
||||
impl PlantHal {
|
||||
pub async fn create() -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> {
|
||||
@@ -242,17 +251,23 @@ impl PlantHal {
|
||||
esp_alloc::heap_allocator!(size: 64 * 1024);
|
||||
esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
|
||||
|
||||
let rtc: Rtc = Rtc::new(peripherals.LPWR);
|
||||
TIME_ACCESS
|
||||
.init(Mutex::new(rtc))
|
||||
.map_err(|_| FatError::String {
|
||||
error: "Init error rct".to_string(),
|
||||
})?;
|
||||
let mut rtc_peripheral: Rtc = Rtc::new(peripherals.LPWR);
|
||||
rtc_peripheral.rwdt.disable();
|
||||
|
||||
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||
esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
|
||||
|
||||
// Initialize and enable the watchdog with 30 second timeout
|
||||
let mut wdt = timg0.wdt;
|
||||
wdt.set_timeout(MwdtStage::Stage0, esp_hal::time::Duration::from_secs(30));
|
||||
wdt.enable();
|
||||
WATCHDOG
|
||||
.init(embassy_sync::blocking_mutex::Mutex::new(RefCell::new(wdt)))
|
||||
.map_err(|_| FatError::String {
|
||||
error: "Watchdog already initialized".to_string(),
|
||||
})?;
|
||||
|
||||
let boot_button = Input::new(
|
||||
peripherals.GPIO9,
|
||||
InputConfig::default().with_pull(Pull::None),
|
||||
@@ -262,22 +277,16 @@ impl PlantHal {
|
||||
let wake_gpio1 = peripherals.GPIO1;
|
||||
|
||||
let rng = Rng::new();
|
||||
let esp_wifi_ctrl = &*mk_static!(
|
||||
Controller<'static>,
|
||||
init().expect("Could not init wifi controller")
|
||||
);
|
||||
|
||||
let (controller, interfaces) =
|
||||
esp_radio::wifi::new(esp_wifi_ctrl, peripherals.WIFI, Default::default())
|
||||
.expect("Could not init wifi");
|
||||
let (controller, interfaces) = esp_radio::wifi::new(peripherals.WIFI, Default::default())
|
||||
.map_err(|e| FatError::String {
|
||||
error: format!("Could not init wifi: {:?}", e),
|
||||
})?;
|
||||
|
||||
let pcnt_module = Pcnt::new(peripherals.PCNT);
|
||||
let mut pcnt_module = Pcnt::new(peripherals.PCNT);
|
||||
pcnt_module.set_interrupt_handler(water::flow_interrupt_handler);
|
||||
|
||||
let free_pins = FreePeripherals {
|
||||
// can: peripherals.can,
|
||||
// adc1: peripherals.adc1,
|
||||
// pcnt0: peripherals.pcnt0,
|
||||
// pcnt1: peripherals.pcnt1,
|
||||
gpio0: peripherals.GPIO0,
|
||||
gpio2: peripherals.GPIO2,
|
||||
gpio3: peripherals.GPIO3,
|
||||
@@ -327,35 +336,32 @@ impl PlantHal {
|
||||
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
|
||||
DataPartitionSubType::Ota,
|
||||
))?
|
||||
.expect("No OTA data partition found")
|
||||
.context("No OTA data partition found")?
|
||||
);
|
||||
|
||||
let ota_data = mk_static!(
|
||||
FlashRegion<MutexFlashStorage>,
|
||||
ota_data.as_embedded_storage(flash_storage_2)
|
||||
);
|
||||
let mut ota_data = ota_data.as_embedded_storage(mk_static!(
|
||||
RmwNorFlashStorage<&mut MutexFlashStorage>,
|
||||
RmwNorFlashStorage::new(flash_storage_2, mk_static!([u8; 4096], [0_u8; 4096]))
|
||||
));
|
||||
|
||||
let state_0 = ota_state(AppPartitionSubType::Ota0, ota_data);
|
||||
let state_1 = ota_state(AppPartitionSubType::Ota1, ota_data);
|
||||
let state_0 = ota_state(AppPartitionSubType::Ota0, &mut ota_data);
|
||||
let state_1 = ota_state(AppPartitionSubType::Ota1, &mut ota_data);
|
||||
let mut ota = Ota::new(ota_data, 2)?;
|
||||
let running = get_current_slot_and_fix_ota_data(&mut ota, state_0, state_1)?;
|
||||
let running = get_current_slot(&pt, &mut ota)?;
|
||||
let target = next_partition(running)?;
|
||||
|
||||
info!("Currently running OTA slot: {running:?}");
|
||||
info!("Updates will be stored in OTA slot: {target:?}");
|
||||
info!("Slot0 state: {state_0:?}");
|
||||
info!("Slot1 state: {state_1:?}");
|
||||
|
||||
//obtain current_state and next_state here!
|
||||
//get current_state and next_state here!
|
||||
let ota_target = match target {
|
||||
AppPartitionSubType::Ota0 => pt
|
||||
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
||||
AppPartitionSubType::Ota0,
|
||||
))?
|
||||
.find_partition(PartitionType::App(AppPartitionSubType::Ota0))?
|
||||
.context("Partition table invalid no ota0")?,
|
||||
AppPartitionSubType::Ota1 => pt
|
||||
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
||||
AppPartitionSubType::Ota1,
|
||||
))?
|
||||
.find_partition(PartitionType::App(AppPartitionSubType::Ota1))?
|
||||
.context("Partition table invalid no ota1")?,
|
||||
_ => {
|
||||
bail!("Invalid target partition");
|
||||
@@ -372,37 +378,30 @@ impl PlantHal {
|
||||
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
|
||||
DataPartitionSubType::LittleFs,
|
||||
))?
|
||||
.expect("Data partition with littlefs not found");
|
||||
.context("Storage data partition not found")?;
|
||||
let data_partition = mk_static!(PartitionEntry, data_partition);
|
||||
|
||||
let data = mk_static!(
|
||||
FlashRegion<MutexFlashStorage>,
|
||||
FlashRegion<'static, MutexFlashStorage>,
|
||||
data_partition.as_embedded_storage(flash_storage_3)
|
||||
);
|
||||
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
|
||||
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
|
||||
if lfs2filesystem.is_mountable() {
|
||||
log::info!("Littlefs2 filesystem is mountable");
|
||||
} else {
|
||||
match lfs2filesystem.format() {
|
||||
Result::Ok(..) => {
|
||||
log::info!("Littlefs2 filesystem is formatted");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Littlefs2 filesystem could not be formatted: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let fs = Arc::new(Mutex::new(
|
||||
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
|
||||
));
|
||||
let savegame = SavegameManager::new(data);
|
||||
info!(
|
||||
"Savegame storage initialized ({} slots × {} KB)",
|
||||
savegame_manager::SAVEGAME_SLOT_COUNT,
|
||||
savegame_manager::SAVEGAME_SLOT_SIZE / 1024
|
||||
);
|
||||
|
||||
let ap = interfaces.ap;
|
||||
let sta = interfaces.sta;
|
||||
let uart0 =
|
||||
Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String {
|
||||
error: "Uart creation failed".to_string(),
|
||||
})?;
|
||||
|
||||
let ap = interfaces.access_point;
|
||||
let sta = interfaces.station;
|
||||
let mut esp = Esp {
|
||||
fs,
|
||||
savegame,
|
||||
rng,
|
||||
controller: Arc::new(Mutex::new(controller)),
|
||||
interface_sta: Some(sta),
|
||||
@@ -414,6 +413,8 @@ impl PlantHal {
|
||||
current: running,
|
||||
slot0_state: state_0,
|
||||
slot1_state: state_1,
|
||||
uart0,
|
||||
rtc: rtc_peripheral,
|
||||
};
|
||||
|
||||
//init,reset rtc memory depending on cause
|
||||
@@ -449,33 +450,34 @@ impl PlantHal {
|
||||
SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu",
|
||||
},
|
||||
};
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
LogMessage::ResetReason,
|
||||
init_rtc_store as u32,
|
||||
to_config_mode as u32,
|
||||
"",
|
||||
&format!("{reasons:?}"),
|
||||
)
|
||||
.await;
|
||||
log(
|
||||
LogMessage::ResetReason,
|
||||
init_rtc_store as u32,
|
||||
to_config_mode as u32,
|
||||
"",
|
||||
&format!("{reasons:?}"),
|
||||
);
|
||||
|
||||
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
|
||||
.await;
|
||||
|
||||
let config = esp.load_config().await;
|
||||
|
||||
log::info!("Init rtc driver");
|
||||
info!("Init rtc driver");
|
||||
|
||||
let sda = peripherals.GPIO20;
|
||||
let scl = peripherals.GPIO19;
|
||||
|
||||
// Configure I2C with 1-second timeout
|
||||
// At 100 Hz I2C clock, one bus cycle = 10ms
|
||||
// For 1 second timeout: 100 bus cycles
|
||||
let i2c = I2c::new(
|
||||
peripherals.I2C0,
|
||||
Config::default()
|
||||
.with_frequency(Rate::from_hz(100))
|
||||
.with_timeout(BusTimeout::Maximum),
|
||||
//.with_frequency(Rate::from_hz(100))
|
||||
//1s at 100khz
|
||||
.with_timeout(BusTimeout::BusCycles(100_000))
|
||||
.with_scl_main_st_timeout(FsmTimeout::new(21)?),
|
||||
)?
|
||||
.with_scl(scl)
|
||||
.with_sda(sda);
|
||||
@@ -484,10 +486,13 @@ impl PlantHal {
|
||||
RefCell<I2c<Blocking>>,
|
||||
> = CriticalSectionMutex::new(RefCell::new(i2c));
|
||||
|
||||
I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver");
|
||||
I2C_DRIVER.init(i2c_bus).map_err(|_| FatError::String {
|
||||
error: "Could not init i2c driver".to_string(),
|
||||
})?;
|
||||
|
||||
let i2c_bus = I2C_DRIVER.get().await;
|
||||
let rtc_device = I2cDevice::new(i2c_bus);
|
||||
let mut bms_device = I2cDevice::new(i2c_bus);
|
||||
let eeprom_device = I2cDevice::new(i2c_bus);
|
||||
|
||||
let mut rtc: Ds323x<
|
||||
@@ -500,10 +505,10 @@ impl PlantHal {
|
||||
let rtc_time = rtc.datetime();
|
||||
match rtc_time {
|
||||
Ok(tt) => {
|
||||
log::info!("Rtc Module reports time at UTC {tt}");
|
||||
info!("Rtc Module reports time at UTC {tt}");
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!("Rtc Module could not be read {err:?}");
|
||||
info!("Rtc Module could not be read {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,86 +523,74 @@ impl PlantHal {
|
||||
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
|
||||
|
||||
let hal = match config {
|
||||
Result::Ok(config) => {
|
||||
Ok(config) => {
|
||||
let battery_interaction: Box<dyn BatteryInteraction + Send> =
|
||||
match config.hardware.battery {
|
||||
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}),
|
||||
BatteryBoardVersion::BQ34Z100G1 => {
|
||||
let battery_device = I2cDevice::new(I2C_DRIVER.get().await);
|
||||
let mut battery_driver = Bq34z100g1Driver {
|
||||
i2c: battery_device,
|
||||
delay: Delay::new(),
|
||||
flash_block_data: [0; 32],
|
||||
};
|
||||
let status = print_battery_bq34z100(&mut battery_driver);
|
||||
match status {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
LogMessage::BatteryCommunicationError,
|
||||
0u32,
|
||||
0,
|
||||
"",
|
||||
&format!("{err:?})"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Box::new(BQ34Z100G1 { battery_driver })
|
||||
}
|
||||
BatteryBoardVersion::WchI2cSlave => {
|
||||
// TODO use correct implementation once availible
|
||||
Box::new(NoBatteryMonitor {})
|
||||
let version = ProtocolVersion::read_from_i2c(&mut bms_device);
|
||||
let version_val = match version {
|
||||
Ok(v) => unsafe { core::mem::transmute::<ProtocolVersion, u32>(v) },
|
||||
Err(_) => 0,
|
||||
};
|
||||
if version_val == 1 {
|
||||
Box::new(WCHI2CSlave { i2c: bms_device })
|
||||
} else {
|
||||
//todo should be an error variant instead?
|
||||
Box::new(NoBatteryMonitor {})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board {
|
||||
BoardVersion::Initial => {
|
||||
initial_hal::create_initial_board(free_pins, config, esp)?
|
||||
}
|
||||
BoardVersion::V3 => {
|
||||
v3_hal::create_v3(free_pins, esp, config, battery_interaction, rtc_module)?
|
||||
}
|
||||
BoardVersion::V4 => {
|
||||
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
let board_hal: Box<dyn BoardInteraction + Send> = //match config.hardware.board {
|
||||
//BoardVersion::Initial => {
|
||||
// initial_hal::create_initial_board(free_pins, config, esp)?
|
||||
//}
|
||||
//BoardVersion::V4 => {
|
||||
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
|
||||
.await?;
|
||||
//}
|
||||
//};
|
||||
|
||||
HAL { board_hal }
|
||||
}
|
||||
Err(err) => {
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
LogMessage::ConfigModeMissingConfig,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
&err.to_string(),
|
||||
)
|
||||
.await;
|
||||
log(
|
||||
LogMessage::ConfigModeMissingConfig,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
&err.to_string(),
|
||||
);
|
||||
HAL {
|
||||
board_hal: initial_hal::create_initial_board(
|
||||
board_hal: v4_hal::create_v4(
|
||||
free_pins,
|
||||
PlantControllerConfig::default(),
|
||||
esp,
|
||||
)?,
|
||||
PlantControllerConfig::default(),
|
||||
Box::new(NoBatteryMonitor {}),
|
||||
rtc_module,
|
||||
)
|
||||
.await?,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Mutex::new(hal))
|
||||
}
|
||||
|
||||
/// Feed the watchdog timer to prevent system reset
|
||||
pub fn feed_watchdog() {
|
||||
if let Some(wdt_mutex) = WATCHDOG.try_get() {
|
||||
wdt_mutex.lock(|cell| {
|
||||
cell.borrow_mut().feed();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ota_state(
|
||||
slot: AppPartitionSubType,
|
||||
ota_data: &mut FlashRegion<MutexFlashStorage>,
|
||||
ota_data: &mut FlashRegion<RmwNorFlashStorage<&mut MutexFlashStorage>>,
|
||||
) -> OtaImageState {
|
||||
// Read and log OTA states for both slots before constructing Ota
|
||||
// Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
|
||||
@@ -613,56 +606,40 @@ fn ota_state(
|
||||
OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined)
|
||||
}
|
||||
|
||||
fn get_current_slot_and_fix_ota_data(
|
||||
ota: &mut Ota<MutexFlashStorage>,
|
||||
state0: OtaImageState,
|
||||
state1: OtaImageState,
|
||||
fn get_current_slot(
|
||||
pt: &PartitionTable,
|
||||
ota: &mut Ota<RmwNorFlashStorage<&mut MutexFlashStorage>>,
|
||||
) -> Result<AppPartitionSubType, FatError> {
|
||||
let state = ota.current_ota_state().unwrap_or_default();
|
||||
let swap = match state {
|
||||
OtaImageState::Invalid => true,
|
||||
OtaImageState::Aborted => true,
|
||||
OtaImageState::Undefined => {
|
||||
info!("Undefined image in current slot, bootloader wrong?");
|
||||
false
|
||||
let booted = pt.booted_partition()?.ok_or(FatError::OTAError)?;
|
||||
let booted_type = booted.partition_type();
|
||||
let booted_ota_type = match booted_type {
|
||||
PartitionType::App(subtype) => subtype,
|
||||
_ => {
|
||||
bail!("Booted partition is not an app partition");
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let current = ota.current_app_partition()?;
|
||||
if swap {
|
||||
let other = match current {
|
||||
AppPartitionSubType::Ota0 => state1,
|
||||
AppPartitionSubType::Ota1 => state0,
|
||||
_ => OtaImageState::Invalid,
|
||||
};
|
||||
|
||||
match other {
|
||||
OtaImageState::Invalid => {
|
||||
bail!(
|
||||
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
|
||||
current,
|
||||
state0,
|
||||
state1
|
||||
);
|
||||
}
|
||||
OtaImageState::Aborted => {
|
||||
bail!(
|
||||
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
|
||||
current,
|
||||
state0,
|
||||
state1
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
info!("Current slot has state {state:?} other state has {other:?} swapping");
|
||||
let next = next_partition(current)?;
|
||||
ota.set_current_app_partition(next)?;
|
||||
let expected_partition = ota.current_app_partition()?;
|
||||
if expected_partition == booted_ota_type {
|
||||
info!("Booted partition matches expected partition");
|
||||
} else {
|
||||
info!("Booted partition does not match expected partition, fixing ota entry");
|
||||
ota.set_current_app_partition(booted_ota_type)?;
|
||||
}
|
||||
|
||||
//we actually booted other slot, than partition table assumes
|
||||
return Ok(ota.current_app_partition()?);
|
||||
};
|
||||
Ok(current)
|
||||
let fixed = ota.current_app_partition()?;
|
||||
let state = ota.current_ota_state();
|
||||
info!("Expected partition: {expected_partition:?}, current partition: {booted_ota_type:?}, state: {state:?}");
|
||||
|
||||
if fixed != booted_ota_type {
|
||||
bail!(
|
||||
"Could not fix ota entry, booted partition is still not correct: {:?} != {:?}",
|
||||
booted_ota_type,
|
||||
fixed
|
||||
);
|
||||
}
|
||||
|
||||
Ok(booted_ota_type)
|
||||
}
|
||||
|
||||
pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSubType> {
|
||||
@@ -676,39 +653,38 @@ pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSub
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub async fn esp_time() -> DateTime<Utc> {
|
||||
let guard = TIME_ACCESS.get().await.lock().await;
|
||||
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
|
||||
}
|
||||
|
||||
pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> {
|
||||
{
|
||||
let guard = TIME_ACCESS.get().await.lock().await;
|
||||
guard.set_current_time_us(time.timestamp_micros() as u64);
|
||||
}
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.get_rtc_module()
|
||||
.set_rtc_time(&time.to_utc())
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)]
|
||||
pub struct Moistures {
|
||||
pub sensor_a_hz: [f32; PLANT_COUNT],
|
||||
pub sensor_b_hz: [f32; PLANT_COUNT],
|
||||
pub sensor_a_hz: [Option<f32>; PLANT_COUNT],
|
||||
pub sensor_b_hz: [Option<f32>; PLANT_COUNT],
|
||||
pub sensor_a_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
pub sensor_b_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
|
||||
pub struct DetectionResult {
|
||||
plant: [DetectionSensorResult; crate::hal::PLANT_COUNT],
|
||||
/// Request: which sensors to send IDENTIFY_CMD to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct DetectionRequest {
|
||||
pub plant: [SensorRequest; PLANT_COUNT],
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
|
||||
|
||||
/// Per-sensor portion of a detection request.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct SensorRequest {
|
||||
pub sensor_a: bool,
|
||||
pub sensor_b: bool,
|
||||
}
|
||||
|
||||
/// Response: detection result per plant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct Detection {
|
||||
pub plant: [DetectionSensorResult; PLANT_COUNT],
|
||||
}
|
||||
|
||||
/// Per-sensor detection result.
|
||||
/// `Some(build_minutes)` = sensor responded; value is its firmware build timestamp
|
||||
/// (minutes since Unix epoch, or 0 if not reported). `None` = not detected.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct DetectionSensorResult {
|
||||
sensor_a: bool,
|
||||
sensor_b: bool,
|
||||
pub sensor_a: Option<u32>,
|
||||
pub sensor_b: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::fat_error::FatResult;
|
||||
use crate::hal::Box;
|
||||
use async_trait::async_trait;
|
||||
use bincode::config::Configuration;
|
||||
use bincode::{config, Decode, Encode};
|
||||
use chrono::{DateTime, Utc};
|
||||
use ds323x::ic::DS3231;
|
||||
use ds323x::interface::I2cInterface;
|
||||
@@ -19,24 +17,21 @@ use esp_hal::Blocking;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
|
||||
const CONFIG: Configuration = config::standard();
|
||||
pub const EEPROM_PAGE: usize = 32;
|
||||
//
|
||||
#[async_trait(?Send)]
|
||||
pub trait RTCModuleInteraction {
|
||||
async fn get_backup_info(&mut self) -> FatResult<BackupHeader>;
|
||||
async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)>;
|
||||
async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()>;
|
||||
async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()>;
|
||||
async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>>;
|
||||
async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()>;
|
||||
}
|
||||
//
|
||||
const BACKUP_HEADER_MAX_SIZE: usize = 64;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)]
|
||||
fn write(&mut self, offset: u32, data: &[u8]) -> FatResult<()>;
|
||||
fn read(&mut self, offset: u32, data: &mut [u8]) -> FatResult<()>;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Default)]
|
||||
pub struct BackupHeader {
|
||||
pub timestamp: i64,
|
||||
crc16: u16,
|
||||
pub(crate) crc16: u16,
|
||||
pub size: u16,
|
||||
}
|
||||
//
|
||||
@@ -46,7 +41,7 @@ pub struct DS3231Module {
|
||||
DS3231,
|
||||
>,
|
||||
|
||||
pub(crate) storage: eeprom24x::Storage<
|
||||
pub storage: eeprom24x::Storage<
|
||||
I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>,
|
||||
B32,
|
||||
TwoBytes,
|
||||
@@ -57,67 +52,6 @@ pub struct DS3231Module {
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl RTCModuleInteraction for DS3231Module {
|
||||
async fn get_backup_info(&mut self) -> FatResult<BackupHeader> {
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
|
||||
self.storage.read(0, &mut header_page_buffer)?;
|
||||
|
||||
let (header, len): (BackupHeader, usize) =
|
||||
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
|
||||
|
||||
log::info!("Raw header is {header_page_buffer:?} with size {len}");
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
|
||||
self.storage.read(0, &mut header_page_buffer)?;
|
||||
let (header, _header_size): (BackupHeader, usize) =
|
||||
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
|
||||
|
||||
let mut buf = [0_u8; 32];
|
||||
let offset = chunk * buf.len() + BACKUP_HEADER_MAX_SIZE;
|
||||
|
||||
let end: usize = header.size as usize + BACKUP_HEADER_MAX_SIZE;
|
||||
let current_end = offset + buf.len();
|
||||
let chunk_size = if current_end > end {
|
||||
end - offset
|
||||
} else {
|
||||
buf.len()
|
||||
};
|
||||
if chunk_size == 0 {
|
||||
Ok((buf, 0, header.crc16))
|
||||
} else {
|
||||
self.storage.read(offset as u32, &mut buf)?;
|
||||
//&buf[..chunk_size];
|
||||
Ok((buf, chunk_size, header.crc16))
|
||||
}
|
||||
}
|
||||
async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()> {
|
||||
//skip header and write after
|
||||
self.storage
|
||||
.write((BACKUP_HEADER_MAX_SIZE + offset) as u32, bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()> {
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
|
||||
let time = self.get_rtc_time().await?.timestamp_millis();
|
||||
let header = BackupHeader {
|
||||
crc16: crc,
|
||||
timestamp: time,
|
||||
size: length as u16,
|
||||
};
|
||||
let config = config::standard();
|
||||
let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?;
|
||||
log::info!("Raw header is {header_page_buffer:?} with size {encoded}");
|
||||
self.storage.write(0, &header_page_buffer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>> {
|
||||
Ok(self.rtc.datetime()?.and_utc())
|
||||
}
|
||||
@@ -126,4 +60,14 @@ impl RTCModuleInteraction for DS3231Module {
|
||||
let naive_time = time.naive_utc();
|
||||
Ok(self.rtc.set_datetime(&naive_time)?)
|
||||
}
|
||||
|
||||
fn write(&mut self, offset: u32, data: &[u8]) -> FatResult<()> {
|
||||
self.storage.write(offset, data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&mut self, offset: u32, data: &mut [u8]) -> FatResult<()> {
|
||||
self.storage.read(offset, data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use embedded_savegame::storage::{Flash, Storage};
|
||||
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
|
||||
use esp_bootloader_esp_idf::partitions::{Error as PartitionError, FlashRegion};
|
||||
use log::{error, info};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::shared_flash::MutexFlashStorage;
|
||||
|
||||
/// Size of each save slot in bytes (16 KB).
|
||||
pub const SAVEGAME_SLOT_SIZE: usize = 16384;
|
||||
//keep a little of space at the end due to partition table offsets
|
||||
const SAFETY: usize = 5;
|
||||
/// Number of slots in the 8 MB storage partition.
|
||||
pub const SAVEGAME_SLOT_COUNT: usize = (8 * 1024 * 1024) / SAVEGAME_SLOT_SIZE - SAFETY; // 507
|
||||
|
||||
/// Metadata about a single existing save slot, returned by [`SavegameManager::list_saves`].
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct SaveInfo {
|
||||
pub idx: usize,
|
||||
pub len: u32,
|
||||
/// UTC timestamp in RFC3339 format when the save was created
|
||||
pub created_at: Option<alloc::string::String>,
|
||||
}
|
||||
|
||||
const SAVE_MAGIC: [u8; 4] = *b"SGM1";
|
||||
const SAVE_HEADER_LEN: usize = 6; // magic (4) + timestamp_len (u16)
|
||||
|
||||
struct ParsedSave<'a> {
|
||||
created_at: &'a str,
|
||||
data: &'a [u8],
|
||||
}
|
||||
|
||||
fn strip_padding(data: &[u8]) -> &[u8] {
|
||||
let mut end = data.len();
|
||||
while end > 0 {
|
||||
let b = data[end - 1];
|
||||
if b == 0x00 || b == 0xFF {
|
||||
end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
&data[..end]
|
||||
}
|
||||
|
||||
fn parse_save(data: &[u8]) -> FatResult<ParsedSave<'_>> {
|
||||
if data.len() < SAVE_HEADER_LEN {
|
||||
return Err(FatError::String {
|
||||
error: "Save payload too short".into(),
|
||||
});
|
||||
}
|
||||
if data[..4] != SAVE_MAGIC {
|
||||
return Err(FatError::String {
|
||||
error: "Save payload has invalid magic".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let timestamp_len = u16::from_le_bytes([data[4], data[5]]) as usize;
|
||||
let timestamp_end = SAVE_HEADER_LEN + timestamp_len;
|
||||
if timestamp_end > data.len() {
|
||||
return Err(FatError::String {
|
||||
error: "Save payload timestamp length exceeds data".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let created_at = core::str::from_utf8(&data[SAVE_HEADER_LEN..timestamp_end]).map_err(|e| {
|
||||
FatError::String {
|
||||
error: alloc::format!("Save payload contains invalid timestamp UTF-8: {e:?}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(ParsedSave {
|
||||
created_at,
|
||||
data: strip_padding(&data[timestamp_end..]),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Flash adapter ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Newtype wrapper around a [`PartitionError`] so we can implement the
|
||||
/// [`core::fmt::Debug`] bound required by [`embedded_savegame::storage::Flash`].
|
||||
#[derive(Debug)]
|
||||
pub struct SavegameFlashError(#[allow(dead_code)] PartitionError);
|
||||
|
||||
/// Adapts a `&mut FlashRegion<'static, MutexFlashStorage>` to the
|
||||
/// [`embedded_savegame::storage::Flash`] trait.
|
||||
///
|
||||
/// `erase(addr)` erases exactly one slot (`SAVEGAME_SLOT_SIZE` bytes) starting
|
||||
/// at `addr`, which is what embedded-savegame expects for NOR flash.
|
||||
pub struct SavegameFlashAdapter<'a> {
|
||||
region: &'a mut FlashRegion<'static, MutexFlashStorage>,
|
||||
}
|
||||
|
||||
impl Flash for SavegameFlashAdapter<'_> {
|
||||
type Error = SavegameFlashError;
|
||||
|
||||
fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), Self::Error> {
|
||||
ReadNorFlash::read(self.region, addr, buf).map_err(SavegameFlashError)
|
||||
}
|
||||
|
||||
fn write(&mut self, addr: u32, data: &mut [u8]) -> Result<(), Self::Error> {
|
||||
info!(
|
||||
"Relative writing to flash at 0x{:x} with length {}",
|
||||
addr,
|
||||
data.len()
|
||||
);
|
||||
let error = NorFlash::write(self.region, addr, data);
|
||||
if let Err(err) = error {
|
||||
error!("error {:?}", err);
|
||||
}
|
||||
error.map_err(SavegameFlashError)
|
||||
}
|
||||
|
||||
/// Erase one full slot at `addr`.
|
||||
/// embedded-savegame calls this before writing to a slot, so we erase
|
||||
/// the entire `SAVEGAME_SLOT_SIZE` bytes so subsequent writes land on
|
||||
/// pre-erased (0xFF) pages.
|
||||
/// Ensures addresses are aligned to ERASE_SIZE (4KB) boundaries.
|
||||
fn erase(&mut self, addr: u32) -> Result<(), Self::Error> {
|
||||
const ERASE_SIZE: u32 = 4096;
|
||||
// Align start address down to erase boundary
|
||||
let aligned_start = (addr / ERASE_SIZE) * ERASE_SIZE;
|
||||
// Align end address up to erase boundary
|
||||
let end = addr + SAVEGAME_SLOT_SIZE as u32;
|
||||
let aligned_end = end.div_ceil(ERASE_SIZE) * ERASE_SIZE;
|
||||
|
||||
info!(
|
||||
"Relative erasing flash at 0x{:x} (aligned to 0x{:x}-0x{:x})",
|
||||
addr, aligned_start, aligned_end
|
||||
);
|
||||
|
||||
if aligned_start != addr || aligned_end != end {
|
||||
log::warn!("Flash erase address not aligned: addr=0x{:x}, slot_size=0x{:x}. Aligned to 0x{:x}-0x{:x}", addr, SAVEGAME_SLOT_SIZE, aligned_start, aligned_end);
|
||||
}
|
||||
|
||||
match NorFlash::erase(self.region, aligned_start, aligned_end) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Flash erase failed: {:?}. 0x{:x}-0x{:x}",
|
||||
err, aligned_start, aligned_end
|
||||
);
|
||||
Err(SavegameFlashError(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SavegameFlashError> for FatError {
|
||||
fn from(e: SavegameFlashError) -> Self {
|
||||
FatError::String {
|
||||
error: alloc::format!("Savegame flash error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── SavegameManager ────────────────────────────────────────────────────────────
|
||||
|
||||
/// High-level save-game manager that stores JSON config blobs on the storage
|
||||
/// partition using [`embedded_savegame`] for wear leveling and power-fail safety.
|
||||
pub struct SavegameManager {
|
||||
storage: Storage<SavegameFlashAdapter<'static>, SAVEGAME_SLOT_SIZE, SAVEGAME_SLOT_COUNT>,
|
||||
}
|
||||
|
||||
impl SavegameManager {
|
||||
pub fn new(region: &'static mut FlashRegion<'static, MutexFlashStorage>) -> Self {
|
||||
Self {
|
||||
storage: Storage::new(SavegameFlashAdapter { region }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist `data` (JSON bytes) to the next available slot with a UTC timestamp.
|
||||
///
|
||||
/// `scan()` advances the internal wear-leveling pointer to the latest valid
|
||||
/// slot before `append()` writes to the next free one.
|
||||
/// Both operations are performed atomically on the same Storage instance.
|
||||
pub fn save(&mut self, data: &[u8], timestamp: &str) -> FatResult<()> {
|
||||
let timestamp_bytes = timestamp.as_bytes();
|
||||
let timestamp_len: u16 =
|
||||
timestamp_bytes
|
||||
.len()
|
||||
.try_into()
|
||||
.map_err(|_| FatError::String {
|
||||
error: "Timestamp too long for save header".into(),
|
||||
})?;
|
||||
|
||||
let mut serialized =
|
||||
Vec::with_capacity(SAVE_HEADER_LEN + timestamp_bytes.len() + data.len());
|
||||
serialized.extend_from_slice(&SAVE_MAGIC);
|
||||
serialized.extend_from_slice(×tamp_len.to_le_bytes());
|
||||
serialized.extend_from_slice(timestamp_bytes);
|
||||
serialized.extend_from_slice(data);
|
||||
|
||||
// Flash storage often requires length to be a multiple of 4.
|
||||
let padding = (4 - (serialized.len() % 4)) % 4;
|
||||
if padding > 0 {
|
||||
serialized.extend_from_slice(&[0u8; 4][..padding]);
|
||||
}
|
||||
|
||||
info!("Serialized config with size {} (padded)", serialized.len());
|
||||
self.storage.append(&mut serialized)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load the most recently saved data. Returns `None` if no valid save exists.
|
||||
/// Unwraps the SaveWrapper and returns only the config data.
|
||||
pub fn load_latest(&mut self) -> FatResult<Option<Vec<u8>>> {
|
||||
let slot = self.storage.scan()?;
|
||||
match slot {
|
||||
None => Ok(None),
|
||||
Some(slot) => self.load_slot(slot.idx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a specific save by slot index. Returns `None` if the slot is
|
||||
/// empty or contains an invalid checksum.
|
||||
/// Unwraps the SaveWrapper and returns only the config data.
|
||||
pub fn load_slot(&mut self, idx: usize) -> FatResult<Option<Vec<u8>>> {
|
||||
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
|
||||
match self.storage.read(idx, &mut buf)? {
|
||||
None => Ok(None),
|
||||
Some(data) => {
|
||||
let parsed = parse_save(data)?;
|
||||
Ok(Some(parsed.data.to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Erase a specific slot by index, effectively deleting it.
|
||||
pub fn delete_slot(&mut self, idx: usize) -> FatResult<()> {
|
||||
self.storage.erase(idx).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Iterate all slots and return metadata for every slot that contains a
|
||||
/// valid save, using the Storage read API to avoid assuming internal slot structure.
|
||||
/// Extracts timestamps from SaveWrapper if available.
|
||||
pub fn list_saves(&mut self) -> FatResult<Vec<SaveInfo>> {
|
||||
let mut saves = Vec::new();
|
||||
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
|
||||
for idx in 0..SAVEGAME_SLOT_COUNT {
|
||||
if let Some(data) = self.storage.read(idx, &mut buf)? {
|
||||
match parse_save(data) {
|
||||
Ok(save) => {
|
||||
saves.push(SaveInfo {
|
||||
idx,
|
||||
len: save.data.len() as u32,
|
||||
created_at: Some(alloc::string::String::from(save.created_at)),
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
saves.push(SaveInfo {
|
||||
idx,
|
||||
len: 0,
|
||||
created_at: Some(err.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(saves)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use embassy_sync::blocking_mutex::CriticalSectionMutex;
|
||||
use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash};
|
||||
use embedded_storage::ReadStorage;
|
||||
use esp_storage::{FlashStorage, FlashStorageError};
|
||||
use log::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MutexFlashStorage {
|
||||
@@ -36,7 +37,7 @@ impl ErrorType for MutexFlashStorage {
|
||||
}
|
||||
|
||||
impl ReadNorFlash for MutexFlashStorage {
|
||||
const READ_SIZE: usize = 0;
|
||||
const READ_SIZE: usize = 1;
|
||||
|
||||
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
|
||||
ReadStorage::read(self, offset, bytes)
|
||||
@@ -48,10 +49,11 @@ impl ReadNorFlash for MutexFlashStorage {
|
||||
}
|
||||
|
||||
impl NorFlash for MutexFlashStorage {
|
||||
const WRITE_SIZE: usize = 0;
|
||||
const ERASE_SIZE: usize = 0;
|
||||
const WRITE_SIZE: usize = 1;
|
||||
const ERASE_SIZE: usize = 4096;
|
||||
|
||||
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
|
||||
info!("Erasing flash from 0x{:x} to 0x{:x}", from, to);
|
||||
self.inner
|
||||
.lock(|f| NorFlash::erase(f.borrow_mut().deref_mut(), from, to))
|
||||
}
|
||||
|
||||
@@ -1,458 +0,0 @@
|
||||
use crate::bail;
|
||||
use crate::fat_error::FatError;
|
||||
use crate::hal::esp::{hold_disable, hold_enable};
|
||||
use crate::hal::rtc::RTCModuleInteraction;
|
||||
use crate::hal::v3_shift_register::ShiftRegister40;
|
||||
use crate::hal::water::TankSensor;
|
||||
use crate::hal::{BoardInteraction, FreePeripherals, Moistures, Sensor, PLANT_COUNT, TIME_ACCESS};
|
||||
use crate::log::{LogMessage, LOG_ACCESS};
|
||||
use crate::{
|
||||
config::PlantControllerConfig,
|
||||
hal::{battery::BatteryInteraction, esp::Esp},
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::string::ToString;
|
||||
use async_trait::async_trait;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_time::Timer;
|
||||
use embedded_hal::digital::OutputPin as _;
|
||||
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
|
||||
use esp_hal::pcnt::channel::CtrlMode::Keep;
|
||||
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
|
||||
use esp_hal::pcnt::unit::Unit;
|
||||
use measurements::{Current, Voltage};
|
||||
|
||||
const PUMP8_BIT: usize = 0;
|
||||
const PUMP1_BIT: usize = 1;
|
||||
const PUMP2_BIT: usize = 2;
|
||||
const PUMP3_BIT: usize = 3;
|
||||
const PUMP4_BIT: usize = 4;
|
||||
const PUMP5_BIT: usize = 5;
|
||||
const PUMP6_BIT: usize = 6;
|
||||
const PUMP7_BIT: usize = 7;
|
||||
const MS_0: usize = 8;
|
||||
const MS_4: usize = 9;
|
||||
const MS_2: usize = 10;
|
||||
const MS_3: usize = 11;
|
||||
const MS_1: usize = 13;
|
||||
const SENSOR_ON: usize = 12;
|
||||
|
||||
const SENSOR_A_1: u8 = 7;
|
||||
const SENSOR_A_2: u8 = 6;
|
||||
const SENSOR_A_3: u8 = 5;
|
||||
const SENSOR_A_4: u8 = 4;
|
||||
const SENSOR_A_5: u8 = 3;
|
||||
const SENSOR_A_6: u8 = 2;
|
||||
const SENSOR_A_7: u8 = 1;
|
||||
const SENSOR_A_8: u8 = 0;
|
||||
|
||||
const SENSOR_B_1: u8 = 8;
|
||||
const SENSOR_B_2: u8 = 9;
|
||||
const SENSOR_B_3: u8 = 10;
|
||||
const SENSOR_B_4: u8 = 11;
|
||||
const SENSOR_B_5: u8 = 12;
|
||||
const SENSOR_B_6: u8 = 13;
|
||||
const SENSOR_B_7: u8 = 14;
|
||||
const SENSOR_B_8: u8 = 15;
|
||||
|
||||
const CHARGING: usize = 14;
|
||||
const AWAKE: usize = 15;
|
||||
|
||||
const FAULT_3: usize = 16;
|
||||
const FAULT_8: usize = 17;
|
||||
const FAULT_7: usize = 18;
|
||||
const FAULT_6: usize = 19;
|
||||
const FAULT_5: usize = 20;
|
||||
const FAULT_4: usize = 21;
|
||||
const FAULT_1: usize = 22;
|
||||
const FAULT_2: usize = 23;
|
||||
|
||||
const REPEAT_MOIST_MEASURE: usize = 1;
|
||||
|
||||
pub struct V3<'a> {
|
||||
config: PlantControllerConfig,
|
||||
battery_monitor: Box<dyn BatteryInteraction + Send>,
|
||||
rtc_module: Box<dyn RTCModuleInteraction + Send>,
|
||||
esp: Esp<'a>,
|
||||
shift_register:
|
||||
Mutex<CriticalSectionRawMutex, ShiftRegister40<Output<'a>, Output<'a>, Output<'a>>>,
|
||||
_shift_register_enable_invert: Output<'a>,
|
||||
tank_sensor: TankSensor<'a>,
|
||||
solar_is_day: Input<'a>,
|
||||
light: Output<'a>,
|
||||
main_pump: Output<'a>,
|
||||
general_fault: Output<'a>,
|
||||
pub signal_counter: Unit<'static, 0>,
|
||||
}
|
||||
|
||||
pub(crate) fn create_v3(
|
||||
peripherals: FreePeripherals<'static>,
|
||||
esp: Esp<'static>,
|
||||
config: PlantControllerConfig,
|
||||
battery_monitor: Box<dyn BatteryInteraction + Send>,
|
||||
rtc_module: Box<dyn RTCModuleInteraction + Send>,
|
||||
) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
|
||||
log::info!("Start v3");
|
||||
let clock = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
|
||||
let latch = Output::new(peripherals.gpio3, Level::Low, OutputConfig::default());
|
||||
let data = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
|
||||
let shift_register = ShiftRegister40::new(clock, latch, data);
|
||||
//disable all
|
||||
for mut pin in shift_register.decompose() {
|
||||
let _ = pin.set_low();
|
||||
}
|
||||
|
||||
// Set always-on status bits
|
||||
let _ = shift_register.decompose()[AWAKE].set_high();
|
||||
let _ = shift_register.decompose()[CHARGING].set_high();
|
||||
|
||||
// Multiplexer defaults: ms0..ms3 low, ms4 high (disabled)
|
||||
let _ = shift_register.decompose()[MS_0].set_low();
|
||||
let _ = shift_register.decompose()[MS_1].set_low();
|
||||
let _ = shift_register.decompose()[MS_2].set_low();
|
||||
let _ = shift_register.decompose()[MS_3].set_low();
|
||||
let _ = shift_register.decompose()[MS_4].set_high();
|
||||
|
||||
let one_wire_pin = Flex::new(peripherals.gpio18);
|
||||
let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
|
||||
|
||||
let flow_sensor_pin = Input::new(
|
||||
peripherals.gpio4,
|
||||
InputConfig::default().with_pull(Pull::Up),
|
||||
);
|
||||
|
||||
let tank_sensor = TankSensor::create(
|
||||
one_wire_pin,
|
||||
peripherals.adc1,
|
||||
peripherals.gpio5,
|
||||
tank_power_pin,
|
||||
flow_sensor_pin,
|
||||
peripherals.pcnt1,
|
||||
)?;
|
||||
|
||||
let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
|
||||
let light = Output::new(peripherals.gpio10, Level::Low, OutputConfig::default());
|
||||
let mut main_pump = Output::new(peripherals.gpio2, Level::Low, OutputConfig::default());
|
||||
main_pump.set_low();
|
||||
let mut general_fault = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
|
||||
general_fault.set_low();
|
||||
|
||||
let mut shift_register_enable_invert =
|
||||
Output::new(peripherals.gpio21, Level::Low, OutputConfig::default());
|
||||
shift_register_enable_invert.set_low();
|
||||
|
||||
let signal_counter = peripherals.pcnt0;
|
||||
|
||||
signal_counter.set_high_limit(Some(i16::MAX))?;
|
||||
|
||||
let ch0 = &signal_counter.channel0;
|
||||
let edge_pin = Input::new(peripherals.gpio22, InputConfig::default());
|
||||
ch0.set_edge_signal(edge_pin.peripheral_input());
|
||||
ch0.set_input_mode(Hold, Increment);
|
||||
ch0.set_ctrl_mode(Keep, Keep);
|
||||
signal_counter.listen();
|
||||
|
||||
Ok(Box::new(V3 {
|
||||
config,
|
||||
battery_monitor,
|
||||
rtc_module,
|
||||
esp,
|
||||
shift_register: Mutex::new(shift_register),
|
||||
_shift_register_enable_invert: shift_register_enable_invert,
|
||||
tank_sensor,
|
||||
solar_is_day,
|
||||
light,
|
||||
main_pump,
|
||||
general_fault,
|
||||
signal_counter,
|
||||
}))
|
||||
}
|
||||
|
||||
impl V3<'_> {
|
||||
async fn inner_measure_moisture_hz(
|
||||
&mut self,
|
||||
plant: usize,
|
||||
sensor: Sensor,
|
||||
) -> Result<f32, FatError> {
|
||||
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
|
||||
for sample in results.iter_mut() {
|
||||
self.signal_counter.pause();
|
||||
self.signal_counter.clear();
|
||||
//Disable all
|
||||
{
|
||||
let shift_register = self.shift_register.lock().await;
|
||||
shift_register.decompose()[MS_4].set_high()?;
|
||||
}
|
||||
|
||||
let sensor_channel = match sensor {
|
||||
Sensor::A => match plant {
|
||||
0 => SENSOR_A_1,
|
||||
1 => SENSOR_A_2,
|
||||
2 => SENSOR_A_3,
|
||||
3 => SENSOR_A_4,
|
||||
4 => SENSOR_A_5,
|
||||
5 => SENSOR_A_6,
|
||||
6 => SENSOR_A_7,
|
||||
7 => SENSOR_A_8,
|
||||
_ => bail!("Invalid plant id {}", plant),
|
||||
},
|
||||
Sensor::B => match plant {
|
||||
0 => SENSOR_B_1,
|
||||
1 => SENSOR_B_2,
|
||||
2 => SENSOR_B_3,
|
||||
3 => SENSOR_B_4,
|
||||
4 => SENSOR_B_5,
|
||||
5 => SENSOR_B_6,
|
||||
6 => SENSOR_B_7,
|
||||
7 => SENSOR_B_8,
|
||||
_ => bail!("Invalid plant id {}", plant),
|
||||
},
|
||||
};
|
||||
|
||||
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
|
||||
{
|
||||
let shift_register = self.shift_register.lock().await;
|
||||
let pin_0 = &mut shift_register.decompose()[MS_0];
|
||||
let pin_1 = &mut shift_register.decompose()[MS_1];
|
||||
let pin_2 = &mut shift_register.decompose()[MS_2];
|
||||
let pin_3 = &mut shift_register.decompose()[MS_3];
|
||||
if is_bit_set(0) {
|
||||
pin_0.set_high()?;
|
||||
} else {
|
||||
pin_0.set_low()?;
|
||||
}
|
||||
if is_bit_set(1) {
|
||||
pin_1.set_high()?;
|
||||
} else {
|
||||
pin_1.set_low()?;
|
||||
}
|
||||
if is_bit_set(2) {
|
||||
pin_2.set_high()?;
|
||||
} else {
|
||||
pin_2.set_low()?;
|
||||
}
|
||||
if is_bit_set(3) {
|
||||
pin_3.set_high()?;
|
||||
} else {
|
||||
pin_3.set_low()?;
|
||||
}
|
||||
|
||||
shift_register.decompose()[MS_4].set_low()?;
|
||||
shift_register.decompose()[SENSOR_ON].set_high()?;
|
||||
}
|
||||
let measurement = 100; //how long to measure and then extrapolate to hz
|
||||
let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz
|
||||
|
||||
//give some time to stabilize
|
||||
Timer::after_millis(10).await;
|
||||
self.signal_counter.resume();
|
||||
Timer::after_millis(measurement).await;
|
||||
self.signal_counter.pause();
|
||||
{
|
||||
let shift_register = self.shift_register.lock().await;
|
||||
shift_register.decompose()[MS_4].set_high()?;
|
||||
shift_register.decompose()[SENSOR_ON].set_low()?;
|
||||
}
|
||||
Timer::after_millis(10).await;
|
||||
let unscaled = self.signal_counter.value();
|
||||
let hz = unscaled as f32 * factor;
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
LogMessage::RawMeasure,
|
||||
unscaled as u32,
|
||||
hz as u32,
|
||||
&plant.to_string(),
|
||||
&format!("{sensor:?}"),
|
||||
)
|
||||
.await;
|
||||
*sample = hz;
|
||||
}
|
||||
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
|
||||
|
||||
let mid = results.len() / 2;
|
||||
let median = results[mid];
|
||||
Ok(median)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<'a> BoardInteraction<'a> for V3<'a> {
|
||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
|
||||
Ok(&mut self.tank_sensor)
|
||||
}
|
||||
|
||||
fn get_esp(&mut self) -> &mut Esp<'a> {
|
||||
&mut self.esp
|
||||
}
|
||||
|
||||
fn get_config(&mut self) -> &PlantControllerConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send> {
|
||||
&mut self.battery_monitor
|
||||
}
|
||||
|
||||
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
|
||||
&mut self.rtc_module
|
||||
}
|
||||
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
|
||||
let shift_register = self.shift_register.lock().await;
|
||||
if charging {
|
||||
let _ = shift_register.decompose()[CHARGING].set_high();
|
||||
} else {
|
||||
let _ = shift_register.decompose()[CHARGING].set_low();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
|
||||
let _ = self.shift_register.lock().await.decompose()[AWAKE].set_low();
|
||||
let guard = TIME_ACCESS.get().await.lock().await;
|
||||
self.esp.deep_sleep(duration_in_ms, guard)
|
||||
}
|
||||
|
||||
fn is_day(&self) -> bool {
|
||||
self.solar_is_day.is_high()
|
||||
}
|
||||
|
||||
async fn light(&mut self, enable: bool) -> Result<(), FatError> {
|
||||
hold_disable(10);
|
||||
if enable {
|
||||
self.light.set_high();
|
||||
} else {
|
||||
self.light.set_low();
|
||||
}
|
||||
hold_enable(10);
|
||||
Ok(())
|
||||
}
|
||||
async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
|
||||
if enable {
|
||||
self.main_pump.set_high();
|
||||
}
|
||||
|
||||
let index = match plant {
|
||||
0 => PUMP1_BIT,
|
||||
1 => PUMP2_BIT,
|
||||
2 => PUMP3_BIT,
|
||||
3 => PUMP4_BIT,
|
||||
4 => PUMP5_BIT,
|
||||
5 => PUMP6_BIT,
|
||||
6 => PUMP7_BIT,
|
||||
7 => PUMP8_BIT,
|
||||
_ => bail!("Invalid pump {plant}"),
|
||||
};
|
||||
let shift_register = self.shift_register.lock().await;
|
||||
if enable {
|
||||
let _ = shift_register.decompose()[index].set_high();
|
||||
} else {
|
||||
let _ = shift_register.decompose()[index].set_low();
|
||||
}
|
||||
|
||||
if !enable {
|
||||
self.main_pump.set_low();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
|
||||
bail!("Not implemented in v3")
|
||||
}
|
||||
|
||||
async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
|
||||
let index = match plant {
|
||||
0 => FAULT_1,
|
||||
1 => FAULT_2,
|
||||
2 => FAULT_3,
|
||||
3 => FAULT_4,
|
||||
4 => FAULT_5,
|
||||
5 => FAULT_6,
|
||||
6 => FAULT_7,
|
||||
7 => FAULT_8,
|
||||
_ => panic!("Invalid plant id {}", plant),
|
||||
};
|
||||
let shift_register = self.shift_register.lock().await;
|
||||
if enable {
|
||||
let _ = shift_register.decompose()[index].set_high();
|
||||
} else {
|
||||
let _ = shift_register.decompose()[index].set_low();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
|
||||
let mut result = Moistures::default();
|
||||
for plant in 0..PLANT_COUNT {
|
||||
let a = self.inner_measure_moisture_hz(plant, Sensor::A).await;
|
||||
let b = self.inner_measure_moisture_hz(plant, Sensor::B).await;
|
||||
let aa = a.unwrap_or(u32::MAX as f32);
|
||||
let bb = b.unwrap_or(u32::MAX as f32);
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
LogMessage::TestSensor,
|
||||
aa as u32,
|
||||
bb as u32,
|
||||
&plant.to_string(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
result.sensor_a_hz[plant] = aa;
|
||||
result.sensor_b_hz[plant] = bb;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn general_fault(&mut self, enable: bool) {
|
||||
hold_disable(6);
|
||||
if enable {
|
||||
self.general_fault.set_high();
|
||||
} else {
|
||||
self.general_fault.set_low();
|
||||
}
|
||||
hold_enable(6);
|
||||
}
|
||||
|
||||
async fn test(&mut self) -> Result<(), FatError> {
|
||||
self.general_fault(true).await;
|
||||
Timer::after_millis(100).await;
|
||||
self.general_fault(false).await;
|
||||
Timer::after_millis(100).await;
|
||||
self.light(true).await?;
|
||||
Timer::after_millis(500).await;
|
||||
|
||||
self.light(false).await?;
|
||||
Timer::after_millis(500).await;
|
||||
for i in 0..PLANT_COUNT {
|
||||
self.fault(i, true).await?;
|
||||
Timer::after_millis(500).await;
|
||||
self.fault(i, false).await?;
|
||||
Timer::after_millis(500).await;
|
||||
}
|
||||
for i in 0..PLANT_COUNT {
|
||||
self.pump(i, true).await?;
|
||||
Timer::after_millis(100).await;
|
||||
self.pump(i, false).await?;
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
self.measure_moisture_hz().await?;
|
||||
Timer::after_millis(10).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_config(&mut self, config: PlantControllerConfig) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
|
||||
bail!("Not implemented in v3")
|
||||
}
|
||||
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
|
||||
bail!("Not implemented in v3")
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
//! Serial-in parallel-out shift register
|
||||
#![allow(warnings)]
|
||||
use core::cell::RefCell;
|
||||
use core::convert::Infallible;
|
||||
use core::iter::Iterator;
|
||||
use core::mem::{self, MaybeUninit};
|
||||
use core::result::{Result, Result::Ok};
|
||||
use embedded_hal::digital::OutputPin;
|
||||
|
||||
trait ShiftRegisterInternal: Send {
|
||||
fn update(&self, index: usize, command: bool) -> Result<(), ()>;
|
||||
}
|
||||
|
||||
/// Output pin of the shift register
|
||||
pub struct ShiftRegisterPin<'a> {
|
||||
shift_register: &'a dyn ShiftRegisterInternal,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> ShiftRegisterPin<'a> {
|
||||
fn new(shift_register: &'a dyn ShiftRegisterInternal, index: usize) -> Self {
|
||||
ShiftRegisterPin {
|
||||
shift_register,
|
||||
index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl embedded_hal::digital::ErrorType for ShiftRegisterPin<'_> {
|
||||
type Error = Infallible;
|
||||
}
|
||||
|
||||
impl OutputPin for ShiftRegisterPin<'_> {
|
||||
fn set_low(&mut self) -> Result<(), Infallible> {
|
||||
self.shift_register.update(self.index, false).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_high(&mut self) -> Result<(), Infallible> {
|
||||
self.shift_register.update(self.index, true).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! ShiftRegisterBuilder {
|
||||
($name: ident, $size: expr) => {
|
||||
/// Serial-in parallel-out shift register
|
||||
pub struct $name<Pin1, Pin2, Pin3>
|
||||
where
|
||||
Pin1: OutputPin + Send,
|
||||
Pin2: OutputPin + Send,
|
||||
Pin3: OutputPin + Send,
|
||||
{
|
||||
clock: RefCell<Pin1>,
|
||||
latch: RefCell<Pin2>,
|
||||
data: RefCell<Pin3>,
|
||||
output_state: RefCell<[bool; $size]>,
|
||||
}
|
||||
|
||||
impl<Pin1, Pin2, Pin3> ShiftRegisterInternal for $name<Pin1, Pin2, Pin3>
|
||||
where
|
||||
Pin1: OutputPin + Send,
|
||||
Pin2: OutputPin + Send,
|
||||
Pin3: OutputPin + Send,
|
||||
{
|
||||
/// Sets the value of the shift register output at `index` to value `command`
|
||||
fn update(&self, index: usize, command: bool) -> Result<(), ()> {
|
||||
self.output_state.borrow_mut()[index] = command;
|
||||
let output_state = self.output_state.borrow();
|
||||
self.latch.borrow_mut().set_low().map_err(|_e| ())?;
|
||||
|
||||
for i in 1..=output_state.len() {
|
||||
if output_state[output_state.len() - i] {
|
||||
self.data.borrow_mut().set_high().map_err(|_e| ())?;
|
||||
} else {
|
||||
self.data.borrow_mut().set_low().map_err(|_e| ())?;
|
||||
}
|
||||
self.clock.borrow_mut().set_high().map_err(|_e| ())?;
|
||||
self.clock.borrow_mut().set_low().map_err(|_e| ())?;
|
||||
}
|
||||
|
||||
self.latch.borrow_mut().set_high().map_err(|_e| ())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Pin1, Pin2, Pin3> $name<Pin1, Pin2, Pin3>
|
||||
where
|
||||
Pin1: OutputPin + Send,
|
||||
Pin2: OutputPin + Send,
|
||||
Pin3: OutputPin + Send,
|
||||
{
|
||||
/// Creates a new SIPO shift register from clock, latch, and data output pins
|
||||
pub fn new(clock: Pin1, latch: Pin2, data: Pin3) -> Self {
|
||||
$name {
|
||||
clock: RefCell::new(clock),
|
||||
latch: RefCell::new(latch),
|
||||
data: RefCell::new(data),
|
||||
output_state: RefCell::new([false; $size]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get embedded-hal output pins to control the shift register outputs
|
||||
pub fn decompose(&self) -> [ShiftRegisterPin<'_>; $size] {
|
||||
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is
|
||||
// safe because the type we are claiming to have initialized here is a
|
||||
// bunch of `MaybeUninit`s, which do not require initialization.
|
||||
let mut pins: [MaybeUninit<ShiftRegisterPin>; $size] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
// Dropping a `MaybeUninit` does nothing, so if there is a panic during this loop,
|
||||
// we have a memory leak, but there is no memory safety issue.
|
||||
for (index, elem) in pins.iter_mut().enumerate() {
|
||||
elem.write(ShiftRegisterPin::new(self, index));
|
||||
}
|
||||
|
||||
// Everything is initialized. Transmute the array to the
|
||||
// initialized type.
|
||||
unsafe { mem::transmute::<_, [ShiftRegisterPin; $size]>(pins) }
|
||||
}
|
||||
|
||||
/// Consume the shift register and return the original clock, latch, and data output pins
|
||||
pub fn release(self) -> (Pin1, Pin2, Pin3) {
|
||||
let Self {
|
||||
clock,
|
||||
latch,
|
||||
data,
|
||||
output_state: _,
|
||||
} = self;
|
||||
(clock.into_inner(), latch.into_inner(), data.into_inner())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ShiftRegisterBuilder!(ShiftRegister8, 8);
|
||||
ShiftRegisterBuilder!(ShiftRegister16, 16);
|
||||
ShiftRegisterBuilder!(ShiftRegister24, 24);
|
||||
ShiftRegisterBuilder!(ShiftRegister32, 32);
|
||||
ShiftRegisterBuilder!(ShiftRegister40, 40);
|
||||
ShiftRegisterBuilder!(ShiftRegister48, 48);
|
||||
ShiftRegisterBuilder!(ShiftRegister56, 56);
|
||||
ShiftRegisterBuilder!(ShiftRegister64, 64);
|
||||
ShiftRegisterBuilder!(ShiftRegister72, 72);
|
||||
ShiftRegisterBuilder!(ShiftRegister80, 80);
|
||||
ShiftRegisterBuilder!(ShiftRegister88, 88);
|
||||
ShiftRegisterBuilder!(ShiftRegister96, 96);
|
||||
ShiftRegisterBuilder!(ShiftRegister104, 104);
|
||||
ShiftRegisterBuilder!(ShiftRegister112, 112);
|
||||
ShiftRegisterBuilder!(ShiftRegister120, 120);
|
||||
ShiftRegisterBuilder!(ShiftRegister128, 128);
|
||||
|
||||
/// 8 output serial-in parallel-out shift register
|
||||
pub type ShiftRegister<Pin1, Pin2, Pin3> = ShiftRegister8<Pin1, Pin2, Pin3>;
|
||||
@@ -1,38 +1,50 @@
|
||||
use crate::bail;
|
||||
use crate::config::PlantControllerConfig;
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||
use crate::hal::battery::BatteryInteraction;
|
||||
use crate::hal::esp::{hold_disable, hold_enable, Esp};
|
||||
use crate::hal::rtc::RTCModuleInteraction;
|
||||
use crate::hal::v4_sensor::{SensorImpl, SensorInteraction};
|
||||
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction, EEPROM_PAGE, X25};
|
||||
use crate::hal::water::TankSensor;
|
||||
use crate::hal::{
|
||||
BoardInteraction, DetectionResult, FreePeripherals, Moistures, I2C_DRIVER, PLANT_COUNT,
|
||||
TIME_ACCESS,
|
||||
BoardInteraction, Detection, DetectionRequest, FreePeripherals, Moistures, Sensor, I2C_DRIVER,
|
||||
PLANT_COUNT,
|
||||
};
|
||||
use crate::log::{LogMessage, LOG_ACCESS};
|
||||
use crate::log::{log, LogMessage};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::ToString;
|
||||
use async_trait::async_trait;
|
||||
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
|
||||
use canapi::SensorSlot;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use core::cmp::min;
|
||||
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_time::Timer;
|
||||
use embassy_time::{Duration, Timer, WithTimeout};
|
||||
use embedded_can::{Frame, Id};
|
||||
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
|
||||
use esp_hal::i2c::master::I2c;
|
||||
use esp_hal::pcnt::channel::CtrlMode::Keep;
|
||||
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
|
||||
use esp_hal::twai::TwaiMode;
|
||||
use esp_hal::{twai, Blocking};
|
||||
use esp_hal::twai::{EspTwaiError, EspTwaiFrame, StandardId, Twai, TwaiConfiguration, TwaiMode};
|
||||
use esp_hal::{twai, Async, Blocking};
|
||||
use ina219::address::{Address, Pin};
|
||||
use ina219::calibration::UnCalibrated;
|
||||
use ina219::configuration::{Configuration, OperatingMode, Resolution};
|
||||
use ina219::SyncIna219;
|
||||
use log::{error, info, warn};
|
||||
use measurements::Resistance;
|
||||
use measurements::{Current, Voltage};
|
||||
// use no_panic::no_panic;
|
||||
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
|
||||
|
||||
pub const BACKUP_HEADER_MAX_SIZE: usize = 64;
|
||||
|
||||
const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64;
|
||||
const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::B125K;
|
||||
const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::Custom(twai::TimingConfig {
|
||||
baud_rate_prescaler: 200, // 40MHz / 200 * 2 = 100 on C6, 100 * 20 = 2000 divisor, 40MHz / 2000 = 20kHz
|
||||
sync_jump_width: 3, // 4 TQ
|
||||
tseg_1: 15,
|
||||
tseg_2: 4,
|
||||
triple_sample: true,
|
||||
});
|
||||
|
||||
pub enum Charger<'a> {
|
||||
SolarMpptV1 {
|
||||
@@ -88,7 +100,7 @@ impl Charger<'_> {
|
||||
operating_mode: OperatingMode::PowerDown,
|
||||
})
|
||||
.map_err(|e| {
|
||||
log::info!(
|
||||
info!(
|
||||
"Error setting ina mppt configuration during deep sleep preparation{e:?}"
|
||||
);
|
||||
});
|
||||
@@ -127,9 +139,16 @@ pub struct V4<'a> {
|
||||
pump_ina: Option<
|
||||
SyncIna219<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>, UnCalibrated>,
|
||||
>,
|
||||
sensor: SensorImpl,
|
||||
can_power: Output<'static>,
|
||||
|
||||
extra1: Output<'a>,
|
||||
extra2: Output<'a>,
|
||||
twai_config: Option<TwaiConfiguration<'static, Blocking>>,
|
||||
|
||||
/// Last known firmware build timestamps per sensor (minutes since Unix epoch).
|
||||
/// Updated during detect_sensors; preserved across normal measurement cycles.
|
||||
sensor_a_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
sensor_b_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
}
|
||||
|
||||
pub(crate) async fn create_v4(
|
||||
@@ -139,23 +158,40 @@ pub(crate) async fn create_v4(
|
||||
battery_monitor: Box<dyn BatteryInteraction + Send>,
|
||||
rtc_module: Box<dyn RTCModuleInteraction + Send>,
|
||||
) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
|
||||
log::info!("Start v4");
|
||||
info!("Start v4");
|
||||
let mut awake = Output::new(peripherals.gpio21, Level::High, OutputConfig::default());
|
||||
awake.set_high();
|
||||
info!("v4: gpio21 awake ok");
|
||||
|
||||
let mut general_fault = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
|
||||
general_fault.set_low();
|
||||
info!("v4: gpio23 general_fault ok");
|
||||
|
||||
let twai_config = Some(TwaiConfiguration::new(
|
||||
peripherals.twai,
|
||||
peripherals.gpio0,
|
||||
peripherals.gpio2,
|
||||
TWAI_BAUDRATE,
|
||||
TwaiMode::Normal,
|
||||
));
|
||||
info!("v4: twai config ok");
|
||||
|
||||
let extra1 = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
|
||||
info!("v4: gpio6 extra1 ok");
|
||||
let extra2 = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
|
||||
info!("v4: gpio15 extra2 ok");
|
||||
|
||||
let one_wire_pin = Flex::new(peripherals.gpio18);
|
||||
info!("v4: gpio18 one_wire ok");
|
||||
let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
|
||||
info!("v4: gpio11 tank_power ok");
|
||||
let flow_sensor_pin = Input::new(
|
||||
peripherals.gpio4,
|
||||
InputConfig::default().with_pull(Pull::Up),
|
||||
);
|
||||
info!("v4: gpio4 flow_sensor ok");
|
||||
|
||||
info!("v4: creating tank sensor");
|
||||
let tank_sensor = TankSensor::create(
|
||||
one_wire_pin,
|
||||
peripherals.adc1,
|
||||
@@ -164,68 +200,31 @@ pub(crate) async fn create_v4(
|
||||
flow_sensor_pin,
|
||||
peripherals.pcnt1,
|
||||
)?;
|
||||
info!("v4: tank sensor ok");
|
||||
|
||||
let sensor_expander_device = I2cDevice::new(I2C_DRIVER.get().await);
|
||||
let mut sensor_expander = Pca9535Immediate::new(sensor_expander_device, 34);
|
||||
let sensor = match sensor_expander.pin_into_output(GPIOBank::Bank0, 0) {
|
||||
Ok(_) => {
|
||||
log::info!("SensorExpander answered");
|
||||
|
||||
let signal_counter = peripherals.pcnt0;
|
||||
|
||||
signal_counter.set_high_limit(Some(i16::MAX))?;
|
||||
|
||||
let ch0 = &signal_counter.channel0;
|
||||
let edge_pin = Input::new(peripherals.gpio22, InputConfig::default());
|
||||
ch0.set_edge_signal(edge_pin.peripheral_input());
|
||||
ch0.set_input_mode(Hold, Increment);
|
||||
ch0.set_ctrl_mode(Keep, Keep);
|
||||
signal_counter.listen();
|
||||
|
||||
for pin in 0..8 {
|
||||
let _ = sensor_expander.pin_into_output(GPIOBank::Bank0, pin);
|
||||
let _ = sensor_expander.pin_into_output(GPIOBank::Bank1, pin);
|
||||
let _ = sensor_expander.pin_set_low(GPIOBank::Bank0, pin);
|
||||
let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin);
|
||||
}
|
||||
|
||||
SensorImpl::PulseCounter {
|
||||
signal_counter,
|
||||
sensor_expander,
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::info!("Can bus mode ");
|
||||
let twai_config = Some(twai::TwaiConfiguration::new(
|
||||
peripherals.twai,
|
||||
peripherals.gpio2,
|
||||
peripherals.gpio0,
|
||||
TWAI_BAUDRATE,
|
||||
TwaiMode::Normal,
|
||||
));
|
||||
let can_power = Output::new(peripherals.gpio22, Level::Low, OutputConfig::default());
|
||||
|
||||
//can bus version
|
||||
SensorImpl::CanBus {
|
||||
twai_config,
|
||||
can_power,
|
||||
}
|
||||
}
|
||||
};
|
||||
let can_power = Output::new(peripherals.gpio22, Level::Low, OutputConfig::default());
|
||||
info!("v4: gpio22 can_power ok");
|
||||
|
||||
let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
|
||||
info!("v4: gpio7 solar_is_day ok");
|
||||
let light = Output::new(peripherals.gpio10, Level::Low, Default::default());
|
||||
info!("v4: gpio10 light ok");
|
||||
let charge_indicator = Output::new(peripherals.gpio3, Level::Low, Default::default());
|
||||
info!("v4: gpio3 charge_indicator ok");
|
||||
|
||||
info!("Start pump expander");
|
||||
let pump_device = I2cDevice::new(I2C_DRIVER.get().await);
|
||||
let mut pump_expander = Pca9535Immediate::new(pump_device, 32);
|
||||
for pin in 0..8 {
|
||||
let _ = pump_expander.pin_set_low(GPIOBank::Bank0, pin);
|
||||
let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin);
|
||||
let _ = pump_expander.pin_into_output(GPIOBank::Bank0, pin);
|
||||
let _ = pump_expander.pin_into_output(GPIOBank::Bank1, pin);
|
||||
let _ = pump_expander.pin_set_low(GPIOBank::Bank0, pin);
|
||||
let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin);
|
||||
}
|
||||
|
||||
info!("Start mppt");
|
||||
let mppt_current = I2cDevice::new(I2C_DRIVER.get().await);
|
||||
let mppt_ina = match SyncIna219::new(mppt_current, Address::from_pins(Pin::Vcc, Pin::Gnd)) {
|
||||
Ok(mut ina) => {
|
||||
@@ -241,16 +240,17 @@ pub(crate) async fn create_v4(
|
||||
Some(ina)
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!("Error creating mppt ina: {err:?}");
|
||||
info!("Error creating mppt ina: {err:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
info!("Start pump current sensor");
|
||||
let pump_current_dev = I2cDevice::new(I2C_DRIVER.get().await);
|
||||
let pump_ina = match SyncIna219::new(pump_current_dev, Address::from_pins(Pin::Gnd, Pin::Sda)) {
|
||||
Ok(ina) => Some(ina),
|
||||
Err(err) => {
|
||||
log::info!("Error creating pump ina: {err:?}");
|
||||
info!("Error creating pump ina: {err:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -262,7 +262,7 @@ pub(crate) async fn create_v4(
|
||||
bus_voltage_range: Default::default(),
|
||||
shunt_voltage_range: Default::default(),
|
||||
bus_resolution: Default::default(),
|
||||
shunt_resolution: ina219::configuration::Resolution::Avg128,
|
||||
shunt_resolution: Resolution::Avg128,
|
||||
operating_mode: Default::default(),
|
||||
})?;
|
||||
|
||||
@@ -275,6 +275,7 @@ pub(crate) async fn create_v4(
|
||||
None => Charger::ErrorInit {},
|
||||
};
|
||||
|
||||
info!("Assembling final v4 board interaction object");
|
||||
let v = V4 {
|
||||
rtc_module,
|
||||
esp,
|
||||
@@ -289,15 +290,18 @@ pub(crate) async fn create_v4(
|
||||
charger,
|
||||
extra1,
|
||||
extra2,
|
||||
sensor,
|
||||
can_power,
|
||||
twai_config,
|
||||
sensor_a_build_minutes: [None; PLANT_COUNT],
|
||||
sensor_b_build_minutes: [None; PLANT_COUNT],
|
||||
};
|
||||
Ok(Box::new(v))
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
|
||||
Ok(&mut self.tank_sensor)
|
||||
fn get_tank_sensor(&mut self) -> &mut TankSensor<'a> {
|
||||
&mut self.tank_sensor
|
||||
}
|
||||
|
||||
fn get_esp(&mut self) -> &mut Esp<'a> {
|
||||
@@ -316,15 +320,24 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
&mut self.rtc_module
|
||||
}
|
||||
|
||||
async fn get_time(&mut self) -> DateTime<Utc> {
|
||||
self.esp.get_time()
|
||||
}
|
||||
|
||||
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
|
||||
self.rtc_module.set_rtc_time(&time.to_utc()).await?;
|
||||
self.esp.set_time(time.to_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
|
||||
self.charger.set_charge_indicator(charging)
|
||||
}
|
||||
|
||||
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
|
||||
async fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
|
||||
self.awake.set_low();
|
||||
self.charger.power_save();
|
||||
let rtc = TIME_ACCESS.get().await.lock().await;
|
||||
self.esp.deep_sleep(duration_in_ms, rtc);
|
||||
self.esp.deep_sleep(duration_in_ms);
|
||||
}
|
||||
|
||||
fn is_day(&self) -> bool {
|
||||
@@ -356,19 +369,11 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
bail!("pump current sensor not available");
|
||||
}
|
||||
Some(pump_ina) => {
|
||||
let v = pump_ina
|
||||
.shunt_voltage()
|
||||
.map_err(|e| FatError::String {
|
||||
error: alloc::format!("{e:?}"),
|
||||
})
|
||||
.map(|v| {
|
||||
let shunt_voltage =
|
||||
Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
|
||||
let shut_value = Resistance::from_ohms(0.05_f64);
|
||||
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
|
||||
Current::from_amperes(current)
|
||||
})?;
|
||||
Ok(v)
|
||||
let raw = pump_ina.shunt_voltage()?;
|
||||
let shunt_voltage = Voltage::from_microvolts(raw.shunt_voltage_uv().abs() as f64);
|
||||
let shut_value = Resistance::from_ohms(0.05_f64);
|
||||
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
|
||||
Ok(Current::from_amperes(current))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,9 +388,48 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
|
||||
self.can_power.set_high();
|
||||
Timer::after_millis(500).await;
|
||||
let config = self.twai_config.take().context("twai config not set")?;
|
||||
let mut twai = config.into_async().start();
|
||||
|
||||
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
|
||||
self.sensor.measure_moisture_hz().await
|
||||
if twai.is_bus_off() {
|
||||
info!("Bus offline after start, attempting recovery");
|
||||
// Re-start to initiate recovery
|
||||
twai = twai.stop().start();
|
||||
}
|
||||
|
||||
let res = (async {
|
||||
Timer::after_millis(10).await;
|
||||
|
||||
let mut moistures = Moistures::default();
|
||||
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
|
||||
.with_timeout(Duration::from_millis(5000))
|
||||
.await;
|
||||
Ok(moistures)
|
||||
})
|
||||
.await;
|
||||
|
||||
let config = twai.stop().into_blocking();
|
||||
self.twai_config.replace(config);
|
||||
self.can_power.set_low();
|
||||
|
||||
// Persist any firmware build timestamps received alongside moisture data.
|
||||
if let Ok(ref moistures) = res {
|
||||
for (i, v) in moistures.sensor_a_build_minutes.iter().enumerate() {
|
||||
if v.is_some() {
|
||||
self.sensor_a_build_minutes[i] = *v;
|
||||
}
|
||||
}
|
||||
for (i, v) in moistures.sensor_b_build_minutes.iter().enumerate() {
|
||||
if v.is_some() {
|
||||
self.sensor_b_build_minutes[i] = *v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn general_fault(&mut self, enable: bool) {
|
||||
@@ -425,13 +469,9 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
}
|
||||
let moisture = self.measure_moisture_hz().await?;
|
||||
for plant in 0..PLANT_COUNT {
|
||||
let a = moisture.sensor_a_hz[plant] as u32;
|
||||
let b = moisture.sensor_b_hz[plant] as u32;
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(LogMessage::TestSensor, a, b, &plant.to_string(), "")
|
||||
.await;
|
||||
let a = moisture.sensor_a_hz[plant].unwrap_or(0.0) as u32;
|
||||
let b = moisture.sensor_b_hz[plant].unwrap_or(0.0) as u32;
|
||||
log(LogMessage::TestSensor, a, b, &(plant + 1).to_string(), "");
|
||||
}
|
||||
Timer::after_millis(10).await;
|
||||
Ok(())
|
||||
@@ -449,7 +489,280 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
self.charger.get_mppt_current()
|
||||
}
|
||||
|
||||
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
|
||||
self.sensor.autodetect().await
|
||||
async fn can_power(&mut self, state: bool) -> FatResult<()> {
|
||||
if state && self.can_power.is_set_low() {
|
||||
self.can_power.set_high();
|
||||
} else {
|
||||
self.can_power.set_low();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fertilizer_pump(&mut self, enable: bool) -> FatResult<()> {
|
||||
if enable {
|
||||
self.extra2.set_high();
|
||||
} else {
|
||||
self.extra2.set_low();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn backup_config(&mut self, controller_config: &PlantControllerConfig) -> FatResult<()> {
|
||||
let mut buffer: [u8; 4096 - BACKUP_HEADER_MAX_SIZE] = [0; 4096 - BACKUP_HEADER_MAX_SIZE];
|
||||
let length = postcard::to_slice(controller_config, &mut buffer)?.len();
|
||||
info!("Writing backup config of size {}", length);
|
||||
let mut checksum = X25.digest();
|
||||
checksum.update(&buffer[..length]);
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
|
||||
let time = self.rtc_module.get_rtc_time().await?.timestamp_millis();
|
||||
let header = BackupHeader {
|
||||
crc16: checksum.finalize(),
|
||||
timestamp: time,
|
||||
size: length as u16,
|
||||
};
|
||||
info!("Header is {:?}", header);
|
||||
postcard::to_slice(&header, &mut header_page_buffer)?;
|
||||
info!("Header is serialized");
|
||||
self.get_rtc_module().write(0, &header_page_buffer)?;
|
||||
info!("Header written");
|
||||
let mut to_write = length;
|
||||
let mut chunk: usize = 0;
|
||||
|
||||
while to_write > 0 {
|
||||
self.progress(chunk as u32).await;
|
||||
let start = chunk * EEPROM_PAGE;
|
||||
let end = start + min(EEPROM_PAGE, to_write);
|
||||
let part = &buffer[start..end];
|
||||
info!(
|
||||
"Writing chunk {} of size {} to offset {}",
|
||||
chunk,
|
||||
part.len(),
|
||||
start
|
||||
);
|
||||
to_write -= part.len();
|
||||
self.get_rtc_module()
|
||||
.write((BACKUP_HEADER_MAX_SIZE + chunk * EEPROM_PAGE) as u32, part)?;
|
||||
chunk += 1;
|
||||
}
|
||||
info!("Backup complete");
|
||||
self.clear_progress().await;
|
||||
Ok(())
|
||||
}
|
||||
async fn read_backup(&mut self) -> FatResult<PlantControllerConfig> {
|
||||
let info = self.backup_info().await?;
|
||||
let mut store = alloc::vec![0_u8; info.size as usize];
|
||||
self.rtc_module
|
||||
.read(BACKUP_HEADER_MAX_SIZE as u32, store.as_mut_slice())?;
|
||||
info!("Read backup data of size {}", store.len());
|
||||
let mut checksum = X25.digest();
|
||||
info!("Calculating CRC");
|
||||
checksum.update(&store[..]);
|
||||
let crc = checksum.finalize();
|
||||
info!("CRC is {:04x}", crc);
|
||||
if crc != info.crc16 {
|
||||
warn!("CRC mismatch in backup data");
|
||||
bail!("CRC mismatch in backup data")
|
||||
}
|
||||
info!("CRC is correct");
|
||||
let decoded = postcard::from_bytes(&store[..])?;
|
||||
info!("Backup data decoded");
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
async fn backup_info(&mut self) -> FatResult<BackupHeader> {
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
self.get_rtc_module().read(0, &mut header_page_buffer)?;
|
||||
info!("Read header page");
|
||||
let info = postcard::take_from_bytes::<BackupHeader>(&header_page_buffer[..]);
|
||||
info!("decoding header: {:?}", info);
|
||||
let (header, _) = info.context("Could not read backup header")?;
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
async fn detect_sensors(&mut self, request: DetectionRequest) -> FatResult<Detection> {
|
||||
self.can_power.set_high();
|
||||
Timer::after_millis(500).await;
|
||||
let config = self.twai_config.take().context("twai config not set")?;
|
||||
let mut twai = config.into_async().start();
|
||||
|
||||
if twai.is_bus_off() {
|
||||
info!("Bus offline after start, attempting recovery");
|
||||
// Re-start to initiate recovery
|
||||
twai = twai.stop().start();
|
||||
}
|
||||
|
||||
let res = (async {
|
||||
Timer::after_millis(1000).await;
|
||||
info!("Sending info messages now");
|
||||
// Send a few test messages per potential sensor node
|
||||
for plant in 0..PLANT_COUNT {
|
||||
for sensor in [Sensor::A, Sensor::B] {
|
||||
let detect = if sensor == Sensor::A {
|
||||
request.plant[plant].sensor_a
|
||||
} else {
|
||||
request.plant[plant].sensor_b
|
||||
};
|
||||
|
||||
if !detect {
|
||||
continue;
|
||||
}
|
||||
let target = StandardId::new(plant_id(
|
||||
IDENTIFY_CMD_OFFSET,
|
||||
sensor.into(),
|
||||
(plant + 1) as u16,
|
||||
))
|
||||
.context(">> Could not create address for sensor! (plant: {}) <<")?;
|
||||
let can_buffer = [0_u8; 0];
|
||||
info!(
|
||||
"Sending test message to plant {} sensor {sensor:?} with id {}",
|
||||
plant + 1,
|
||||
target.as_raw()
|
||||
);
|
||||
if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) {
|
||||
// Try a few times; we intentionally ignore rx here and rely on stub logic
|
||||
let resu = twai
|
||||
.transmit_async(&frame)
|
||||
.with_timeout(Duration::from_millis(500))
|
||||
.await;
|
||||
match resu {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Error sending test message to plant {} sensor {sensor:?}: {err:?}",
|
||||
plant + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Error building CAN frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut moistures = Moistures::default();
|
||||
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
|
||||
.with_timeout(Duration::from_millis(3000))
|
||||
.await;
|
||||
|
||||
let result: Detection = moistures.into();
|
||||
|
||||
info!("Autodetection result: {result:?}");
|
||||
Ok((result, moistures.sensor_a_build_minutes, moistures.sensor_b_build_minutes))
|
||||
})
|
||||
.await;
|
||||
|
||||
let config = twai.stop().into_blocking();
|
||||
self.twai_config.replace(config);
|
||||
self.can_power.set_low();
|
||||
|
||||
match res {
|
||||
Ok((detection, a_builds, b_builds)) => {
|
||||
self.sensor_a_build_minutes = a_builds;
|
||||
self.sensor_b_build_minutes = b_builds;
|
||||
Ok(detection)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sensor_build_minutes(&self) -> ([Option<u32>; PLANT_COUNT], [Option<u32>; PLANT_COUNT]) {
|
||||
(self.sensor_a_build_minutes, self.sensor_b_build_minutes)
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_can_measurements(
|
||||
as_async: &mut Twai<'_, Async>,
|
||||
moistures: &mut Moistures,
|
||||
) -> FatResult<()> {
|
||||
loop {
|
||||
match as_async.receive_async().await {
|
||||
Ok(can_frame) => match can_frame.id() {
|
||||
Id::Standard(id) => {
|
||||
info!("Received CAN message: {id:?}");
|
||||
let rawid = id.as_raw();
|
||||
match classify(rawid) {
|
||||
None => {}
|
||||
Some(msg) => {
|
||||
info!(
|
||||
"received message of kind {:?} (plant: {}, sensor: {:?})",
|
||||
msg.0, msg.1, msg.2
|
||||
);
|
||||
let plant = msg.1 as usize;
|
||||
let sensor = msg.2;
|
||||
let data = can_frame.data();
|
||||
if msg.0 == MessageKind::MoistureData {
|
||||
info!("Received moisture data: {:?}", data);
|
||||
if let Ok(bytes) = data.try_into() {
|
||||
let frequency = u32::from_be_bytes(bytes);
|
||||
match sensor {
|
||||
SensorSlot::A => {
|
||||
moistures.sensor_a_hz[plant - 1] =
|
||||
Some(frequency as f32);
|
||||
}
|
||||
SensorSlot::B => {
|
||||
moistures.sensor_b_hz[plant - 1] =
|
||||
Some(frequency as f32);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Received moisture data with invalid length: {} (expected 4)", data.len());
|
||||
}
|
||||
} else if msg.0 == MessageKind::FirmwareBuild {
|
||||
info!("Received firmware build data: {:?}", data);
|
||||
if let Ok(bytes) = data.try_into() {
|
||||
let build_minutes = u32::from_be_bytes(bytes);
|
||||
match sensor {
|
||||
SensorSlot::A => {
|
||||
moistures.sensor_a_build_minutes[plant - 1] =
|
||||
Some(build_minutes);
|
||||
}
|
||||
SensorSlot::B => {
|
||||
moistures.sensor_b_build_minutes[plant - 1] =
|
||||
Some(build_minutes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Received firmware build data with invalid length: {} (expected 4)", data.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Id::Extended(ext) => {
|
||||
warn!("Received extended ID: {ext:?}");
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
EspTwaiError::BusOff => {
|
||||
bail!("Bus offline")
|
||||
}
|
||||
EspTwaiError::NonCompliantDlc(_) => {}
|
||||
EspTwaiError::EmbeddedHAL(_) => {}
|
||||
}
|
||||
error!("Error receiving CAN message: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Moistures> for Detection {
|
||||
fn from(value: Moistures) -> Self {
|
||||
let mut result = Detection::default();
|
||||
for (plant, sensor) in value.sensor_a_hz.iter().enumerate() {
|
||||
if sensor.is_some() {
|
||||
// Sensor responded; include build timestamp (0 = timestamp not reported)
|
||||
result.plant[plant].sensor_a =
|
||||
Some(value.sensor_a_build_minutes[plant].unwrap_or(0));
|
||||
}
|
||||
}
|
||||
for (plant, sensor) in value.sensor_b_hz.iter().enumerate() {
|
||||
if sensor.is_some() {
|
||||
result.plant[plant].sensor_b =
|
||||
Some(value.sensor_b_build_minutes[plant].unwrap_or(0));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
use crate::bail;
|
||||
use crate::fat_error::{ContextExt, FatResult};
|
||||
use crate::hal::Box;
|
||||
use crate::hal::{DetectionResult, Moistures, Sensor};
|
||||
use crate::log::{LogMessage, LOG_ACCESS};
|
||||
use alloc::format;
|
||||
use alloc::string::ToString;
|
||||
use async_trait::async_trait;
|
||||
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
|
||||
use canapi::SensorSlot;
|
||||
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_time::{Duration, Timer, WithTimeout};
|
||||
use embedded_can::{Frame, Id};
|
||||
use esp_hal::gpio::Output;
|
||||
use esp_hal::i2c::master::I2c;
|
||||
use esp_hal::pcnt::unit::Unit;
|
||||
use esp_hal::twai::{EspTwaiFrame, StandardId, Twai, TwaiConfiguration};
|
||||
use esp_hal::{Async, Blocking};
|
||||
use log::{error, info, warn};
|
||||
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
|
||||
|
||||
const REPEAT_MOIST_MEASURE: usize = 10;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait SensorInteraction {
|
||||
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures>;
|
||||
}
|
||||
|
||||
const MS0: u8 = 1_u8;
|
||||
const MS1: u8 = 0_u8;
|
||||
const MS2: u8 = 3_u8;
|
||||
const MS3: u8 = 4_u8;
|
||||
const MS4: u8 = 2_u8;
|
||||
const SENSOR_ON: u8 = 5_u8;
|
||||
|
||||
pub enum SensorImpl {
|
||||
PulseCounter {
|
||||
signal_counter: Unit<'static, 0>,
|
||||
sensor_expander:
|
||||
Pca9535Immediate<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
|
||||
},
|
||||
CanBus {
|
||||
twai_config: Option<TwaiConfiguration<'static, Blocking>>,
|
||||
can_power: Output<'static>,
|
||||
},
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl SensorInteraction for SensorImpl {
|
||||
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
|
||||
match self {
|
||||
SensorImpl::PulseCounter {
|
||||
signal_counter,
|
||||
sensor_expander,
|
||||
..
|
||||
} => {
|
||||
let mut result = Moistures::default();
|
||||
for plant in 0..crate::hal::PLANT_COUNT {
|
||||
result.sensor_a_hz[plant] =
|
||||
Self::inner_pulse(plant, Sensor::A, signal_counter, sensor_expander)
|
||||
.await?;
|
||||
info!(
|
||||
"Sensor {} {:?}: {}",
|
||||
plant,
|
||||
Sensor::A,
|
||||
result.sensor_a_hz[plant]
|
||||
);
|
||||
result.sensor_b_hz[plant] =
|
||||
Self::inner_pulse(plant, Sensor::B, signal_counter, sensor_expander)
|
||||
.await?;
|
||||
info!(
|
||||
"Sensor {} {:?}: {}",
|
||||
plant,
|
||||
Sensor::B,
|
||||
result.sensor_b_hz[plant]
|
||||
);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
SensorImpl::CanBus {
|
||||
twai_config,
|
||||
can_power,
|
||||
} => {
|
||||
can_power.set_high();
|
||||
let config = twai_config.take().expect("twai config not set");
|
||||
let mut twai = config.into_async().start();
|
||||
|
||||
loop {
|
||||
let rec = twai.receive();
|
||||
match rec {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
info!("Error receiving CAN message: {err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer::after_millis(10).await;
|
||||
|
||||
let mut moistures = Moistures::default();
|
||||
let _ = Self::wait_for_can_measurements(&mut twai, &mut moistures)
|
||||
.with_timeout(Duration::from_millis(5000))
|
||||
.await;
|
||||
|
||||
can_power.set_low();
|
||||
|
||||
let config = twai.stop().into_blocking();
|
||||
twai_config.replace(config);
|
||||
|
||||
Ok(moistures)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SensorImpl {
|
||||
pub async fn autodetect(&mut self) -> FatResult<DetectionResult> {
|
||||
match self {
|
||||
SensorImpl::PulseCounter { .. } => {
|
||||
bail!("Only CAN bus implementation supports autodetection")
|
||||
}
|
||||
SensorImpl::CanBus {
|
||||
twai_config,
|
||||
can_power,
|
||||
} => {
|
||||
// Power on CAN transceiver and start controller
|
||||
can_power.set_high();
|
||||
let config = twai_config.take().expect("twai config not set");
|
||||
let mut as_async = config.into_async().start();
|
||||
// Give CAN some time to stabilize
|
||||
Timer::after_millis(10).await;
|
||||
|
||||
// Send a few test messages per potential sensor node
|
||||
for plant in 0..crate::hal::PLANT_COUNT {
|
||||
for sensor in [Sensor::A, Sensor::B] {
|
||||
let target = StandardId::new(plant_id(
|
||||
IDENTIFY_CMD_OFFSET,
|
||||
sensor.into(),
|
||||
plant as u16,
|
||||
))
|
||||
.context(">> Could not create address for sensor! (plant: {}) <<")?;
|
||||
let can_buffer = [0_u8; 0];
|
||||
if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) {
|
||||
// Try a few times; we intentionally ignore rx here and rely on stub logic
|
||||
let resu = as_async.transmit_async(&frame).await;
|
||||
match resu {
|
||||
Ok(_) => {
|
||||
info!("Sent test message to plant {plant} sensor {sensor:?}");
|
||||
}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Error sending test message to plant {plant} sensor {sensor:?}: {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Error building CAN frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut moistures = Moistures::default();
|
||||
let _ = Self::wait_for_can_measurements(&mut as_async, &mut moistures)
|
||||
.with_timeout(Duration::from_millis(5000))
|
||||
.await;
|
||||
|
||||
let config = as_async.stop().into_blocking();
|
||||
can_power.set_low();
|
||||
twai_config.replace(config);
|
||||
|
||||
let result = moistures.into();
|
||||
|
||||
info!("Autodetection result: {result:?}");
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_can_measurements(
|
||||
as_async: &mut Twai<'_, Async>,
|
||||
moistures: &mut Moistures,
|
||||
) -> FatResult<()> {
|
||||
loop {
|
||||
match as_async.receive_async().await {
|
||||
Ok(can_frame) => match can_frame.id() {
|
||||
Id::Standard(id) => {
|
||||
info!("Received CAN message: {id:?}");
|
||||
let rawid = id.as_raw();
|
||||
match classify(rawid) {
|
||||
None => {}
|
||||
Some(msg) => {
|
||||
info!(
|
||||
"received message of kind {:?} (plant: {}, sensor: {:?})",
|
||||
msg.0, msg.1, msg.2
|
||||
);
|
||||
if msg.0 == MessageKind::MoistureData {
|
||||
let plant = msg.1 as usize;
|
||||
let sensor = msg.2;
|
||||
let data = can_frame.data();
|
||||
|
||||
match sensor {
|
||||
SensorSlot::A => {
|
||||
moistures.sensor_a_hz[plant] = data[0] as f32;
|
||||
}
|
||||
SensorSlot::B => {
|
||||
moistures.sensor_b_hz[plant] = data[0] as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Id::Extended(ext) => {
|
||||
warn!("Received extended ID: {ext:?}");
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error receiving CAN message: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn inner_pulse(
|
||||
plant: usize,
|
||||
sensor: Sensor,
|
||||
signal_counter: &mut Unit<'_, 0>,
|
||||
sensor_expander: &mut Pca9535Immediate<
|
||||
I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>,
|
||||
>,
|
||||
) -> FatResult<f32> {
|
||||
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
|
||||
for sample in results.iter_mut() {
|
||||
signal_counter.pause();
|
||||
signal_counter.clear();
|
||||
|
||||
//Disable all
|
||||
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
|
||||
|
||||
let sensor_channel = match sensor {
|
||||
Sensor::A => plant as u32,
|
||||
Sensor::B => (15 - plant) as u32,
|
||||
};
|
||||
|
||||
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
|
||||
if is_bit_set(0) {
|
||||
sensor_expander.pin_set_high(GPIOBank::Bank0, MS0)?;
|
||||
} else {
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
|
||||
}
|
||||
if is_bit_set(1) {
|
||||
sensor_expander.pin_set_high(GPIOBank::Bank0, MS1)?;
|
||||
} else {
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
|
||||
}
|
||||
if is_bit_set(2) {
|
||||
sensor_expander.pin_set_high(GPIOBank::Bank0, MS2)?;
|
||||
} else {
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
|
||||
}
|
||||
if is_bit_set(3) {
|
||||
sensor_expander.pin_set_high(GPIOBank::Bank0, MS3)?;
|
||||
} else {
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
|
||||
}
|
||||
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?;
|
||||
sensor_expander.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?;
|
||||
|
||||
let measurement = 100; // TODO what is this scaling factor? what is its purpose?
|
||||
let factor = 1000f32 / measurement as f32;
|
||||
|
||||
//give some time to stabilize
|
||||
Timer::after_millis(10).await;
|
||||
signal_counter.resume();
|
||||
Timer::after_millis(measurement).await;
|
||||
signal_counter.pause();
|
||||
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?;
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
|
||||
sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
|
||||
Timer::after_millis(10).await;
|
||||
let unscaled = 1337; //signal_counter.get_counter_value()? as i32;
|
||||
let hz = unscaled as f32 * factor;
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
LogMessage::RawMeasure,
|
||||
unscaled as u32,
|
||||
hz as u32,
|
||||
&plant.to_string(),
|
||||
&format!("{sensor:?}"),
|
||||
)
|
||||
.await;
|
||||
*sample = hz;
|
||||
}
|
||||
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
|
||||
|
||||
let mid = results.len() / 2;
|
||||
let median = results[mid];
|
||||
Ok(median)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Moistures> for DetectionResult {
|
||||
fn from(value: Moistures) -> Self {
|
||||
let mut result = DetectionResult::default();
|
||||
for (plant, sensor) in value.sensor_a_hz.iter().enumerate() {
|
||||
result.plant[plant].sensor_a = *sensor > 1.0_f32;
|
||||
}
|
||||
for (plant, sensor) in value.sensor_b_hz.iter().enumerate() {
|
||||
result.plant[plant].sensor_b = *sensor > 1.0_f32;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,28 @@ use crate::bail;
|
||||
use crate::fat_error::FatError;
|
||||
use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
|
||||
use embassy_time::Timer;
|
||||
use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation};
|
||||
use esp_hal::analog::adc::{Adc, AdcCalLine, AdcConfig, AdcPin, Attenuation};
|
||||
use esp_hal::delay::Delay;
|
||||
use esp_hal::gpio::{Flex, Input, Output, OutputConfig, Pull};
|
||||
use esp_hal::gpio::{DriveMode, Flex, Input, InputConfig, Output, OutputConfig, Pull};
|
||||
use esp_hal::pcnt::channel::CtrlMode::Keep;
|
||||
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
|
||||
use esp_hal::pcnt::unit::Unit;
|
||||
use esp_hal::peripherals::GPIO5;
|
||||
use esp_hal::Blocking;
|
||||
use esp_println::println;
|
||||
use esp_hal::Async;
|
||||
use log::{error, info};
|
||||
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
|
||||
use portable_atomic::{AtomicUsize, Ordering};
|
||||
|
||||
unsafe impl Send for TankSensor<'_> {}
|
||||
|
||||
static FLOW_OVERFLOW_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub struct TankSensor<'a> {
|
||||
one_wire_bus: OneWire<Flex<'a>>,
|
||||
tank_channel: Adc<'a, ADC1<'a>, Blocking>,
|
||||
tank_channel: Adc<'a, ADC1<'a>, Async>,
|
||||
tank_power: Output<'a>,
|
||||
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>>,
|
||||
flow_counter: Unit<'a, 1>,
|
||||
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>, AdcCalLine<ADC1<'a>>>,
|
||||
flow_unit: Unit<'static, 1>,
|
||||
}
|
||||
|
||||
impl<'a> TankSensor<'a> {
|
||||
@@ -28,67 +33,132 @@ impl<'a> TankSensor<'a> {
|
||||
gpio5: GPIO5<'a>,
|
||||
tank_power: Output<'a>,
|
||||
flow_sensor: Input,
|
||||
pcnt1: Unit<'a, 1>,
|
||||
pcnt1: Unit<'static, 1>,
|
||||
) -> Result<TankSensor<'a>, FatError> {
|
||||
one_wire_pin.apply_output_config(&OutputConfig::default().with_pull(Pull::None));
|
||||
one_wire_pin.apply_output_config(
|
||||
&OutputConfig::default()
|
||||
.with_drive_mode(DriveMode::OpenDrain)
|
||||
.with_pull(Pull::None),
|
||||
);
|
||||
one_wire_pin.apply_input_config(&InputConfig::default().with_pull(Pull::None));
|
||||
one_wire_pin.set_high();
|
||||
one_wire_pin.set_input_enable(true);
|
||||
one_wire_pin.set_output_enable(true);
|
||||
info!("tank: one_wire pin config ok");
|
||||
|
||||
let mut adc1_config = AdcConfig::new();
|
||||
let tank_pin = adc1_config.enable_pin(gpio5, Attenuation::_11dB);
|
||||
let tank_channel = Adc::new(adc1, adc1_config);
|
||||
info!("tank: adc config created");
|
||||
let tank_pin =
|
||||
adc1_config.enable_pin_with_cal::<_, AdcCalLine<_>>(gpio5, Attenuation::_11dB);
|
||||
info!("tank: adc pin cal ok");
|
||||
let tank_channel = Adc::new(adc1, adc1_config).into_async();
|
||||
info!("tank: adc channel ok");
|
||||
|
||||
let one_wire_bus = OneWire::new(one_wire_pin, false);
|
||||
info!("tank: one_wire bus ok");
|
||||
|
||||
pcnt1.set_high_limit(Some(i16::MAX))?;
|
||||
info!("tank: pcnt high limit ok");
|
||||
// Reject pulses shorter than ~12.8 µs (1023 APB cycles @ 80 MHz) to suppress EMI noise
|
||||
// on the sensor cable. Real flow pulses are in the millisecond range.
|
||||
match pcnt1.set_filter(Some(1023)) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("tank: failed to set pcnt filter: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let ch0 = &pcnt1.channel0;
|
||||
ch0.set_edge_signal(flow_sensor.peripheral_input());
|
||||
info!("tank: pcnt edge signal ok");
|
||||
ch0.set_input_mode(Hold, Increment);
|
||||
ch0.set_ctrl_mode(Keep, Keep);
|
||||
info!("tank: pcnt input/ctrl mode ok");
|
||||
|
||||
pcnt1.listen();
|
||||
info!("tank: pcnt listen ok");
|
||||
|
||||
Ok(TankSensor {
|
||||
one_wire_bus,
|
||||
tank_channel,
|
||||
tank_power,
|
||||
tank_pin,
|
||||
flow_counter: pcnt1,
|
||||
flow_unit: pcnt1,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset_flow_meter(&mut self) {
|
||||
self.flow_counter.pause();
|
||||
self.flow_counter.clear();
|
||||
// Pause, clear counter, clear any pending interrupt, then reset the overflow counter —
|
||||
// all inside a single critical section to prevent a race where the interrupt fires
|
||||
// between the overflow reset and the pause.
|
||||
critical_section::with(|_| {
|
||||
self.flow_unit.pause();
|
||||
self.flow_unit.clear();
|
||||
self.flow_unit.reset_interrupt();
|
||||
FLOW_OVERFLOW_COUNTER.store(0, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn start_flow_meter(&mut self) {
|
||||
self.flow_counter.resume();
|
||||
}
|
||||
|
||||
pub fn get_flow_meter_value(&mut self) -> i16 {
|
||||
self.flow_counter.value()
|
||||
self.flow_unit.resume();
|
||||
}
|
||||
|
||||
pub fn stop_flow_meter(&mut self) -> i16 {
|
||||
self.flow_counter.pause();
|
||||
self.get_flow_meter_value()
|
||||
critical_section::with(|_| {
|
||||
let val = self.flow_unit.value();
|
||||
self.flow_unit.pause();
|
||||
val
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_full_flow_count(&self) -> u32 {
|
||||
// Read both values inside a single critical section so an overflow interrupt cannot
|
||||
// fire between the two reads and produce an inconsistent result.
|
||||
critical_section::with(|_| {
|
||||
let overflowed = FLOW_OVERFLOW_COUNTER.load(Ordering::SeqCst) as u32;
|
||||
let current = self.flow_unit.value() as u32;
|
||||
overflowed * (i16::MAX as u32 + 1) + current
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn water_temperature_c(&mut self) -> Result<f32, FatError> {
|
||||
//multisample should be moved to water_temperature_c
|
||||
let mut attempt = 1;
|
||||
let mut delay = Delay::new();
|
||||
self.one_wire_bus.reset(&mut delay)?;
|
||||
|
||||
let presence = self.one_wire_bus.reset(&mut delay)?;
|
||||
info!("OneWire: reset presence pulse = {}", presence);
|
||||
if !presence {
|
||||
info!("OneWire: no device responded to reset — check pull-up resistor and wiring");
|
||||
}
|
||||
|
||||
let mut search = DeviceSearch::new();
|
||||
let mut water_temp_sensor: Option<Device> = None;
|
||||
let mut devices_found = 0u8;
|
||||
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
|
||||
devices_found += 1;
|
||||
info!(
|
||||
"OneWire: found device #{} family=0x{:02X} addr={:02X?}",
|
||||
devices_found, device.address[0], device.address
|
||||
);
|
||||
if device.address[0] == ds18b20::FAMILY_CODE {
|
||||
water_temp_sensor = Some(device);
|
||||
break;
|
||||
} else {
|
||||
info!(
|
||||
"OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})",
|
||||
device.address[0],
|
||||
ds18b20::FAMILY_CODE
|
||||
);
|
||||
}
|
||||
}
|
||||
if devices_found == 0 {
|
||||
info!("OneWire: search found zero devices on the bus");
|
||||
}
|
||||
|
||||
match water_temp_sensor {
|
||||
Some(device) => {
|
||||
println!("Found one wire device: {:?}", device);
|
||||
info!("Found one wire device: {:?}", device);
|
||||
let mut water_temp_sensor = DS18B20::new(device)?;
|
||||
|
||||
let water_temp: Result<f32, FatError> = loop {
|
||||
@@ -97,11 +167,11 @@ impl<'a> TankSensor<'a> {
|
||||
.await;
|
||||
match &temp {
|
||||
Ok(res) => {
|
||||
println!("Water temp is {}", res);
|
||||
info!("Water temp is {}", res);
|
||||
break temp;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Could not get water temp {} attempt {}", err, attempt)
|
||||
info!("Could not get water temp {} attempt {}", err, attempt)
|
||||
}
|
||||
}
|
||||
if attempt == 5 {
|
||||
@@ -138,16 +208,26 @@ impl<'a> TankSensor<'a> {
|
||||
|
||||
let mut store = [0_u16; TANK_MULTI_SAMPLE];
|
||||
for sample in store.iter_mut() {
|
||||
let value = self.tank_channel.read_oneshot(&mut self.tank_pin);
|
||||
//force yield
|
||||
*sample = self.tank_channel.read_oneshot(&mut self.tank_pin).await;
|
||||
//force yield between successful samples
|
||||
Timer::after_millis(10).await;
|
||||
*sample = value.unwrap();
|
||||
}
|
||||
self.tank_power.set_low();
|
||||
|
||||
store.sort();
|
||||
//TODO probably wrong? check!
|
||||
let median_mv = store[6] as f32 * 3300_f32 / 4096_f32;
|
||||
Ok(median_mv)
|
||||
let median_mv = store[TANK_MULTI_SAMPLE / 2] as f32;
|
||||
Ok(median_mv / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[esp_hal::handler]
|
||||
pub fn flow_interrupt_handler() {
|
||||
use esp_hal::peripherals::PCNT;
|
||||
let pcnt = PCNT::regs();
|
||||
if pcnt.int_raw().read().cnt_thr_event_u(1).bit() {
|
||||
if pcnt.u_status(1).read().h_lim().bit() {
|
||||
FLOW_OVERFLOW_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
pcnt.int_clr().write(|w| w.cnt_thr_event_u(1).set_bit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
|
||||
use log::{LevelFilter, Log, Metadata, Record};
|
||||
|
||||
const MAX_LIVE_LOG_ENTRIES: usize = 128;
|
||||
|
||||
struct LiveLogBuffer {
|
||||
entries: Vec<(u64, String)>,
|
||||
next_seq: u64,
|
||||
}
|
||||
|
||||
impl LiveLogBuffer {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
next_seq: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, text: String) {
|
||||
if self.entries.len() >= MAX_LIVE_LOG_ENTRIES {
|
||||
self.entries.remove(0);
|
||||
}
|
||||
self.entries.push((self.next_seq, text));
|
||||
self.next_seq += 1;
|
||||
}
|
||||
|
||||
fn get_after(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
|
||||
let next_seq = self.next_seq;
|
||||
match after {
|
||||
None => (self.entries.clone(), false, next_seq),
|
||||
Some(after_seq) => {
|
||||
let result: Vec<_> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(seq, _)| *seq > after_seq)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Dropped if there are entries that should exist (seq > after_seq) but
|
||||
// the oldest retained entry has a higher seq than after_seq + 1.
|
||||
let dropped = if next_seq > after_seq.saturating_add(1) {
|
||||
if let Some((oldest_seq, _)) = self.entries.first() {
|
||||
*oldest_seq > after_seq.saturating_add(1)
|
||||
} else {
|
||||
// Buffer empty but entries were written — all dropped
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
(result, dropped, next_seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InterceptorLogger {
|
||||
live_log: BlockingMutex<CriticalSectionRawMutex, core::cell::RefCell<LiveLogBuffer>>,
|
||||
}
|
||||
|
||||
impl InterceptorLogger {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
live_log: BlockingMutex::new(core::cell::RefCell::new(LiveLogBuffer::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (entries_after, dropped, next_seq).
|
||||
/// Pass `after = None` to retrieve the entire current buffer.
|
||||
/// Pass `after = Some(seq)` to retrieve only entries with seq > that value.
|
||||
pub fn get_live_logs(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
|
||||
self.live_log.lock(|buf| buf.borrow().get_after(after))
|
||||
}
|
||||
|
||||
pub fn init(&'static self) {
|
||||
match log::set_logger(self).map(|()| log::set_max_level(LevelFilter::Info)) {
|
||||
Ok(()) => {}
|
||||
Err(_e) => {
|
||||
esp_println::println!("ERROR: Logger already set");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for InterceptorLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= log::Level::Info
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let message = alloc::format!("{}: {}", record.level(), record.args());
|
||||
|
||||
// Print to serial
|
||||
esp_println::println!("{}", message);
|
||||
|
||||
// Store in live log ring buffer
|
||||
self.live_log.lock(|buf| {
|
||||
buf.borrow_mut().push(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::hal::TIME_ACCESS;
|
||||
use crate::vec;
|
||||
use crate::BOARD_ACCESS;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use bytemuck::{AnyBitPattern, Pod, Zeroable};
|
||||
use deranged::RangedU8;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::channel::Channel;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use esp_hal::Persistable;
|
||||
use log::{info, warn};
|
||||
@@ -27,11 +28,45 @@ static mut LOG_ARRAY: LogArray = LogArray {
|
||||
head: 0,
|
||||
};
|
||||
|
||||
// this is the only reference that is created for LOG_ARRAY and the only way to access it
|
||||
// this is the only reference created for LOG_ARRAY and the only way to access it
|
||||
#[allow(static_mut_refs)]
|
||||
pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> =
|
||||
unsafe { Mutex::new(&mut LOG_ARRAY) };
|
||||
|
||||
mod interceptor;
|
||||
|
||||
pub use interceptor::InterceptorLogger;
|
||||
|
||||
pub static INTERCEPTOR: InterceptorLogger = InterceptorLogger::new();
|
||||
|
||||
pub struct LogRequest {
|
||||
pub message_key: LogMessage,
|
||||
pub number_a: u32,
|
||||
pub number_b: u32,
|
||||
pub txt_short: heapless::String<TXT_SHORT_LENGTH>,
|
||||
pub txt_long: heapless::String<TXT_LONG_LENGTH>,
|
||||
}
|
||||
|
||||
static LOG_CHANNEL: Channel<CriticalSectionRawMutex, LogRequest, 16> = Channel::new();
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn log_task() {
|
||||
loop {
|
||||
let request = LOG_CHANNEL.receive().await;
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
request.message_key,
|
||||
request.number_a,
|
||||
request.number_b,
|
||||
request.txt_short.as_str(),
|
||||
request.txt_long.as_str(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
const TXT_SHORT_LENGTH: usize = 8;
|
||||
const TXT_LONG_LENGTH: usize = 32;
|
||||
|
||||
@@ -80,24 +115,31 @@ impl From<LogEntryInner> for LogEntry {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn log(
|
||||
message_key: LogMessage,
|
||||
number_a: u32,
|
||||
number_b: u32,
|
||||
txt_short: &str,
|
||||
txt_long: &str,
|
||||
) {
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(message_key, number_a, number_b, txt_short, txt_long)
|
||||
.await
|
||||
pub fn log(message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &str, txt_long: &str) {
|
||||
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
|
||||
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
|
||||
|
||||
limit_length(txt_short, &mut txt_short_stack);
|
||||
limit_length(txt_long, &mut txt_long_stack);
|
||||
|
||||
match LOG_CHANNEL.try_send(LogRequest {
|
||||
message_key,
|
||||
number_a,
|
||||
number_b,
|
||||
txt_short: txt_short_stack,
|
||||
txt_long: txt_long_stack,
|
||||
}) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
warn!("Log channel full, dropping log entry");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogArray {
|
||||
pub fn get(&mut self) -> Vec<LogEntry> {
|
||||
let head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
|
||||
RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap());
|
||||
RangedU8::new(self.head).unwrap_or(RangedU8::new_saturating(0));
|
||||
|
||||
let mut rv: Vec<LogEntry> = Vec::new();
|
||||
let mut index = head.wrapping_sub(1);
|
||||
@@ -120,17 +162,11 @@ impl LogArray {
|
||||
txt_long: &str,
|
||||
) {
|
||||
let mut head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
|
||||
RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap());
|
||||
|
||||
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
|
||||
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
|
||||
|
||||
limit_length(txt_short, &mut txt_short_stack);
|
||||
limit_length(txt_long, &mut txt_long_stack);
|
||||
RangedU8::new(self.head).unwrap_or(RangedU8::new_saturating(0));
|
||||
|
||||
let time = {
|
||||
let guard = TIME_ACCESS.get().await.lock().await;
|
||||
guard.current_time_us()
|
||||
let mut guard = BOARD_ACCESS.get().await.lock().await;
|
||||
guard.board_hal.get_esp().rtc.current_time_us()
|
||||
} / 1000;
|
||||
|
||||
let ordinal = message_key.ordinal() as u16;
|
||||
@@ -148,12 +184,8 @@ impl LogArray {
|
||||
to_modify.message_id = ordinal;
|
||||
to_modify.a = number_a;
|
||||
to_modify.b = number_b;
|
||||
to_modify
|
||||
.txt_short
|
||||
.clone_from_slice(txt_short_stack.as_bytes());
|
||||
to_modify
|
||||
.txt_long
|
||||
.clone_from_slice(txt_long_stack.as_bytes());
|
||||
to_modify.txt_short.clone_from_slice(txt_short.as_bytes());
|
||||
to_modify.txt_long.clone_from_slice(txt_long.as_bytes());
|
||||
head = head.wrapping_add(1);
|
||||
self.head = head.get();
|
||||
}
|
||||
@@ -186,7 +218,6 @@ fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<L
|
||||
)
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,6 +311,20 @@ pub enum LogMessage {
|
||||
PumpOpenLoopCurrent,
|
||||
#[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")]
|
||||
PumpMissingSensorCurrent,
|
||||
#[strum(
|
||||
serialize = "Fertilizer applied for ${number_a}s on plant ${number_b} (last application ${txt_short} minutes ago)"
|
||||
)]
|
||||
FertilizerApplied,
|
||||
#[strum(serialize = "MPPT Current sensor could not be reached")]
|
||||
MPPTError,
|
||||
#[strum(
|
||||
serialize = "Trace: a: ${number_a} b: ${number_b} txt_s ${txt_short} long ${txt_long}"
|
||||
)]
|
||||
Trace,
|
||||
#[strum(serialize = "Parsing error reading message")]
|
||||
UnknownMessage,
|
||||
#[strum(serialize = "Going to deep sleep for ${number_a} minutes")]
|
||||
DeepSleep,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -298,9 +343,9 @@ impl From<&LogMessage> for MessageTranslation {
|
||||
}
|
||||
|
||||
impl LogMessage {
|
||||
pub fn to_log_localisation_config() -> Vec<MessageTranslation> {
|
||||
pub fn log_localisation_config() -> Vec<MessageTranslation> {
|
||||
Vec::from_iter((0..LogMessage::len()).map(|i| {
|
||||
let msg_type = LogMessage::from_ordinal(i).unwrap();
|
||||
let msg_type = LogMessage::from_ordinal(i).unwrap_or(LogMessage::UnknownMessage);
|
||||
(&msg_type).into()
|
||||
}))
|
||||
}
|
||||
|
||||
+559
-482
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "mcutie"
|
||||
version = "3.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
homeassistant = []
|
||||
serde = ["dep:serde", "heapless/serde"]
|
||||
defmt = []
|
||||
log = ["dep:log"]
|
||||
|
||||
[dependencies]
|
||||
embassy-net = { version = "0.8.0", default-features = false, features = ["tcp", "dns", "proto-ipv4", "proto-ipv6", "medium-ethernet"] }
|
||||
embassy-sync = { version = "0.8.0", default-features = false }
|
||||
embassy-time = { version = "0.5.1", default-features = false }
|
||||
embassy-futures = { version = "0.1.2", default-features = false }
|
||||
embedded-io = { version = "0.7.1", default-features = false }
|
||||
embedded-io-async = { version = "0.7.0", default-features = false }
|
||||
heapless = { version = "0.7.17", default-features = false }
|
||||
mqttrs = { version = "0.4.1", default-features = false }
|
||||
once_cell = { version = "1.21.3", default-features = false, features = ["critical-section"] }
|
||||
pin-project = { version = "1.1.10", default-features = false }
|
||||
hex = { version = "0.4.3", default-features = false }
|
||||
serde = { version = "1.0.228", default-features = false, features = ["derive"], optional = true }
|
||||
log = { version = "0.4.28", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures-executor = "0.3.31"
|
||||
futures-timer = "3.0.3"
|
||||
futures-util = "0.3.31"
|
||||
@@ -0,0 +1,124 @@
|
||||
use core::{cmp, fmt, ops::Deref};
|
||||
|
||||
use embedded_io::{SliceWriteError, Write};
|
||||
use mqttrs::{encode_slice, Packet};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// A stack allocated buffer that can be written to and then read back from.
|
||||
/// Dereferencing as a [`u8`] slice allows access to previously written data.
|
||||
///
|
||||
/// Can be written to with [`write!`] and supports [`embedded_io::Write`] and
|
||||
/// [`embedded_io_async::Write`].
|
||||
pub struct Buffer<const N: usize> {
|
||||
bytes: [u8; N],
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for Buffer<N> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Buffer<N> {
|
||||
/// Creates a new buffer.
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self {
|
||||
bytes: [0; N],
|
||||
cursor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new buffer and writes the given data into it.
|
||||
pub(crate) fn from(buf: &[u8]) -> Result<Self, Error> {
|
||||
let mut buffer = Self::new();
|
||||
match buffer.write_all(buf) {
|
||||
Ok(()) => Ok(buffer),
|
||||
Err(_) => Err(Error::TooLarge),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode_packet(&mut self, packet: &Packet<'_>) -> Result<(), mqttrs::Error> {
|
||||
let len = encode_slice(packet, &mut self.bytes[self.cursor..])?;
|
||||
self.cursor += len;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// Serializes a value into this buffer using JSON.
|
||||
pub(crate) fn serialize_json<T: serde::Serialize>(
|
||||
&mut self,
|
||||
value: &T,
|
||||
) -> Result<(), serde_json_core::ser::Error> {
|
||||
let len = serde_json_core::to_slice(value, &mut self.bytes[self.cursor..])?;
|
||||
self.cursor += len;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// Deserializes this buffer using JSON into the given type.
|
||||
pub fn deserialize_json<'a, T: serde::Deserialize<'a>>(
|
||||
&'a self,
|
||||
) -> Result<T, serde_json_core::de::Error> {
|
||||
let (result, _) = serde_json_core::from_slice(self)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// The number of bytes available for writing into this buffer.
|
||||
pub fn available(&self) -> usize {
|
||||
N - self.cursor
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Deref for Buffer<N> {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.bytes[0..self.cursor]
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> fmt::Write for Buffer<N> {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.write_all(s.as_bytes()).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> embedded_io::ErrorType for Buffer<N> {
|
||||
type Error = SliceWriteError;
|
||||
}
|
||||
|
||||
impl<const N: usize> embedded_io::Write for Buffer<N> {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
|
||||
if buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let writable = cmp::min(self.available(), buf.len());
|
||||
if writable == 0 {
|
||||
Err(SliceWriteError::Full)
|
||||
} else {
|
||||
self.bytes[self.cursor..self.cursor + writable].copy_from_slice(buf);
|
||||
self.cursor += writable;
|
||||
Ok(writable)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> embedded_io_async::Write for Buffer<N> {
|
||||
async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
|
||||
<Self as embedded_io::Write>::write(self, buf)
|
||||
}
|
||||
|
||||
async fn flush(&mut self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#![macro_use]
|
||||
|
||||
#[cfg(all(feature = "defmt", feature = "log"))]
|
||||
compile_error!("The `defmt` and `log` features cannot both be enabled at the same time.");
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
use core::fmt;
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
pub(crate) use ::defmt::Debug2Format;
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
pub(crate) struct Debug2Format<D: fmt::Debug>(pub(crate) D);
|
||||
|
||||
#[cfg(feature = "log")]
|
||||
impl<D: fmt::Debug> fmt::Debug for Debug2Format<D> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[collapse_debuginfo(yes)]
|
||||
macro_rules! trace {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::trace!($s $(, $x)*);
|
||||
#[cfg(feature = "log")]
|
||||
::log::trace!($s $(, $x)*);
|
||||
#[cfg(not(any(feature="defmt", feature="log")))]
|
||||
let _ = ($( & $x ),*);
|
||||
};
|
||||
}
|
||||
|
||||
#[collapse_debuginfo(yes)]
|
||||
macro_rules! debug {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug!($s $(, $x)*);
|
||||
#[cfg(feature = "log")]
|
||||
::log::debug!($s $(, $x)*);
|
||||
#[cfg(not(any(feature="defmt", feature="log")))]
|
||||
let _ = ($( & $x ),*);
|
||||
};
|
||||
}
|
||||
|
||||
#[collapse_debuginfo(yes)]
|
||||
macro_rules! info {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::info!($s $(, $x)*);
|
||||
#[cfg(feature = "log")]
|
||||
::log::info!($s $(, $x)*);
|
||||
#[cfg(not(any(feature="defmt", feature="log")))]
|
||||
let _ = ($( & $x ),*);
|
||||
};
|
||||
}
|
||||
|
||||
#[collapse_debuginfo(yes)]
|
||||
macro_rules! warn {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::warn!($s $(, $x)*);
|
||||
#[cfg(feature = "log")]
|
||||
::log::warn!($s $(, $x)*);
|
||||
#[cfg(not(any(feature="defmt", feature="log")))]
|
||||
let _ = ($( & $x ),*);
|
||||
};
|
||||
}
|
||||
|
||||
#[collapse_debuginfo(yes)]
|
||||
macro_rules! error {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::error!($s $(, $x)*);
|
||||
#[cfg(feature = "log")]
|
||||
::log::error!($s $(, $x)*);
|
||||
#[cfg(not(any(feature="defmt", feature="log")))]
|
||||
let _ = ($( & $x ),*);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Tools for publishing a [Home Assistant binary sensor](https://www.home-assistant.io/integrations/binary_sensor.mqtt/).
|
||||
use core::ops::Deref;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{homeassistant::Component, Error, Publishable, Topic};
|
||||
|
||||
/// The state of the sensor. Can be easily converted to or from a [`bool`].
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "&str", into = "&'static str")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum BinarySensorState {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
impl From<BinarySensorState> for &'static str {
|
||||
fn from(state: BinarySensorState) -> Self {
|
||||
match state {
|
||||
BinarySensorState::On => "ON",
|
||||
BinarySensorState::Off => "OFF",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for BinarySensorState {
|
||||
fn from(st: &'a str) -> Self {
|
||||
if st == "ON" {
|
||||
Self::On
|
||||
} else {
|
||||
Self::Off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for BinarySensorState {
|
||||
fn from(val: bool) -> Self {
|
||||
if val {
|
||||
BinarySensorState::On
|
||||
} else {
|
||||
BinarySensorState::Off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BinarySensorState> for bool {
|
||||
fn from(val: BinarySensorState) -> Self {
|
||||
match val {
|
||||
BinarySensorState::On => true,
|
||||
BinarySensorState::Off => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for BinarySensorState {
|
||||
fn as_ref(&self) -> &'static [u8] {
|
||||
match self {
|
||||
Self::On => "ON".as_bytes(),
|
||||
Self::Off => "OFF".as_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of sensor.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum BinarySensorClass {
|
||||
Battery,
|
||||
BatteryCharging,
|
||||
CarbonMonoxide,
|
||||
Cold,
|
||||
Connectivity,
|
||||
Door,
|
||||
GarageDoor,
|
||||
Gas,
|
||||
Heat,
|
||||
Light,
|
||||
Lock,
|
||||
Moisture,
|
||||
Motion,
|
||||
Moving,
|
||||
Occupancy,
|
||||
Opening,
|
||||
Plug,
|
||||
Power,
|
||||
Presence,
|
||||
Problem,
|
||||
Running,
|
||||
Safety,
|
||||
Smoke,
|
||||
Sound,
|
||||
Tamper,
|
||||
Update,
|
||||
Vibration,
|
||||
Window,
|
||||
}
|
||||
|
||||
/// A binary sensor that can publish a [`BinarySensorState`] status.
|
||||
#[derive(Serialize)]
|
||||
pub struct BinarySensor {
|
||||
/// The type of sensor
|
||||
pub device_class: Option<BinarySensorClass>,
|
||||
}
|
||||
|
||||
impl Component for BinarySensor {
|
||||
type State = BinarySensorState;
|
||||
|
||||
fn platform() -> &'static str {
|
||||
"binary_sensor"
|
||||
}
|
||||
|
||||
async fn publish_state<T: Deref<Target = str>>(
|
||||
&self,
|
||||
topic: &Topic<T>,
|
||||
state: Self::State,
|
||||
) -> Result<(), Error> {
|
||||
topic.with_bytes(state).publish().await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Tools for publishing a [Home Assistant button](https://www.home-assistant.io/integrations/button.mqtt/).
|
||||
use core::ops::Deref;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{homeassistant::Component, Error, Topic};
|
||||
|
||||
/// The type of button.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ButtonClass {
|
||||
Identify,
|
||||
Restart,
|
||||
Update,
|
||||
}
|
||||
|
||||
/// A button that can be pressed.
|
||||
#[derive(Serialize)]
|
||||
pub struct Button {
|
||||
/// The type of button.
|
||||
pub device_class: Option<ButtonClass>,
|
||||
}
|
||||
|
||||
impl Component for Button {
|
||||
type State = ();
|
||||
|
||||
fn platform() -> &'static str {
|
||||
"button"
|
||||
}
|
||||
|
||||
async fn publish_state<T: Deref<Target = str>>(
|
||||
&self,
|
||||
_topic: &Topic<T>,
|
||||
_state: Self::State,
|
||||
) -> Result<(), Error> {
|
||||
// Buttons don't have a state
|
||||
Err(Error::Invalid)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
//! Tools for publishing a [Home Assistant light](https://www.home-assistant.io/integrations/light.mqtt/).
|
||||
use core::{ops::Deref, str};
|
||||
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
fmt::Debug2Format,
|
||||
homeassistant::{binary_sensor::BinarySensorState, ser::List, Component},
|
||||
Error, Payload, Publishable, Topic,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum SupportedColorMode {
|
||||
OnOff,
|
||||
Brightness,
|
||||
#[serde(rename = "color_temp")]
|
||||
ColorTemp,
|
||||
Hs,
|
||||
Xy,
|
||||
Rgb,
|
||||
Rgbw,
|
||||
Rgbww,
|
||||
White,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct SerializedColor {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
h: Option<f32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
s: Option<f32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
x: Option<f32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
y: Option<f32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
r: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
g: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
b: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
w: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
c: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LedPayload<'a> {
|
||||
state: BinarySensorState,
|
||||
#[serde(default)]
|
||||
brightness: Option<u8>,
|
||||
#[serde(default)]
|
||||
color_temp: Option<u32>,
|
||||
#[serde(default)]
|
||||
color: Option<SerializedColor>,
|
||||
#[serde(default)]
|
||||
effect: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// The color of the light in various forms.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "lowercase", tag = "color_mode", content = "color")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Color {
|
||||
None,
|
||||
Brightness(u8),
|
||||
ColorTemp(u32),
|
||||
Hs {
|
||||
#[serde(rename = "h")]
|
||||
hue: f32,
|
||||
#[serde(rename = "s")]
|
||||
saturation: f32,
|
||||
},
|
||||
Xy {
|
||||
x: f32,
|
||||
y: f32,
|
||||
},
|
||||
Rgb {
|
||||
#[serde(rename = "r")]
|
||||
red: u8,
|
||||
#[serde(rename = "g")]
|
||||
green: u8,
|
||||
#[serde(rename = "b")]
|
||||
blue: u8,
|
||||
},
|
||||
Rgbw {
|
||||
#[serde(rename = "r")]
|
||||
red: u8,
|
||||
#[serde(rename = "g")]
|
||||
green: u8,
|
||||
#[serde(rename = "b")]
|
||||
blue: u8,
|
||||
#[serde(rename = "w")]
|
||||
white: u8,
|
||||
},
|
||||
Rgbww {
|
||||
#[serde(rename = "r")]
|
||||
red: u8,
|
||||
#[serde(rename = "g")]
|
||||
green: u8,
|
||||
#[serde(rename = "b")]
|
||||
blue: u8,
|
||||
#[serde(rename = "c")]
|
||||
cool_white: u8,
|
||||
#[serde(rename = "w")]
|
||||
warm_white: u8,
|
||||
},
|
||||
}
|
||||
|
||||
/// The state of the light. This can be sent to the broker and received as a
|
||||
/// command from Home Assistant.
|
||||
pub struct LightState<'a> {
|
||||
/// Whether the light is on or off.
|
||||
pub state: BinarySensorState,
|
||||
/// The color of the light.
|
||||
pub color: Color,
|
||||
/// Any effect that is applied.
|
||||
pub effect: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> LightState<'a> {
|
||||
/// Parses the state from a command payload.
|
||||
pub fn from_payload(payload: &'a Payload) -> Result<Self, Error> {
|
||||
let parsed: LedPayload<'a> = match payload.deserialize_json() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
warn!("Failed to deserialize packet: {:?}", Debug2Format(&e));
|
||||
if let Ok(s) = str::from_utf8(payload) {
|
||||
trace!("{}", s);
|
||||
}
|
||||
return Err(Error::PacketError);
|
||||
}
|
||||
};
|
||||
|
||||
let color = if let Some(color) = parsed.color {
|
||||
if let Some(x) = color.x {
|
||||
Color::Xy {
|
||||
x,
|
||||
y: color.y.unwrap_or_default(),
|
||||
}
|
||||
} else if let Some(h) = color.h {
|
||||
Color::Hs {
|
||||
hue: h,
|
||||
saturation: color.s.unwrap_or_default(),
|
||||
}
|
||||
} else if let Some(c) = color.c {
|
||||
Color::Rgbww {
|
||||
red: color.r.unwrap_or_default(),
|
||||
green: color.g.unwrap_or_default(),
|
||||
blue: color.b.unwrap_or_default(),
|
||||
cool_white: c,
|
||||
warm_white: color.w.unwrap_or_default(),
|
||||
}
|
||||
} else if let Some(w) = color.w {
|
||||
Color::Rgbw {
|
||||
red: color.r.unwrap_or_default(),
|
||||
green: color.g.unwrap_or_default(),
|
||||
blue: color.b.unwrap_or_default(),
|
||||
white: w,
|
||||
}
|
||||
} else {
|
||||
Color::Rgb {
|
||||
red: color.r.unwrap_or_default(),
|
||||
green: color.g.unwrap_or_default(),
|
||||
blue: color.b.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
} else if let Some(color_temp) = parsed.color_temp {
|
||||
Color::ColorTemp(color_temp)
|
||||
} else if let Some(brightness) = parsed.brightness {
|
||||
Color::Brightness(brightness)
|
||||
} else {
|
||||
Color::None
|
||||
};
|
||||
|
||||
Ok(LightState {
|
||||
state: parsed.state,
|
||||
color,
|
||||
effect: parsed.effect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for LightState<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut len = 1;
|
||||
|
||||
if self.effect.is_some() {
|
||||
len += 1;
|
||||
}
|
||||
|
||||
match self.color {
|
||||
Color::None => {}
|
||||
Color::Brightness(_) | Color::ColorTemp(_) => len += 1,
|
||||
_ => len += 2,
|
||||
}
|
||||
|
||||
let mut serializer = serializer.serialize_struct("LightState", len)?;
|
||||
|
||||
serializer.serialize_field("state", &self.state)?;
|
||||
|
||||
if let Some(effect) = self.effect {
|
||||
serializer.serialize_field("effect", effect)?;
|
||||
} else {
|
||||
serializer.skip_field("effect")?;
|
||||
}
|
||||
|
||||
match self.color {
|
||||
Color::None => {
|
||||
serializer.skip_field("brightness")?;
|
||||
serializer.skip_field("color_temp")?;
|
||||
serializer.skip_field("color")?;
|
||||
}
|
||||
Color::Brightness(b) => {
|
||||
serializer.skip_field("color_temp")?;
|
||||
serializer.skip_field("color")?;
|
||||
|
||||
serializer.serialize_field("brightness", &b)?
|
||||
}
|
||||
Color::ColorTemp(c) => {
|
||||
serializer.skip_field("brightness")?;
|
||||
serializer.skip_field("color")?;
|
||||
|
||||
serializer.serialize_field("color_temp", &c)?
|
||||
}
|
||||
Color::Hs { hue, saturation } => {
|
||||
serializer.skip_field("brightness")?;
|
||||
serializer.skip_field("color_temp")?;
|
||||
|
||||
serializer.serialize_field("color_mode", "hs")?;
|
||||
|
||||
let color = SerializedColor {
|
||||
h: Some(hue),
|
||||
s: Some(saturation),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
serializer.serialize_field("color", &color)?
|
||||
}
|
||||
Color::Xy { x, y } => {
|
||||
serializer.skip_field("brightness")?;
|
||||
serializer.skip_field("color_temp")?;
|
||||
|
||||
serializer.serialize_field("color_mode", "xy")?;
|
||||
|
||||
let color = SerializedColor {
|
||||
x: Some(x),
|
||||
y: Some(y),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
serializer.serialize_field("color", &color)?
|
||||
}
|
||||
Color::Rgb { red, green, blue } => {
|
||||
serializer.skip_field("brightness")?;
|
||||
serializer.skip_field("color_temp")?;
|
||||
|
||||
serializer.serialize_field("color_mode", "rgb")?;
|
||||
|
||||
let color = SerializedColor {
|
||||
r: Some(red),
|
||||
g: Some(green),
|
||||
b: Some(blue),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
serializer.serialize_field("color", &color)?
|
||||
}
|
||||
Color::Rgbw {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
white,
|
||||
} => {
|
||||
serializer.skip_field("brightness")?;
|
||||
serializer.skip_field("color_temp")?;
|
||||
|
||||
serializer.serialize_field("color_mode", "rgbw")?;
|
||||
|
||||
let color = SerializedColor {
|
||||
r: Some(red),
|
||||
g: Some(green),
|
||||
b: Some(blue),
|
||||
w: Some(white),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
serializer.serialize_field("color", &color)?
|
||||
}
|
||||
Color::Rgbww {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
cool_white,
|
||||
warm_white,
|
||||
} => {
|
||||
serializer.skip_field("brightness")?;
|
||||
serializer.skip_field("color_temp")?;
|
||||
|
||||
serializer.serialize_field("color_mode", "rgbww")?;
|
||||
|
||||
let color = SerializedColor {
|
||||
r: Some(red),
|
||||
g: Some(green),
|
||||
b: Some(blue),
|
||||
c: Some(cool_white),
|
||||
w: Some(warm_white),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
serializer.serialize_field("color", &color)?
|
||||
}
|
||||
}
|
||||
|
||||
serializer.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// A light entity
|
||||
pub struct Light<'a, const C: usize, const E: usize> {
|
||||
/// The color modes supported by the light.
|
||||
pub supported_color_modes: [SupportedColorMode; C],
|
||||
/// Any effects that can be used.
|
||||
pub effects: [&'a str; E],
|
||||
}
|
||||
|
||||
impl<const C: usize, const E: usize> Serialize for Light<'_, C, E> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut len = 2;
|
||||
|
||||
if C > 0 {
|
||||
len += 1;
|
||||
}
|
||||
|
||||
if E > 0 {
|
||||
len += 2;
|
||||
}
|
||||
|
||||
let mut serializer = serializer.serialize_struct("Light", len)?;
|
||||
|
||||
serializer.serialize_field("schema", "json")?;
|
||||
|
||||
if C > 0 {
|
||||
serializer.serialize_field("sup_clrm", &List::new(&self.supported_color_modes))?;
|
||||
} else {
|
||||
serializer.skip_field("sup_clrm")?;
|
||||
}
|
||||
|
||||
if E > 0 {
|
||||
serializer.serialize_field("effect", &true)?;
|
||||
serializer.serialize_field("fx_list", &List::new(&self.effects))?;
|
||||
} else {
|
||||
serializer.skip_field("effect")?;
|
||||
serializer.skip_field("fx_list")?;
|
||||
}
|
||||
|
||||
serializer.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const C: usize, const E: usize> Component for Light<'_, C, E> {
|
||||
type State = LightState<'static>;
|
||||
|
||||
fn platform() -> &'static str {
|
||||
"light"
|
||||
}
|
||||
|
||||
async fn publish_state<T: Deref<Target = str>>(
|
||||
&self,
|
||||
topic: &Topic<T>,
|
||||
state: Self::State,
|
||||
) -> Result<(), Error> {
|
||||
topic.with_json(state).publish().await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
//! Home Assistant auto-discovery and related messages.
|
||||
//!
|
||||
//! Normally you would declare your entities statically in your binary. It is
|
||||
//! then trivial to send out discovery messages or state changes.
|
||||
//!
|
||||
//! ```
|
||||
//! # use mcutie::{Publishable, Topic};
|
||||
//! # use mcutie::homeassistant::{Entity, Device, Origin, AvailabilityState, AvailabilityTopics};
|
||||
//! # use mcutie::homeassistant::binary_sensor::{BinarySensor, BinarySensorClass, BinarySensorState};
|
||||
//! const DEVICE_AVAILABILITY_TOPIC: Topic<&'static str> = Topic::Device("status");
|
||||
//! const MOTION_STATE_TOPIC: Topic<&'static str> = Topic::Device("motion/status");
|
||||
//!
|
||||
//! const DEVICE: Device<'static> = Device::new();
|
||||
//! const ORIGIN: Origin<'static> = Origin::new();
|
||||
//!
|
||||
//! const MOTION_SENSOR: Entity<'static, 1, BinarySensor> = Entity {
|
||||
//! device: DEVICE,
|
||||
//! origin: ORIGIN,
|
||||
//! object_id: "motion",
|
||||
//! unique_id: Some("motion"),
|
||||
//! name: "Motion",
|
||||
//! availability: AvailabilityTopics::All([DEVICE_AVAILABILITY_TOPIC]),
|
||||
//! state_topic: Some(MOTION_STATE_TOPIC),
|
||||
//! command_topic: None,
|
||||
//! component: BinarySensor {
|
||||
//! device_class: Some(BinarySensorClass::Motion),
|
||||
//! },
|
||||
//! };
|
||||
//!
|
||||
//! async fn send_discovery_messages() {
|
||||
//! MOTION_SENSOR.publish_discovery().await.unwrap();
|
||||
//! DEVICE_AVAILABILITY_TOPIC.with_bytes(AvailabilityState::Online).publish().await.unwrap();
|
||||
//! }
|
||||
//!
|
||||
//! async fn send_state(state: BinarySensorState) {
|
||||
//! MOTION_SENSOR.publish_state(state).await.unwrap();
|
||||
//! }
|
||||
//! ```
|
||||
use core::{future::Future, ops::Deref};
|
||||
|
||||
use mqttrs::QoS;
|
||||
use serde::{
|
||||
ser::{Error as _, SerializeStruct},
|
||||
Serialize, Serializer,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
device_id, device_type, homeassistant::ser::DiscoverySerializer, io::publish, Error,
|
||||
McutieTask, MqttMessage, Payload, Publishable, Topic, TopicString, DATA_CHANNEL,
|
||||
};
|
||||
|
||||
pub mod binary_sensor;
|
||||
pub mod button;
|
||||
pub mod light;
|
||||
pub mod sensor;
|
||||
mod ser;
|
||||
|
||||
const HA_STATUS_TOPIC: Topic<&'static str> = Topic::General("homeassistant/status");
|
||||
const STATE_ONLINE: &str = "online";
|
||||
const STATE_OFFLINE: &str = "offline";
|
||||
|
||||
/// A trait representing a specific type of entity in Home Assistant
|
||||
pub trait Component: Serialize {
|
||||
/// The state to publish.
|
||||
type State;
|
||||
|
||||
/// The platform identifier for this entity. Internal.
|
||||
fn platform() -> &'static str;
|
||||
|
||||
/// Publishes this entity's state to the MQTT broker.
|
||||
fn publish_state<T: Deref<Target = str>>(
|
||||
&self,
|
||||
topic: &Topic<T>,
|
||||
state: Self::State,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
}
|
||||
|
||||
impl<'t, T, L, const S: usize> McutieTask<'t, T, L, S>
|
||||
where
|
||||
T: Deref<Target = str> + 't,
|
||||
L: Publishable + 't,
|
||||
{
|
||||
pub(super) async fn ha_after_connected(&self) {
|
||||
let _ = HA_STATUS_TOPIC.subscribe(false).await;
|
||||
}
|
||||
|
||||
pub(super) async fn ha_handle_update(
|
||||
&self,
|
||||
topic: &Topic<TopicString>,
|
||||
payload: &Payload,
|
||||
) -> bool {
|
||||
if topic == &HA_STATUS_TOPIC {
|
||||
if payload.as_ref() == STATE_ONLINE.as_bytes() {
|
||||
DATA_CHANNEL.send(MqttMessage::HomeAssistantOnline).await;
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deref<Target = str>> Serialize for Topic<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut topic = TopicString::new();
|
||||
self.to_string(&mut topic)
|
||||
.map_err(|_| S::Error::custom("topic was too large to serialize"))?;
|
||||
serializer.serialize_str(&topic)
|
||||
}
|
||||
}
|
||||
|
||||
fn name_or_device<S>(name: &Option<&str>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(name.unwrap_or_else(|| device_type()))
|
||||
}
|
||||
|
||||
/// Represents the device in Home Assistant.
|
||||
///
|
||||
/// Can just be the default in which case useful properties such as the ID are
|
||||
/// automatically included.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Device<'a> {
|
||||
/// A name to identify the device. If not provided the default device type is
|
||||
/// used.
|
||||
pub name: Option<&'a str>,
|
||||
/// An optional configuration URL for the device.
|
||||
pub configuration_url: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Device<'_> {
|
||||
/// Creates a new default device.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
configuration_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Device<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut len = 2;
|
||||
if self.configuration_url.is_some() {
|
||||
len += 1;
|
||||
}
|
||||
|
||||
let mut serializer = serializer.serialize_struct("Device", len)?;
|
||||
|
||||
serializer.serialize_field("name", self.name.unwrap_or_else(|| device_type()))?;
|
||||
serializer.serialize_field("ids", device_id())?;
|
||||
|
||||
if let Some(cu) = self.configuration_url {
|
||||
serializer.serialize_field("cu", cu)?;
|
||||
} else {
|
||||
serializer.skip_field("cu")?;
|
||||
}
|
||||
|
||||
serializer.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the device's origin in Home Assistant.
|
||||
///
|
||||
/// Can just be the default in which case useful properties are automatically
|
||||
/// included.
|
||||
#[derive(Clone, Copy, Default, Serialize)]
|
||||
pub struct Origin<'a> {
|
||||
/// A name to identify the device's origin. If not provided the default
|
||||
/// device type is used.
|
||||
#[serde(serialize_with = "name_or_device")]
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Origin<'_> {
|
||||
/// Creates a new default origin.
|
||||
pub const fn new() -> Self {
|
||||
Self { name: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// A single entity for Home Assistant.
|
||||
///
|
||||
/// Calling [`Entity::publish_discovery`] will publish the discovery message to
|
||||
/// allow Home Assistant to detect this entity. Read the
|
||||
/// [Home Assistant MQTT docs](https://www.home-assistant.io/integrations/mqtt/)
|
||||
/// for information on what some of these properties mean.
|
||||
pub struct Entity<'a, const A: usize, C: Component> {
|
||||
/// The device this entity is a part of.
|
||||
pub device: Device<'a>,
|
||||
/// The origin of the device.
|
||||
pub origin: Origin<'a>,
|
||||
/// An object identifier to allow for entity ID customisation in Home Assistant.
|
||||
pub object_id: &'a str,
|
||||
/// An optional unique identifier for the entity.
|
||||
pub unique_id: Option<&'a str>,
|
||||
/// A friendly name for the entity.
|
||||
pub name: &'a str,
|
||||
/// Specifies the availability topics that Home Assistant will listen to to
|
||||
/// determine this entity's availability.
|
||||
pub availability: AvailabilityTopics<'a, A>,
|
||||
/// The state topic that this entity's state is published to.
|
||||
pub state_topic: Option<Topic<&'a str>>,
|
||||
/// The command topic that this entity receives commands from.
|
||||
pub command_topic: Option<Topic<&'a str>>,
|
||||
/// The specific entity.
|
||||
pub component: C,
|
||||
}
|
||||
|
||||
impl<const A: usize, C: Component> Entity<'_, A, C> {
|
||||
/// Publishes the discovery message for this entity to the broker.
|
||||
pub async fn publish_discovery(&self) -> Result<(), Error> {
|
||||
let mut topic = TopicString::new();
|
||||
topic
|
||||
.push_str(option_env!("HA_DISCOVERY_PREFIX").unwrap_or("homeassistant"))
|
||||
.map_err(|_| Error::TooLarge)?;
|
||||
topic.push('/').map_err(|_| Error::TooLarge)?;
|
||||
topic.push_str(C::platform()).map_err(|_| Error::TooLarge)?;
|
||||
topic.push('/').map_err(|_| Error::TooLarge)?;
|
||||
topic
|
||||
.push_str(self.object_id)
|
||||
.map_err(|_| Error::TooLarge)?;
|
||||
topic.push_str("/config").map_err(|_| Error::TooLarge)?;
|
||||
|
||||
let mut payload = Payload::new();
|
||||
payload.serialize_json(self).map_err(|_| Error::TooLarge)?;
|
||||
|
||||
publish(&topic, &payload, QoS::AtMostOnce, false).await
|
||||
}
|
||||
|
||||
/// Publishes this entity's state to the broker.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`Error::Invalid`] if the entity doesn't have a state topic.
|
||||
pub async fn publish_state(&self, state: C::State) -> Result<(), Error> {
|
||||
if let Some(topic) = self.state_topic {
|
||||
self.component.publish_state(&topic, state).await
|
||||
} else {
|
||||
Err(Error::Invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A payload representing a device or entity's availability.
|
||||
#[allow(missing_docs)]
|
||||
pub enum AvailabilityState {
|
||||
Online,
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for AvailabilityState {
|
||||
fn as_ref(&self) -> &'static [u8] {
|
||||
match self {
|
||||
Self::Online => STATE_ONLINE.as_bytes(),
|
||||
Self::Offline => STATE_OFFLINE.as_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The availiabity topics that home assistant will use to determine an entity's
|
||||
/// availability.
|
||||
pub enum AvailabilityTopics<'a, const A: usize> {
|
||||
/// The entity is always available.
|
||||
None,
|
||||
/// The entity is available if all of the topics are publishes as online.
|
||||
All([Topic<&'a str>; A]),
|
||||
/// The entity is available if any of the topics are publishes as online.
|
||||
Any([Topic<&'a str>; A]),
|
||||
/// The entity is available based on the most recent of the topics to
|
||||
/// publish state.
|
||||
Latest([Topic<&'a str>; A]),
|
||||
}
|
||||
|
||||
impl<const A: usize, C: Component> Serialize for Entity<'_, A, C> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let outer = DiscoverySerializer {
|
||||
discovery: self,
|
||||
inner: serializer,
|
||||
};
|
||||
|
||||
self.component.serialize(outer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Tools for publishing a [Home Assistant sensor](https://www.home-assistant.io/integrations/sensor.mqtt/).
|
||||
use core::ops::Deref;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{homeassistant::Component, Error, Publishable, Topic};
|
||||
|
||||
/// The type of sensor.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum SensorClass {
|
||||
ApparentPower,
|
||||
Aqi,
|
||||
AtmosphericPressure,
|
||||
Battery,
|
||||
CarbonDioxide,
|
||||
CarbonMonoxide,
|
||||
Current,
|
||||
DataRate,
|
||||
DataSize,
|
||||
Date,
|
||||
Distance,
|
||||
Duration,
|
||||
Energy,
|
||||
EnergyStorage,
|
||||
Enum,
|
||||
Frequency,
|
||||
Gas,
|
||||
Humidity,
|
||||
Illuminance,
|
||||
Irradiance,
|
||||
Moisture,
|
||||
Monetary,
|
||||
NitrogenDioxide,
|
||||
NitrogenMonoxide,
|
||||
NitrousOxide,
|
||||
Ozone,
|
||||
Ph,
|
||||
Pm1,
|
||||
Pm25,
|
||||
Pm10,
|
||||
PowerFactor,
|
||||
Power,
|
||||
Precipitation,
|
||||
PrecipitationIntensity,
|
||||
Pressure,
|
||||
ReactivePower,
|
||||
SignalStrength,
|
||||
SoundPressure,
|
||||
Speed,
|
||||
SulphurDioxide,
|
||||
Temperature,
|
||||
Timestamp,
|
||||
VolatileOrganicCompounds,
|
||||
VolatileOrganicCompoundsParts,
|
||||
Voltage,
|
||||
Volume,
|
||||
VolumeFlowRate,
|
||||
VolumeStorage,
|
||||
Water,
|
||||
Weight,
|
||||
WindSpeed,
|
||||
}
|
||||
|
||||
/// The type of measurement that this entity publishes.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SensorStateClass {
|
||||
/// A measurement at a singe point in time.
|
||||
Measurement,
|
||||
/// A cumulative total that can increase or decrease over time.
|
||||
Total,
|
||||
/// A cumulative total that can only increase.
|
||||
TotalIncreasing,
|
||||
}
|
||||
|
||||
/// A binary sensor that can publish a [`f32`] value.
|
||||
#[derive(Serialize)]
|
||||
pub struct Sensor<'u> {
|
||||
/// The type of sensor.
|
||||
pub device_class: Option<SensorClass>,
|
||||
/// The type of measurement that this sensor reports.
|
||||
pub state_class: Option<SensorStateClass>,
|
||||
/// The unit of measurement for this sensor.
|
||||
pub unit_of_measurement: Option<&'u str>,
|
||||
}
|
||||
|
||||
impl Component for Sensor<'_> {
|
||||
type State = f32;
|
||||
|
||||
fn platform() -> &'static str {
|
||||
"sensor"
|
||||
}
|
||||
|
||||
async fn publish_state<T: Deref<Target = str>>(
|
||||
&self,
|
||||
topic: &Topic<T>,
|
||||
state: Self::State,
|
||||
) -> Result<(), Error> {
|
||||
topic.with_display(state).publish().await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
use core::ops::Deref;
|
||||
|
||||
use serde::{
|
||||
ser::{SerializeSeq, SerializeStruct},
|
||||
Serialize, Serializer,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
homeassistant::{AvailabilityTopics, Component, Entity},
|
||||
Topic,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct AvailabilityTopicItem<'a> {
|
||||
topic: Topic<&'a str>,
|
||||
}
|
||||
|
||||
struct AvailabilityTopicList<'a, T: Deref<Target = str>, const N: usize> {
|
||||
list: &'a [Topic<T>; N],
|
||||
}
|
||||
|
||||
impl<'a, const N: usize, T: Deref<Target = str>> AvailabilityTopicList<'a, T, N> {
|
||||
pub(super) fn new(list: &'a [Topic<T>; N]) -> Self {
|
||||
Self { list }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deref<Target = str>, const N: usize> Serialize for AvailabilityTopicList<'_, T, N> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut serializer = serializer.serialize_seq(Some(N))?;
|
||||
|
||||
for topic in self.list {
|
||||
serializer.serialize_element(&AvailabilityTopicItem {
|
||||
topic: topic.as_ref(),
|
||||
})?;
|
||||
}
|
||||
|
||||
serializer.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct List<'a, T: Serialize, const N: usize> {
|
||||
list: &'a [T; N],
|
||||
}
|
||||
|
||||
impl<'a, T: Serialize, const N: usize> List<'a, T, N> {
|
||||
pub(super) fn new(list: &'a [T; N]) -> Self {
|
||||
Self { list }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize, const N: usize> Serialize for List<'_, T, N> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut serializer = serializer.serialize_seq(Some(N))?;
|
||||
|
||||
for item in self.list {
|
||||
serializer.serialize_element(item)?;
|
||||
}
|
||||
|
||||
serializer.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct DiscoverySerializer<'a, const A: usize, C: Component, S: Serializer> {
|
||||
pub(super) discovery: &'a Entity<'a, A, C>,
|
||||
pub(super) inner: S,
|
||||
}
|
||||
|
||||
impl<const A: usize, C: Component, S: Serializer> Serializer for DiscoverySerializer<'_, A, C, S> {
|
||||
type Ok = S::Ok;
|
||||
type Error = S::Error;
|
||||
type SerializeSeq = S::SerializeSeq;
|
||||
type SerializeTuple = S::SerializeTuple;
|
||||
type SerializeTupleStruct = S::SerializeTupleStruct;
|
||||
type SerializeTupleVariant = S::SerializeTupleVariant;
|
||||
type SerializeMap = S::SerializeMap;
|
||||
type SerializeStruct = S::SerializeStruct;
|
||||
type SerializeStructVariant = S::SerializeStructVariant;
|
||||
|
||||
fn serialize_struct(
|
||||
self,
|
||||
name: &'static str,
|
||||
mut len: usize,
|
||||
) -> Result<Self::SerializeStruct, Self::Error> {
|
||||
len += 5;
|
||||
if self.discovery.state_topic.is_some() {
|
||||
len += 1;
|
||||
}
|
||||
if self.discovery.command_topic.is_some() {
|
||||
len += 1;
|
||||
}
|
||||
if self.discovery.unique_id.is_some() {
|
||||
len += 1;
|
||||
}
|
||||
if !matches!(self.discovery.availability, AvailabilityTopics::None) {
|
||||
len += 2;
|
||||
}
|
||||
|
||||
let mut serializer = self.inner.serialize_struct(name, len)?;
|
||||
|
||||
serializer.serialize_field("dev", &self.discovery.device)?;
|
||||
serializer.serialize_field("o", &self.discovery.origin)?;
|
||||
serializer.serialize_field("p", C::platform())?;
|
||||
serializer.serialize_field("obj_id", self.discovery.object_id)?;
|
||||
|
||||
serializer.serialize_field("name", self.discovery.name)?;
|
||||
|
||||
if let Some(t) = self.discovery.state_topic {
|
||||
serializer.serialize_field("stat_t", &t)?;
|
||||
} else {
|
||||
serializer.skip_field("stat_t")?;
|
||||
}
|
||||
|
||||
if let Some(t) = self.discovery.command_topic {
|
||||
serializer.serialize_field("cmd_t", &t)?;
|
||||
} else {
|
||||
serializer.skip_field("cmd_t")?;
|
||||
}
|
||||
|
||||
match &self.discovery.availability {
|
||||
AvailabilityTopics::None => {
|
||||
serializer.skip_field("avty")?;
|
||||
serializer.skip_field("avty_mode")?;
|
||||
}
|
||||
AvailabilityTopics::All(topics) => {
|
||||
serializer.serialize_field("avty_mode", "all")?;
|
||||
serializer.serialize_field("avty", &AvailabilityTopicList::new(topics))?;
|
||||
}
|
||||
AvailabilityTopics::Any(topics) => {
|
||||
serializer.serialize_field("avty_mode", "any")?;
|
||||
serializer.serialize_field("avty", &AvailabilityTopicList::new(topics))?;
|
||||
}
|
||||
AvailabilityTopics::Latest(topics) => {
|
||||
serializer.serialize_field("avty_mode", "latest")?;
|
||||
serializer.serialize_field("avty", &AvailabilityTopicList::new(topics))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(v) = self.discovery.unique_id {
|
||||
serializer.serialize_field("uniq_id", v)?;
|
||||
} else {
|
||||
serializer.skip_field("uniq_id")?;
|
||||
}
|
||||
|
||||
Ok(serializer)
|
||||
}
|
||||
|
||||
fn serialize_bool(self, _: bool) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_i8(self, _: i8) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_i16(self, _: i16) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_i32(self, _: i32) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_i64(self, _: i64) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_u8(self, _: u8) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_u16(self, _: u16) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_u32(self, _: u32) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_u64(self, _: u64) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_f32(self, _: f32) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_f64(self, _: f64) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_char(self, _: char) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_str(self, _: &str) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_bytes(self, _: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_some<T>(self, _: &T) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_unit_struct(self, _: &'static str) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_newtype_struct<T>(self, _: &'static str, _: &T) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_newtype_variant<T>(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: &T,
|
||||
) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_seq(self, _: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_tuple(self, _: usize) -> Result<Self::SerializeTuple, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_tuple_struct(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Result<Self::SerializeTupleStruct, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Result<Self::SerializeTupleVariant, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_map(self, _: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Result<Self::SerializeStructVariant, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_i128(self, _: i128) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_u128(self, _: u128) -> Result<Self::Ok, Self::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn collect_seq<I>(self, _: I) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
I: IntoIterator,
|
||||
<I as IntoIterator>::Item: Serialize,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn collect_map<K, V, I>(self, _: I) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
K: Serialize,
|
||||
V: Serialize,
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn collect_str<T>(self, _: &T) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: ?Sized + core::fmt::Display,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_human_readable(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
use core::ops::Deref;
|
||||
|
||||
pub(crate) use atomic16::assign_pid;
|
||||
use embassy_futures::select::{select, select4, Either};
|
||||
use embassy_net::{
|
||||
dns::DnsQueryType,
|
||||
tcp::{TcpReader, TcpSocket, TcpWriter},
|
||||
Stack,
|
||||
};
|
||||
use embassy_sync::{
|
||||
blocking_mutex::raw::CriticalSectionRawMutex,
|
||||
pubsub::{PubSubChannel, Subscriber, WaitResult},
|
||||
};
|
||||
use embassy_time::Timer;
|
||||
use embedded_io_async::Write;
|
||||
use mqttrs::{
|
||||
decode_slice, Connect, ConnectReturnCode, LastWill, Packet, Pid, Protocol, Publish, QoS, QosPid,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
device_id, fmt::Debug2Format, pipe::ConnectedPipe, ControlMessage, Error, MqttMessage, Payload,
|
||||
Publishable, Topic, TopicString, CONFIRMATION_TIMEOUT, DATA_CHANNEL, DEFAULT_BACKOFF,
|
||||
RESET_BACKOFF,
|
||||
};
|
||||
|
||||
static SEND_QUEUE: ConnectedPipe<CriticalSectionRawMutex, Payload, 10> = ConnectedPipe::new();
|
||||
|
||||
pub(crate) static CONTROL_CHANNEL: PubSubChannel<CriticalSectionRawMutex, ControlMessage, 2, 5, 0> =
|
||||
PubSubChannel::new();
|
||||
|
||||
type ControlSubscriber = Subscriber<'static, CriticalSectionRawMutex, ControlMessage, 2, 5, 0>;
|
||||
|
||||
pub(crate) async fn subscribe() -> ControlSubscriber {
|
||||
loop {
|
||||
if let Ok(sub) = CONTROL_CHANNEL.subscriber() {
|
||||
return sub;
|
||||
}
|
||||
|
||||
Timer::after_millis(50).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_has_atomic = "16")]
|
||||
mod atomic16 {
|
||||
use core::sync::atomic::{AtomicU16, Ordering};
|
||||
|
||||
use mqttrs::Pid;
|
||||
|
||||
static PID: AtomicU16 = AtomicU16::new(0);
|
||||
|
||||
pub(crate) async fn assign_pid() -> Pid {
|
||||
Pid::new() + PID.fetch_add(1, Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_has_atomic = "16"))]
|
||||
mod atomic16 {
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||
use mqttrs::Pid;
|
||||
|
||||
static PID_MUTEX: Mutex<CriticalSectionRawMutex, u16> = Mutex::new(0);
|
||||
|
||||
pub(crate) async fn assign_pid() -> Pid {
|
||||
let mut locked = PID_MUTEX.lock().await;
|
||||
*locked += 1;
|
||||
|
||||
Pid::new() + *locked
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn send_packet(packet: Packet<'_>) -> Result<(), Error> {
|
||||
let mut buffer = Payload::new();
|
||||
|
||||
match buffer.encode_packet(&packet) {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"Sending packet to broker: {:?}",
|
||||
Debug2Format(&packet.get_type())
|
||||
);
|
||||
SEND_QUEUE.push(buffer).await;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Failed to send packet");
|
||||
Err(Error::PacketError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_publish(
|
||||
mut subscriber: ControlSubscriber,
|
||||
expected_pid: Pid,
|
||||
) -> Result<(), Error> {
|
||||
match select(
|
||||
async {
|
||||
loop {
|
||||
match subscriber.next_message().await {
|
||||
WaitResult::Lagged(_) => {
|
||||
// Maybe we missed the message?
|
||||
}
|
||||
WaitResult::Message(ControlMessage::Published(published_pid)) => {
|
||||
if published_pid == expected_pid {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timer::after_millis(CONFIRMATION_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::First(r) => r,
|
||||
Either::Second(_) => Err(Error::TimedOut),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn publish(
|
||||
topic_name: &str,
|
||||
payload: &[u8],
|
||||
qos: QoS,
|
||||
retain: bool,
|
||||
) -> Result<(), Error> {
|
||||
let subscriber = subscribe().await;
|
||||
|
||||
let (qospid, pid) = match qos {
|
||||
QoS::AtMostOnce => (QosPid::AtMostOnce, None),
|
||||
QoS::AtLeastOnce => {
|
||||
let pid = assign_pid().await;
|
||||
(QosPid::AtLeastOnce(pid), Some(pid))
|
||||
}
|
||||
QoS::ExactlyOnce => {
|
||||
let pid = assign_pid().await;
|
||||
(QosPid::ExactlyOnce(pid), Some(pid))
|
||||
}
|
||||
};
|
||||
|
||||
let packet = Packet::Publish(Publish {
|
||||
dup: false,
|
||||
qospid,
|
||||
retain,
|
||||
topic_name,
|
||||
payload,
|
||||
});
|
||||
|
||||
send_packet(packet).await?;
|
||||
|
||||
if let Some(expected_pid) = pid {
|
||||
wait_for_publish(subscriber, expected_pid).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn packet_size(buffer: &[u8]) -> Option<usize> {
|
||||
let mut pos = 1;
|
||||
let mut multiplier = 1;
|
||||
let mut value = 0;
|
||||
|
||||
while pos < buffer.len() {
|
||||
value += (buffer[pos] & 127) as usize * multiplier;
|
||||
multiplier *= 128;
|
||||
|
||||
if (buffer[pos] & 128) == 0 {
|
||||
return Some(value + pos + 1);
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
if pos == 5 {
|
||||
return Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// The MQTT task that must be run in order for the stack to operate.
|
||||
pub struct McutieTask<'t, T, L, const S: usize>
|
||||
where
|
||||
T: Deref<Target = str> + 't,
|
||||
L: Publishable + 't,
|
||||
{
|
||||
pub(crate) network: Stack<'t>,
|
||||
pub(crate) broker: &'t str,
|
||||
pub(crate) last_will: Option<L>,
|
||||
pub(crate) username: Option<&'t str>,
|
||||
pub(crate) password: Option<&'t str>,
|
||||
pub(crate) subscriptions: [Topic<T>; S],
|
||||
pub(crate) keep_alive: u16
|
||||
}
|
||||
|
||||
impl<'t, T, L, const S: usize> McutieTask<'t, T, L, S>
|
||||
where
|
||||
T: Deref<Target = str> + 't,
|
||||
L: Publishable + 't,
|
||||
{
|
||||
#[cfg(not(feature = "homeassistant"))]
|
||||
async fn ha_handle_update(&self, _topic: &Topic<TopicString>, _payload: &Payload) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn recv_loop(&self, mut reader: TcpReader<'_>) -> Result<(), Error> {
|
||||
let mut buffer = [0_u8; 4096];
|
||||
let mut cursor: usize = 0;
|
||||
|
||||
let controller = CONTROL_CHANNEL.immediate_publisher();
|
||||
|
||||
loop {
|
||||
match reader.read(&mut buffer[cursor..]).await {
|
||||
Ok(0) => {
|
||||
error!("Receive socket closed");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(len) => {
|
||||
cursor += len;
|
||||
}
|
||||
Err(_) => {
|
||||
error!("I/O failure reading packet");
|
||||
return Err(Error::IOError);
|
||||
}
|
||||
}
|
||||
|
||||
let mut start_pos = 0;
|
||||
loop {
|
||||
let packet_length = match packet_size(&buffer[start_pos..cursor]) {
|
||||
Some(0) => {
|
||||
error!("Invalid MQTT packet");
|
||||
return Err(Error::PacketError);
|
||||
}
|
||||
Some(len) => len,
|
||||
None => {
|
||||
// None is returned when there is not yet enough data to decode a packet.
|
||||
if start_pos != 0 {
|
||||
// Adjust the buffer to reclaim any unused data
|
||||
buffer.copy_within(start_pos..cursor, 0);
|
||||
cursor -= start_pos;
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let packet = match decode_slice(&buffer[start_pos..(start_pos + packet_length)]) {
|
||||
Ok(Some(p)) => p,
|
||||
Ok(None) => {
|
||||
error!("Packet length calculation failed.");
|
||||
return Err(Error::PacketError);
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Invalid MQTT packet");
|
||||
return Err(Error::PacketError);
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Received packet from broker: {:?}",
|
||||
Debug2Format(&packet.get_type())
|
||||
);
|
||||
|
||||
match packet {
|
||||
Packet::Connack(connack) => match connack.code {
|
||||
ConnectReturnCode::Accepted => {
|
||||
#[cfg(feature = "homeassistant")]
|
||||
self.ha_after_connected().await;
|
||||
|
||||
for topic in &self.subscriptions {
|
||||
let _ = topic.subscribe(false).await;
|
||||
}
|
||||
|
||||
DATA_CHANNEL.send(MqttMessage::Connected).await;
|
||||
}
|
||||
_ => {
|
||||
error!("Connection request to broker was not accepted");
|
||||
return Err(Error::IOError);
|
||||
}
|
||||
},
|
||||
Packet::Pingresp => {}
|
||||
|
||||
Packet::Publish(publish) => {
|
||||
match (
|
||||
Topic::from_str(publish.topic_name),
|
||||
Payload::from(publish.payload),
|
||||
) {
|
||||
(Ok(topic), Ok(payload)) => {
|
||||
if !self.ha_handle_update(&topic, &payload).await {
|
||||
DATA_CHANNEL
|
||||
.send(MqttMessage::Publish(topic, payload))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Unable to process publish data as it was too large");
|
||||
}
|
||||
}
|
||||
|
||||
match publish.qospid {
|
||||
mqttrs::QosPid::AtMostOnce => {}
|
||||
mqttrs::QosPid::AtLeastOnce(pid) => {
|
||||
send_packet(Packet::Puback(pid)).await?;
|
||||
}
|
||||
mqttrs::QosPid::ExactlyOnce(pid) => {
|
||||
send_packet(Packet::Pubrec(pid)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Packet::Puback(pid) => {
|
||||
controller.publish_immediate(ControlMessage::Published(pid));
|
||||
}
|
||||
Packet::Pubrec(pid) => {
|
||||
controller.publish_immediate(ControlMessage::Published(pid));
|
||||
send_packet(Packet::Pubrel(pid)).await?;
|
||||
}
|
||||
Packet::Pubrel(pid) => send_packet(Packet::Pubrel(pid)).await?,
|
||||
Packet::Pubcomp(_) => {}
|
||||
|
||||
Packet::Suback(suback) => {
|
||||
if let Some(return_code) = suback.return_codes.first() {
|
||||
controller.publish_immediate(ControlMessage::Subscribed(
|
||||
suback.pid,
|
||||
*return_code,
|
||||
));
|
||||
} else {
|
||||
warn!("Unexpected suback with no return codes");
|
||||
}
|
||||
}
|
||||
Packet::Unsuback(pid) => {
|
||||
controller.publish_immediate(ControlMessage::Unsubscribed(pid));
|
||||
}
|
||||
|
||||
Packet::Connect(_)
|
||||
| Packet::Subscribe(_)
|
||||
| Packet::Pingreq
|
||||
| Packet::Unsubscribe(_)
|
||||
| Packet::Disconnect => {
|
||||
debug!(
|
||||
"Unexpected packet from broker: {:?}",
|
||||
Debug2Format(&packet.get_type())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
start_pos += packet_length;
|
||||
if start_pos == cursor {
|
||||
cursor = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_loop(&self, mut writer: TcpWriter<'_>) {
|
||||
let mut buffer = Payload::new();
|
||||
|
||||
let mut last_will_topic = TopicString::new();
|
||||
let mut last_will_payload = Payload::new();
|
||||
|
||||
let last_will = self.last_will.as_ref().and_then(|p| {
|
||||
if p.write_topic(&mut last_will_topic).is_ok()
|
||||
&& p.write_payload(&mut last_will_payload).is_ok()
|
||||
{
|
||||
Some(LastWill {
|
||||
topic: &last_will_topic,
|
||||
message: &last_will_payload,
|
||||
qos: p.qos(),
|
||||
retain: p.retain(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Send our connection request.
|
||||
if buffer
|
||||
.encode_packet(&Packet::Connect(Connect {
|
||||
protocol: Protocol::MQTT311,
|
||||
keep_alive: self.keep_alive,
|
||||
client_id: device_id(),
|
||||
clean_session: true,
|
||||
last_will,
|
||||
username: self.username,
|
||||
password: self.password.map(|s| s.as_bytes()),
|
||||
}))
|
||||
.is_err()
|
||||
{
|
||||
error!("Failed to encode connection packet");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = writer.write_all(&buffer).await {
|
||||
error!("Failed to send connection packet: {:?}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let reader = SEND_QUEUE.reader();
|
||||
|
||||
loop {
|
||||
let buffer = reader.receive().await;
|
||||
|
||||
trace!("Writer sending packet");
|
||||
if let Err(e) = writer.write_all(&buffer).await {
|
||||
error!("Failed to send data: {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the MQTT stack. The future returned from this must be awaited for everything to work.
|
||||
pub async fn run(self) {
|
||||
let mut timeout: Option<u64> = None;
|
||||
|
||||
let mut rx_buffer = [0; 4096];
|
||||
let mut tx_buffer = [0; 4096];
|
||||
|
||||
loop {
|
||||
if let Some(millis) = timeout.replace(DEFAULT_BACKOFF) {
|
||||
Timer::after_millis(millis).await;
|
||||
}
|
||||
|
||||
if !self.network.is_config_up() {
|
||||
debug!("Waiting for network to configure.");
|
||||
self.network.wait_config_up().await;
|
||||
debug!("Network configured.");
|
||||
}
|
||||
|
||||
let ip_addrs = match self.network.dns_query(self.broker, DnsQueryType::A).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Failed to lookup '{}' for broker: {:?}", self.broker, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let ip = match ip_addrs.first() {
|
||||
Some(i) => *i,
|
||||
None => {
|
||||
error!("No IP address found for broker '{}'", self.broker);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Connecting to {}:1883", ip);
|
||||
|
||||
let mut socket = TcpSocket::new(self.network, &mut rx_buffer, &mut tx_buffer);
|
||||
if let Err(e) = socket.connect((ip, 1883)).await {
|
||||
error!("Failed to connect to {}:1883: {:?}", ip, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Connected to {}", self.broker);
|
||||
timeout = Some(RESET_BACKOFF);
|
||||
|
||||
let (reader, writer) = socket.split();
|
||||
|
||||
let recv_loop = self.recv_loop(reader);
|
||||
let send_loop = self.write_loop(writer);
|
||||
|
||||
let ping_loop = async {
|
||||
loop {
|
||||
Timer::after_secs(45).await;
|
||||
|
||||
let _ = send_packet(Packet::Pingreq).await;
|
||||
}
|
||||
};
|
||||
|
||||
let link_down = async {
|
||||
self.network.wait_link_down().await;
|
||||
warn!("Network link lost");
|
||||
};
|
||||
|
||||
let ip_down = async {
|
||||
self.network.wait_config_down().await;
|
||||
warn!("Network config lost");
|
||||
};
|
||||
|
||||
select4(send_loop, ping_loop, recv_loop, select(link_down, ip_down)).await;
|
||||
|
||||
socket.close();
|
||||
|
||||
warn!("Lost connection with broker");
|
||||
DATA_CHANNEL.send(MqttMessage::Disconnected).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
#![no_std]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
//! MQTT client support crate vendored into this repository.
|
||||
|
||||
use core::{ops::Deref, str};
|
||||
|
||||
pub use buffer::Buffer;
|
||||
use embassy_net::{HardwareAddress, Stack};
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel};
|
||||
use heapless::String;
|
||||
pub use io::McutieTask;
|
||||
pub use mqttrs::QoS;
|
||||
use mqttrs::{Pid, SubscribeReturnCodes};
|
||||
use once_cell::sync::OnceCell;
|
||||
pub use publish::*;
|
||||
pub use topic::Topic;
|
||||
|
||||
// This must come first so the macros are visible
|
||||
pub(crate) mod fmt;
|
||||
|
||||
mod buffer;
|
||||
#[cfg(feature = "homeassistant")]
|
||||
pub mod homeassistant;
|
||||
mod io;
|
||||
mod pipe;
|
||||
mod publish;
|
||||
mod topic;
|
||||
|
||||
// This really needs to match that used by mqttrs.
|
||||
const TOPIC_LENGTH: usize = 256;
|
||||
const PAYLOAD_LENGTH: usize = 2048;
|
||||
|
||||
/// A fixed length stack allocated string. The length is fixed by the mqttrs crate.
|
||||
pub type TopicString = String<TOPIC_LENGTH>;
|
||||
/// A fixed length buffer of 2048 bytes.
|
||||
pub type Payload = Buffer<PAYLOAD_LENGTH>;
|
||||
|
||||
// By default in the event of an error connecting to the broker we will wait for 5s.
|
||||
const DEFAULT_BACKOFF: u64 = 5000;
|
||||
// If the connection dropped then re-connect more quickly.
|
||||
const RESET_BACKOFF: u64 = 200;
|
||||
// How long to wait for the broker to confirm actions.
|
||||
const CONFIRMATION_TIMEOUT: u64 = 2000;
|
||||
|
||||
static DATA_CHANNEL: Channel<CriticalSectionRawMutex, MqttMessage, 10> = Channel::new();
|
||||
|
||||
static DEVICE_TYPE: OnceCell<String<32>> = OnceCell::new();
|
||||
static DEVICE_ID: OnceCell<String<32>> = OnceCell::new();
|
||||
|
||||
fn device_id() -> &'static str {
|
||||
DEVICE_ID.get().unwrap()
|
||||
}
|
||||
|
||||
fn device_type() -> &'static str {
|
||||
DEVICE_TYPE.get().unwrap()
|
||||
}
|
||||
|
||||
/// Various errors
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum Error {
|
||||
/// An IO error occured.
|
||||
IOError,
|
||||
/// The operation timed out.
|
||||
TimedOut,
|
||||
/// An attempt was made to encode something too large.
|
||||
TooLarge,
|
||||
/// A packet or payload could not be decoded or encoded.
|
||||
PacketError,
|
||||
/// An invalid or unsupported operation was attempted.
|
||||
Invalid,
|
||||
/// A value was rejected.
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
/// A message from the MQTT broker.
|
||||
pub enum MqttMessage {
|
||||
/// The broker has been connected to successfully. Generally in response to this message a
|
||||
/// device should subscribe to topics of interest and send out any device state.
|
||||
Connected,
|
||||
/// New data received from the broker.
|
||||
Publish(Topic<TopicString>, Payload),
|
||||
/// The connection to the broker has been dropped.
|
||||
Disconnected,
|
||||
/// Home Assistant has come online and you should send any discovery messages.
|
||||
#[cfg(feature = "homeassistant")]
|
||||
HomeAssistantOnline,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ControlMessage {
|
||||
Published(Pid),
|
||||
Subscribed(Pid, SubscribeReturnCodes),
|
||||
Unsubscribed(Pid),
|
||||
}
|
||||
|
||||
/// Receives messages from the broker.
|
||||
pub struct McutieReceiver;
|
||||
|
||||
impl McutieReceiver {
|
||||
/// Waits for the next message from the broker.
|
||||
pub async fn receive(&self) -> MqttMessage {
|
||||
DATA_CHANNEL.receive().await
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder to configure the MQTT stack.
|
||||
pub struct McutieBuilder<'t, T, L, const S: usize>
|
||||
where
|
||||
T: Deref<Target = str> + 't,
|
||||
L: Publishable + 't,
|
||||
{
|
||||
network: Stack<'t>,
|
||||
device_type: &'t str,
|
||||
device_id: Option<&'t str>,
|
||||
broker: &'t str,
|
||||
last_will: Option<L>,
|
||||
username: Option<&'t str>,
|
||||
password: Option<&'t str>,
|
||||
subscriptions: [Topic<T>; S],
|
||||
}
|
||||
|
||||
impl<'t, T: Deref<Target = str> + 't, L: Publishable + 't> McutieBuilder<'t, T, L, 0> {
|
||||
/// Creates a new builder with the initial required configuration.
|
||||
///
|
||||
/// `device_type` is expected to be the same for all devices of the same type.
|
||||
/// `broker` may be an IP address or a DNS name for the broker to connect to.
|
||||
pub fn new(network: Stack<'t>, device_type: &'t str, broker: &'t str) -> Self {
|
||||
Self {
|
||||
network,
|
||||
device_type,
|
||||
broker,
|
||||
device_id: None,
|
||||
last_will: None,
|
||||
username: None,
|
||||
password: None,
|
||||
subscriptions: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t, T: Deref<Target = str> + 't, L: Publishable + 't, const S: usize>
|
||||
McutieBuilder<'t, T, L, S>
|
||||
{
|
||||
/// Add some default topics to subscribe to.
|
||||
pub fn with_subscriptions<const N: usize>(
|
||||
self,
|
||||
subscriptions: [Topic<T>; N],
|
||||
) -> McutieBuilder<'t, T, L, N> {
|
||||
McutieBuilder {
|
||||
network: self.network,
|
||||
device_type: self.device_type,
|
||||
broker: self.broker,
|
||||
device_id: self.device_id,
|
||||
last_will: self.last_will,
|
||||
username: self.username,
|
||||
password: self.password,
|
||||
subscriptions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t, T: Deref<Target = str> + 't, L: Publishable + 't, const S: usize>
|
||||
McutieBuilder<'t, T, L, S>
|
||||
{
|
||||
/// Adds authentication for the broker.
|
||||
pub fn with_authentication(self, username: &'t str, password: &'t str) -> Self {
|
||||
Self {
|
||||
username: Some(username),
|
||||
password: Some(password),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a last will message to be published in the event of disconnection.
|
||||
pub fn with_last_will(self, last_will: L) -> Self {
|
||||
Self {
|
||||
last_will: Some(last_will),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a custom unique device identifier. If none is set then the network
|
||||
/// MAC address is used.
|
||||
pub fn with_device_id(self, device_id: &'t str) -> Self {
|
||||
Self {
|
||||
device_id: Some(device_id),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialises the MQTT stack returning a receiver for listening to
|
||||
/// messages from the broker and a future that must be run in order for the
|
||||
/// stack to operate.
|
||||
pub fn build(self, keep_alive: u16) -> (McutieReceiver, McutieTask<'t, T, L, S>) {
|
||||
let mut dtype = String::<32>::new();
|
||||
dtype.push_str(self.device_type).unwrap();
|
||||
DEVICE_TYPE.set(dtype).unwrap();
|
||||
|
||||
let mut did = String::<32>::new();
|
||||
if let Some(device_id) = self.device_id {
|
||||
did.push_str(device_id).unwrap();
|
||||
} else if let HardwareAddress::Ethernet(address) = self.network.hardware_address() {
|
||||
let mut buffer = [0_u8; 12];
|
||||
hex::encode_to_slice(address.as_bytes(), &mut buffer).unwrap();
|
||||
did.push_str(str::from_utf8(&buffer).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
DEVICE_ID.set(did).unwrap();
|
||||
|
||||
(
|
||||
McutieReceiver {},
|
||||
McutieTask {
|
||||
network: self.network,
|
||||
broker: self.broker,
|
||||
last_will: self.last_will,
|
||||
username: self.username,
|
||||
password: self.password,
|
||||
subscriptions: self.subscriptions,
|
||||
keep_alive,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
use core::{
|
||||
cell::RefCell,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
use embassy_sync::blocking_mutex::{raw::RawMutex, Mutex};
|
||||
use pin_project::pin_project;
|
||||
|
||||
struct PipeData<T, const N: usize> {
|
||||
connect_count: usize,
|
||||
receiver_waker: Option<Waker>,
|
||||
sender_waker: Option<Waker>,
|
||||
pending: Option<T>,
|
||||
}
|
||||
|
||||
fn swap_wakers(waker: &mut Option<Waker>, new_waker: &Waker) {
|
||||
if let Some(old_waker) = waker.take() {
|
||||
if old_waker.will_wake(new_waker) {
|
||||
*waker = Some(old_waker)
|
||||
} else {
|
||||
if !new_waker.will_wake(&old_waker) {
|
||||
old_waker.wake();
|
||||
}
|
||||
|
||||
*waker = Some(new_waker.clone());
|
||||
}
|
||||
} else {
|
||||
*waker = Some(new_waker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ReceiveFuture<'a, M: RawMutex, T, const N: usize> {
|
||||
pipe: &'a ConnectedPipe<M, T, N>,
|
||||
}
|
||||
|
||||
impl<M: RawMutex, T, const N: usize> Future for ReceiveFuture<'_, M, T, N> {
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.pipe.inner.lock(|cell| {
|
||||
let mut inner = cell.borrow_mut();
|
||||
|
||||
if let Some(waker) = inner.sender_waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
if let Some(item) = inner.pending.take() {
|
||||
if let Some(old_waker) = inner.receiver_waker.take() {
|
||||
old_waker.wake();
|
||||
}
|
||||
|
||||
Poll::Ready(item)
|
||||
} else {
|
||||
swap_wakers(&mut inner.receiver_waker, cx.waker());
|
||||
Poll::Pending
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PipeReader<'a, M: RawMutex, T, const N: usize> {
|
||||
pipe: &'a ConnectedPipe<M, T, N>,
|
||||
}
|
||||
|
||||
impl<M: RawMutex, T, const N: usize> PipeReader<'_, M, T, N> {
|
||||
#[must_use]
|
||||
pub(crate) fn receive(&self) -> ReceiveFuture<'_, M, T, N> {
|
||||
ReceiveFuture { pipe: self.pipe }
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: RawMutex, T, const N: usize> Drop for PipeReader<'_, M, T, N> {
|
||||
fn drop(&mut self) {
|
||||
self.pipe.inner.lock(|cell| {
|
||||
let mut inner = cell.borrow_mut();
|
||||
inner.connect_count -= 1;
|
||||
|
||||
if inner.connect_count == 0 {
|
||||
inner.pending = None;
|
||||
}
|
||||
|
||||
if let Some(waker) = inner.sender_waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
pub(crate) struct PushFuture<'a, M: RawMutex, T, const N: usize> {
|
||||
data: Option<T>,
|
||||
pipe: &'a ConnectedPipe<M, T, N>,
|
||||
}
|
||||
|
||||
impl<M: RawMutex, T, const N: usize> Future for PushFuture<'_, M, T, N> {
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.pipe.inner.lock(|cell| {
|
||||
let project = self.project();
|
||||
let mut inner = cell.borrow_mut();
|
||||
|
||||
if let Some(receiver) = inner.receiver_waker.take() {
|
||||
receiver.wake();
|
||||
}
|
||||
|
||||
if project.data.is_none() || inner.connect_count == 0 {
|
||||
trace!("Dropping packet");
|
||||
Poll::Ready(())
|
||||
} else if inner.pending.is_some() {
|
||||
swap_wakers(&mut inner.sender_waker, cx.waker());
|
||||
Poll::Pending
|
||||
} else {
|
||||
inner.pending = project.data.take();
|
||||
|
||||
Poll::Ready(())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A pipe that knows whether a receiver is connected. If so pushing to the
|
||||
/// queue waits until there is space in the queue, otherwise data is simply
|
||||
/// dropped.
|
||||
pub(crate) struct ConnectedPipe<M: RawMutex, T, const N: usize> {
|
||||
inner: Mutex<M, RefCell<PipeData<T, N>>>,
|
||||
}
|
||||
|
||||
impl<M: RawMutex, T, const N: usize> ConnectedPipe<M, T, N> {
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self {
|
||||
inner: Mutex::new(RefCell::new(PipeData {
|
||||
connect_count: 0,
|
||||
receiver_waker: None,
|
||||
sender_waker: None,
|
||||
pending: None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// A future that waits for a new item to be available.
|
||||
pub(crate) fn reader(&self) -> PipeReader<'_, M, T, N> {
|
||||
self.inner.lock(|cell| {
|
||||
let mut inner = cell.borrow_mut();
|
||||
inner.connect_count += 1;
|
||||
|
||||
PipeReader { pipe: self }
|
||||
})
|
||||
}
|
||||
|
||||
/// Pushes an item to the reader, waiting for a slot to become available if
|
||||
/// connected.
|
||||
#[must_use]
|
||||
pub(crate) fn push(&self, data: T) -> PushFuture<'_, M, T, N> {
|
||||
PushFuture {
|
||||
data: Some(data),
|
||||
pipe: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::time::Duration;
|
||||
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use futures_executor::{LocalPool, ThreadPool};
|
||||
use futures_timer::Delay;
|
||||
use futures_util::{future::select, pin_mut, task::SpawnExt, FutureExt};
|
||||
|
||||
use super::ConnectedPipe;
|
||||
|
||||
async fn wait_milis(milis: u64) {
|
||||
Delay::new(Duration::from_millis(milis)).await;
|
||||
}
|
||||
|
||||
// #[futures_test::test]
|
||||
#[test]
|
||||
fn test_send_receive() {
|
||||
let mut executor = LocalPool::new();
|
||||
let spawner = executor.spawner();
|
||||
|
||||
static PIPE: ConnectedPipe<CriticalSectionRawMutex, usize, 5> = ConnectedPipe::new();
|
||||
|
||||
// Task that sends
|
||||
spawner
|
||||
.spawn(async {
|
||||
wait_milis(10).await;
|
||||
|
||||
PIPE.push(23).await;
|
||||
PIPE.push(56).await;
|
||||
PIPE.push(67).await;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Task that receives
|
||||
spawner
|
||||
.spawn(async {
|
||||
let reader = PIPE.reader();
|
||||
let value = reader.receive().await;
|
||||
assert_eq!(value, 23);
|
||||
let value = reader.receive().await;
|
||||
assert_eq!(value, 56);
|
||||
let value = reader.receive().await;
|
||||
assert_eq!(value, 67);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
executor.run();
|
||||
}
|
||||
|
||||
#[futures_test::test]
|
||||
async fn test_send_drop() {
|
||||
static PIPE: ConnectedPipe<CriticalSectionRawMutex, usize, 5> = ConnectedPipe::new();
|
||||
|
||||
PIPE.push(23).await;
|
||||
PIPE.push(56).await;
|
||||
PIPE.push(67).await;
|
||||
|
||||
// Create a reader after sending
|
||||
let reader = PIPE.reader();
|
||||
let receive = reader.receive().fuse();
|
||||
pin_mut!(receive);
|
||||
|
||||
let timeout = wait_milis(50).fuse();
|
||||
pin_mut!(timeout);
|
||||
|
||||
let either = select(receive, timeout).await;
|
||||
|
||||
match either {
|
||||
futures_util::future::Either::Left(_) => {
|
||||
panic!("There should be nothing to receive!");
|
||||
}
|
||||
futures_util::future::Either::Right(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[futures_test::test]
|
||||
async fn test_bulk_send_publish() {
|
||||
static PIPE: ConnectedPipe<CriticalSectionRawMutex, usize, 5> = ConnectedPipe::new();
|
||||
|
||||
let executor = ThreadPool::new().unwrap();
|
||||
|
||||
executor
|
||||
.spawn(async {
|
||||
for i in 0..1000 {
|
||||
PIPE.push(i).await;
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
executor
|
||||
.spawn(async {
|
||||
for i in 1000..2000 {
|
||||
PIPE.push(i).await;
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let reader = PIPE.reader();
|
||||
for _ in 0..800 {
|
||||
reader.receive().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
use core::{fmt::Display, future::Future, ops::Deref};
|
||||
|
||||
use embedded_io::Write;
|
||||
use mqttrs::QoS;
|
||||
|
||||
use crate::{io::publish, Error, Payload, Topic, TopicString};
|
||||
|
||||
/// A message that can be published to an MQTT broker.
|
||||
pub trait Publishable {
|
||||
/// Write this message's topic into the supplied buffer.
|
||||
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error>;
|
||||
|
||||
/// Write this message's payload into the supplied buffer.
|
||||
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error>;
|
||||
|
||||
/// Get this message's QoS level.
|
||||
fn qos(&self) -> QoS {
|
||||
QoS::AtMostOnce
|
||||
}
|
||||
|
||||
/// Whether the broker should retain this message.
|
||||
fn retain(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Publishes this message to the broker. If the stack has not yet been
|
||||
/// initialized this is likely to panic.
|
||||
fn publish(&self) -> impl Future<Output = Result<(), Error>> {
|
||||
async {
|
||||
let mut topic = TopicString::new();
|
||||
self.write_topic(&mut topic)?;
|
||||
|
||||
let mut payload = Payload::new();
|
||||
self.write_payload(&mut payload)?;
|
||||
|
||||
publish(&topic, &payload, self.qos(), self.retain()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Publishable`] with a raw byte payload.
|
||||
pub struct PublishBytes<'a, T, B: AsRef<[u8]>> {
|
||||
pub(crate) topic: &'a Topic<T>,
|
||||
pub(crate) data: B,
|
||||
pub(crate) qos: QoS,
|
||||
pub(crate) retain: bool,
|
||||
}
|
||||
|
||||
impl<T, B: AsRef<[u8]>> PublishBytes<'_, T, B> {
|
||||
/// Sets the QoS level for this message.
|
||||
pub fn qos(mut self, qos: QoS) -> Self {
|
||||
self.qos = qos;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the broker should retain this message.
|
||||
pub fn retain(mut self, retain: bool) -> Self {
|
||||
self.retain = retain;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Deref<Target = str> + 'a, B: AsRef<[u8]>> Publishable for PublishBytes<'a, T, B> {
|
||||
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error> {
|
||||
self.topic.to_string(buffer)
|
||||
}
|
||||
|
||||
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error> {
|
||||
buffer
|
||||
.write_all(self.data.as_ref())
|
||||
.map_err(|_| Error::TooLarge)
|
||||
}
|
||||
|
||||
fn qos(&self) -> QoS {
|
||||
self.qos
|
||||
}
|
||||
|
||||
fn retain(&self) -> bool {
|
||||
self.retain
|
||||
}
|
||||
|
||||
async fn publish(&self) -> Result<(), Error> {
|
||||
let mut topic = TopicString::new();
|
||||
self.write_topic(&mut topic)?;
|
||||
|
||||
publish(&topic, self.data.as_ref(), self.qos(), self.retain()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Publishable`] with a payload that implements [`Display`].
|
||||
pub struct PublishDisplay<'a, T, D: Display> {
|
||||
pub(crate) topic: &'a Topic<T>,
|
||||
pub(crate) data: D,
|
||||
pub(crate) qos: QoS,
|
||||
pub(crate) retain: bool,
|
||||
}
|
||||
|
||||
impl<T, D: Display> PublishDisplay<'_, T, D> {
|
||||
/// Sets the QoS level for this message.
|
||||
pub fn qos(mut self, qos: QoS) -> Self {
|
||||
self.qos = qos;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the broker should retain this message.
|
||||
pub fn retain(mut self, retain: bool) -> Self {
|
||||
self.retain = retain;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Deref<Target = str> + 'a, D: Display> Publishable for PublishDisplay<'a, T, D> {
|
||||
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error> {
|
||||
self.topic.to_string(buffer)
|
||||
}
|
||||
|
||||
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error> {
|
||||
write!(buffer, "{}", self.data).map_err(|_| Error::TooLarge)
|
||||
}
|
||||
|
||||
fn qos(&self) -> QoS {
|
||||
self.qos
|
||||
}
|
||||
|
||||
fn retain(&self) -> bool {
|
||||
self.retain
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// A [`Publishable`] with that serializes a JSON payload.
|
||||
pub struct PublishJson<'a, T, D: serde::Serialize> {
|
||||
pub(crate) topic: &'a Topic<T>,
|
||||
pub(crate) data: D,
|
||||
pub(crate) qos: QoS,
|
||||
pub(crate) retain: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<T, D: serde::Serialize> PublishJson<'_, T, D> {
|
||||
/// Sets the QoS level for this message.
|
||||
pub fn qos(mut self, qos: QoS) -> Self {
|
||||
self.qos = qos;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the broker should retain this message.
|
||||
pub fn retain(mut self, retain: bool) -> Self {
|
||||
self.retain = retain;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'a, T: Deref<Target = str> + 'a, D: serde::Serialize> Publishable for PublishJson<'a, T, D> {
|
||||
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error> {
|
||||
self.topic.to_string(buffer)
|
||||
}
|
||||
|
||||
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error> {
|
||||
buffer
|
||||
.serialize_json(&self.data)
|
||||
.map_err(|_| Error::TooLarge)
|
||||
}
|
||||
|
||||
fn qos(&self) -> QoS {
|
||||
self.qos
|
||||
}
|
||||
|
||||
fn retain(&self) -> bool {
|
||||
self.retain
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
use core::{fmt::Display, ops::Deref};
|
||||
|
||||
use embassy_futures::select::{select, Either};
|
||||
use embassy_sync::pubsub::WaitResult;
|
||||
use embassy_time::Timer;
|
||||
use heapless::{String, Vec};
|
||||
use mqttrs::{Packet, QoS, Subscribe, SubscribeReturnCodes, SubscribeTopic, Unsubscribe};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use crate::publish::PublishJson;
|
||||
use crate::{
|
||||
device_id, device_type,
|
||||
io::{assign_pid, send_packet, subscribe},
|
||||
publish::{PublishBytes, PublishDisplay},
|
||||
ControlMessage, Error, TopicString, CONFIRMATION_TIMEOUT,
|
||||
};
|
||||
|
||||
/// An MQTT topic that is optionally prefixed with the device type and unique ID.
|
||||
/// Normally you will define all your application's topics as consts with static
|
||||
/// lifetimes.
|
||||
///
|
||||
/// A [`Topic`] is the main entry to publishing messages to the broker.
|
||||
///
|
||||
/// ```
|
||||
/// # use mcutie::{Publishable, Topic};
|
||||
/// const DEVICE_AVAILABILITY: Topic<&'static str> = Topic::Device("state");
|
||||
///
|
||||
/// async fn send_status(status: &'static str) {
|
||||
/// let _ = DEVICE_AVAILABILITY.with_bytes(status.as_bytes()).publish().await;
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Topic<T> {
|
||||
/// A topic that is prefixed with the device type.
|
||||
DeviceType(T),
|
||||
/// A topic that is prefixed with the device type and unique ID.
|
||||
Device(T),
|
||||
/// Any topic.
|
||||
General(T),
|
||||
}
|
||||
|
||||
impl<A, B> PartialEq<Topic<A>> for Topic<B>
|
||||
where
|
||||
B: PartialEq<A>,
|
||||
{
|
||||
fn eq(&self, other: &Topic<A>) -> bool {
|
||||
match (self, other) {
|
||||
(Topic::DeviceType(l0), Topic::DeviceType(r0)) => l0 == r0,
|
||||
(Topic::Device(l0), Topic::Device(r0)) => l0 == r0,
|
||||
(Topic::General(l0), Topic::General(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Topic<T> {
|
||||
/// Creates a publishable message with something that can return a reference
|
||||
/// to the payload in bytes.
|
||||
///
|
||||
/// Defaults to non-retained with QoS of 0 (AtMostOnce).
|
||||
pub fn with_bytes<B: AsRef<[u8]>>(&self, data: B) -> PublishBytes<'_, T, B> {
|
||||
PublishBytes {
|
||||
topic: self,
|
||||
data,
|
||||
qos: QoS::AtMostOnce,
|
||||
retain: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a publishable message with something that implements [`Display`].
|
||||
///
|
||||
/// Defaults to non-retained with QoS of 0 (AtMostOnce).
|
||||
pub fn with_display<D: Display>(&self, data: D) -> PublishDisplay<'_, T, D> {
|
||||
PublishDisplay {
|
||||
topic: self,
|
||||
data,
|
||||
qos: QoS::AtMostOnce,
|
||||
retain: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// Creates a publishable message with something that can be serialized to
|
||||
/// JSON.
|
||||
///
|
||||
/// Defaults to non-retained with QoS of 0 (AtMostOnce).
|
||||
pub fn with_json<D: serde::Serialize>(&self, data: D) -> PublishJson<'_, T, D> {
|
||||
PublishJson {
|
||||
topic: self,
|
||||
data,
|
||||
qos: QoS::AtMostOnce,
|
||||
retain: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Topic<TopicString> {
|
||||
pub(crate) fn from_str(mut st: &str) -> Result<Self, Error> {
|
||||
let mut strip_prefix = |pr: &str| -> bool {
|
||||
if st.starts_with(pr) && st.len() > pr.len() && &st[pr.len()..pr.len() + 1] == "/" {
|
||||
st = &st[pr.len() + 1..];
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if strip_prefix(device_type()) {
|
||||
if strip_prefix(device_id()) {
|
||||
let mut topic = TopicString::new();
|
||||
topic.push_str(st).map_err(|_| Error::TooLarge)?;
|
||||
Ok(Topic::Device(topic))
|
||||
} else {
|
||||
let mut topic = TopicString::new();
|
||||
topic.push_str(st).map_err(|_| Error::TooLarge)?;
|
||||
Ok(Topic::DeviceType(topic))
|
||||
}
|
||||
} else {
|
||||
let mut topic = TopicString::new();
|
||||
topic.push_str(st).map_err(|_| Error::TooLarge)?;
|
||||
Ok(Topic::General(topic))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deref<Target = str>> Topic<T> {
|
||||
pub(crate) fn to_string<const N: usize>(&self, result: &mut String<N>) -> Result<(), Error> {
|
||||
match self {
|
||||
Topic::Device(st) => {
|
||||
result
|
||||
.push_str(device_type())
|
||||
.map_err(|_| Error::TooLarge)?;
|
||||
result.push_str("/").map_err(|_| Error::TooLarge)?;
|
||||
result.push_str(device_id()).map_err(|_| Error::TooLarge)?;
|
||||
result.push_str("/").map_err(|_| Error::TooLarge)?;
|
||||
result.push_str(st.as_ref()).map_err(|_| Error::TooLarge)?;
|
||||
}
|
||||
Topic::DeviceType(st) => {
|
||||
result
|
||||
.push_str(device_type())
|
||||
.map_err(|_| Error::TooLarge)?;
|
||||
result.push_str("/").map_err(|_| Error::TooLarge)?;
|
||||
result.push_str(st.as_ref()).map_err(|_| Error::TooLarge)?;
|
||||
}
|
||||
Topic::General(st) => {
|
||||
result.push_str(st.as_ref()).map_err(|_| Error::TooLarge)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts to a topic containing an [`str`]. Particularly useful for converting from an owned
|
||||
/// string for match patterns.
|
||||
pub fn as_ref(&self) -> Topic<&str> {
|
||||
match self {
|
||||
Topic::DeviceType(st) => Topic::DeviceType(st.as_ref()),
|
||||
Topic::Device(st) => Topic::Device(st.as_ref()),
|
||||
Topic::General(st) => Topic::General(st.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribes to this topic. If `wait_for_ack` is true then this will wait until confirmation
|
||||
/// is received from the broker before returning.
|
||||
pub async fn subscribe(&self, wait_for_ack: bool) -> Result<(), Error> {
|
||||
let mut subscriber = subscribe().await;
|
||||
|
||||
let mut topic_path = TopicString::new();
|
||||
if self.to_string(&mut topic_path).is_err() {
|
||||
return Err(Error::TooLarge);
|
||||
}
|
||||
|
||||
let pid = assign_pid().await;
|
||||
|
||||
let mut subscribe_topic_path = String::<256>::new();
|
||||
subscribe_topic_path
|
||||
.push_str(topic_path.as_str())
|
||||
.map_err(|_| Error::TooLarge)?;
|
||||
let subscribe_topic = SubscribeTopic {
|
||||
topic_path: subscribe_topic_path,
|
||||
qos: QoS::AtLeastOnce,
|
||||
};
|
||||
|
||||
// The size of this vec must match that used by mqttrs.
|
||||
let topics = match Vec::<SubscribeTopic, 5>::from_slice(&[subscribe_topic]) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return Err(Error::TooLarge),
|
||||
};
|
||||
|
||||
let packet = Packet::Subscribe(Subscribe { pid, topics });
|
||||
|
||||
send_packet(packet).await?;
|
||||
|
||||
if wait_for_ack {
|
||||
match select(
|
||||
async {
|
||||
loop {
|
||||
match subscriber.next_message().await {
|
||||
WaitResult::Lagged(_) => {
|
||||
// Maybe we missed the message?
|
||||
}
|
||||
WaitResult::Message(ControlMessage::Subscribed(
|
||||
subscribed_pid,
|
||||
return_code,
|
||||
)) => {
|
||||
if subscribed_pid == pid {
|
||||
if matches!(return_code, SubscribeReturnCodes::Success(_)) {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(Error::IOError);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timer::after_millis(CONFIRMATION_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::First(r) => r,
|
||||
Either::Second(_) => Err(Error::TimedOut),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsubscribes from a topic. If `wait_for_ack` is true then this will wait until confirmation is
|
||||
/// received from the broker before returning.
|
||||
pub async fn unsubscribe(&self, wait_for_ack: bool) -> Result<(), Error> {
|
||||
let mut subscriber = subscribe().await;
|
||||
|
||||
let mut topic_path = TopicString::new();
|
||||
if self.to_string(&mut topic_path).is_err() {
|
||||
return Err(Error::TooLarge);
|
||||
}
|
||||
|
||||
let pid = assign_pid().await;
|
||||
|
||||
// The size of this vec must match that used by mqttrs.
|
||||
let mut unsubscribe_topic_path = String::<256>::new();
|
||||
unsubscribe_topic_path
|
||||
.push_str(topic_path.as_str())
|
||||
.map_err(|_| Error::TooLarge)?;
|
||||
let topics = match Vec::<String<256>, 5>::from_slice(&[unsubscribe_topic_path]) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return Err(Error::TooLarge),
|
||||
};
|
||||
|
||||
let packet = Packet::Unsubscribe(Unsubscribe { pid, topics });
|
||||
|
||||
send_packet(packet).await?;
|
||||
|
||||
if wait_for_ack {
|
||||
match select(
|
||||
async {
|
||||
loop {
|
||||
match subscriber.next_message().await {
|
||||
WaitResult::Lagged(_) => {
|
||||
// Maybe we missed the message?
|
||||
}
|
||||
WaitResult::Message(ControlMessage::Unsubscribed(subscribed_pid)) => {
|
||||
if subscribed_pid == pid {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timer::after_millis(CONFIRMATION_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::First(r) => r,
|
||||
Either::Second(_) => Err(Error::TimedOut),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
use crate::config::NetworkConfig;
|
||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||
use crate::hal::battery::{BatteryError, BatteryInfo, BatteryState};
|
||||
use crate::hal::{PlantHal, HAL};
|
||||
use crate::log::{log, LogMessage};
|
||||
use crate::plant_state::PlantState;
|
||||
use crate::tank::TankState;
|
||||
use crate::{bail, VersionInfo};
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use chrono::DateTime;
|
||||
use chrono_tz::Tz;
|
||||
use core::sync::atomic::Ordering;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_net::Stack;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::MutexGuard;
|
||||
use embassy_sync::once_lock::OnceLock;
|
||||
use embassy_time::{Duration, Timer, WithTimeout};
|
||||
use log::{error, info, warn};
|
||||
use mcutie::{
|
||||
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
|
||||
QoS, Topic,
|
||||
};
|
||||
use portable_atomic::AtomicBool;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false);
|
||||
static MQTT_ROUND_TRIP_RECEIVED: AtomicBool = AtomicBool::new(false);
|
||||
pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false);
|
||||
static MQTT_BASE_TOPIC: OnceLock<String> = OnceLock::new();
|
||||
|
||||
pub fn is_stay_alive() -> bool {
|
||||
MQTT_STAY_ALIVE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub async fn publish(subtopic: &str, message: &str) {
|
||||
let online = MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed);
|
||||
if !online {
|
||||
return;
|
||||
}
|
||||
let roundtrip_ok = MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed);
|
||||
if !roundtrip_ok {
|
||||
info!("MQTT roundtrip not received yet, dropping message");
|
||||
return;
|
||||
}
|
||||
match publish_inner(subtopic, message).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Error during mqtt send on topic {subtopic} with message {message:#?} error is {err:?}"
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async fn publish_inner(subtopic: &str, message: &str) -> FatResult<()> {
|
||||
if !subtopic.starts_with("/") {
|
||||
bail!("Subtopic without / at start {}", subtopic);
|
||||
}
|
||||
if subtopic.len() > 192 {
|
||||
bail!("Subtopic exceeds 192 chars {}", subtopic);
|
||||
}
|
||||
let base_topic = MQTT_BASE_TOPIC
|
||||
.try_get()
|
||||
.context("missing base topic in static!")?;
|
||||
|
||||
let full_topic = format!("{base_topic}{subtopic}");
|
||||
|
||||
loop {
|
||||
let result = Topic::General(full_topic.as_str())
|
||||
.with_display(message)
|
||||
.retain(true)
|
||||
.publish()
|
||||
.await;
|
||||
match result {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(err) => {
|
||||
let retry = match err {
|
||||
Error::IOError => false,
|
||||
Error::TimedOut => true,
|
||||
Error::TooLarge => false,
|
||||
Error::PacketError => false,
|
||||
Error::Invalid => false,
|
||||
Error::Rejected => false,
|
||||
};
|
||||
if !retry {
|
||||
bail!(
|
||||
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
|
||||
&full_topic,
|
||||
message,
|
||||
err
|
||||
);
|
||||
}
|
||||
info!(
|
||||
"Retransmit for {} with message {:#?} error is {:?} retrying {}",
|
||||
&full_topic, message, err, retry
|
||||
);
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::util::mk_static;
|
||||
|
||||
pub async fn mqtt_init(
|
||||
network_config: &'static NetworkConfig,
|
||||
stack: Stack<'static>,
|
||||
spawner: Spawner,
|
||||
) -> FatResult<()> {
|
||||
let base_topic = network_config
|
||||
.base_topic
|
||||
.as_ref()
|
||||
.context("missing base topic")?;
|
||||
if base_topic.is_empty() {
|
||||
bail!("Mqtt base_topic was empty")
|
||||
}
|
||||
MQTT_BASE_TOPIC
|
||||
.init(base_topic.to_string())
|
||||
.map_err(|_| FatError::String {
|
||||
error: "Error setting basetopic".to_string(),
|
||||
})?;
|
||||
|
||||
let mqtt_url = network_config
|
||||
.mqtt_url
|
||||
.as_ref()
|
||||
.context("missing mqtt url")?;
|
||||
if mqtt_url.is_empty() {
|
||||
bail!("Mqtt url was empty")
|
||||
}
|
||||
|
||||
let last_will_topic = format!("{base_topic}/state");
|
||||
let round_trip_topic = format!("{base_topic}/internal/roundtrip");
|
||||
let stay_alive_topic = format!("{base_topic}/stay_alive");
|
||||
|
||||
let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> =
|
||||
McutieBuilder::new(stack, "plant ctrl", mqtt_url);
|
||||
if let (Some(mqtt_user), Some(mqtt_password)) = (
|
||||
network_config.mqtt_user.as_ref(),
|
||||
network_config.mqtt_password.as_ref(),
|
||||
) {
|
||||
builder = builder.with_authentication(mqtt_user, mqtt_password);
|
||||
info!("With authentification");
|
||||
}
|
||||
|
||||
let lwt = Topic::General(last_will_topic);
|
||||
let lwt = mk_static!(Topic<String>, lwt);
|
||||
let lwt = lwt.with_display("lost").retain(true).qos(QoS::AtLeastOnce);
|
||||
builder = builder.with_last_will(lwt);
|
||||
//TODO make configurable
|
||||
builder = builder.with_device_id("plantctrl");
|
||||
|
||||
let builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 2> = builder
|
||||
.with_subscriptions([
|
||||
Topic::General(round_trip_topic.clone()),
|
||||
Topic::General(stay_alive_topic.clone()),
|
||||
]);
|
||||
|
||||
let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16;
|
||||
let (receiver, task) = builder.build(keep_alive);
|
||||
|
||||
spawner.spawn(mqtt_incoming_task(
|
||||
receiver,
|
||||
round_trip_topic.clone(),
|
||||
stay_alive_topic.clone(),
|
||||
)?);
|
||||
spawner.spawn(mqtt_runner(task)?);
|
||||
|
||||
log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
|
||||
|
||||
log(LogMessage::MqttInfo, 0, 0, "", mqtt_url);
|
||||
|
||||
let mqtt_timeout = 15000;
|
||||
let res = async {
|
||||
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
|
||||
PlantHal::feed_watchdog();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
bail!("Timeout waiting MQTT connect event")
|
||||
}
|
||||
|
||||
let _ = Topic::General(round_trip_topic.clone())
|
||||
.with_display("online_text")
|
||||
.publish()
|
||||
.await;
|
||||
|
||||
let res = async {
|
||||
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
|
||||
PlantHal::feed_watchdog();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
|
||||
bail!("Timeout waiting MQTT roundtrip")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn mqtt_runner(
|
||||
task: McutieTask<'static, String, PublishDisplay<'static, String, &'static str>, 2>,
|
||||
) {
|
||||
task.run().await;
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn mqtt_incoming_task(
|
||||
receiver: McutieReceiver,
|
||||
round_trip_topic: String,
|
||||
stay_alive_topic: String,
|
||||
) {
|
||||
loop {
|
||||
let message = receiver.receive().await;
|
||||
match message {
|
||||
MqttMessage::Connected => {
|
||||
info!("Mqtt connected");
|
||||
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
|
||||
}
|
||||
MqttMessage::Publish(topic, payload) => match topic {
|
||||
Topic::DeviceType(_type_topic) => {}
|
||||
Topic::Device(_device_topic) => {}
|
||||
Topic::General(topic) => {
|
||||
let subtopic = topic.as_str();
|
||||
|
||||
if subtopic.eq(round_trip_topic.as_str()) {
|
||||
MQTT_ROUND_TRIP_RECEIVED.store(true, Ordering::Relaxed);
|
||||
} else if subtopic.eq(stay_alive_topic.as_str()) {
|
||||
let value = payload.eq_ignore_ascii_case("true".as_ref())
|
||||
|| payload.eq_ignore_ascii_case("1".as_ref());
|
||||
let a = match value {
|
||||
true => 1,
|
||||
false => 0,
|
||||
};
|
||||
log(LogMessage::MqttStayAliveRec, a, 0, "", "");
|
||||
MQTT_STAY_ALIVE.store(value, Ordering::Relaxed);
|
||||
} else {
|
||||
log(LogMessage::UnknownTopic, 0, 0, "", &topic);
|
||||
}
|
||||
}
|
||||
},
|
||||
MqttMessage::Disconnected => {
|
||||
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
|
||||
info!("Mqtt disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish_tank_state(
|
||||
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
||||
tank_state: &TankState,
|
||||
water_temp: FatResult<f32>,
|
||||
) -> FatResult<()> {
|
||||
let state = serde_json::to_string(
|
||||
&tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp),
|
||||
)?;
|
||||
let _ = publish("/water", &state).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn publish_plant_states(
|
||||
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
||||
timezone_time: &DateTime<Tz>,
|
||||
plantstate: &[PlantState; 8],
|
||||
) -> FatResult<()> {
|
||||
for (plant_id, (plant_state, plant_conf)) in plantstate
|
||||
.iter()
|
||||
.zip(&board.board_hal.get_config().plants.clone())
|
||||
.enumerate()
|
||||
{
|
||||
let state = serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time))?;
|
||||
let plant_topic = format!("/plant{}", plant_id + 1);
|
||||
let _ = publish(&plant_topic, &state).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn publish_firmware_info(version: VersionInfo, ip_address: &str, timezone_time: &str) {
|
||||
publish("/firmware/address", ip_address).await;
|
||||
let version = &serde_json::to_string(&version);
|
||||
match version {
|
||||
Ok(version_str) => publish("/firmware/state", version_str).await,
|
||||
Err(e) => error!("Failed to serialize version info: {}", e),
|
||||
}
|
||||
publish("/firmware/last_online", timezone_time).await;
|
||||
publish("/state", "online").await;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
|
||||
///mqtt struct to track pump activities
|
||||
pub struct PumpInfo {
|
||||
pub enabled: bool,
|
||||
pub pump_ineffective: bool,
|
||||
pub median_current_ma: u16,
|
||||
pub max_current_ma: u16,
|
||||
pub min_current_ma: u16,
|
||||
pub error: String,
|
||||
pub flow_raw: u32,
|
||||
pub flow_ml: f32,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn pump_info(
|
||||
plant_id: usize,
|
||||
pump_active: bool,
|
||||
pump_ineffective: bool,
|
||||
median_current_ma: u16,
|
||||
max_current_ma: u16,
|
||||
min_current_ma: u16,
|
||||
error: String,
|
||||
flow_raw: u32,
|
||||
flow_ml: f32,
|
||||
) {
|
||||
let pump_info = PumpInfo {
|
||||
enabled: pump_active,
|
||||
pump_ineffective,
|
||||
median_current_ma,
|
||||
max_current_ma,
|
||||
min_current_ma,
|
||||
error,
|
||||
flow_raw,
|
||||
flow_ml,
|
||||
};
|
||||
let pump_topic = format!("/pump{}", plant_id + 1);
|
||||
|
||||
match serde_json::to_string(&pump_info) {
|
||||
Ok(state) => {
|
||||
let _ = publish(&pump_topic, &state).await;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error publishing pump state {err}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Wi-Fi scan result details for MQTT
|
||||
#[derive(Serialize, Debug, PartialEq)]
|
||||
pub struct WifiScanResult {
|
||||
pub ssid: String,
|
||||
pub bssid: String,
|
||||
pub rssi: i32,
|
||||
pub channel: u8,
|
||||
pub auth_method: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, PartialEq)]
|
||||
pub struct Solar {
|
||||
pub current_ma: u32,
|
||||
pub voltage_ma: u32,
|
||||
}
|
||||
|
||||
pub async fn publish_mppt_state(
|
||||
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
||||
) -> FatResult<()> {
|
||||
let current = board.board_hal.get_mptt_current().await?;
|
||||
let voltage = board.board_hal.get_mptt_voltage().await?;
|
||||
let solar_state = Solar {
|
||||
current_ma: current.as_milliamperes() as u32,
|
||||
voltage_ma: voltage.as_millivolts() as u32,
|
||||
};
|
||||
if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) {
|
||||
let _ = publish("/mppt", &serialized_solar_state_bytes).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn publish_battery_state(
|
||||
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
||||
) -> FatResult<()> {
|
||||
let telemetry = match board.board_hal.get_battery_monitor().get_state().await {
|
||||
Ok(BatteryState::Info(info)) => info,
|
||||
Ok(BatteryState::Unknown) => BatteryInfo {
|
||||
voltage_mv: None,
|
||||
avg_current_ma: None,
|
||||
soc_pct: None,
|
||||
soh_pct: None,
|
||||
temperature_c: None,
|
||||
remaining_mah: None,
|
||||
design_mah: None,
|
||||
error: Some(BatteryError::NoBatteryMonitor),
|
||||
},
|
||||
Err(e) => BatteryInfo {
|
||||
voltage_mv: None,
|
||||
avg_current_ma: None,
|
||||
soc_pct: None,
|
||||
soh_pct: None,
|
||||
temperature_c: None,
|
||||
remaining_mah: None,
|
||||
design_mah: None,
|
||||
error: Some(BatteryError::CommunicationError {
|
||||
message: alloc::format!("{:?}", e),
|
||||
}),
|
||||
},
|
||||
};
|
||||
let json = serde_json::to_string(&telemetry)?;
|
||||
publish("/battery", &json).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish Wi-Fi scan details to MQTT
|
||||
pub async fn publish_wifi_scan(
|
||||
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
||||
) -> FatResult<()> {
|
||||
let mut wifi_details = board.board_hal.get_esp().wifi_scan_details().await?;
|
||||
|
||||
// Sort by RSSI in descending order (strongest first)
|
||||
wifi_details.sort_by(|a, b| b.rssi.cmp(&a.rssi));
|
||||
|
||||
// Take only the strongest 10 results
|
||||
let wifi_results: Vec<WifiScanResult> = wifi_details
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|d| WifiScanResult {
|
||||
ssid: d.ssid.clone(),
|
||||
bssid: d.bssid.clone(),
|
||||
rssi: d.rssi,
|
||||
channel: d.channel,
|
||||
auth_method: d.auth_method.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let json = serde_json::to_string(&wifi_results)?;
|
||||
publish("/wifi_scan", &json).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
use crate::bail;
|
||||
use crate::config::NetworkConfig;
|
||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||
use crate::hal::HAL;
|
||||
use crate::mqtt;
|
||||
use crate::util::mk_static;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::sync::Arc;
|
||||
use chrono::{DateTime, Utc};
|
||||
use core::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use edge_dhcp::{
|
||||
io::{self, DEFAULT_SERVER_PORT},
|
||||
server::{Server, ServerOptions},
|
||||
};
|
||||
use edge_nal::UdpBind;
|
||||
use edge_nal_embassy::{Udp, UdpBuffers};
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_net::dns::DnsQueryType;
|
||||
use embassy_net::udp::{PacketMetadata, UdpSocket};
|
||||
use embassy_net::{DhcpConfig, Runner, Stack, StackResources, StaticConfigV4};
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::{Mutex, MutexGuard};
|
||||
use embassy_time::{Duration, Timer, WithTimeout};
|
||||
use esp_hal::rng::Rng;
|
||||
use esp_println::println;
|
||||
use esp_radio::wifi::ap::AccessPointConfig;
|
||||
use esp_radio::wifi::sta::StationConfig;
|
||||
use esp_radio::wifi::{AuthenticationMethod, Config, Interface};
|
||||
use log::{error, info, warn};
|
||||
use option_lock::OptionLock;
|
||||
use serde::Serialize;
|
||||
use sntpc::{get_time, NtpContext, NtpTimestampGenerator, NtpUdpSocket};
|
||||
|
||||
const NTP_SERVER: &str = "pool.ntp.org";
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Timestamp {
|
||||
stamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl NtpTimestampGenerator for Timestamp {
|
||||
fn init(&mut self) {
|
||||
self.stamp = DateTime::default();
|
||||
}
|
||||
|
||||
fn timestamp_sec(&self) -> u64 {
|
||||
self.stamp.timestamp() as u64
|
||||
}
|
||||
|
||||
fn timestamp_subsec_micros(&self) -> u32 {
|
||||
self.stamp.timestamp_subsec_micros()
|
||||
}
|
||||
}
|
||||
|
||||
struct EmbassyNtpSocket<'a, 'b> {
|
||||
socket: &'a UdpSocket<'b>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> EmbassyNtpSocket<'a, 'b> {
|
||||
fn new(socket: &'a UdpSocket<'b>) -> Self {
|
||||
Self { socket }
|
||||
}
|
||||
}
|
||||
|
||||
impl NtpUdpSocket for EmbassyNtpSocket<'_, '_> {
|
||||
async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> sntpc::Result<usize> {
|
||||
self.socket
|
||||
.send_to(buf, addr)
|
||||
.await
|
||||
.map_err(|_| sntpc::Error::Network)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, buf: &mut [u8]) -> sntpc::Result<(usize, SocketAddr)> {
|
||||
let (len, metadata) = self
|
||||
.socket
|
||||
.recv_from(buf)
|
||||
.await
|
||||
.map_err(|_| sntpc::Error::Network)?;
|
||||
let addr = match metadata.endpoint.addr {
|
||||
embassy_net::IpAddress::Ipv4(ip) => IpAddr::V4(ip),
|
||||
embassy_net::IpAddress::Ipv6(ip) => IpAddr::V6(ip),
|
||||
};
|
||||
Ok((len, SocketAddr::new(addr, metadata.endpoint.port)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sntp(max_wait_ms: u32, stack: Stack<'_>) -> FatResult<DateTime<Utc>> {
|
||||
println!("start sntp");
|
||||
let mut rx_meta = [PacketMetadata::EMPTY; 16];
|
||||
let mut rx_buffer = [0; 4096];
|
||||
let mut tx_meta = [PacketMetadata::EMPTY; 16];
|
||||
let mut tx_buffer = [0; 4096];
|
||||
|
||||
let mut socket = UdpSocket::new(
|
||||
stack,
|
||||
&mut rx_meta,
|
||||
&mut rx_buffer,
|
||||
&mut tx_meta,
|
||||
&mut tx_buffer,
|
||||
);
|
||||
socket.bind(123).context("Could not bind UDP socket")?;
|
||||
|
||||
let context = NtpContext::new(Timestamp::default());
|
||||
let ntp_socket = EmbassyNtpSocket::new(&socket);
|
||||
|
||||
let ntp_addrs = stack
|
||||
.dns_query(NTP_SERVER, DnsQueryType::A)
|
||||
.await
|
||||
.context("Failed to resolve DNS")?;
|
||||
|
||||
if ntp_addrs.is_empty() {
|
||||
bail!("No IP addresses found for NTP server");
|
||||
}
|
||||
let ntp = ntp_addrs[0];
|
||||
info!("NTP server: {ntp:?}");
|
||||
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
let addr: IpAddr = ntp.into();
|
||||
let timeout = get_time(SocketAddr::from((addr, 123)), &ntp_socket, context)
|
||||
.with_timeout(Duration::from_millis((max_wait_ms / 10) as u64))
|
||||
.await;
|
||||
|
||||
match timeout {
|
||||
Ok(result) => {
|
||||
let time = result?;
|
||||
info!("Time: {time:?}");
|
||||
return DateTime::from_timestamp(time.seconds as i64, 0)
|
||||
.context("Could not convert Sntp result");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("sntp timeout, retry: {err:?}");
|
||||
counter += 1;
|
||||
if counter > 10 {
|
||||
bail!("Failed to get time from NTP server");
|
||||
}
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, PartialEq)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum SntpMode {
|
||||
OFFLINE,
|
||||
SYNC { current: DateTime<Utc> },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, PartialEq)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum NetworkMode {
|
||||
WIFI {
|
||||
sntp: SntpMode,
|
||||
mqtt: bool,
|
||||
ip_address: String,
|
||||
},
|
||||
OFFLINE,
|
||||
}
|
||||
|
||||
#[embassy_executor::task(pool_size = 2)]
|
||||
pub(crate) async fn net_task(mut runner: Runner<'static, Interface<'static>>) {
|
||||
runner.run().await;
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub(crate) async fn run_dhcp(stack: Stack<'static>, ip: Ipv4Addr) {
|
||||
let mut buf = [0u8; 1500];
|
||||
|
||||
let mut gw_buf = [Ipv4Addr::UNSPECIFIED];
|
||||
|
||||
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
|
||||
let unbound_socket = Udp::new(stack, &buffers);
|
||||
let mut bound_socket = match unbound_socket
|
||||
.bind(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::UNSPECIFIED,
|
||||
DEFAULT_SERVER_PORT,
|
||||
)))
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("dhcp task failed to bind socket: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
_ = io::server::run(
|
||||
&mut Server::<_, 64>::new_with_et(ip),
|
||||
&ServerOptions::new(ip, Some(&mut gw_buf)),
|
||||
&mut bound_socket,
|
||||
&mut buf,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| warn!("DHCP server error: {e:?}"));
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wifi_ap(
|
||||
ssid: String,
|
||||
interface_ap: Interface<'static>,
|
||||
controller: &Arc<Mutex<CriticalSectionRawMutex, esp_radio::wifi::WifiController<'static>>>,
|
||||
rng: &mut Rng,
|
||||
spawner: Spawner,
|
||||
) -> FatResult<Stack<'static>> {
|
||||
let gw_ip_addr = Ipv4Addr::new(192, 168, 71, 1);
|
||||
|
||||
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
||||
address: embassy_net::Ipv4Cidr::new(gw_ip_addr, 24),
|
||||
gateway: Some(gw_ip_addr),
|
||||
dns_servers: Default::default(),
|
||||
});
|
||||
|
||||
let seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
||||
|
||||
println!("init secondary stack");
|
||||
let (stack, runner) = embassy_net::new(
|
||||
interface_ap,
|
||||
config,
|
||||
mk_static!(StackResources<4>, StackResources::<4>::new()),
|
||||
seed,
|
||||
);
|
||||
let stack = mk_static!(Stack, stack);
|
||||
|
||||
let client_config = Config::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
|
||||
controller.lock().await.set_config(&client_config)?;
|
||||
|
||||
println!("start net task");
|
||||
spawner.spawn(net_task(runner)?);
|
||||
println!("run dhcp");
|
||||
spawner.spawn(run_dhcp(*stack, gw_ip_addr)?);
|
||||
|
||||
loop {
|
||||
if stack.is_link_up() {
|
||||
break;
|
||||
}
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
while !stack.is_config_up() {
|
||||
Timer::after(Duration::from_millis(100)).await
|
||||
}
|
||||
println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr}/");
|
||||
stack
|
||||
.config_v4()
|
||||
.inspect(|c| println!("ipv4 config: {c:?}"));
|
||||
|
||||
Ok(*stack)
|
||||
}
|
||||
|
||||
pub async fn wifi(
|
||||
network_config: &NetworkConfig,
|
||||
interface_sta: Interface<'static>,
|
||||
controller: &Arc<Mutex<CriticalSectionRawMutex, esp_radio::wifi::WifiController<'static>>>,
|
||||
rng: &mut Rng,
|
||||
spawner: Spawner,
|
||||
) -> FatResult<Stack<'static>> {
|
||||
esp_radio::wifi_set_log_verbose();
|
||||
let ssid = match &network_config.ssid {
|
||||
Some(ssid) => {
|
||||
if ssid.is_empty() {
|
||||
bail!("Wifi ssid was empty")
|
||||
}
|
||||
ssid.as_str().to_string()
|
||||
}
|
||||
None => {
|
||||
bail!("Wifi ssid was empty")
|
||||
}
|
||||
};
|
||||
let password = match network_config.password {
|
||||
Some(ref password) => password.as_str().to_string(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
let max_wait = network_config.max_wait;
|
||||
let retry_count = network_config.retry_count;
|
||||
|
||||
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
|
||||
|
||||
let seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
||||
|
||||
let (stack, runner) = embassy_net::new(
|
||||
interface_sta,
|
||||
config,
|
||||
mk_static!(StackResources<8>, StackResources::<8>::new()),
|
||||
seed,
|
||||
);
|
||||
let stack = mk_static!(Stack, stack);
|
||||
|
||||
let auth_method = if password.is_empty() {
|
||||
AuthenticationMethod::None
|
||||
} else {
|
||||
AuthenticationMethod::Wpa2Personal
|
||||
};
|
||||
|
||||
// Spawn the network task once
|
||||
spawner.spawn(net_task(runner)?);
|
||||
|
||||
let mut attempts = 0;
|
||||
|
||||
while attempts <= retry_count {
|
||||
if attempts > 0 {
|
||||
info!("WiFi connection retry {}/{}", attempts, retry_count);
|
||||
} else {
|
||||
info!("attempting to connect wifi {}", ssid);
|
||||
}
|
||||
|
||||
let client_config = StationConfig::default()
|
||||
.with_ssid(ssid.clone())
|
||||
.with_auth_method(auth_method)
|
||||
.with_scan_method(esp_radio::wifi::sta::ScanMethod::AllChannels)
|
||||
.with_listen_interval(10)
|
||||
.with_beacon_timeout(10)
|
||||
.with_failure_retry_cnt(3)
|
||||
.with_password(password.clone());
|
||||
|
||||
// Set config and attempt connection
|
||||
controller
|
||||
.lock()
|
||||
.await
|
||||
.set_config(&Config::Station(client_config))?;
|
||||
|
||||
match controller
|
||||
.lock()
|
||||
.await
|
||||
.connect_async()
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
result?;
|
||||
}
|
||||
Err(e) => {
|
||||
let disconnect_info = controller.lock().await.disconnect_async().await;
|
||||
warn!("Wifi disconnect info {:?}", disconnect_info);
|
||||
warn!("WiFi connection attempt {} failed: Timeout waiting for wifi sta connected: {:?}", attempts + 1, e);
|
||||
attempts += 1;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let res = async {
|
||||
while !stack.is_link_up() {
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
warn!(
|
||||
"WiFi connection attempt {} failed: link up timeout",
|
||||
attempts + 1
|
||||
);
|
||||
attempts += 1;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let res = async {
|
||||
while !stack.is_config_up() {
|
||||
Timer::after(Duration::from_millis(100)).await
|
||||
}
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
warn!(
|
||||
"WiFi connection attempt {} failed: config up timeout",
|
||||
attempts + 1
|
||||
);
|
||||
attempts += 1;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Success!
|
||||
info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
|
||||
return Ok(*stack);
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
bail!("WiFi connection failed after all retries");
|
||||
}
|
||||
|
||||
pub async fn try_connect_wifi_sntp_mqtt(
|
||||
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
|
||||
stack_store: &mut OptionLock<Stack<'static>>,
|
||||
spawner: Spawner,
|
||||
) -> NetworkMode {
|
||||
let nw_conf = &board.board_hal.get_config().network.clone();
|
||||
let esp = board.board_hal.get_esp();
|
||||
let device = match esp.interface_sta.take() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
info!("Offline mode due to STA interface already taken");
|
||||
board.board_hal.general_fault(true).await;
|
||||
return NetworkMode::OFFLINE;
|
||||
}
|
||||
};
|
||||
match wifi(nw_conf, device, &esp.controller, &mut esp.rng, spawner).await {
|
||||
Ok(stack) => {
|
||||
stack_store.replace(stack);
|
||||
|
||||
let sntp_mode: SntpMode = match sntp(1000 * 10, stack).await {
|
||||
Ok(new_time) => {
|
||||
info!("Using time from sntp {}", new_time.to_rfc3339());
|
||||
let _ = board
|
||||
.board_hal
|
||||
.get_rtc_module()
|
||||
.set_rtc_time(&new_time)
|
||||
.await;
|
||||
SntpMode::SYNC { current: new_time }
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("sntp error: {err}");
|
||||
board.board_hal.general_fault(true).await;
|
||||
SntpMode::OFFLINE
|
||||
}
|
||||
};
|
||||
|
||||
let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() {
|
||||
let nw_config = board.board_hal.get_config().network.clone();
|
||||
let nw_config = mk_static!(NetworkConfig, nw_config);
|
||||
match mqtt::mqtt_init(nw_config, stack, spawner).await {
|
||||
Ok(_) => {
|
||||
info!("Mqtt connection ready");
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Could not connect mqtt due to {err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let ip = match stack.config_v4() {
|
||||
Some(config) => config.address.address().to_string(),
|
||||
None => match stack.config_v6() {
|
||||
Some(config) => config.address.address().to_string(),
|
||||
None => String::from("No IP"),
|
||||
},
|
||||
};
|
||||
NetworkMode::WIFI {
|
||||
sntp: sntp_mode,
|
||||
mqtt: mqtt_connected,
|
||||
ip_address: ip,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Offline mode due to {err}");
|
||||
board.board_hal.general_fault(true).await;
|
||||
NetworkMode::OFFLINE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
use crate::config::SensorCombineMode;
|
||||
use crate::hal::Moistures;
|
||||
use crate::plant_state::PlantWateringMode::TargetMoisture;
|
||||
use crate::{config::PlantConfig, hal::HAL, in_time_range};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 60kHz (500Hz margin)
|
||||
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, think like cactus levels
|
||||
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 160000.; // 160kHz -> very wet
|
||||
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 400.; // this is really, really dry, think like cactus levels
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum MoistureSensorError {
|
||||
MissingMessage,
|
||||
NotExpectedMessage { hz: f32 },
|
||||
ShortCircuit { hz: f32, max: f32 },
|
||||
OpenLoop { hz: f32, min: f32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub enum MoistureSensorState {
|
||||
Disabled,
|
||||
MoistureValue { raw_hz: f32, moisture_percent: f32 },
|
||||
MoistureValue { hz: f32, moisture_percent: f32 },
|
||||
NoMessage,
|
||||
SensorError(MoistureSensorError),
|
||||
}
|
||||
|
||||
@@ -30,7 +35,7 @@ impl MoistureSensorState {
|
||||
|
||||
pub fn moisture_percent(&self) -> Option<f32> {
|
||||
if let MoistureSensorState::MoistureValue {
|
||||
raw_hz: _,
|
||||
hz: _,
|
||||
moisture_percent,
|
||||
} = self
|
||||
{
|
||||
@@ -44,21 +49,40 @@ impl MoistureSensorState {
|
||||
impl MoistureSensorState {}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct SensorTelemetry {
|
||||
pub moisture_pct: Option<f32>,
|
||||
pub raw_hz: Option<f32>,
|
||||
pub error: Option<MoistureSensorError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum PumpError {
|
||||
PumpNotWorking {
|
||||
failed_attempts: usize,
|
||||
max_allowed_failures: usize,
|
||||
},
|
||||
OverCurrent {
|
||||
current_ma: u16,
|
||||
max_allowed_ma: u16,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PumpState {
|
||||
consecutive_pump_count: u32,
|
||||
previous_pump: Option<DateTime<Utc>>,
|
||||
pub overcurrent_error: Option<u16>,
|
||||
}
|
||||
|
||||
impl PumpState {
|
||||
fn is_err(&self, plant_config: &PlantConfig) -> Option<PumpError> {
|
||||
if let Some(current_ma) = self.overcurrent_error {
|
||||
return Some(PumpError::OverCurrent {
|
||||
current_ma,
|
||||
max_allowed_ma: plant_config.max_pump_current_ma,
|
||||
});
|
||||
}
|
||||
if self.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 {
|
||||
Some(PumpError::PumpNotWorking {
|
||||
failed_attempts: self.consecutive_pump_count as usize,
|
||||
@@ -82,8 +106,36 @@ pub struct PlantState {
|
||||
pub sensor_a: MoistureSensorState,
|
||||
pub sensor_b: MoistureSensorState,
|
||||
pub pump: PumpState,
|
||||
/// Last known firmware build timestamp for sensor A (minutes since Unix epoch).
|
||||
/// Set during sensor detection; None if detection has not been run yet.
|
||||
pub sensor_a_firmware_build_minutes: Option<u32>,
|
||||
/// Last known firmware build timestamp for sensor B.
|
||||
pub sensor_b_firmware_build_minutes: Option<u32>,
|
||||
/// Last time fertilizer was applied.
|
||||
pub last_fertilizer_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Map sensor frequency to moisture percentage using inverse power-law scaling (quadratic).
|
||||
///
|
||||
/// For resistive probes with 555 timer oscillator:
|
||||
/// - Dry soil has high resistance → low oscillation frequency
|
||||
/// - Wet soil has low resistance → high oscillation frequency
|
||||
///
|
||||
/// The relationship is non-linear: most frequency change occurs in the wet range.
|
||||
/// Using inverse power-law to give better discrimination at high moisture levels.
|
||||
///
|
||||
/// Formula: moisture = (1 - (f_max - f) / (f_max - f_min))^2 * 100
|
||||
/// = ((f - f_min) / (f_max - f_min))^2 * 100
|
||||
///
|
||||
/// But with k=0.5 (square root) for better high-end discrimination:
|
||||
/// Formula: moisture = sqrt((f - f_min) / (f_max - f_min)) * 100
|
||||
///
|
||||
/// Examples with default range (400-160000 Hz) using k=0.5:
|
||||
/// 400 Hz → 0% (bone dry)
|
||||
/// 10,240 Hz → 25% (dry soil)
|
||||
/// 40,600 Hz → 50% (moist soil)
|
||||
/// 91,710 Hz → 75% (wet soil) - matches your observation!
|
||||
/// 160,000 Hz → 100% (saturated)
|
||||
fn map_range_moisture(
|
||||
s: f32,
|
||||
min_frequency: Option<f32>,
|
||||
@@ -105,70 +157,107 @@ fn map_range_moisture(
|
||||
max: max_freq,
|
||||
});
|
||||
}
|
||||
let moisture_percent = (s - min_freq) * 100.0 / (max_freq - min_freq);
|
||||
|
||||
Ok(moisture_percent)
|
||||
// Normalize to 0-1 range
|
||||
let t = (s - min_freq) / (max_freq - min_freq);
|
||||
|
||||
// Apply power-law mapping with k=0.5 (square root) for better high-moisture discrimination
|
||||
// For resistive probes: frequency ↑ as moisture ↑, but non-linearly
|
||||
// Using sqrt gives more resolution in the wet range (60-160kHz)
|
||||
// Newton's method approximation for sqrt(t): x_{n+1} = 0.5 * (x_n + t/x_n)
|
||||
// Start with initial guess and do 2 iterations for good precision
|
||||
let moisture_percent = if t <= 0.0 {
|
||||
0.0
|
||||
} else if t >= 1.0 {
|
||||
100.0
|
||||
} else {
|
||||
// Newton's method for sqrt(t)
|
||||
let mut x = t; // Initial guess
|
||||
x = 0.5 * (x + t / x); // First iteration
|
||||
x = 0.5 * (x + t / x); // Second iteration for better precision
|
||||
x * 100.0
|
||||
};
|
||||
|
||||
Ok(moisture_percent.clamp(0.0, 100.0))
|
||||
}
|
||||
|
||||
impl PlantState {
|
||||
pub async fn read_hardware_state(
|
||||
pub async fn interpret_raw_values(
|
||||
moistures: Moistures,
|
||||
plant_id: usize,
|
||||
board: &mut HAL<'_>,
|
||||
) -> Self {
|
||||
let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
|
||||
let raw = moistures.sensor_a_hz[plant_id];
|
||||
match map_range_moisture(
|
||||
raw,
|
||||
board.board_hal.get_config().plants[plant_id]
|
||||
.moisture_sensor_min_frequency
|
||||
.map(|a| a as f32),
|
||||
board.board_hal.get_config().plants[plant_id]
|
||||
.moisture_sensor_max_frequency
|
||||
.map(|b| b as f32),
|
||||
) {
|
||||
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
|
||||
raw_hz: raw,
|
||||
moisture_percent,
|
||||
},
|
||||
Err(err) => MoistureSensorState::SensorError(err),
|
||||
let min = board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency;
|
||||
let max = board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency;
|
||||
|
||||
let raw_to_value = |raw: Option<f32>, expected: bool| -> MoistureSensorState {
|
||||
match raw {
|
||||
None => {
|
||||
if expected {
|
||||
MoistureSensorState::SensorError(MoistureSensorError::MissingMessage)
|
||||
} else {
|
||||
MoistureSensorState::NoMessage
|
||||
}
|
||||
}
|
||||
Some(raw) => {
|
||||
if expected {
|
||||
match map_range_moisture(raw, min.map(|a| a as f32), max.map(|b| b as f32))
|
||||
{
|
||||
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
|
||||
hz: raw,
|
||||
moisture_percent,
|
||||
},
|
||||
Err(err) => MoistureSensorState::SensorError(err),
|
||||
}
|
||||
} else {
|
||||
MoistureSensorState::SensorError(MoistureSensorError::NotExpectedMessage {
|
||||
hz: raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MoistureSensorState::Disabled
|
||||
};
|
||||
|
||||
let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b {
|
||||
let raw = moistures.sensor_b_hz[plant_id];
|
||||
match map_range_moisture(
|
||||
raw,
|
||||
board.board_hal.get_config().plants[plant_id]
|
||||
.moisture_sensor_min_frequency
|
||||
.map(|a| a as f32),
|
||||
board.board_hal.get_config().plants[plant_id]
|
||||
.moisture_sensor_max_frequency
|
||||
.map(|b| b as f32),
|
||||
) {
|
||||
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
|
||||
raw_hz: raw,
|
||||
moisture_percent,
|
||||
},
|
||||
Err(err) => MoistureSensorState::SensorError(err),
|
||||
}
|
||||
} else {
|
||||
MoistureSensorState::Disabled
|
||||
};
|
||||
let expected_a = board.board_hal.get_config().plants[plant_id].sensor_a;
|
||||
let expected_b = board.board_hal.get_config().plants[plant_id].sensor_b;
|
||||
|
||||
let sensor_a = { raw_to_value(moistures.sensor_a_hz[plant_id], expected_a) };
|
||||
let sensor_b = { raw_to_value(moistures.sensor_b_hz[plant_id], expected_b) };
|
||||
|
||||
let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id);
|
||||
let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id);
|
||||
let last_fertilizer_timestamp = board.board_hal.get_esp().last_fertilizer_time(plant_id);
|
||||
let (a_builds, b_builds) = board.board_hal.get_sensor_build_minutes();
|
||||
|
||||
let last_fertilizer_time = DateTime::from_timestamp_millis(last_fertilizer_timestamp);
|
||||
|
||||
// Create plant state first, then check for warnings
|
||||
let state = Self {
|
||||
sensor_a,
|
||||
sensor_b,
|
||||
pump: PumpState {
|
||||
consecutive_pump_count,
|
||||
previous_pump,
|
||||
overcurrent_error: None,
|
||||
},
|
||||
sensor_a_firmware_build_minutes: a_builds[plant_id],
|
||||
sensor_b_firmware_build_minutes: b_builds[plant_id],
|
||||
last_fertilizer_time,
|
||||
};
|
||||
if state.is_err() {
|
||||
|
||||
// Check for sensor warning condition (expected 2 sensors, only 1 responding)
|
||||
let has_a =
|
||||
state.sensor_a.moisture_percent().is_some() && state.sensor_a.is_err().is_none();
|
||||
let has_b =
|
||||
state.sensor_b.moisture_percent().is_some() && state.sensor_b.is_err().is_none();
|
||||
|
||||
// Check if we expected two sensors but only got one
|
||||
let has_sensor_warning =
|
||||
expected_a && expected_b && ((has_a && !has_b) || (!has_a && has_b));
|
||||
|
||||
// Set fault LED for both errors AND sensor warnings
|
||||
let has_issue = state.is_err() || has_sensor_warning;
|
||||
if has_issue {
|
||||
let _ = board.board_hal.fault(plant_id, true).await;
|
||||
}
|
||||
state
|
||||
@@ -191,26 +280,25 @@ impl PlantState {
|
||||
self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some()
|
||||
}
|
||||
|
||||
pub fn plant_moisture(
|
||||
&self,
|
||||
) -> (
|
||||
Option<u8>,
|
||||
(Option<&MoistureSensorError>, Option<&MoistureSensorError>),
|
||||
) {
|
||||
/// Get combined moisture value with configurable combination mode and sensor warning.
|
||||
///
|
||||
/// Returns:
|
||||
/// - Combined moisture percentage (or None if no valid readings)
|
||||
/// - Tuple of errors from sensor A and B
|
||||
/// - Sensor warning indicating if warning LED should be lit (MissingSecondSensor)
|
||||
pub fn plant_moisture_with_warning(&self, plant_conf: &PlantConfig) -> Option<f32> {
|
||||
match (
|
||||
self.sensor_a.moisture_percent(),
|
||||
self.sensor_b.moisture_percent(),
|
||||
) {
|
||||
(Some(moisture_a), Some(moisture_b)) => {
|
||||
(Some(((moisture_a + moisture_b) / 2.) as u8), (None, None))
|
||||
}
|
||||
(Some(moisture_percent), _) => {
|
||||
(Some(moisture_percent as u8), (None, self.sensor_b.is_err()))
|
||||
}
|
||||
(_, Some(moisture_percent)) => {
|
||||
(Some(moisture_percent as u8), (self.sensor_a.is_err(), None))
|
||||
}
|
||||
_ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())),
|
||||
(Some(moisture_a), Some(moisture_b)) => match plant_conf.sensor_combine_mode {
|
||||
SensorCombineMode::Min => Some(moisture_a.min(moisture_b)),
|
||||
SensorCombineMode::Max => Some(moisture_a.max(moisture_b)),
|
||||
SensorCombineMode::Avg => Some((moisture_a + moisture_b) / 2.0),
|
||||
},
|
||||
(Some(moisture), _) => Some(moisture),
|
||||
(_, Some(moisture)) => Some(moisture),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +310,11 @@ impl PlantState {
|
||||
match plant_conf.mode {
|
||||
PlantWateringMode::Off => false,
|
||||
PlantWateringMode::TargetMoisture => {
|
||||
let (moisture_percent, _) = self.plant_moisture();
|
||||
let moisture_percent = self.plant_moisture_with_warning(plant_conf);
|
||||
if let Some(moisture_percent) = moisture_percent {
|
||||
if self.pump_in_timeout(plant_conf, current_time) {
|
||||
false
|
||||
} else if moisture_percent < plant_conf.target_moisture {
|
||||
} else if moisture_percent < plant_conf.target_moisture.into() {
|
||||
in_time_range(
|
||||
current_time,
|
||||
plant_conf.pump_hour_start,
|
||||
@@ -248,23 +336,26 @@ impl PlantState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mqtt_info(
|
||||
&self,
|
||||
plant_conf: &PlantConfig,
|
||||
current_time: &DateTime<Tz>,
|
||||
) -> PlantInfo<'_> {
|
||||
pub fn to_mqtt_info(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> PlantInfo {
|
||||
let moisture_pct = self.plant_moisture_with_warning(plant_conf);
|
||||
PlantInfo {
|
||||
sensor_a: &self.sensor_a,
|
||||
sensor_b: &self.sensor_b,
|
||||
moisture_pct,
|
||||
sensor_a: Self::sensor_to_telemetry(&self.sensor_a),
|
||||
sensor_b: Self::sensor_to_telemetry(&self.sensor_b),
|
||||
mode: plant_conf.mode,
|
||||
target_pct: if plant_conf.mode == TargetMoisture {
|
||||
Some(plant_conf.target_moisture as f32)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
do_water: self.needs_to_be_watered(plant_conf, current_time),
|
||||
dry: if let Some(moisture_percent) = self.plant_moisture().0 {
|
||||
moisture_percent < plant_conf.target_moisture
|
||||
dry: if let Some(moisture_percent) = moisture_pct {
|
||||
moisture_percent < plant_conf.target_moisture.into()
|
||||
} else {
|
||||
false
|
||||
},
|
||||
cooldown: self.pump_in_timeout(plant_conf, current_time),
|
||||
out_of_work_hour: in_time_range(
|
||||
out_of_work_hour: !in_time_range(
|
||||
current_time,
|
||||
plant_conf.pump_hour_start,
|
||||
plant_conf.pump_hour_end,
|
||||
@@ -289,17 +380,67 @@ impl PlantState {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
last_fertilizer: self
|
||||
.last_fertilizer_time
|
||||
.map(|t| t.with_timezone(¤t_time.timezone())),
|
||||
next_fertilizer: if matches!(
|
||||
plant_conf.mode,
|
||||
PlantWateringMode::TimerOnly
|
||||
| PlantWateringMode::TargetMoisture
|
||||
| PlantWateringMode::MinMoisture
|
||||
) {
|
||||
self.last_fertilizer_time.and_then(|last_fert| {
|
||||
// Convert to Tz for calculation, then back
|
||||
let tz_last_fert = last_fert.with_timezone(¤t_time.timezone());
|
||||
tz_last_fert
|
||||
.checked_add_signed(TimeDelta::minutes(
|
||||
plant_conf.fertilizer_cooldown_min.into(),
|
||||
))
|
||||
.map(|t| t.with_timezone(¤t_time.timezone()))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
sensor_a_firmware_build_minutes: self.sensor_a_firmware_build_minutes,
|
||||
sensor_b_firmware_build_minutes: self.sensor_b_firmware_build_minutes,
|
||||
}
|
||||
}
|
||||
|
||||
fn sensor_to_telemetry(sensor: &MoistureSensorState) -> SensorTelemetry {
|
||||
match sensor {
|
||||
MoistureSensorState::NoMessage => SensorTelemetry {
|
||||
moisture_pct: None,
|
||||
raw_hz: None,
|
||||
error: None,
|
||||
},
|
||||
MoistureSensorState::MoistureValue {
|
||||
hz,
|
||||
moisture_percent,
|
||||
} => SensorTelemetry {
|
||||
moisture_pct: Some(*moisture_percent),
|
||||
raw_hz: Some(*hz),
|
||||
error: None,
|
||||
},
|
||||
MoistureSensorState::SensorError(err) => SensorTelemetry {
|
||||
moisture_pct: None,
|
||||
raw_hz: None,
|
||||
error: Some(err.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
/// State of a single plant to be tracked
|
||||
pub struct PlantInfo<'a> {
|
||||
pub struct PlantInfo {
|
||||
/// combined plant moisture from available sensors
|
||||
moisture_pct: Option<f32>,
|
||||
/// moisture target, if in targetmode
|
||||
target_pct: Option<f32>,
|
||||
/// state of humidity sensor on bank a
|
||||
sensor_a: &'a MoistureSensorState,
|
||||
sensor_a: SensorTelemetry,
|
||||
/// state of humidity sensor on bank b
|
||||
sensor_b: &'a MoistureSensorState,
|
||||
sensor_b: SensorTelemetry,
|
||||
/// configured plant watering mode
|
||||
mode: PlantWateringMode,
|
||||
/// the plant needs to be watered
|
||||
@@ -317,4 +458,12 @@ pub struct PlantInfo<'a> {
|
||||
last_pump: Option<DateTime<Tz>>,
|
||||
/// next time when pump should activate
|
||||
next_pump: Option<DateTime<Tz>>,
|
||||
/// last time when fertilizer was applied
|
||||
last_fertilizer: Option<DateTime<Tz>>,
|
||||
/// next time when fertilizer should be applied
|
||||
next_fertilizer: Option<DateTime<Tz>>,
|
||||
/// firmware build timestamp of sensor A (minutes since Unix epoch); None if unknown
|
||||
sensor_a_firmware_build_minutes: Option<u32>,
|
||||
/// firmware build timestamp of sensor B (minutes since Unix epoch); None if unknown
|
||||
sensor_b_firmware_build_minutes: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ const OPEN_TANK_VOLTAGE: f32 = 3.0;
|
||||
pub const WATER_FROZEN_THRESH: f32 = 4.0;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum TankError {
|
||||
SensorDisabled,
|
||||
SensorMissing(f32),
|
||||
SensorMissing { raw_mv: f32 },
|
||||
SensorValueError { value: f32, min: f32, max: f32 },
|
||||
BoardError(String),
|
||||
BoardError { message: String },
|
||||
}
|
||||
|
||||
pub enum TankState {
|
||||
@@ -25,7 +26,9 @@ pub enum TankState {
|
||||
|
||||
fn raw_voltage_to_divider_percent(raw_value_mv: f32) -> Result<f32, TankError> {
|
||||
if raw_value_mv > OPEN_TANK_VOLTAGE {
|
||||
return Err(TankError::SensorMissing(raw_value_mv));
|
||||
return Err(TankError::SensorMissing {
|
||||
raw_mv: raw_value_mv,
|
||||
});
|
||||
}
|
||||
|
||||
let r2 = raw_value_mv * 50.0 / (3.3 - raw_value_mv);
|
||||
@@ -141,15 +144,15 @@ impl TankState {
|
||||
TankInfo {
|
||||
enough_water,
|
||||
warn_level,
|
||||
left_ml,
|
||||
volume_ml: left_ml,
|
||||
sensor_error: tank_err,
|
||||
raw,
|
||||
fill_raw_v: raw,
|
||||
water_frozen: water_temp
|
||||
.as_ref()
|
||||
.is_ok_and(|temp| *temp < WATER_FROZEN_THRESH),
|
||||
water_temp: water_temp.as_ref().copied().ok(),
|
||||
water_temp_c: water_temp.as_ref().copied().ok(),
|
||||
temp_sensor_error: water_temp.as_ref().err().map(|err| err.to_string()),
|
||||
percent,
|
||||
fill_pct: percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,10 +164,13 @@ pub async fn determine_tank_state(
|
||||
match board
|
||||
.board_hal
|
||||
.get_tank_sensor()
|
||||
.map(|f| f.tank_sensor_voltage())
|
||||
.tank_sensor_voltage()
|
||||
.await
|
||||
{
|
||||
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv.await.unwrap()),
|
||||
Err(err) => TankState::Error(TankError::BoardError(err.to_string())),
|
||||
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv),
|
||||
Err(err) => TankState::Error(TankError::BoardError {
|
||||
message: err.to_string(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
TankState::Disabled
|
||||
@@ -179,16 +185,16 @@ pub struct TankInfo {
|
||||
/// warning that water needs to be refilled soon
|
||||
pub(crate) warn_level: bool,
|
||||
/// estimation how many ml are still in the tank
|
||||
pub(crate) left_ml: Option<f32>,
|
||||
pub(crate) volume_ml: Option<f32>,
|
||||
/// if there is an issue with the water level sensor
|
||||
pub(crate) sensor_error: Option<TankError>,
|
||||
/// raw water sensor value
|
||||
pub(crate) raw: Option<f32>,
|
||||
pub(crate) fill_raw_v: Option<f32>,
|
||||
/// percent value
|
||||
pub(crate) percent: Option<f32>,
|
||||
pub(crate) fill_pct: Option<f32>,
|
||||
/// water in the tank might be frozen
|
||||
pub(crate) water_frozen: bool,
|
||||
/// water temperature
|
||||
pub(crate) water_temp: Option<f32>,
|
||||
pub(crate) water_temp_c: Option<f32>,
|
||||
pub(crate) temp_sensor_error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
macro_rules! mk_static {
|
||||
($t:ty,$val:expr) => {{
|
||||
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
|
||||
#[deny(unused_attributes)]
|
||||
let x = STATIC_CELL.uninit().write($val);
|
||||
x
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use mk_static;
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::rtc::X25;
|
||||
use crate::hal::savegame_manager::SAVEGAME_SLOT_SIZE;
|
||||
use crate::webserver::read_up_to_bytes_from_request;
|
||||
use crate::BOARD_ACCESS;
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use chrono::DateTime;
|
||||
use edge_http::io::server::Connection;
|
||||
@@ -21,48 +21,9 @@ pub(crate) async fn get_backup_config<T, const N: usize>(
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
// First pass: verify checksum without sending data
|
||||
let mut checksum = X25.digest();
|
||||
let mut chunk = 0_usize;
|
||||
loop {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(chunk as u32).await;
|
||||
let (buf, len, expected_crc) = board
|
||||
.board_hal
|
||||
.get_rtc_module()
|
||||
.get_backup_config(chunk)
|
||||
.await?;
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let backup = board.board_hal.read_backup().await?;
|
||||
|
||||
// Update checksum with the actual data bytes of this chunk
|
||||
checksum.update(&buf[..len]);
|
||||
|
||||
let is_last = len == 0 || len < buf.len();
|
||||
if is_last {
|
||||
let actual_crc = checksum.finalize();
|
||||
if actual_crc != expected_crc {
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.clear_progress()
|
||||
.await;
|
||||
conn.initiate_response(
|
||||
409,
|
||||
Some(
|
||||
format!("Checksum mismatch expected {expected_crc} got {actual_crc}")
|
||||
.as_str(),
|
||||
),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
return Ok(Some(409));
|
||||
}
|
||||
break;
|
||||
}
|
||||
chunk += 1;
|
||||
}
|
||||
// Second pass: stream data
|
||||
conn.initiate_response(
|
||||
200,
|
||||
@@ -75,35 +36,8 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut chunk = 0_usize;
|
||||
loop {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(chunk as u32).await;
|
||||
let (buf, len, _expected_crc) = board
|
||||
.board_hal
|
||||
.get_rtc_module()
|
||||
.get_backup_config(chunk)
|
||||
.await?;
|
||||
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
conn.write_all(&buf[..len]).await?;
|
||||
|
||||
if len < buf.len() {
|
||||
break;
|
||||
}
|
||||
chunk += 1;
|
||||
}
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.clear_progress()
|
||||
.await;
|
||||
conn.write_all(serde_json::to_string(&backup)?.as_bytes())
|
||||
.await?;
|
||||
Ok(Some(200))
|
||||
}
|
||||
|
||||
@@ -113,49 +47,12 @@ pub(crate) async fn backup_config<T, const N: usize>(
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let mut offset = 0_usize;
|
||||
let mut buf = [0_u8; 32];
|
||||
|
||||
let mut checksum = X25.digest();
|
||||
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
let to_write = conn.read(&mut buf).await?;
|
||||
if to_write == 0 {
|
||||
info!("backup finished");
|
||||
break;
|
||||
} else {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(counter).await;
|
||||
|
||||
counter += 1;
|
||||
board
|
||||
.board_hal
|
||||
.get_rtc_module()
|
||||
.backup_config(offset, &buf[0..to_write])
|
||||
.await?;
|
||||
checksum.update(&buf[0..to_write]);
|
||||
}
|
||||
offset += to_write;
|
||||
}
|
||||
|
||||
let input = read_up_to_bytes_from_request(conn, Some(SAVEGAME_SLOT_SIZE)).await?;
|
||||
info!("Read input with length {}", input.len());
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board
|
||||
.board_hal
|
||||
.get_rtc_module()
|
||||
.backup_config_finalize(checksum.finalize(), offset)
|
||||
.await?;
|
||||
board.board_hal.clear_progress().await;
|
||||
conn.initiate_response(
|
||||
200,
|
||||
Some("OK"),
|
||||
&[
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Headers", "*"),
|
||||
("Access-Control-Allow-Methods", "*"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let config_to_backup = serde_json::from_slice(&input)?;
|
||||
info!("Parsed send config to object");
|
||||
board.board_hal.backup_config(&config_to_backup).await?;
|
||||
Ok(Some("saved".to_owned()))
|
||||
}
|
||||
|
||||
@@ -166,10 +63,12 @@ where
|
||||
T: Read + Write,
|
||||
{
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let header = board.board_hal.get_rtc_module().get_backup_info().await;
|
||||
let json = match header {
|
||||
let info = board.board_hal.backup_info().await;
|
||||
|
||||
let json = match info {
|
||||
Ok(h) => {
|
||||
let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap();
|
||||
info!("Got backup info: {:?}", h);
|
||||
let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap_or_default();
|
||||
let wbh = WebBackupHeader {
|
||||
timestamp: timestamp.to_rfc3339(),
|
||||
size: h.size,
|
||||
@@ -177,6 +76,7 @@ where
|
||||
serde_json::to_string(&wbh)?
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Error getting backup info: {:?}", err);
|
||||
let wbh = WebBackupHeader {
|
||||
timestamp: err.to_string(),
|
||||
size: 0,
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::webserver::read_up_to_bytes_from_request;
|
||||
use crate::BOARD_ACCESS;
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use edge_http::io::server::Connection;
|
||||
use edge_http::Method;
|
||||
use edge_nal::io::{Read, Write};
|
||||
use log::info;
|
||||
|
||||
pub(crate) async fn list_files<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let result = board.board_hal.get_esp().list_files().await?;
|
||||
let file_list_json = serde_json::to_string(&result)?;
|
||||
Ok(Some(file_list_json))
|
||||
}
|
||||
pub(crate) async fn file_operations<T, const N: usize>(
|
||||
conn: &mut Connection<'_, T, { N }>,
|
||||
method: Method,
|
||||
path: &&str,
|
||||
prefix: &&str,
|
||||
) -> Result<Option<u32>, FatError>
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let filename = &path[prefix.len()..];
|
||||
info!("file request for {filename} with method {method}");
|
||||
Ok(match method {
|
||||
Method::Delete => {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.delete_file(filename.to_owned())
|
||||
.await?;
|
||||
conn.initiate_response(
|
||||
200,
|
||||
Some("OK"),
|
||||
&[
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Headers", "*"),
|
||||
("Access-Control-Allow-Methods", "*"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Some(200)
|
||||
}
|
||||
Method::Get => {
|
||||
let disposition = format!("attachment; filename=\"{filename}\"");
|
||||
let size = {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.get_size(filename.to_owned())
|
||||
.await?
|
||||
};
|
||||
|
||||
conn.initiate_response(
|
||||
200,
|
||||
Some("OK"),
|
||||
&[
|
||||
("Content-Type", "application/octet-stream"),
|
||||
("Content-Disposition", disposition.as_str()),
|
||||
("Content-Length", &format!("{size}")),
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Headers", "*"),
|
||||
("Access-Control-Allow-Methods", "*"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut chunk = 0;
|
||||
loop {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(chunk).await;
|
||||
let read_chunk = board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.get_file(filename.to_owned(), chunk)
|
||||
.await?;
|
||||
let length = read_chunk.1;
|
||||
if length == 0 {
|
||||
info!("file request for {filename} finished");
|
||||
break;
|
||||
}
|
||||
let data = &read_chunk.0[0..length];
|
||||
conn.write_all(data).await?;
|
||||
if length < read_chunk.0.len() {
|
||||
info!("file request for {filename} finished");
|
||||
break;
|
||||
}
|
||||
chunk += 1;
|
||||
}
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.clear_progress()
|
||||
.await;
|
||||
Some(200)
|
||||
}
|
||||
Method::Post => {
|
||||
{
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
//ensure the file is deleted first; otherwise we would need to truncate the file which will not work with streaming
|
||||
let _ = board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.delete_file(filename.to_owned())
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut offset = 0_usize;
|
||||
let mut chunk = 0;
|
||||
loop {
|
||||
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
|
||||
if buf.is_empty() {
|
||||
info!("file request for {filename} finished");
|
||||
break;
|
||||
} else {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(chunk as u32).await;
|
||||
board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.write_file(filename.to_owned(), offset as u32, &buf)
|
||||
.await?;
|
||||
}
|
||||
offset += buf.len();
|
||||
chunk += 1;
|
||||
}
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.clear_progress()
|
||||
.await;
|
||||
conn.initiate_response(
|
||||
200,
|
||||
Some("OK"),
|
||||
&[
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Headers", "*"),
|
||||
("Access-Control-Allow-Methods", "*"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Some(200)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::{esp_time, PLANT_COUNT};
|
||||
use crate::hal::PLANT_COUNT;
|
||||
use crate::log::LogMessage;
|
||||
use crate::plant_state::{MoistureSensorState, PlantState};
|
||||
use crate::tank::determine_tank_state;
|
||||
@@ -23,6 +23,8 @@ struct LoadData<'a> {
|
||||
struct Moistures {
|
||||
moisture_a: Vec<String>,
|
||||
moisture_b: Vec<String>,
|
||||
sensor_a_build_minutes: Vec<Option<u32>>,
|
||||
sensor_b_build_minutes: Vec<Option<u32>>,
|
||||
}
|
||||
#[derive(Serialize, Debug)]
|
||||
struct SolarState {
|
||||
@@ -40,32 +42,43 @@ where
|
||||
let moistures = board.board_hal.measure_moisture_hz().await?;
|
||||
let mut plant_state = Vec::new();
|
||||
for i in 0..PLANT_COUNT {
|
||||
plant_state.push(PlantState::read_hardware_state(moistures, i, &mut board).await);
|
||||
plant_state.push(PlantState::interpret_raw_values(moistures, i, &mut board).await);
|
||||
}
|
||||
let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a {
|
||||
MoistureSensorState::Disabled => "disabled".to_string(),
|
||||
MoistureSensorState::MoistureValue {
|
||||
raw_hz,
|
||||
hz: raw_hz,
|
||||
moisture_percent,
|
||||
} => {
|
||||
format!("{moisture_percent:.2}% {raw_hz}hz",)
|
||||
}
|
||||
MoistureSensorState::SensorError(err) => format!("{err:?}"),
|
||||
MoistureSensorState::NoMessage => "No Message".to_string(),
|
||||
}));
|
||||
let b = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_b {
|
||||
MoistureSensorState::Disabled => "disabled".to_string(),
|
||||
MoistureSensorState::MoistureValue {
|
||||
raw_hz,
|
||||
hz: raw_hz,
|
||||
moisture_percent,
|
||||
} => {
|
||||
format!("{moisture_percent:.2}% {raw_hz}hz",)
|
||||
}
|
||||
MoistureSensorState::SensorError(err) => format!("{err:?}"),
|
||||
MoistureSensorState::NoMessage => "No Message".to_string(),
|
||||
}));
|
||||
|
||||
let sensor_a_build_minutes: Vec<Option<u32>> = plant_state
|
||||
.iter()
|
||||
.map(|s| s.sensor_a_firmware_build_minutes)
|
||||
.collect();
|
||||
let sensor_b_build_minutes: Vec<Option<u32>> = plant_state
|
||||
.iter()
|
||||
.map(|s| s.sensor_b_firmware_build_minutes)
|
||||
.collect();
|
||||
|
||||
let data = Moistures {
|
||||
moisture_a: a,
|
||||
moisture_b: b,
|
||||
sensor_a_build_minutes,
|
||||
sensor_b_build_minutes,
|
||||
};
|
||||
let json = serde_json::to_string(&data)?;
|
||||
|
||||
@@ -80,10 +93,11 @@ where
|
||||
{
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let tank_state = determine_tank_state(&mut board).await;
|
||||
//should be multisampled
|
||||
let sensor = board.board_hal.get_tank_sensor()?;
|
||||
|
||||
let water_temp: FatResult<f32> = sensor.water_temperature_c().await;
|
||||
let water_temp: FatResult<f32> = board
|
||||
.board_hal
|
||||
.get_tank_sensor()
|
||||
.water_temperature_c()
|
||||
.await;
|
||||
Ok(Some(serde_json::to_string(&tank_state.as_mqtt_info(
|
||||
&board.board_hal.get_config().tank,
|
||||
&water_temp,
|
||||
@@ -109,30 +123,51 @@ pub(crate) async fn get_solar_state<T, const N: usize>(
|
||||
Ok(Some(serde_json::to_string(&state)?))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_version_web<T, const N: usize>(
|
||||
pub(crate) async fn get_firmware_info_web<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
Ok(Some(serde_json::to_string(&get_version(&mut board).await)?))
|
||||
}
|
||||
|
||||
/// Return the current in-memory config, or — when `saveidx` is `Some(idx)` —
|
||||
/// the JSON stored in save slot `idx`.
|
||||
pub(crate) async fn get_config<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
saveidx: Option<usize>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let json = match saveidx {
|
||||
None => serde_json::to_string(board.board_hal.get_config())?,
|
||||
Some(idx) => board.board_hal.get_esp().load_config_slot(idx).await?,
|
||||
};
|
||||
Ok(Some(json))
|
||||
}
|
||||
|
||||
/// Return a JSON array describing every valid save slot on flash.
|
||||
pub(crate) async fn list_saves<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let json = serde_json::to_string(&board.board_hal.get_config())?;
|
||||
Ok(Some(json))
|
||||
let saves = board.board_hal.get_esp().list_saves().await?;
|
||||
Ok(Some(serde_json::to_string(&saves)?))
|
||||
}
|
||||
|
||||
/// Erase (delete) a single save slot by index.
|
||||
pub(crate) async fn delete_save<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
idx: usize,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.get_esp().delete_save_slot(idx).await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_battery_state<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let battery_state = board
|
||||
.board_hal
|
||||
.get_battery_monitor()
|
||||
.get_battery_state()
|
||||
.await?;
|
||||
let battery_state = board.board_hal.get_battery_monitor().get_state().await?;
|
||||
Ok(Some(serde_json::to_string(&battery_state)?))
|
||||
}
|
||||
|
||||
@@ -153,7 +188,12 @@ pub(crate) async fn get_time<T, const N: usize>(
|
||||
},
|
||||
};
|
||||
|
||||
let native = esp_time().await.with_timezone(&tz).to_rfc3339();
|
||||
let native = board
|
||||
.board_hal
|
||||
.get_time()
|
||||
.await
|
||||
.with_timezone(&tz)
|
||||
.to_rfc3339();
|
||||
|
||||
let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await {
|
||||
Ok(time) => time.with_timezone(&tz).to_rfc3339(),
|
||||
@@ -175,6 +215,15 @@ pub(crate) async fn get_log_localization_config<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
Ok(Some(serde_json::to_string(
|
||||
&LogMessage::to_log_localisation_config(),
|
||||
&LogMessage::log_localisation_config(),
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Return Wi-Fi scan details including signal strength (RSSI)
|
||||
pub(crate) async fn get_wifi_details<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let wifi_details = board.board_hal.get_esp().wifi_scan_details().await?;
|
||||
Ok(Some(serde_json::to_string(&wifi_details)?))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::fat_error::FatResult;
|
||||
use crate::log::LOG_ACCESS;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use edge_http::io::server::Connection;
|
||||
use edge_nal::io::{Read, Write};
|
||||
use serde::Serialize;
|
||||
|
||||
pub(crate) async fn get_log<T, const N: usize>(
|
||||
conn: &mut Connection<'_, T, N>,
|
||||
@@ -34,3 +37,29 @@ where
|
||||
conn.write_all("]".as_bytes()).await?;
|
||||
Ok(Some(200))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LiveLogEntry {
|
||||
seq: u64,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LiveLogResponse {
|
||||
entries: Vec<LiveLogEntry>,
|
||||
dropped: bool,
|
||||
next_seq: u64,
|
||||
}
|
||||
|
||||
pub(crate) async fn get_live_log(after: Option<u64>) -> FatResult<Option<String>> {
|
||||
let (raw_entries, dropped, next_seq) = crate::log::INTERCEPTOR.get_live_logs(after);
|
||||
let response = LiveLogResponse {
|
||||
entries: raw_entries
|
||||
.into_iter()
|
||||
.map(|(seq, text)| LiveLogEntry { seq, text })
|
||||
.collect(),
|
||||
dropped,
|
||||
next_seq,
|
||||
};
|
||||
Ok(Some(serde_json::to_string(&response)?))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//offer ota and config mode
|
||||
|
||||
mod backup_manager;
|
||||
mod file_manager;
|
||||
mod get_json;
|
||||
mod get_log;
|
||||
mod get_static;
|
||||
@@ -10,16 +9,17 @@ mod post_json;
|
||||
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config};
|
||||
use crate::webserver::file_manager::{file_operations, list_files};
|
||||
use crate::webserver::get_json::{
|
||||
get_battery_state, get_config, get_live_moisture, get_log_localization_config, get_solar_state,
|
||||
get_time, get_timezones, get_version_web, tank_info,
|
||||
delete_save, get_battery_state, get_config, get_firmware_info_web, get_live_moisture,
|
||||
get_log_localization_config, get_solar_state, get_time, get_timezones, get_wifi_details,
|
||||
list_saves, tank_info,
|
||||
};
|
||||
use crate::webserver::get_log::get_log;
|
||||
use crate::webserver::get_log::{get_live_log, get_log};
|
||||
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
|
||||
use crate::webserver::ota::ota_operations;
|
||||
use crate::webserver::post_json::{
|
||||
board_test, detect_sensors, night_lamp_test, pump_test, set_config, wifi_scan, write_time,
|
||||
board_test, can_power, detect_sensors, fertilizer_pump_test, night_lamp_test, pump_test,
|
||||
set_config, wifi_scan, write_time,
|
||||
};
|
||||
use crate::{bail, BOARD_ACCESS};
|
||||
use alloc::borrow::ToOwned;
|
||||
@@ -59,10 +59,7 @@ impl Handler for HTTPRequestRouter {
|
||||
let method = headers.method;
|
||||
let path = headers.path;
|
||||
|
||||
let prefix = "/file?filename=";
|
||||
let status = if path.starts_with(prefix) {
|
||||
file_operations(conn, method, &path, &prefix).await?
|
||||
} else if path == "/ota" {
|
||||
let status = if path == "/ota" {
|
||||
ota_operations(conn, method).await.map_err(|e| {
|
||||
error!("Error handling ota: {e}");
|
||||
e
|
||||
@@ -77,17 +74,33 @@ impl Handler for HTTPRequestRouter {
|
||||
"/get_backup_config" => get_backup_config(conn).await?,
|
||||
&_ => {
|
||||
let json = match path {
|
||||
"/version" => Some(get_version_web(conn).await),
|
||||
"/firmware_info" => Some(get_firmware_info_web(conn).await),
|
||||
"/time" => Some(get_time(conn).await),
|
||||
"/battery" => Some(get_battery_state(conn).await),
|
||||
"/solar" => Some(get_solar_state(conn).await),
|
||||
"/get_config" => Some(get_config(conn).await),
|
||||
"/files" => Some(list_files(conn).await),
|
||||
"/log_localization" => Some(get_log_localization_config(conn).await),
|
||||
"/tank" => Some(tank_info(conn).await),
|
||||
"/backup_info" => Some(backup_info(conn).await),
|
||||
"/timezones" => Some(get_timezones().await),
|
||||
"/moisture" => Some(get_live_moisture(conn).await),
|
||||
"/list_saves" => Some(list_saves(conn).await),
|
||||
"/wifi_details" => Some(get_wifi_details(conn).await),
|
||||
// /live_log accepts an optional ?after=N query parameter
|
||||
p if p == "/live_log" || p.starts_with("/live_log?") => {
|
||||
let after: Option<u64> = p
|
||||
.find("after=")
|
||||
.and_then(|pos| p[pos + 6..].split('&').next())
|
||||
.and_then(|s| s.parse().ok());
|
||||
Some(get_live_log(after).await)
|
||||
}
|
||||
// /get_config accepts an optional ?saveidx=N query parameter
|
||||
p if p == "/get_config" || p.starts_with("/get_config?") => {
|
||||
let saveidx: Option<usize> = p
|
||||
.find("saveidx=")
|
||||
.and_then(|pos| p[pos + 8..].split('&').next())
|
||||
.and_then(|s| s.parse().ok());
|
||||
Some(get_config(conn, saveidx).await)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match json {
|
||||
@@ -103,9 +116,11 @@ impl Handler for HTTPRequestRouter {
|
||||
"/time" => Some(write_time(conn).await),
|
||||
"/backup_config" => Some(backup_config(conn).await),
|
||||
"/pumptest" => Some(pump_test(conn).await),
|
||||
"/can_power" => Some(can_power(conn).await),
|
||||
"/lamptest" => Some(night_lamp_test(conn).await),
|
||||
"/fertilizerpumptest" => Some(fertilizer_pump_test(conn).await),
|
||||
"/boardtest" => Some(board_test().await),
|
||||
"/detect_sensors" => Some(detect_sensors().await),
|
||||
"/detect_sensors" => Some(detect_sensors(conn).await),
|
||||
"/reboot" => {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.get_esp().set_restart_to_conf(true);
|
||||
@@ -125,7 +140,28 @@ impl Handler for HTTPRequestRouter {
|
||||
Some(json) => Some(handle_json(conn, json).await?),
|
||||
}
|
||||
}
|
||||
Method::Options | Method::Delete | Method::Head | Method::Put => None,
|
||||
Method::Delete => {
|
||||
// DELETE /delete_save?idx=N
|
||||
let json = if path == "/delete_save" || path.starts_with("/delete_save?") {
|
||||
let idx: Option<usize> = path
|
||||
.find("idx=")
|
||||
.and_then(|pos| path[pos + 4..].split('&').next())
|
||||
.and_then(|s| s.parse().ok());
|
||||
match idx {
|
||||
Some(idx) => Some(delete_save(conn, idx).await),
|
||||
None => Some(Err(FatError::String {
|
||||
error: "missing idx parameter".into(),
|
||||
})),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match json {
|
||||
None => None,
|
||||
Some(json) => Some(handle_json(conn, json).await?),
|
||||
}
|
||||
}
|
||||
Method::Options | Method::Head | Method::Put => None,
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
@@ -179,6 +215,7 @@ where
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
|
||||
pub async fn http_server(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) {
|
||||
let buffer: TcpBuffers<2, 1024, 1024> = TcpBuffers::new();
|
||||
let tcp = Tcp::new(stack, &buffer);
|
||||
@@ -239,6 +276,7 @@ where
|
||||
Err(err) => {
|
||||
let error_text = err.to_string();
|
||||
info!("error handling process {error_text}");
|
||||
|
||||
conn.initiate_response(
|
||||
500,
|
||||
Some("OK"),
|
||||
|
||||
@@ -41,7 +41,6 @@ where
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(chunk as u32).await;
|
||||
// Erase next block if we are at a 4K boundary (including the first block at offset 0)
|
||||
info!("erasing and writing block 0x{offset:x}");
|
||||
board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use crate::config::PlantControllerConfig;
|
||||
use crate::fat_error::FatResult;
|
||||
use crate::hal::esp_set_time;
|
||||
use crate::hal::savegame_manager::SAVEGAME_SLOT_SIZE;
|
||||
use crate::hal::DetectionRequest;
|
||||
use crate::webserver::read_up_to_bytes_from_request;
|
||||
use crate::{do_secure_pump, BOARD_ACCESS};
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use chrono::DateTime;
|
||||
use edge_http::io::server::Connection;
|
||||
use edge_nal::io::{Read, Write};
|
||||
use esp_radio::wifi::ap::AccessPointInfo;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -29,16 +32,21 @@ pub struct TestPump {
|
||||
pump: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct CanPower {
|
||||
state: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn wifi_scan<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
info!("start wifi scan");
|
||||
let mut ssids: Vec<String> = Vec::new();
|
||||
let scan_result = board.board_hal.get_esp().wifi_scan().await?;
|
||||
let scan_result: Vec<AccessPointInfo> = board.board_hal.get_esp().wifi_scan().await?;
|
||||
scan_result
|
||||
.iter()
|
||||
.for_each(|s| ssids.push(s.ssid.to_string()));
|
||||
.for_each(|s| ssids.push(s.ssid.as_str().to_owned()));
|
||||
let ssid_json = serde_json::to_string(&SSIDList { ssids })?;
|
||||
info!("Sending ssid list {}", &ssid_json);
|
||||
Ok(Some(ssid_json))
|
||||
@@ -50,9 +58,16 @@ pub(crate) async fn board_test() -> FatResult<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn detect_sensors() -> FatResult<Option<String>> {
|
||||
pub(crate) async fn detect_sensors<T, const N: usize>(
|
||||
request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>>
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let actual_data = read_up_to_bytes_from_request(request, None).await?;
|
||||
let detect: DetectionRequest = serde_json::from_slice(&actual_data)?;
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let result = board.board_hal.detect_sensors().await?;
|
||||
let result = board.board_hal.detect_sensors(detect).await?;
|
||||
let json = serde_json::to_string(&result)?;
|
||||
Ok(Some(json))
|
||||
}
|
||||
@@ -88,6 +103,19 @@ where
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn fertilizer_pump_test<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>>
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.fertilizer_pump(true).await?;
|
||||
embassy_time::Timer::after_millis(1000).await;
|
||||
board.board_hal.fertilizer_pump(false).await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn write_time<T, const N: usize>(
|
||||
request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>>
|
||||
@@ -96,8 +124,9 @@ where
|
||||
{
|
||||
let actual_data = read_up_to_bytes_from_request(request, None).await?;
|
||||
let time: SetTime = serde_json::from_slice(&actual_data)?;
|
||||
let parsed = DateTime::parse_from_rfc3339(time.time).unwrap();
|
||||
esp_set_time(parsed).await?;
|
||||
let parsed = DateTime::parse_from_rfc3339(time.time)?;
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.set_time(&parsed).await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -107,7 +136,8 @@ pub(crate) async fn set_config<T, const N: usize>(
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let all = read_up_to_bytes_from_request(request, Some(4096)).await?;
|
||||
//accept nearly full slotsize leave some space for header
|
||||
let all = read_up_to_bytes_from_request(request, Some(SAVEGAME_SLOT_SIZE - 512)).await?;
|
||||
let length = all.len();
|
||||
let config: PlantControllerConfig = serde_json::from_slice(&all)?;
|
||||
|
||||
@@ -117,3 +147,19 @@ where
|
||||
board.board_hal.set_config(config);
|
||||
Ok(Some("Ok".to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn can_power<T, const N: usize>(
|
||||
request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>>
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let actual_data = read_up_to_bytes_from_request(request, None).await?;
|
||||
let can_power_request: CanPower = serde_json::from_slice(&actual_data)?;
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
|
||||
board.board_hal.can_power(can_power_request.state).await?;
|
||||
let enable = can_power_request.state;
|
||||
info!("set can power to {enable}");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
+239
-238
@@ -94,9 +94,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/buffers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz",
|
||||
"integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
|
||||
"integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -128,19 +128,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/json-pack": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz",
|
||||
"integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==",
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz",
|
||||
"integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/base64": "^1.1.2",
|
||||
"@jsonjoy.com/buffers": "^1.0.0",
|
||||
"@jsonjoy.com/buffers": "^1.2.0",
|
||||
"@jsonjoy.com/codegen": "^1.0.0",
|
||||
"@jsonjoy.com/json-pointer": "^1.0.1",
|
||||
"@jsonjoy.com/json-pointer": "^1.0.2",
|
||||
"@jsonjoy.com/util": "^1.9.0",
|
||||
"hyperdyperid": "^1.2.0",
|
||||
"thingies": "^2.5.0"
|
||||
"thingies": "^2.5.0",
|
||||
"tree-dump": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
@@ -318,22 +319,22 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
|
||||
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
|
||||
"version": "4.17.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
|
||||
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
"@types/qs": "*",
|
||||
"@types/serve-static": "*"
|
||||
"@types/serve-static": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "4.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
||||
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
||||
"version": "4.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz",
|
||||
"integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -358,9 +359,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
|
||||
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
|
||||
"version": "1.17.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz",
|
||||
"integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -381,12 +382,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
|
||||
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||
"version": "25.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.12.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
@@ -421,13 +422,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
|
||||
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
@@ -442,15 +442,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
|
||||
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
|
||||
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/node": "*",
|
||||
"@types/send": "*"
|
||||
"@types/send": "<1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static/node_modules/@types/send": {
|
||||
"version": "0.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
|
||||
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sockjs": {
|
||||
@@ -707,7 +718,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -732,7 +742,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -834,9 +843,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
|
||||
"integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -873,24 +882,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
@@ -941,9 +950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.26.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
||||
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -959,13 +968,12 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
"electron-to-chromium": "^1.5.218",
|
||||
"node-releases": "^2.0.21",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -1049,9 +1057,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001743",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz",
|
||||
"integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==",
|
||||
"version": "1.0.30001761",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
|
||||
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1284,9 +1292,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1294,9 +1302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1387,9 +1395,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
|
||||
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz",
|
||||
"integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1404,9 +1412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser-id": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
|
||||
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
|
||||
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1573,9 +1581,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.222",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz",
|
||||
"integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==",
|
||||
"version": "1.5.267",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emojis-list": {
|
||||
@@ -1599,9 +1607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"version": "5.18.4",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
|
||||
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -1622,9 +1630,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/envinfo": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
|
||||
"integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
|
||||
"integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1655,9 +1663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
@@ -1759,40 +1767,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"body-parser": "~1.20.3",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"qs": "~6.14.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
@@ -1812,9 +1820,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
|
||||
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -1882,9 +1890,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -1916,18 +1924,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2076,9 +2084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regex.js": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz",
|
||||
"integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz",
|
||||
"integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -2272,12 +2280,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz",
|
||||
"integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==",
|
||||
"version": "5.6.5",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz",
|
||||
"integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/html-minifier-terser": "^6.0.0",
|
||||
"html-minifier-terser": "^6.0.2",
|
||||
@@ -2333,20 +2340,24 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-parser-js": {
|
||||
@@ -2465,9 +2476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
|
||||
"integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2712,9 +2723,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz",
|
||||
"integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
|
||||
"integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2723,12 +2734,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
|
||||
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-utils": {
|
||||
@@ -2797,9 +2812,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.42.0.tgz",
|
||||
"integrity": "sha512-RG+4HMGyIVp6UWDWbFmZ38yKrSzblPnfJu0PyPt0hw52KW4PPlPp+HdV4qZBG0hLDuYVnf8wfQT4NymKXnlQjA==",
|
||||
"version": "4.51.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz",
|
||||
"integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -2810,9 +2825,6 @@
|
||||
"tree-dump": "^1.0.3",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/streamich"
|
||||
@@ -2956,9 +2968,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
|
||||
"integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
@@ -2966,9 +2978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.21",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
|
||||
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@@ -3274,13 +3286,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -3329,16 +3341,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -3384,7 +3396,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -3514,13 +3525,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
@@ -3640,9 +3651,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
@@ -3680,9 +3691,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -3693,40 +3704,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -3813,16 +3814,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -4115,9 +4116,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4174,9 +4175,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -4187,9 +4188,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.44.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||
"version": "5.44.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
@@ -4205,9 +4206,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
|
||||
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
@@ -4343,8 +4344,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
@@ -4361,12 +4361,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4376,9 +4375,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
||||
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
@@ -4404,9 +4403,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -4488,9 +4487,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz",
|
||||
"integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
@@ -4511,11 +4510,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.101.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
|
||||
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||
"version": "5.104.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
|
||||
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -4525,22 +4523,22 @@
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.24.0",
|
||||
"browserslist": "^4.28.1",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"enhanced-resolve": "^5.17.4",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"loader-runner": "^4.2.0",
|
||||
"loader-runner": "^4.3.1",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.2",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.1",
|
||||
"schema-utils": "^4.3.3",
|
||||
"tapable": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.16",
|
||||
"watchpack": "^2.4.4",
|
||||
"webpack-sources": "^3.3.3"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4565,7 +4563,6 @@
|
||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^2.1.1",
|
||||
@@ -4617,14 +4614,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-middleware": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.3.tgz",
|
||||
"integrity": "sha512-5kA/PzpZzDz5mNOkcNLmU1UdjGeSSxd7rt1akWpI70jMNHLASiBPRaQZn0hgyhvhawfIwSnnLfDABIxL3ueyFg==",
|
||||
"version": "7.4.5",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz",
|
||||
"integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "^2.0.10",
|
||||
"memfs": "^4.6.0",
|
||||
"memfs": "^4.43.1",
|
||||
"mime-types": "^3.0.1",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
@@ -4657,16 +4654,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-middleware/node_modules/mime-types": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server": {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
export interface LogArray extends Array<LogEntry> {
|
||||
}
|
||||
|
||||
export interface LiveLogEntry {
|
||||
seq: number,
|
||||
text: string,
|
||||
}
|
||||
|
||||
export interface LiveLogResponse {
|
||||
entries: LiveLogEntry[],
|
||||
dropped: boolean,
|
||||
next_seq: number,
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string,
|
||||
message_id: number,
|
||||
@@ -34,6 +45,12 @@ export interface NetworkConfig {
|
||||
max_wait: number
|
||||
}
|
||||
|
||||
export interface SaveInfo {
|
||||
idx: number,
|
||||
len: number,
|
||||
created_at: string | null,
|
||||
}
|
||||
|
||||
export interface FileList {
|
||||
total: number,
|
||||
used: number,
|
||||
@@ -92,6 +109,7 @@ export enum BoardVersion {
|
||||
export interface BoardHardware {
|
||||
board: BoardVersion,
|
||||
battery: BatteryBoardVersion,
|
||||
pump_corrosion_protection: boolean,
|
||||
}
|
||||
|
||||
export interface PlantControllerConfig {
|
||||
@@ -110,6 +128,8 @@ export interface PlantConfig {
|
||||
min_moisture: number,
|
||||
pump_time_s: number,
|
||||
pump_cooldown_min: number,
|
||||
fertilizer_s: number,
|
||||
fertilizer_cooldown_min: number,
|
||||
pump_hour_start: number,
|
||||
pump_hour_end: number,
|
||||
pump_limit_ml: number,
|
||||
@@ -121,6 +141,7 @@ export interface PlantConfig {
|
||||
min_pump_current_ma: number,
|
||||
max_pump_current_ma: number,
|
||||
ignore_current_error: boolean,
|
||||
sensor_combine_mode: string,
|
||||
}
|
||||
|
||||
export interface PumpTestResult {
|
||||
@@ -141,6 +162,10 @@ export interface TestPump {
|
||||
pump: number
|
||||
}
|
||||
|
||||
export interface CanPower {
|
||||
state: boolean
|
||||
}
|
||||
|
||||
export interface SetTime {
|
||||
time: string
|
||||
}
|
||||
@@ -153,6 +178,8 @@ export interface GetTime {
|
||||
export interface Moistures {
|
||||
moisture_a: [string],
|
||||
moisture_b: [string],
|
||||
sensor_a_build_minutes: Array<number | null>,
|
||||
sensor_b_build_minutes: Array<number | null>,
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
@@ -161,44 +188,70 @@ export interface VersionInfo {
|
||||
current: string,
|
||||
slot0_state: string,
|
||||
slot1_state: string,
|
||||
heap_total: number,
|
||||
heap_used: number,
|
||||
heap_free: number,
|
||||
heap_max_used: number,
|
||||
}
|
||||
|
||||
export interface BatteryState {
|
||||
temperature: string
|
||||
voltage_milli_volt: string,
|
||||
current_milli_ampere: string,
|
||||
cycle_count: string,
|
||||
design_milli_ampere: string,
|
||||
remaining_milli_ampere: string,
|
||||
state_of_charge: string,
|
||||
state_of_health: string
|
||||
}
|
||||
|
||||
export interface DetectionPlant {
|
||||
a: boolean,
|
||||
b: boolean
|
||||
/// Request: which sensors to send IDENTIFY_CMD to.
|
||||
export interface SensorRequest {
|
||||
sensor_a: boolean,
|
||||
sensor_b: boolean,
|
||||
}
|
||||
|
||||
export interface DetectionResult {
|
||||
plants: DetectionPlant[]
|
||||
export interface DetectionRequest {
|
||||
plant: SensorRequest[]
|
||||
}
|
||||
|
||||
/// Response: detection result per plant.
|
||||
/// sensor_a / sensor_b: firmware build timestamp in minutes since Unix epoch,
|
||||
/// or null if the sensor did not respond.
|
||||
export interface DetectionPlant {
|
||||
sensor_a: number | null,
|
||||
sensor_b: number | null,
|
||||
}
|
||||
|
||||
export interface Detection {
|
||||
plant: DetectionPlant[]
|
||||
}
|
||||
|
||||
/// Wi-Fi scan result details for UI display
|
||||
export interface WifiScanResult {
|
||||
ssid: string,
|
||||
bssid: string,
|
||||
rssi: number, // signal strength in dBm
|
||||
channel: number,
|
||||
auth_method: string
|
||||
}
|
||||
|
||||
export interface TankInfo {
|
||||
/// is there enough water in the tank
|
||||
/// there is enough water in the tank
|
||||
enough_water: boolean,
|
||||
/// warning that water needs to be refilled soon
|
||||
warn_level: boolean,
|
||||
/// estimation how many ml are still in tank
|
||||
left_ml: number | null,
|
||||
/// if there is was an issue with the water level sensor
|
||||
/// estimation how many ml are still in the tank
|
||||
volume_ml: number | null,
|
||||
/// if there is an issue with the water level sensor
|
||||
sensor_error: string | null,
|
||||
/// raw water sensor value
|
||||
raw: number | null,
|
||||
fill_raw_v: number | null,
|
||||
/// percent value
|
||||
percent: number | null,
|
||||
/// water in tank might be frozen
|
||||
fill_pct: number | null,
|
||||
/// water in the tank might be frozen
|
||||
water_frozen: boolean,
|
||||
/// water temperature
|
||||
water_temp: number | null,
|
||||
water_temp_c: number | null,
|
||||
temp_sensor_error: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,6 @@
|
||||
<span class="powerflexkey">mA:</span>
|
||||
<span class="powerflexvalue" id="battery_current_milli_ampere" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">Cycles:</span>
|
||||
<span class="powerflexvalue" id="battery_cycle_count" ></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="powerflexkey">design mA:</span>
|
||||
<span class="powerflexvalue" id="battery_design_milli_ampere" ></span>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {BatteryState} from "./api";
|
||||
export class BatteryView{
|
||||
voltage_milli_volt: HTMLSpanElement;
|
||||
current_milli_ampere: HTMLSpanElement;
|
||||
cycle_count: HTMLSpanElement;
|
||||
design_milli_ampere: HTMLSpanElement;
|
||||
remaining_milli_ampere: HTMLSpanElement;
|
||||
state_of_charge: HTMLSpanElement;
|
||||
@@ -18,7 +17,6 @@ export class BatteryView{
|
||||
(document.getElementById("batteryview") as HTMLElement).innerHTML = require("./batteryview.html")
|
||||
this.voltage_milli_volt = document.getElementById("battery_voltage_milli_volt") as HTMLSpanElement;
|
||||
this.current_milli_ampere = document.getElementById("battery_current_milli_ampere") as HTMLSpanElement;
|
||||
this.cycle_count = document.getElementById("battery_cycle_count") as HTMLSpanElement;
|
||||
this.design_milli_ampere = document.getElementById("battery_design_milli_ampere") as HTMLSpanElement;
|
||||
this.remaining_milli_ampere = document.getElementById("battery_remaining_milli_ampere") as HTMLSpanElement;
|
||||
this.state_of_charge = document.getElementById("battery_state_of_charge") as HTMLSpanElement;
|
||||
@@ -41,7 +39,6 @@ export class BatteryView{
|
||||
if (batterystate == null) {
|
||||
this.voltage_milli_volt.innerText = "N/A"
|
||||
this.current_milli_ampere.innerText = "N/A"
|
||||
this.cycle_count.innerText = "N/A"
|
||||
this.design_milli_ampere.innerText = "N/A"
|
||||
this.remaining_milli_ampere.innerText = "N/A"
|
||||
this.state_of_charge.innerText = "N/A"
|
||||
@@ -50,12 +47,11 @@ export class BatteryView{
|
||||
} else {
|
||||
this.voltage_milli_volt.innerText = String(+batterystate.voltage_milli_volt/1000)
|
||||
this.current_milli_ampere.innerText = batterystate.current_milli_ampere
|
||||
this.cycle_count.innerText = batterystate.cycle_count
|
||||
this.design_milli_ampere.innerText = batterystate.design_milli_ampere
|
||||
this.remaining_milli_ampere.innerText = batterystate.remaining_milli_ampere
|
||||
this.state_of_charge.innerText = batterystate.state_of_charge
|
||||
this.state_of_health.innerText = batterystate.state_of_health
|
||||
this.temperature.innerText = String(+batterystate.temperature / 100)
|
||||
this.temperature.innerText = String(+batterystate.temperature)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,44 +29,10 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="subtitle">Files:</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Total Size</div>
|
||||
<div id="filetotalsize" class="filevalue"></div>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Used Size</div>
|
||||
<div id="fileusedsize" class="filevalue"></div>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">Free Size</div>
|
||||
<div id="filefreesize" class="filevalue"></div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
|
||||
<div class="subtitle" >Upload:</div>
|
||||
</div>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double;">
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">
|
||||
File:
|
||||
</div>
|
||||
<input id="fileuploadfile" class="filevalue" type="file">
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="filekey">
|
||||
Name:
|
||||
</div>
|
||||
<input id="fileuploadname" class="filevalue" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-bottom-style: double;">
|
||||
<button id="fileuploadbtn" class="subtitle">Upload</button>
|
||||
</div>
|
||||
<div class="subtitle">Save Slots:</div>
|
||||
<br>
|
||||
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
|
||||
<div class="subtitle">List:</div>
|
||||
</div>
|
||||
<div id="fileList" class="flexcontainer" style="border-left-style: double; border-right-style: double; border-bottom-style: double;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user