19 Commits

Author SHA1 Message Date
0c0b62e2ed update: improve documentation and restructure code for modular hardware integration, add CAN communication to HAL, and update KiCad layouts 2026-01-23 22:02:14 +01:00
1de40085fb add: implement UART-based serial configuration handling and improve error handling in charge indicator updates 2026-01-05 19:57:57 +01:00
8fc2a89503 refactor: simplify battery monitoring logic, remove unused fields, and replace BQ34Z100G1 integration with WCH I2C implementation 2026-01-04 23:59:14 +01:00
350820741a update: adjust sensor programmer hardware files, add new footprint, and reassign nets 2026-01-04 18:42:09 +01:00
d33b05e1d7 remove HAL implementation files for v3 and v4, and the build script 2026-01-04 18:41:38 +01:00
412a26390a add hardware files for sensor programmer 2025-12-25 22:22:30 +01:00
af275abf15 minify pcb for can 2025-12-22 18:46:56 +01:00
ca2fd8a5e1 improved sensor canbus code 2025-12-06 04:01:16 +01:00
6ffbf710d3 slight adjustments 2025-11-19 12:20:26 +01:00
f6767d1827 refactor: organize dependencies in Cargo.toml for clarity and consistency 2025-11-13 21:33:01 +01:00
3db71eea45 Merge branch 'goodby-espidf' of ssh://git.mannheim.ccc.de:1337/C3MA/PlantCtrl into goodby-espidf 2025-11-13 20:08:45 +01:00
576b81bb66 vcs config 2025-11-13 20:08:19 +01:00
8cd9e08e93 shared flash test 2025-11-02 02:30:21 +01:00
0519ca3efe read and write to nal 2025-10-31 23:39:10 +01:00
f366aace7f esp-hal release wip 2025-10-31 23:22:40 +01:00
8b0734d029 config for sensor 2025-10-31 14:01:43 +01:00
cafe1b264e refactor: unify moisture handling, update config structure, and add peer dependencies 2025-10-23 22:44:44 +02:00
1db3f7af64 chore: 📎 + fmt 2025-10-18 20:40:38 +02:00
6357ec773f Merge branch 'containerize-toolchains' into goodby-espidf 2025-10-18 19:03:13 +02:00
83 changed files with 286298 additions and 14910 deletions

View File

