From 8c4d5267b0e859ad6cc830a9dcc64fde87c28ab5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:51:40 +0000 Subject: [PATCH] Phase 3: Add comprehensive documentation and tests Co-authored-by: Genaker <9213670+Genaker@users.noreply.github.com> --- CHANGELOG.md | 138 ++++++++++ CONTRIBUTING.md | 319 +++++++++++++++++++++++ README.md | 1 + TROUBLESHOOTING.md | 267 +++++++++++++++++++ __pycache__/serial_utils.cpython-312.pyc | Bin 0 -> 5130 bytes test/test_serial_utils.py | 216 +++++++++++++++ 6 files changed, 941 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 TROUBLESHOOTING.md create mode 100644 __pycache__/serial_utils.cpython-312.pyc create mode 100644 test/test_serial_utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d23a163 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,138 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- **Python Utility Module**: Created `serial_utils.py` with shared functions for CRC16 calculation and data parsing + - Eliminates code duplication across `ASCII_SA.py`, `SpectrumScan.py`, and `scripts/rpi-proxy-fc.py` + - Provides centralized input validation and error handling + - Includes backward compatibility fallbacks + +- **Troubleshooting Guide**: Added comprehensive `TROUBLESHOOTING.md` with solutions for common issues + - Build and compilation problems + - Upload failures + - Runtime errors + - Configuration issues + - Python script errors + - Performance optimization tips + - Environment variables reference + +- **Environment Variable Support**: Python scripts now support configuration via environment variables + - `LORA_SA_PORT`: Serial port for LoRa device + - `DRONE_PORT`: Serial port for drone/flight controller + - `SERIAL_BAUDRATE`: Communication baudrate + - `SERIAL_TIMEOUT`: Read timeout in seconds + +- **Signal Handlers**: Added graceful shutdown handling in `ASCII_SA.py` + - Proper cleanup on Ctrl+C (SIGINT) + - Prevents terminal corruption on exit + +- **C++ Destructors**: Added proper destructors to prevent memory leaks + - `StackedChart`: Cleans up dynamically allocated charts array + - `BarChart`: Cleans up `ys` and `changed` arrays + - Implements proper RAII pattern + +### Changed + +- **Exception Handling (Python)**: + - Replaced bare `except:` clauses with specific exception types + - Fixed incorrect `try-finally` logic in `scripts/rpi-proxy-fc.py` + - Added proper error logging with descriptive messages + - Improved error recovery and user feedback + +- **Input Validation (Python)**: + - Added comprehensive validation in `parse_scan_result()` functions + - Frequency range validation (100 MHz to 6 GHz) + - RSSI range validation (-200 to 0 dBm) + - Scan count bounds checking (1 to 10,000) + - Data integrity verification (count matches actual data length) + +- **Memory Management (C++)**: + - Fixed mixed allocation methods in `StackedChart.cpp` + - Changed `free(charts)` to `delete[] charts` for consistency + - Fixed BLE callback memory leak in `src/main.cpp` + - Replaced heap allocation with static instance for `MyServerCallbacks` + +- **Code Organization**: + - Extracted hardcoded constants to configuration variables + - Consolidated CRC16 implementations into single shared function + - Improved code comments and documentation + - Better separation of concerns in Python scripts + +### Fixed + +- **Python Scripts**: + - Fixed infinite loops without proper exit mechanisms + - Fixed silent exception swallowing with curses errors + - Fixed Unicode decode errors in serial communication + - Fixed malformed data handling in `ASCII_SA.py` + +- **C++ Code**: + - Fixed memory leaks in chart classes + - Fixed undefined behavior from mixed allocation methods (new/free) + - Fixed BLE server callback memory leak + - Fixed potential buffer overflows from missing input validation + +### Security + +- **Input Validation**: Added bounds checking to prevent crashes from malformed serial data +- **Error Messages**: Improved error messages without exposing sensitive system information +- **Exception Handling**: Removed unsafe bare except clauses that could hide critical errors + +### Documentation + +- Added troubleshooting guide with common solutions +- Documented environment variable configuration +- Added examples for error handling +- Improved code comments in critical sections +- Added reference to troubleshooting guide in main README + +### Performance + +- Reduced memory allocations by using static instances where possible +- Improved error handling overhead by using specific exception types +- Better resource cleanup prevents memory leaks over time + +--- + +## Notes for Developers + +### Breaking Changes +None - all changes are backward compatible. Scripts will use local fallback implementations if `serial_utils.py` is not available. + +### Migration Guide +No migration needed. Existing code continues to work. To use new features: + +1. **Environment Variables**: Set before running scripts + ```bash + export LORA_SA_PORT=/dev/ttyUSB0 + python3 ASCII_SA.py + ``` + +2. **Shared Utilities**: Import from `serial_utils` for new code + ```python + from serial_utils import crc16, parse_scan_result + ``` + +### Testing Recommendations +- Test Python scripts with invalid/corrupted serial data +- Verify memory leak fixes with long-running tests +- Check backward compatibility with existing configurations +- Validate environment variable override functionality + +--- + +## Future Improvements + +Tracked in GitHub issues: +- [ ] Add unit tests for `serial_utils.py` validation functions +- [ ] Add integration tests for serial communication +- [ ] Create automated memory leak testing for C++ code +- [ ] Add configuration file support (YAML/JSON) for complex setups +- [ ] Implement logging framework for better debugging +- [ ] Add telemetry and metrics collection diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b279eed --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,319 @@ +# Contributing to LoraSA + +Thank you for your interest in contributing to LoraSA! This document provides guidelines and best practices for contributing to the project. + +## Code of Conduct + +- Be respectful and inclusive +- Provide constructive feedback +- Focus on what is best for the community +- Show empathy towards other community members + +## Getting Started + +1. **Fork the repository** on GitHub +2. **Clone your fork** locally: + ```bash + git clone https://github.com/YOUR_USERNAME/LoraSA.git + cd LoraSA + ``` +3. **Create a branch** for your changes: + ```bash + git checkout -b feature/your-feature-name + ``` + +## Development Setup + +### For C++ Development (ESP32) +1. Install VSCode and PlatformIO extension +2. Open project in VSCode +3. Select appropriate environment from `platformio.ini` +4. Build and upload to your board + +### For Python Development +1. Install Python 3.7 or higher +2. Install dependencies: + ```bash + pip install pyserial matplotlib numpy + ``` +3. Test scripts from repository root: + ```bash + python3 ASCII_SA.py + python3 SpectrumScan.py + ``` + +## Code Quality Standards + +### Python Code + +#### Style Guidelines +- Follow PEP 8 style guide +- Use meaningful variable names +- Maximum line length: 100 characters +- Use type hints for function parameters and returns (Python 3.7+) + +#### Best Practices +1. **Error Handling**: + ```python + # ❌ Bad - bare except + try: + risky_operation() + except: + pass + + # ✅ Good - specific exceptions + try: + risky_operation() + except (ValueError, IOError) as e: + logger.error(f"Operation failed: {e}") + # Handle or re-raise + ``` + +2. **Input Validation**: + ```python + # ❌ Bad - no validation + def process_data(freq, rssi): + return freq * rssi + + # ✅ Good - validate inputs + def process_data(freq, rssi): + if not (100000 <= freq <= 6000000): + raise ValueError(f"Invalid frequency: {freq}") + if not (-200 <= rssi <= 0): + raise ValueError(f"Invalid RSSI: {rssi}") + return freq * rssi + ``` + +3. **Resource Management**: + ```python + # ❌ Bad - manual cleanup + ser = serial.Serial(port, baudrate) + data = ser.read() + ser.close() + + # ✅ Good - context manager + with serial.Serial(port, baudrate) as ser: + data = ser.read() + # Automatically closed + ``` + +4. **Constants**: + ```python + # ❌ Bad - magic numbers + if timeout > 5: + ... + + # ✅ Good - named constants + DEFAULT_TIMEOUT = 5 + if timeout > DEFAULT_TIMEOUT: + ... + ``` + +### C++ Code + +#### Style Guidelines +- Follow existing code style in the project +- Use 4 spaces for indentation (no tabs) +- Maximum line length: 100 characters +- Use `clang-format` with provided `.clang-format` configuration + +#### Best Practices + +1. **Memory Management**: + ```cpp + // ❌ Bad - memory leak + MyClass* obj = new MyClass(); + // Never deleted + + // ✅ Good - RAII or manual cleanup + std::unique_ptr obj(new MyClass()); + // Or with destructor: + ~MyClass() { + delete[] dynamicArray; + } + ``` + +2. **Array Allocation**: + ```cpp + // ❌ Bad - mixed allocation + char* arr = new char[100]; + free(arr); // Wrong! + + // ✅ Good - consistent allocation + char* arr = new char[100]; + delete[] arr; + ``` + +3. **Null Checks**: + ```cpp + // ❌ Bad - no null check + void process(Data* data) { + data->value = 10; // Crash if data is null + } + + // ✅ Good - validate pointer + void process(Data* data) { + if (data == nullptr) { + LOG("Error: null pointer"); + return; + } + data->value = 10; + } + ``` + +4. **Destructors**: + ```cpp + // ❌ Bad - missing destructor + class MyChart { + float* data; + public: + MyChart() { data = new float[100]; } + // No destructor - memory leak! + }; + + // ✅ Good - proper cleanup + class MyChart { + float* data; + public: + MyChart() { data = new float[100]; } + ~MyChart() { delete[] data; } + }; + ``` + +## Testing + +### Python Tests +Run existing tests: +```bash +cd test +python -m unittest test_rssi.py +``` + +Add tests for new features: +```python +import unittest +from serial_utils import parse_scan_result + +class TestParseFunction(unittest.TestCase): + def test_valid_input(self): + line = "SCAN_RESULT 2 [(850000, -100), (860000, -90)]" + count, data = parse_scan_result(line) + self.assertEqual(count, 2) + self.assertEqual(len(data), 2) + + def test_invalid_frequency(self): + line = "SCAN_RESULT 1 [(1, -100)]" # Freq too low + with self.assertRaises(ValueError): + parse_scan_result(line) +``` + +### C++ Tests +Tests are in `test/` directory. Run with PlatformIO: +```bash +pio test +``` + +## Pull Request Process + +1. **Update Documentation**: + - Update README.md if adding features + - Add entry to CHANGELOG.md + - Update TROUBLESHOOTING.md if relevant + +2. **Test Your Changes**: + - Build and test on actual hardware if possible + - Run existing test suite + - Test edge cases and error conditions + +3. **Commit Messages**: + ``` + Short summary (50 chars or less) + + More detailed explanation if needed. Wrap at 72 characters. + + - Bullet points are okay + - Explain what and why, not how + + Fixes #123 + ``` + +4. **Create Pull Request**: + - Describe changes clearly + - Reference related issues + - Include testing steps + - Add screenshots for UI changes + +5. **Code Review**: + - Address reviewer feedback + - Keep discussions focused and professional + - Be open to suggestions + +## Common Pitfalls to Avoid + +### Python +- ❌ Bare `except:` clauses +- ❌ Magic numbers without constants +- ❌ Missing input validation +- ❌ Ignoring exceptions with `pass` +- ❌ Hardcoded paths/ports + +### C++ +- ❌ Memory leaks (new without delete) +- ❌ Mixed allocation (new/free, malloc/delete) +- ❌ Missing destructors for classes with dynamic allocation +- ❌ Null pointer dereferences +- ❌ Buffer overflows from unchecked indices + +## Security Guidelines + +1. **Never commit**: + - Credentials or API keys + - Personal information + - Binary files (unless necessary) + +2. **Input Validation**: + - Always validate external inputs + - Check array bounds + - Validate ranges for frequencies, RSSI, etc. + +3. **Error Messages**: + - Don't expose internal paths + - Don't reveal system information + - Provide helpful but safe messages + +## Documentation + +- Comment complex algorithms +- Use docstrings for Python functions +- Update README for new features +- Add examples for new functionality + +## Performance Considerations + +- Avoid allocations in tight loops +- Use appropriate data structures +- Profile before optimizing +- Consider memory constraints of ESP32 + +## Hardware Testing + +If possible, test on: +- Heltec WiFi LoRa 32 V3 +- LilyGo T3S3 boards +- Different frequency ranges +- Various operating conditions + +## Getting Help + +- Check [TROUBLESHOOTING.md](TROUBLESHOOTING.md) +- Search existing issues +- Ask questions in issue comments +- Be specific about your setup + +## License + +By contributing, you agree that your contributions will be licensed under the same license as the project (see LICENSE.md). + +--- + +Thank you for contributing to LoraSA! 🎉 diff --git a/README.md b/README.md index 65993a0..dbd5206 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - [Send Scan Data via Lora](#send-scan-data-via-lora) - [Seek on Jam / FPV OSD using Flight controller](#seek-on-jam-fpv-osd-using-flight-controller) - [Platformio targets](#platformio-targets) +- [Troubleshooting](TROUBLESHOOTING.md) 📖 **NEW** ## Supported boards: diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..0f68ee0 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,267 @@ +# Troubleshooting Guide + +## Common Issues and Solutions + +### Build and Compilation Issues + +#### Error: "Unknown board ID" +**Symptom**: Build fails with `Error: Unknown board ID 'heltec_wifi_lora_32_V3'` + +**Solution**: Update your ESP32 Expressif catalog: +```bash +pio pkg update -g -p espressif32 +``` + +#### Build Timeout or Slow First Build +**Symptom**: First compilation takes very long (>5 minutes) + +**Solution**: This is normal - PlatformIO needs to download and compile all libraries on first run. Subsequent builds will be much faster (~30-60 seconds). + +--- + +### Upload Issues + +#### Cannot Upload to Board +**Symptom**: Upload fails with "Failed to connect to ESP32" + +**Solution**: +1. Press and hold **BOOT** button +2. Press **RESET** button once +3. Release **BOOT** button +4. Try upload again within 5 seconds + +#### USB Driver Not Found (Windows) +**Symptom**: Device not recognized or COM port not showing + +**Solution**: Install CP2101 USB drivers: +- Download from: https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers +- For Windows: Use the executable installer +- For macOS: Use legacy driver if standard driver doesn't work + +--- + +### Runtime Errors + +#### No Display Output +**Symptom**: Device powers on but screen stays blank + +**Solution**: +1. Check if correct board is selected in `platformio.ini` +2. Verify `src_dir` matches your board type: + - `src` for OLED displays (Heltec V3, LilyGo) + - `tft_src` for TFT displays (Vision Master T190) + - `eink_src` for e-ink displays (Vision Master E290) + +#### Scan Results Show No Signals +**Symptom**: Spectrum analyzer runs but shows no activity + +**Solution**: +1. Check antenna is properly connected +2. Verify frequency range matches your region's LoRa bands +3. Adjust threshold in configuration (try lower value like -120 dBm) +4. Test with known active frequency (e.g., WiFi 2.4 GHz) + +#### Python Scripts Fail to Connect +**Symptom**: `ASCII_SA.py` or `SpectrumScan.py` cannot open serial port + +**Solution**: +1. Close Arduino IDE or PlatformIO serial monitor (only one program can use serial port) +2. Check permissions on Linux: + ```bash + sudo usermod -a -G dialout $USER + # Then logout and login again + ``` +3. Verify correct port: + - Linux: Usually `/dev/ttyUSB0` or `/dev/ttyACM0` + - Windows: Check Device Manager for COM port number + - macOS: Usually `/dev/cu.usbserial-*` + +4. Set port via environment variable: + ```bash + export LORA_SA_PORT=/dev/ttyUSB0 + python3 ASCII_SA.py + ``` + +--- + +### Configuration Issues + +#### OSD Feature Not Working +**Symptom**: Compiled with OSD_ENABLED but no video output + +**Solution**: +1. Verify DFRobot OSD wiring (see main README) +2. Check SPI pins match your board configuration +3. Test camera independently before connecting to OSD + +#### Custom Frequency Range Not Working +**Symptom**: Device scans wrong frequencies despite configuration + +**Solution**: +1. Ensure `SCAN_DIAPAZONES` is properly formatted: + ```c + int SCAN_DIAPAZONES[] = {850890, 920950}; // Two ranges: 850-890 and 920-950 + ``` +2. If using single range, set `RANGE_PER_PAGE = FREQ_END - FREQ_BEGIN` +3. Frequency values must be within radio chip capabilities: + - SX1262: 150-960 MHz + - SX1280: 2400-2500 MHz + - LR1121: 150-960 MHz, 2400-2500 MHz + +--- + +### Python Script Errors + +#### ModuleNotFoundError: No module named 'serial_utils' +**Symptom**: Python scripts fail with import error + +**Solution**: Ensure you're running scripts from the repository root: +```bash +cd /path/to/LoraSA +python3 ASCII_SA.py +``` + +Or install in development mode: +```bash +export PYTHONPATH="${PYTHONPATH}:/path/to/LoraSA" +``` + +#### ValueError: Invalid SCAN_RESULT format +**Symptom**: Script crashes when parsing serial data + +**Solution**: +1. This indicates corrupted serial data +2. Check USB cable quality (use high-quality, short cables) +3. Reduce baudrate in configuration if errors persist: + ```bash + export SERIAL_BAUDRATE=57600 + ``` +4. Update to latest firmware version + +--- + +### Performance Issues + +#### Slow Scan Rate +**Symptom**: Less than 1 scan per second + +**Solution**: +1. Reduce scan range: smaller `FREQ_END - FREQ_BEGIN` +2. Increase bandwidth setting (faster but less sensitive) +3. Disable waterfall if enabled +4. Disable WiFi/BT scanning if not needed + +#### High Battery Drain +**Symptom**: Battery depletes quickly + +**Solution**: +1. Reduce screen brightness +2. Use power-saving mode (press P button on startup) +3. Disable Bluetooth/WiFi if not in use +4. Use sleep mode between scans + +--- + +### Network and Communication + +#### BLE/WiFi Scanning Not Working +**Symptom**: `WIFI_SCANNING_ENABLED` or `BT_SCANNING_ENABLED` doesn't show results + +**Solution**: +1. Ensure OSD is enabled (required for WiFi/BT scanning) +2. Verify defines are set: + ```c + #define OSD_ENABLED true + #define WIFI_SCANNING_ENABLED true + #define BT_SCANNING_ENABLED true + ``` +3. Check antenna placement (WiFi/BT uses different antenna than LoRa on some boards) + +#### LoRa Communication Between Devices Fails +**Symptom**: Data not transmitting via LoRa + +**Solution**: +1. Verify both devices use same frequency configuration +2. Check `is_host` setting (one device should be host, one client) +3. Ensure LoRa TX/RX are properly configured in `lib/config/config.h` +4. Test with shorter distance first (<100m) + +--- + +## Getting Additional Help + +If none of these solutions work: + +1. **Check Serial Monitor Output**: Connect via USB and monitor serial output for error messages + ```bash + pio device monitor + ``` + +2. **Enable Debug Logging**: Add to build flags: + ```ini + build_flags = -DCORE_DEBUG_LEVEL=5 + ``` + +3. **Report Issue**: Include: + - Board model and version + - PlatformIO environment used + - Complete error message + - Serial output log + - Steps to reproduce + +4. **Community Resources**: + - GitHub Issues: https://github.com/Genaker/LoraSA/issues + - Check existing issues for similar problems + - Provide hardware details when asking for help + +--- + +## Environment Variables Reference + +The Python scripts now support environment variables for easier configuration: + +```bash +# Serial port configuration +export LORA_SA_PORT=/dev/ttyUSB0 # LoRa device port +export DRONE_PORT=/dev/ttyACM0 # Drone/FC port (rpi-proxy-fc.py) +export SERIAL_BAUDRATE=115200 # Communication speed +export SERIAL_TIMEOUT=5 # Read timeout in seconds + +# Run script with custom config +python3 ASCII_SA.py + +# Or inline +LORA_SA_PORT=/dev/ttyUSB1 python3 SpectrumScan.py +``` + +--- + +## Advanced Debugging + +### Memory Issues + +If experiencing crashes or freezes: + +1. **Check Memory Usage**: + ```c + Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap()); + ``` + +2. **Reduce Buffer Sizes**: Edit configuration to use less memory +3. **Disable Features**: Turn off waterfall, WiFi scanning to save memory + +### Timing Issues + +If scans appear delayed or unresponsive: + +1. **Disable Watchdog**: May need to increase timeout +2. **Profile Code**: Add timing measurements +3. **Reduce Display Updates**: Update less frequently + +### Radio Issues + +If radio performance is poor: + +1. **Check Antenna**: Measure VSWR if possible +2. **Verify Frequency**: Use SDR to confirm actual transmission frequency +3. **Test with Known Good Hardware**: Isolate hardware vs software issues diff --git a/__pycache__/serial_utils.cpython-312.pyc b/__pycache__/serial_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c41c14485427150d11aec4acb12efafcf2bdb9f GIT binary patch literal 5130 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 literal 0 HcmV?d00001 diff --git a/test/test_serial_utils.py b/test/test_serial_utils.py new file mode 100644 index 0000000..a71ab89 --- /dev/null +++ b/test/test_serial_utils.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +""" +Unit tests for serial_utils module. +Run with: python3 -m unittest test_serial_utils.py +""" + +import unittest +import sys +import os + +# Add parent directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from serial_utils import ( + crc16, + parse_scan_result, + parse_scan_result_regex, + validate_serial_config +) + + +class TestCRC16(unittest.TestCase): + """Tests for CRC16 calculation.""" + + def test_crc16_basic(self): + """Test basic CRC16 calculation.""" + result = crc16("test", 0) + self.assertIsInstance(result, int) + self.assertGreaterEqual(result, 0) + self.assertLessEqual(result, 0xFFFF) + + def test_crc16_empty(self): + """Test CRC16 with empty string.""" + result = crc16("", 0) + # Empty string with initial value 0 produces 0 + self.assertEqual(result, 0) + + def test_crc16_consistency(self): + """Test that same input gives same output.""" + input_str = "SCAN_RESULT 123" + result1 = crc16(input_str, 0) + result2 = crc16(input_str, 0) + self.assertEqual(result1, result2) + + +class TestParseScanResult(unittest.TestCase): + """Tests for parse_scan_result function.""" + + def test_valid_input(self): + """Test parsing valid SCAN_RESULT data.""" + line = "SCAN_RESULT 2 [(850000, -100), (860000, -90)]" + count, data = parse_scan_result(line) + + self.assertEqual(count, 2) + self.assertEqual(len(data), 2) + self.assertEqual(data[0], [850000, -100]) + self.assertEqual(data[1], [860000, -90]) + + def test_valid_with_garbage(self): + """Test parsing with garbage before SCAN_RESULT.""" + line = "garbage data SCAN_RESULT 1 [(900000, -80)]" + count, data = parse_scan_result(line) + + self.assertEqual(count, 1) + self.assertEqual(len(data), 1) + + def test_missing_scan_result(self): + """Test error handling for missing SCAN_RESULT.""" + with self.assertRaises(ValueError) as cm: + parse_scan_result("no scan result here") + self.assertIn("SCAN_RESULT", str(cm.exception)) + + def test_invalid_format(self): + """Test error handling for invalid format.""" + with self.assertRaises(ValueError): + parse_scan_result("SCAN_RESULT") # Missing count and data + + def test_count_mismatch(self): + """Test error handling when count doesn't match data length.""" + line = "SCAN_RESULT 5 [(850000, -100)]" # Says 5 but only 1 item + with self.assertRaises(ValueError) as cm: + parse_scan_result(line) + self.assertIn("does not match count", str(cm.exception)) + + def test_invalid_frequency_low(self): + """Test error handling for frequency too low.""" + line = "SCAN_RESULT 1 [(1000, -100)]" # 1 kHz - too low + with self.assertRaises(ValueError) as cm: + parse_scan_result(line) + self.assertIn("Invalid frequency", str(cm.exception)) + + def test_invalid_frequency_high(self): + """Test error handling for frequency too high.""" + line = "SCAN_RESULT 1 [(9000000, -100)]" # 9 GHz - too high + with self.assertRaises(ValueError) as cm: + parse_scan_result(line) + self.assertIn("Invalid frequency", str(cm.exception)) + + def test_invalid_rssi_positive(self): + """Test error handling for positive RSSI.""" + line = "SCAN_RESULT 1 [(850000, 50)]" # Positive RSSI - invalid + with self.assertRaises(ValueError) as cm: + parse_scan_result(line) + self.assertIn("Invalid RSSI", str(cm.exception)) + + def test_invalid_rssi_low(self): + """Test error handling for RSSI too low.""" + line = "SCAN_RESULT 1 [(850000, -300)]" # -300 dBm - too low + with self.assertRaises(ValueError) as cm: + parse_scan_result(line) + self.assertIn("Invalid RSSI", str(cm.exception)) + + def test_count_too_large(self): + """Test error handling for unreasonably large count.""" + # Create a line with a very large count value + line = "SCAN_RESULT 99999 []" + with self.assertRaises(ValueError) as cm: + parse_scan_result(line) + # Should fail either on count validation or count mismatch + + +class TestParseScanResultRegex(unittest.TestCase): + """Tests for parse_scan_result_regex function.""" + + def test_valid_input(self): + """Test parsing valid data with regex method.""" + line = "SCAN_RESULT 2 [ (850000, -100), (860000, -90) ]" + data = parse_scan_result_regex(line) + + self.assertEqual(len(data), 2) + self.assertEqual(data[0], (850000, -100)) + self.assertEqual(data[1], (860000, -90)) + + def test_empty_result(self): + """Test error handling for empty result.""" + line = "SCAN_RESULT 0 []" + with self.assertRaises(ValueError) as cm: + parse_scan_result_regex(line) + self.assertIn("No valid frequency/RSSI pairs", str(cm.exception)) + + def test_missing_scan_result(self): + """Test error handling when SCAN_RESULT is missing.""" + with self.assertRaises(ValueError): + parse_scan_result_regex("just some data") + + +class TestValidateSerialConfig(unittest.TestCase): + """Tests for validate_serial_config function.""" + + def test_valid_config(self): + """Test validation with valid configuration.""" + # Should not raise exception + validate_serial_config("/dev/ttyUSB0", 115200, 5) + + def test_invalid_port_empty(self): + """Test error handling for empty port.""" + with self.assertRaises(ValueError) as cm: + validate_serial_config("", 115200, 5) + self.assertIn("Port", str(cm.exception)) + + def test_invalid_port_none(self): + """Test error handling for None port.""" + with self.assertRaises(ValueError): + validate_serial_config(None, 115200, 5) + + def test_invalid_baudrate(self): + """Test error handling for invalid baudrate.""" + with self.assertRaises(ValueError) as cm: + validate_serial_config("/dev/ttyUSB0", 12345, 5) + self.assertIn("Baudrate", str(cm.exception)) + + def test_invalid_timeout_negative(self): + """Test error handling for negative timeout.""" + with self.assertRaises(ValueError) as cm: + validate_serial_config("/dev/ttyUSB0", 115200, -1) + self.assertIn("Timeout", str(cm.exception)) + + def test_invalid_timeout_too_high(self): + """Test error handling for timeout too high.""" + with self.assertRaises(ValueError) as cm: + validate_serial_config("/dev/ttyUSB0", 115200, 500) + self.assertIn("Timeout", str(cm.exception)) + + def test_valid_baudrates(self): + """Test that all common baudrates are accepted.""" + valid_baudrates = [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600] + for baudrate in valid_baudrates: + validate_serial_config("/dev/ttyUSB0", baudrate, 5) + + +class TestIntegration(unittest.TestCase): + """Integration tests combining multiple functions.""" + + def test_full_parsing_pipeline(self): + """Test complete parsing pipeline.""" + # Simulate real data from device + raw_data = "other data SCAN_RESULT 3 [(850000, -110), (851000, -105), (852000, -100)]" + + # Parse the data + count, data = parse_scan_result(raw_data) + + # Verify results + self.assertEqual(count, 3) + self.assertEqual(len(data), 3) + + # Check individual values + for freq, rssi in data: + self.assertGreaterEqual(freq, 100000) + self.assertLessEqual(freq, 6000000) + self.assertGreaterEqual(rssi, -200) + self.assertLessEqual(rssi, 0) + + +if __name__ == '__main__': + # Run tests with verbose output + unittest.main(verbosity=2)