dredger-iot 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +21 -1
 - data/README.md +167 -1
 - data/bin/dredger +48 -0
 - data/lib/dredger/iot/bus/gpio_label_adapter.rb +14 -4
 - data/lib/dredger/iot/pins/raspberry_pi.rb +54 -0
 - data/lib/dredger/iot/pins.rb +1 -0
 - data/lib/dredger/iot/sensors/adxl345.rb +28 -0
 - data/lib/dredger/iot/sensors/adxl345_provider.rb +103 -0
 - data/lib/dredger/iot/sensors/neo6m.rb +35 -0
 - data/lib/dredger/iot/sensors/neo6m_provider.rb +140 -0
 - data/lib/dredger/iot/sensors/scd30.rb +28 -0
 - data/lib/dredger/iot/sensors/scd30_provider.rb +142 -0
 - data/lib/dredger/iot/sensors/yf_s201.rb +32 -0
 - data/lib/dredger/iot/sensors/yf_s201_provider.rb +88 -0
 - data/lib/dredger/iot/sensors.rb +8 -0
 - data/lib/dredger/iot/version.rb +1 -1
 - metadata +11 -2
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 701ad99dd1f271df310ba3dffbb3295084b54e64689f4d3ed77f9b6ab9e31df5
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: e9b5ba95c4d2af83b529e6f66af5c81fb83c43f3f679e89e894209661651f93e
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 5362ae41f8f92d403809728857c139b00e3d8141e8f83725a3cc302eb7e7ace2c32b8bd7306fba5e83c2ee197e42c633b7beaeb9a84b9b77f1d95299e80aad1d
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 220b5834cd302cef375c3922211c055788855d4b516b7260aa952d13fc312a3f9c028248863906e93a38badbd32839d2083783bb1431ba5fd1ada0b4df92bca5
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            ## [0.3.0] - 2025-10-06
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 14 
     | 
    
         
            +
            - New sensors for industrial IoT applications:
         
     | 
| 
      
 15 
     | 
    
         
            +
              - **ADXL345** - I2C 3-axis accelerometer for vibration monitoring (±2g, ±4g, ±8g, ±16g ranges)
         
     | 
| 
      
 16 
     | 
    
         
            +
              - **SCD30** - I2C NDIR CO2 sensor with integrated temperature and humidity (400-10,000 ppm range)
         
     | 
| 
      
 17 
     | 
    
         
            +
              - **YF-S201** - GPIO hall effect flow meter for liquid flow measurement (1-30 L/min range)
         
     | 
| 
      
 18 
     | 
    
         
            +
              - **NEO-6M** - UART/Serial GPS module with NMEA 0183 parsing for location tracking
         
     | 
| 
      
 19 
     | 
    
         
            +
            - README: comprehensive usage examples for all new sensors
         
     | 
| 
      
 20 
     | 
    
         
            +
            - README: organized sensor list by category (Environmental, Light & Motion, Industrial)
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            ## [0.2.1] - 2025-10-05
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 25 
     | 
    
         
            +
            - Examples: Raspberry Pi GPIO blink script (GPIO17)
         
     | 
| 
      
 26 
     | 
    
         
            +
            - Docs: Raspberry Pi OS instructions to enable I2C and 1-Wire
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
       10 
28 
     | 
    
         
             
            ## [0.2.0] - 2025-10-05
         
     | 
| 
       11 
29 
     | 
    
         | 
| 
       12 
30 
     | 
    
         
             
            ### Added
         
     | 
| 
         @@ -64,7 +82,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       64 
82 
     | 
    
         
             
            - RuboCop configuration and compliance
         
     | 
| 
       65 
83 
     | 
    
         
             
            - Comprehensive documentation and usage examples
         
     | 
| 
       66 
84 
     | 
    
         | 
| 
       67 
     | 
    
         
            -
            [Unreleased]: https://github.com/TheMadBotterINC/dredger-iot/compare/v0. 
     | 
| 
      
 85 
     | 
    
         
            +
            [Unreleased]: https://github.com/TheMadBotterINC/dredger-iot/compare/v0.3.0...HEAD
         
     | 
| 
      
 86 
     | 
    
         
            +
            [0.3.0]: https://github.com/TheMadBotterINC/dredger-iot/compare/v0.2.1...v0.3.0
         
     | 
| 
      
 87 
     | 
    
         
            +
            [0.2.1]: https://github.com/TheMadBotterINC/dredger-iot/compare/v0.2.0...v0.2.1
         
     | 
| 
       68 
88 
     | 
    
         
             
            [0.2.0]: https://github.com/TheMadBotterINC/dredger-iot/compare/v0.1.2...v0.2.0
         
     | 
| 
       69 
89 
     | 
    
         
             
            [0.1.2]: https://github.com/TheMadBotterINC/dredger-iot/compare/v0.1.1...v0.1.2
         
     | 
| 
       70 
90 
     | 
    
         
             
            [0.1.1]: https://github.com/TheMadBotterINC/dredger-iot/compare/v0.1.0...v0.1.1
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -70,6 +70,26 @@ Environment overrides: 
     | 
|
| 
       70 
70 
     | 
    
         
             
            - `DREDGER_IOT_GPIO_BACKEND`: `simulation` | `libgpiod`
         
     | 
| 
       71 
71 
     | 
    
         
             
            - `DREDGER_IOT_I2C_BACKEND`: `simulation` | `linux`
         
     | 
| 
       72 
72 
     | 
    
         | 
| 
      
 73 
     | 
    
         
            +
            ## Raspberry Pi GPIO label mapping
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
            When the libgpiod backend is selected via Auto, Dredger-IoT resolves Raspberry Pi labels to the corresponding chip:line before accessing the GPIO line. Accepted labels:
         
     | 
| 
      
 76 
     | 
    
         
            +
            - GPIO17 or BCM17 (Broadcom numbering)
         
     | 
| 
      
 77 
     | 
    
         
            +
            - PIN11 or BOARD11 (header pin numbers)
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
            On most Raspberry Pi boards, GPIO lines are exposed on gpiochip0 and the line offset matches the BCM number. The adapter will translate labels accordingly.
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
            Example:
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
            ```ruby path=null start=null
         
     | 
| 
      
 84 
     | 
    
         
            +
            require 'dredger/iot'
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            gpio = Dredger::IoT::Bus::Auto.gpio # picks libgpiod on RPi, otherwise simulation
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
            # Use Raspberry Pi labels
         
     | 
| 
      
 89 
     | 
    
         
            +
            gpio.set_direction('GPIO17', :out)
         
     | 
| 
      
 90 
     | 
    
         
            +
            gpio.write('GPIO17', 1)
         
     | 
| 
      
 91 
     | 
    
         
            +
            ```
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
       73 
93 
     | 
    
         
             
            ## Beaglebone P9_XX label mapping
         
     | 
| 
       74 
94 
     | 
    
         | 
| 
       75 
95 
     | 
    
         
             
            When the libgpiod backend is selected via Auto, Dredger-IoT resolves Beaglebone labels like `P9_12` to the corresponding `gpiochipN:line` before accessing the GPIO line. A minimal built-in table is provided and can be extended in future releases.
         
     | 
| 
         @@ -93,15 +113,24 @@ If you run on a development host (no /dev/gpiochip0), Auto will default to the s 
     | 
|
| 
       93 
113 
     | 
    
         | 
| 
       94 
114 
     | 
    
         
             
            Dredger-IoT includes drivers for popular embedded sensors:
         
     | 
| 
       95 
115 
     | 
    
         | 
| 
      
 116 
     | 
    
         
            +
            **Environmental Sensors:**
         
     | 
| 
       96 
117 
     | 
    
         
             
            - **DHT22** - GPIO humidity/temperature sensor
         
     | 
| 
       97 
118 
     | 
    
         
             
            - **BME280** - I2C temperature/humidity/pressure sensor
         
     | 
| 
       98 
     | 
    
         
            -
            - **DS18B20** - 1-Wire digital temperature sensor
         
     | 
| 
       99 
119 
     | 
    
         
             
            - **BMP180** - I2C barometric pressure/temperature sensor
         
     | 
| 
       100 
120 
     | 
    
         
             
            - **MCP9808** - I2C high-accuracy temperature sensor
         
     | 
| 
       101 
121 
     | 
    
         
             
            - **SHT31** - I2C temperature/humidity sensor
         
     | 
| 
      
 122 
     | 
    
         
            +
            - **DS18B20** - 1-Wire digital temperature sensor
         
     | 
| 
      
 123 
     | 
    
         
            +
            - **SCD30** - I2C NDIR CO2/temperature/humidity sensor (NEW)
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
            **Light & Motion Sensors:**
         
     | 
| 
       102 
126 
     | 
    
         
             
            - **BH1750** - I2C ambient light sensor (lux)
         
     | 
| 
       103 
127 
     | 
    
         
             
            - **TSL2561** - I2C ambient light sensor (lux)
         
     | 
| 
      
 128 
     | 
    
         
            +
            - **ADXL345** - I2C 3-axis accelerometer (vibration monitoring) (NEW)
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            **Industrial Sensors:**
         
     | 
| 
       104 
131 
     | 
    
         
             
            - **INA219** - I2C bus voltage/current monitor
         
     | 
| 
      
 132 
     | 
    
         
            +
            - **YF-S201** - GPIO hall effect flow meter (liquid flow) (NEW)
         
     | 
| 
      
 133 
     | 
    
         
            +
            - **NEO-6M** - UART/Serial GPS module (location tracking) (NEW)
         
     | 
| 
       105 
134 
     | 
    
         | 
| 
       106 
135 
     | 
    
         
             
            Sensors use a provider pattern for testability and hardware abstraction.
         
     | 
| 
       107 
136 
     | 
    
         | 
| 
         @@ -225,6 +254,101 @@ temp = sensor.readings.first 
     | 
|
| 
       225 
254 
     | 
    
         
             
            puts "#{temp.value}°C"
         
     | 
| 
       226 
255 
     | 
    
         
             
            ```
         
     | 
| 
       227 
256 
     | 
    
         | 
| 
      
 257 
     | 
    
         
            +
            ### ADXL345 Accelerometer (Vibration Monitoring)
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
            ```ruby path=null start=null
         
     | 
| 
      
 260 
     | 
    
         
            +
            require 'dredger/iot'
         
     | 
| 
      
 261 
     | 
    
         
            +
             
     | 
| 
      
 262 
     | 
    
         
            +
            # Set up I2C bus and ADXL345 provider
         
     | 
| 
      
 263 
     | 
    
         
            +
            i2c = Dredger::IoT::Bus::Auto.i2c
         
     | 
| 
      
 264 
     | 
    
         
            +
            provider = Dredger::IoT::Sensors::ADXL345Provider.new(i2c_bus: i2c, range: 2) # ±2g
         
     | 
| 
      
 265 
     | 
    
         
            +
             
     | 
| 
      
 266 
     | 
    
         
            +
            # Create sensor instance (default I2C address 0x53)
         
     | 