@@ -6037,7 +6037,7 @@
(descr "SOT, 3 Pin (JEDEC TO-236 Var AB https://www.jedec.org/document_search?search_api_views_fulltext=TO-236), generated with kicad-footprint-generator ipc_gullwing_generator.py") (descr "SOT, 3 Pin (JEDEC TO-236 Var AB https://www.jedec.org/document_search?search_api_views_fulltext=TO-236), generated with kicad-footprint-generator ipc_gullwing_generator.py")
(tags "SOT TO_SOT_SMD") (tags "SOT TO_SOT_SMD")
(property "Reference" "D6" (property "Reference" "D6"
(at 0 -2.4 180) (at -1.9875 -2.25 180)
(layer "F.SilkS") (layer "F.SilkS")
(uuid "ba224372-284d-4e83-bdee-3dcc26b3a1ec") (uuid "ba224372-284d-4e83-bdee-3dcc26b3a1ec")
(effects (effects
@@ -12996,7 +12996,7 @@
(drill 1) (drill 1)
(layers "*.Cu" "*.Mask") (layers "*.Cu" "*.Mask")
(remove_unused_layers no) (remove_unused_layers no)
(net 44 "CONFIG2") (net 22 "BOOT_SEL")
(pinfunction "Pin_3") (pinfunction "Pin_3")
(pintype "passive") (pintype "passive")
(uuid "7e1e9d83-af98-48df-9773-39ee7824a6f6") (uuid "7e1e9d83-af98-48df-9773-39ee7824a6f6")
@@ -18704,7 +18704,7 @@
(descr "SOT, 3 Pin (JEDEC TO-236 Var AB https://www.jedec.org/document_search?search_api_views_fulltext=TO-236), generated with kicad-footprint-generator ipc_gullwing_generator.py") (descr "SOT, 3 Pin (JEDEC TO-236 Var AB https://www.jedec.org/document_search?search_api_views_fulltext=TO-236), generated with kicad-footprint-generator ipc_gullwing_generator.py")
(tags "SOT TO_SOT_SMD") (tags "SOT TO_SOT_SMD")
(property "Reference" "D1" (property "Reference" "D1"
(at 0 -2.4 0) (at 0 2.3 0)
(layer "F.SilkS") (layer "F.SilkS")
(uuid "9750a299-8bf6-4149-9d59-b90e672d444d") (uuid "9750a299-8bf6-4149-9d59-b90e672d444d")
(effects (effects
@@ -22662,7 +22662,7 @@
(descr "SOT, 3 Pin (JEDEC TO-236 Var AB https://www.jedec.org/document_search?search_api_views_fulltext=TO-236), generated with kicad-footprint-generator ipc_gullwing_generator.py") (descr "SOT, 3 Pin (JEDEC TO-236 Var AB https://www.jedec.org/document_search?search_api_views_fulltext=TO-236), generated with kicad-footprint-generator ipc_gullwing_generator.py")
(tags "SOT TO_SOT_SMD") (tags "SOT TO_SOT_SMD")
(property "Reference" "D4" (property "Reference" "D4"
(at 0 -2.4 0) (at 2.9875 -0.25 0)
(layer "F.SilkS") (layer "F.SilkS")
(uuid "f9abdc1a-0e87-4699-bd29-df7c0baae9cd") (uuid "f9abdc1a-0e87-4699-bd29-df7c0baae9cd")
(effects (effects
@@ -27517,7 +27517,7 @@
) )
) )
(gr_text "Extension Module" (gr_text "Extension Module"
(at 212.58 97.67 0) (at 212.516642 96.15145 0)
(layer "F.SilkS") (layer "F.SilkS")
(uuid "e56a58c6-c9a0-4ac6-ae66-550392612e86") (uuid "e56a58c6-c9a0-4ac6-ae66-550392612e86")
(effects (effects
@@ -30814,6 +30814,14 @@
(net 22) (net 22)
(uuid "a0e70239-5b1c-4c3c-a476-64be66d07299") (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 (segment
(start 217.33 53.496) (start 217.33 53.496)
(end 217.5 53.496) (end 217.5 53.496)
@@ -30862,6 +30870,14 @@
(net 22) (net 22)
(uuid "f1e984ca-47cc-4a62-83f5-873ad9276c13") (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 (segment
(start 217.7 53.496) (start 217.7 53.496)
(end 215.731 53.496) (end 215.731 53.496)
@@ -30878,6 +30894,14 @@
(net 22) (net 22)
(uuid "02ec00f7-269e-4eee-bd30-b44c72206ead") (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 (via
(at 247.33 57.67) (at 247.33 57.67)
(size 0.8) (size 0.8)
@@ -30894,6 +30918,14 @@
(net 22) (net 22)
(uuid "668f3e0f-6796-4544-9fff-44be590ce44b") (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 (segment
(start 241.08 51.42) (start 241.08 51.42)
(end 247.33 57.67) (end 247.33 57.67)
@@ -30902,6 +30934,14 @@
(net 22) (net 22)
(uuid "6f9fed78-f413-48d8-be42-a9dd34d1adfe") (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 (segment
(start 245.88 48.17) (start 245.88 48.17)
(end 246.48 48.17) (end 246.48 48.17)
@@ -31574,30 +31614,6 @@
(net 44) (net 44)
(uuid "ea858ae5-b45b-4221-888a-e8a2838d0cf3") (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 (segment
(start 186.655 113.695) (start 186.655 113.695)
(end 186.655 112.621) (end 186.655 112.621)

View File

@@ -1,6 +1,6 @@
{ {
"board": { "board": {
"active_layer": 0, "active_layer": 2,
"active_layer_preset": "All Layers", "active_layer_preset": "All Layers",
"auto_track_width": false, "auto_track_width": false,
"hidden_netclasses": [], "hidden_netclasses": [],

View File

@@ -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" (global_label "3_3V"
(shape input) (shape input)
(at 224.79 57.15 0) (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" (global_label "GND"
(shape input) (shape input)
(at 113.03 123.19 270) (at 113.03 123.19 270)

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"board": { "board": {
"active_layer": 0, "active_layer": 0,
"active_layer_preset": "", "active_layer_preset": "",
"auto_track_width": true, "auto_track_width": false,
"hidden_netclasses": [], "hidden_netclasses": [],
"hidden_nets": [], "hidden_nets": [],
"high_contrast_mode": 0, "high_contrast_mode": 0,

View File

@@ -51,7 +51,13 @@
"min_clearance": 0.5 "min_clearance": 0.5
} }
}, },
"diff_pair_dimensions": [], "diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [], "drc_exclusions": [],
"meta": { "meta": {
"version": 2 "version": 2
@@ -70,9 +76,9 @@
"duplicate_footprints": "warning", "duplicate_footprints": "warning",
"extra_footprint": "warning", "extra_footprint": "warning",
"footprint": "error", "footprint": "error",
"footprint_filters_mismatch": "ignore", "footprint_filters_mismatch": "warning",
"footprint_symbol_mismatch": "warning", "footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore", "footprint_type_mismatch": "warning",
"hole_clearance": "error", "hole_clearance": "error",
"hole_to_hole": "warning", "hole_to_hole": "warning",
"holes_co_located": "warning", "holes_co_located": "warning",
@@ -86,20 +92,20 @@
"malformed_courtyard": "error", "malformed_courtyard": "error",
"microvia_drill_out_of_range": "error", "microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning", "mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore", "missing_courtyard": "warning",
"missing_footprint": "warning", "missing_footprint": "warning",
"net_conflict": "warning", "net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning", "nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore", "npth_inside_courtyard": "warning",
"padstack": "warning", "padstack": "warning",
"pth_inside_courtyard": "ignore", "pth_inside_courtyard": "warning",
"shorting_items": "error", "shorting_items": "error",
"silk_edge_clearance": "warning", "silk_edge_clearance": "warning",
"silk_over_copper": "warning", "silk_over_copper": "warning",
"silk_overlap": "warning", "silk_overlap": "warning",
"skew_out_of_range": "error", "skew_out_of_range": "error",
"solder_mask_bridge": "error", "solder_mask_bridge": "error",
"starved_thermal": "error", "starved_thermal": "warning",
"text_height": "warning", "text_height": "warning",
"text_on_edge_cuts": "error", "text_on_edge_cuts": "error",
"text_thickness": "warning", "text_thickness": "warning",
@@ -119,7 +125,7 @@
"max_error": 0.005, "max_error": 0.005,
"min_clearance": 0.0, "min_clearance": 0.0,
"min_connection": 0.0, "min_connection": 0.0,
"min_copper_edge_clearance": 0.5, "min_copper_edge_clearance": 0.3,
"min_groove_width": 0.0, "min_groove_width": 0.0,
"min_hole_clearance": 0.25, "min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25, "min_hole_to_hole": 0.25,
@@ -133,7 +139,7 @@
"min_track_width": 0.0, "min_track_width": 0.0,
"min_via_annular_width": 0.1, "min_via_annular_width": 0.1,
"min_via_diameter": 0.5, "min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.0, "solder_mask_to_copper_clearance": 0.005,
"use_height_for_length_calcs": true "use_height_for_length_calcs": true
}, },
"teardrop_options": [ "teardrop_options": [
@@ -180,7 +186,12 @@
"td_width_to_size_filter_ratio": 0.9 "td_width_to_size_filter_ratio": 0.9
} }
], ],
"track_widths": [], "track_widths": [
0.0,
0.2,
0.5,
1.0
],
"tuning_pattern_settings": { "tuning_pattern_settings": {
"diff_pair_defaults": { "diff_pair_defaults": {
"corner_radius_percentage": 80, "corner_radius_percentage": 80,
@@ -207,7 +218,12 @@
"spacing": 0.6 "spacing": 0.6
} }
}, },
"via_dimensions": [], "via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
}
],
"zones_allow_external_fillets": false "zones_allow_external_fillets": false
}, },
"ipc2581": { "ipc2581": {
@@ -431,7 +447,7 @@
"pin_not_connected": "error", "pin_not_connected": "error",
"pin_not_driven": "error", "pin_not_driven": "error",
"pin_to_pin": "warning", "pin_to_pin": "warning",
"power_pin_not_driven": "error", "power_pin_not_driven": "ignore",
"same_local_global_label": "warning", "same_local_global_label": "warning",
"similar_label_and_power": "warning", "similar_label_and_power": "warning",
"similar_labels": "warning", "similar_labels": "warning",
@@ -490,7 +506,7 @@
"plot": "", "plot": "",
"pos_files": "", "pos_files": "",
"specctra_dsn": "", "specctra_dsn": "",
"step": "", "step": "sensor.step",
"svg": "", "svg": "",
"vrml": "" "vrml": ""
}, },
@@ -818,6 +834,24 @@
"label": "#", "label": "#",
"name": "${ITEM_NUMBER}", "name": "${ITEM_NUMBER}",
"show": false "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": "", "filter_string": "",

File diff suppressed because it is too large Load Diff

186347
Hardware/Sensor/sensor.step Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,9 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> <mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../../website/themes/blowfish" vcs="Git" /> <mapping directory="$PROJECT_DIR$/../../website/themes/blowfish" vcs="Git" />
</component> </component>
<component name="VcsProjectSettings">
<option name="detectVcsMappingsAutomatically" value="false" />
</component>
</project> </project>

View File

@@ -66,6 +66,7 @@ dependencies = [
"embedded-can", "embedded-can",
"heapless", "heapless",
"log", "log",
"nb 1.1.0",
"panic-halt", "panic-halt",
"qingke", "qingke",
"qingke-rt", "qingke-rt",

View File

@@ -28,6 +28,7 @@ embassy-usb = { version = "0.3.0" }
embassy-futures = { version = "0.1.0" } embassy-futures = { version = "0.1.0" }
embassy-sync = { version = "0.6.0" } embassy-sync = { version = "0.6.0" }
embedded-can = "0.4.1" embedded-can = "0.4.1"
nb = "1.1"
embedded-alloc = { version = "0.6.0", default-features = false, features = ["llff"] } embedded-alloc = { version = "0.6.0", default-features = false, features = ["llff"] }
# This is okay because we should automatically use whatever ch32-hal uses # This is okay because we should automatically use whatever ch32-hal uses
@@ -51,3 +52,11 @@ strip = true # symbols are not flashed to the microcontroller, so don't strip
lto = true lto = true
debug = false debug = false
opt-level = "z" # Optimize for size. opt-level = "z" # Optimize for size.
[[bin]]
name = "sensor"
path = "src/main.rs"
test = false
bench = false
doctest = false

View File

@@ -3,26 +3,28 @@
extern crate alloc; extern crate alloc;
use crate::hal::peripherals::CAN1; use crate::hal::peripherals::CAN1;
use core::fmt::Write as _; use canapi::id::{plant_id, MessageKind, IDENTIFY_CMD_OFFSET, MOISTURE_DATA_OFFSET};
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET, MOISTURE_DATA_OFFSET};
use canapi::SensorSlot; use canapi::SensorSlot;
use ch32_hal::gpio::{Level, Output, Speed};
use ch32_hal::adc::{Adc, SampleTime, ADC_MAX}; use ch32_hal::adc::{Adc, SampleTime, ADC_MAX};
use ch32_hal::can; use ch32_hal::can;
use ch32_hal::can::{Can, CanFifo, CanFilter, CanFrame, CanMode}; use ch32_hal::can::{Can, CanFifo, CanFilter, CanFrame, CanMode};
use ch32_hal::mode::{NonBlocking}; use ch32_hal::gpio::{Flex, Level, Output, Pull, Speed};
use ch32_hal::mode::NonBlocking;
use ch32_hal::peripherals::USBD; use ch32_hal::peripherals::USBD;
use embassy_executor::{Spawner, task}; use core::fmt::Write as _;
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_usb::class::cdc_acm::{CdcAcmClass, State}; use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
use embassy_usb::{Builder, UsbDevice}; use embassy_usb::{Builder, UsbDevice};
use embassy_futures::yield_now;
use hal::usbd::{Driver};
use hal::{bind_interrupts};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::{Channel};
use embassy_time::{Instant, Duration, Delay, Timer};
use embedded_can::{Id, StandardId}; use embedded_can::{Id, StandardId};
use hal::bind_interrupts;
use hal::usbd::Driver;
use {ch32_hal as hal, panic_halt as _}; 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 { macro_rules! mk_static {
($t:ty,$val:expr) => {{ ($t:ty,$val:expr) => {{
@@ -33,26 +35,18 @@ macro_rules! mk_static {
}}; }};
} }
bind_interrupts!(struct Irqs { bind_interrupts!(struct Irqs {
USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>; USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>;
}); });
use embedded_alloc::LlffHeap as Heap;
use embedded_can::nb::Can as nb_can;
use qingke::riscv::asm::delay;
use log::log;
#[global_allocator] #[global_allocator]
static HEAP: Heap = Heap::empty(); static HEAP: Heap = Heap::empty();
static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new(); static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new();
#[embassy_executor::main(entry = "qingke_rt::entry")] #[embassy_executor::main(entry = "qingke_rt::entry")]
async fn main(spawner: Spawner) { async fn main(spawner: Spawner) {
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2)); ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
//
unsafe { unsafe {
static mut HEAP_SPACE: [u8; 4096] = [0; 4096]; // 4 KiB heap, adjust as needed static mut HEAP_SPACE: [u8; 4096] = [0; 4096]; // 4 KiB heap, adjust as needed
HEAP.init(HEAP_SPACE.as_ptr() as usize, HEAP_SPACE.len()); HEAP.init(HEAP_SPACE.as_ptr() as usize, HEAP_SPACE.len());
@@ -63,14 +57,53 @@ async fn main(spawner: Spawner) {
..Default::default() ..Default::default()
}); });
// Build driver and USB stack using 'static buffers // Build driver and USB stack using 'static buffers
let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11); let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11);
let mut probe_gnd = Flex::new(p.PB1);
probe_gnd.set_as_input(Pull::None);
// 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;
let slot = if res_pb3.unwrap_or(false) { SensorSlot::B } else { SensorSlot::A };
let mut addr: u8 = 0;
if res_pb4.unwrap_or(false) { addr |= 1; }
if res_pb5.unwrap_or(false) { addr |= 2; }
if res_pb6.unwrap_or(false) { addr |= 4; }
if res_pb7.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 invalid_config = res_pb3.is_none() || res_pb4.is_none() || res_pb5.is_none() || res_pb6.is_none() || res_pb7.is_none();
let mut config = embassy_usb::Config::new(0xC0DE, 0xCAFE); let mut config = embassy_usb::Config::new(0xC0DE, 0xCAFE);
config.manufacturer = Some("Embassy"); config.manufacturer = Some("Can Sensor v0.2");
config.product = Some("USB-serial example"); let msg = mk_static!(heapless::String<128>, heapless::String::new());;
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.serial_number = Some("12345678");
config.max_power = 100; config.max_power = 100;
config.max_packet_size_0 = 64; config.max_packet_size_0 = 64;
@@ -84,75 +117,121 @@ async fn main(spawner: Spawner) {
let mut builder = Builder::new( let mut builder = Builder::new(
driver, driver,
config, config,
mk_static!([u8;256], [0; 256]), mk_static!([u8; 256], [0; 256]),
mk_static!([u8;256], [0; 256]), mk_static!([u8; 256], [0; 256]),
&mut [], // no msos descriptors &mut [], // no msos descriptors
mk_static!([u8;64], [0; 64]), mk_static!([u8; 64], [0; 64]),
); );
// Initialize CDC state and create CDC-ACM class // Initialize CDC state and create CDC-ACM class
let class = mk_static!(CdcAcmClass<'static, Driver<'static, hal::peripherals::USBD>>, let class = mk_static!(
CdcAcmClass::new( CdcAcmClass<'static, Driver<'static, hal::peripherals::USBD>>,
&mut builder, CdcAcmClass::new(&mut builder, mk_static!(State, State::new()), 64)
mk_static!(State, State::new()),
64
)
); );
// Build USB device // Build USB device
let usb = mk_static!(UsbDevice<Driver<USBD>>, builder.build()) ; 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 led = Output::new(p.PB2, Level::Low, Speed::Low);
let info = Output::new(p.PA3, Level::Low, Speed::Low);
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 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;
};
// 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) // Create ADC on ADC1 and use PA1 as analog input (Threshold/Trigger)
let adc = Adc::new(p.ADC1, Default::default()); let adc = Adc::new(p.ADC1, Default::default());
let ain = p.PA1; let ain = p.PA1;
let config = can::can::Config::default(); 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"); 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)); ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
spawner.spawn(usb_task(usb)).unwrap(); spawner.spawn(usb_task(usb)).unwrap();
spawner.spawn(usb_writer(class)).unwrap(); spawner.spawn(usb_writer(class)).unwrap();
// move Q output, LED, ADC and analog input into worker task // move Q output, LED, ADC and analog input into worker task
spawner.spawn(worker(q_out, led, adc, ain, can)).unwrap(); spawner.spawn(worker(probe_gnd, q_out, info, adc, ain, can, StandardId::new(moisture_id).unwrap(), StandardId::new(identify_id).unwrap())).unwrap();
// Prevent main from exiting // Prevent main from exiting
core::future::pending::<()>().await; core::future::pending::<()>().await;
} }
// 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> {
pin.set_as_input(Pull::Down);
Timer::after_millis(2).await;
let low_read = pin.is_high();
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 }
}
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 {
info_led.set_high();
Timer::after_millis(200).await;
info_led.set_low();
Timer::after_millis(200).await;
}
// Pause between sequences
Timer::after_secs(2).await;
}
}
#[task] #[task]
async fn worker( async fn worker(
mut probe_gnd: Flex<'static>,
mut q: Output<'static>, mut q: Output<'static>,
mut led: Output<'static>, mut info: Output<'static>,
mut adc: Adc<'static, hal::peripherals::ADC1>, mut adc: Adc<'static, hal::peripherals::ADC1>,
mut ain: hal::peripherals::PA1, mut ain: hal::peripherals::PA1,
mut can: Can<'static, CAN1, NonBlocking>, mut can: Can<'static, CAN1, NonBlocking>,
moisture_id: StandardId,
identify_id: StandardId
) { ) {
// 555 emulation state: Q initially Low // 555 emulation state: Q initially Low
let mut q_high = false; let mut q_high = false;
let low_th: u16 = (ADC_MAX as u16) / 3; // ~1/3 Vref 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 high_th: u16 = ((ADC_MAX as u32 * 2) / 3) as u16; // ~2/3 Vref
let moisture_address = StandardId::new(plant_id(MOISTURE_DATA_OFFSET, SensorSlot::A, 0)).unwrap();
let identity_address = StandardId::new(plant_id(IDENTIFY_CMD_OFFSET, SensorSlot::A, 0)).unwrap();
let mut filter = CanFilter::new_id_list(); let mut filter = CanFilter::new_id_list();
filter filter
.get(0) .get(0)
.unwrap() .unwrap()
.set(identity_address.into(), Default::default()); .set(identify_id.into(), Default::default());
can.add_filter(filter);
//can.add_filter(CanFilter::accept_all());
//can.add_filter(filter);
can.add_filter(CanFilter::accept_all());
loop { loop {
// Count rising edges of Q in a 100 ms window // Count rising edges of Q in a 100 ms window
@@ -160,8 +239,13 @@ async fn worker(
let mut pulses: u32 = 0; let mut pulses: u32 = 0;
let mut last_q = q_high; let mut last_q = q_high;
while Instant::now().checked_duration_since(start).unwrap_or(Duration::from_millis(0)) probe_gnd.set_as_output(Speed::Low);
< Duration::from_millis(1000) probe_gnd.set_low();
let probe_duration = Duration::from_millis(1000);
while Instant::now()
.checked_duration_since(start)
.unwrap_or(Duration::from_millis(0))
< probe_duration
{ {
// Sample the analog input (Threshold/Trigger on A1) // Sample the analog input (Threshold/Trigger on A1)
let val: u16 = adc.convert(&mut ain, SampleTime::CYCLES28_5); let val: u16 = adc.convert(&mut ain, SampleTime::CYCLES28_5);
@@ -192,6 +276,7 @@ async fn worker(
// Yield to allow USB and other tasks to run // Yield to allow USB and other tasks to run
yield_now().await; yield_now().await;
} }
probe_gnd.set_as_input(Pull::None);
// Compute frequency from 100 ms window // Compute frequency from 100 ms window
let freq_hz = pulses; // pulses per 0.1s => Hz let freq_hz = pulses; // pulses per 0.1s => Hz
@@ -199,65 +284,69 @@ async fn worker(
let mut msg: heapless::String<128> = heapless::String::new(); let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!( let _ = write!(
&mut msg, &mut msg,
"555 window=100ms pulses={} freq={} Hz (A1->Q on PB0)\r\n", "555 window={}ms pulses={} freq={} Hz (A1->Q on PB0)\r\n", probe_duration.as_millis(),
pulses, freq_hz pulses, freq_hz
); );
log(msg); log(msg);
let mut moisture = CanFrame::new(moisture_id, &(freq_hz as u16).to_be_bytes()).unwrap();
let mut moisture = CanFrame::new(moisture_address, &[freq_hz as u8]).unwrap(); match can.transmit(&mut moisture) {
match can.transmit(&mut moisture){
Ok(..) => { Ok(..) => {
let mut msg: heapless::String<128> = heapless::String::new(); let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!( let _ = write!(&mut msg, "Send to canbus");
&mut msg,
"Send to canbus"
);
log(msg); log(msg);
} }
Err(err) => { Err(err) => {
for _ in 0..3 {
info.set_high();
Timer::after_millis(100).await;
info.set_low();
Timer::after_millis(100).await;
}
let mut msg: heapless::String<128> = heapless::String::new(); let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!( let _ = write!(&mut msg, "err {:?}", err);
&mut msg,
"err {:?}"
,err
);
log(msg); log(msg);
} }
} }
loop { loop {
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(
&mut msg,
"Check identity addr received: {:#x} \r\n",
identify_id.as_raw()
);
log(msg);
yield_now().await; yield_now().await;
match can.receive() { match can.receive() {
Ok(frame) => { Ok(frame) => match frame.id() {
match frame.id() { Id::Standard(s_frame) => {
Id::Standard(s_frame) => { let mut msg: heapless::String<128> = heapless::String::new();
let mut msg: heapless::String<128> = heapless::String::new(); let _ = write!(
let _ = write!( &mut msg,
&mut msg, "Received from canbus: {:?} ident is {:?} \r\n",
"Received from canbus: {:?} ident is {:?} \r\n", s_frame.as_raw(),
s_frame.as_raw(), identify_id.as_raw()
identity_address.as_raw() );
); log(msg);
log(msg); if s_frame.as_raw() == identify_id.as_raw() {
if s_frame.as_raw() == identity_address.as_raw() { for _ in 0..10 {
for _ in 0..10 { Timer::after_millis(250).await;
Timer::after_millis(250).await; info.toggle();
led.toggle();
}
led.set_low();
} }
info.set_low();
} }
Id::Extended(_) => {}
} }
} Id::Extended(_) => {}
_ => { },
Err(err) => {
break; break;
} }
} }
} }
} }
} }
@@ -275,10 +364,9 @@ async fn usb_task(usb: &'static mut UsbDevice<'static, Driver<'static, hal::peri
#[task] #[task]
async fn usb_writer( async fn usb_writer(
class: &'static mut CdcAcmClass<'static, Driver<'static, hal::peripherals::USBD>> class: &'static mut CdcAcmClass<'static, Driver<'static, hal::peripherals::USBD>>,
) { ) {
loop { loop {
class.wait_connection().await; class.wait_connection().await;
printer(class).await; printer(class).await;
} }

8
Software/MainBoard/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
Software/MainBoard/.idea/MainBoard.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<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" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
Software/MainBoard/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/MainBoard.iml" filepath="$PROJECT_DIR$/.idea/MainBoard.iml" />
</modules>
</component>
</project>

7
Software/MainBoard/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/../../website/themes/blowfish" vcs="Git" />
</component>
</project>

172
Software/MainBoard/d1.json Normal file
View File

@@ -0,0 +1,172 @@
{
"hardware": {
"board": "V4",
"battery": "Disabled"
},
"network": {
"max_wait": 10000,
"ap_ssid": "PlantCtrl Init",
"ssid": "private",
"password": "wertertzu",
"mqtt_url": "",
"mqtt_user": null,
"mqtt_password": null,
"base_topic": ""
},
"tank": {
"tank_allow_pumping_if_sensor_error": true,
"tank_empty_percent": 5,
"tank_full_percent": 95,
"tank_sensor_enabled": false,
"tank_useable_ml": 50000,
"tank_warn_percent": 40,
"ml_per_pulse": 0
},
"night_lamp": {
"night_lamp_hour_start": 19,
"night_lamp_hour_end": 2,
"night_lamp_only_when_dark": true,
"enabled": true,
"low_soc_cutoff": 30,
"low_soc_restore": 50
},
"plants": [
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_b": false,
"sensor_a": true,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
}
],
"timezone": "Europe/Berlin"
}

180
Software/MainBoard/d2.json Normal file
View File

@@ -0,0 +1,180 @@
{
"hardware": {
"board": "V4",
"battery": "Disabled"
},
"network": {
"ap_ssid": "PlantCtrl Init",
"ssid": "private",
"password": "wertertzu",
"mqtt_url": "",
"base_topic": "",
"mqtt_user": null,
"mqtt_password": null,
"max_wait": 10000
},
"tank": {
"tank_sensor_enabled": false,
"tank_allow_pumping_if_sensor_error": true,
"tank_useable_ml": 50000,
"tank_warn_percent": 40,
"tank_empty_percent": 5,
"tank_full_percent": 95,
"ml_per_pulse": 0.0
},
"night_lamp": {
"enabled": true,
"night_lamp_hour_start": 19,
"night_lamp_hour_end": 2,
"night_lamp_only_when_dark": true,
"low_soc_cutoff": 30,
"low_soc_restore": 50
},
"plants": [
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
},
{
"mode": "Off",
"target_moisture": 40,
"min_moisture": 30,
"pump_time_s": 30,
"pump_limit_ml": 5000,
"pump_cooldown_min": 60,
"pump_hour_start": 9,
"pump_hour_end": 20,
"sensor_a": true,
"sensor_b": false,
"max_consecutive_pump_count": 10,
"moisture_sensor_min_frequency": null,
"moisture_sensor_max_frequency": null,
"min_pump_current_ma": 10,
"max_pump_current_ma": 3000,
"ignore_current_error": true
}
],
"timezone": "Europe/Berlin"
}

View File

@@ -3,6 +3,7 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <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> </component>
</project> </project>

View File

@@ -37,19 +37,27 @@ partition_table = "partitions.csv"
[dependencies] [dependencies]
# Shared CAN API # Project/Shared
canapi = { path = "../../Shared/canapi" } canapi = { path = "../../Shared/canapi" }
#ESP stuff
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] }
esp-hal = { version = "=1.0.0-rc.0", features = [
"esp32c6",
"log-04",
"unstable",
"rt"
] }
log = "0.4.27"
embassy-net = { version = "0.7.1", default-features = false, features = [ # 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"] }
# 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"] }
# Networking and protocol stacks
embassy-net = { version = "0.7.1", features = [
"dhcpv4", "dhcpv4",
"log", "log",
"medium-ethernet", "medium-ethernet",
@@ -58,36 +66,6 @@ embassy-net = { version = "0.7.1", default-features = false, features = [
"proto-ipv4", "proto-ipv4",
"dns" "dns"
] } ] }
embedded-io = "0.6.1"
embedded-io-async = "0.6.1"
esp-alloc = "0.8.0"
esp-backtrace = { version = "0.17.0", features = [
"esp32c6",
"exception-handler",
"panic-handler",
"println",
"colors",
"custom-halt"
] }
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
# for more networking protocol support see https://crates.io/crates/edge-net
embassy-executor = { version = "0.7.0", features = [
"log",
"task-arena-size-64",
"nightly"
] }
embassy-time = { version = "0.5.0", features = ["log"], default-features = false }
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6", "log-04"] }
esp-storage = { version = "0.7.0", features = ["esp32c6"] }
esp-wifi = { version = "0.15.0", features = [
"builtin-scheduler",
"esp-alloc",
"esp32c6",
"log-04",
"smoltcp",
"wifi",
] }
smoltcp = { version = "0.12.0", default-features = false, features = [ smoltcp = { version = "0.12.0", default-features = false, features = [
"alloc", "alloc",
"log", "log",
@@ -103,60 +81,59 @@ smoltcp = { version = "0.12.0", default-features = false, features = [
"socket-tcp", "socket-tcp",
"socket-udp", "socket-udp",
] } ] }
#static_cell = "2.1.1" sntpc = { version = "0.6.1", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
embedded-hal = "1.0.0"
embedded-hal-bus = { version = "0.3.0" }
#Hardware additional driver
#bq34z100 = { version = "0.3.0", default-features = false }
onewire = "0.4.0"
#strum = { version = "0.27.0", default-feature = false, features = ["derive"] }
measurements = "0.11.0"
ds323x = "0.6.0"
#json
serde = { version = "1.0.219", features = ["derive", "alloc"], default-features = false }
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"] }
chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
chrono-tz = { version = "0.10.4", default-features = false, features = ["filter-by-regex"] }
eeprom24x = "0.7.2"
crc = "3.2.1"
strum_macros = "0.27.0"
unit-enum = "1.4.1"
pca9535 = { version = "2.0.0" }
ina219 = { version = "0.2.0" }
embedded-storage = "=0.3.1"
portable-atomic = "1.11.1"
embassy-sync = { version = "0.7.2", features = ["log"] }
async-trait = "0.1.89"
bq34z100 = { version = "0.4.0", default-features = false }
edge-dhcp = "0.6.0" edge-dhcp = "0.6.0"
edge-nal = "0.5.0" edge-nal = "0.5.0"
edge-nal-embassy = "0.6.0" edge-nal-embassy = "0.6.0"
static_cell = "2.1.1"
edge-http = { version = "0.6.1", features = ["log"] } edge-http = { version = "0.6.1", features = ["log"] }
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
littlefs2-core = "0.1.1" # Hardware abstraction traits and HAL adapters
bytemuck = { version = "1.23.2", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] } embedded-hal = "1.0.0"
deranged = "0.5.3" embedded-storage = "0.3.1"
embassy-embedded-hal = "0.5.0" embassy-embedded-hal = "0.5.0"
bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
sntpc = { version = "0.6.0", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
option-lock = { version = "0.3.1", default-features = false }
#stay in sync with mcutie version here!
heapless = { version = "0.7.17", features = ["serde"] }
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
nb = "1.1.0"
embedded-can = "0.4.1" 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" }
# Storage and filesystem
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
littlefs2-core = "0.1.2"
# 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"] }
# Time and time zones
chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
chrono-tz = { version = "0.10.4", default-features = false, features = ["filter-by-regex"] }
# Utilities and pure functional code (no hardware I/O)
heapless = { version = "0.7.17", features = ["serde"] } # stay in sync with mcutie version
static_cell = "2.1.1"
portable-atomic = "1.11.1"
crc = "3.3.0"
bytemuck = { version = "1.24.0", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] }
deranged = "0.5.5"
strum_macros = "0.27.2"
unit-enum = "1.4.3"
async-trait = "0.1.89"
option-lock = { version = "0.3.1", default-features = false }
measurements = "0.11.1"
# Project-specific
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
[patch.crates-io] [patch.crates-io]
mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' } mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' }
#bq34z100 = { path = "../../bq34z100_rust" } #bq34z100 = { path = "../../bq34z100_rust" }
[build-dependencies] [build-dependencies]
vergen = { version = "8.2.6", features = ["build", "git", "gitcl"] } vergen = { version = "8.3.2", features = ["build", "git", "gitcl"] }

View File

@@ -2,6 +2,11 @@
rm ./src/webserver/index.html.gz rm ./src/webserver/index.html.gz
rm ./src/webserver/bundle.js.gz rm ./src/webserver/bundle.js.gz
rm ./src_webpack/index.html.gz
rm ./src_webpack/bundle.js.gz
rm ./src_webpack/index.html
rm ./src_webpack/bundle.js
set -e set -e
pushd ./src_webpack/ pushd ./src_webpack/

View File

@@ -83,14 +83,12 @@ impl Default for TankConfig {
pub enum BatteryBoardVersion { pub enum BatteryBoardVersion {
#[default] #[default]
Disabled, Disabled,
BQ34Z100G1,
WchI2cSlave, WchI2cSlave,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub enum BoardVersion { pub enum BoardVersion {
Initial,
#[default] #[default]
INITIAL,
V3,
V4, V4,
} }
@@ -115,8 +113,8 @@ pub struct PlantControllerConfig {
#[serde(default)] #[serde(default)]
pub struct PlantConfig { pub struct PlantConfig {
pub mode: PlantWateringMode, pub mode: PlantWateringMode,
pub target_moisture: f32, pub target_moisture: u8,
pub min_moisture: f32, pub min_moisture: u8,
pub pump_time_s: u16, pub pump_time_s: u16,
pub pump_limit_ml: u16, pub pump_limit_ml: u16,
pub pump_cooldown_min: u16, pub pump_cooldown_min: u16,
@@ -125,8 +123,8 @@ pub struct PlantConfig {
pub sensor_a: bool, pub sensor_a: bool,
pub sensor_b: bool, pub sensor_b: bool,
pub max_consecutive_pump_count: u8, pub max_consecutive_pump_count: u8,
pub moisture_sensor_min_frequency: Option<f32>, // Optional min frequency pub moisture_sensor_min_frequency: Option<u16>, // Optional min frequency
pub moisture_sensor_max_frequency: Option<f32>, // Optional max frequency pub moisture_sensor_max_frequency: Option<u16>, // Optional max frequency
pub min_pump_current_ma: u16, pub min_pump_current_ma: u16,
pub max_pump_current_ma: u16, pub max_pump_current_ma: u16,
pub ignore_current_error: bool, pub ignore_current_error: bool,
@@ -135,9 +133,9 @@ pub struct PlantConfig {
impl Default for PlantConfig { impl Default for PlantConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
mode: PlantWateringMode::OFF, mode: PlantWateringMode::Off,
target_moisture: 40., target_moisture: 40,
min_moisture: 30., min_moisture: 30,
pump_time_s: 30, pump_time_s: 30,
pump_limit_ml: 5000, pump_limit_ml: 5000,
pump_cooldown_min: 60, pump_cooldown_min: 60,

View File

@@ -2,6 +2,7 @@ use alloc::format;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use core::convert::Infallible; use core::convert::Infallible;
use core::fmt; use core::fmt;
use core::fmt::Debug;
use core::str::Utf8Error; use core::str::Utf8Error;
use embassy_embedded_hal::shared_bus::I2cDeviceError; use embassy_embedded_hal::shared_bus::I2cDeviceError;
use embassy_executor::SpawnError; use embassy_executor::SpawnError;
@@ -10,8 +11,9 @@ use embedded_storage::nor_flash::NorFlashErrorKind;
use esp_hal::i2c::master::ConfigError; use esp_hal::i2c::master::ConfigError;
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit}; use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
use esp_hal::twai::EspTwaiError; use esp_hal::twai::EspTwaiError;
use esp_wifi::wifi::WifiError; use esp_radio::wifi::WifiError;
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError}; use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
use lib_bms_protocol::BmsProtocolError;
use littlefs2_core::PathError; use littlefs2_core::PathError;
use onewire::Error; use onewire::Error;
use pca9535::ExpanderError; use pca9535::ExpanderError;
@@ -19,6 +21,9 @@ use pca9535::ExpanderError;
//All error superconstruct //All error superconstruct
#[derive(Debug)] #[derive(Debug)]
pub enum FatError { pub enum FatError {
BMSError {
error: String,
},
OneWireError { OneWireError {
error: Error<Infallible>, error: Error<Infallible>,
}, },
@@ -78,28 +83,29 @@ impl fmt::Display for FatError {
FatError::SpawnError { error } => { FatError::SpawnError { error } => {
write!(f, "SpawnError {:?}", error.to_string()) write!(f, "SpawnError {:?}", error.to_string())
} }
FatError::OneWireError { error } => write!(f, "OneWireError {:?}", error), FatError::OneWireError { error } => write!(f, "OneWireError {error:?}"),
FatError::String { error } => write!(f, "{}", error), FatError::String { error } => write!(f, "{error}"),
FatError::LittleFSError { error } => write!(f, "LittleFSError {:?}", error), FatError::LittleFSError { error } => write!(f, "LittleFSError {error:?}"),
FatError::PathError { error } => write!(f, "PathError {:?}", error), FatError::PathError { error } => write!(f, "PathError {error:?}"),
FatError::TryLockError { error } => write!(f, "TryLockError {:?}", error), FatError::TryLockError { error } => write!(f, "TryLockError {error:?}"),
FatError::WifiError { error } => write!(f, "WifiError {:?}", error), FatError::WifiError { error } => write!(f, "WifiError {error:?}"),
FatError::SerdeError { error } => write!(f, "SerdeError {:?}", error), FatError::SerdeError { error } => write!(f, "SerdeError {error:?}"),
FatError::PreconditionFailed { error } => write!(f, "PreconditionFailed {:?}", error), FatError::PreconditionFailed { error } => write!(f, "PreconditionFailed {error:?}"),
FatError::PartitionError { error } => { FatError::PartitionError { error } => {
write!(f, "PartitionError {:?}", error) write!(f, "PartitionError {error:?}")
} }
FatError::NoBatteryMonitor => { FatError::NoBatteryMonitor => {
write!(f, "No Battery Monitor") write!(f, "No Battery Monitor")
} }
FatError::I2CConfigError { error } => write!(f, "I2CConfigError {:?}", error), FatError::I2CConfigError { error } => write!(f, "I2CConfigError {error:?}"),
FatError::DS323 { error } => write!(f, "DS323 {:?}", error), FatError::DS323 { error } => write!(f, "DS323 {error:?}"),
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error), FatError::Eeprom24x { error } => write!(f, "Eeprom24x {error:?}"),
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error), FatError::ExpanderError { error } => write!(f, "ExpanderError {error:?}"),
FatError::CanBusError { error } => { FatError::CanBusError { error } => {
write!(f, "CanBusError {:?}", error) write!(f, "CanBusError {error:?}")
} }
FatError::SNTPError { error } => write!(f, "SNTPError {:?}", error), FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"),
FatError::BMSError { error } => write!(f, "BMSError, {error}"),
} }
} }
} }
@@ -194,34 +200,34 @@ 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 { fn from(value: edge_http::io::Error<E>) -> Self {
FatError::String { FatError::String {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
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 { fn from(value: ds323x::Error<E>) -> Self {
FatError::DS323 { FatError::DS323 {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
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 { fn from(value: eeprom24x::Error<E>) -> Self {
FatError::Eeprom24x { FatError::Eeprom24x {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
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 { fn from(value: ExpanderError<I2cDeviceError<E>>) -> Self {
FatError::ExpanderError { FatError::ExpanderError {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
@@ -229,7 +235,7 @@ impl<E: core::fmt::Debug> From<ExpanderError<I2cDeviceError<E>>> for FatError {
impl From<bincode::error::DecodeError> for FatError { impl From<bincode::error::DecodeError> for FatError {
fn from(value: bincode::error::DecodeError) -> Self { fn from(value: bincode::error::DecodeError) -> Self {
FatError::Eeprom24x { FatError::Eeprom24x {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
@@ -237,7 +243,7 @@ impl From<bincode::error::DecodeError> for FatError {
impl From<bincode::error::EncodeError> for FatError { impl From<bincode::error::EncodeError> for FatError {
fn from(value: bincode::error::EncodeError) -> Self { fn from(value: bincode::error::EncodeError) -> Self {
FatError::Eeprom24x { FatError::Eeprom24x {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
@@ -248,25 +254,25 @@ 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 { fn from(value: I2cDeviceError<E>) -> Self {
FatError::String { FatError::String {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
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 { fn from(value: BusVoltageReadError<I2cDeviceError<E>>) -> Self {
FatError::String { FatError::String {
error: format!("{:?}", value), 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 { fn from(value: ShuntVoltageReadError<I2cDeviceError<E>>) -> Self {
FatError::String { FatError::String {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
@@ -280,14 +286,14 @@ impl From<Infallible> for FatError {
impl From<InvalidLowLimit> for FatError { impl From<InvalidLowLimit> for FatError {
fn from(value: InvalidLowLimit) -> Self { fn from(value: InvalidLowLimit) -> Self {
FatError::String { FatError::String {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
impl From<InvalidHighLimit> for FatError { impl From<InvalidHighLimit> for FatError {
fn from(value: InvalidHighLimit) -> Self { fn from(value: InvalidHighLimit) -> Self {
FatError::String { FatError::String {
error: format!("{:?}", value), error: format!("{value:?}"),
} }
} }
} }
@@ -316,3 +322,14 @@ impl From<sntpc::Error> for FatError {
FatError::SNTPError { error: value } 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()}
}
}
}
}

View File

@@ -1,39 +1,30 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::Box; use crate::hal::Box;
use crate::fat_error::{FatError, FatResult};
use async_trait::async_trait; use async_trait::async_trait;
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use esp_hal::delay::Delay;
use esp_hal::i2c::master::I2c; use esp_hal::i2c::master::I2c;
use esp_hal::Blocking; use esp_hal::Blocking;
use measurements::Temperature; use lib_bms_protocol::{BatteryState as bstate, BmsReadable, Config, FirmwareVersion, ProtocolVersion};
use serde::Serialize; use serde::Serialize;
#[async_trait(?Send)] #[async_trait(?Send)]
pub trait BatteryInteraction { pub trait BatteryInteraction {
async fn state_charge_percent(&mut self) -> FatResult<f32>; async fn get_state(&mut self) -> FatResult<BatteryState>;
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>; async fn get_firmware(&mut self) -> FatResult<FirmwareVersion>;
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16>; async fn get_protocol(&mut self) -> FatResult<ProtocolVersion>;
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16>; async fn reset(&mut self) -> FatResult<()>;
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>;
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct BatteryInfo { pub struct BatteryInfo {
pub voltage_milli_volt: u16, pub voltage_milli_volt: u32,
pub average_current_milli_ampere: i16, pub average_current_milli_ampere: i32,
pub cycle_count: u16, pub design_milli_ampere_hour: u32,
pub design_milli_ampere_hour: u16, pub remaining_milli_ampere_hour: u32,
pub remaining_milli_ampere_hour: u16, pub state_of_charge: u8,
pub state_of_charge: f32, pub state_of_health: u32,
pub state_of_health: u16, pub temperature: i32,
pub temperature: u16,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -46,213 +37,62 @@ pub enum BatteryState {
pub struct NoBatteryMonitor {} pub struct NoBatteryMonitor {}
#[async_trait(?Send)] #[async_trait(?Send)]
impl BatteryInteraction for NoBatteryMonitor { impl BatteryInteraction for NoBatteryMonitor {
async fn state_charge_percent(&mut self) -> FatResult<f32> { async fn get_state(&mut self) -> FatResult<BatteryState> {
// 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> {
Ok(BatteryState::Unknown) 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 //TODO implement this battery monitor kind once controller is complete
#[allow(dead_code)] #[allow(dead_code)]
pub struct WchI2cSlave {} pub struct WchI2cSlave {}
pub type I2cDev = I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>; pub struct WCHI2CSlave<'a> {
pub(crate) i2c: I2cDevice<'a, CriticalSectionRawMutex, I2c<'a, Blocking>>
pub struct BQ34Z100G1 {
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
} }
#[async_trait(?Send)] #[async_trait(?Send)]
impl BatteryInteraction for BQ34Z100G1 { impl BatteryInteraction for WCHI2CSlave<'_> {
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),
})
}
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.remaining_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> { async fn get_state(&mut self) -> FatResult<BatteryState> {
self.battery_driver let state = bstate::read_from_i2c(&mut self.i2c)?;
.full_charge_capacity() let config = Config::read_from_i2c(&mut self.i2c)?;
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> { let state_of_charge = (state.remaining_capacity_mah * 100 / state.lifetime_capacity_mah) as u8;
self.battery_driver let state_of_health = state.lifetime_capacity_mah / config.capacity_mah * 100;
.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 { Ok(BatteryState::Info(BatteryInfo {
voltage_milli_volt: self.voltage_milli_volt().await?, voltage_milli_volt: state.current_mv,
average_current_milli_ampere: self.average_current_milli_ampere().await?, average_current_milli_ampere: 1337,
cycle_count: self.cycle_count().await?, design_milli_ampere_hour: config.capacity_mah,
design_milli_ampere_hour: self.design_milli_ampere_hour().await?, remaining_milli_ampere_hour: state.remaining_capacity_mah,
remaining_milli_ampere_hour: self.remaining_milli_ampere_hour().await?, state_of_charge,
state_of_charge: self.state_charge_percent().await?, state_of_health,
state_of_health: self.state_health_percent().await?, temperature: state.temperature_celcius,
temperature: self.bat_temperature().await?,
})) }))
} }
}
pub fn print_battery_bq34z100( async fn get_firmware(&mut self) -> FatResult<FirmwareVersion> {
battery_driver: &mut Bq34z100g1Driver<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>, Delay>, todo!()
) -> 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!");
} }
let flags = battery_driver.get_flags_decoded().unwrap_or(Flags { async fn get_protocol(&mut self) -> FatResult<ProtocolVersion> {
fast_charge_allowed: false, todo!()
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);
let chem_id = battery_driver.chem_id().unwrap_or_else(|e| { async fn reset(&mut self) -> FatResult<()> {
log::info!("Chemid {:?}", e); todo!()
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: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current);
let _ = battery_driver.unsealed();
let _ = battery_driver.it_enable();
Ok(())
}

View File

@@ -7,6 +7,7 @@ use serde::Serialize;
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::shared_flash::MutexFlashStorage;
use alloc::string::ToString; use alloc::string::ToString;
use alloc::sync::Arc; use alloc::sync::Arc;
use alloc::{format, string::String, vec, vec::Vec}; use alloc::{format, string::String, vec, vec::Vec};
@@ -22,8 +23,9 @@ use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer, WithTimeout}; use embassy_time::{Duration, Timer, WithTimeout};
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash}; use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::ota::OtaImageState::Valid; use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState, Slot}; use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::partitions::FlashRegion; use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
use esp_hal::Blocking;
use esp_hal::gpio::{Input, RtcPinWithResistors}; use esp_hal::gpio::{Input, RtcPinWithResistors};
use esp_hal::rng::Rng; use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{ use esp_hal::rtc_cntl::{
@@ -31,15 +33,15 @@ use esp_hal::rtc_cntl::{
Rtc, Rtc,
}; };
use esp_hal::system::software_reset; use esp_hal::system::software_reset;
use esp_hal::uart::Uart;
use esp_println::println; use esp_println::println;
use esp_storage::FlashStorage; use esp_radio::wifi::{
use esp_wifi::wifi::{ AccessPointConfig, AccessPointInfo, AuthMethod, ClientConfig, ModeConfig, ScanConfig,
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration, ScanTypeConfig, WifiController, WifiDevice, WifiStaState,
ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState,
}; };
use littlefs2::fs::Filesystem; use littlefs2::fs::Filesystem;
use littlefs2_core::{FileType, PathBuf, SeekFrom}; use littlefs2_core::{FileType, PathBuf, SeekFrom};
use log::{info, warn}; use log::{error, info, warn};
use mcutie::{ use mcutie::{
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable, Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
QoS, Topic, QoS, Topic,
@@ -49,13 +51,13 @@ use smoltcp::socket::udp::PacketMetadata;
use smoltcp::wire::DnsQueryType; use smoltcp::wire::DnsQueryType;
use sntpc::{get_time, NtpContext, NtpTimestampGenerator}; use sntpc::{get_time, NtpContext, NtpTimestampGenerator};
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LOW_VOLTAGE_DETECTED: i8 = 0; static mut LOW_VOLTAGE_DETECTED: i8 = 0;
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut RESTART_TO_CONF: i8 = 0; static mut RESTART_TO_CONF: i8 = 0;
const CONFIG_FILE: &str = "config.json"; const CONFIG_FILE: &str = "config.json";
@@ -126,10 +128,11 @@ pub struct Esp<'a> {
// RTC-capable GPIO used as external wake source (store the raw peripheral) // RTC-capable GPIO used as external wake source (store the raw peripheral)
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>, pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
pub uart0: Uart<'a, Blocking>,
pub ota: Ota<'static, FlashStorage>, pub ota: Ota<'static, MutexFlashStorage>,
pub ota_target: &'static mut FlashRegion<'static, FlashStorage>, pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>,
pub current: Slot, pub current: AppPartitionSubType,
pub slot0_state: OtaImageState, pub slot0_state: OtaImageState,
pub slot1_state: OtaImageState, pub slot1_state: OtaImageState,
} }
@@ -146,16 +149,47 @@ macro_rules! mk_static {
($t:ty,$val:expr) => {{ ($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)] #[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val)); let x = STATIC_CELL.uninit().write($val);
x x
}}; }};
} }
impl Esp<'_> { impl Esp<'_> {
pub(crate) async fn read_serial_line(&mut self) -> FatResult<Option<alloc::string::String>> {
let mut buf = [0u8; 1];
let mut line = String::new();
loop {
match self.uart0.read_buffered(&mut buf) {
Ok(read) => {
if (read == 0) {
return Ok(None);
}
let c = buf[0] as char;
if c == '\n' {
return Ok(Some(line));
}
line.push(c);
}
Err(error ) => {
if line.is_empty() {
return Ok(None);
} else {
error!("Error reading serial line: {error:?}");
// If we already have some data, we should probably wait a bit or just return what we have?
// But the protocol expects a full line or message.
// For simplicity in config mode, we can block here or just return None if nothing is there yet.
// However, if we started receiving, we should probably finish or timeout.
continue;
}
}
}
}
}
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> { pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
let file = PathBuf::try_from(filename.as_str())?; let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await; let access = self.fs.lock().await;
access.remove(&*file)?; access.remove(&file)?;
Ok(()) Ok(())
} }
pub(crate) async fn write_file( pub(crate) async fn write_file(
@@ -168,7 +202,7 @@ impl Esp<'_> {
let access = self.fs.lock().await; let access = self.fs.lock().await;
access.open_file_with_options_and_then( access.open_file_with_options_and_then(
|options| options.read(true).write(true).create(true), |options| options.read(true).write(true).create(true),
&*file, &file,
|file| { |file| {
file.seek(SeekFrom::Start(offset))?; file.seek(SeekFrom::Start(offset))?;
file.write(buf)?; file.write(buf)?;
@@ -181,7 +215,7 @@ impl Esp<'_> {
pub async fn get_size(&mut self, filename: String) -> FatResult<usize> { pub async fn get_size(&mut self, filename: String) -> FatResult<usize> {
let file = PathBuf::try_from(filename.as_str())?; let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await; let access = self.fs.lock().await;
let data = access.metadata(&*file)?; let data = access.metadata(&file)?;
Ok(data.len()) Ok(data.len())
} }
pub(crate) async fn get_file( pub(crate) async fn get_file(
@@ -198,7 +232,7 @@ impl Esp<'_> {
let offset = chunk * buf.len() as u32; let offset = chunk * buf.len() as u32;
access.open_file_with_options_and_then( access.open_file_with_options_and_then(
|options| options.read(true), |options| options.read(true),
&*file, &file,
|file| { |file| {
let length = file.len()? as u32; let length = file.len()? as u32;
if length == 0 { if length == 0 {
@@ -226,7 +260,7 @@ impl Esp<'_> {
self.ota_target.write(offset, buf)?; self.ota_target.write(offset, buf)?;
self.ota_target.read(offset, read_back)?; self.ota_target.read(offset, read_back)?;
if buf != read_back { if buf != read_back {
info!("Expected {:?} but got {:?}", buf, read_back); info!("Expected {buf:?} but got {read_back:?}");
bail!( bail!(
"Flash error, read back does not match write buffer at offset {:x}", "Flash error, read back does not match write buffer at offset {:x}",
offset offset
@@ -236,21 +270,24 @@ impl Esp<'_> {
} }
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> { pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
let current = self.ota.current_slot()?; let current = self.ota.current_app_partition()?;
if self.ota.current_ota_state()? != OtaImageState::Valid { if self.ota.current_ota_state()? != Valid {
info!( info!("Validating current slot {current:?} as it was able to ota");
"Validating current slot {:?} as it was able to ota",
current
);
self.ota.set_current_ota_state(Valid)?; self.ota.set_current_ota_state(Valid)?;
} }
let next = match current {
self.ota.set_current_slot(current.next())?; AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
_ => {
bail!("Invalid current slot {current:?} for ota");
}
};
self.ota.set_current_app_partition(next)?;
info!("switched slot"); info!("switched slot");
self.ota.set_current_ota_state(OtaImageState::New)?; self.ota.set_current_ota_state(OtaImageState::New)?;
info!("switched state for new partition"); info!("switched state for new partition");
let state_new = self.ota.current_ota_state()?; let state_new = self.ota.current_ota_state()?;
info!("state on new partition now {:?}", state_new); info!("state on new partition now {state_new:?}");
//determine nextslot crc //determine nextslot crc
self.set_restart_to_conf(true); self.set_restart_to_conf(true);
@@ -290,7 +327,7 @@ impl Esp<'_> {
if ntp_addrs.is_empty() { if ntp_addrs.is_empty() {
bail!("Failed to resolve DNS"); bail!("Failed to resolve DNS");
} }
info!("NTP server: {:?}", ntp_addrs); info!("NTP server: {ntp_addrs:?}");
let mut counter = 0; let mut counter = 0;
loop { loop {
@@ -302,12 +339,12 @@ impl Esp<'_> {
match timeout { match timeout {
Ok(result) => { Ok(result) => {
let time = result?; let time = result?;
info!("Time: {:?}", time); info!("Time: {time:?}");
return DateTime::from_timestamp(time.seconds as i64, 0) return DateTime::from_timestamp(time.seconds as i64, 0)
.context("Could not convert Sntp result"); .context("Could not convert Sntp result");
} }
Err(err) => { Err(err) => {
warn!("sntp timeout, retry: {:?}", err); warn!("sntp timeout, retry: {err:?}");
counter += 1; counter += 1;
if counter > 10 { if counter > 10 {
bail!("Failed to get time from NTP server"); bail!("Failed to get time from NTP server");
@@ -322,16 +359,10 @@ impl Esp<'_> {
info!("start wifi scan"); info!("start wifi scan");
let mut lock = self.controller.try_lock()?; let mut lock = self.controller.try_lock()?;
info!("start wifi scan lock"); info!("start wifi scan lock");
let scan_config = ScanConfig { let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active {
ssid: None, min: Default::default(),
bssid: None, max: Default::default(),
channel: None, });
show_hidden: false,
scan_type: ScanTypeConfig::Active {
min: Default::default(),
max: Default::default(),
},
};
let rv = lock.scan_with_config_async(scan_config).await?; let rv = lock.scan_with_config_async(scan_config).await?;
info!("end wifi scan lock"); info!("end wifi scan lock");
Ok(rv) Ok(rv)
@@ -381,14 +412,12 @@ impl Esp<'_> {
} }
} }
pub(crate) async fn wifi_ap(&mut self) -> FatResult<Stack<'static>> { pub(crate) async fn wifi_ap(&mut self, spawner: Spawner) -> FatResult<Stack<'static>> {
let ssid = match self.load_config().await { let ssid = match self.load_config().await {
Ok(config) => config.network.ap_ssid.as_str().to_string(), Ok(config) => config.network.ap_ssid.as_str().to_string(),
Err(_) => "PlantCtrl Emergency Mode".to_string(), Err(_) => "PlantCtrl Emergency Mode".to_string(),
}; };
let spawner = Spawner::for_current_executor().await;
let device = self.interface_ap.take().unwrap(); let device = self.interface_ap.take().unwrap();
let gw_ip_addr_str = "192.168.71.1"; let gw_ip_addr_str = "192.168.71.1";
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip"); let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
@@ -411,22 +440,16 @@ impl Esp<'_> {
); );
let stack = mk_static!(Stack, stack); let stack = mk_static!(Stack, stack);
let client_config = Configuration::AccessPoint(AccessPointConfiguration { let client_config =
ssid: ssid.clone(), ModeConfig::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
..Default::default() self.controller.lock().await.set_config(&client_config)?;
});
self.controller
.lock()
.await
.set_configuration(&client_config)?;
println!("start new"); println!("start new");
self.controller.lock().await.start()?; self.controller.lock().await.start()?;
println!("start net task"); println!("start net task");
spawner.spawn(net_task(runner)).ok(); spawner.spawn(net_task(runner)).ok();
println!("run dhcp"); println!("run dhcp");
spawner.spawn(run_dhcp(stack.clone(), gw_ip_addr_str)).ok(); spawner.spawn(run_dhcp(*stack, gw_ip_addr_str)).ok();
loop { loop {
if stack.is_link_up() { if stack.is_link_up() {
@@ -442,14 +465,15 @@ impl Esp<'_> {
.config_v4() .config_v4()
.inspect(|c| println!("ipv4 config: {c:?}")); .inspect(|c| println!("ipv4 config: {c:?}"));
Ok(stack.clone()) Ok(*stack)
} }
pub(crate) async fn wifi( pub(crate) async fn wifi(
&mut self, &mut self,
network_config: &NetworkConfig, network_config: &NetworkConfig,
spawner: Spawner,
) -> FatResult<Stack<'static>> { ) -> FatResult<Stack<'static>> {
esp_wifi::wifi_set_log_verbose(); esp_radio::wifi_set_log_verbose();
let ssid = network_config.ssid.clone(); let ssid = network_config.ssid.clone();
match &ssid { match &ssid {
Some(ssid) => { Some(ssid) => {
@@ -469,8 +493,6 @@ impl Esp<'_> {
}; };
let max_wait = network_config.max_wait; let max_wait = network_config.max_wait;
let spawner = Spawner::for_current_executor().await;
let device = self.interface_sta.take().unwrap(); let device = self.interface_sta.take().unwrap();
let config = embassy_net::Config::dhcpv4(DhcpConfig::default()); let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
@@ -485,17 +507,15 @@ impl Esp<'_> {
); );
let stack = mk_static!(Stack, stack); let stack = mk_static!(Stack, stack);
let client_config = Configuration::Client(ClientConfiguration { let client_config = ClientConfig::default()
ssid, .with_ssid(ssid)
bssid: None, .with_auth_method(AuthMethod::Wpa2Personal)
auth_method: AuthMethod::WPA2Personal, //FIXME read from config, fill via scan .with_password(password);
password,
channel: None,
});
self.controller self.controller
.lock() .lock()
.await .await
.set_configuration(&client_config)?; .set_config(&ModeConfig::Client(client_config))?;
spawner.spawn(net_task(runner)).ok(); spawner.spawn(net_task(runner)).ok();
self.controller.lock().await.start_async().await?; self.controller.lock().await.start_async().await?;
@@ -504,13 +524,10 @@ impl Esp<'_> {
guard.current_time_us() guard.current_time_us()
} + max_wait as u64 * 1000; } + max_wait as u64 * 1000;
loop { loop {
let state = esp_wifi::wifi::sta_state(); let state = esp_radio::wifi::sta_state();
match state { if state == WifiStaState::Started {
WifiState::StaStarted => { self.controller.lock().await.connect()?;
self.controller.lock().await.connect()?; break;
break;
}
_ => {}
} }
if { if {
let guard = TIME_ACCESS.get().await.lock().await; let guard = TIME_ACCESS.get().await.lock().await;
@@ -526,12 +543,9 @@ impl Esp<'_> {
guard.current_time_us() guard.current_time_us()
} + max_wait as u64 * 1000; } + max_wait as u64 * 1000;
loop { loop {
let state = esp_wifi::wifi::sta_state(); let state = esp_radio::wifi::sta_state();
match state { if state == WifiStaState::Connected {
WifiState::StaConnected => { break;
break;
}
_ => {}
} }
if { if {
let guard = TIME_ACCESS.get().await.lock().await; let guard = TIME_ACCESS.get().await.lock().await;
@@ -572,7 +586,7 @@ impl Esp<'_> {
} }
info!("Connected WIFI, dhcp: {:?}", stack.config_v4()); info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
Ok(stack.clone()) Ok(*stack)
} }
pub fn deep_sleep( pub fn deep_sleep(
@@ -580,14 +594,11 @@ impl Esp<'_> {
duration_in_ms: u64, duration_in_ms: u64,
mut rtc: MutexGuard<CriticalSectionRawMutex, Rtc>, mut rtc: MutexGuard<CriticalSectionRawMutex, Rtc>,
) -> ! { ) -> ! {
// Configure and enter deep sleep using esp-hal. Also keep prior behavior where
// duration_in_ms == 0 triggers an immediate reset.
// Mark the current OTA image as valid if we reached here while in pending verify. // Mark the current OTA image as valid if we reached here while in pending verify.
if let Ok(cur) = self.ota.current_ota_state() { if let Ok(cur) = self.ota.current_ota_state() {
if cur == OtaImageState::PendingVerify { if cur == OtaImageState::PendingVerify {
self.ota self.ota
.set_current_ota_state(OtaImageState::Valid) .set_current_ota_state(Valid)
.expect("Could not set image to valid"); .expect("Could not set image to valid");
} }
} }
@@ -611,12 +622,12 @@ impl Esp<'_> {
} }
let data = self.fs.lock().await.read::<4096>(&cfg)?; let data = self.fs.lock().await.read::<4096>(&cfg)?;
let config: PlantControllerConfig = serde_json::from_slice(&data)?; let config: PlantControllerConfig = serde_json::from_slice(&data)?;
return Ok(config); Ok(config)
} }
pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> { pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> {
let filesystem = self.fs.lock().await; let filesystem = self.fs.lock().await;
let cfg = PathBuf::try_from(CONFIG_FILE)?; let cfg = PathBuf::try_from(CONFIG_FILE)?;
filesystem.write(&cfg, &*config)?; filesystem.write(&cfg, &config)?;
Ok(()) Ok(())
} }
pub(crate) async fn list_files(&self) -> FatResult<FileList> { pub(crate) async fn list_files(&self) -> FatResult<FileList> {
@@ -690,19 +701,15 @@ impl Esp<'_> {
"", "",
) )
.await; .await;
for i in 0..PLANT_COUNT { // is executed before main, no other code will alter these values during printing
log::info!( #[allow(static_mut_refs)]
"LAST_WATERING_TIMESTAMP[{}] = UTC {}", for (i, time) in LAST_WATERING_TIMESTAMP.iter().enumerate() {
i, info!("LAST_WATERING_TIMESTAMP[{i}] = UTC {time}");
LAST_WATERING_TIMESTAMP[i]
);
} }
for i in 0..PLANT_COUNT { // is executed before main, no other code will alter these values during printing
log::info!( #[allow(static_mut_refs)]
"CONSECUTIVE_WATERING_PLANT[{}] = {}", for (i, item) in CONSECUTIVE_WATERING_PLANT.iter().enumerate() {
i, info!("CONSECUTIVE_WATERING_PLANT[{i}] = {item}");
CONSECUTIVE_WATERING_PLANT[i]
);
} }
} }
} }
@@ -712,6 +719,7 @@ impl Esp<'_> {
&mut self, &mut self,
network_config: &'static NetworkConfig, network_config: &'static NetworkConfig,
stack: Stack<'static>, stack: Stack<'static>,
spawner: Spawner,
) -> FatResult<()> { ) -> FatResult<()> {
let base_topic = network_config let base_topic = network_config
.base_topic .base_topic
@@ -734,9 +742,9 @@ impl Esp<'_> {
bail!("Mqtt url was empty") bail!("Mqtt url was empty")
} }
let last_will_topic = format!("{}/state", base_topic); let last_will_topic = format!("{base_topic}/state");
let round_trip_topic = format!("{}/internal/roundtrip", base_topic); let round_trip_topic = format!("{base_topic}/internal/roundtrip");
let stay_alive_topic = format!("{}/stay_alive", base_topic); let stay_alive_topic = format!("{base_topic}/stay_alive");
let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> = let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> =
McutieBuilder::new(stack, "plant ctrl", mqtt_url); McutieBuilder::new(stack, "plant ctrl", mqtt_url);
@@ -764,7 +772,6 @@ impl Esp<'_> {
let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16; let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16;
let (receiver, task) = builder.build(keep_alive); let (receiver, task) = builder.build(keep_alive);
let spawner = Spawner::for_current_executor().await;
spawner.spawn(mqtt_incoming_task( spawner.spawn(mqtt_incoming_task(
receiver, receiver,
round_trip_topic.clone(), round_trip_topic.clone(),
@@ -879,8 +886,7 @@ impl Esp<'_> {
Ok(()) => {} Ok(()) => {}
Err(err) => { Err(err) => {
info!( info!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}", "Error during mqtt send on topic {subtopic} with message {message:#?} error is {err:?}"
subtopic, message, err
); );
} }
}; };
@@ -932,7 +938,7 @@ async fn mqtt_incoming_task(
LOG_ACCESS LOG_ACCESS
.lock() .lock()
.await .await
.log(LogMessage::UnknownTopic, 0, 0, "", &*topic) .log(LogMessage::UnknownTopic, 0, 0, "", &topic)
.await; .await;
} }
} }
@@ -988,7 +994,7 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
&mut buf, &mut buf,
) )
.await .await
.inspect_err(|e| log::warn!("DHCP server error: {e:?}")); .inspect_err(|e| warn!("DHCP server error: {e:?}"));
Timer::after(Duration::from_millis(500)).await; Timer::after(Duration::from_millis(500)).await;
} }
} }

View File

@@ -121,7 +121,6 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
async fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
self.general_fault.set_level(enable.into()); self.general_fault.set_level(enable.into());
} }

View File

@@ -1,6 +1,6 @@
use crate::hal::shared_flash::MutexFlashStorage;
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash}; use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::FlashRegion; use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_storage::FlashStorage;
use littlefs2::consts::U4096 as lfsCache; use littlefs2::consts::U4096 as lfsCache;
use littlefs2::consts::U512 as lfsLookahead; use littlefs2::consts::U512 as lfsLookahead;
use littlefs2::driver::Storage as lfs2Storage; use littlefs2::driver::Storage as lfs2Storage;
@@ -9,7 +9,7 @@ use littlefs2::io::Result as lfs2Result;
use log::error; use log::error;
pub struct LittleFs2Filesystem { pub struct LittleFs2Filesystem {
pub(crate) storage: &'static mut FlashRegion<'static, FlashStorage>, pub(crate) storage: &'static mut FlashRegion<'static, MutexFlashStorage>,
} }
impl lfs2Storage for LittleFs2Filesystem { impl lfs2Storage for LittleFs2Filesystem {
@@ -24,7 +24,7 @@ impl lfs2Storage for LittleFs2Filesystem {
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> { fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
let read_size: usize = Self::READ_SIZE; let read_size: usize = Self::READ_SIZE;
if off % read_size != 0 { if off % read_size != 0 {
error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {} read_size: {}", off, read_size); error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {off} read_size: {read_size}");
return Err(lfs2Error::IO); return Err(lfs2Error::IO);
} }
if buf.len() % read_size != 0 { if buf.len() % read_size != 0 {
@@ -34,7 +34,7 @@ impl lfs2Storage for LittleFs2Filesystem {
match self.storage.read(off as u32, buf) { match self.storage.read(off as u32, buf) {
Ok(..) => Ok(buf.len()), Ok(..) => Ok(buf.len()),
Err(err) => { Err(err) => {
error!("Littlefs2Filesystem read error: {:?}", err); error!("Littlefs2Filesystem read error: {err:?}");
Err(lfs2Error::IO) Err(lfs2Error::IO)
} }
} }
@@ -43,7 +43,7 @@ impl lfs2Storage for LittleFs2Filesystem {
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> { fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
let write_size: usize = Self::WRITE_SIZE; let write_size: usize = Self::WRITE_SIZE;
if off % write_size != 0 { if off % write_size != 0 {
error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {} write_size: {}", off, write_size); error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {off} write_size: {write_size}");
return Err(lfs2Error::IO); return Err(lfs2Error::IO);
} }
if data.len() % write_size != 0 { if data.len() % write_size != 0 {
@@ -53,7 +53,7 @@ impl lfs2Storage for LittleFs2Filesystem {
match self.storage.write(off as u32, data) { match self.storage.write(off as u32, data) {
Ok(..) => Ok(data.len()), Ok(..) => Ok(data.len()),
Err(err) => { Err(err) => {
error!("Littlefs2Filesystem write error: {:?}", err); error!("Littlefs2Filesystem write error: {err:?}");
Err(lfs2Error::IO) Err(lfs2Error::IO)
} }
} }
@@ -62,26 +62,26 @@ impl lfs2Storage for LittleFs2Filesystem {
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> { fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
let block_size: usize = Self::BLOCK_SIZE; let block_size: usize = Self::BLOCK_SIZE;
if off % block_size != 0 { if off % block_size != 0 {
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {} block_size: {}", off, block_size); error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {off} block_size: {block_size}");
return lfs2Result::Err(lfs2Error::IO); return Err(lfs2Error::IO);
} }
if len % block_size != 0 { if len % block_size != 0 {
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {} block_size: {}", len, block_size); error!("Littlefs2Filesystem erase error: length not aligned to block size length: {len} block_size: {block_size}");
return lfs2Result::Err(lfs2Error::IO); return Err(lfs2Error::IO);
} }
match check_erase(self.storage, off as u32, (off+len) as u32) { match check_erase(self.storage, off as u32, (off + len) as u32) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("Littlefs2Filesystem check erase error: {:?}", err); error!("Littlefs2Filesystem check erase error: {err:?}");
return lfs2Result::Err(lfs2Error::IO); return Err(lfs2Error::IO);
} }
} }
match self.storage.erase(off as u32, (off + len) as u32) { match self.storage.erase(off as u32, (off + len) as u32) {
Ok(..) => lfs2Result::Ok(len), Ok(..) => Ok(len),
Err(err) => { Err(err) => {
error!("Littlefs2Filesystem erase error: {:?}", err); error!("Littlefs2Filesystem erase error: {err:?}");
lfs2Result::Err(lfs2Error::IO) Err(lfs2Error::IO)
} }
} }
} }

View File

@@ -1,14 +1,16 @@
use lib_bms_protocol::BmsReadable;
use esp_hal::uart::{Config as UartConfig};
pub(crate) mod battery; pub(crate) mod battery;
// mod can_api; // replaced by external canapi crate // mod can_api; // replaced by external canapi crate
pub mod esp; pub mod esp;
mod initial_hal; mod initial_hal;
mod little_fs2storage_adapter; mod little_fs2storage_adapter;
pub(crate) mod rtc; pub(crate) mod rtc;
mod v3_hal; mod shared_flash;
mod v3_shift_register;
mod v4_hal; mod v4_hal;
pub(crate) mod v4_sensor;
mod water; mod water;
use lib_bms_protocol::ProtocolVersion;
use crate::alloc::string::ToString; use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use esp_hal::peripherals::Peripherals; use esp_hal::peripherals::Peripherals;
@@ -27,14 +29,8 @@ use esp_hal::peripherals::GPIO2;
use esp_hal::peripherals::GPIO21; use esp_hal::peripherals::GPIO21;
use esp_hal::peripherals::GPIO22; use esp_hal::peripherals::GPIO22;
use esp_hal::peripherals::GPIO23; use esp_hal::peripherals::GPIO23;
use esp_hal::peripherals::GPIO24;
use esp_hal::peripherals::GPIO25;
use esp_hal::peripherals::GPIO26;
use esp_hal::peripherals::GPIO27; use esp_hal::peripherals::GPIO27;
use esp_hal::peripherals::GPIO28;
use esp_hal::peripherals::GPIO29;
use esp_hal::peripherals::GPIO3; use esp_hal::peripherals::GPIO3;
use esp_hal::peripherals::GPIO30;
use esp_hal::peripherals::GPIO4; use esp_hal::peripherals::GPIO4;
use esp_hal::peripherals::GPIO5; use esp_hal::peripherals::GPIO5;
use esp_hal::peripherals::GPIO6; use esp_hal::peripherals::GPIO6;
@@ -54,13 +50,13 @@ use crate::{
}; };
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::format; use alloc::format;
use alloc::string::String;
use alloc::sync::Arc; use alloc::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bincode::{Decode, Encode}; use bincode::{Decode, Encode};
use bq34z100::Bq34z100g1Driver; use canapi::SensorSlot;
use chrono::{DateTime, FixedOffset, Utc}; use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell; use core::cell::RefCell;
use canapi::SensorSlot;
use ds323x::ic::DS3231; use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface; use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x}; use ds323x::{DateTimeAccess, Ds323x};
@@ -79,19 +75,19 @@ use esp_hal::gpio::{Input, InputConfig, Pull};
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1}; use crate::hal::battery::{WCHI2CSlave};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS; use crate::log::LOG_ACCESS;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use embedded_storage::nor_flash::ReadNorFlash; use embedded_storage::ReadStorage;
use esp_alloc as _; use esp_alloc as _;
use esp_backtrace as _; use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState}; use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::ota::{Slot as ota_slot, Slot};
use esp_hal::delay::Delay; use esp_hal::delay::Delay;
use esp_hal::i2c::master::{BusTimeout, Config, I2c}; use esp_hal::i2c::master::{BusTimeout, Config, I2c};
use esp_hal::interrupt::software::SoftwareInterruptControl;
use esp_hal::pcnt::unit::Unit; use esp_hal::pcnt::unit::Unit;
use esp_hal::pcnt::Pcnt; use esp_hal::pcnt::Pcnt;
use esp_hal::rng::Rng; use esp_hal::rng::Rng;
@@ -100,14 +96,15 @@ use esp_hal::system::reset_reason;
use esp_hal::time::Rate; use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup; use esp_hal::timer::timg::TimerGroup;
use esp_hal::Blocking; use esp_hal::Blocking;
use esp_hal::uart::Uart;
use esp_radio::{init, Controller};
use esp_storage::FlashStorage; use esp_storage::FlashStorage;
use esp_wifi::{init, EspWifiController};
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem}; use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage; use littlefs2::object_safe::DynStorage;
use log::{error, info, warn}; use log::{error, info, warn};
use portable_atomic::AtomicBool; use portable_atomic::AtomicBool;
use serde::Serialize; use serde::Serialize;
use shared_flash::MutexFlashStorage;
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new(); pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
@@ -127,9 +124,9 @@ pub enum Sensor {
B, B,
} }
impl Into<SensorSlot> for Sensor { impl From<Sensor> for SensorSlot {
fn into(self) -> SensorSlot { fn from(val: Sensor) -> Self {
match self { match val {
Sensor::A => SensorSlot::A, Sensor::A => SensorSlot::A,
Sensor::B => SensorSlot::B, Sensor::B => SensorSlot::B,
} }
@@ -138,6 +135,7 @@ impl Into<SensorSlot> for Sensor {
pub struct PlantHal {} pub struct PlantHal {}
#[allow(clippy::upper_case_acronyms)]
pub struct HAL<'a> { pub struct HAL<'a> {
pub board_hal: Box<dyn BoardInteraction<'a> + Send>, pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
} }
@@ -172,28 +170,28 @@ pub trait BoardInteraction<'a> {
async fn progress(&mut self, counter: u32) { async fn progress(&mut self, counter: u32) {
// Indicate progress is active to suppress default wait_infinity blinking // 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);
let current = counter % PLANT_COUNT as u32; let current = counter % PLANT_COUNT as u32;
for led in 0..PLANT_COUNT { for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, current == led as u32).await { if let Err(err) = self.fault(led, current == led as u32).await {
warn!("Fault on plant {}: {:?}", led, err); warn!("Fault on plant {led}: {err:?}");
} }
} }
let even = counter % 2 == 0; let even = counter % 2 == 0;
let _ = self.general_fault(even.into()).await; let _ = self.general_fault(even).await;
} }
async fn clear_progress(&mut self) { async fn clear_progress(&mut self) {
for led in 0..PLANT_COUNT { for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, false).await { if let Err(err) = self.fault(led, false).await {
warn!("Fault on plant {}: {:?}", led, err); warn!("Fault on plant {led}: {err:?}");
} }
} }
let _ = self.general_fault(false).await; let _ = self.general_fault(false).await;
// Reset progress active flag so wait_infinity can resume blinking // 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);
} }
} }
@@ -221,13 +219,7 @@ pub struct FreePeripherals<'a> {
pub gpio21: GPIO21<'a>, pub gpio21: GPIO21<'a>,
pub gpio22: GPIO22<'a>, pub gpio22: GPIO22<'a>,
pub gpio23: GPIO23<'a>, pub gpio23: GPIO23<'a>,
pub gpio24: GPIO24<'a>,
pub gpio25: GPIO25<'a>,
pub gpio26: GPIO26<'a>,
pub gpio27: GPIO27<'a>, pub gpio27: GPIO27<'a>,
pub gpio28: GPIO28<'a>,
pub gpio29: GPIO29<'a>,
pub gpio30: GPIO30<'a>,
pub twai: TWAI0<'a>, pub twai: TWAI0<'a>,
pub pcnt0: Unit<'a, 0>, pub pcnt0: Unit<'a, 0>,
pub pcnt1: Unit<'a, 1>, pub pcnt1: Unit<'a, 1>,
@@ -238,7 +230,7 @@ macro_rules! mk_static {
($t:ty,$val:expr) => {{ ($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)] #[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val)); let x = STATIC_CELL.uninit().write($val);
x x
}}; }};
} }
@@ -258,7 +250,9 @@ impl PlantHal {
error: "Init error rct".to_string(), error: "Init error rct".to_string(),
})?; })?;
let systimer = SystemTimer::new(peripherals.SYSTIMER); let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
let boot_button = Input::new( let boot_button = Input::new(
peripherals.GPIO9, peripherals.GPIO9,
@@ -268,29 +262,19 @@ impl PlantHal {
// Reserve GPIO1 for deep sleep wake (configured just before entering sleep) // Reserve GPIO1 for deep sleep wake (configured just before entering sleep)
let wake_gpio1 = peripherals.GPIO1; let wake_gpio1 = peripherals.GPIO1;
let rng = Rng::new(peripherals.RNG); let rng = Rng::new();
let timg0 = TimerGroup::new(peripherals.TIMG0);
let esp_wifi_ctrl = &*mk_static!( let esp_wifi_ctrl = &*mk_static!(
EspWifiController<'static>, Controller<'static>,
init(timg0.timer0, rng.clone()).expect("Could not init wifi controller") init().expect("Could not init wifi controller")
); );
let (controller, interfaces) = let (controller, interfaces) =
esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).expect("Could not init wifi"); esp_radio::wifi::new(esp_wifi_ctrl, peripherals.WIFI, Default::default())
.expect("Could not init wifi");
use esp_hal::timer::systimer::SystemTimer;
esp_hal_embassy::init(systimer.alarm0);
//let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
//
let pcnt_module = Pcnt::new(peripherals.PCNT); let pcnt_module = Pcnt::new(peripherals.PCNT);
let free_pins = FreePeripherals { let free_pins = FreePeripherals {
// can: peripherals.can,
// adc1: peripherals.adc1,
// pcnt0: peripherals.pcnt0,
// pcnt1: peripherals.pcnt1,
gpio0: peripherals.GPIO0, gpio0: peripherals.GPIO0,
gpio2: peripherals.GPIO2, gpio2: peripherals.GPIO2,
gpio3: peripherals.GPIO3, gpio3: peripherals.GPIO3,
@@ -311,13 +295,7 @@ impl PlantHal {
gpio21: peripherals.GPIO21, gpio21: peripherals.GPIO21,
gpio22: peripherals.GPIO22, gpio22: peripherals.GPIO22,
gpio23: peripherals.GPIO23, gpio23: peripherals.GPIO23,
gpio24: peripherals.GPIO24,
gpio25: peripherals.GPIO25,
gpio26: peripherals.GPIO26,
gpio27: peripherals.GPIO27, gpio27: peripherals.GPIO27,
gpio28: peripherals.GPIO28,
gpio29: peripherals.GPIO29,
gpio30: peripherals.GPIO30,
twai: peripherals.TWAI0, twai: peripherals.TWAI0,
pcnt0: pcnt_module.unit0, pcnt0: pcnt_module.unit0,
pcnt1: pcnt_module.unit1, pcnt1: pcnt_module.unit1,
@@ -328,9 +306,18 @@ impl PlantHal {
[u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN], [u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN],
[0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN] [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]
); );
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let bullshit = MutexFlashStorage {
inner: Arc::new(CriticalSectionMutex::new(RefCell::new(FlashStorage::new(
peripherals.FLASH,
)))),
};
let flash_storage = mk_static!(MutexFlashStorage, bullshit.clone());
let flash_storage_2 = mk_static!(MutexFlashStorage, bullshit.clone());
let flash_storage_3 = mk_static!(MutexFlashStorage, bullshit.clone());
let pt = let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?; esp_bootloader_esp_idf::partitions::read_partition_table(flash_storage, tablebuffer)?;
let ota_data = mk_static!( let ota_data = mk_static!(
PartitionEntry, PartitionEntry,
@@ -341,42 +328,41 @@ impl PlantHal {
); );
let ota_data = mk_static!( let ota_data = mk_static!(
FlashRegion<FlashStorage>, FlashRegion<MutexFlashStorage>,
ota_data.as_embedded_storage(storage_ota) ota_data.as_embedded_storage(flash_storage_2)
); );
let state_0 = ota_state(ota_slot::Slot0, ota_data); let state_0 = ota_state(AppPartitionSubType::Ota0, ota_data);
let state_1 = ota_state(ota_slot::Slot1, ota_data); let state_1 = ota_state(AppPartitionSubType::Ota1, ota_data);
let mut ota = Ota::new(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_and_fix_ota_data(&mut ota, state_0, state_1)?;
let target = running.next(); let target = next_partition(running)?;
info!("Currently running OTA slot: {:?}", running); info!("Currently running OTA slot: {running:?}");
info!("Slot0 state: {:?}", state_0); info!("Slot0 state: {state_0:?}");
info!("Slot1 state: {:?}", state_1); info!("Slot1 state: {state_1:?}");
//obtain current_state and next_state here! //get current_state and next_state here!
let ota_target = match target { let ota_target = match target {
Slot::None => { AppPartitionSubType::Ota0 => pt
panic!("No OTA slot active?");
}
Slot::Slot0 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota0, AppPartitionSubType::Ota0,
))? ))?
.context("Partition table invalid no ota0")?, .context("Partition table invalid no ota0")?,
Slot::Slot1 => pt AppPartitionSubType::Ota1 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota1, AppPartitionSubType::Ota1,
))? ))?
.context("Partition table invalid no ota1")?, .context("Partition table invalid no ota1")?,
_ => {
bail!("Invalid target partition");
}
}; };
let ota_target = mk_static!(PartitionEntry, ota_target); let ota_target = mk_static!(PartitionEntry, ota_target);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let ota_target = mk_static!( let ota_target = mk_static!(
FlashRegion<FlashStorage>, FlashRegion<MutexFlashStorage>,
ota_target.as_embedded_storage(storage_ota) ota_target.as_embedded_storage(flash_storage)
); );
let data_partition = pt let data_partition = pt
@@ -386,30 +372,35 @@ impl PlantHal {
.expect("Data partition with littlefs not found"); .expect("Data partition with littlefs not found");
let data_partition = mk_static!(PartitionEntry, data_partition); let data_partition = mk_static!(PartitionEntry, data_partition);
let storage_data = mk_static!(FlashStorage, FlashStorage::new());
let data = mk_static!( let data = mk_static!(
FlashRegion<FlashStorage>, FlashRegion<MutexFlashStorage>,
data_partition.as_embedded_storage(storage_data) data_partition.as_embedded_storage(flash_storage_3)
); );
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data }); let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate()); let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
if lfs2filesystem.is_mountable() { if lfs2filesystem.is_mountable() {
log::info!("Littlefs2 filesystem is mountable"); info!("Littlefs2 filesystem is mountable");
} else { } else {
match lfs2filesystem.format() { match lfs2filesystem.format() {
Result::Ok(..) => { Ok(..) => {
log::info!("Littlefs2 filesystem is formatted"); info!("Littlefs2 filesystem is formatted");
} }
Err(err) => { Err(err) => {
error!("Littlefs2 filesystem could not be formatted: {:?}", err); error!("Littlefs2 filesystem could not be formatted: {err:?}");
} }
} }
} }
#[allow(clippy::arc_with_non_send_sync)]
let fs = Arc::new(Mutex::new( let fs = Arc::new(Mutex::new(
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"), lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
)); ));
let uart0 = Uart::new(peripherals.UART0, UartConfig::default())
.map_err(|_| FatError::String {
error: "Uart creation failed".to_string(),
})?;
let ap = interfaces.ap; let ap = interfaces.ap;
let sta = interfaces.sta; let sta = interfaces.sta;
let mut esp = Esp { let mut esp = Esp {
@@ -425,6 +416,7 @@ impl PlantHal {
current: running, current: running,
slot0_state: state_0, slot0_state: state_0,
slot1_state: state_1, slot1_state: state_1,
uart0
}; };
//init,reset rtc memory depending on cause //init,reset rtc memory depending on cause
@@ -477,7 +469,7 @@ impl PlantHal {
let config = esp.load_config().await; let config = esp.load_config().await;
log::info!("Init rtc driver"); info!("Init rtc driver");
let sda = peripherals.GPIO20; let sda = peripherals.GPIO20;
let scl = peripherals.GPIO19; let scl = peripherals.GPIO19;
@@ -498,8 +490,9 @@ impl PlantHal {
I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver"); I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver");
let i2c_bus = I2C_DRIVER.get().await; let i2c_bus = I2C_DRIVER.get().await;
let rtc_device = I2cDevice::new(&i2c_bus); let rtc_device = I2cDevice::new(i2c_bus);
let eeprom_device = I2cDevice::new(&i2c_bus); let mut bms_device = I2cDevice::new(i2c_bus);
let eeprom_device = I2cDevice::new(i2c_bus);
let mut rtc: Ds323x< let mut rtc: Ds323x<
I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>, I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>,
@@ -511,10 +504,10 @@ impl PlantHal {
let rtc_time = rtc.datetime(); let rtc_time = rtc.datetime();
match rtc_time { match rtc_time {
Ok(tt) => { Ok(tt) => {
log::info!("Rtc Module reports time at UTC {}", tt); info!("Rtc Module reports time at UTC {tt}");
} }
Err(err) => { Err(err) => {
log::info!("Rtc Module could not be read {:?}", err); info!("Rtc Module could not be read {err:?}");
} }
} }
@@ -529,54 +522,34 @@ impl PlantHal {
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>; Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
let hal = match config { let hal = match config {
Result::Ok(config) => { Ok(config) => {
let battery_interaction: Box<dyn BatteryInteraction + Send> = let battery_interaction: Box<dyn BatteryInteraction + Send> =
match config.hardware.battery { match config.hardware.battery {
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}), 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 => { BatteryBoardVersion::WchI2cSlave => {
// TODO use correct implementation once availible let version = ProtocolVersion::read_from_i2c(&mut bms_device);
Box::new(NoBatteryMonitor {}) 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 { let board_hal: Box<dyn BoardInteraction + Send> = //match config.hardware.board {
BoardVersion::INITIAL => { //BoardVersion::Initial => {
initial_hal::create_initial_board(free_pins, config, esp)? // initial_hal::create_initial_board(free_pins, config, esp)?
} //}
BoardVersion::V3 => { //BoardVersion::V4 => {
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) v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await? .await?;
} //}
}; //};
HAL { board_hal } HAL { board_hal }
} }
@@ -593,11 +566,9 @@ impl PlantHal {
) )
.await; .await;
HAL { HAL {
board_hal: initial_hal::create_initial_board( board_hal: v4_hal::create_v4(free_pins, esp, PlantControllerConfig::default(),
free_pins, Box::new(NoBatteryMonitor {}), rtc_module)
PlantControllerConfig::default(), .await?
esp,
)?,
} }
} }
}; };
@@ -606,29 +577,29 @@ impl PlantHal {
} }
} }
fn ota_state(slot: ota_slot, ota_data: &mut FlashRegion<FlashStorage>) -> OtaImageState { fn ota_state(
slot: AppPartitionSubType,
ota_data: &mut FlashRegion<MutexFlashStorage>,
) -> OtaImageState {
// Read and log OTA states for both slots before constructing Ota // 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] // Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
// Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000 // Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000
if slot == ota_slot::None {
return OtaImageState::Undefined;
}
let mut slot_buf = [0u8; 32]; let mut slot_buf = [0u8; 32];
if slot == ota_slot::Slot0 { if slot == AppPartitionSubType::Ota0 {
let _ = ota_data.read(0x0000, &mut slot_buf); let _ = ReadStorage::read(ota_data, 0x0000, &mut slot_buf);
} else { } else {
let _ = ota_data.read(0x1000, &mut slot_buf); let _ = ReadStorage::read(ota_data, 0x1000, &mut slot_buf);
} }
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4])); let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
let state0 = OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined);
state0 OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined)
} }
fn get_current_slot_and_fix_ota_data( fn get_current_slot_and_fix_ota_data(
ota: &mut Ota<FlashStorage>, ota: &mut Ota<MutexFlashStorage>,
state0: OtaImageState, state0: OtaImageState,
state1: OtaImageState, state1: OtaImageState,
) -> Result<ota_slot, FatError> { ) -> Result<AppPartitionSubType, FatError> {
let state = ota.current_ota_state().unwrap_or_default(); let state = ota.current_ota_state().unwrap_or_default();
let swap = match state { let swap = match state {
OtaImageState::Invalid => true, OtaImageState::Invalid => true,
@@ -639,11 +610,11 @@ fn get_current_slot_and_fix_ota_data(
} }
_ => false, _ => false,
}; };
let current = ota.current_slot()?; let current = ota.current_app_partition()?;
if swap { if swap {
let other = match current { let other = match current {
ota_slot::Slot0 => state1, AppPartitionSubType::Ota0 => state1,
ota_slot::Slot1 => state0, AppPartitionSubType::Ota1 => state0,
_ => OtaImageState::Invalid, _ => OtaImageState::Invalid,
}; };
@@ -666,17 +637,27 @@ fn get_current_slot_and_fix_ota_data(
} }
_ => {} _ => {}
} }
info!( info!("Current slot has state {state:?} other state has {other:?} swapping");
"Current slot has state {:?} other state has {:?} swapping", let next = next_partition(current)?;
state, other ota.set_current_app_partition(next)?;
);
ota.set_current_slot(current.next())?;
//we actually booted other slot, than partition table assumes //we actually booted other slot, than partition table assumes
return Ok(ota.current_slot()?); return Ok(ota.current_app_partition()?);
}; };
Ok(current) Ok(current)
} }
pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSubType> {
let next = match current {
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
_ => {
bail!("Current slot is not ota0 or ota1");
}
};
Ok(next)
}
pub async fn esp_time() -> DateTime<Utc> { pub async fn esp_time() -> DateTime<Utc> {
let guard = TIME_ACCESS.get().await.lock().await; let guard = TIME_ACCESS.get().await.lock().await;
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap() DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
@@ -699,17 +680,17 @@ pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> {
} }
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)] #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)]
pub struct Moistures{ pub struct Moistures {
pub sensor_a_hz: [f32; PLANT_COUNT], pub sensor_a_hz: [f32; PLANT_COUNT],
pub sensor_b_hz: [f32; PLANT_COUNT], pub sensor_b_hz: [f32; PLANT_COUNT],
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
pub struct DetectionResult { pub struct DetectionResult {
plant: [DetectionSensorResult; crate::hal::PLANT_COUNT] plant: [DetectionSensorResult; PLANT_COUNT],
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
pub struct DetectionSensorResult{ pub struct DetectionSensorResult {
sensor_a: bool, sensor_a: bool,
sensor_b: bool, sensor_b: bool,
} }

View File

@@ -1,5 +1,5 @@
use crate::hal::Box;
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::hal::Box;
use async_trait::async_trait; use async_trait::async_trait;
use bincode::config::Configuration; use bincode::config::Configuration;
use bincode::{config, Decode, Encode}; use bincode::{config, Decode, Encode};
@@ -65,7 +65,7 @@ impl RTCModuleInteraction for DS3231Module {
let (header, len): (BackupHeader, usize) = let (header, len): (BackupHeader, usize) =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?; bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
log::info!("Raw header is {:?} with size {}", header_page_buffer, len); log::info!("Raw header is {header_page_buffer:?} with size {len}");
Ok(header) Ok(header)
} }
@@ -97,7 +97,7 @@ impl RTCModuleInteraction for DS3231Module {
async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()> { async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()> {
//skip header and write after //skip header and write after
self.storage self.storage
.write((BACKUP_HEADER_MAX_SIZE + offset) as u32, &bytes)?; .write((BACKUP_HEADER_MAX_SIZE + offset) as u32, bytes)?;
Ok(()) Ok(())
} }
@@ -113,11 +113,7 @@ impl RTCModuleInteraction for DS3231Module {
}; };
let config = config::standard(); let config = config::standard();
let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?; let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?;
log::info!( log::info!("Raw header is {header_page_buffer:?} with size {encoded}");
"Raw header is {:?} with size {}",
header_page_buffer,
encoded
);
self.storage.write(0, &header_page_buffer)?; self.storage.write(0, &header_page_buffer)?;
Ok(()) Ok(())
} }

View File

@@ -0,0 +1,63 @@
use alloc::sync::Arc;
use core::cell::RefCell;
use core::ops::{Deref, DerefMut};
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash};
use embedded_storage::ReadStorage;
use esp_storage::{FlashStorage, FlashStorageError};
#[derive(Clone)]
pub struct MutexFlashStorage {
pub(crate) inner: Arc<CriticalSectionMutex<RefCell<FlashStorage<'static>>>>,
}
impl ReadStorage for MutexFlashStorage {
type Error = FlashStorageError;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStorageError> {
self.inner
.lock(|f| ReadStorage::read(f.borrow_mut().deref_mut(), offset, bytes))
}
fn capacity(&self) -> usize {
self.inner
.lock(|f| ReadStorage::capacity(f.borrow().deref()))
}
}
impl embedded_storage::Storage for MutexFlashStorage {
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
NorFlash::write(self, offset, bytes)
}
}
impl ErrorType for MutexFlashStorage {
type Error = FlashStorageError;
}
impl ReadNorFlash for MutexFlashStorage {
const READ_SIZE: usize = 1;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
ReadStorage::read(self, offset, bytes)
}
fn capacity(&self) -> usize {
ReadStorage::capacity(self)
}
}
impl NorFlash for MutexFlashStorage {
const WRITE_SIZE: usize = 1;
const ERASE_SIZE: usize = 4096;
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
self.inner
.lock(|f| NorFlash::erase(f.borrow_mut().deref_mut(), from, to))
}
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
self.inner
.lock(|f| NorFlash::write(f.borrow_mut().deref_mut(), offset, bytes))
}
}

View File

@@ -1,450 +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 repeat in 0..REPEAT_MOIST_MEASURE {
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;
results[repeat] = 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_else(|_| u32::MAX as f32);
let bb = b.unwrap_or_else(|_| 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")
}
}

View File

@@ -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>;

View File

@@ -1,29 +1,35 @@
use crate::bail; use crate::bail;
use crate::config::PlantControllerConfig; 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::battery::BatteryInteraction;
use crate::hal::esp::{hold_disable, hold_enable, Esp}; use crate::hal::esp::{hold_disable, hold_enable, Esp};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v4_sensor::{SensorImpl, SensorInteraction};
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, DetectionResult, FreePeripherals, Moistures, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS}; use crate::hal::{
BoardInteraction, DetectionResult, FreePeripherals, Moistures, Sensor, I2C_DRIVER, PLANT_COUNT,
TIME_ACCESS,
};
use crate::log::{LogMessage, LOG_ACCESS}; use crate::log::{LogMessage, LOG_ACCESS};
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::string::ToString; use alloc::string::{String, ToString};
use alloc::vec;
use async_trait::async_trait; 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_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; 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::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::I2c; use esp_hal::i2c::master::I2c;
use esp_hal::pcnt::channel::CtrlMode::Keep; use esp_hal::peripherals;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment}; use esp_hal::twai::{EspTwaiError, EspTwaiFrame, StandardId, Twai, TwaiConfiguration, TwaiMode};
use esp_hal::twai::TwaiMode; use esp_hal::{twai, Async, Blocking};
use esp_hal::{twai, Blocking};
use ina219::address::{Address, Pin}; use ina219::address::{Address, Pin};
use ina219::calibration::UnCalibrated; use ina219::calibration::UnCalibrated;
use ina219::configuration::{Configuration, OperatingMode, Resolution}; use ina219::configuration::{Configuration, OperatingMode, Resolution};
use ina219::SyncIna219; use ina219::SyncIna219;
use log::{error, info, warn};
use measurements::Resistance; use measurements::Resistance;
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
@@ -74,35 +80,29 @@ impl<'a> Charger<'a> {
impl Charger<'_> { impl Charger<'_> {
pub(crate) fn power_save(&mut self) { pub(crate) fn power_save(&mut self) {
match self { if let Charger::SolarMpptV1 { mppt_ina, .. } = self {
Charger::SolarMpptV1 { mppt_ina, .. } => { let _ = mppt_ina
let _ = mppt_ina .set_configuration(Configuration {
.set_configuration(Configuration { reset: Default::default(),
reset: Default::default(), bus_voltage_range: Default::default(),
bus_voltage_range: Default::default(), shunt_voltage_range: Default::default(),
shunt_voltage_range: Default::default(), bus_resolution: Default::default(),
bus_resolution: Default::default(), shunt_resolution: Default::default(),
shunt_resolution: Default::default(), operating_mode: OperatingMode::PowerDown,
operating_mode: OperatingMode::PowerDown, })
}) .map_err(|e| {
.map_err(|e| { info!(
log::info!( "Error setting ina mppt configuration during deep sleep preparation{e:?}"
"Error setting ina mppt configuration during deep sleep preparation{:?}", );
e });
);
});
}
_ => {}
} }
} }
fn set_charge_indicator(&mut self, charging: bool) -> FatResult<()> { fn set_charge_indicator(&mut self, charging: bool) -> FatResult<()> {
match self { if let Self::SolarMpptV1 {
Self::SolarMpptV1 { charge_indicator, ..
charge_indicator, .. } = self
} => { {
charge_indicator.set_level(charging.into()); charge_indicator.set_level(charging.into());
}
_ => {}
} }
Ok(()) Ok(())
} }
@@ -130,7 +130,11 @@ pub struct V4<'a> {
pump_ina: Option< pump_ina: Option<
SyncIna219<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>, UnCalibrated>, SyncIna219<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>, UnCalibrated>,
>, >,
sensor: SensorImpl, twai_peripheral: Option<esp_hal::peripherals::TWAI0<'static>>,
twai_rx_pin: Option<esp_hal::peripherals::GPIO2<'static>>,
twai_tx_pin: Option<esp_hal::peripherals::GPIO0<'static>>,
can_power: Output<'static>,
extra1: Output<'a>, extra1: Output<'a>,
extra2: Output<'a>, extra2: Output<'a>,
} }
@@ -142,13 +146,19 @@ pub(crate) async fn create_v4(
battery_monitor: Box<dyn BatteryInteraction + Send>, battery_monitor: Box<dyn BatteryInteraction + Send>,
rtc_module: Box<dyn RTCModuleInteraction + Send>, rtc_module: Box<dyn RTCModuleInteraction + Send>,
) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> { ) -> 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()); let mut awake = Output::new(peripherals.gpio21, Level::High, OutputConfig::default());
awake.set_high(); awake.set_high();
let mut general_fault = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default()); let mut general_fault = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
general_fault.set_low(); general_fault.set_low();
let twai_peripheral = Some(peripherals.twai);
let twai_rx_pin = Some(peripherals.gpio2);
let twai_tx_pin = Some(peripherals.gpio0);
let extra1 = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default()); let extra1 = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
let extra2 = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default()); let extra2 = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
@@ -168,58 +178,13 @@ pub(crate) async fn create_v4(
peripherals.pcnt1, peripherals.pcnt1,
)?; )?;
let sensor_expander_device = I2cDevice::new(I2C_DRIVER.get().await); let can_power = Output::new(peripherals.gpio22, Level::Low, OutputConfig::default());
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 solar_is_day = Input::new(peripherals.gpio7, InputConfig::default()); let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
let light = Output::new(peripherals.gpio10, Level::Low, Default::default()); let light = Output::new(peripherals.gpio10, Level::Low, Default::default());
let charge_indicator = Output::new(peripherals.gpio3, Level::Low, Default::default()); let charge_indicator = Output::new(peripherals.gpio3, Level::Low, Default::default());
info!("Start pump expander");
let pump_device = I2cDevice::new(I2C_DRIVER.get().await); let pump_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut pump_expander = Pca9535Immediate::new(pump_device, 32); let mut pump_expander = Pca9535Immediate::new(pump_device, 32);
for pin in 0..8 { for pin in 0..8 {
@@ -229,6 +194,7 @@ pub(crate) async fn create_v4(
let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin); let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin);
} }
info!("Start mppt");
let mppt_current = I2cDevice::new(I2C_DRIVER.get().await); let mppt_current = I2cDevice::new(I2C_DRIVER.get().await);
let mppt_ina = match SyncIna219::new(mppt_current, Address::from_pins(Pin::Vcc, Pin::Gnd)) { let mppt_ina = match SyncIna219::new(mppt_current, Address::from_pins(Pin::Vcc, Pin::Gnd)) {
Ok(mut ina) => { Ok(mut ina) => {
@@ -244,16 +210,17 @@ pub(crate) async fn create_v4(
Some(ina) Some(ina)
} }
Err(err) => { Err(err) => {
log::info!("Error creating mppt ina: {:?}", err); info!("Error creating mppt ina: {err:?}");
None None
} }
}; };
info!("Start pump current sensor");
let pump_current_dev = I2cDevice::new(I2C_DRIVER.get().await); 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)) { let pump_ina = match SyncIna219::new(pump_current_dev, Address::from_pins(Pin::Gnd, Pin::Sda)) {
Ok(ina) => Some(ina), Ok(ina) => Some(ina),
Err(err) => { Err(err) => {
log::info!("Error creating pump ina: {:?}", err); info!("Error creating pump ina: {err:?}");
None None
} }
}; };
@@ -265,7 +232,7 @@ pub(crate) async fn create_v4(
bus_voltage_range: Default::default(), bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(), shunt_voltage_range: Default::default(),
bus_resolution: Default::default(), bus_resolution: Default::default(),
shunt_resolution: ina219::configuration::Resolution::Avg128, shunt_resolution: Resolution::Avg128,
operating_mode: Default::default(), operating_mode: Default::default(),
})?; })?;
@@ -278,6 +245,7 @@ pub(crate) async fn create_v4(
None => Charger::ErrorInit {}, None => Charger::ErrorInit {},
}; };
info!("Assembling final v4 board interaction object");
let v = V4 { let v = V4 {
rtc_module, rtc_module,
esp, esp,
@@ -289,14 +257,37 @@ pub(crate) async fn create_v4(
config, config,
battery_monitor, battery_monitor,
pump_ina, pump_ina,
twai_peripheral,
twai_rx_pin,
twai_tx_pin,
charger, charger,
extra1, extra1,
extra2, extra2,
sensor, can_power,
}; };
Ok(Box::new(v)) Ok(Box::new(v))
} }
impl<'a> V4<'a> {
fn teardown_twai(&mut self, old: TwaiConfiguration<Blocking>) {
drop(old);
// Re-acquire the peripheral and pins
let twai = unsafe { peripherals::TWAI0::steal() };
let rx_pin = unsafe { peripherals::GPIO2::steal() };
let tx_pin = unsafe { peripherals::GPIO0::steal() };
// Set pins to low to avoid parasitic powering
let mut rx = Input::new(rx_pin, InputConfig::default().with_pull(Pull::None));
let mut tx = Input::new(tx_pin, InputConfig::default().with_pull(Pull::None));
// Release the pins from Output back to raw pins and store everything
self.twai_peripheral = Some(twai);
self.twai_rx_pin = Some(unsafe { peripherals::GPIO2::steal() });
self.twai_tx_pin = Some(unsafe { peripherals::GPIO0::steal() });
self.can_power.set_low();
}
}
#[async_trait(?Send)] #[async_trait(?Send)]
impl<'a> BoardInteraction<'a> for V4<'a> { impl<'a> BoardInteraction<'a> for V4<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
@@ -362,7 +353,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
let v = pump_ina let v = pump_ina
.shunt_voltage() .shunt_voltage()
.map_err(|e| FatError::String { .map_err(|e| FatError::String {
error: alloc::format!("{:?}", e), error: alloc::format!("{e:?}"),
}) })
.map(|v| { .map(|v| {
let shunt_voltage = let shunt_voltage =
@@ -386,9 +377,37 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
} }
Ok(()) Ok(())
} }
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
self.can_power.set_high();
let twai_config = TwaiConfiguration::new(
self.twai_peripheral.take().unwrap(),
self.twai_rx_pin.take().unwrap(),
self.twai_tx_pin.take().unwrap(),
TWAI_BAUDRATE,
TwaiMode::Normal,
);
let mut twai = twai_config.into_async().start();
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> { loop {
self.sensor.measure_moisture_hz().await 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 _ = wait_for_can_measurements(&mut twai, &mut moistures)
.with_timeout(Duration::from_millis(2000))
.await;
self.teardown_twai(twai.stop().into_blocking());
Ok(moistures)
} }
async fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
@@ -453,6 +472,130 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
} }
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> { async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
self.sensor.autodetect().await // Power on CAN transceiver and start controller
self.can_power.set_high();
let twai_config = TwaiConfiguration::new(
self.twai_peripheral.take().unwrap(),
self.twai_rx_pin.take().unwrap(),
self.twai_tx_pin.take().unwrap(),
TWAI_BAUDRATE,
TwaiMode::Normal,
);
info!("convert can");
let mut as_async = twai_config.into_async().start();
// Give CAN some time to stabilize
Timer::after_millis(10).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 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)
.with_timeout(Duration::from_millis(1000))
.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 _ = wait_for_can_measurements(&mut as_async, &mut moistures)
.with_timeout(Duration::from_millis(1000))
.await;
let config = as_async.stop().into_blocking();
self.teardown_twai(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();
if data.len() == 2 {
let frequency = u16::from_be_bytes([data[0], data[1]]);
match sensor {
SensorSlot::A => {
moistures.sensor_a_hz[plant] = frequency as f32;
}
SensorSlot::B => {
moistures.sensor_b_hz[plant] = frequency as f32;
}
}
}
}
}
}
}
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 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
} }
} }

View File

@@ -1,310 +0,0 @@
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
use crate::bail;
use crate::fat_error::{ContextExt, FatError, FatResult};
use canapi::{SensorSlot};
use crate::hal::{DetectionResult, Moistures, Sensor};
use crate::hal::Box;
use crate::log::{LogMessage, LOG_ACCESS};
use alloc::format;
use alloc::string::ToString;
use async_trait::async_trait;
use bincode::config;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Instant, 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.start();
loop {
let rec = twai.receive();
match rec {
Ok(_) => {}
Err(err) => {
info!("Error receiving CAN message: {:?}", err);
break;
}
}
}
Timer::after_millis(10).await;
let can = Self::inner_can(&mut twai).await;
can_power.set_low();
let config = twai.stop();
twai_config.replace(config);
let value = can?;
Ok(value)
}
}
}
}
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 {} sensor {:?}",
plant, sensor
);
}
Err(err) => {
info!("Error sending test message to plant {} sensor {:?}: {:?}", plant, sensor, err);
}
}
} else {
info!("Error building CAN frame");
}
}
}
let mut result = DetectionResult::default();
// Wait for messages to arrive
let _ = Self::wait_for_can_measurements(&mut as_async, &mut result).with_timeout(Duration::from_millis(5000)).await;
let config = as_async.stop().into_blocking();
can_power.set_low();
twai_config.replace(config);
info!("Autodetection result: {:?}", result);
Ok(result)
}
}
}
async fn wait_for_can_measurements(as_async: &mut Twai<'_, Async>, result: &mut DetectionResult) {
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;
match sensor {
SensorSlot::A => {
result.plant[plant].sensor_a = true;
}
SensorSlot::B => {
result.plant[plant].sensor_b = true;
}
}
}
}
}
}
Id::Extended(ext) => {
warn!("Received extended ID: {:?}", ext);
}
}
}
Err(err) => {
error!("Error receiving CAN message: {:?}", err);
break;
}
}
}
}
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 repeat in 0..REPEAT_MOIST_MEASURE {
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;
results[repeat] = 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 fn inner_can(
twai: &mut Twai<'static, Blocking>,
) -> FatResult<Moistures> {
[0_u8; 8];
config::standard();
let timeout = Instant::now()
.checked_add(embassy_time::Duration::from_millis(100))
.context("Timeout")?;
loop {
let answer = twai.receive();
match answer {
Ok(answer) => {
info!("Received CAN message: {:?}", answer);
}
Err(error) => match error {
nb::Error::Other(error) => {
return Err(FatError::CanBusError { error });
}
nb::Error::WouldBlock => {
if Instant::now() > timeout {
bail!("Timeout waiting for CAN answer");
}
Timer::after_millis(10).await;
}
},
}
}
}
}

