|
| 1 | +/* |
| 2 | + * ocpp_logic.c - Pure C OCPP decision logic |
| 3 | + * |
| 4 | + * Extracted from esp32.cpp OCPP lambdas. All functions are pure: they take |
| 5 | + * inputs and return decisions without side effects, making them testable |
| 6 | + * natively with gcc on any host. |
| 7 | + */ |
| 8 | + |
| 9 | +#include "ocpp_logic.h" |
| 10 | +#include "evse_ctx.h" |
| 11 | +#include <string.h> |
| 12 | +#include <stdio.h> |
| 13 | + |
| 14 | +/* ---- Auth path selection ---- */ |
| 15 | + |
| 16 | +ocpp_auth_path_t ocpp_select_auth_path(uint8_t rfid_reader) { |
| 17 | + /* RFIDReader=0 (disabled) or RFIDReader=6 (Rmt/OCPP): OCPP controls Access_bit */ |
| 18 | + if (rfid_reader == 0 || rfid_reader == 6) { |
| 19 | + return OCPP_AUTH_PATH_OCPP_CONTROLLED; |
| 20 | + } |
| 21 | + /* RFIDReader=1..5: built-in RFID store controls charging, OCPP just reports */ |
| 22 | + return OCPP_AUTH_PATH_BUILTIN_RFID; |
| 23 | +} |
| 24 | + |
| 25 | +/* ---- Connector state ---- */ |
| 26 | + |
| 27 | +bool ocpp_is_connector_plugged(uint8_t cp_voltage) { |
| 28 | + /* Matches esp32.cpp: PILOT_3V..PILOT_9V means connector is plugged */ |
| 29 | + return cp_voltage >= PILOT_3V && cp_voltage <= PILOT_9V; |
| 30 | +} |
| 31 | + |
| 32 | +bool ocpp_is_ev_ready(uint8_t cp_voltage) { |
| 33 | + /* PILOT_3V..PILOT_6V = State C (EV requesting charge) */ |
| 34 | + return cp_voltage >= PILOT_3V && cp_voltage <= PILOT_6V; |
| 35 | +} |
| 36 | + |
| 37 | +/* ---- Access decision ---- */ |
| 38 | + |
| 39 | +bool ocpp_should_set_access(bool permits_charge, bool prev_permits_charge) { |
| 40 | + /* |
| 41 | + * Set Access_bit only on rising edge: OCPP just started permitting charge. |
| 42 | + * From esp32.cpp: if (!OcppTrackPermitsCharge && ocppPermitsCharge()) |
| 43 | + * This ensures we set Access_bit only once per OCPP transaction, so other |
| 44 | + * modules can override it without OCPP re-setting it. |
| 45 | + */ |
| 46 | + return !prev_permits_charge && permits_charge; |
| 47 | +} |
| 48 | + |
| 49 | +bool ocpp_should_clear_access(bool permits_charge, uint8_t access_status) { |
| 50 | + /* |
| 51 | + * Clear Access_bit when OCPP revokes permission and Access is currently ON. |
| 52 | + * From esp32.cpp: if (AccessStatus == ON && !ocppPermitsCharge()) |
| 53 | + */ |
| 54 | + return access_status == 1 && !permits_charge; /* 1 = ON */ |
| 55 | +} |
| 56 | + |
| 57 | +/* ---- RFID hex formatting ---- */ |
| 58 | + |
| 59 | +void ocpp_format_rfid_hex(const uint8_t *rfid, size_t rfid_len, |
| 60 | + char *out, size_t out_size) { |
| 61 | + if (!rfid || !out || out_size == 0 || rfid_len == 0) { |
| 62 | + if (out && out_size > 0) out[0] = '\0'; |
| 63 | + return; |
| 64 | + } |
| 65 | + |
| 66 | + size_t start = 0; |
| 67 | + size_t count = rfid_len; |
| 68 | + |
| 69 | + /* |
| 70 | + * Old reader format: rfid[0]==0x01 means the 6-byte UID starts at rfid[1]. |
| 71 | + * New reader format: rfid[0]!=0x01 means the 7-byte UID starts at rfid[0]. |
| 72 | + * From esp32.cpp ocppLoop(): |
| 73 | + * if (RFID[0] == 0x01) snprintf(buf, ..., RFID[1]..RFID[6]) |
| 74 | + * else snprintf(buf, ..., RFID[0]..RFID[6]) |
| 75 | + */ |
| 76 | + if (rfid_len >= 7 && rfid[0] == 0x01) { |
| 77 | + start = 1; |
| 78 | + count = 6; |
| 79 | + } |
| 80 | + |
| 81 | + out[0] = '\0'; |
| 82 | + for (size_t i = start; i < start + count && i < rfid_len; i++) { |
| 83 | + size_t pos = (i - start) * 2; |
| 84 | + if (pos + 2 >= out_size) break; |
| 85 | + snprintf(out + pos, 3, "%02X", rfid[i]); |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +/* ---- Load balancing exclusivity ---- */ |
| 90 | + |
| 91 | +ocpp_lb_status_t ocpp_check_lb_exclusivity(uint8_t load_bl, bool ocpp_mode, |
| 92 | + bool was_standalone) { |
| 93 | + if (!ocpp_mode) { |
| 94 | + return OCPP_LB_OK; |
| 95 | + } |
| 96 | + |
| 97 | + if (load_bl != 0) { |
| 98 | + /* LoadBl is non-zero while OCPP is active — Smart Charging is ineffective. |
| 99 | + * The state machine guards with !ctx->LoadBl, so OCPP limits are silently |
| 100 | + * ignored even though the backend thinks they're enforced. */ |
| 101 | + return OCPP_LB_CONFLICT; |
| 102 | + } |
| 103 | + |
| 104 | + if (!was_standalone) { |
| 105 | + /* LoadBl was non-zero when OCPP was initialized, now it's 0. |
| 106 | + * The Smart Charging callback was never registered — need OCPP reinit. */ |
| 107 | + return OCPP_LB_NEEDS_REINIT; |
| 108 | + } |
| 109 | + |
| 110 | + return OCPP_LB_OK; |
| 111 | +} |
| 112 | + |
| 113 | +/* ---- Settings validation ---- */ |
| 114 | + |
| 115 | +ocpp_validate_result_t ocpp_validate_backend_url(const char *url) { |
| 116 | + if (!url || url[0] == '\0') { |
| 117 | + return OCPP_VALIDATE_EMPTY; |
| 118 | + } |
| 119 | + |
| 120 | + /* Must start with ws:// or wss:// */ |
| 121 | + bool has_ws = (strncmp(url, "ws://", 5) == 0); |
| 122 | + bool has_wss = (strncmp(url, "wss://", 6) == 0); |
| 123 | + |
| 124 | + if (!has_ws && !has_wss) { |
| 125 | + return OCPP_VALIDATE_BAD_SCHEME; |
| 126 | + } |
| 127 | + |
| 128 | + /* Must have content after the scheme */ |
| 129 | + size_t scheme_len = has_wss ? 6 : 5; |
| 130 | + if (url[scheme_len] == '\0') { |
| 131 | + return OCPP_VALIDATE_BAD_SCHEME; |
| 132 | + } |
| 133 | + |
| 134 | + return OCPP_VALIDATE_OK; |
| 135 | +} |
| 136 | + |
| 137 | +ocpp_validate_result_t ocpp_validate_chargebox_id(const char *cb_id) { |
| 138 | + if (!cb_id || cb_id[0] == '\0') { |
| 139 | + return OCPP_VALIDATE_EMPTY; |
| 140 | + } |
| 141 | + |
| 142 | + size_t len = strlen(cb_id); |
| 143 | + |
| 144 | + /* OCPP 1.6 CiString20: max 20 characters */ |
| 145 | + if (len > 20) { |
| 146 | + return OCPP_VALIDATE_TOO_LONG; |
| 147 | + } |
| 148 | + |
| 149 | + /* Only printable ASCII allowed, no special/control chars */ |
| 150 | + for (size_t i = 0; i < len; i++) { |
| 151 | + char c = cb_id[i]; |
| 152 | + if (c < 0x20 || c > 0x7E) { |
| 153 | + return OCPP_VALIDATE_BAD_CHARS; |
| 154 | + } |
| 155 | + /* Reject chars that could cause issues in OCPP identifiers */ |
| 156 | + if (c == '<' || c == '>' || c == '&' || c == '"' || c == '\'') { |
| 157 | + return OCPP_VALIDATE_BAD_CHARS; |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + return OCPP_VALIDATE_OK; |
| 162 | +} |
| 163 | + |
| 164 | +ocpp_validate_result_t ocpp_validate_auth_key(const char *auth_key) { |
| 165 | + if (!auth_key || auth_key[0] == '\0') { |
| 166 | + /* Empty auth key is acceptable (no auth) */ |
| 167 | + return OCPP_VALIDATE_OK; |
| 168 | + } |
| 169 | + |
| 170 | + size_t len = strlen(auth_key); |
| 171 | + |
| 172 | + /* OCPP 1.6: max 40 characters for AuthorizationKey */ |
| 173 | + if (len > 40) { |
| 174 | + return OCPP_VALIDATE_TOO_LONG; |
| 175 | + } |
| 176 | + |
| 177 | + return OCPP_VALIDATE_OK; |
| 178 | +} |
0 commit comments