#include "lora_transport.h" #include "hil_trace.h" #include #include #include static RxRejectReason g_last_rx_reject_reason = RxRejectReason::None; static uint32_t g_last_rx_reject_log_ms = 0; static void note_reject(RxRejectReason reason) { g_last_rx_reject_reason = reason; if (SERIAL_DEBUG_MODE) { uint32_t now_ms = millis(); if (now_ms - g_last_rx_reject_log_ms >= 1000) { g_last_rx_reject_log_ms = now_ms; Serial.printf("lora_rx: reject reason=%s\n", rx_reject_reason_text(reason)); hil_trace_eventf("lora_reject", "\"reason\":\"%s\"", rx_reject_reason_text(reason)); } } } RxRejectReason lora_get_last_rx_reject_reason() { RxRejectReason reason = g_last_rx_reject_reason; g_last_rx_reject_reason = RxRejectReason::None; return reason; } static uint16_t crc16_ccitt(const uint8_t *data, size_t len) { uint16_t crc = 0xFFFF; for (size_t i = 0; i < len; ++i) { crc ^= static_cast(data[i]) << 8; for (uint8_t b = 0; b < 8; ++b) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } } return crc; } void lora_init() { SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS); LoRa.setPins(PIN_LORA_NSS, PIN_LORA_RST, PIN_LORA_DIO0); LoRa.begin(LORA_FREQUENCY); LoRa.setSpreadingFactor(LORA_SPREADING_FACTOR); LoRa.setSignalBandwidth(LORA_BANDWIDTH); LoRa.setCodingRate4(LORA_CODING_RATE); LoRa.enableCrc(); LoRa.setSyncWord(LORA_SYNC_WORD); } bool lora_send(const LoraPacket &pkt) { if (LORA_SEND_BYPASS) { return true; } uint32_t t0 = 0; uint32_t t1 = 0; uint32_t t2 = 0; uint32_t t3 = 0; uint32_t t4 = 0; if (SERIAL_DEBUG_MODE) { t0 = millis(); } LoRa.idle(); if (SERIAL_DEBUG_MODE) { t1 = millis(); } uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2]; size_t idx = 0; buffer[idx++] = pkt.protocol_version; buffer[idx++] = static_cast(pkt.role); buffer[idx++] = static_cast(pkt.device_id_short >> 8); buffer[idx++] = static_cast(pkt.device_id_short & 0xFF); buffer[idx++] = static_cast(pkt.payload_type); if (pkt.payload_len > LORA_MAX_PAYLOAD) { return false; } memcpy(&buffer[idx], pkt.payload, pkt.payload_len); idx += pkt.payload_len; uint16_t crc = crc16_ccitt(buffer, idx); buffer[idx++] = static_cast(crc >> 8); buffer[idx++] = static_cast(crc & 0xFF); if (pkt.payload_type == PayloadType::MeterBatch && hil_fault_take(HilFault::CorruptChunk) && idx > 7) { // Flip a payload byte after calculating the frame CRC so the receiver's // production CRC path rejects this one HIL frame. buffer[5 + (pkt.payload_len > 6 ? 6 : 0)] ^= 0x01; hil_trace_event("fault_injected", "\"fault\":\"corrupt_chunk\""); } LoRa.beginPacket(); if (SERIAL_DEBUG_MODE) { t2 = millis(); } LoRa.write(buffer, idx); if (SERIAL_DEBUG_MODE) { t3 = millis(); } int result = LoRa.endPacket(false); if (SERIAL_DEBUG_MODE) { t4 = millis(); Serial.printf("lora_tx: idle=%lums begin=%lums write=%lums end=%lums total=%lums len=%u\n", static_cast(t1 - t0), static_cast(t2 - t1), static_cast(t3 - t2), static_cast(t4 - t3), static_cast(t4 - t0), static_cast(idx)); } return result == 1; } bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) { uint32_t start = millis(); while (true) { int packet_size = LoRa.parsePacket(); if (packet_size > 0) { if (packet_size < 7) { while (LoRa.available()) { LoRa.read(); } note_reject(RxRejectReason::LengthMismatch); return false; } uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2]; size_t len = 0; while (LoRa.available() && len < sizeof(buffer)) { buffer[len++] = LoRa.read(); } if (len < 7) { note_reject(RxRejectReason::LengthMismatch); return false; } uint16_t crc_calc = crc16_ccitt(buffer, len - 2); uint16_t crc_rx = static_cast(buffer[len - 2] << 8) | buffer[len - 1]; if (crc_calc != crc_rx) { note_reject(RxRejectReason::CrcFail); return false; } if (buffer[0] != PROTOCOL_VERSION) { note_reject(RxRejectReason::BadProtocol); return false; } pkt.protocol_version = buffer[0]; pkt.role = static_cast(buffer[1]); pkt.device_id_short = static_cast(buffer[2] << 8) | buffer[3]; pkt.payload_type = static_cast(buffer[4]); pkt.payload_len = len - 7; if (pkt.payload_len > LORA_MAX_PAYLOAD) { note_reject(RxRejectReason::LengthMismatch); return false; } memcpy(pkt.payload, &buffer[5], pkt.payload_len); pkt.rssi_dbm = static_cast(LoRa.packetRssi()); pkt.snr_db = LoRa.packetSnr(); return true; } if (timeout_ms == 0 || millis() - start >= timeout_ms) { return false; } delay(5); } } void lora_idle() { LoRa.idle(); } void lora_sleep() { LoRa.sleep(); } void lora_receive_continuous() { LoRa.receive(); } bool lora_receive_window(LoraPacket &pkt, uint32_t timeout_ms) { if (timeout_ms == 0) { return false; } LoRa.receive(); bool got = lora_receive(pkt, timeout_ms); LoRa.sleep(); return got; } uint32_t lora_airtime_ms(size_t packet_len) { if (packet_len == 0) { return 0; } const double bw = static_cast(LORA_BANDWIDTH); const double sf = static_cast(LORA_SPREADING_FACTOR); const double cr = static_cast(LORA_CODING_RATE - 4); // coding rate denominator: 4/(4+cr) const double tsym = (1 << LORA_SPREADING_FACTOR) / bw; const double t_preamble = (static_cast(LORA_PREAMBLE_LEN) + 4.25) * tsym; const bool low_data_rate_opt = (LORA_SPREADING_FACTOR >= 11) && (LORA_BANDWIDTH <= 125000); const double de = low_data_rate_opt ? 1.0 : 0.0; const double ih = 0.0; const double crc = 1.0; const double payload_symb_nb = 8.0 + max( ceil((8.0 * packet_len - 4.0 * sf + 28.0 + 16.0 * crc - 20.0 * ih) / (4.0 * (sf - 2.0 * de))) * (cr + 4.0), 0.0); const double t_payload = payload_symb_nb * tsym; const double t_packet = t_preamble + t_payload; return static_cast(ceil(t_packet * 1000.0)); }