View File

@@ -137,11 +137,11 @@ impl<'a> TankSensor<'a> {
Timer::after_millis(100).await; Timer::after_millis(100).await;
let mut store = [0_u16; TANK_MULTI_SAMPLE]; let mut store = [0_u16; TANK_MULTI_SAMPLE];
for multisample in 0..TANK_MULTI_SAMPLE { for sample in store.iter_mut() {
let value = self.tank_channel.read_oneshot(&mut self.tank_pin); let value = self.tank_channel.read_oneshot(&mut self.tank_pin);
//force yield //force yield
Timer::after_millis(10).await; Timer::after_millis(10).await;
store[multisample] = value.unwrap(); *sample = value.unwrap();
} }
self.tank_power.set_low(); self.tank_power.set_low();

View File

@@ -7,14 +7,14 @@ use deranged::RangedU8;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use esp_hal::Persistable; use esp_hal::Persistable;
use log::info; use log::{info, warn};
use serde::Serialize; use serde::Serialize;
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
use unit_enum::UnitEnum; use unit_enum::UnitEnum;
const LOG_ARRAY_SIZE: u8 = 220; const LOG_ARRAY_SIZE: u8 = 220;
const MAX_LOG_ARRAY_INDEX: u8 = LOG_ARRAY_SIZE - 1; const MAX_LOG_ARRAY_INDEX: u8 = LOG_ARRAY_SIZE - 1;
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LOG_ARRAY: LogArray = LogArray { static mut LOG_ARRAY: LogArray = LogArray {
buffer: [LogEntryInner { buffer: [LogEntryInner {
timestamp: 0, timestamp: 0,
@@ -26,8 +26,11 @@ static mut LOG_ARRAY: LogArray = LogArray {
}; LOG_ARRAY_SIZE as usize], }; LOG_ARRAY_SIZE as usize],
head: 0, head: 0,
}; };
// 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> = pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> =
unsafe { Mutex::new(&mut *&raw mut LOG_ARRAY) }; unsafe { Mutex::new(&mut LOG_ARRAY) };
const TXT_SHORT_LENGTH: usize = 8; const TXT_SHORT_LENGTH: usize = 8;
const TXT_LONG_LENGTH: usize = 32; const TXT_LONG_LENGTH: usize = 32;
@@ -138,7 +141,7 @@ impl LogArray {
template_string = template_string.replace("${txt_long}", txt_long); template_string = template_string.replace("${txt_long}", txt_long);
template_string = template_string.replace("${txt_short}", txt_short); template_string = template_string.replace("${txt_short}", txt_short);
info!("{}", template_string); info!("{template_string}");
let to_modify = &mut self.buffer[head.get() as usize]; let to_modify = &mut self.buffer[head.get() as usize];
to_modify.timestamp = time; to_modify.timestamp = time;
@@ -147,10 +150,10 @@ impl LogArray {
to_modify.b = number_b; to_modify.b = number_b;
to_modify to_modify
.txt_short .txt_short
.clone_from_slice(&txt_short_stack.as_bytes()); .clone_from_slice(txt_short_stack.as_bytes());
to_modify to_modify
.txt_long .txt_long
.clone_from_slice(&txt_long_stack.as_bytes()); .clone_from_slice(txt_long_stack.as_bytes());
head = head.wrapping_add(1); head = head.wrapping_add(1);
self.head = head.get(); self.head = head.get();
} }
@@ -162,18 +165,37 @@ fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<L
Ok(_) => {} //continue adding chars Ok(_) => {} //continue adding chars
Err(_) => { Err(_) => {
//clear space for two asci chars //clear space for two asci chars
info!("pushing char {char} to limit {LIMIT} current value {target} input {input}");
while target.len() + 2 >= LIMIT { while target.len() + 2 >= LIMIT {
target.pop().unwrap(); target.pop();
} }
//add .. to shortened strings //add .. to shortened strings
target.push('.').unwrap(); match target.push('.') {
target.push('.').unwrap(); Ok(_) => {}
return; Err(_) => {
warn!(
"Error pushin . to limit {LIMIT} current value {target} input {input}"
)
}
}
match target.push('.') {
Ok(_) => {}
Err(_) => {
warn!(
"Error pushin . to limit {LIMIT} current value {target} input {input}"
)
}
}
} }
} }
} }
while target.len() < LIMIT { while target.len() < LIMIT {
target.push(' ').unwrap(); match target.push(' ') {
Ok(_) => {}
Err(_) => {
warn!("Error pushing space to limit {LIMIT} current value {target} input {input}")
}
}
} }
} }
@@ -257,6 +279,8 @@ pub enum LogMessage {
PumpOpenLoopCurrent, PumpOpenLoopCurrent,
#[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")] #[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")]
PumpMissingSensorCurrent, PumpMissingSensorCurrent,
#[strum(serialize = "MPPT Current sensor could not be reached")]
MPPTError,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -275,7 +299,7 @@ impl From<&LogMessage> for MessageTranslation {
} }
impl LogMessage { 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| { Vec::from_iter((0..LogMessage::len()).map(|i| {
let msg_type = LogMessage::from_ordinal(i).unwrap(); let msg_type = LogMessage::from_ordinal(i).unwrap();
(&msg_type).into() (&msg_type).into()

View File

@@ -13,23 +13,24 @@
esp_bootloader_esp_idf::esp_app_desc!(); esp_bootloader_esp_idf::esp_app_desc!();
use esp_backtrace as _; use esp_backtrace as _;
use crate::config::{NetworkConfig, PlantConfig}; use crate::config::{NetworkConfig, PlantConfig, PlantControllerConfig};
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::hal::esp::MQTT_STAY_ALIVE; use crate::hal::esp::MQTT_STAY_ALIVE;
use crate::hal::{esp_time, TIME_ACCESS};
use crate::hal::PROGRESS_ACTIVE; use crate::hal::PROGRESS_ACTIVE;
use crate::hal::{esp_time, TIME_ACCESS};
use crate::log::{log, LOG_ACCESS}; use crate::log::{log, LOG_ACCESS};
use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH}; use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
use crate::webserver::http_server; use crate::webserver::http_server;
use crate::{ use crate::{
config::BoardVersion::INITIAL, config::BoardVersion::Initial,
hal::{PlantHal, HAL, PLANT_COUNT}, hal::{PlantHal, HAL, PLANT_COUNT},
}; };
use ::log::{info, warn}; use ::log::{error, info, warn};
use alloc::borrow::ToOwned; use alloc::borrow::ToOwned;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::sync::Arc; use alloc::sync::Arc;
use alloc::{format, vec}; use alloc::{format, vec};
use alloc::vec::Vec;
use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono::{DateTime, Datelike, Timelike, Utc};
use chrono_tz::Tz::{self, UTC}; use chrono_tz::Tz::{self, UTC};
use core::sync::atomic::{AtomicBool, Ordering}; use core::sync::atomic::{AtomicBool, Ordering};
@@ -38,7 +39,7 @@ use embassy_net::Stack;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard}; use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use embassy_time::Timer; use embassy_time::{Duration, Timer, WithTimeout};
use esp_hal::rom::ets_delay_us; use esp_hal::rom::ets_delay_us;
use esp_hal::system::software_reset; use esp_hal::system::software_reset;
use esp_println::{logger, println}; use esp_println::{logger, println};
@@ -136,18 +137,18 @@ pub struct PumpResult {
#[derive(Serialize, Debug, PartialEq)] #[derive(Serialize, Debug, PartialEq)]
enum SntpMode { enum SntpMode {
OFFLINE, Offline,
SYNC { current: DateTime<Utc> }, Sync { current: DateTime<Utc> },
} }
#[derive(Serialize, Debug, PartialEq)] #[derive(Serialize, Debug, PartialEq)]
enum NetworkMode { enum NetworkMode {
WIFI { Wifi {
sntp: SntpMode, sntp: SntpMode,
mqtt: bool, mqtt: bool,
ip_address: String, ip_address: String,
}, },
OFFLINE, Offline,
} }
async fn safe_main(spawner: Spawner) -> FatResult<()> { async fn safe_main(spawner: Spawner) -> FatResult<()> {
@@ -172,7 +173,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
value value
} }
Err(err) => { Err(err) => {
info!("rtc module error: {:?}", err); info!("rtc module error: {err:?}");
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
esp_time().await esp_time().await
} }
@@ -187,8 +188,16 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "") .log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "")
.await; .await;
} }
info!("cur is {}", cur); info!("cur is {cur}");
update_charge_indicator(&mut board).await; match update_charge_indicator(&mut board).await {
Ok(_) => {}
Err(error) => {
board.board_hal.general_fault(true).await;
error!("Error updating charge indicator: {error}");
log(LogMessage::MPPTError, 0, 0, "", "").await;
let _ = board.board_hal.set_charge_indicator(false).await;
}
}
if board.board_hal.get_esp().get_restart_to_conf() { if board.board_hal.get_esp().get_restart_to_conf() {
LOG_ACCESS LOG_ACCESS
.lock() .lock()
@@ -228,12 +237,12 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("no mode override"); info!("no mode override");
} }
if board.board_hal.get_config().hardware.board == INITIAL if board.board_hal.get_config().hardware.board == Initial
&& board.board_hal.get_config().network.ssid.is_none() && board.board_hal.get_config().network.ssid.is_none()
{ {
info!("No wifi configured, starting initial config mode"); info!("No wifi configured, starting initial config mode");
let stack = board.board_hal.get_esp().wifi_ap().await?; let stack = board.board_hal.get_esp().wifi_ap(spawner).await?;
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
println!("starting webserver"); println!("starting webserver");
@@ -244,34 +253,34 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let mut stack: OptionLock<Stack> = OptionLock::empty(); let mut stack: OptionLock<Stack> = OptionLock::empty();
let network_mode = if board.board_hal.get_config().network.ssid.is_some() { let network_mode = if board.board_hal.get_config().network.ssid.is_some() {
try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await try_connect_wifi_sntp_mqtt(&mut board, &mut stack, spawner).await
} else { } else {
info!("No wifi configured"); info!("No wifi configured");
//the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings; //the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings;
Timer::after_millis(100).await; Timer::after_millis(100).await;
NetworkMode::OFFLINE NetworkMode::Offline
}; };
if matches!(network_mode, NetworkMode::OFFLINE) && to_config { if matches!(network_mode, NetworkMode::Offline) && to_config {
info!("Could not connect to station and config mode forced, switching to ap mode!"); info!("Could not connect to station and config mode forced, switching to ap mode!");
let res = { let res = {
let esp = board.board_hal.get_esp(); let esp = board.board_hal.get_esp();
esp.wifi_ap().await esp.wifi_ap(spawner).await
}; };
match res { match res {
Ok(ap_stack) => { Ok(ap_stack) => {
stack.replace(ap_stack); stack.replace(ap_stack);
info!("Started ap, continuing") info!("Started ap, continuing")
} }
Err(err) => info!("Could not start config override ap mode due to {}", err), Err(err) => info!("Could not start config override ap mode due to {err}"),
} }
} }
let tz = &board.board_hal.get_config().timezone; let tz = &board.board_hal.get_config().timezone;
let timezone = match tz { let timezone = match tz {
Some(tz_str) => tz_str.parse::<Tz>().unwrap_or_else(|_| { Some(tz_str) => tz_str.parse::<Tz>().unwrap_or_else(|_| {
info!("Invalid timezone '{}', falling back to UTC", tz_str); info!("Invalid timezone '{tz_str}', falling back to UTC");
UTC UTC
}), }),
None => UTC, // Fallback to UTC if no timezone is set None => UTC, // Fallback to UTC if no timezone is set
@@ -286,7 +295,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
timezone_time timezone_time
); );
if let NetworkMode::WIFI { ref ip_address, .. } = network_mode { if let NetworkMode::Wifi { ref ip_address, .. } = network_mode {
publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await; publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await;
publish_battery_state(&mut board).await; publish_battery_state(&mut board).await;
let _ = publish_mppt_state(&mut board).await; let _ = publish_mppt_state(&mut board).await;
@@ -297,15 +306,15 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.await .await
.log( .log(
LogMessage::StartupInfo, LogMessage::StartupInfo,
matches!(network_mode, NetworkMode::WIFI { .. }) as u32, matches!(network_mode, NetworkMode::Wifi { .. }) as u32,
matches!( matches!(
network_mode, network_mode,
NetworkMode::WIFI { NetworkMode::Wifi {
sntp: SntpMode::SYNC { .. }, sntp: SntpMode::Sync { .. },
.. ..
} }
) as u32, ) as u32,
matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. }) matches!(network_mode, NetworkMode::Wifi { mqtt: true, .. })
.to_string() .to_string()
.as_str(), .as_str(),
"", "",
@@ -356,7 +365,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
LogMessage::TankSensorValueRangeError, LogMessage::TankSensorValueRangeError,
min as u32, min as u32,
max as u32, max as u32,
&format!("{}", value), &format!("{value}"),
"", "",
) )
.await .await
@@ -402,14 +411,14 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let moisture = board.board_hal.measure_moisture_hz().await?; let moisture = board.board_hal.measure_moisture_hz().await?;
let plantstate: [PlantState; PLANT_COUNT] = [ let plantstate: [PlantState; PLANT_COUNT] = [
PlantState::read_hardware_state(moisture,0, &mut board).await, PlantState::read_hardware_state(moisture, 0, &mut board).await,
PlantState::read_hardware_state(moisture,1, &mut board).await, PlantState::read_hardware_state(moisture, 1, &mut board).await,
PlantState::read_hardware_state(moisture,2, &mut board).await, PlantState::read_hardware_state(moisture, 2, &mut board).await,
PlantState::read_hardware_state(moisture,3, &mut board).await, PlantState::read_hardware_state(moisture, 3, &mut board).await,
PlantState::read_hardware_state(moisture,4, &mut board).await, PlantState::read_hardware_state(moisture, 4, &mut board).await,
PlantState::read_hardware_state(moisture,5, &mut board).await, PlantState::read_hardware_state(moisture, 5, &mut board).await,
PlantState::read_hardware_state(moisture,6, &mut board).await, PlantState::read_hardware_state(moisture, 6, &mut board).await,
PlantState::read_hardware_state(moisture,7, &mut board).await, PlantState::read_hardware_state(moisture, 7, &mut board).await,
]; ];
publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await; publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await;
@@ -492,22 +501,18 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("state of charg"); info!("state of charg");
let is_day = board.board_hal.is_day(); let is_day = board.board_hal.is_day();
let state_of_charge = board let battery_state = board.board_hal.get_battery_monitor().get_state().await.unwrap_or(BatteryState::Unknown);
.board_hal info!("Battery state is {battery_state:?}");
.get_battery_monitor()
.state_charge_percent()
.await
.unwrap_or(0.);
// try to load full battery state if failed the battery state is unknown let state_of_charge = match &battery_state {
let battery_state = board BatteryState::Unknown => {
.board_hal 0
.get_battery_monitor() }
.get_battery_state() BatteryState::Info(data) => {
.await data.state_of_charge
.unwrap_or(BatteryState::Unknown); }
};
info!("Battery state is {:?}", battery_state);
let mut light_state = LightState { let mut light_state = LightState {
enabled: board.board_hal.get_config().night_lamp.enabled, enabled: board.board_hal.get_config().night_lamp.enabled,
..Default::default() ..Default::default()
@@ -530,7 +535,6 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.get_config() .get_config()
.night_lamp .night_lamp
.low_soc_cutoff .low_soc_cutoff
.into()
{ {
board.board_hal.get_esp().set_low_voltage_in_cycle(); board.board_hal.get_esp().set_low_voltage_in_cycle();
info!("Set low voltage in cycle"); info!("Set low voltage in cycle");
@@ -540,7 +544,6 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.get_config() .get_config()
.night_lamp .night_lamp
.low_soc_restore .low_soc_restore
.into()
{ {
board.board_hal.get_esp().clear_low_voltage_in_cycle(); board.board_hal.get_esp().clear_low_voltage_in_cycle();
info!("Clear low voltage in cycle"); info!("Clear low voltage in cycle");
@@ -573,7 +576,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
board.board_hal.light(false).await?; board.board_hal.light(false).await?;
} }
info!("Lightstate is {:?}", light_state); info!("Lightstate is {light_state:?}");
} }
match &serde_json::to_string(&light_state) { match &serde_json::to_string(&light_state) {
@@ -585,13 +588,13 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.await; .await;
} }
Err(err) => { Err(err) => {
info!("Error publishing lightstate {}", err); info!("Error publishing lightstate {err}");
} }
}; };
let deep_sleep_duration_minutes: u32 = let deep_sleep_duration_minutes: u32 =
// if battery soc is unknown assume battery has enough change // if battery soc is unknown assume battery has enough change
if state_of_charge < 10.0 && !matches!(battery_state, BatteryState::Unknown) { if state_of_charge < 10 && !matches!(battery_state, BatteryState::Unknown) {
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
@@ -616,7 +619,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.mqtt_publish("/state", "sleep") .mqtt_publish("/state", "sleep")
.await; .await;
info!("Go to sleep for {} minutes", deep_sleep_duration_minutes); info!("Go to sleep for {deep_sleep_duration_minutes} minutes");
//determine next event //determine next event
//is light out of work trigger soon? //is light out of work trigger soon?
//is battery low ?? //is battery low ??
@@ -625,7 +628,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
//mark_app_valid(); //mark_app_valid();
let stay_alive = MQTT_STAY_ALIVE.load(Ordering::Relaxed); let stay_alive = MQTT_STAY_ALIVE.load(Ordering::Relaxed);
info!("Check stay alive, current state is {}", stay_alive); info!("Check stay alive, current state is {stay_alive}");
if stay_alive { if stay_alive {
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
@@ -679,49 +682,45 @@ pub async fn do_secure_pump(
let current_ma = current.as_milliamperes() as u16; let current_ma = current.as_milliamperes() as u16;
current_collector[step] = current_ma; current_collector[step] = current_ma;
let high_current = current_ma > plant_config.max_pump_current_ma; let high_current = current_ma > plant_config.max_pump_current_ma;
if high_current { if high_current && first_error {
if first_error { log(
log( LogMessage::PumpOverCurrent,
LogMessage::PumpOverCurrent, plant_id as u32 + 1,
plant_id as u32 + 1, current_ma as u32,
current_ma as u32, plant_config.max_pump_current_ma.to_string().as_str(),
plant_config.max_pump_current_ma.to_string().as_str(), step.to_string().as_str(),
step.to_string().as_str(), )
) .await;
.await; board.board_hal.general_fault(true).await;
board.board_hal.general_fault(true).await; board.board_hal.fault(plant_id, true).await?;
board.board_hal.fault(plant_id, true).await?; if !plant_config.ignore_current_error {
if !plant_config.ignore_current_error { error = true;
error = true; break;
break;
}
first_error = false;
} }
first_error = false;
} }
let low_current = current_ma < plant_config.min_pump_current_ma; let low_current = current_ma < plant_config.min_pump_current_ma;
if low_current { if low_current && first_error {
if first_error { log(
log( LogMessage::PumpOpenLoopCurrent,
LogMessage::PumpOpenLoopCurrent, plant_id as u32 + 1,
plant_id as u32 + 1, current_ma as u32,
current_ma as u32, plant_config.min_pump_current_ma.to_string().as_str(),
plant_config.min_pump_current_ma.to_string().as_str(), step.to_string().as_str(),
step.to_string().as_str(), )
) .await;
.await; board.board_hal.general_fault(true).await;
board.board_hal.general_fault(true).await; board.board_hal.fault(plant_id, true).await?;
board.board_hal.fault(plant_id, true).await?; if !plant_config.ignore_current_error {
if !plant_config.ignore_current_error { error = true;
error = true; break;
break;
}
first_error = false;
} }
first_error = false;
} }
} }
Err(err) => { Err(err) => {
if !plant_config.ignore_current_error { if !plant_config.ignore_current_error {
info!("Error getting pump current: {}", err); info!("Error getting pump current: {err}");
log( log(
LogMessage::PumpMissingSensorCurrent, LogMessage::PumpMissingSensorCurrent,
plant_id as u32, plant_id as u32,
@@ -744,10 +743,7 @@ pub async fn do_secure_pump(
board.board_hal.get_tank_sensor()?.stop_flow_meter(); board.board_hal.get_tank_sensor()?.stop_flow_meter();
let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value(); let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value();
let flow_value_ml = final_flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse; let flow_value_ml = final_flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
info!( info!("Final flow value is {final_flow_value} with {flow_value_ml} ml");
"Final flow value is {} with {} ml",
final_flow_value, flow_value_ml
);
current_collector.sort(); current_collector.sort();
Ok(PumpResult { Ok(PumpResult {
median_current_ma: current_collector[current_collector.len() / 2], median_current_ma: current_collector[current_collector.len() / 2],
@@ -762,27 +758,18 @@ pub async fn do_secure_pump(
async fn update_charge_indicator( async fn update_charge_indicator(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) { ) -> FatResult<()>{
//FIXME add config and code to allow power supply mode, in this case this is a nop
//we have mppt controller, ask it for charging current //we have mppt controller, ask it for charging current
if let Ok(current) = board.board_hal.get_mptt_current().await { let current = board.board_hal.get_mptt_current().await?;
let _ = board board
.board_hal
.set_charge_indicator(current.as_milliamperes() > 20_f64);
}
//fallback to battery controller and ask it instead
else if let Ok(charging) = board
.board_hal .board_hal
.get_battery_monitor() .set_charge_indicator(current.as_milliamperes() > 20_f64)
.average_current_milli_ampere() .await?;
.await Ok(())
{
let _ = board.board_hal.set_charge_indicator(charging > 20);
} else {
//who knows
let _ = board.board_hal.set_charge_indicator(false);
}
} }
async fn publish_tank_state( async fn publish_tank_state(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
tank_state: &TankState, tank_state: &TankState,
@@ -792,7 +779,11 @@ async fn publish_tank_state(
&tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp), &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp),
) )
.unwrap(); .unwrap();
let _ = board.board_hal.get_esp().mqtt_publish("/water", &*state); board
.board_hal
.get_esp()
.mqtt_publish("/water", &state)
.await;
} }
async fn publish_plant_states( async fn publish_plant_states(
@@ -819,78 +810,83 @@ async fn publish_plant_states(
async fn publish_firmware_info( async fn publish_firmware_info(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
version: VersionInfo, version: VersionInfo,
ip_address: &String, ip_address: &str,
timezone_time: &String, timezone_time: &str,
) { ) {
let esp = board.board_hal.get_esp(); let esp = board.board_hal.get_esp();
let _ = esp.mqtt_publish("/firmware/address", ip_address).await; esp.mqtt_publish("/firmware/address", ip_address).await;
let _ = esp esp.mqtt_publish("/firmware/state", format!("{:?}", &version).as_str())
.mqtt_publish("/firmware/state", format!("{:?}", &version).as_str())
.await; .await;
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time); esp.mqtt_publish("/firmware/last_online", timezone_time)
let _ = esp.mqtt_publish("/state", "online").await; .await;
esp.mqtt_publish("/state", "online").await;
} }
macro_rules! mk_static { macro_rules! mk_static {
($t:ty,$val:expr) => {{ ($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)] #[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val)); let x = STATIC_CELL.uninit().write($val);
x x
}}; }};
} }
async fn try_connect_wifi_sntp_mqtt( async fn try_connect_wifi_sntp_mqtt(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
stack_store: &mut OptionLock<Stack<'static>>, stack_store: &mut OptionLock<Stack<'static>>,
spawner: Spawner,
) -> NetworkMode { ) -> NetworkMode {
let nw_conf = &board.board_hal.get_config().network.clone(); let nw_conf = &board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().wifi(nw_conf).await { match board.board_hal.get_esp().wifi(nw_conf, spawner).await {
Ok(stack) => { Ok(stack) => {
stack_store.replace(stack); stack_store.replace(stack);
let sntp_mode: SntpMode = match board let sntp_mode: SntpMode = match board.board_hal.get_esp().sntp(1000 * 10, stack).await {
.board_hal
.get_esp()
.sntp(1000 * 10, stack.clone())
.await
{
Ok(new_time) => { Ok(new_time) => {
info!("Using time from sntp {}", new_time.to_rfc3339()); info!("Using time from sntp {}", new_time.to_rfc3339());
let _ = board.board_hal.get_rtc_module().set_rtc_time(&new_time); let _ = board
SntpMode::SYNC { current: new_time } .board_hal
.get_rtc_module()
.set_rtc_time(&new_time)
.await;
SntpMode::Sync { current: new_time }
} }
Err(err) => { Err(err) => {
warn!("sntp error: {}", err); warn!("sntp error: {err}");
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
SntpMode::OFFLINE SntpMode::Offline
} }
}; };
let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() { 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 = board.board_hal.get_config().network.clone();
let nw_config = mk_static!(NetworkConfig, nw_config); let nw_config = mk_static!(NetworkConfig, nw_config);
match board.board_hal.get_esp().mqtt(nw_config, stack).await { match board
.board_hal
.get_esp()
.mqtt(nw_config, stack, spawner)
.await
{
Ok(_) => { Ok(_) => {
info!("Mqtt connection ready"); info!("Mqtt connection ready");
true true
} }
Err(err) => { Err(err) => {
warn!("Could not connect mqtt due to {}", err); warn!("Could not connect mqtt due to {err}");
false false
} }
} }
} else { } else {
false false
}; };
NetworkMode::WIFI { NetworkMode::Wifi {
sntp: sntp_mode, sntp: sntp_mode,
mqtt: mqtt_connected, mqtt: mqtt_connected,
ip_address: stack.hardware_address().to_string(), ip_address: stack.hardware_address().to_string(),
} }
} }
Err(err) => { Err(err) => {
info!("Offline mode due to {}", err); info!("Offline mode due to {err}");
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
NetworkMode::OFFLINE NetworkMode::Offline
} }
} }
} }
@@ -926,7 +922,7 @@ async fn pump_info(
.await; .await;
} }
Err(err) => { Err(err) => {
warn!("Error publishing pump state {}", err); warn!("Error publishing pump state {err}");
} }
}; };
} }
@@ -941,10 +937,11 @@ async fn publish_mppt_state(
voltage_ma: voltage.as_millivolts() as u32, voltage_ma: voltage.as_millivolts() as u32,
}; };
if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) { if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) {
let _ = board board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/mppt", &serialized_solar_state_bytes); .mqtt_publish("/mppt", &serialized_solar_state_bytes)
.await;
} }
Ok(()) Ok(())
} }
@@ -955,7 +952,7 @@ async fn publish_battery_state(
let state = board let state = board
.board_hal .board_hal
.get_battery_monitor() .get_battery_monitor()
.get_battery_state() .get_state()
.await; .await;
let value = match state { let value = match state {
Ok(state) => { Ok(state) => {
@@ -968,7 +965,7 @@ async fn publish_battery_state(
let _ = board let _ = board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/battery", &*value) .mqtt_publish("/battery", &value)
.await; .await;
} }
} }
@@ -984,10 +981,27 @@ async fn wait_infinity(
let delay = wait_type.blink_pattern(); let delay = wait_type.blink_pattern();
let mut led_count = 8; let mut led_count = 8;
let mut pattern_step = 0; let mut pattern_step = 0;
let serial_config_receive = AtomicBool::new(false);
let mut suppress_further_mppt_error = false;
loop { loop {
{ {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
update_charge_indicator(&mut board).await; match update_charge_indicator(&mut board).await{
Ok(_) => {}
Err(error) => {
if !suppress_further_mppt_error {
error!("Error updating charge indicator: {error}");
suppress_further_mppt_error = true;
}
}
};
match handle_serial_config(&mut board, &serial_config_receive, &reboot_now).await {
Ok(_) => {}
Err(e) => {
error!("Error handling serial config: {e}");
}
}
// Skip default blink code when a progress display is active // Skip default blink code when a progress display is active
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) { if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
@@ -1040,6 +1054,7 @@ async fn wait_infinity(
reboot_now.store(true, Ordering::Relaxed); reboot_now.store(true, Ordering::Relaxed);
} }
if reboot_now.load(Ordering::Relaxed) { if reboot_now.load(Ordering::Relaxed) {
info!("Rebooting now");
//ensure clean http answer //ensure clean http answer
Timer::after_millis(500).await; Timer::after_millis(500).await;
BOARD_ACCESS BOARD_ACCESS
@@ -1054,12 +1069,54 @@ async fn wait_infinity(
} }
} }
#[esp_hal_embassy::main] async fn handle_serial_config(board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>, serial_config_receive: &AtomicBool, reboot_now: &AtomicBool) -> FatResult<()> {
match board.board_hal.get_esp().read_serial_line().await {
Ok(serial_line) => {
match serial_line {
None => {
Ok(())
}
Some(line) => {
if serial_config_receive.load(Ordering::Relaxed) {
let ll = line.as_str();
let config: PlantControllerConfig = serde_json::from_str(ll)?;
board.board_hal.get_esp().save_config(Vec::from(ll.as_bytes())).await?;
board.board_hal.set_config(config);
serial_config_receive.store(false, Ordering::Relaxed);
info!("Config received, rebooting");
board.board_hal.get_esp().set_restart_to_conf(false);
reboot_now.store(true, Ordering::Relaxed);
Ok(())
} else {
if line == "automation:streamconfig" {
serial_config_receive.store(true, Ordering::Relaxed);
info!("streamconfig:recieving");
}
Ok(())
}
}
}
}
Err(_) => {
error!("Error reading serial line");
Ok(())
}
}
}
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! { async fn main(spawner: Spawner) -> ! {
// intialize embassy // intialize embassy
logger::init_logger_from_env(); logger::init_logger_from_env();
//force init here! //force init here!
match BOARD_ACCESS.init(PlantHal::create().await.unwrap()) { match BOARD_ACCESS.init(
PlantHal::create()
.with_timeout(Duration::from_secs(10))
.await
.unwrap()
.unwrap(),
) {
Ok(_) => {} Ok(_) => {}
Err(_) => { Err(_) => {
panic!("Could not set hal to static") panic!("Could not set hal to static")

View File

@@ -1,9 +1,5 @@
use crate::hal::Moistures; use crate::hal::Moistures;
use crate::{ use crate::{config::PlantConfig, hal::HAL, in_time_range};
config::PlantConfig,
hal::HAL,
in_time_range,
};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -76,7 +72,7 @@ impl PumpState {
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
pub enum PlantWateringMode { pub enum PlantWateringMode {
OFF, Off,
TargetMoisture, TargetMoisture,
MinMoisture, MinMoisture,
TimerOnly, TimerOnly,
@@ -115,20 +111,28 @@ fn map_range_moisture(
} }
impl PlantState { impl PlantState {
pub async fn read_hardware_state(moistures: Moistures, plant_id: usize, board: &mut HAL<'_>) -> Self { pub async fn read_hardware_state(
moistures: Moistures,
plant_id: usize,
board: &mut HAL<'_>,
) -> Self {
let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a { let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
let raw = moistures.sensor_a_hz[plant_id]; let raw = moistures.sensor_a_hz[plant_id];
match map_range_moisture( match map_range_moisture(
raw, raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, board.board_hal.get_config().plants[plant_id]
board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency, .moisture_sensor_min_frequency
) { .map(|a| a as f32),
Ok(moisture_percent) => MoistureSensorState::MoistureValue { board.board_hal.get_config().plants[plant_id]
raw_hz: raw, .moisture_sensor_max_frequency
moisture_percent, .map(|b| b as f32),
}, ) {
Err(err) => MoistureSensorState::SensorError(err), Ok(moisture_percent) => MoistureSensorState::MoistureValue {
} raw_hz: raw,
moisture_percent,
},
Err(err) => MoistureSensorState::SensorError(err),
}
} else { } else {
MoistureSensorState::Disabled MoistureSensorState::Disabled
}; };
@@ -137,8 +141,12 @@ impl PlantState {
let raw = moistures.sensor_b_hz[plant_id]; let raw = moistures.sensor_b_hz[plant_id];
match map_range_moisture( match map_range_moisture(
raw, raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, board.board_hal.get_config().plants[plant_id]
board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency, .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 { Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw, raw_hz: raw,
@@ -161,13 +169,13 @@ impl PlantState {
}, },
}; };
if state.is_err() { if state.is_err() {
let _ = board.board_hal.fault(plant_id, true); let _ = board.board_hal.fault(plant_id, true).await;
} }
state state
} }
pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool { pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool {
if matches!(plant_conf.mode, PlantWateringMode::OFF) { if matches!(plant_conf.mode, PlantWateringMode::Off) {
return false; return false;
} }
self.pump.previous_pump.is_some_and(|last_pump| { self.pump.previous_pump.is_some_and(|last_pump| {
@@ -186,7 +194,7 @@ impl PlantState {
pub fn plant_moisture( pub fn plant_moisture(
&self, &self,
) -> ( ) -> (
Option<f32>, Option<u8>,
(Option<&MoistureSensorError>, Option<&MoistureSensorError>), (Option<&MoistureSensorError>, Option<&MoistureSensorError>),
) { ) {
match ( match (
@@ -194,10 +202,14 @@ impl PlantState {
self.sensor_b.moisture_percent(), self.sensor_b.moisture_percent(),
) { ) {
(Some(moisture_a), Some(moisture_b)) => { (Some(moisture_a), Some(moisture_b)) => {
(Some((moisture_a + moisture_b) / 2.), (None, None)) (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))
} }
(Some(moisture_percent), _) => (Some(moisture_percent), (None, self.sensor_b.is_err())),
(_, Some(moisture_percent)) => (Some(moisture_percent), (self.sensor_a.is_err(), None)),
_ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())), _ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())),
} }
} }
@@ -208,7 +220,7 @@ impl PlantState {
current_time: &DateTime<Tz>, current_time: &DateTime<Tz>,
) -> bool { ) -> bool {
match plant_conf.mode { match plant_conf.mode {
PlantWateringMode::OFF => false, PlantWateringMode::Off => false,
PlantWateringMode::TargetMoisture => { PlantWateringMode::TargetMoisture => {
let (moisture_percent, _) = self.plant_moisture(); let (moisture_percent, _) = self.plant_moisture();
if let Some(moisture_percent) = moisture_percent { if let Some(moisture_percent) = moisture_percent {
@@ -229,28 +241,8 @@ impl PlantState {
} }
} }
PlantWateringMode::MinMoisture => { PlantWateringMode::MinMoisture => {
let (moisture_percent, _) = self.plant_moisture(); // TODO
if let Some(_moisture_percent) = moisture_percent { false
if self.pump_in_timeout(plant_conf, current_time) {
false
} else if !in_time_range(
current_time,
plant_conf.pump_hour_start,
plant_conf.pump_hour_end,
) {
false
} else if true {
//if not cooldown min and below max
true
} else if true {
//if below min disable cooldown min
true
} else {
false
}
} else {
false
}
} }
PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time), PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time),
} }

View File

@@ -1,7 +1,7 @@
use crate::alloc::string::{String, ToString}; use crate::alloc::string::{String, ToString};
use crate::config::TankConfig; use crate::config::TankConfig;
use crate::hal::HAL;
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::hal::HAL;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::MutexGuard; use embassy_sync::mutex::MutexGuard;
use serde::Serialize; use serde::Serialize;
@@ -161,7 +161,7 @@ pub async fn determine_tank_state(
match board match board
.board_hal .board_hal
.get_tank_sensor() .get_tank_sensor()
.and_then(|f| core::prelude::v1::Ok(f.tank_sensor_voltage())) .map(|f| f.tank_sensor_voltage())
{ {
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv.await.unwrap()), Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv.await.unwrap()),
Err(err) => TankState::Error(TankError::BoardError(err.to_string())), Err(err) => TankState::Error(TankError::BoardError(err.to_string())),

View File

@@ -6,7 +6,7 @@ use alloc::format;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use chrono::DateTime; use chrono::DateTime;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -51,11 +51,8 @@ where
conn.initiate_response( conn.initiate_response(
409, 409,
Some( Some(
format!( format!("Checksum mismatch expected {expected_crc} got {actual_crc}")
"Checksum mismatch expected {} got {}", .as_str(),
expected_crc, actual_crc
)
.as_str(),
), ),
&[], &[],
) )
@@ -131,7 +128,7 @@ where
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(counter).await; board.board_hal.progress(counter).await;
counter = counter + 1; counter += 1;
board board
.board_hal .board_hal
.get_rtc_module() .get_rtc_module()
@@ -139,7 +136,7 @@ where
.await?; .await?;
checksum.update(&buf[0..to_write]); checksum.update(&buf[0..to_write]);
} }
offset = offset + to_write; offset += to_write;
} }
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;

View File

@@ -6,7 +6,7 @@ use alloc::format;
use alloc::string::String; use alloc::string::String;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use edge_http::Method; use edge_http::Method;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info; use log::info;
pub(crate) async fn list_files<T, const N: usize>( pub(crate) async fn list_files<T, const N: usize>(
@@ -27,7 +27,7 @@ where
T: Read + Write, T: Read + Write,
{ {
let filename = &path[prefix.len()..]; let filename = &path[prefix.len()..];
info!("file request for {} with method {}", filename, method); info!("file request for {filename} with method {method}");
Ok(match method { Ok(match method {
Method::Delete => { Method::Delete => {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
@@ -65,7 +65,7 @@ where
&[ &[
("Content-Type", "application/octet-stream"), ("Content-Type", "application/octet-stream"),
("Content-Disposition", disposition.as_str()), ("Content-Disposition", disposition.as_str()),
("Content-Length", &format!("{}", size)), ("Content-Length", &format!("{size}")),
("Access-Control-Allow-Origin", "*"), ("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"), ("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"), ("Access-Control-Allow-Methods", "*"),
@@ -84,16 +84,16 @@ where
.await?; .await?;
let length = read_chunk.1; let length = read_chunk.1;
if length == 0 { if length == 0 {
info!("file request for {} finished", filename); info!("file request for {filename} finished");
break; break;
} }
let data = &read_chunk.0[0..length]; let data = &read_chunk.0[0..length];
conn.write_all(data).await?; conn.write_all(data).await?;
if length < read_chunk.0.len() { if length < read_chunk.0.len() {
info!("file request for {} finished", filename); info!("file request for {filename} finished");
break; break;
} }
chunk = chunk + 1; chunk += 1;
} }
BOARD_ACCESS BOARD_ACCESS
.get() .get()
@@ -120,8 +120,8 @@ where
let mut chunk = 0; let mut chunk = 0;
loop { loop {
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?; let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
if buf.len() == 0 { if buf.is_empty() {
info!("file request for {} finished", filename); info!("file request for {filename} finished");
break; break;
} else { } else {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
@@ -132,8 +132,8 @@ where
.write_file(filename.to_owned(), offset as u32, &buf) .write_file(filename.to_owned(), offset as u32, &buf)
.await?; .await?;
} }
offset = offset + buf.len(); offset += buf.len();
chunk = chunk + 1; chunk += 1;
} }
BOARD_ACCESS BOARD_ACCESS
.get() .get()

View File

@@ -1,4 +1,3 @@
use core::str::FromStr;
use crate::fat_error::{FatError, FatResult}; use crate::fat_error::{FatError, FatResult};
use crate::hal::{esp_time, PLANT_COUNT}; use crate::hal::{esp_time, PLANT_COUNT};
use crate::log::LogMessage; use crate::log::LogMessage;
@@ -9,8 +8,9 @@ use alloc::format;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::vec::Vec; use alloc::vec::Vec;
use chrono_tz::Tz; use chrono_tz::Tz;
use core::str::FromStr;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info; use log::info;
use serde::Serialize; use serde::Serialize;
@@ -131,7 +131,7 @@ pub(crate) async fn get_battery_state<T, const N: usize>(
let battery_state = board let battery_state = board
.board_hal .board_hal
.get_battery_monitor() .get_battery_monitor()
.get_battery_state() .get_state()
.await?; .await?;
Ok(Some(serde_json::to_string(&battery_state)?)) Ok(Some(serde_json::to_string(&battery_state)?))
} }
@@ -142,21 +142,15 @@ pub(crate) async fn get_time<T, const N: usize>(
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
let conf = board.board_hal.get_config(); let conf = board.board_hal.get_config();
let tz:Tz = match conf.timezone.as_ref(){ let tz: Tz = match conf.timezone.as_ref() {
None => { None => Tz::UTC,
Tz::UTC Some(tz_string) => match Tz::from_str(tz_string) {
} Ok(tz) => tz,
Some(tz_string) => { Err(err) => {
match Tz::from_str(tz_string) { info!("failed parsing timezone {err}");
Ok(tz) => { Tz::UTC
tz
}
Err(err) => {
info!("failed parsing timezone {}", err);
Tz::UTC
}
} }
} },
}; };
let native = esp_time().await.with_timezone(&tz).to_rfc3339(); let native = esp_time().await.with_timezone(&tz).to_rfc3339();
@@ -164,7 +158,7 @@ pub(crate) async fn get_time<T, const N: usize>(
let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await { let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(time) => time.with_timezone(&tz).to_rfc3339(), Ok(time) => time.with_timezone(&tz).to_rfc3339(),
Err(err) => { Err(err) => {
format!("Error getting time: {}", err) format!("Error getting time: {err}")
} }
}; };
@@ -181,6 +175,6 @@ pub(crate) async fn get_log_localization_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>, _request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> { ) -> FatResult<Option<String>> {
Ok(Some(serde_json::to_string( Ok(Some(serde_json::to_string(
&LogMessage::to_log_localisation_config(), &LogMessage::log_localisation_config(),
)?)) )?))
} }

View File

@@ -1,7 +1,7 @@
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::log::LOG_ACCESS; use crate::log::LOG_ACCESS;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
pub(crate) async fn get_log<T, const N: usize>( pub(crate) async fn get_log<T, const N: usize>(
conn: &mut Connection<'_, T, N>, conn: &mut Connection<'_, T, N>,

View File

@@ -1,6 +1,6 @@
use crate::fat_error::FatError; use crate::fat_error::FatError;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
pub(crate) async fn serve_favicon<T, const N: usize>( pub(crate) async fn serve_favicon<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>, conn: &mut Connection<'_, T, { N }>,

View File

@@ -19,7 +19,7 @@ use crate::webserver::get_log::get_log;
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index}; use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
use crate::webserver::ota::ota_operations; use crate::webserver::ota::ota_operations;
use crate::webserver::post_json::{ use crate::webserver::post_json::{
board_test, night_lamp_test, pump_test, set_config, wifi_scan, write_time, detect_sensors, board_test, detect_sensors, night_lamp_test, pump_test, set_config, wifi_scan, write_time,
}; };
use crate::{bail, BOARD_ACCESS}; use crate::{bail, BOARD_ACCESS};
use alloc::borrow::ToOwned; use alloc::borrow::ToOwned;
@@ -32,11 +32,11 @@ use core::result::Result::Ok;
use core::sync::atomic::{AtomicBool, Ordering}; use core::sync::atomic::{AtomicBool, Ordering};
use edge_http::io::server::{Connection, Handler, Server}; use edge_http::io::server::{Connection, Handler, Server};
use edge_http::Method; use edge_http::Method;
use edge_nal::io::{Read, Write};
use edge_nal::TcpBind; use edge_nal::TcpBind;
use edge_nal_embassy::{Tcp, TcpBuffers}; use edge_nal_embassy::{Tcp, TcpBuffers};
use embassy_net::Stack; use embassy_net::Stack;
use embassy_time::Instant; use embassy_time::Instant;
use embedded_io_async::{Read, Write};
use log::{error, info}; use log::{error, info};
struct HTTPRequestRouter { struct HTTPRequestRouter {
@@ -64,7 +64,7 @@ impl Handler for HTTPRequestRouter {
file_operations(conn, method, &path, &prefix).await? file_operations(conn, method, &path, &prefix).await?
} else if path == "/ota" { } else if path == "/ota" {
ota_operations(conn, method).await.map_err(|e| { ota_operations(conn, method).await.map_err(|e| {
error!("Error handling ota: {}", e); error!("Error handling ota: {e}");
e e
})? })?
} else { } else {
@@ -203,7 +203,7 @@ async fn handle_json<'a, T, const N: usize>(
) -> FatResult<u32> ) -> FatResult<u32>
where where
T: Read + Write, T: Read + Write,
<T as embedded_io_async::ErrorType>::Error: Debug, <T as edge_nal::io::ErrorType>::Error: Debug,
{ {
match chain { match chain {
Ok(answer) => match answer { Ok(answer) => match answer {
@@ -238,7 +238,7 @@ where
}, },
Err(err) => { Err(err) => {
let error_text = err.to_string(); let error_text = err.to_string();
info!("error handling process {}", error_text); info!("error handling process {error_text}");
conn.initiate_response( conn.initiate_response(
500, 500,
Some("OK"), Some("OK"),

View File

@@ -3,7 +3,7 @@ use crate::webserver::read_up_to_bytes_from_request;
use crate::BOARD_ACCESS; use crate::BOARD_ACCESS;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use edge_http::Method; use edge_http::Method;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info; use log::info;
pub(crate) async fn ota_operations<T, const N: usize>( pub(crate) async fn ota_operations<T, const N: usize>(
@@ -32,7 +32,7 @@ where
let mut chunk = 0; let mut chunk = 0;
loop { loop {
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?; let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
if buf.len() == 0 { if buf.is_empty() {
info!("file request for ota finished"); info!("file request for ota finished");
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().finalize_ota().await?; board.board_hal.get_esp().finalize_ota().await?;
@@ -45,11 +45,11 @@ where
board board
.board_hal .board_hal
.get_esp() .get_esp()
.write_ota(offset as u32, &*buf) .write_ota(offset as u32, &buf)
.await?; .await?;
} }
offset = offset + buf.len(); offset += buf.len();
chunk = chunk + 1; chunk += 1;
} }
BOARD_ACCESS BOARD_ACCESS
.get() .get()

View File

@@ -7,7 +7,7 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec; use alloc::vec::Vec;
use chrono::DateTime; use chrono::DateTime;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -113,7 +113,7 @@ where
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().save_config(all).await?; board.board_hal.get_esp().save_config(all).await?;
info!("Wrote config config {:?} with size {}", config, length); info!("Wrote config config {config:?} with size {length}");
board.board_hal.set_config(config); board.board_hal.set_config(config);
Ok(Some("Ok".to_string())) Ok(Some("Ok".to_string()))
} }

View File

@@ -94,9 +94,9 @@
} }
}, },
"node_modules/@jsonjoy.com/buffers": { "node_modules/@jsonjoy.com/buffers": {
"version": "1.0.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
"integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -128,19 +128,20 @@
} }
}, },
"node_modules/@jsonjoy.com/json-pack": { "node_modules/@jsonjoy.com/json-pack": {
"version": "1.14.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz",
"integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@jsonjoy.com/base64": "^1.1.2", "@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/codegen": "^1.0.0",
"@jsonjoy.com/json-pointer": "^1.0.1", "@jsonjoy.com/json-pointer": "^1.0.2",
"@jsonjoy.com/util": "^1.9.0", "@jsonjoy.com/util": "^1.9.0",
"hyperdyperid": "^1.2.0", "hyperdyperid": "^1.2.0",
"thingies": "^2.5.0" "thingies": "^2.5.0",
"tree-dump": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=10.0" "node": ">=10.0"
@@ -318,22 +319,22 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/express": { "node_modules/@types/express": {
"version": "4.17.23", "version": "4.17.25",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/body-parser": "*", "@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33", "@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*", "@types/qs": "*",
"@types/serve-static": "*" "@types/serve-static": "^1"
} }
}, },
"node_modules/@types/express-serve-static-core": { "node_modules/@types/express-serve-static-core": {
"version": "4.19.6", "version": "4.19.7",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -358,9 +359,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/http-proxy": { "node_modules/@types/http-proxy": {
"version": "1.17.16", "version": "1.17.17",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz",
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -381,12 +382,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.5.2", "version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.12.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@types/node-forge": { "node_modules/@types/node-forge": {
@@ -421,13 +422,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/send": { "node_modules/@types/send": {
"version": "0.17.5", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/mime": "^1",
"@types/node": "*" "@types/node": "*"
} }
}, },
@@ -442,15 +442,26 @@
} }
}, },
"node_modules/@types/serve-static": { "node_modules/@types/serve-static": {
"version": "1.15.8", "version": "1.15.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/http-errors": "*", "@types/http-errors": "*",
"@types/node": "*", "@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": { "node_modules/@types/sockjs": {
@@ -832,9 +843,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.6", "version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
@@ -871,24 +882,24 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"destroy": "1.2.0", "destroy": "~1.2.0",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.4.24", "iconv-lite": "~0.4.24",
"on-finished": "2.4.1", "on-finished": "~2.4.1",
"qs": "6.13.0", "qs": "~6.14.0",
"raw-body": "2.5.2", "raw-body": "~2.5.3",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8", "node": ">= 0.8",
@@ -939,9 +950,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.26.2", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -958,11 +969,11 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.3", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001741", "caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.218", "electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.21", "node-releases": "^2.0.27",
"update-browserslist-db": "^1.1.3" "update-browserslist-db": "^1.2.0"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@@ -1046,9 +1057,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001743", "version": "1.0.30001761",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
"integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1281,9 +1292,9 @@
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.1", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1291,9 +1302,9 @@
} }
}, },
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -1384,9 +1395,9 @@
} }
}, },
"node_modules/default-browser": { "node_modules/default-browser": {
"version": "5.2.1", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz",
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1401,9 +1412,9 @@
} }
}, },
"node_modules/default-browser-id": { "node_modules/default-browser-id": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1570,9 +1581,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.222", "version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/emojis-list": { "node_modules/emojis-list": {
@@ -1596,9 +1607,9 @@
} }
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.3", "version": "5.18.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.4",
@@ -1619,9 +1630,9 @@
} }
}, },
"node_modules/envinfo": { "node_modules/envinfo": {
"version": "7.14.0", "version": "7.21.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
"integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -1652,9 +1663,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.7.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-object-atoms": { "node_modules/es-object-atoms": {
@@ -1756,40 +1767,40 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.2", "version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "~1.20.3",
"content-disposition": "0.5.4", "content-disposition": "~0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.7.1", "cookie": "~0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "~1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~2.0.0", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.3.1", "finalhandler": "~1.3.1",
"fresh": "0.5.2", "fresh": "~0.5.2",
"http-errors": "2.0.0", "http-errors": "~2.0.0",
"merge-descriptors": "1.0.3", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "~2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.12", "path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "~6.14.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.19.0", "send": "~0.19.0",
"serve-static": "1.16.2", "serve-static": "~1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "~2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"utils-merge": "1.0.1", "utils-merge": "1.0.1",
"vary": "~1.1.2" "vary": "~1.1.2"
@@ -1809,9 +1820,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-equals": { "node_modules/fast-equals": {
"version": "5.2.2", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -1879,9 +1890,9 @@
} }
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@@ -1913,18 +1924,18 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~2.0.0", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "~2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"statuses": "2.0.1", "statuses": "~2.0.2",
"unpipe": "~1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
@@ -2073,9 +2084,9 @@
} }
}, },
"node_modules/glob-to-regex.js": { "node_modules/glob-to-regex.js": {
"version": "1.0.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz",
"integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -2269,9 +2280,9 @@
} }
}, },
"node_modules/html-webpack-plugin": { "node_modules/html-webpack-plugin": {
"version": "5.6.4", "version": "5.6.5",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz",
"integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2329,20 +2340,24 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"depd": "2.0.0", "depd": "~2.0.0",
"inherits": "2.0.4", "inherits": "~2.0.4",
"setprototypeof": "1.2.0", "setprototypeof": "~1.2.0",
"statuses": "2.0.1", "statuses": "~2.0.2",
"toidentifier": "1.0.1" "toidentifier": "~1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/http-parser-js": { "node_modules/http-parser-js": {
@@ -2461,9 +2476,9 @@
} }
}, },
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "2.2.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2708,9 +2723,9 @@
} }
}, },
"node_modules/launch-editor": { "node_modules/launch-editor": {
"version": "2.11.1", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
"integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2719,12 +2734,16 @@
} }
}, },
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.3.0", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.11.5" "node": ">=6.11.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/loader-utils": { "node_modules/loader-utils": {
@@ -2793,9 +2812,9 @@
} }
}, },
"node_modules/memfs": { "node_modules/memfs": {
"version": "4.42.0", "version": "4.51.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.42.0.tgz", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz",
"integrity": "sha512-RG+4HMGyIVp6UWDWbFmZ38yKrSzblPnfJu0PyPt0hw52KW4PPlPp+HdV4qZBG0hLDuYVnf8wfQT4NymKXnlQjA==", "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -2806,9 +2825,6 @@
"tree-dump": "^1.0.3", "tree-dump": "^1.0.3",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"engines": {
"node": ">= 4.0.0"
},
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/streamich" "url": "https://github.com/sponsors/streamich"
@@ -2952,9 +2968,9 @@
} }
}, },
"node_modules/node-forge": { "node_modules/node-forge": {
"version": "1.3.1", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
"dev": true, "dev": true,
"license": "(BSD-3-Clause OR GPL-2.0)", "license": "(BSD-3-Clause OR GPL-2.0)",
"engines": { "engines": {
@@ -2962,9 +2978,9 @@
} }
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.21", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/normalize-path": { "node_modules/normalize-path": {
@@ -3270,13 +3286,13 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -3325,16 +3341,16 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "2.5.2", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.4.24", "iconv-lite": "~0.4.24",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@@ -3509,13 +3525,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.16.0", "is-core-module": "^2.16.1",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
}, },
@@ -3635,9 +3651,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.2", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.9", "@types/json-schema": "^7.0.9",
@@ -3675,9 +3691,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@@ -3688,40 +3704,30 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.19.0", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"destroy": "1.2.0", "destroy": "1.2.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"fresh": "0.5.2", "fresh": "~0.5.2",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"mime": "1.6.0", "mime": "1.6.0",
"ms": "2.1.3", "ms": "2.1.3",
"on-finished": "2.4.1", "on-finished": "~2.4.1",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"statuses": "2.0.1" "statuses": "~2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "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": { "node_modules/send/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3808,16 +3814,16 @@
} }
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.16.2", "version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encodeurl": "~2.0.0", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.19.0" "send": "~0.19.1"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@@ -4110,9 +4116,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4169,9 +4175,9 @@
} }
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.3", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -4182,9 +4188,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.44.0", "version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
@@ -4200,9 +4206,9 @@
} }
}, },
"node_modules/terser-webpack-plugin": { "node_modules/terser-webpack-plugin": {
"version": "5.3.14", "version": "5.3.16",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
@@ -4355,9 +4361,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.2", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -4369,9 +4375,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.12.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicorn-magic": { "node_modules/unicorn-magic": {
@@ -4397,9 +4403,9 @@
} }
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -4481,9 +4487,9 @@
} }
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.4", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz",
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
@@ -4504,9 +4510,9 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.101.3", "version": "5.104.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
@@ -4517,22 +4523,22 @@
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0", "acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3", "acorn-import-phases": "^1.0.3",
"browserslist": "^4.24.0", "browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2", "chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.3", "enhanced-resolve": "^5.17.4",
"es-module-lexer": "^1.2.1", "es-module-lexer": "^2.0.0",
"eslint-scope": "5.1.1", "eslint-scope": "5.1.1",
"events": "^3.2.0", "events": "^3.2.0",
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11", "graceful-fs": "^4.2.11",
"json-parse-even-better-errors": "^2.3.1", "json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0", "loader-runner": "^4.3.1",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^4.3.2", "schema-utils": "^4.3.3",
"tapable": "^2.1.1", "tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.11", "terser-webpack-plugin": "^5.3.16",
"watchpack": "^2.4.1", "watchpack": "^2.4.4",
"webpack-sources": "^3.3.3" "webpack-sources": "^3.3.3"
}, },
"bin": { "bin": {
@@ -4608,14 +4614,14 @@
} }
}, },
"node_modules/webpack-dev-middleware": { "node_modules/webpack-dev-middleware": {
"version": "7.4.3", "version": "7.4.5",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.3.tgz", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz",
"integrity": "sha512-5kA/PzpZzDz5mNOkcNLmU1UdjGeSSxd7rt1akWpI70jMNHLASiBPRaQZn0hgyhvhawfIwSnnLfDABIxL3ueyFg==", "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"colorette": "^2.0.10", "colorette": "^2.0.10",
"memfs": "^4.6.0", "memfs": "^4.43.1",
"mime-types": "^3.0.1", "mime-types": "^3.0.1",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"range-parser": "^1.2.1", "range-parser": "^1.2.1",
@@ -4648,16 +4654,20 @@
} }
}, },
"node_modules/webpack-dev-middleware/node_modules/mime-types": { "node_modules/webpack-dev-middleware/node_modules/mime-types": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "^1.54.0" "mime-db": "^1.54.0"
}, },
"engines": { "engines": {
"node": ">= 0.6" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/webpack-dev-server": { "node_modules/webpack-dev-server": {

View File

@@ -84,7 +84,7 @@ export enum BatteryBoardVersion {
} }
export enum BoardVersion { export enum BoardVersion {
INITIAL = "INITIAL", Initial = "Initial",
V3 = "V3", V3 = "V3",
V4 = "V4" V4 = "V4"
} }
@@ -112,6 +112,7 @@ export interface PlantConfig {
pump_cooldown_min: number, pump_cooldown_min: number,
pump_hour_start: number, pump_hour_start: number,
pump_hour_end: number, pump_hour_end: number,
pump_limit_ml: number,
sensor_a: boolean, sensor_a: boolean,
sensor_b: boolean, sensor_b: boolean,
max_consecutive_pump_count: number, max_consecutive_pump_count: number,
@@ -166,7 +167,6 @@ export interface BatteryState {
temperature: string temperature: string
voltage_milli_volt: string, voltage_milli_volt: string,
current_milli_ampere: string, current_milli_ampere: string,
cycle_count: string,
design_milli_ampere: string, design_milli_ampere: string,
remaining_milli_ampere: string, remaining_milli_ampere: string,
state_of_charge: string, state_of_charge: string,
@@ -174,12 +174,12 @@ export interface BatteryState {
} }
export interface DetectionPlant { export interface DetectionPlant {
a: boolean, sensor_a: boolean,
b: boolean sensor_b: boolean
} }
export interface DetectionResult { export interface DetectionResult {
plants: DetectionPlant[] plant: DetectionPlant[]
} }
export interface TankInfo { export interface TankInfo {
@@ -200,4 +200,5 @@ export interface TankInfo {
/// water temperature /// water temperature
water_temp: number | null, water_temp: number | null,
temp_sensor_error: string | null temp_sensor_error: string | null
} }

View File

@@ -23,10 +23,6 @@
<span class="powerflexkey">mA:</span> <span class="powerflexkey">mA:</span>
<span class="powerflexvalue" id="battery_current_milli_ampere" ></span> <span class="powerflexvalue" id="battery_current_milli_ampere" ></span>
</div> </div>
<div class="flexcontainer">
<span class="powerflexkey">Cycles:</span>
<span class="powerflexvalue" id="battery_cycle_count" ></span>
</div>
<div class="flexcontainer"> <div class="flexcontainer">
<span class="powerflexkey">design mA:</span> <span class="powerflexkey">design mA:</span>
<span class="powerflexvalue" id="battery_design_milli_ampere" ></span> <span class="powerflexvalue" id="battery_design_milli_ampere" ></span>

View File

@@ -4,7 +4,6 @@ import {BatteryState} from "./api";
export class BatteryView{ export class BatteryView{
voltage_milli_volt: HTMLSpanElement; voltage_milli_volt: HTMLSpanElement;
current_milli_ampere: HTMLSpanElement; current_milli_ampere: HTMLSpanElement;
cycle_count: HTMLSpanElement;
design_milli_ampere: HTMLSpanElement; design_milli_ampere: HTMLSpanElement;
remaining_milli_ampere: HTMLSpanElement; remaining_milli_ampere: HTMLSpanElement;
state_of_charge: HTMLSpanElement; state_of_charge: HTMLSpanElement;
@@ -18,7 +17,6 @@ export class BatteryView{
(document.getElementById("batteryview") as HTMLElement).innerHTML = require("./batteryview.html") (document.getElementById("batteryview") as HTMLElement).innerHTML = require("./batteryview.html")
this.voltage_milli_volt = document.getElementById("battery_voltage_milli_volt") as HTMLSpanElement; 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.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.design_milli_ampere = document.getElementById("battery_design_milli_ampere") as HTMLSpanElement;
this.remaining_milli_ampere = document.getElementById("battery_remaining_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; this.state_of_charge = document.getElementById("battery_state_of_charge") as HTMLSpanElement;
@@ -41,7 +39,6 @@ export class BatteryView{
if (batterystate == null) { if (batterystate == null) {
this.voltage_milli_volt.innerText = "N/A" this.voltage_milli_volt.innerText = "N/A"
this.current_milli_ampere.innerText = "N/A" this.current_milli_ampere.innerText = "N/A"
this.cycle_count.innerText = "N/A"
this.design_milli_ampere.innerText = "N/A" this.design_milli_ampere.innerText = "N/A"
this.remaining_milli_ampere.innerText = "N/A" this.remaining_milli_ampere.innerText = "N/A"
this.state_of_charge.innerText = "N/A" this.state_of_charge.innerText = "N/A"
@@ -50,12 +47,11 @@ export class BatteryView{
} else { } else {
this.voltage_milli_volt.innerText = String(+batterystate.voltage_milli_volt/1000) this.voltage_milli_volt.innerText = String(+batterystate.voltage_milli_volt/1000)
this.current_milli_ampere.innerText = batterystate.current_milli_ampere 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.design_milli_ampere.innerText = batterystate.design_milli_ampere
this.remaining_milli_ampere.innerText = batterystate.remaining_milli_ampere this.remaining_milli_ampere.innerText = batterystate.remaining_milli_ampere
this.state_of_charge.innerText = batterystate.state_of_charge this.state_of_charge.innerText = batterystate.state_of_charge
this.state_of_health.innerText = batterystate.state_of_health this.state_of_health.innerText = batterystate.state_of_health
this.temperature.innerText = String(+batterystate.temperature / 100) this.temperature.innerText = String(+batterystate.temperature)
} }

View File

@@ -12,7 +12,7 @@ export class HardwareConfigView {
Object.keys(BoardVersion).forEach(version => { Object.keys(BoardVersion).forEach(version => {
let option = document.createElement("option"); let option = document.createElement("option");
if (version == BoardVersion.INITIAL.toString()){ if (version == BoardVersion.Initial.toString()){
option.selected = true option.selected = true
} }
option.innerText = version.toString(); option.innerText = version.toString();
@@ -42,4 +42,4 @@ export class HardwareConfigView {
battery : BatteryBoardVersion[this.hardware_battery_value.value as keyof typeof BatteryBoardVersion], battery : BatteryBoardVersion[this.hardware_battery_value.value as keyof typeof BatteryBoardVersion],
} }
} }
} }

View File

@@ -2,6 +2,7 @@ import {deepEqual} from 'fast-equals';
declare var PUBLIC_URL: string; declare var PUBLIC_URL: string;
console.log("Url is " + PUBLIC_URL); console.log("Url is " + PUBLIC_URL);
console.log("Public url is " + PUBLIC_URL);
document.body.innerHTML = require('./main.html') as string; document.body.innerHTML = require('./main.html') as string;
@@ -28,7 +29,7 @@ import {
SetTime, SSIDList, TankInfo, SetTime, SSIDList, TankInfo,
TestPump, TestPump,
VersionInfo, VersionInfo,
FileList, SolarState, PumpTestResult FileList, SolarState, PumpTestResult, DetectionResult
} from "./api"; } from "./api";
import {SolarView} from "./solarview"; import {SolarView} from "./solarview";
import {toast} from "./toast"; import {toast} from "./toast";
@@ -289,6 +290,7 @@ export class Controller {
method: "POST", method: "POST",
body: pretty body: pretty
}).then( }).then(
_ => controller.progressview.removeProgress("write_rtc") _ => controller.progressview.removeProgress("write_rtc")
) )
} }
@@ -296,6 +298,7 @@ export class Controller {
configChanged() { configChanged() {
const current = controller.getConfig(); const current = controller.getConfig();
var pretty = JSON.stringify(current, undefined, 0); var pretty = JSON.stringify(current, undefined, 0);
controller.submitView.setJson(pretty); controller.submitView.setJson(pretty);
@@ -375,11 +378,15 @@ export class Controller {
fetch(PUBLIC_URL + "/detect_sensors", { method: "POST" }) fetch(PUBLIC_URL + "/detect_sensors", { method: "POST" })
.then(response => response.json()) .then(response => response.json())
.then (json => json as DetectionResult)
.then(json => { .then(json => {
clearTimeout(timerId); clearTimeout(timerId);
controller.progressview.removeProgress("detect_sensors"); controller.progressview.removeProgress("detect_sensors");
const pretty = JSON.stringify(json); const pretty = JSON.stringify(json);
toast.info("Detection result: " + pretty); toast.info("Detection result: " + pretty);
console.log(pretty);
this.plantViews.applyDetectionResult(json);
}) })
.catch(error => { .catch(error => {
clearTimeout(timerId); clearTimeout(timerId);

View File

@@ -53,7 +53,7 @@
Mode: Mode:
</div> </div>
<select class="plantvalue" id="plant_${plantId}_mode"> <select class="plantvalue" id="plant_${plantId}_mode">
<option value="OFF">Off</option> <option value="Off">Off</option>
<option value="TargetMoisture">Target</option> <option value="TargetMoisture">Target</option>
<option value="MinMoisture">Min Moisture</option> <option value="MinMoisture">Min Moisture</option>
<option value="TimerOnly">Timer</option> <option value="TimerOnly">Timer</option>

View File

@@ -1,4 +1,4 @@
import {PlantConfig, PumpTestResult} from "./api"; import {DetectionPlant, DetectionResult, PlantConfig, PumpTestResult} from "./api";
const PLANT_COUNT = 8; const PLANT_COUNT = 8;
@@ -47,6 +47,13 @@ export class PlantViews {
const plantView = this.plants[plantId]; const plantView = this.plants[plantId];
plantView.setTestResult(response) plantView.setTestResult(response)
} }
applyDetectionResult(json: DetectionResult) {
for (let i = 0; i < PLANT_COUNT; i++) {
var plantResult = json.plant[i];
this.plants[i].setDetectionResult(plantResult);
}
}
} }
@@ -80,9 +87,12 @@ export class PlantView {
private readonly pump_test_pump_time: HTMLElement; private readonly pump_test_pump_time: HTMLElement;
private readonly pump_test_flow_ml: HTMLElement; private readonly pump_test_flow_ml: HTMLElement;
private readonly pump_test_flow_raw: HTMLElement; private readonly pump_test_flow_raw: HTMLElement;
private showDisabled: boolean = false;
private readonly controller: Controller;
constructor(plantId: number, parent: HTMLDivElement, controller: Controller) { constructor(plantId: number, parent: HTMLDivElement, controller: Controller) {
this.controller = controller;
this.plantId = plantId; this.plantId = plantId;
this.plantDiv = document.createElement("div")! as HTMLDivElement this.plantDiv = document.createElement("div")! as HTMLDivElement
const template = require('./plant.html') as string; const template = require('./plant.html') as string;
@@ -213,10 +223,18 @@ export class PlantView {
console.log("updateVisibility plantConfig: " + plantConfig.mode) console.log("updateVisibility plantConfig: " + plantConfig.mode)
let showSensor = plantConfig.sensor_a || plantConfig.sensor_b let showSensor = plantConfig.sensor_a || plantConfig.sensor_b
let showPump = plantConfig.mode !== "OFF" let showPump = plantConfig.mode !== "Off"
let showTarget = plantConfig.mode === "TargetMoisture" let showTarget = plantConfig.mode === "TargetMoisture"
let showMin = plantConfig.mode === "MinMoisture" let showMin = plantConfig.mode === "MinMoisture"
if(this.showDisabled || plantConfig.sensor_a || plantConfig.sensor_b) {
console.log("Showing plant " + this.plantId);
this.plantDiv.style.display = "block";
} else {
console.log("Hiding plant " + this.plantId);
this.plantDiv.style.display = "none";
}
console.log("updateVisibility showsensor: " + showSensor + " pump " + showPump + " target " +showTarget + " min " + showMin) console.log("updateVisibility showsensor: " + showSensor + " pump " + showPump + " target " +showTarget + " min " + showMin)
for (const element of Array.from(sensorOnly)) { for (const element of Array.from(sensorOnly)) {
@@ -299,6 +317,7 @@ export class PlantView {
target_moisture: this.targetMoisture.valueAsNumber, target_moisture: this.targetMoisture.valueAsNumber,
min_moisture: this.minMoisture.valueAsNumber, min_moisture: this.minMoisture.valueAsNumber,
pump_time_s: this.pumpTimeS.valueAsNumber, pump_time_s: this.pumpTimeS.valueAsNumber,
pump_limit_ml: 5000,
pump_cooldown_min: this.pumpCooldown.valueAsNumber, pump_cooldown_min: this.pumpCooldown.valueAsNumber,
pump_hour_start: +this.pumpHourStart.value, pump_hour_start: +this.pumpHourStart.value,
pump_hour_end: +this.pumpHourEnd.value, pump_hour_end: +this.pumpHourEnd.value,
@@ -314,4 +333,20 @@ export class PlantView {
this.updateVisibility(conv); this.updateVisibility(conv);
return conv; return conv;
} }
setDetectionResult(plantResult: DetectionPlant) {
var changed = false;
if (this.sensorAInstalled.checked != plantResult.sensor_b){
changed = true;
this.sensorAInstalled.checked = plantResult.sensor_a;
}
if (this.sensorBInstalled.checked != plantResult.sensor_b){
changed = true;
this.sensorBInstalled.checked = plantResult.sensor_b;
}
if (changed) {
this.controller.configChanged();
}
}
} }

View File

@@ -1,5 +1,5 @@
import { Controller } from "./main"; import { Controller } from "./main";
import {TankConfig, TankInfo} from "./api"; import {DetectionResult, TankConfig, TankInfo} from "./api";
export class TankConfigView { export class TankConfigView {
private readonly tank_useable_ml: HTMLInputElement; private readonly tank_useable_ml: HTMLInputElement;
@@ -157,4 +157,6 @@ export class TankConfigView {
ml_per_pulse: this.ml_per_pulse.valueAsNumber ml_per_pulse: this.ml_per_pulse.valueAsNumber
} }
} }
}
}

View File

@@ -20,6 +20,7 @@ class ToastService {
} }
error(message: string) { error(message: string) {
console.error(message);
const el = this.createToast(message, 'error'); const el = this.createToast(message, 'error');
this.container.appendChild(el); this.container.appendChild(el);
// Only dismiss on click // Only dismiss on click

View File

@@ -10,7 +10,7 @@ console.log("Dev server is " + isDevServer);
var host; var host;
if (isDevServer) { if (isDevServer) {
//ensure no trailing / //ensure no trailing /
host = 'http://10.23.44.186'; host = 'http://192.168.1.105';
} else { } else {
host = ''; host = '';
} }

8
Software/Shared/canapi/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

11
Software/Shared/canapi/.idea/canapi.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/canapi.iml" filepath="$PROJECT_DIR$/.idea/canapi.iml" />
</modules>
</component>
</project>

7
Software/Shared/canapi/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/../../../website/themes/blowfish" vcs="Git" />
</component>
</project>

View File

@@ -44,7 +44,6 @@ pub mod id {
pub const MOISTURE_DATA_OFFSET: u16 = 0; // periodic data from sensor (sensor -> controller) pub const MOISTURE_DATA_OFFSET: u16 = 0; // periodic data from sensor (sensor -> controller)
pub const IDENTIFY_CMD_OFFSET: u16 = 32; // identify LED command (controller -> sensor) pub const IDENTIFY_CMD_OFFSET: u16 = 32; // identify LED command (controller -> sensor)
#[inline] #[inline]
pub const fn plant_id(message_type_offset: u16, sensor: SensorSlot, plant: u16) -> u16 { pub const fn plant_id(message_type_offset: u16, sensor: SensorSlot, plant: u16) -> u16 {
match sensor { match sensor {
@@ -56,8 +55,8 @@ pub mod id {
/// Kinds of message spaces recognized by the addressing scheme. /// Kinds of message spaces recognized by the addressing scheme.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageKind { pub enum MessageKind {
MoistureData, // sensor -> controller MoistureData, // sensor -> controller
IdentifyCmd, // controller -> sensor IdentifyCmd, // controller -> sensor
} }
/// Try to classify a received 11-bit standard ID into a known message kind and extract plant and sensor slot. /// Try to classify a received 11-bit standard ID into a known message kind and extract plant and sensor slot.
@@ -72,11 +71,15 @@ pub mod id {
// Helper: decode within a given group offset // Helper: decode within a given group offset
const fn decode_in_group(rel: u16, group_base: u16) -> Option<(u8, SensorSlot)> { const fn decode_in_group(rel: u16, group_base: u16) -> Option<(u8, SensorSlot)> {
if rel < group_base { return None; } if rel < group_base {
return None;
}
let inner = rel - group_base; let inner = rel - group_base;
if inner < PLANTS_PER_GROUP { // A slot if inner < PLANTS_PER_GROUP {
// A slot
Some((inner as u8, SensorSlot::A)) Some((inner as u8, SensorSlot::A))
} else if inner >= B_OFFSET && inner < B_OFFSET + PLANTS_PER_GROUP { // B slot } else if inner >= B_OFFSET && inner < B_OFFSET + PLANTS_PER_GROUP {
// B slot
Some(((inner - B_OFFSET) as u8, SensorSlot::B)) Some(((inner - B_OFFSET) as u8, SensorSlot::B))
} else { } else {
None None
@@ -118,9 +121,9 @@ pub mod id {
/// Fits into 5 bytes with bincode-v2 (no varint): u8 + u8 + u16 = 4, alignment may keep 4. /// Fits into 5 bytes with bincode-v2 (no varint): u8 + u8 + u16 = 4, alignment may keep 4.
#[derive(Debug, Clone, Copy, Encode, Decode)] #[derive(Debug, Clone, Copy, Encode, Decode)]
pub struct MoistureData { pub struct MoistureData {
pub plant: u8, // 0..MAX_PLANTS-1 pub plant: u8, // 0..MAX_PLANTS-1
pub sensor: SensorSlot, // A/B pub sensor: SensorSlot, // A/B
pub hz: u16, // measured frequency of moisture sensor pub hz: u16, // measured frequency of moisture sensor
} }
/// Request a sensor to report immediately (controller -> sensor). /// Request a sensor to report immediately (controller -> sensor).

View File

@@ -1,124 +0,0 @@
use std::{collections::VecDeque, env, process::Command};
use vergen::EmitBuilder;
fn linker_be_nice() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
let kind = &args[1];
let what = &args[2];
match kind.as_str() {
"undefined-symbol" => match what.as_str() {
"_defmt_timestamp" => {
eprintln!();
eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`");
eprintln!();
}
"_stack_start" => {
eprintln!();
eprintln!("💡 Is the linker script `linkall.x` missing?");
eprintln!();
}
"esp_wifi_preempt_enable"
| "esp_wifi_preempt_yield_task"
| "esp_wifi_preempt_task_create" => {
eprintln!();
eprintln!("💡 `esp-wifi` has no scheduler enabled. Make sure you have the `builtin-scheduler` feature enabled, or that you provide an external scheduler.");
eprintln!();
}
"embedded_test_linker_file_not_added_to_rustflags" => {
eprintln!();
eprintln!("💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests");
eprintln!();
}
_ => (),
},
// we don't have anything helpful for "missing-lib" yet
_ => {
std::process::exit(1);
}
}
std::process::exit(0);
}
println!(
"cargo:rustc-link-arg=--error-handling-script={}",
std::env::current_exe().unwrap().display()
);
}
fn main() {
if Command::new("podman").arg("--version").output().is_err() {
println!("Could not find `podman` installation, assuming the developer has setup all required tool for build manually! … ")
}
webpack();
linker_be_nice();
let _ = EmitBuilder::builder().all_git().all_build().emit();
}
fn webpack() {
//println!("cargo:rerun-if-changed=./src/src_webpack");
Command::new("rm")
.arg("./src/webserver/bundle.js.gz")
.output()
.unwrap();
match Command::new("cmd").spawn() {
Ok(_) => {
println!("Assuming build on windows");
let output = Command::new("cmd")
.arg("/K")
.arg("npx")
.arg("webpack")
.current_dir("./src_webpack")
.output()
.unwrap();
println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success());
// move webpack results to rust webserver src
let _ = Command::new("cmd")
.arg("/K")
.arg("move")
.arg("./src_webpack/bundle.js.gz")
.arg("./src/webserver")
.output()
.unwrap();
let _ = Command::new("cmd")
.arg("/K")
.arg("move")
.arg("./src_webpack/index.html.gz")
.arg("./src/webserver")
.output()
.unwrap();
}
Err(_) => {
println!("Assuming build on linux");
let output = Command::new("npx")
.arg("webpack")
.current_dir("./src_webpack")
.output()
.unwrap();
println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success());
// move webpack results to rust webserver src
let _ = Command::new("mv")
.arg("./src_webpack/bundle.js.gz")
.arg("./src/webserver")
.output()
.unwrap();
let _ = Command::new("mv")
.arg("./src_webpack/index.html.gz")
.arg("./src/webserver")
.output()
.unwrap();
}
}
}

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4"> <module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager">
<exclude-output />
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@@ -5,11 +5,13 @@ draft: false
description: "a description" description: "a description"
tags: ["battery", "bq34z100"] tags: ["battery", "bq34z100"]
--- ---
# BatteryManagment # Battery Management Module
The project contains an additional companion board, with a bq34z100 battery management ic. The project contains an additional companion board (Fuel Gauge), with a bq34z100 battery management IC.
It allows to track the health and charge for an external battery and is supposed to be soldered directly to the battery. It allows to track the health and charge for an external battery and is supposed to be soldered directly to the battery.
The main board contains a xt30 connector for power, and additionally a two pin I2C bus next/combined with it, to communicate with the BatteryManagement. The MainBoard contains a connector for power, and additionally a two-pin I2C bus to communicate with the Battery Management module.
<!-- TODO: Add photo of the new modular Battery Management board -->
# Setup # Setup
{{< alert >}} {{< alert >}}
@@ -28,7 +30,7 @@ A protected Battery is required. There is only a very simplistic output voltage
2. Then the data/register for low voltage flash write protection should be set to 0V, as else with the voltage divider and no further configuration, the IC will refuse all write requests. 2. Then the data/register for low voltage flash write protection should be set to 0V, as else with the voltage divider and no further configuration, the IC will refuse all write requests.
3. After this the supplied golden image can be used, it will setup the battery for 6Ah and a 4S lifepo. Different values can be adjusted after this to the users liking. 3. After this the supplied golden image can be used, it will setup the battery for 6Ah and a 4S lifepo. Different values can be adjusted after this to the users liking.
{{< alert >}} {{< alert >}}
The main board, does not care or process any of the charge discharge limits that can be set. Ensure that the battery can supply enough current as well as accept a 2A charging current from the MPPT system. The main board, does not care or process any of the charge discharge limits that can be set. Ensure that the battery can supply enough current as well as accept a 2.4A charging current from the MPPT system.
{{< /alert >}} {{< /alert >}}
The golden image sets the statups led up, to be in blinky mode. one very long interval means, that the battery is pretty much full. A few very short flashes mean that the battery is nearly empty. No light means, that the battery is in discharge protection and shut down. The golden image sets the statups led up, to be in blinky mode. one very long interval means, that the battery is pretty much full. A few very short flashes mean that the battery is nearly empty. No light means, that the battery is in discharge protection and shut down.

View File

@@ -5,22 +5,39 @@ draft: false
description: "a description" description: "a description"
tags: ["MPPT", "solar"] tags: ["MPPT", "solar"]
--- ---
# MPPT # MPPT Module
If the board is powered by solar, a on board MPPT converter tries to track the best current and voltage, so that the battery gets maximized charging current. If the system is powered by solar, a dedicated MPPT (Maximum Power Point Tracking) module can be added. It tries to track the best current and voltage, so that the battery gets maximized charging current.
The currently used MPPT limits max current to 2A. The currently used MPPT module is based on the CN3795 and limits max current to 2.4A.
There are two adjustable values for the MPPT, Input Power Point and Output Voltage. <!-- TODO: Add photo of the new modular MPPT board -->
The module has two configurable values: **Input Power Point (MPPT)** and **Output Voltage (Battery Charge Voltage)**. These are set using fixed resistors rather than potentiometers to ensure stability and reduce costs.
### Configuration
The voltages are determined by voltage dividers. The default values are:
- **MPPT Voltage:** ~17.1V (set by R2 and R5)
- **Battery Voltage:** ~14.7V (set by R8 and R1)
#### Formulas
The reference voltage for the CN3795 is 1.205V.
- $V_{MPPT} = 1.205 \times (1 + \frac{R2}{R5})$
- $V_{BAT} = 1.205 \times (1 + \frac{R8}{R1})$
Default resistor values:
- $R1, R5 = 2.491k\Omega$
- $R2 = 33k\Omega$
- $R8 = 27k\Omega$
### Status LEDs
- **D4 (Red):** Charging
- **D5 (Green):** Done / Standby
#Setup #Setup
{{< alert >}} {{< alert >}}
Note: Only protected Batteries are supported! Note: Only protected Batteries are supported!
{{< /alert >}} {{< /alert >}}
1. Supply the Ext Charge connector with 20V and a limited current (suggested 100mA). 1. **Verify Resistors:** Before connecting, ensure that R2 and R8 match your solar panel's MPPT voltage and your battery's required charging voltage.
2. If the board does not startup, adjust the MPPT untill it supplies a output voltage. Note: The ESP should be able to startup at any settable output voltage, as it uses a converter to 3.3v anyways. 2. **Initial Test:** Supply the Ext Charge connector with a voltage higher than the MPPT setpoint (e.g., 20V) and a limited current (suggested 100mA).
3. Adjust the Output Voltage to the correct charging voltage for your Chosen Battery. 3. **Check Output:** Measure the output voltage at the battery connector. It should match the calculated $V_{BAT}$.
4. Connect a sink (or the a non fully charged battery) to the Output, now adjust the MPPT so, that the Input Voltage drops to the optimal working Voltage of your Solar panel. The Converter will try to always keep the Panel at this Voltage by adjusting the Output Current accordingly. 4. **Connect Battery:** Once verified, you can connect the battery and the solar panel.
Setting the MPPT slightly lower than the suggested Voltage will ensure more reliable operation, as the optimal voltage might drop a bit due to ageing and dust on the panel. Normally during summer there is more than enough power, so a slight inefficentcy won't be noticable.
However during winter in low light and bad weather conditions, that might be the difference between charging at all or no power. (If the panel does not reach the MPPT voltage, there is no charging current)

View File

@@ -5,11 +5,36 @@ draft: false
description: "a description" description: "a description"
tags: ["nightlight"] tags: ["nightlight"]
--- ---
# Nightlight # Nightlight Module
The board has a adjustable light output, which can be used to power a small led strip. The system supports a dedicated light output module, which can be used to power a small LED strip for night lighting or growth support.
In the configuration, the working hours can be set, as well as if the light is allowed to be turned on, even if the solar panel still supplies power.
The supply should theoretically be able to supply up to 2A with a voltage lower than the battery.
The module is based on the **AP63200** synchronous buck converter. It provides an adjustable output voltage, allowing it to drive various types of LED strips or other low-voltage DC loads.
### Configuration
The output voltage is adjustable via a potentiometer (**RV1**). This allows you to set the brightness or the correct operating voltage for your LED strip.
#### Formula
The reference voltage for the AP63200 is 0.8V. The output voltage is determined by the feedback divider:
- $V_{OUT} = 0.8 \times (1 + \frac{R_{high}}{R5})$
- $R_{high} = R1 + RV1 + R3$
Default values:
- $R1 = 10k\Omega$
- $R3 = 1k\Omega$
- $R5 = 5k\Omega$
- $RV1 = 0\Omega$ to $50k\Omega$ (Potentiometer)
With these values, the output voltage is adjustable from approximately **1.76V** to **9.76V**.
### Status LED
- **I1 (White):** Indicates that the light output is enabled.
# Setup
{{< alert >}} {{< alert >}}
Ensure the voltage is correctly set, before connecting a load the first time! Ensure the voltage is correctly set using **RV1** before connecting a load for the first time! High voltages can damage LED strips.
{{< /alert >}} {{< /alert >}}
1. **Power On:** Enable the light output through the software interface.
2. **Measure Voltage:** Use a multimeter to measure the voltage at the output connector (**LIGHT1**).
3. **Adjust RV1:** Turn the potentiometer until you reach the desired voltage or brightness.
4. **Connect Load:** Once the voltage is set correctly, you can connect your LED strip.

View File

@@ -5,21 +5,38 @@ draft: false
description: "a description" description: "a description"
tags: ["sensor"] tags: ["sensor"]
--- ---
# Sensors & Pumps Module
This functionality is now provided by dedicated modules that can be connected to the MainBoard.
# Sensors # Sensors
The board can address up to 16 sensors, with an A und B sensor per plant. The moisture sensing functionality is handled by a dedicated **CAN bus-based Sensor Module**. This modular approach allows for better scalability and reduces electrical interference by moving the measurement logic closer to the sensors and using digital communication.
The A sensor is always used, the B sensor is optional and use is suggested for larger planters.
The sensor itself are just two spikes with a defined distance. They can be bought readymade or simply be done DIY with two long nails (use galvanized or non rusting ones) ## Sensor Module (CAN bus)
The standard sensor module features its own **CH32V203 RISC-V microcontroller**, which handles the measurement of soil moisture and communicates the results back to the MainBoard via the CAN bus.
The Sensor is switched via multiplexer chips to a 555 oscilator circuit that uses he the earths resistance for charging and discharging a small capacitor. The ESP simply counts how often the charge pulse was seen. * **Capacity:** Supports up to 16 sensors (typically 8 plants with an A and B sensor each).
While this sensor can be easily influenced since it runs in lower uA - mA range, due to the frequencys of several khz for normal moist plants, it is still very resistant, eg a 50hz failure signal from a nearbly power circuit would barely be registered. * **Reliability:** Digital communication via CAN bus ensures data integrity even over longer cable runs and in electrically noisy environments.
Since the Sensor is changing polarity between charging and discharging, corrosion due to organic battery effects is minimized, also it prevents errors due having build a battery (You can easily charge a planter to several V if you do not change polarity). * **Addressing:** The A sensor is always used; the B sensor is optional and suggested for larger planters to provide a better average of the soil moisture.
## Sensor Hardware
The sensors themselves remain simple and cost-effective:
* **Design:** Two spikes with a defined distance.
* **DIY Friendly:** Can be bought readymade or easily made with two long nails (galvanized or stainless steel suggested to prevent rusting).
## Measurement Principle
The new CAN-based sensor module uses a sophisticated measurement technique that replaces the outdated 555-oscillator and multiplexer design. By using a dedicated MCU for measurement:
* **Minimized Corrosion:** The system changes polarity between measurements, minimizing corrosion due to organic battery effects (electrolysis) and preventing errors caused by building up a DC voltage in the soil.
* **Interference Resistance:** The measurement is resistant to common failure signals, such as 50Hz hum from nearby power circuits.
* **Digital Accuracy:** The local MCU processes the analog signals and sends precise digital values to the MainBoard.
# Pumps # Pumps
The board contains 8 low side switched pump outputs. The pumps are running directly from the battery without further voltage conversion, so ensure that the can survive the full voltage range of the battery. The Pump module contains low side switched pump outputs. The pumps are running directly from the battery without further voltage conversion, so ensure that they can survive the full voltage range of the battery.
Each output can supply up to 3A continously. Each output can supply up to 3A continously.
The board will never switch more than one output concurrently, so there is no need to size the battery for higher maximum load. The board will never switch more than one output concurrently, so there is no need to size the battery for higher maximum load.
An additinal extra out exists, that is switched, when any of the pump outputs is supposed to run. An additinal extra output exists, that is switched when any of the pump outputs is supposed to run.
<!-- TODO: Add photo of the new modular Pump and Sensor boards -->
This allows for multiple possible setups This allows for multiple possible setups
## Layout Central Pump ## Layout Central Pump

View File

@@ -11,18 +11,35 @@ tags: ["esp32", "hardware"]
<img src="pcb_back.png" class="grid-w50" /> <img src="pcb_back.png" class="grid-w50" />
{{< /gallery >}} {{< /gallery >}}
<!-- TODO: Add new screenshots of the modular PCB setup -->
{{< gitea server="https://git.mannheim.ccc.de/" repo="C3MA/PlantCtrl" >}} {{< gitea server="https://git.mannheim.ccc.de/" repo="C3MA/PlantCtrl" >}}
## ESP32-C6 ## Modular Design
* RiscV The PlantCtrl hardware has been redesigned from a single large PCB into a more flexible, modular system. This allows users to only build or use the components they actually need for their specific setup.
* Fully done in Kicad (#schematics here link)
* Supports 16 Moisture Sensors (two per plant) The system now consists of a **MainBoard** which acts as the controller and several pluggable or connectable modules.
* Support for 8 Pumps/Valves
* Support for main Pump/Valve ## MainBoard (ESP32-C6)
* All Pump/Valve outputs are 7-24V based on supplied Voltage * **Processor:** ESP32-C6 (RiscV)
* Use of efficient DC-DC Converters for 3.3V and for Led-Nightlight * **Connectivity:** WiFi, Bluetooth, Zigbee/Thread support
* Embedded Solar MPPT Charger * **Form Factor:** Compact main board with headers for modules
* Can also be used with a power supply * **Fully Open Source:** Designed in KiCad
## Available Modules
* **MPPT Charger:** Efficient solar charging for batteries.
* **Pump Driver:** High-current outputs for pumps and valves.
* **Sensor Interface:** Support for multiple moisture sensors.
* **Light Controller:** For LED nightlights or growth lights.
## Capabilities
* **Moisture Sensors:** Supports multiple capacitive or resistive sensors via expansion modules.
* **Pumps/Valves:** Support for multiple independent watering zones.
* **Power:**
* Solar powered with MPPT
* Battery powered with optional Battery Management (Fuel Gauge)
* Can also be used with a standard power supply (7-24V)
* **Efficient Power:** Use of high-efficiency DC-DC converters for 3.3V and peripherals.
## Made with: ## Made with:
<a href="https://www.kicad.org/"> <a href="https://www.kicad.org/">

View File

@@ -5,24 +5,47 @@ draft: false
description: "a description" description: "a description"
tags: ["firmeware", "upload"] tags: ["firmeware", "upload"]
--- ---
# Prebuild # From Source
1. Download image from
2. todo something espflash tool here
# From source
## Preconditions ## Preconditions
* rustup with current version * **Rust:** Current version of `rustup`.
* espup with current version * **ESP32 Toolchain:** `espup` installed and configured.
* npm /npx * **espflash:** Installed via `cargo install espflash`.
* Connect the board via usb to the computer * **Node.js:** `npm` installed (for the web interface).
* Ensure the esp is running (eg not in deepsleep if prior version was installed)
# Compiling and uploading
1. Clone the gitea
2. go to the rust/src_webroot
3. npm -i
4. cd ..
5. cargo run
Depending on the setup of the cargo runner, either a OTA image is build or the firmware is directly flashed to the esp. ## Flashing via USB
1. Connect the MainBoard to your computer via USB.
2. Ensure the board is powered and not in deep sleep.
3. Build the web interface (only required once or if you changed something in `src_webpack`):
```bash
cd Software/MainBoard/rust/src_webpack
npm install
npx webpack
cd ..
```
4. Flash the firmware:
```bash
espflash flash --monitor --partition-table partitions.csv
```
*Note: If the flashing fails, you might need to put the ESP32-C6 into bootloader mode by holding the BOOT button while resetting or connecting USB.*
Note: If the bootloader is currently booting from ota_2, flashing ota_1 via usb will not make it switch. You might need to manually erase it in this case prior. ### Simplified Flashing with Scripts
You can use the provided bash scripts to automate the build and flash process:
* **`./flash.sh`**: Cleans temporary files, builds the web interface, compiles the firmware, and flashes it to the board.
* **`./all.sh`**: Similar to `flash.sh`, but also saves a local `image.bin` of the firmware.
## OTA (Over-the-Air) Update
You can also update the firmware wirelessly if the system is already running and connected to your network.
1. Generate the OTA binary:
```bash
cargo build --release
```
2. The binary will be at `target/riscv32imac-unknown-none-elf/release/plant-ctrl2`.
3. Open the PlantCtrl web interface in your browser.
4. Navigate to the **OTA** section.
5. Upload the `plant-ctrl2` file.
6. The system will reboot into the new firmware once the upload and verification are complete.
## Troubleshooting
* **Bootloader Partition:** If the bootloader is currently booting from `ota_2`, flashing to the factory partition via USB might not automatically switch the active partition. You can use `espflash erase-parts otadata` to reset the OTA state.
* **Serial Terminal:** Use `espflash monitor` to view the serial logs for debugging.

View File

@@ -16,11 +16,11 @@ Connect to the AccessPoint and browse to http://192.168.71.1 to set it up
3. The ESP will now try to connect to a WIFI station if configured and start the webserver, if the connection to the station cannot be made, it will open an AccessPoint instead. 3. The ESP will now try to connect to a WIFI station if configured and start the webserver, if the connection to the station cannot be made, it will open an AccessPoint instead.
# MQTT # MQTT
It is possible to use the topic /stay/alive to enter the config mode. It is possible to use the MQTT topic `{base_topic}/stay_alive` to enter the config mode remotely.
If this topic contains a retained message with the value true, the ESP will enter ConfigMode during it's next cycle and stay there. If this topic contains a retained message with the value `true` or `1`, the ESP will enter ConfigMode during its next cycle and stay there.
Note: You must set /stay/alive to retained false again after this, otherwise it will always enter configmode, even if you exist it via the webui. Note: You must set `{base_topic}/stay_alive` to `false` or `0` (or clear the retained message) after this, otherwise it will always enter configmode, even if you exit it via the webui.
# USB reboot # USB reboot
By connecting to the USB Terminal via a Serial Monitor, the ESP is reset and will enter configmode as if the button for normal operation was pressed By connecting to the USB Terminal via a Serial Monitor, the ESP is reset and will enter configmode as if the button for normal operation was pressed

View File

@@ -6,22 +6,84 @@ description: "a description"
tags: ["mqtt", "esp"] tags: ["mqtt", "esp"]
--- ---
# MQTT # MQTT
An Mqtt server can be configured and will be used to dump all kinds of statistical data. A configured MQTT server will receive statistical and status data from the controller.
|Topic|Example|Description| ### Topics
|/firmware/address|
| Topic | Example | Description | | Topic | Example | Description |
|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------| |-------|---------|-------------|
| firmware/address | 192.168.1.2 | The Ip address in station mode | | `firmware/address` | `192.168.1.2` | IP address in station mode |
| firmware/githash | feature/esp32c6@1ce4d74a | The branch and hash during build time | | `firmware/state` | `VersionInfo { ... }` | Debug information about the current firmware and OTA slots |
| firmware/buildtime | 2025-01-21T20:56:18.168163570Z | Compile time | | `firmware/last_online` | `2025-01-22T08:56:46.664+01:00` | Last time the board was online |
| firmware/last_online | 2025-01-22T08:56:46.664+01:00 | Last time this board was online | | `state` | `online` | Current state of the controller |
| firmware/ota_state | Partition state is ESP_OTA_IMG_VALID | The OTA state, relevant for rollback | | `mppt` | `{"current_ma":1200,"voltage_ma":18500}` | MPPT charging metrics |
| firmware/partition_address | 0x10000 | The OTA partition used, 0x10000 is ota_1 | | `battery` | `{"Info":{"voltage_milli_volt":12860,"average_current_milli_ampere":-16,...}}` | Battery health and charge data |
| state | online | Current State, expected are online or sleep | | `water` | `{"enough_water":true,"warn_level":false,"left_ml":1337,...}` | Water tank status |
| battery | {<br/>"voltage_milli_volt":"12860",<br/>"current_milli_ampere":"-16",<br/>"cycle_count":"12",<br/>"design_milli_ampere":"6000",<br/>"remaining_milli_ampere":"806",<br/>"state_of_charge":"15",<br/>"state_of_health":"93",<br/>"temperature":"2957"<br/>} | Dump of battery data | | `plant{1-8}` | `{"sensor_a":...,"sensor_b":...,"mode":"TargetMoisture",...}` | Detailed status for each plant slot |
| water | {<br/>"enough_water":true,<br/>"warn_level":false,<br/>"left_ml":1337,<br/>"sensor_error":false,<br/>"raw":0,"water_frozen":<br/>"tank sensor error"<br/>} | Water Status dump | | `pump{1-8}` | `{"enabled":true,"pump_ineffective":false,...}` | Metrics for the last pump activity |
| plant1 | {<br/>"a":"disabled",<br/>"a_raw":"0",<br/>"b":"disabled",<br/>"b_raw":"0",<br/>"mode":"OFF",<br/>"consecutive_pump_count":0,<br/>"dry":false,<br/>"active":false,<br/>"pump_error":false,<br/>"not_effective":false,<br/>"cooldown":false,<br/>"out_of_work_hour":false,<br/>"last_pump":"N/A",<br/>"next_pump":"N/A"<br/>} | Plant status dump | | `light` | `{"enabled":true,"active":true,...}` | Night light status |
| light | {<br/>"active":false,<br/>"out_of_work_hour":true,<br/>"battery_low":true,<br/>"is_day":false<br/>} | Light status dump | | `deepsleep` | `night 1h` | Why and how long the ESP will sleep |
| deepsleep | night 1h | Why and how long the ESP will sleep |
### Data Structures
#### Firmware State (`firmware/state`)
Contains a debug dump of the `VersionInfo` struct:
- `git_hash`: Branch and commit hash
- `build_time`: Compilation timestamp
- `current`: Current running partition
- `slot0_state`: State of OTA slot 0
- `slot1_state`: State of OTA slot 1
#### MPPT (`mppt`)
- `current_ma`: Solar charging current in mA
- `voltage_ma`: Solar panel voltage in mV
#### Battery (`battery`)
Can be `"Unknown"` or an `Info` object:
- `voltage_milli_volt`: Battery voltage
- `average_current_milli_ampere`: Current draw/charge
- `design_milli_ampere_hour`: Battery capacity
- `remaining_milli_ampere_hour`: Remaining capacity
- `state_of_charge`: Charge percentage (0-100)
- `state_of_health`: Health percentage (0-100)
- `temperature`: Temperature in degrees Celsius
#### Water (`water`)
- `enough_water`: Boolean, true if level is above empty threshold
- `warn_level`: Boolean, true if level is below warning threshold
- `left_ml`: Estimated remaining water in ml
- `percent`: Estimated fill level in percent
- `raw`: Raw sensor voltage in mV
- `sensor_error`: Details if the level sensor fails
- `water_frozen`: Boolean, true if temperature is below freezing
- `water_temp`: Water temperature in degrees Celsius
- `temp_sensor_error`: Details if the temperature sensor fails
#### Plant (`plant{1-8}`)
- `sensor_a` / `sensor_b`: Moisture sensor status
- `Disabled`
- `{"MoistureValue":{"raw_hz":5000,"moisture_percent":65}}`
- `{"SensorError":{"ShortCircuit":{"hz":...,"max":...}}}`
- `mode`: Watering mode (`Off`, `TargetMoisture`, `MinMoisture`, `TimerOnly`)
- `do_water`: Boolean, true if watering is currently required
- `dry`: Boolean, true if moisture is below target
- `cooldown`: Boolean, true if the pump is in cooldown period
- `out_of_work_hour`: Boolean, true if currently outside allowed watering hours
- `consecutive_pump_count`: Number of pump cycles without reaching target
- `pump_error`: Details if the pump is failing
- `last_pump`: Timestamp of last activity
- `next_pump`: Estimated timestamp for next allowed activity
#### Pump (`pump{1-8}`)
- `enabled`: Boolean, pump was active
- `pump_ineffective`: Boolean, no flow detected during pumping
- `median_current_ma`: Median pump current
- `max_current_ma`: Peak pump current
- `min_current_ma`: Minimum pump current
#### Light (`light`)
- `enabled`: Boolean, is enabled in config
- `active`: Boolean, led is currently on
- `out_of_work_hour`: Boolean, led should not be on at this time of day
- `battery_low`: Boolean, battery is low so led usage is restricted
- `is_day`: Boolean, the sun is up (determined by solar panel voltage)

View File

@@ -5,37 +5,57 @@ draft: false
description: "How to compile the project" description: "How to compile the project"
tags: ["clone", "compile"] tags: ["clone", "compile"]
--- ---
# Preconditons: # Preconditions:
* NPM is installed * **Rust:** `rustup` installed.
* rustup is installed * **ESP32 Toolchain:** `espup` installed.
* espup is installed * **Build Utilities:** `ldproxy` and `espflash` installed.
* **Node.js:** `npm` installed (for the web interface).
# Cloning the Repository
Clone the repository including submodules:
```bash
git clone --recursive https://git.mannheim.ccc.de/C3MA/PlantCtrl.git
cd PlantCtrl/Software/MainBoard/rust
```
# Cloning Git # Toolchain Setup
Clone the git via the tool of your choice to your local computer 1. **Install Rust:** If not already done, visit [rustup.rs](https://rustup.rs/).
``` 2. **Install ldproxy:**
git clone https://git.mannheim.ccc.de/C3MA/PlantCtrl.git ```bash
``` cargo install ldproxy
switch to the newly cloned folder ```
``` 3. **Install espup:**
cd PlantCtrl/rust ```bash
``` cargo install espup
# Install rust ```
rustup description 4. **Install ESP toolchain:**
install ldproxy ```bash
``` espup install
cargo install ldproxy ```
``` 5. **Install espflash:**
# Esp Toolchain ```bash
espup description cargo install espflash
# Webpack install ```
The buildin config website is currently build inline via npm - typescript - webpack and then directly embedded into the binary, so it is required to have webpack build working.
``` # Building the Web Interface
The configuration website is built using TypeScript and Webpack, then embedded into the Rust binary.
```bash
cd src_webpack/ cd src_webpack/
npm install npm install
```
Check the webpack build is working
```
npx webpack npx webpack
cd ..
``` ```
# Cargo Build
# Compiling the Firmware
Build the project using Cargo:
```bash
cargo build --release
```
The resulting binary will be located in `target/riscv32imac-unknown-none-elf/release/plant-ctrl2`.
# Using Build Scripts
To simplify the process, several bash scripts are provided in the `Software/MainBoard/rust` directory:
* **`image_build.sh`**: Automatically builds the web interface, compiles the Rust firmware in release mode, and creates a flashable `image.bin`.
* **`all.sh`**: Performs all steps from `image_build.sh` and additionally flashes the firmware to a connected device and starts the serial monitor.
* **`wokwi_build.sh`**: Builds a debug version of the firmware and creates a full 16MB flash image for use with the [Wokwi simulator](https://wokwi.com).

View File

@@ -2,13 +2,50 @@
title: "Software" title: "Software"
date: 2025-01-24 date: 2025-01-24
draft: false draft: false
description: "Information about the Firmware" description: "Information about the PlantCtrl Firmware"
tags: ["software"] tags: ["software", "rust", "esp32-c6", "firmware"]
--- ---
* esp-idf-hal based rust project
* can be run standalon or with wifi The PlantCtrl firmware is a modern, reliable, and efficient system designed specifically for the ESP32-C6 RISC-V microcontroller. It is written entirely in **Rust**, leveraging the power of asynchronous programming to ensure low power consumption and high responsiveness.
* MQTT
* SNTP to sync the RTC/Clock ## Core Technology Stack
* Abitrary file upload and download into SPIFS volume
* Flash Battery Controller from file in Flash * **Language:** [Rust](https://www.rust-lang.org/) (no_std)
* Flash Firmware via OTA directly from website * **Async Runtime:** [Embassy](https://embassy.dev/)
* **Hardware Abstraction:** `esp-hal` for ESP32-C6
* **Filesystem:** [LittleFS2](https://github.com/littlefs-project/littlefs) for robust persistent storage
* **Web Framework:** `edge-http` for the integrated web server
## Key Features
### Intelligent Watering
The software manages up to 8 independent watering zones.
* **Moisture-Based Control:** Automatically triggers pumps when soil moisture drops below a configurable threshold.
* **Safety Limits:** Includes pump time limits, volume limits (requires flow sensor), and mandatory cooldown periods.
* **Scheduling:** Define specific hours during which watering is allowed to occur.
* **Pump Protection:** Monitors pump current to detect dry running or blockages (using INA219).
### Modular Sensor Integration
The firmware communicates with the new **CAN bus-based sensor modules**.
* **Digital Accuracy:** Receives precise moisture data over the robust CAN bus protocol.
* **Multi-Sensor Support:** Supports A and B sensor pairs for each plant to provide better averaging in large containers.
### Networking & IoT
* **WiFi:** Supports both Station mode (connecting to your home network) and Access Point mode (for initial setup).
* **MQTT & Home Assistant:** Full integration with Home Assistant via the `mcutie` crate. Reports battery status, moisture levels, tank levels, and allows remote configuration.
* **Time Sync:** Uses **SNTP** to keep the internal Real-Time Clock (RTC) accurate.
* **OTA Updates:** Firmware can be updated wirelessly via the web interface.
### Power Management
* **Solar Optimized:** Works in tandem with the MPPT module to maximize solar charging.
* **Battery Aware:** Monitors battery State of Charge (SoC). It can automatically disable non-essential features (like the nightlight) or enter a deep-sleep "rescue mode" if the battery is critically low.
* **Low Power:** Uses ESP32-C6 power-saving features to ensure long autonomous operation.
### Integrated Web Interface
The firmware hosts a comprehensive web dashboard built with **TypeScript** and **Webpack**.
* **Real-time Monitoring:** View current moisture, battery health, and solar production.
* **Easy Configuration:** Change all system settings (WiFi, MQTT, watering rules) directly from your browser.
* **Diagnostics:** Access system logs and hardware status information.
## Emergency Rescue Mode
If the system detects a missing configuration or an invalid system time, it automatically enters **Emergency Rescue Mode**. In this mode, it opens a WiFi Access Point ("PlantCtrl Init") allowing you to perform initial setup or recover the system.

View File

@@ -1,27 +1,26 @@
--- ---
title: "Software" title: "PlantCtrl"
date: 2025-01-24 date: 2025-01-24
draft: false draft: false
description: "Information about the Firmware" description: "Solar Powered Plant Monitoring and Watering"
tags: ["software"] tags: ["iot", "plants", "solar"]
--- ---
PlantCtrl is an open-source, modular, solar-powered system for monitoring and watering your plants.
Initial creation story here ## Key Features
* **Modular Design:** Choose the components you need (MPPT, Pumps, Sensors, Lights).
* **Solar Powered:** Integrated MPPT charger for battery operation.
* **Low Power:** Optimized for long-term autonomous operation.
* **IoT Enabled:** MQTT support for integration with Home Assistant or other systems.
* **Rust Firmware:** Reliable and efficient firmware based on ESP-IDF and Rust.
## Target group:
* DIY Enthusiasts
* Open Source supporters
* Small scale use (balcony, greenhouse, indoor garden)
* Automatic Watering from a water tank
<div align="left"> [Explore Hardware]({{< relref "/hardware" >}})
## Target group: [Explore Software]({{< relref "/software" >}})
* DIY
* OpenSource
* Small scale (as in balcony/garden not agriculture)
* Automatic Watering for a limited amount of time via a water tank (I do not recommend pipe connection due to flooding risks)
* Use as is, or improve as you see fit
</div>
[Hardware]({{< relref "/hardware" >}})
[Software]({{< relref "/software" >}})
{{< article link="/hardware/" >}}