Skip to content
  • MySensors
  • OpenHardware.io
  • Categories
  • Recent
  • Tags
  • Popular
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
Brand Logo
  1. Home
  2. Troubleshooting
  3. Weather station

Weather station

Scheduled Pinned Locked Moved Troubleshooting
12 Posts 3 Posters 5.7k Views 2 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • HeinzH Offline
    HeinzH Offline
    Heinz
    Hero Member
    wrote on last edited by
    #2

    Wiring is shown here
    http://www.mysensors.org/build/pressure

    The child ids of BARO and LIGHT are both 2 in your sketch.

    N 1 Reply Last reply
    0
    • HeinzH Offline
      HeinzH Offline
      Heinz
      Hero Member
      wrote on last edited by
      #3

      The BMP085 and the DHT have both a temperature: lastTemp of both sketches collide!
      Have a look at this sketch which is a merge of a BMP085 and a DHT
      https://github.com/windkh/mysensors/blob/master/WeatherStationSensor/WeatherStationSensor.ino

      1 Reply Last reply
      0
      • HeinzH Heinz

        Wiring is shown here
        http://www.mysensors.org/build/pressure

        The child ids of BARO and LIGHT are both 2 in your sketch.

        N Offline
        N Offline
        NBM
        wrote on last edited by
        #4

        @Heinz I know. But is it possible to use A2 and A3?

        HeinzH 1 Reply Last reply
        0
        • N NBM

          @Heinz I know. But is it possible to use A2 and A3?

          HeinzH Offline
          HeinzH Offline
          Heinz
          Hero Member
          wrote on last edited by
          #5

          @NBM said:

          @Heinz I know. But is it possible to use A2 and A3?

          I don't know.

          1 Reply Last reply
          0
          • HeinzH Offline
            HeinzH Offline
            Heinz
            Hero Member
            wrote on last edited by Heinz
            #6

            A function is missing in your case:

            void incomingMessage(const MyMessage &message){}
            
            N D 2 Replies Last reply
            0
            • HeinzH Heinz

              A function is missing in your case:

              void incomingMessage(const MyMessage &message){}
              
              N Offline
              N Offline
              NBM
              wrote on last edited by
              #7

              @Heinz Thank you.

              I have made this sketch without errors.

              // DHT-Sensor
              // BMP085 Sensor
              
              #include <SPI.h>
              #include <MySensor.h>
              #include <Wire.h>
              #include <Adafruit_BMP085_U.h>
              #include <Adafruit_Sensor.h>
              #include <DHT.h>  
              
              
              const bool sendAlways = true;
              
              const float SEALEVEL = 688; // Westendorf
              const float SEALEVEL_PRESSURE = 1013.25;
              
              // ----------------------------------------------------------------------------
              #define CHILD_ID_HUM 2
              #define CHILD_ID_TEMP 3
              #define HUMIDITY_SENSOR_DIGITAL_PIN 3
              #define CHILD_ID_LIGHT 1
              #define LIGHT_SENSOR_ANALOG_PIN 1
              
              DHT dht;
              float lastTemp;
              float lastHum;
              boolean metric = true;
              MyMessage msgHum(CHILD_ID_HUM, V_HUM);
              MyMessage msgTemp(CHILD_ID_TEMP, V_TEMP);
              
              
              // ----------------------------------------------------------------------------
              #define BARO_CHILD 0
              #define TEMP_CHILD 1
              
              unsigned long SLEEP_TIME = 30000; // Sleep time between reads (in milliseconds)
              unsigned long SLEEP_TIME1 = 30000; // 1 minute required for forecast algorithm
              unsigned long SLEEP_TIME2 = 30000; // 1 minute required for forecast algorithm
              unsigned long SLEEP_TIME3 = 900000;  // sleep time between reads (seconds * 1000 milliseconds)
              
              
              Adafruit_BMP085_Unified bmp = Adafruit_BMP085_Unified(1);    // Digital Pressure Sensor 
              MySensor gw;
              
              MyMessage msg(CHILD_ID_LIGHT, V_LIGHT_LEVEL);
              int lastLightLevel;
              int BATTERY_SENSE_PIN = A0;  // select the input pin for the battery sense point
              int oldBatteryPcnt = 0;
              
              /*
              DP/Dt explanation
              0 = "Stable Weather Pattern"
              1 = "Slowly rising Good Weather", "Clear/Sunny "
              2 = "Slowly falling L-Pressure ", "Cloudy/Rain "
              3 = "Quickly rising H-Press",     "Not Stable"
              4 = "Quickly falling L-Press",    "Thunderstorm"
              5 = "Unknown (More Time needed)
              */
              
              const char *weatherStrings[] = { "stable", "sunny", "cloudy", "unstable", "thunderstorm", "unknown" };
              enum FORECAST
              {
                STABLE = 0,     // Stable weather
                SUNNY = 1,      // Slowly rising HP stable good weather
                CLOUDY = 2,     // Slowly falling Low Pressure System, stable rainy weather
                UNSTABLE = 3,   // Quickly rising HP, not stable weather
                THUNDERSTORM = 4, // Quickly falling LP, Thunderstorm, not stable
                UNKNOWN = 5     // Unknown, more time needed
              };
              
              const char *situationStrings[] = { "very low", "low", "normal", "high", "very high" };
              enum WEATHER_SITUATION
              {
                VERY_LOW_PRESSURE = 0,    // Tiefdruck p>-7.5hPa
                LOW_PRESSURE = 1,     // Tiefdruck p>-2.5hPa
                NORMAL_PRESSURE = 2,    // Normal  p <+/-02.5hPa  
                HIGH_PRESSURE = 3,      // Hochdruck p>2.5hPa
                VERY_HIGH_PRESSURE = 4,   // Hochdruck p>7.5hPa
              };
              
              float lastPressure = -1;
              float lastPressureTemp = -1;
              int lastForecast = -1;
              int lastSituation = NORMAL_PRESSURE;
              
              const int LAST_SAMPLES_COUNT = 5;
              float lastPressureSamples[LAST_SAMPLES_COUNT];
              
              // get kPa/h be dividing hPa by 10 
              #define CONVERSION_FACTOR (1.0/10.0)
              
              int minuteCount = 0;
              bool firstRound = true;
              
              // average value is used in forecast algorithm.
              float pressureAvg;
              // average after 2 hours is used as reference value for the next iteration.
              float pressureAvg2;
              float dP_dt;
              
              MyMessage tempMsg(TEMP_CHILD, V_TEMP);
              MyMessage pressureMsg(BARO_CHILD, V_PRESSURE);
              MyMessage forecastMsg(BARO_CHILD, V_FORECAST);
              MyMessage situationMsg(BARO_CHILD, V_VAR1);
              MyMessage forecastMsg2(BARO_CHILD, V_VAR2);
              
              void displaySensorDetails(void)
              {
                sensor_t sensor;
                bmp.getSensor(&sensor);
                Serial.println(F("------------------------------------"));
                Serial.print(F("Sensor:       ")); Serial.println(sensor.name);
                Serial.print(F("Driver Ver:   ")); Serial.println(sensor.version);
                Serial.print(F("Unique ID:    ")); Serial.println(sensor.sensor_id);
                Serial.print(F("Max Value:    ")); Serial.print(sensor.max_value); Serial.println(F(" hPa"));
                Serial.print(F("Min Value:    ")); Serial.print(sensor.min_value); Serial.println(F(" hPa"));
                Serial.print(F("Resolution:   ")); Serial.print(sensor.resolution); Serial.println(F(" hPa"));
                Serial.println(F("------------------------------------"));
                Serial.println(F(""));
                delay(500);
              }
              
              void initPressureSensor()
              {
                if (!bmp.begin())
                {
                  Serial.println(F("Could not find a valid BMP085 sensor, check wiring!"));
                  while (1) {}
                }
              
                displaySensorDetails();
              }
              
              void initHumiditySensor()
              {
                dht.setup(HUMIDITY_SENSOR_DIGITAL_PIN);
              }
              
              void setup() 
              {
                   // use the 1.1 V internal reference
                #if defined(__AVR_ATmega2560__)
                 analogReference(INTERNAL1V1);
                #else
                 analogReference(INTERNAL);
                #endif
                gw.begin();
                gw.sendSketchInfo("Weather Station Sensor", "2.1");
              
                initPressureSensor();
                initHumiditySensor();
              
                gw.present(BARO_CHILD, S_BARO);
                gw.present(TEMP_CHILD, S_TEMP);
                gw.present(CHILD_ID_HUM, S_HUM);
                gw.present(CHILD_ID_TEMP, S_TEMP);
                gw.present(CHILD_ID_LIGHT, S_LIGHT_LEVEL);
              
                metric = gw.getConfig().isMetric;
              
               int lightLevel = (1023-analogRead(LIGHT_SENSOR_ANALOG_PIN))/10.23; 
                Serial.println(lightLevel);
                if (lightLevel != lastLightLevel) {
                    gw.send(msg.set(lightLevel));
                    lastLightLevel = lightLevel;
                }
                 gw.sleep(SLEEP_TIME);  
              }
              
              int getWeatherSituation(float pressure)
              {
                int situation = NORMAL_PRESSURE;
              
                float delta = pressure - SEALEVEL_PRESSURE;
                if (delta > 7.5)
                {
                  situation = VERY_HIGH_PRESSURE;
                }
                else if (delta > 2.5)
                {
                  situation = HIGH_PRESSURE;
                }
                else if (delta < -7.5)
                {
                  situation = VERY_LOW_PRESSURE;
                }
                else if (delta < -2.5)
                {
                  situation = LOW_PRESSURE;
                }
                else
                {
                  situation = NORMAL_PRESSURE;
                }
              
                return situation;
              }
              
              bool updatePressureSensor()
              {
                bool changed = false;
              
                sensors_event_t event;
                bmp.getEvent(&event);
              
                if (event.pressure)
                {
                  float absolutePressure = event.pressure;
                  Serial.print(F("abs Pressure = "));
                  Serial.print(absolutePressure);
                  Serial.println(F(" hPa"));
                  
                  //float altitude = bmp.pressureToAltitude(SEALEVEL_PRESSURE, pressure);
                  //Serial.print(F("Altitude = "));
                  //Serial.print(altitude);
                  //Serial.println(F(" m"));
              
                  //sealevel pressure p0 from absolute pressure.
                  float pressure = bmp.seaLevelForAltitude(SEALEVEL, absolutePressure);
              
                  float pressureTemperature;
                  bmp.getTemperature(&pressureTemperature);
              
                  if (!metric) 
                  {
                    // Convert to fahrenheit
                    pressureTemperature = pressureTemperature * 9.0 / 5.0 + 32.0;
                  }
              
                  int forecast = sample(pressure);
                  int situation = getWeatherSituation(pressure);
              
              
                  if (sendAlways || (pressureTemperature != lastPressureTemp))
                  {
                    changed = true;
                    lastPressureTemp = pressureTemperature;
                    Serial.print(F("Temperature = "));
                    Serial.print(pressureTemperature);
                    Serial.println(metric ? F(" *C") : F(" *F"));
                    if (!gw.send(tempMsg.set(lastPressureTemp, 1)))
                    {
                      lastPressureTemp = -1.0;
                    }
                  }
              
              
                  if (sendAlways || (pressure != lastPressure))
                  {
                    changed = true;
                    lastPressure = pressure;
                    Serial.print(F("sealevel Pressure = "));
                    Serial.print(pressure);
                    Serial.println(F(" hPa"));
                    if (!gw.send(pressureMsg.set(lastPressure, 1)))
                    {
                      lastPressure = -1.0;
                    }
                  }
              
              
                  if (sendAlways || (forecast != lastForecast))
                  {
                    changed = true;
                    lastForecast = forecast;
                    Serial.print(F("Forecast = "));
                    Serial.println(weatherStrings[forecast]);
                    if (gw.send(forecastMsg.set(weatherStrings[lastForecast])))
                    {
                      if (!gw.send(forecastMsg2.set(lastForecast)))
                      {
                      }
                    }
                    else
                    {
                      lastForecast = -1.0;
                    }
                  }
              
              
                  if (sendAlways || (situation != lastSituation))
                  {
                    changed = true;
                    lastSituation = situation;
                    Serial.print(F("Situation = "));
                    Serial.println(situationStrings[situation]);
                    if (!gw.send(situationMsg.set(lastSituation, 0)))
                    {
                      lastSituation = -1.0;
                    }
                  }
                }
              
                return changed;
              }
              
              bool updateHumiditySensor()
              {
                bool changed = false;
              
                float temperature = dht.getTemperature();
                float humidity = dht.getHumidity();
              
                if (!isnan(temperature))
                {
              #ifdef SEND_WHEN_CHANGED
                  if (temperature != lastTemp)
              #endif
                  {
                    lastTemp = temperature;
                    if (!metric)
                    {
                      temperature = dht.toFahrenheit(temperature);
                    }
                    changed = true;
                    Serial.print(F("T: "));
                    Serial.println(temperature);
                    if (!gw.send(msgTemp.set(temperature, 1)))
                    {
                      lastTemp = -1.0;
                    }
                  }
                }
                else
                {
                  Serial.println(F("Failed reading temperature from DHT"));
                }
                
                
                if (!isnan(humidity))
                {
              #ifdef SEND_WHEN_CHANGED
                  if (humidity != lastHum)
              #endif
                  {
                    lastHum = humidity;
                    changed = true;
                    Serial.print(F("H: "));
                    Serial.println(humidity);
                    if (!gw.send(msgHum.set(lastHum, 1)))
                    {
                      lastHum = -1.0;
                    }
                  }
                }
                else
                {
                  Serial.println(F("Failed reading humidity from DHT"));
                }
                
                return changed;
              }
              
              void loop() 
              {
                 // get the battery Voltage
                 int sensorValue = analogRead(BATTERY_SENSE_PIN);
                 #ifdef DEBUG
                 Serial.println(sensorValue);
                 #endif
                 
                 // 1M, 470K divider across battery and using internal ADC ref of 1.1V
                 // Sense point is bypassed with 0.1 uF cap to reduce noise at that point
                 // ((1e6+470e3)/470e3)*1.1 = Vmax = 3.44 Volts
                 // 3.44/1023 = Volts per bit = 0.003363075
                 float batteryV  = sensorValue * 0.003363075;
                 int batteryPcnt = sensorValue / 10;
              
                 #ifdef DEBUG
                 Serial.print("Battery Voltage: ");
                 Serial.print(batteryV);
                 Serial.println(" V");
              
                 Serial.print("Battery percent: ");
                 Serial.print(batteryPcnt);
                 Serial.println(" %");
                 #endif
              
                 if (oldBatteryPcnt != batteryPcnt) {
                   // Power up radio after sleep
                   gw.sendBatteryLevel(batteryPcnt);
                   oldBatteryPcnt = batteryPcnt;
                 }
                updatePressureSensor();
                gw.sleep(SLEEP_TIME1);
                updateHumiditySensor();
                gw.sleep(SLEEP_TIME2);
              
                gw.sleep(SLEEP_TIME3);
              }
              
              
              float getLastPressureSamplesAverage()
              {
                float lastPressureSamplesAverage = 0;
                for (int i = 0; i < LAST_SAMPLES_COUNT; i++)
                {
                  lastPressureSamplesAverage += lastPressureSamples[i];
                }
                lastPressureSamplesAverage /= LAST_SAMPLES_COUNT;
              
                //Serial.print(F("### 5min-Average:"));
                //Serial.print(lastPressureSamplesAverage);
                //Serial.println(F(" hPa"));
              
                return lastPressureSamplesAverage;
              }
              
              
              
              // Algorithm found here
              // http://www.freescale.com/files/sensors/doc/app_note/AN3914.pdf
              // Pressure in hPa -->  forecast done by calculating kPa/h
              int sample(float pressure)
              {
                // Calculate the average of the last n minutes.
                int index = minuteCount % LAST_SAMPLES_COUNT;
                lastPressureSamples[index] = pressure;
              
                minuteCount++;
                if (minuteCount > 185)
                {
                  minuteCount = 6;
                }
              
                if (minuteCount == 5)
                {
                  pressureAvg = getLastPressureSamplesAverage();
                }
                else if (minuteCount == 35)
                {
                  float lastPressureAvg = getLastPressureSamplesAverage();
                  float change = (lastPressureAvg - pressureAvg) * CONVERSION_FACTOR;
                  if (firstRound) // first time initial 3 hour
                  {
                    dP_dt = change * 2; // note this is for t = 0.5hour
                  }
                  else
                  {
                    dP_dt = change / 1.5; // divide by 1.5 as this is the difference in time from 0 value.
                  }
                }
                else if (minuteCount == 65)
                {
                  float lastPressureAvg = getLastPressureSamplesAverage();
                  float change = (lastPressureAvg - pressureAvg) * CONVERSION_FACTOR;
                  if (firstRound) //first time initial 3 hour
                  {
                    dP_dt = change; //note this is for t = 1 hour
                  }
                  else
                  {
                    dP_dt = change / 2; //divide by 2 as this is the difference in time from 0 value
                  }
                }
                else if (minuteCount == 95)
                {
                  float lastPressureAvg = getLastPressureSamplesAverage();
                  float change = (lastPressureAvg - pressureAvg) * CONVERSION_FACTOR;
                  if (firstRound) // first time initial 3 hour
                  {
                    dP_dt = change / 1.5; // note this is for t = 1.5 hour
                  }
                  else
                  {
                    dP_dt = change / 2.5; // divide by 2.5 as this is the difference in time from 0 value
                  }
                }
                else if (minuteCount == 125)
                {
                  float lastPressureAvg = getLastPressureSamplesAverage();
                  pressureAvg2 = lastPressureAvg; // store for later use.
                  float change = (lastPressureAvg - pressureAvg) * CONVERSION_FACTOR;
                  if (firstRound) // first time initial 3 hour
                  {
                    dP_dt = change / 2; // note this is for t = 2 hour
                  }
                  else
                  {
                    dP_dt = change / 3; // divide by 3 as this is the difference in time from 0 value
                  }
                }
                else if (minuteCount == 155)
                {
                  float lastPressureAvg = getLastPressureSamplesAverage();
                  float change = (lastPressureAvg - pressureAvg) * CONVERSION_FACTOR;
                  if (firstRound) // first time initial 3 hour
                  {
                    dP_dt = change / 2.5; // note this is for t = 2.5 hour
                  }
                  else
                  {
                    dP_dt = change / 3.5; // divide by 3.5 as this is the difference in time from 0 value
                  }
                }
                else if (minuteCount == 185)
                {
                  float lastPressureAvg = getLastPressureSamplesAverage();
                  float change = (lastPressureAvg - pressureAvg) * CONVERSION_FACTOR;
                  if (firstRound) // first time initial 3 hour
                  {
                    dP_dt = change / 3; // note this is for t = 3 hour
                  }
                  else
                  {
                    dP_dt = change / 4; // divide by 4 as this is the difference in time from 0 value
                  }
                  pressureAvg = pressureAvg2; // Equating the pressure at 0 to the pressure at 2 hour after 3 hours have past.
                  firstRound = false; // flag to let you know that this is on the past 3 hour mark. Initialized to 0 outside main loop.
                }
              
                int forecast = UNKNOWN;
                if (minuteCount < 35 && firstRound) //if time is less than 35 min on the first 3 hour interval.
                {
                  forecast = UNKNOWN;
                }
                else if (dP_dt < (-0.25))
                {
                  forecast = THUNDERSTORM;
                }
                else if (dP_dt > 0.25)
                {
                  forecast = UNSTABLE;
                }
                else if ((dP_dt > (-0.25)) && (dP_dt < (-0.05)))
                {
                  forecast = CLOUDY;
                }
                else if ((dP_dt > 0.05) && (dP_dt < 0.25))
                {
                  forecast = SUNNY;
                }
                else if ((dP_dt >(-0.05)) && (dP_dt < 0.05))
                {
                  forecast = STABLE;
                }
                else
                {
                  forecast = UNKNOWN;
                }
              
                Serial.print(F("Forecast at minute "));
                Serial.print(minuteCount);
                Serial.print(F(" dP/dt = "));
                Serial.print(dP_dt);
                Serial.print(F("kPa/h --> "));  
                Serial.println(weatherStrings[forecast]);
              
                return forecast;
              }```
              1 Reply Last reply
              1
              • HeinzH Heinz

                A function is missing in your case:

                void incomingMessage(const MyMessage &message){}
                
                D Offline
                D Offline
                Dylano
                wrote on last edited by
                #8

                @Heinz

                Is this sketch working great now?
                And how is the battery?
                How long can work this sketch, on what type battery?

                Domoticz, with a lot of working hardware, include mysensors :-)
                OpenPLI, RuneAudio, Solarmeter, etc......

                Not a great builder of software and hardware, more a user...
                Only i try to do my best :-(

                1 Reply Last reply
                0
                • HeinzH Offline
                  HeinzH Offline
                  Heinz
                  Hero Member
                  wrote on last edited by
                  #9

                  @Dylano the sketch in this thread is from @NBM, if you mean
                  https://github.com/windkh/mysensors/blob/master/WeatherStationSensor/WeatherStationSensor.ino
                  then this one is working very well.

                  This one is the official mysensors example
                  https://github.com/mysensors/Arduino/blob/development/libraries/MySensors/examples/WeatherStationSensor/WeatherStationSensor.ino

                  1 Reply Last reply
                  0
                  • D Offline
                    D Offline
                    Dylano
                    wrote on last edited by
                    #10

                    still got an error:
                    C:\Users\DM_PC\Desktop\Mysensors 23-2-2015\Sketches Werkend\sketch_mar05a\sketch_mar05a.ino:8:29: fatal error: Adafruit_Sensor.h: No such file or directory

                    #include <Adafruit_Sensor.h>

                    Only where can i download the adafruit library?

                    Domoticz, with a lot of working hardware, include mysensors :-)
                    OpenPLI, RuneAudio, Solarmeter, etc......

                    Not a great builder of software and hardware, more a user...
                    Only i try to do my best :-(

                    HeinzH 1 Reply Last reply
                    0
                    • D Dylano

                      still got an error:
                      C:\Users\DM_PC\Desktop\Mysensors 23-2-2015\Sketches Werkend\sketch_mar05a\sketch_mar05a.ino:8:29: fatal error: Adafruit_Sensor.h: No such file or directory

                      #include <Adafruit_Sensor.h>

                      Only where can i download the adafruit library?

                      HeinzH Offline
                      HeinzH Offline
                      Heinz
                      Hero Member
                      wrote on last edited by
                      #11

                      @Dylano

                      https://github.com/adafruit/Adafruit_Sensor
                      https://github.com/adafruit/Adafruit_BMP085_Unified

                      D 1 Reply Last reply
                      0
                      • HeinzH Heinz

                        @Dylano

                        https://github.com/adafruit/Adafruit_Sensor
                        https://github.com/adafruit/Adafruit_BMP085_Unified

                        D Offline
                        D Offline
                        Dylano
                        wrote on last edited by
                        #12

                        @Heinz said:

                        @Dylano

                        https://github.com/adafruit/Adafruit_Sensor
                        https://github.com/adafruit/Adafruit_BMP085_Unified

                        Thanks!!!

                        Domoticz, with a lot of working hardware, include mysensors :-)
                        OpenPLI, RuneAudio, Solarmeter, etc......

                        Not a great builder of software and hardware, more a user...
                        Only i try to do my best :-(

                        1 Reply Last reply
                        0

                        Hello! It looks like you're interested in this conversation, but you don't have an account yet.

                        Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.

                        With your input, this post could be even better 💗

                        Register Login
                        Reply
                        • Reply as topic
                        Log in to reply
                        • Oldest to Newest
                        • Newest to Oldest
                        • Most Votes


                        16

                        Online

                        12.0k

                        Users

                        11.2k

                        Topics

                        113.4k

                        Posts


                        Copyright 2025 TBD   |   Forum Guidelines   |   Privacy Policy   |   Terms of Service
                        • Login

                        • Don't have an account? Register

                        • Login or register to search.
                        • First post
                          Last post
                        0
                        • MySensors
                        • OpenHardware.io
                        • Categories
                        • Recent
                        • Tags
                        • Popular