| 
      
 267 
     | 
    
         
            +
            sensor = Dredger::IoT::Sensors::ADXL345.new(
         
     | 
| 
      
 268 
     | 
    
         
            +
              i2c_addr: 0x53,
         
     | 
| 
      
 269 
     | 
    
         
            +
              provider: provider,
         
     | 
| 
      
 270 
     | 
    
         
            +
              metadata: { location: 'motor_mount' }
         
     | 
| 
      
 271 
     | 
    
         
            +
            )
         
     | 
| 
      
 272 
     | 
    
         
            +
             
     | 
| 
      
 273 
     | 
    
         
            +
            readings = sensor.readings
         
     | 
| 
      
 274 
     | 
    
         
            +
            readings.each { |r| puts "#{r.sensor_type}: #{r.value} #{r.unit}" }
         
     | 
| 
      
 275 
     | 
    
         
            +
            # => acceleration_x: 0.024 g
         
     | 
| 
      
 276 
     | 
    
         
            +
            # => acceleration_y: -0.012 g
         
     | 
| 
      
 277 
     | 
    
         
            +
            # => acceleration_z: 0.980 g
         
     | 
| 
      
 278 
     | 
    
         
            +
            ```
         
     | 
| 
      
 279 
     | 
    
         
            +
             
     | 
| 
      
 280 
     | 
    
         
            +
            ### SCD30 CO2 Sensor
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
            ```ruby path=null start=null
         
     | 
| 
      
 283 
     | 
    
         
            +
            require 'dredger/iot'
         
     | 
| 
      
 284 
     | 
    
         
            +
             
     | 
| 
      
 285 
     | 
    
         
            +
            # Set up I2C bus and SCD30 provider
         
     | 
| 
      
 286 
     | 
    
         
            +
            i2c = Dredger::IoT::Bus::Auto.i2c
         
     | 
| 
      
 287 
     | 
    
         
            +
            provider = Dredger::IoT::Sensors::SCD30Provider.new(i2c_bus: i2c, interval: 2)
         
     | 
| 
      
 288 
     | 
    
         
            +
             
     | 
| 
      
 289 
     | 
    
         
            +
            # Create sensor instance (default I2C address 0x61)
         
     | 
| 
      
 290 
     | 
    
         
            +
            sensor = Dredger::IoT::Sensors::SCD30.new(
         
     | 
| 
      
 291 
     | 
    
         
            +
              i2c_addr: 0x61,
         
     | 
| 
      
 292 
     | 
    
         
            +
              provider: provider,
         
     | 
| 
      
 293 
     | 
    
         
            +
              metadata: { location: 'greenhouse' }
         
     | 
| 
      
 294 
     | 
    
         
            +
            )
         
     | 
| 
      
 295 
     | 
    
         
            +
             
     | 
| 
      
 296 
     | 
    
         
            +
            readings = sensor.readings
         
     | 
| 
      
 297 
     | 
    
         
            +
            readings.each { |r| puts "#{r.sensor_type}: #{r.value} #{r.unit}" }
         
     | 
| 
      
 298 
     | 
    
         
            +
            # => co2: 412.5 ppm
         
     | 
| 
      
 299 
     | 
    
         
            +
            # => temperature: 23.45 celsius
         
     | 
| 
      
 300 
     | 
    
         
            +
            # => humidity: 45.2 %
         
     | 
| 
      
 301 
     | 
    
         
            +
            ```
         
     | 
| 
      
 302 
     | 
    
         
            +
             
     | 
| 
      
 303 
     | 
    
         
            +
            ### YF-S201 Flow Meter
         
     | 
| 
      
 304 
     | 
    
         
            +
             
     | 
| 
      
 305 
     | 
    
         
            +
            ```ruby path=null start=null
         
     | 
| 
      
 306 
     | 
    
         
            +
            require 'dredger/iot'
         
     | 
| 
      
 307 
     | 
    
         
            +
             
     | 
| 
      
 308 
     | 
    
         
            +
            # Set up GPIO bus and flow meter provider
         
     | 
| 
      
 309 
     | 
    
         
            +
            gpio = Dredger::IoT::Bus::Auto.gpio
         
     | 
| 
      
 310 
     | 
    
         
            +
            provider = Dredger::IoT::Sensors::YFS201Provider.new(gpio_bus: gpio, calibration_factor: 7.5)
         
     | 
| 
      
 311 
     | 
    
         
            +
             
     | 
| 
      
 312 
     | 
    
         
            +
            # Create sensor instance
         
     | 
| 
      
 313 
     | 
    
         
            +
            sensor = Dredger::IoT::Sensors::YFS201.new(
         
     | 
| 
      
 314 
     | 
    
         
            +
              pin_label: 'P9_12',
         
     | 
| 
      
 315 
     | 
    
         
            +
              provider: provider,
         
     | 
| 
      
 316 
     | 
    
         
            +
              sample_duration: 1.0, # Count pulses for 1 second
         
     | 
| 
      
 317 
     | 
    
         
            +
              metadata: { location: 'main_line' }
         
     | 
| 
      
 318 
     | 
    
         
            +
            )
         
     | 
| 
      
 319 
     | 
    
         
            +
             
     | 
| 
      
 320 
     | 
    
         
            +
            readings = sensor.readings
         
     | 
| 
      
 321 
     | 
    
         
            +
            flow = readings.first
         
     | 
| 
      
 322 
     | 
    
         
            +
            puts "Flow: #{flow.value} #{flow.unit}"
         
     | 
| 
      
 323 
     | 
    
         
            +
            puts "Pulses: #{flow.metadata[:pulses]}"
         
     | 
| 
      
 324 
     | 
    
         
            +
            # => Flow: 5.25 L/min
         
     | 
| 
      
 325 
     | 
    
         
            +
            # => Pulses: 656
         
     | 
| 
      
 326 
     | 
    
         
            +
            ```
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
            ### NEO-6M GPS Module
         
     | 
| 
      
 329 
     | 
    
         
            +
             
     | 
| 
      
 330 
     | 
    
         
            +
            ```ruby path=null start=null
         
     | 
| 
      
 331 
     | 
    
         
            +
            require 'dredger/iot'
         
     | 
| 
      
 332 
     | 
    
         
            +
             
     | 
| 
      
 333 
     | 
    
         
            +
            # Set up GPS provider
         
     | 
| 
      
 334 
     | 
    
         
            +
            provider = Dredger::IoT::Sensors::NEO6MProvider.new(baud_rate: 9600, timeout: 5)
         
     | 
| 
      
 335 
     | 
    
         
            +
             
     | 
| 
      
 336 
     | 
    
         
            +
            # Create sensor instance
         
     | 
| 
      
 337 
     | 
    
         
            +
            sensor = Dredger::IoT::Sensors::NEO6M.new(
         
     | 
| 
      
 338 
     | 
    
         
            +
              device: '/dev/ttyAMA0', # Serial device
         
     | 
| 
      
 339 
     | 
    
         
            +
              provider: provider,
         
     | 
| 
      
 340 
     | 
    
         
            +
              metadata: { vehicle: 'truck_1' }
         
     | 
| 
      
 341 
     | 
    
         
            +
            )
         
     | 
| 
      
 342 
     | 
    
         
            +
             
     | 
| 
      
 343 
     | 
    
         
            +
            readings = sensor.readings
         
     | 
| 
      
 344 
     | 
    
         
            +
            readings.each { |r| puts "#{r.sensor_type}: #{r.value} #{r.unit}" }
         
     | 
| 
      
 345 
     | 
    
         
            +
            # => latitude: 37.774929 degrees
         
     | 
| 
      
 346 
     | 
    
         
            +
            # => longitude: -122.419418 degrees
         
     | 
| 
      
 347 
     | 
    
         
            +
            # => altitude: 52.4 m
         
     | 
| 
      
 348 
     | 
    
         
            +
            # => speed: 12.5 km/h
         
     | 
| 
      
 349 
     | 
    
         
            +
            # => gps_quality: 8 satellites
         
     | 
| 
      
 350 
     | 
    
         
            +
            ```
         
     | 
| 
      
 351 
     | 
    
         
            +
             
     | 
| 
       228 
352 
     | 
    
         
             
            ### Multiple Sensors with Scheduled Polling
         
     | 
| 
       229 
353 
     | 
    
         | 
| 
       230 
354 
     | 
    
         
             
            ```ruby path=null start=null
         
     | 
| 
         @@ -353,6 +477,29 @@ ls /sys/bus/w1/devices/ 
     | 
|
| 
       353 
477 
     | 
    
         
             
            # Should show devices like: 28-00000xxxxxx
         
     | 
| 
       354 
478 
     | 
    
         
             
            ```
         
     | 
| 
       355 
479 
     | 
    
         | 
| 
      
 480 
     | 
    
         
            +
            #### Raspberry Pi OS: Enable I2C and 1-Wire
         
     | 
| 
      
 481 
     | 
    
         
            +
             
     | 
| 
      
 482 
     | 
    
         
            +
            On Raspberry Pi OS you can enable I2C and 1-Wire via raspi-config or by editing /boot/config.txt.
         
     | 
| 
      
 483 
     | 
    
         
            +
             
     | 
| 
      
 484 
     | 
    
         
            +
            Option A: raspi-config (recommended)
         
     | 
| 
      
 485 
     | 
    
         
            +
            ```bash path=null start=null
         
     | 
| 
      
 486 
     | 
    
         
            +
            sudo raspi-config
         
     | 
| 
      
 487 
     | 
    
         
            +
            # Interface Options → I2C → Enable
         
     | 
| 
      
 488 
     | 
    
         
            +
            # Interface Options → 1-Wire → Enable
         
     | 
| 
      
 489 
     | 
    
         
            +
            sudo reboot
         
     | 
| 
      
 490 
     | 
    
         
            +
            ```
         
     | 
| 
      
 491 
     | 
    
         
            +
             
     | 
| 
      
 492 
     | 
    
         
            +
            Option B: edit /boot/config.txt
         
     | 
| 
      
 493 
     | 
    
         
            +
            ```bash path=null start=null
         
     | 
| 
      
 494 
     | 
    
         
            +
            # Enable I2C
         
     | 
| 
      
 495 
     | 
    
         
            +
            sudo sed -i 's/^#\?dtparam=i2c_arm=.*/dtparam=i2c_arm=on/' /boot/config.txt
         
     | 
| 
      
 496 
     | 
    
         
            +
            # Enable 1-Wire on default BCM4 (PIN7)
         
     | 
| 
      
 497 
     | 
    
         
            +
            echo 'dtoverlay=w1-gpio,gpiopin=4' | sudo tee -a /boot/config.txt
         
     | 
| 
      
 498 
     | 
    
         
            +
            sudo reboot
         
     | 
| 
      
 499 
     | 
    
         
            +
            ```
         
     | 
| 
      
 500 
     | 
    
         
            +
             
     | 
| 
      
 501 
     | 
    
         
            +
            Note: Dredger-IoT accepts Raspberry Pi labels like GPIO17, BCM17, and PIN11.
         
     | 
| 
      
 502 
     | 
    
         
            +
             
     | 
| 
       356 
503 
     | 
    
         
             
            #### Beaglebone Black Device Tree
         
     | 
| 
       357 
504 
     | 
    
         | 
| 
       358 
505 
     | 
    
         
             
            For Beaglebone Black, you may need to enable device tree overlays:
         
     | 
| 
         @@ -553,6 +700,25 @@ reading.timestamp    # Time object when reading was taken 
     | 
|
| 
       553 
700 
     | 
    
         
             
                dredger read ina219 0x40 --shunt 0.1
         
     | 
| 
       554 
701 
     | 
    
         
             
                ```
         
     | 
| 
       555 
702 
     | 
    
         | 
| 
      
 703 
     | 
    
         
            +
            - **`ADXL345`** - 3-axis accelerometer (I2C)
         
     | 
| 
      
 704 
     | 
    
         
            +
              - Parameters: `i2c_addr` (default: `0x53`), `provider`
         
     | 
| 
      
 705 
     | 
    
         
            +
              - Returns: acceleration_x (g), acceleration_y (g), acceleration_z (g)
         
     | 
| 
      
 706 
     | 
    
         
            +
              - Ranges: ±2g, ±4g, ±8g, ±16g (configurable in provider)
         
     | 
| 
      
 707 
     | 
    
         
            +
              
         
     | 
| 
      
 708 
     | 
    
         
            +
            - **`SCD30`** - NDIR CO2 sensor (I2C)
         
     | 
| 
      
 709 
     | 
    
         
            +
              - Parameters: `i2c_addr` (default: `0x61`), `provider`
         
     | 
| 
      
 710 
     | 
    
         
            +
              - Returns: co2 (ppm), temperature (celsius), humidity (%)
         
     | 
| 
      
 711 
     | 
    
         
            +
              - Range: 400-10,000 ppm CO2
         
     | 
| 
      
 712 
     | 
    
         
            +
              
         
     | 
| 
      
 713 
     | 
    
         
            +
            - **`YFS201`** - Hall effect flow meter (GPIO)
         
     | 
| 
      
 714 
     | 
    
         
            +
              - Parameters: `pin_label`, `provider`, `sample_duration` (default: `1.0`)
         
     | 
| 
      
 715 
     | 
    
         
            +
              - Returns: flow_rate (L/min)
         
     | 
| 
      
 716 
     | 
    
         
            +
              - Range: 1-30 L/min
         
     | 
| 
      
 717 
     | 
    
         
            +
              
         
     | 
| 
      
 718 
     | 
    
         
            +
            - **`NEO6M`** - GPS module (UART/Serial)
         
     | 
| 
      
 719 
     | 
    
         
            +
              - Parameters: `device` (default: `'/dev/ttyAMA0'`), `provider`
         
     | 
| 
      
 720 
     | 
    
         
            +
              - Returns: latitude (degrees), longitude (degrees), altitude (m), speed (km/h), gps_quality (satellites)
         
     | 
| 
      
 721 
     | 
    
         
            +
             
     | 
| 
       556 
722 
     | 
    
         
             
            ### Scheduling
         
     | 
| 
       557 
723 
     | 
    
         | 
| 
       558 
724 
     | 
    
         
             
            #### `Dredger::IoT::Scheduler.periodic_with_jitter`
         
     | 
    
        data/bin/dredger
    CHANGED
    
    | 
         @@ -32,6 +32,8 @@ class DredgerCLI 
     | 
|
| 
       32 
32 
     | 
    
         
             
                  test_i2c
         
     | 
| 
       33 
33 
     | 
    
         
             
                when 'info'
         
     | 
| 
       34 
34 
     | 
    
         
             
                  show_info
         
     | 
| 
      
 35 
     | 
    
         
            +
                when 'doctor'
         
     | 
| 
      
 36 
     | 
    
         
            +
                  doctor
         
     | 
| 
       35 
37 
     | 
    
         
             
                else
         
     | 
| 
       36 
38 
     | 
    
         
             
                  puts parser
         
     | 
| 
       37 
39 
     | 
    
         
             
                  exit 1
         
     | 
| 
         @@ -53,6 +55,7 @@ class DredgerCLI 
     | 
|
| 
       53 
55 
     | 
    
         
             
                  opts.separator '  read SENSOR [OPTIONS]  Read from a sensor'
         
     | 
| 
       54 
56 
     | 
    
         
             
                  opts.separator '  test-gpio PIN          Test GPIO pin'
         
     | 
| 
       55 
57 
     | 
    
         
             
                  opts.separator '  test-i2c               Scan I2C bus'
         
     | 
| 
      
 58 
     | 
    
         
            +
                  opts.separator '  doctor                 Check system prerequisites'
         
     | 
| 
       56 
59 
     | 
    
         
             
                  opts.separator '  info                   Show system information'
         
     | 
| 
       57 
60 
     | 
    
         
             
                  opts.separator ''
         
     | 
| 
       58 
61 
     | 
    
         
             
                  opts.separator 'Options:'
         
     | 
| 
         @@ -270,6 +273,51 @@ class DredgerCLI 
     | 
|
| 
       270 
273 
     | 
    
         
             
                puts "  DREDGER_IOT_I2C_BACKEND:  #{ENV['DREDGER_IOT_I2C_BACKEND'] || '(not set)'}"
         
     | 
| 
       271 
274 
     | 
    
         
             
              end
         
     | 
| 
       272 
275 
     | 
    
         | 
| 
      
 276 
     | 
    
         
            +
              def doctor
         
     | 
| 
      
 277 
     | 
    
         
            +
                puts 'Doctor: checking system prerequisites...'
         
     | 
| 
      
 278 
     | 
    
         
            +
                puts
         
     | 
| 
      
 279 
     | 
    
         
            +
                check_device_node('/dev/gpiochip0', 'GPIO (libgpiod) device')
         
     | 
| 
      
 280 
     | 
    
         
            +
                check_device_node('/dev/i2c-1', 'I2C device (bus 1)')
         
     | 
| 
      
 281 
     | 
    
         
            +
                check_path('/sys/bus/w1/devices', '1-Wire bus (DS18B20) directory')
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
      
 283 
     | 
    
         
            +
                puts
         
     | 
| 
      
 284 
     | 
    
         
            +
                puts 'Kernel modules:'
         
     | 
| 
      
 285 
     | 
    
         
            +
                mods = read_proc_modules
         
     | 
| 
      
 286 
     | 
    
         
            +
                puts "  i2c-dev: #{mods.include?('i2c_dev') ? 'loaded' : 'missing'}"
         
     | 
| 
      
 287 
     | 
    
         
            +
                puts "  w1-gpio: #{mods.include?('w1_gpio') ? 'loaded' : 'missing'}"
         
     | 
| 
      
 288 
     | 
    
         
            +
                puts "  w1-therm: #{mods.include?('w1_therm') ? 'loaded' : 'missing'}"
         
     | 
| 
      
 289 
     | 
    
         
            +
             
     | 
| 
      
 290 
     | 
    
         
            +
                puts
         
     | 
| 
      
 291 
     | 
    
         
            +
                puts 'Recommendations:'
         
     | 
| 
      
 292 
     | 
    
         
            +
                puts "  sudo apt-get install gpiod i2c-tools"
         
     | 
| 
      
 293 
     | 
    
         
            +
                puts "  sudo usermod -a -G gpio $USER  # for GPIO access"
         
     | 
| 
      
 294 
     | 
    
         
            +
                puts "  sudo usermod -a -G i2c $USER   # for I2C access"
         
     | 
| 
      
 295 
     | 
    
         
            +
                puts "  sudo modprobe i2c-dev"
         
     | 
| 
      
 296 
     | 
    
         
            +
                puts "  sudo modprobe w1-gpio; sudo modprobe w1-therm"
         
     | 
| 
      
 297 
     | 
    
         
            +
              end
         
     | 
| 
      
 298 
     | 
    
         
            +
             
     | 
| 
      
 299 
     | 
    
         
            +
              def check_device_node(path, desc)
         
     | 
| 
      
 300 
     | 
    
         
            +
                if File.exist?(path)
         
     | 
| 
      
 301 
     | 
    
         
            +
                  puts "  ✔ #{desc}: present (#{path})"
         
     | 
| 
      
 302 
     | 
    
         
            +
                else
         
     | 
| 
      
 303 
     | 
    
         
            +
                  puts "  ✖ #{desc}: missing (#{path})"
         
     | 
| 
      
 304 
     | 
    
         
            +
                end
         
     | 
| 
      
 305 
     | 
    
         
            +
              end
         
     | 
| 
      
 306 
     | 
    
         
            +
             
     | 
| 
      
 307 
     | 
    
         
            +
              def check_path(path, desc)
         
     | 
| 
      
 308 
     | 
    
         
            +
                if Dir.exist?(path)
         
     | 
| 
      
 309 
     | 
    
         
            +
                  puts "  ✔ #{desc}: present (#{path})"
         
     | 
| 
      
 310 
     | 
    
         
            +
                else
         
     | 
| 
      
 311 
     | 
    
         
            +
                  puts "  ✖ #{desc}: missing (#{path})"
         
     | 
| 
      
 312 
     | 
    
         
            +
                end
         
     | 
| 
      
 313 
     | 
    
         
            +
              end
         
     | 
| 
      
 314 
     | 
    
         
            +
             
     | 
| 
      
 315 
     | 
    
         
            +
              def read_proc_modules
         
     | 
| 
      
 316 
     | 
    
         
            +
                File.read('/proc/modules')
         
     | 
| 
      
 317 
     | 
    
         
            +
              rescue StandardError
         
     | 
| 
      
 318 
     | 
    
         
            +
                ''
         
     | 
| 
      
 319 
     | 
    
         
            +
              end
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
       273 
321 
     | 
    
         
             
              def setup_backends
         
     | 
| 
       274 
322 
     | 
    
         
             
                case @options[:backend]
         
     | 
| 
       275 
323 
     | 
    
         
             
                when 'simulation'
         
     | 
| 
         @@ -5,9 +5,11 @@ module Dredger 
     | 
|
| 
       5 
5 
     | 
    
         
             
                module Bus
         
     | 
| 
       6 
6 
     | 
    
         
             
                  # Adapts label strings (e.g. 'P9_12') to PinRef with chip:line for libgpiod backends
         
     | 
| 
       7 
7 
     | 
    
         
             
                  class GPIOLabelAdapter
         
     | 
| 
       8 
     | 
    
         
            -
                     
     | 
| 
      
 8 
     | 
    
         
            +
                    # mapper can be a single mapper module/class or an Array of them.
         
     | 
| 
      
 9 
     | 
    
         
            +
                    # Defaults to both Beaglebone and RaspberryPi mappers.
         
     | 
| 
      
 10 
     | 
    
         
            +
                    def initialize(backend:, mapper: [Dredger::IoT::Pins::Beaglebone, Dredger::IoT::Pins::RaspberryPi])
         
     | 
| 
       9 
11 
     | 
    
         
             
                      @backend = backend
         
     | 
| 
       10 
     | 
    
         
            -
                      @ 
     | 
| 
      
 12 
     | 
    
         
            +
                      @mappers = Array(mapper)
         
     | 
| 
       11 
13 
     | 
    
         
             
                    end
         
     | 
| 
       12 
14 
     | 
    
         | 
| 
       13 
15 
     | 
    
         
             
                    def set_direction(pin, direction)
         
     | 
| 
         @@ -24,16 +26,24 @@ module Dredger 
     | 
|
| 
       24 
26 
     | 
    
         | 
| 
       25 
27 
     | 
    
         
             
                    private
         
     | 
| 
       26 
28 
     | 
    
         | 
| 
      
 29 
     | 
    
         
            +
                    # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         
     | 
| 
       27 
30 
     | 
    
         
             
                    def resolve(pin)
         
     | 
| 
       28 
31 
     | 
    
         
             
                      # Already a PinRef with line
         
     | 
| 
       29 
32 
     | 
    
         
             
                      return pin if pin.respond_to?(:line) && !pin.line.nil?
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
       30 
34 
     | 
    
         
             
                      # Numeric line
         
     | 
| 
       31 
35 
     | 
    
         
             
                      return Integer(pin) if pin.is_a?(Integer) || pin.to_s =~ /^\d+$/
         
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
                       
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                      # Try all mappers in order
         
     | 
| 
      
 38 
     | 
    
         
            +
                      @mappers.each do |m|
         
     | 
| 
      
 39 
     | 
    
         
            +
                        if m.respond_to?(:resolve_label_to_pinref) && m.respond_to?(:valid_label?) && m.valid_label?(pin)
         
     | 
| 
      
 40 
     | 
    
         
            +
                          return m.resolve_label_to_pinref(pin)
         
     | 
| 
      
 41 
     | 
    
         
            +
                        end
         
     | 
| 
      
 42 
     | 
    
         
            +
                      end
         
     | 
| 
       34 
43 
     | 
    
         | 
| 
       35 
44 
     | 
    
         
             
                      raise ArgumentError, 'Unsupported pin format'
         
     | 
| 
       36 
45 
     | 
    
         
             
                    end
         
     | 
| 
      
 46 
     | 
    
         
            +
                    # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         
     | 
| 
       37 
47 
     | 
    
         
             
                  end
         
     | 
| 
       38 
48 
     | 
    
         
             
                end
         
     | 
| 
       39 
49 
     | 
    
         
             
              end
         
     | 
| 
         @@ -0,0 +1,54 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Pins
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Raspberry Pi header/BCM label mapping.
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Supports labels like:
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # - GPIO17, BCM17
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # - PIN11 (a.k.a. BOARD11)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  class RaspberryPi
         
     | 
| 
      
 11 
     | 
    
         
            +
                    PinRef = Struct.new(:label, :chip, :line, keyword_init: true) do
         
     | 
| 
      
 12 
     | 
    
         
            +
                      def to_s
         
     | 
| 
      
 13 
     | 
    
         
            +
                        chip && line ? "#{label}(chip#{chip}:#{line})" : label.to_s
         
     | 
| 
      
 14 
     | 
    
         
            +
                      end
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    # Subset of BOARD pin to BCM mapping for common usable GPIOs on 40-pin header
         
     | 
| 
      
 18 
     | 
    
         
            +
                    BOARD_TO_BCM = {
         
     | 
| 
      
 19 
     | 
    
         
            +
                      3 => 2, 5 => 3, 7 => 4, 8 => 14, 10 => 15, 11 => 17, 12 => 18, 13 => 27,
         
     | 
| 
      
 20 
     | 
    
         
            +
                      15 => 22, 16 => 23, 18 => 24, 19 => 10, 21 => 9, 22 => 25, 23 => 11,
         
     | 
| 
      
 21 
     | 
    
         
            +
                      24 => 8, 26 => 7, 29 => 5, 31 => 6, 32 => 12, 33 => 13, 35 => 19,
         
     | 
| 
      
 22 
     | 
    
         
            +
                      36 => 16, 37 => 26, 38 => 20, 40 => 21
         
     | 
| 
      
 23 
     | 
    
         
            +
                    }.freeze
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                    # Accept variants like GPIO17, BCM17, PIN11, BOARD11
         
     | 
| 
      
 26 
     | 
    
         
            +
                    def self.valid_label?(label)
         
     | 
| 
      
 27 
     | 
    
         
            +
                      s = label.to_s.upcase
         
     | 
| 
      
 28 
     | 
    
         
            +
                      return true if s.match?(/^(GPIO|BCM)\d+$/)
         
     | 
| 
      
 29 
     | 
    
         
            +
                      return true if s.match?(/^(PIN|BOARD)\d+$/)
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                      false
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    def self.resolve_label_to_pinref(label)
         
     | 
| 
      
 35 
     | 
    
         
            +
                      s = label.to_s.upcase
         
     | 
| 
      
 36 
     | 
    
         
            +
                      if s =~ /^(GPIO|BCM)(\d+)$/
         
     | 
| 
      
 37 
     | 
    
         
            +
                        bcm = Regexp.last_match(2).to_i
         
     | 
| 
      
 38 
     | 
    
         
            +
                        return PinRef.new(label: label.to_s, chip: 0, line: bcm)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                      if s =~ /^(PIN|BOARD)(\d+)$/
         
     | 
| 
      
 42 
     | 
    
         
            +
                        board = Regexp.last_match(2).to_i
         
     | 
| 
      
 43 
     | 
    
         
            +
                        bcm = BOARD_TO_BCM[board]
         
     | 
| 
      
 44 
     | 
    
         
            +
                        raise ArgumentError, "Unknown/unsupported board pin: #{label}" if bcm.nil?
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                        return PinRef.new(label: label.to_s, chip: 0, line: bcm)
         
     | 
| 
      
 47 
     | 
    
         
            +
                      end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                      raise ArgumentError, "Unknown Raspberry Pi pin label: #{label}"
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/dredger/iot/pins.rb
    CHANGED
    
    
| 
         @@ -0,0 +1,28 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # ADXL345 3-axis digital accelerometer sensor (I2C/SPI)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Measures acceleration/vibration in x, y, z axes
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Uses a provider interface to allow simulation in tests and hardware backends in production.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class ADXL345 < BaseSensor
         
     | 
| 
      
 10 
     | 
    
         
            +
                    # provider must respond to :read_measurements(i2c_addr) -> { x_g:, y_g:, z_g: }
         
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(i2c_addr: 0x53, provider:, metadata: {})
         
     | 
| 
      
 12 
     | 
    
         
            +
                      super(metadata: metadata)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @i2c_addr = i2c_addr
         
     | 
| 
      
 14 
     | 
    
         
            +
                      @provider = provider
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    def readings
         
     | 
| 
      
 18 
     | 
    
         
            +
                      sample = @provider.read_measurements(@i2c_addr)
         
     | 
| 
      
 19 
     | 
    
         
            +
                      [
         
     | 
| 
      
 20 
     | 
    
         
            +
                        reading(sensor_type: 'acceleration_x', value: sample[:x_g], unit: 'g'),
         
     | 
| 
      
 21 
     | 
    
         
            +
                        reading(sensor_type: 'acceleration_y', value: sample[:y_g], unit: 'g'),
         
     | 
| 
      
 22 
     | 
    
         
            +
                        reading(sensor_type: 'acceleration_z', value: sample[:z_g], unit: 'g')
         
     | 
| 
      
 23 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,103 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Hardware provider for ADXL345 3-axis accelerometer over I2C.
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ADXL345.pdf
         
     | 
| 
      
 8 
     | 
    
         
            +
                  #
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # Key features:
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # - ±2g, ±4g, ±8g, ±16g selectable measurement ranges
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # - 10-bit to 13-bit resolution
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # - I2C (up to 400 kHz) or SPI interface
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # - Default I2C address: 0x53 (ALT address: 0x1D if SDO/ALT pulled high)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  #
         
     | 
| 
      
 15 
     | 
    
         
            +
                  # Key registers:
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # - 0x00: DEVID (should be 0xE5)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # - 0x2D: POWER_CTL (power modes)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  # - 0x31: DATA_FORMAT (range and resolution)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # - 0x32-0x37: DATAX0, DATAX1, DATAY0, DATAY1, DATAZ0, DATAZ1 (acceleration data)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  class ADXL345Provider
         
     | 
| 
      
 21 
     | 
    
         
            +
                    DEVID_REG = 0x00
         
     | 
| 
      
 22 
     | 
    
         
            +
                    DEVID_EXPECTED = 0xE5
         
     | 
| 
      
 23 
     | 
    
         
            +
                    POWER_CTL_REG = 0x2D
         
     | 
| 
      
 24 
     | 
    
         
            +
                    DATA_FORMAT_REG = 0x31
         
     | 
| 
      
 25 
     | 
    
         
            +
                    DATAX0_REG = 0x32
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    # Measurement mode bit (POWER_CTL register)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    MEASURE_BIT = 0x08
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    # i2c_bus: an I2C bus interface (e.g., Dredger::IoT::Bus::Auto.i2c)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    # range: measurement range in g (2, 4, 8, or 16)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    def initialize(i2c_bus:, range: 2)
         
     | 
| 
      
 33 
     | 
    
         
            +
                      @i2c = i2c_bus
         
     | 
| 
      
 34 
     | 
    
         
            +
                      @range = range
         
     | 
| 
      
 35 
     | 
    
         
            +
                      @scale_factor = calculate_scale_factor(range)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                    # Read acceleration measurements from the ADXL345 at the given I2C address.
         
     | 
| 
      
 39 
     | 
    
         
            +
                    # Returns { x_g: Float, y_g: Float, z_g: Float }
         
     | 
| 
      
 40 
     | 
    
         
            +
                    def read_measurements(addr)
         
     | 
| 
      
 41 
     | 
    
         
            +
                      # Verify device ID
         
     | 
| 
      
 42 
     | 
    
         
            +
                      dev_id = @i2c.read(addr, 1, register: DEVID_REG).first
         
     | 
| 
      
 43 
     | 
    
         
            +
                      raise IOError, "ADXL345 not found (devid=0x#{dev_id.to_s(16)})" unless dev_id == DEVID_EXPECTED
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                      # Configure sensor (one-time setup)
         
     | 
| 
      
 46 
     | 
    
         
            +
                      configure_sensor(addr)
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                      # Read 6 bytes of acceleration data (X, Y, Z as 16-bit signed integers)
         
     | 
| 
      
 49 
     | 
    
         
            +
                      raw = @i2c.read(addr, 6, register: DATAX0_REG)
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                      # Parse 16-bit signed values (little-endian)
         
     | 
| 
      
 52 
     | 
    
         
            +
                      x_raw = to_signed16(raw[0] | (raw[1] << 8))
         
     | 
| 
      
 53 
     | 
    
         
            +
                      y_raw = to_signed16(raw[2] | (raw[3] << 8))
         
     | 
| 
      
 54 
     | 
    
         
            +
                      z_raw = to_signed16(raw[4] | (raw[5] << 8))
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                      # Convert to g units using scale factor
         
     | 
| 
      
 57 
     | 
    
         
            +
                      {
         
     | 
| 
      
 58 
     | 
    
         
            +
                        x_g: (x_raw * @scale_factor).round(3),
         
     | 
| 
      
 59 
     | 
    
         
            +
                        y_g: (y_raw * @scale_factor).round(3),
         
     | 
| 
      
 60 
     | 
    
         
            +
                        z_g: (z_raw * @scale_factor).round(3)
         
     | 
| 
      
 61 
     | 
    
         
            +
                      }
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    private
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    # Configure sensor for measurement mode with specified range
         
     | 
| 
      
 67 
     | 
    
         
            +
                    def configure_sensor(addr)
         
     | 
| 
      
 68 
     | 
    
         
            +
                      # Set data format: range bits [1:0]
         
     | 
| 
      
 69 
     | 
    
         
            +
                      # ±2g: 0b00, ±4g: 0b01, ±8g: 0b10, ±16g: 0b11
         
     | 
| 
      
 70 
     | 
    
         
            +
                      range_bits = case @range
         
     | 
| 
      
 71 
     | 
    
         
            +
                                   when 2 then 0b00
         
     | 
| 
      
 72 
     | 
    
         
            +
                                   when 4 then 0b01
         
     | 
| 
      
 73 
     | 
    
         
            +
                                   when 8 then 0b10
         
     | 
| 
      
 74 
     | 
    
         
            +
                                   when 16 then 0b11
         
     | 
| 
      
 75 
     | 
    
         
            +
                                   else raise ArgumentError, "Invalid range: #{@range}g (must be 2, 4, 8, or 16)"
         
     | 
| 
      
 76 
     | 
    
         
            +
                                   end
         
     | 
| 
      
 77 
     | 
    
         
            +
                      @i2c.write(addr, [range_bits], register: DATA_FORMAT_REG)
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                      # Enable measurement mode
         
     | 
| 
      
 80 
     | 
    
         
            +
                      @i2c.write(addr, [MEASURE_BIT], register: POWER_CTL_REG)
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                      # Wait for sensor to stabilize
         
     | 
| 
      
 83 
     | 
    
         
            +
                      sleep(0.01)
         
     | 
| 
      
 84 
     | 
    
         
            +
                    end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                    # Calculate scale factor for converting raw values to g
         
     | 
| 
      
 87 
     | 
    
         
            +
                    # ADXL345 uses 10-bit resolution in full resolution mode
         
     | 
| 
      
 88 
     | 
    
         
            +
                    # Scale factor: range / 512 (for 10-bit)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    def calculate_scale_factor(range)
         
     | 
| 
      
 90 
     | 
    
         
            +
                      # In full resolution mode, scale is ~3.9 mg/LSB regardless of range
         
     | 
| 
      
 91 
     | 
    
         
            +
                      # In fixed 10-bit mode, scale depends on range
         
     | 
| 
      
 92 
     | 
    
         
            +
                      # Using simplified calculation: range / 512
         
     | 
| 
      
 93 
     | 
    
         
            +
                      range / 512.0
         
     | 
| 
      
 94 
     | 
    
         
            +
                    end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                    # Convert unsigned 16-bit to signed 16-bit
         
     | 
| 
      
 97 
     | 
    
         
            +
                    def to_signed16(val)
         
     | 
| 
      
 98 
     | 
    
         
            +
                      val > 32_767 ? val - 65_536 : val
         
     | 
| 
      
 99 
     | 
    
         
            +
                    end
         
     | 
| 
      
 100 
     | 
    
         
            +
                  end
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
              end
         
     | 
| 
      
 103 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,35 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # NEO-6M GPS module (UART/Serial)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Provides location, altitude, speed, and satellite information
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Uses a provider interface to allow simulation in tests and hardware backends in production.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class NEO6M < BaseSensor
         
     | 
| 
      
 10 
     | 
    
         
            +
                    # provider must respond to :read_position(device) -> { latitude:, longitude:, altitude:, speed:, satellites: }
         
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(device: '/dev/ttyAMA0', provider:, metadata: {})
         
     | 
| 
      
 12 
     | 
    
         
            +
                      super(metadata: metadata)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @device = device
         
     | 
| 
      
 14 
     | 
    
         
            +
                      @provider = provider
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    def readings
         
     | 
| 
      
 18 
     | 
    
         
            +
                      sample = @provider.read_position(@device)
         
     | 
| 
      
 19 
     | 
    
         
            +
                      [
         
     | 
| 
      
 20 
     | 
    
         
            +
                        reading(sensor_type: 'latitude', value: sample[:latitude], unit: 'degrees'),
         
     | 
| 
      
 21 
     | 
    
         
            +
                        reading(sensor_type: 'longitude', value: sample[:longitude], unit: 'degrees'),
         
     | 
| 
      
 22 
     | 
    
         
            +
                        reading(sensor_type: 'altitude', value: sample[:altitude], unit: 'm'),
         
     | 
| 
      
 23 
     | 
    
         
            +
                        reading(sensor_type: 'speed', value: sample[:speed], unit: 'km/h'),
         
     | 
| 
      
 24 
     | 
    
         
            +
                        reading(
         
     | 
| 
      
 25 
     | 
    
         
            +
                          sensor_type: 'gps_quality',
         
     | 
| 
      
 26 
     | 
    
         
            +
                          value: sample[:satellites],
         
     | 
| 
      
 27 
     | 
    
         
            +
                          unit: 'satellites',
         
     | 
| 
      
 28 
     | 
    
         
            +
                          metadata: { fix_quality: sample[:fix_quality] }
         
     | 
| 
      
 29 
     | 
    
         
            +
                        )
         
     | 
| 
      
 30 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,140 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'io/wait'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 6 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 7 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Hardware provider for NEO-6M GPS module via UART/Serial with NMEA parsing.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # Datasheet: https://www.u-blox.com/sites/default/files/products/documents/NEO-6_DataSheet_(GPS.G6-HW-09005).pdf
         
     | 
| 
      
 10 
     | 
    
         
            +
                  #
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # Key features:
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # - 50-channel GPS receiver
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # - UART interface (default: 9600 baud, 8N1)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # - NMEA 0183 protocol output
         
     | 
| 
      
 15 
     | 
    
         
            +
                  # - Update rate: 1-5 Hz (default: 1 Hz)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # - Cold start: ~27s, Warm start: ~1s
         
     | 
| 
      
 17 
     | 
    
         
            +
                  #
         
     | 
| 
      
 18 
     | 
    
         
            +
                  # NMEA Sentences:
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # - $GPGGA: Global Positioning System Fix Data (position, altitude, satellites)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  # - $GPRMC: Recommended Minimum Navigation Information (position, speed, date/time)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  # - $GPGSA: GPS DOP and Active Satellites
         
     | 
| 
      
 22 
     | 
    
         
            +
                  # - $GPGSV: GPS Satellites in View
         
     | 
| 
      
 23 
     | 
    
         
            +
                  class NEO6MProvider
         
     | 
| 
      
 24 
     | 
    
         
            +
                    # baud_rate: serial baud rate (default: 9600)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    # timeout: read timeout in seconds (default: 5)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    def initialize(baud_rate: 9600, timeout: 5)
         
     | 
| 
      
 27 
     | 
    
         
            +
                      @baud_rate = baud_rate
         
     | 
| 
      
 28 
     | 
    
         
            +
                      @timeout = timeout
         
     | 
| 
      
 29 
     | 
    
         
            +
                    end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                    # Read GPS position by parsing NMEA sentences from the serial device.
         
     | 
| 
      
 32 
     | 
    
         
            +
                    # Returns { latitude: Float, longitude: Float, altitude: Float, speed: Float, satellites: Integer, fix_quality: Integer }
         
     | 
| 
      
 33 
     | 
    
         
            +
                    #
         
     | 
| 
      
 34 
     | 
    
         
            +
                    # @param device [String] Serial device path (e.g., '/dev/ttyAMA0', '/dev/ttyUSB0')
         
     | 
| 
      
 35 
     | 
    
         
            +
                    def read_position(device)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      File.open(device, 'r+') do |serial|
         
     | 
| 
      
 37 
     | 
    
         
            +
                        configure_serial(serial)
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                        # Read NMEA sentences until we have both GGA and RMC (or timeout)
         
     | 
| 
      
 40 
     | 
    
         
            +
                        gga_data = nil
         
     | 
| 
      
 41 
     | 
    
         
            +
                        rmc_data = nil
         
     | 
| 
      
 42 
     | 
    
         
            +
                        start_time = Time.now
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                        while (gga_data.nil? || rmc_data.nil?) && (Time.now - start_time < @timeout)
         
     | 
| 
      
 45 
     | 
    
         
            +
                          line = read_line(serial, @timeout)
         
     | 
| 
      
 46 
     | 
    
         
            +
                          next unless line
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                          if line.start_with?('$GPGGA') || line.start_with?('$GNGGA')
         
     | 
| 
      
 49 
     | 
    
         
            +
                            gga_data = parse_gga(line)
         
     | 
| 
      
 50 
     | 
    
         
            +
                          elsif line.start_with?('$GPRMC') || line.start_with?('$GNRMC')
         
     | 
| 
      
 51 
     | 
    
         
            +
                            rmc_data = parse_rmc(line)
         
     | 
| 
      
 52 
     | 
    
         
            +
                          end
         
     | 
| 
      
 53 
     | 
    
         
            +
                        end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                        raise IOError, 'GPS timeout: no valid NMEA sentences received' if gga_data.nil? && rmc_data.nil?
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                        # Merge data from both sentences (GGA has altitude/satellites, RMC has speed)
         
     | 
| 
      
 58 
     | 
    
         
            +
                        {
         
     | 
| 
      
 59 
     | 
    
         
            +
                          latitude: gga_data&.dig(:latitude) || rmc_data&.dig(:latitude) || 0.0,
         
     | 
| 
      
 60 
     | 
    
         
            +
                          longitude: gga_data&.dig(:longitude) || rmc_data&.dig(:longitude) || 0.0,
         
     | 
| 
      
 61 
     | 
    
         
            +
                          altitude: gga_data&.dig(:altitude) || 0.0,
         
     | 
| 
      
 62 
     | 
    
         
            +
                          speed: rmc_data&.dig(:speed) || 0.0,
         
     | 
| 
      
 63 
     | 
    
         
            +
                          satellites: gga_data&.dig(:satellites) || 0,
         
     | 
| 
      
 64 
     | 
    
         
            +
                          fix_quality: gga_data&.dig(:fix_quality) || 0
         
     | 
| 
      
 65 
     | 
    
         
            +
                        }
         
     | 
| 
      
 66 
     | 
    
         
            +
                      end
         
     | 
| 
      
 67 
     | 
    
         
            +
                    end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                    private
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    # Configure serial port (Linux termios settings)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    def configure_serial(serial)
         
     | 
| 
      
 73 
     | 
    
         
            +
                      # Set raw mode, no echo, baud rate
         
     | 
| 
      
 74 
     | 
    
         
            +
                      # This would typically use `stty` or `termios` gem
         
     | 
| 
      
 75 
     | 
    
         
            +
                      # For simplicity, assuming device is already configured
         
     | 
| 
      
 76 
     | 
    
         
            +
                      # In production, use: system("stty -F #{device} #{@baud_rate} raw -echo")
         
     | 
| 
      
 77 
     | 
    
         
            +
                    end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                    # Read a line from serial with timeout
         
     | 
| 
      
 80 
     | 
    
         
            +
                    def read_line(serial, timeout)
         
     | 
| 
      
 81 
     | 
    
         
            +
                      return nil unless serial.wait_readable(timeout)
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                      serial.gets&.chomp
         
     | 
| 
      
 84 
     | 
    
         
            +
                    end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                    # Parse $GPGGA sentence: Global Positioning System Fix Data
         
     | 
| 
      
 87 
     | 
    
         
            +
                    # Format: $GPGGA,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh
         
     | 
| 
      
 88 
     | 
    
         
            +
                    # Example: $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
         
     | 
| 
      
 89 
     | 
    
         
            +
                    def parse_gga(sentence)
         
     | 
| 
      
 90 
     | 
    
         
            +
                      parts = sentence.split(',')
         
     | 
| 
      
 91 
     | 
    
         
            +
                      return nil if parts.size < 15 || parts[6].to_i.zero? # Check fix quality
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                      {
         
     | 
| 
      
 94 
     | 
    
         
            +
                        latitude: parse_coordinate(parts[2], parts[3]),
         
     | 
| 
      
 95 
     | 
    
         
            +
                        longitude: parse_coordinate(parts[4], parts[5]),
         
     | 
| 
      
 96 
     | 
    
         
            +
                        fix_quality: parts[6].to_i,
         
     | 
| 
      
 97 
     | 
    
         
            +
                        satellites: parts[7].to_i,
         
     | 
| 
      
 98 
     | 
    
         
            +
                        altitude: parts[9].to_f
         
     | 
| 
      
 99 
     | 
    
         
            +
                      }
         
     | 
| 
      
 100 
     | 
    
         
            +
                    end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    # Parse $GPRMC sentence: Recommended Minimum Navigation Information
         
     | 
| 
      
 103 
     | 
    
         
            +
                    # Format: $GPRMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,ddmmyy,x.x,a*hh
         
     | 
| 
      
 104 
     | 
    
         
            +
                    # Example: $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
         
     | 
| 
      
 105 
     | 
    
         
            +
                    def parse_rmc(sentence)
         
     | 
| 
      
 106 
     | 
    
         
            +
                      parts = sentence.split(',')
         
     | 
| 
      
 107 
     | 
    
         
            +
                      return nil if parts.size < 12 || parts[2] != 'A' # Check if data is valid
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                      {
         
     | 
| 
      
 110 
     | 
    
         
            +
                        latitude: parse_coordinate(parts[3], parts[4]),
         
     | 
| 
      
 111 
     | 
    
         
            +
                        longitude: parse_coordinate(parts[5], parts[6]),
         
     | 
| 
      
 112 
     | 
    
         
            +
                        speed: parts[7].to_f * 1.852 # Convert knots to km/h
         
     | 
| 
      
 113 
     | 
    
         
            +
                      }
         
     | 
| 
      
 114 
     | 
    
         
            +
                    end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                    # Parse NMEA coordinate format (ddmm.mmmm) to decimal degrees
         
     | 
| 
      
 117 
     | 
    
         
            +
                    # @param coord_str [String] Coordinate string (e.g., "4807.038")
         
     | 
| 
      
 118 
     | 
    
         
            +
                    # @param direction [String] Direction (N/S for latitude, E/W for longitude)
         
     | 
| 
      
 119 
     | 
    
         
            +
                    def parse_coordinate(coord_str, direction)
         
     | 
| 
      
 120 
     | 
    
         
            +
                      return 0.0 if coord_str.nil? || coord_str.empty?
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                      # Determine if latitude or longitude based on length
         
     | 
| 
      
 123 
     | 
    
         
            +
                      # Latitude: ddmm.mmmm (2 digit degrees)
         
     | 
| 
      
 124 
     | 
    
         
            +
                      # Longitude: dddmm.mmmm (3 digit degrees)
         
     | 
| 
      
 125 
     | 
    
         
            +
                      degree_digits = coord_str.length >= 5 && coord_str[4] == '.' ? 2 : 3
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                      degrees = coord_str[0, degree_digits].to_f
         
     | 
| 
      
 128 
     | 
    
         
            +
                      minutes = coord_str[degree_digits..-1].to_f
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                      decimal_degrees = degrees + (minutes / 60.0)
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
                      # Apply direction (negative for South and West)
         
     | 
| 
      
 133 
     | 
    
         
            +
                      decimal_degrees *= -1 if %w[S W].include?(direction)
         
     | 
| 
      
 134 
     | 
    
         
            +
             
     | 
| 
      
 135 
     | 
    
         
            +
                      decimal_degrees.round(6)
         
     | 
| 
      
 136 
     | 
    
         
            +
                    end
         
     | 
| 
      
 137 
     | 
    
         
            +
                  end
         
     | 
| 
      
 138 
     | 
    
         
            +
                end
         
     | 
| 
      
 139 
     | 
    
         
            +
              end
         
     | 
| 
      
 140 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,28 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # SCD30 NDIR CO2 sensor with integrated temperature and humidity sensor
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Measures CO2 concentration, temperature, and humidity
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Uses a provider interface to allow simulation in tests and hardware backends in production.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class SCD30 < BaseSensor
         
     | 
| 
      
 10 
     | 
    
         
            +
                    # provider must respond to :read_measurements(i2c_addr) -> { co2_ppm:, temperature_c:, humidity: }
         
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(i2c_addr: 0x61, provider:, metadata: {})
         
     | 
| 
      
 12 
     | 
    
         
            +
                      super(metadata: metadata)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @i2c_addr = i2c_addr
         
     | 
| 
      
 14 
     | 
    
         
            +
                      @provider = provider
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    def readings
         
     | 
| 
      
 18 
     | 
    
         
            +
                      sample = @provider.read_measurements(@i2c_addr)
         
     | 
| 
      
 19 
     | 
    
         
            +
                      [
         
     | 
| 
      
 20 
     | 
    
         
            +
                        reading(sensor_type: 'co2', value: sample[:co2_ppm], unit: 'ppm'),
         
     | 
| 
      
 21 
     | 
    
         
            +
                        reading(sensor_type: 'temperature', value: sample[:temperature_c], unit: 'celsius'),
         
     | 
| 
      
 22 
     | 
    
         
            +
                        reading(sensor_type: 'humidity', value: sample[:humidity], unit: '%')
         
     | 
| 
      
 23 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,142 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Hardware provider for SCD30 CO2, temperature, and humidity sensor over I2C.
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Datasheet: https://sensirion.com/media/documents/4EAF6AF8/61652C3C/Sensirion_CO2_Sensors_SCD30_Datasheet.pdf
         
     | 
| 
      
 8 
     | 
    
         
            +
                  #
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # Key features:
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # - NDIR CO2 sensor (400-10,000 ppm range)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # - Integrated SHT31 temperature and humidity sensor
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # - I2C interface (default address: 0x61)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # - Automatic self-calibration
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # - Measurement interval: 2-1800 seconds
         
     | 
| 
      
 15 
     | 
    
         
            +
                  #
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # Key commands:
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # - 0x0010: Start continuous measurement
         
     | 
| 
      
 18 
     | 
    
         
            +
                  # - 0x0104: Stop continuous measurement
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # - 0x0202: Set measurement interval
         
     | 
| 
      
 20 
     | 
    
         
            +
                  # - 0x0300: Data ready status
         
     | 
| 
      
 21 
     | 
    
         
            +
                  # - 0x0027: Read measurement (18 bytes: CO2, temp, humidity)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  class SCD30Provider
         
     | 
| 
      
 23 
     | 
    
         
            +
                    CMD_START_MEASUREMENT = 0x0010
         
     | 
| 
      
 24 
     | 
    
         
            +
                    CMD_STOP_MEASUREMENT = 0x0104
         
     | 
| 
      
 25 
     | 
    
         
            +
                    CMD_SET_INTERVAL = 0x0202
         
     | 
| 
      
 26 
     | 
    
         
            +
                    CMD_DATA_READY = 0x0300
         
     | 
| 
      
 27 
     | 
    
         
            +
                    CMD_READ_MEASUREMENT = 0x0027
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    # i2c_bus: an I2C bus interface (e.g., Dredger::IoT::Bus::Auto.i2c)
         
     | 
| 
      
 30 
     | 
    
         
            +
                    # interval: measurement interval in seconds (2-1800)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    # ambient_pressure: ambient pressure compensation in mBar (700-1400, 0=disable)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    def initialize(i2c_bus:, interval: 2, ambient_pressure: 0)
         
     | 
| 
      
 33 
     | 
    
         
            +
                      @i2c = i2c_bus
         
     | 
| 
      
 34 
     | 
    
         
            +
                      @interval = interval
         
     | 
| 
      
 35 
     | 
    
         
            +
                      @ambient_pressure = ambient_pressure
         
     | 
| 
      
 36 
     | 
    
         
            +
                      @initialized = false
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                    # Read measurements from the SCD30 at the given I2C address.
         
     | 
| 
      
 40 
     | 
    
         
            +
                    # Returns { co2_ppm: Float, temperature_c: Float, humidity: Float }
         
     | 
| 
      
 41 
     | 
    
         
            +
                    def read_measurements(addr)
         
     | 
| 
      
 42 
     | 
    
         
            +
                      # Initialize sensor on first read
         
     | 
| 
      
 43 
     | 
    
         
            +
                      initialize_sensor(addr) unless @initialized
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                      # Wait for data ready
         
     | 
| 
      
 46 
     | 
    
         
            +
                      wait_for_data_ready(addr)
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                      # Read 18 bytes of measurement data
         
     | 
| 
      
 49 
     | 
    
         
            +
                      # Format: [CO2_MSB, CO2_LSB, CO2_CRC] [CO2_MSB, CO2_LSB, CO2_CRC] [T_MSB, T_LSB, T_CRC] [T_MSB, T_LSB, T_CRC] [H_MSB, H_LSB, H_CRC] [H_MSB, H_LSB, H_CRC]
         
     | 
| 
      
 50 
     | 
    
         
            +
                      # Actually: 3 float32 values (4 bytes each + CRC after every 2 bytes = 6 bytes per value)
         
     | 
| 
      
 51 
     | 
    
         
            +
                      @i2c.write(addr, [CMD_READ_MEASUREMENT >> 8, CMD_READ_MEASUREMENT & 0xFF])
         
     | 
| 
      
 52 
     | 
    
         
            +
                      sleep(0.01) # Wait for response
         
     | 
| 
      
 53 
     | 
    
         
            +
                      raw = @i2c.read(addr, 18)
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                      # Parse float32 values with CRC validation
         
     | 
| 
      
 56 
     | 
    
         
            +
                      co2_ppm = parse_float32_with_crc(raw, 0)
         
     | 
| 
      
 57 
     | 
    
         
            +
                      temp_c = parse_float32_with_crc(raw, 6)
         
     | 
| 
      
 58 
     | 
    
         
            +
                      humidity = parse_float32_with_crc(raw, 12)
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                      {
         
     | 
| 
      
 61 
     | 
    
         
            +
                        co2_ppm: co2_ppm.round(1),
         
     | 
| 
      
 62 
     | 
    
         
            +
                        temperature_c: temp_c.round(2),
         
     | 
| 
      
 63 
     | 
    
         
            +
                        humidity: humidity.round(1)
         
     | 
| 
      
 64 
     | 
    
         
            +
                      }
         
     | 
| 
      
 65 
     | 
    
         
            +
                    end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                    private
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                    # Initialize sensor with measurement interval and start continuous measurement
         
     | 
| 
      
 70 
     | 
    
         
            +
                    def initialize_sensor(addr)
         
     | 
| 
      
 71 
     | 
    
         
            +
                      # Set measurement interval
         
     | 
| 
      
 72 
     | 
    
         
            +
                      write_command_with_arg(addr, CMD_SET_INTERVAL, @interval)
         
     | 
| 
      
 73 
     | 
    
         
            +
                      sleep(0.01)
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                      # Start continuous measurement with optional pressure compensation
         
     | 
| 
      
 76 
     | 
    
         
            +
                      write_command_with_arg(addr, CMD_START_MEASUREMENT, @ambient_pressure)
         
     | 
| 
      
 77 
     | 
    
         
            +
                      sleep(0.02)
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                      @initialized = true
         
     | 
| 
      
 80 
     | 
    
         
            +
                    end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                    # Wait for data to be ready (poll data ready status)
         
     | 
| 
      
 83 
     | 
    
         
            +
                    def wait_for_data_ready(addr, timeout: 2.0)
         
     | 
| 
      
 84 
     | 
    
         
            +
                      start_time = Time.now
         
     | 
| 
      
 85 
     | 
    
         
            +
                      loop do
         
     | 
| 
      
 86 
     | 
    
         
            +
                        @i2c.write(addr, [CMD_DATA_READY >> 8, CMD_DATA_READY & 0xFF])
         
     | 
| 
      
 87 
     | 
    
         
            +
                        sleep(0.01)
         
     | 
| 
      
 88 
     | 
    
         
            +
                        status = @i2c.read(addr, 3)
         
     | 
| 
      
 89 
     | 
    
         
            +
                        # Data ready when bit 0 of word is 1
         
     | 
| 
      
 90 
     | 
    
         
            +
                        data_ready = (status[1] & 0x01) == 1
         
     | 
| 
      
 91 
     | 
    
         
            +
                        return if data_ready
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                        raise IOError, 'SCD30 data ready timeout' if Time.now - start_time > timeout
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                        sleep(0.1)
         
     | 
| 
      
 96 
     | 
    
         
            +
                      end
         
     | 
| 
      
 97 
     | 
    
         
            +
                    end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                    # Write a command with a 16-bit argument and CRC
         
     | 
| 
      
 100 
     | 
    
         
            +
                    def write_command_with_arg(addr, command, arg)
         
     | 
| 
      
 101 
     | 
    
         
            +
                      cmd_msb = command >> 8
         
     | 
| 
      
 102 
     | 
    
         
            +
                      cmd_lsb = command & 0xFF
         
     | 
| 
      
 103 
     | 
    
         
            +
                      arg_msb = arg >> 8
         
     | 
| 
      
 104 
     | 
    
         
            +
                      arg_lsb = arg & 0xFF
         
     | 
| 
      
 105 
     | 
    
         
            +
                      crc = calculate_crc8([arg_msb, arg_lsb])
         
     | 
| 
      
 106 
     | 
    
         
            +
                      @i2c.write(addr, [cmd_msb, cmd_lsb, arg_msb, arg_lsb, crc])
         
     | 
| 
      
 107 
     | 
    
         
            +
                    end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                    # Parse a float32 value from 6 bytes (4 data + 2 CRC)
         
     | 
| 
      
 110 
     | 
    
         
            +
                    # Format: [MSB0, LSB0, CRC0, MSB1, LSB1, CRC1]
         
     | 
| 
      
 111 
     | 
    
         
            +
                    def parse_float32_with_crc(data, offset)
         
     | 
| 
      
 112 
     | 
    
         
            +
                      # Verify CRCs
         
     | 
| 
      
 113 
     | 
    
         
            +
                      crc0 = calculate_crc8([data[offset], data[offset + 1]])
         
     | 
| 
      
 114 
     | 
    
         
            +
                      crc1 = calculate_crc8([data[offset + 3], data[offset + 4]])
         
     | 
| 
      
 115 
     | 
    
         
            +
                      raise IOError, 'SCD30 CRC error' unless crc0 == data[offset + 2] && crc1 == data[offset + 5]
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                      # Combine bytes into uint32 and convert to float32
         
     | 
| 
      
 118 
     | 
    
         
            +
                      bytes = [
         
     | 
| 
      
 119 
     | 
    
         
            +
                        data[offset],
         
     | 
| 
      
 120 
     | 
    
         
            +
                        data[offset + 1],
         
     | 
| 
      
 121 
     | 
    
         
            +
                        data[offset + 3],
         
     | 
| 
      
 122 
     | 
    
         
            +
                        data[offset + 4]
         
     | 
| 
      
 123 
     | 
    
         
            +
                      ].pack('C4').unpack1('N')
         
     | 
| 
      
 124 
     | 
    
         
            +
                      [bytes].pack('L>').unpack1('g')
         
     | 
| 
      
 125 
     | 
    
         
            +
                    end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                    # Calculate CRC-8 checksum (polynomial: 0x31, init: 0xFF)
         
     | 
| 
      
 128 
     | 
    
         
            +
                    def calculate_crc8(data)
         
     | 
| 
      
 129 
     | 
    
         
            +
                      crc = 0xFF
         
     | 
| 
      
 130 
     | 
    
         
            +
                      data.each do |byte|
         
     | 
| 
      
 131 
     | 
    
         
            +
                        crc ^= byte
         
     | 
| 
      
 132 
     | 
    
         
            +
                        8.times do
         
     | 
| 
      
 133 
     | 
    
         
            +
                          crc = (crc & 0x80) != 0 ? ((crc << 1) ^ 0x31) : (crc << 1)
         
     | 
| 
      
 134 
     | 
    
         
            +
                          crc &= 0xFF
         
     | 
| 
      
 135 
     | 
    
         
            +
                        end
         
     | 
| 
      
 136 
     | 
    
         
            +
                      end
         
     | 
| 
      
 137 
     | 
    
         
            +
                      crc
         
     | 
| 
      
 138 
     | 
    
         
            +
                    end
         
     | 
| 
      
 139 
     | 
    
         
            +
                  end
         
     | 
| 
      
 140 
     | 
    
         
            +
                end
         
     | 
| 
      
 141 
     | 
    
         
            +
              end
         
     | 
| 
      
 142 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,32 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # YF-S201 water flow sensor (hall effect pulse counter)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Measures liquid flow rate by counting pulses from hall effect sensor
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Uses a provider interface to allow simulation in tests and hardware backends in production.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class YFS201 < BaseSensor
         
     | 
| 
      
 10 
     | 
    
         
            +
                    # provider must respond to :read_flow_rate(pin_label, duration) -> { flow_rate_lpm: Float, pulses: Integer }
         
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(pin_label:, provider:, sample_duration: 1.0, metadata: {})
         
     | 
| 
      
 12 
     | 
    
         
            +
                      super(metadata: metadata)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @pin_label = pin_label
         
     | 
| 
      
 14 
     | 
    
         
            +
                      @provider = provider
         
     | 
| 
      
 15 
     | 
    
         
            +
                      @sample_duration = sample_duration
         
     | 
| 
      
 16 
     | 
    
         
            +
                    end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    def readings
         
     | 
| 
      
 19 
     | 
    
         
            +
                      sample = @provider.read_flow_rate(@pin_label, @sample_duration)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      [
         
     | 
| 
      
 21 
     | 
    
         
            +
                        reading(
         
     | 
| 
      
 22 
     | 
    
         
            +
                          sensor_type: 'flow_rate',
         
     | 
| 
      
 23 
     | 
    
         
            +
                          value: sample[:flow_rate_lpm],
         
     | 
| 
      
 24 
     | 
    
         
            +
                          unit: 'L/min',
         
     | 
| 
      
 25 
     | 
    
         
            +
                          metadata: { pulses: sample[:pulses], duration: @sample_duration }
         
     | 
| 
      
 26 
     | 
    
         
            +
                        )
         
     | 
| 
      
 27 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,88 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dredger
         
     | 
| 
      
 4 
     | 
    
         
            +
              module IoT
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Sensors
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Hardware provider for YF-S201 water flow sensor via GPIO pulse counting.
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # Datasheet: https://www.hobbytronics.co.uk/download/YF-S201.pdf
         
     | 
| 
      
 8 
     | 
    
         
            +
                  #
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # Key features:
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # - Hall effect sensor with digital output
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # - Flow rate range: 1-30 L/min
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # - Pulse frequency: ~4.5 * flow_rate (L/min)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # - Working voltage: 5V DC
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # - Thread size: G1/2" (DN15)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  #
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # Calibration:
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # - Frequency (Hz) = 7.5 * flow rate (L/min) (official spec)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  # - In practice: F = K * Q where K ≈ 4.5-7.5 depending on unit
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # - Default calibration factor: 7.5 (pulses per liter)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  class YFS201Provider
         
     | 
| 
      
 21 
     | 
    
         
            +
                    # gpio_bus: a GPIO bus interface (e.g., Dredger::IoT::Bus::Auto.gpio)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    # calibration_factor: pulses per liter (default: 7.5 for YF-S201)
         
     | 
| 
      
 23 
     | 
    
         
            +
                    def initialize(gpio_bus:, calibration_factor: 7.5)
         
     | 
| 
      
 24 
     | 
    
         
            +
                      @gpio = gpio_bus
         
     | 
| 
      
 25 
     | 
    
         
            +
                      @calibration_factor = calibration_factor
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    # Read flow rate by counting pulses over the specified duration.
         
     | 
| 
      
 29 
     | 
    
         
            +
                    # Returns { flow_rate_lpm: Float, pulses: Integer }
         
     | 
| 
      
 30 
     | 
    
         
            +
                    #
         
     | 
| 
      
 31 
     | 
    
         
            +
                    # @param pin_label [String] GPIO pin label (e.g., 'P9_12', 'GPIO17')
         
     | 
| 
      
 32 
     | 
    
         
            +
                    # @param duration [Float] Sampling duration in seconds
         
     | 
| 
      
 33 
     | 
    
         
            +
                    def read_flow_rate(pin_label, duration)
         
     | 
| 
      
 34 
     | 
    
         
            +
                      # Configure pin as input
         
     | 
| 
      
 35 
     | 
    
         
            +
                      @gpio.set_direction(pin_label, :in)
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                      # Count rising edge pulses over the duration
         
     | 
| 
      
 38 
     | 
    
         
            +
                      pulses = count_pulses(pin_label, duration)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                      # Calculate flow rate in L/min
         
     | 
| 
      
 41 
     | 
    
         
            +
                      # pulses_per_second = pulses / duration
         
     | 
| 
      
 42 
     | 
    
         
            +
                      # liters_per_second = pulses_per_second / calibration_factor
         
     | 
| 
      
 43 
     | 
    
         
            +
                      # liters_per_minute = liters_per_second * 60
         
     | 
| 
      
 44 
     | 
    
         
            +
                      flow_rate_lpm = (pulses / duration / @calibration_factor * 60.0).round(2)
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                      {
         
     | 
| 
      
 47 
     | 
    
         
            +
                        flow_rate_lpm: flow_rate_lpm,
         
     | 
| 
      
 48 
     | 
    
         
            +
                        pulses: pulses
         
     | 
| 
      
 49 
     | 
    
         
            +
                      }
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    private
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                    # Count rising edge transitions on the pin over the specified duration.
         
     | 
| 
      
 55 
     | 
    
         
            +
                    # This is a simplified implementation - production code should use:
         
     | 
| 
      
 56 
     | 
    
         
            +
                    # - Hardware interrupts (kernel module or pigpio daemon)
         
     | 
| 
      
 57 
     | 
    
         
            +
                    # - Edge detection via sysfs GPIO (epoll)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    # - High-priority thread for accurate timing
         
     | 
| 
      
 59 
     | 
    
         
            +
                    def count_pulses(pin_label, duration)
         
     | 
| 
      
 60 
     | 
    
         
            +
                      pulses = 0
         
     | 
| 
      
 61 
     | 
    
         
            +
                      last_state = @gpio.read(pin_label)
         
     | 
| 
      
 62 
     | 
    
         
            +
                      start_time = Time.now
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                      # Poll GPIO at high frequency to detect edges
         
     | 
| 
      
 65 
     | 
    
         
            +
                      # Note: This is CPU-intensive and timing-dependent
         
     | 
| 
      
 66 
     | 
    
         
            +
                      # For production, use hardware interrupts or kernel GPIO edge detection
         
     | 
| 
      
 67 
     | 
    
         
            +
                      while Time.now - start_time < duration
         
     | 
| 
      
 68 
     | 
    
         
            +
                        current_state = @gpio.read(pin_label)
         
     | 
| 
      
 69 
     | 
    
         
            +
                        
         
     | 
| 
      
 70 
     | 
    
         
            +
                        # Detect rising edge (0 -> 1 transition)
         
     | 
| 
      
 71 
     | 
    
         
            +
                        if current_state == 1 && last_state == 0
         
     | 
| 
      
 72 
     | 
    
         
            +
                          pulses += 1
         
     | 
| 
      
 73 
     | 
    
         
            +
                        end
         
     | 
| 
      
 74 
     | 
    
         
            +
                        
         
     | 
| 
      
 75 
     | 
    
         
            +
                        last_state = current_state
         
     | 
| 
      
 76 
     | 
    
         
            +
                        
         
     | 
| 
      
 77 
     | 
    
         
            +
                        # Small sleep to reduce CPU usage
         
     | 
| 
      
 78 
     | 
    
         
            +
                        # Trade-off: Higher sleep = lower CPU, but may miss pulses
         
     | 
| 
      
 79 
     | 
    
         
            +
                        # For accurate counting, use interrupts instead
         
     | 
| 
      
 80 
     | 
    
         
            +
                        sleep(0.0001) # 0.1ms
         
     | 
| 
      
 81 
     | 
    
         
            +
                      end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                      pulses
         
     | 
| 
      
 84 
     | 
    
         
            +
                    end
         
     | 
| 
      
 85 
     | 
    
         
            +
                  end
         
     | 
| 
      
 86 
     | 
    
         
            +
                end
         
     | 
| 
      
 87 
     | 
    
         
            +
              end
         
     | 
| 
      
 88 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/dredger/iot/sensors.rb
    CHANGED
    
    | 
         @@ -17,3 +17,11 @@ require_relative 'sensors/tsl2561' 
     | 
|
| 
       17 
17 
     | 
    
         
             
            require_relative 'sensors/tsl2561_provider'
         
     | 
| 
       18 
18 
     | 
    
         
             
            require_relative 'sensors/ina219'
         
     | 
| 
       19 
19 
     | 
    
         
             
            require_relative 'sensors/ina219_provider'
         
     | 
| 
      
 20 
     | 
    
         
            +
            require_relative 'sensors/adxl345'
         
     | 
| 
      
 21 
     | 
    
         
            +
            require_relative 'sensors/adxl345_provider'
         
     | 
| 
      
 22 
     | 
    
         
            +
            require_relative 'sensors/scd30'
         
     | 
| 
      
 23 
     | 
    
         
            +
            require_relative 'sensors/scd30_provider'
         
     | 
| 
      
 24 
     | 
    
         
            +
            require_relative 'sensors/yf_s201'
         
     | 
| 
      
 25 
     | 
    
         
            +
            require_relative 'sensors/yf_s201_provider'
         
     | 
| 
      
 26 
     | 
    
         
            +
            require_relative 'sensors/neo6m'
         
     | 
| 
      
 27 
     | 
    
         
            +
            require_relative 'sensors/neo6m_provider'
         
     | 
    
        data/lib/dredger/iot/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: dredger-iot
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.3.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - The Mad Botter INC
         
     | 
| 
         @@ -49,9 +49,12 @@ files: 
     | 
|
| 
       49 
49 
     | 
    
         
             
            - lib/dredger/iot/bus/i2c_linux.rb
         
     | 
| 
       50 
50 
     | 
    
         
             
            - lib/dredger/iot/pins.rb
         
     | 
| 
       51 
51 
     | 
    
         
             
            - lib/dredger/iot/pins/beaglebone.rb
         
     | 
| 
      
 52 
     | 
    
         
            +
            - lib/dredger/iot/pins/raspberry_pi.rb
         
     | 
| 
       52 
53 
     | 
    
         
             
            - lib/dredger/iot/reading.rb
         
     | 
| 
       53 
54 
     | 
    
         
             
            - lib/dredger/iot/scheduler.rb
         
     | 
| 
       54 
55 
     | 
    
         
             
            - lib/dredger/iot/sensors.rb
         
     | 
| 
      
 56 
     | 
    
         
            +
            - lib/dredger/iot/sensors/adxl345.rb
         
     | 
| 
      
 57 
     | 
    
         
            +
            - lib/dredger/iot/sensors/adxl345_provider.rb
         
     | 
| 
       55 
58 
     | 
    
         
             
            - lib/dredger/iot/sensors/base_sensor.rb
         
     | 
| 
       56 
59 
     | 
    
         
             
            - lib/dredger/iot/sensors/bh1750.rb
         
     | 
| 
       57 
60 
     | 
    
         
             
            - lib/dredger/iot/sensors/bh1750_provider.rb
         
     | 
| 
         @@ -65,10 +68,16 @@ files: 
     | 
|
| 
       65 
68 
     | 
    
         
             
            - lib/dredger/iot/sensors/ina219.rb
         
     | 
| 
       66 
69 
     | 
    
         
             
            - lib/dredger/iot/sensors/ina219_provider.rb
         
     | 
| 
       67 
70 
     | 
    
         
             
            - lib/dredger/iot/sensors/mcp9808.rb
         
     | 
| 
      
 71 
     | 
    
         
            +
            - lib/dredger/iot/sensors/neo6m.rb
         
     | 
| 
      
 72 
     | 
    
         
            +
            - lib/dredger/iot/sensors/neo6m_provider.rb
         
     | 
| 
      
 73 
     | 
    
         
            +
            - lib/dredger/iot/sensors/scd30.rb
         
     | 
| 
      
 74 
     | 
    
         
            +
            - lib/dredger/iot/sensors/scd30_provider.rb
         
     | 
| 
       68 
75 
     | 
    
         
             
            - lib/dredger/iot/sensors/sht31.rb
         
     | 
| 
       69 
76 
     | 
    
         
             
            - lib/dredger/iot/sensors/sht31_provider.rb
         
     | 
| 
       70 
77 
     | 
    
         
             
            - lib/dredger/iot/sensors/tsl2561.rb
         
     | 
| 
       71 
78 
     | 
    
         
             
            - lib/dredger/iot/sensors/tsl2561_provider.rb
         
     | 
| 
      
 79 
     | 
    
         
            +
            - lib/dredger/iot/sensors/yf_s201.rb
         
     | 
| 
      
 80 
     | 
    
         
            +
            - lib/dredger/iot/sensors/yf_s201_provider.rb
         
     | 
| 
       72 
81 
     | 
    
         
             
            - lib/dredger/iot/version.rb
         
     | 
| 
       73 
82 
     | 
    
         
             
            homepage: https://github.com/TheMadBotterINC/dredger-iot
         
     | 
| 
       74 
83 
     | 
    
         
             
            licenses:
         
     | 
| 
         @@ -97,7 +106,7 @@ post_install_message: "\n    ═════════════════ 
     | 
|
| 
       97 
106 
     | 
    
         
             
              \  |                    \n              |_____________________________|                    \n
         
     | 
| 
       98 
107 
     | 
    
         
             
              \        ~~~       \\                 //          ~~~            \n                    \\_______________//
         
     | 
| 
       99 
108 
     | 
    
         
             
              \                         \n    ═══════════════════════════════════════════════════════════════════\nHardware
         
     | 
| 
       100 
     | 
    
         
            -
              Integration for Embedded Linux v0. 
     | 
| 
      
 109 
     | 
    
         
            +
              Integration for Embedded Linux v0.3.0\n    ═══════════════════════════════════════════════════════════════════\n\n
         
     | 
| 
       101 
110 
     | 
    
         
             
              \   \U0001F389 Thanks for installing!\n\n    \U0001F4DA Hardware Setup (kernel modules
         
     | 
| 
       102 
111 
     | 
    
         
             
              & permissions):\n       https://github.com/TheMadBotterINC/dredger-iot#hardware-setup\n\n
         
     | 
| 
       103 
112 
     | 
    
         
             
              \   \U0001F680 Quick Start:\n       require 'dredger/iot'\n       gpio = Dredger::IoT::Bus::Auto.gpio\n
         
     |