Sitemap / Advertise

Introduction

Through WhatsApp, get variables(location, altitude, pressure...) from NodeMCU as requested or send commands to NodeMCU via Twilio's API.


Tags

Share

WhatsApp Mapping and Weather Forecast Chat Bot

Advertisement:


read_later

Read Later



read_later

Read Later

Introduction

Through WhatsApp, get variables(location, altitude, pressure...) from NodeMCU as requested or send commands to NodeMCU via Twilio's API.

Tags

Share





Advertisement

Advertisement




    Components :
  • [1]NodeMCU v3.0 [ESP-12E]
  • [1]NodeMCU(ESP8266) LoLin Base Shield
  • [1]GY-NEO6MV2 GPS Module
  • [1]BMP180 Pressure and Temperature Sensor
  • [1]L298N Motor Driver
  • [2]Wheel and DC Motor Kit
  • [1]Ball Caster
  • [1]Mini Breadboard
  • [1]Covered 8-AA Battery Holder
  • [1]Female/Male Jumper W.
  • [1]Male/Male Jumper W.

Description

For a few weeks, I have been working with Twilio’s API, especially for WhatsApp messaging, and even created a project sending the variables coming from NodeMCU to my phone, which you can find here. But, my former project was merely sending data packets to my phone as WhatsApp messages periodically. And, I was determined to take my nascent idea and improve it by using WhatsApp response messages and a MySQL database and thus created this project. In that way, NodeMCU transfers variables to the phone as WhatsApp messages when the user requested them or gets commands from the user through WhatsApp. To manage that, I created a web application in PHP, which detects whether a WhatsApp message was sent or not and response the message depending on the keyword in the body. The application, named WhatsApp Two-Way Connection Hub, saves all variables transferred from NodeMCU and commands received from WhatsApp to a dedicated MySQL database and therefore communicates the user and NodeMCU instantly when something requested. I shared the source code of the application below if you want to use the localhost or your server instead of TheAmplituhedron to create a connection hub, but first, you have to download Twilio Helper Library in your directory.

In this project, I wanted to make a remote mapping and weather forecast bot sending temperature, pressure, altitude, latitude, and longitude when requested as WhatsApp response messages and control the base by giving commands through WhatsApp. To gather data, I decided to use a BMP180 Pressure and Temperature Sensor, and a GY-NEO6MV2 GPS Module and to make the base remote and controllable, I used an L298N Motor Driver and two Wheel and DC Motor Kit along with a ball caster.

Also, you can view the location generated by the GPS module on Google Maps through WhatsApp.

First of all, download the required libraries for Arduino IDE below to continue the following steps.

Required Libraries:

For NodeMCU boards, click here.

For GY-NEO6MV2 GPS Module, click here.

For BMP180 Pressure and Temperature Sensor, click here.

project-image
Figure - 28.1

Twilio for WhatsApp

To send WhatsApp messages through Twilio's API, the only thing you need to do is to sign up for a Twilio Trial Account.

After signing up, with your SID and Auth Token, you can use Twilio API for WhatsApp without a charge. As explained on Twilio, you need to join a shared phone number with your phone in order to initiate the API and WhatsApp template messages.

Important: To be able to response WhatsApp messages coming from your verified phone, you must change the default endpoint URL of your Twilio application in the Sandbox with your connection hub path.

For more information and learn how to activate your account, click the link below.

Go to Twilio.

You can sign up to Twilio with my referral code here.

project-image
Figure - 28.2


project-image
Figure - 28.3


project-image
Figure - 28.4

How to create a connection hub to manage data transferring

For subscribers of my website, I programmed a web application,named WhatsApp Two-Way Connection Hub, which allows you to response WhatsApp messages coming from a verified phone number by a Twilio application in order to get variables from NodeMCU, or any other device that can make an HTTP Request, as WhatsApp messages and send commands to the device.

This application saves the data coming from the device temporarily into a dedicated MySQL server hence no need to call the current variables from the device every time the data requested by the user. To register variables from the device properly, you must send the variables through an HTTP Request to your unique connection hub path.

Important: To be able to use the application, you must change the default endpoint URL of your Twilio application in the Sandbox with your connection hub path generated with your hedron by TheAmplituhedron API.

When you enter a specific keyword , which you can see below for each data on your dedicated database, on WhatsApp, this application will response that message by sending the requested variable associated with the keyword to your phone or transferring commands to the device.

Data holders in the database: Data_1, Data_2, Data_3, Data_4, Data_5, Data_6, Command

Keywords(pre-defined and integrated):

For more information about this application and getting an auto-generated two-way connection hub, click the link below.

Go to WhatsApp Two-Way Connection Hub.

Reminder: WhatsApp Two-Way Connection Hub is open-source. If you do not want to use TheAmplituhedron instead of localhost or your server in your projects, you can download the source code below and thus specialize the keywords by creating your connection hub application on your server.

But, to manage that, you have to download Twilio PHP Helper Library at your directory from here.

project-image
Figure - 28.5


project-image
Figure - 28.6


project-image
Figure - 28.7


project-image
Figure - 28.8

Features

1) Collect date, time, latitude,and longitude information generated by a GY-NEO6MV2 GPS Module.

project-image
Figure - 28.9

2) Get temperature, pressure, and altitude from a BMP180 Pressure Sensor.

project-image
Figure - 28.10

3) Make an HTTP Get Request to transfer variables to the database and receive the command from the database as a response.

project-image
Figure - 28.11

4) Depending on the command, control an L298N Motor Driver.

project-image
Figure - 28.12

5) Monitor all processes on the serial monitor.

project-image
Figure - 28.13

6) Elicit response messages to display variables saved to the database as requested through WhatsApp or give commands to the device by sending the command on WhatsApp.

project-image
Figure - 28.14

Connections

NodeMCU GPIO pin connections are well-explained at the code down below. Make hardware connections as depicted on figures below.

I just fastened all components to an old cylindrical metal box by using a hot glue gun and solder some components with a solder gun.

Note: I used a covered 8-AA battery holder as an external power supply to feed the device and components because it is easily connected with the base shield.

project-image-slide project-image-slide project-image-slide project-image-slide project-image-slide
Slide




project-image-slide project-image-slide project-image-slide project-image-slide project-image-slide
Slide




project-image
Figure - 28.15

Videos

Serial Monitor Demonstration | WhatsApp Mapping and Weather Forecast Chat Bot

Using Keywords | WhatsApp Mapping and Weather Forecast Chat Bot

Field Test | WhatsApp Mapping and Weather Forecast Chat Bot

Code

Source Code(NodeMCU)

Download



          /////////////////////////////////////////////  
         //      WhatsApp Mapping and Weather       //
        //            Forecast Chat Bot            //
       //               (via Twilio)              //
      //           ---------------------         //
     //              NodeMCU (ESP-12E)          //           
    //              by Kutluhan Aktar          // 
   //                                         //
  /////////////////////////////////////////////

// By only subscribing to TheAmplituhedron, you will be able get your data packets from NodeMCU, or any other device that can make an HTTP GET Request, to your phone as WhatsApp messages via Twilio through TheAmplituhedron
// when you enter the keywords and control the base remotely as shown on the project tutorial page.
// You will need a Twilio account to get WhatsApp messages automatically from the two-way connection hub when you enter a keyword.
// Then, you can use your unique connection hub including the Twilio WhatsApp API by merely creating a Twilio Account and configuring your endpoint URL as your unique two-way
// connection hub path that you can find on your dashboard by cliking the implied web application.
// Note: Do not forget that you may have to take a tutorial if you do not know how to set an endpoints URls on the Twilio Sandbox.
// Note: You will also need your unique hedron to connect the WhatsApp Two-Way Connection Hub application.
//
// Click the links below for more information about Twilio.
// https://www.twilio.com/whatsapp
// https://www.theamplituhedron.com/dashboard/WhatsApp-Two-Way-Connection-Hub/
//
// As a reminder, my website has SSL protection so that you need to identify your NodeMCU connection by entering TheAmplituhedron FingerPrint or ThumbPrint.
// You can learn about it more from the link below.
// https://www.theamplituhedron.com/projects/WhatsApp-Mapping-and-Weather-Forecast-Chat-Bot/
//
// In this project turorial, I decided to use a BMP180 Pressure and Temperature Sensor, and a GY-NEO6MV2 GPS Module as data providers.
// And, to make my project base remote and controllable, I used an L298N Motor Driver and two Wheel and DC Motor Kit along with a Ball Caster.
//
// I created a free two-way connection hub for TheAmplituhedron subscribers and used it in this project. But, if you do not want to use this service,
// you can copy the source code from the link on the top and crete your own application.
// When you enter a keyword for a specific task, you can either get variables detected by NodeMCU or send commands to control wheels remotely.
// Reminder: WhatsApp Two-Way Connection Hub is created for this project and therefore all keywords are pre-defined by me but it is open-source so you can create your own application.
// KEYWORDS:
// Temperature -> Get Data_1
// Pressure -> Get Data_2
// Altitude -> Get Data_3
// Date -> Get Data_4
// Time -> Get Data_5
// Latitude and Longitude -> Get Data_6
// Map Location -> Get Google Maps link using GPS information
// Go Straight -> Send itself as a command to the device
// Go Back -> Send itself as a command to the device
// Go Right -> Send itself as a command to the device
// Go Left -> Send itself as a command to the device
// Halt -> Send itself as a command to the device
// Programmed by
// About
// How r u?
// Further information
// Contact
// Spidey -> Test media transferring
// Batman -> Test media transferring
// Help -> Print all keywords
//
// Connections
// NodeMCU (ESP-12E) :           
//                                GY-NEO6MV2 GPS Module
// D5  --------------------------- RX
// D4  --------------------------- TX
//                                BMP180 Pressure and Temperature Sensor
// D1  --------------------------- SCL
// D2  --------------------------- SDA
//                                L298N Motor Driver
// D3  --------------------------- IN_1
// D6  --------------------------- IN_2
// D7  --------------------------- IN_3
// D8  --------------------------- IN_4


// Include required libraries to send data to a website, in this case to TheAmplituhedron.com.
#include <ESP8266WiFi.h>
#include <WiFiClient.h> 
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>

// Include TinyGPS++ to get variables from the GY-NEO6MV2 GPS Module.
#include <TinyGPS++.h>
#include <SoftwareSerial.h>

#include <SFE_BMP180.h>
#include <Wire.h>

// You will need to create an SFE_BMP180 object, here called "pressure".
SFE_BMP180 pressure;

// Connect BMP180 SDA and SCL pins to the labeled pins below.
// On NodeMCU ESP8266 12-E:
// D1            SCL
// D2            SDA

// Define a baseline pressure (sea-level or other) in mb to predict altitude. 
#define p0 1013

// Define your WIFI settings.
const char *ssid = "Your_WiFi_SSID";
const char *password = "Your_WiFi_Password";

// Define required variables by TinyGPS++ library.
static const int RXPin = D4, TXPin = D5;
static const uint32_t GPSBaud = 9600;

// Create data holders to send data packets.
String fingerPrint, connectionHub, HEDRON, DATA, Command;
String latitude, longitude, date, month, day, year, _time, hour, minute, second, centisecond;
double Temperature, Pressure, Altitude;

// Define the output pins for the L298N Motor Driver
#define IN_1 D3
#define IN_2 D6
#define IN_3 D7
#define IN_4 D8

// The TinyGPS++ object
TinyGPSPlus gps;

// The serial connection to the GPS device
SoftwareSerial ss(RXPin, TXPin);

void setup() {
  
  Serial.begin(115200);

  pinMode(IN_1, OUTPUT);
  pinMode(IN_2, OUTPUT);
  pinMode(IN_3, OUTPUT);
  pinMode(IN_4, OUTPUT);
  
  // Initiate the GPS Module.
  ss.begin(GPSBaud);
  // Initialize the sensor (it is important to get calibration values stored on the device).
  pressure.begin();
  
  // It is just for assuring that if the connection is alive.
  WiFi.mode(WIFI_OFF);
  delay(1000);
  // This mode allows NodeMCU to connect any WiFi directly.
  WiFi.mode(WIFI_STA);        
  // Connect NodeMCU to your WiFi.
  WiFi.begin(ssid, password);
  
  Serial.print("\n\n");
  Serial.print("Try to connect to WiFi. Please wait! ");
  Serial.print("\n\n");
  // Halt the code until connected to WiFi.
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print("*");
  }
 
  // If connection is successful, write WiFi SSID to serial monitor along with assigned IPAddress.
  Serial.print("\n\n");
  Serial.print("-------------------------------------");
  Serial.print("\n\n");
  Serial.print("Connection is successful!");
  Serial.print("\n\n");
  Serial.print("Connected WiFi SSID : ");
  Serial.print(ssid);
  Serial.print("\n\n");
  Serial.println("Connected IPAddress : ");
  //Serial.println(WiFi.localIP());
  Serial.print("\n\n");
  
  // Give time to ESP8266 for rebooting properly.
  delay(3000);
  
}

void loop() {
  
  // This sketch displays information every time a new sentence is correctly encoded on the GPS Module.
    while (ss.available() > 0)
    if (gps.encode(ss.read()))
      get_GPS_Data();
  // Check whether the GPS connection is alive or not.
  if (millis() > 5000 && gps.charsProcessed() < 10) { Serial.println("No GPS detected: check wiring."); while(true); }

  // Get temperature, pressure, and altitude from the BMP180 Pressure Sensor.
  get_temperature_pressure_altitude();

  // Send variables to WhatsApp Two-Way Connection Hub to be saved to MySQL Database.
  send_variables_to_server_and_get_command();
  // Activate features by commands.
  activate_commands();
  
}

void get_GPS_Data(){
  Serial.print("\nGathering information from the GPS Module...\n------------------------------\n");
  if(gps.location.isValid()){
      longitude = String(gps.location.lng(), 6);
      latitude = String(gps.location.lat(), 6);
      Serial.print("Location: " + latitude + " , " + longitude + "\n");
  }else{
     longitude = "NotDetected";
     latitude = "NotDetected";
     Serial.print("Location: Not detected...\n");
  }

    if (gps.date.isValid()){
    month = String(gps.date.month());
    day = String(gps.date.day());
    year = String(gps.date.year());
    date = month + "/" + day + "/" + year;
    Serial.print("Date: " + date + "\n");
  }else{
    date = "Failed";
    Serial.print("Date: Failed...\n");
  }

  if (gps.time.isValid()){
    if (gps.time.hour() < 10){ hour = "0" + String(gps.time.hour()); }else{ hour = String(gps.time.hour());}
    if (gps.time.minute() < 10){ minute = "0" + String(gps.time.minute()); }else{ minute = String(gps.time.minute()); } 
    if (gps.time.second() < 10){ second = "0" + String(gps.time.second()); }else{ second = String(gps.time.second()); }
    if (gps.time.centisecond() < 10){ centisecond = "0" + String(gps.time.centisecond()); }else{ centisecond = String(gps.time.centisecond()); }
    _time = hour + ":" + minute + ":" + second + "." + centisecond;
    Serial.print("Time: " + _time + "\n");
  }
  else{
    _time = "Failed";
    Serial.print("Time: Failed...\n");
  }
}

void get_temperature_pressure_altitude(){
// You must first get a temperature measurement to perform a pressure reading.
  
// Start a temperature measurement:
// If request is successful, the number of ms to wait is returned.
// If request is unsuccessful, 0 is returned.

  char status = pressure.startTemperature();
  Serial.print("\nGathering information from the BMP180 Pressure Sensor...\n------------------------------\n");
  if (status != 0){
     // Wait for the measurement to complete:
     delay(status);
     // Retrieve the completed temperature measurement:
     // Note that the measurement is stored in the variable Temperature.
     // Function returns 1 if successful, 0 if failure.
     status = pressure.getTemperature(Temperature);
     if (status != 0){
      Serial.print("Temperature: " + String(Temperature) + " C\n");
      // Start a pressure measurement:
      // The parameter is the oversampling setting, from 0 to 3 (highest res, longest wait).
      // If request is successful, the number of ms to wait is returned.
      // If request is unsuccessful, 0 is returned.
      status = pressure.startPressure(0);
        if (status != 0){
          // Wait for the measurement to complete:
          delay(status);
          // Retrieve the completed pressure measurement:
          // Note that the measurement is stored in the variable Pressure.
          // Note also that the function requires the previous temperature measurement(Temperature).
          // (If temperature is stable, you can do one temperature measurement for a number of pressure measurements.)
          // Function returns 1 if successful, 0 if failure.
          status = pressure.getPressure(Pressure,Temperature);
            if (status != 0){
              Serial.print("Pressure: " + String(Pressure) + " mb\n");
              // Determine your altitude from the pressure reading,
              // use the altitude function along with a baseline pressure (sea-level or other).
              // Parameters: Pressure = absolute pressure in mb, p0 = baseline pressure in mb.
              // Result: a = altitude in m.
              Altitude = pressure.altitude(Pressure,p0);
              Serial.print("Altitude: " + String(Altitude) + " m\n");
              }else{
                Serial.print("Altitude: Failed...\n");
              }

          }else{
            Serial.print("Pressure: Failed...\n");
          }
          
      }else{
        Serial.print("Tempature: None...\n");
      }
  
   }else{
    Serial.print("Tempature: Failed...\n");
   }

   delay(1000);
   
 }

void send_variables_to_server_and_get_command(){
    // Enter the fingerprint.
    fingerPrint = "46 3c 5c 2c 67 11 cd 88 b7 e9 76 74 41 34 48 bd bc a5 b9 cf";

   // Enter your hedron provided by TheAmplituhedron to use the connection hub if you will use WhatsApp Two-Way Connection Hub.
    HEDRON = "Your_Hedron";

    DATA = "/?Data_1=" + String(Temperature) + "*C&Data_2=" + String(Pressure) + "mb&Data_3=" + String(Altitude) + "m&Data_4=" + date + "&Data_5=" + _time + "&Data_6=" + latitude + "," + longitude;
    connectionHub = "https://www.theamplituhedron.com/dashboard/WhatsApp-Two-Way-Connection-Hub/" + HEDRON + DATA;
    
     // Create HTTP object to make a request.
    HTTPClient http;    
    // Send a request. 
    http.begin(connectionHub, fingerPrint);
  
    int httpCode = http.GET();
    Command = http.getString();           
    if(httpCode == 200){ Serial.print("\n\nVariables are transferred to the server successfully!\nComannd: " + Command + "\n"); }else{ Serial.print("Server Failed! Error Code: " + httpCode); }
 
    // End HTTP Get Request.
    http.end();

}

void activate_commands(){
  if(Command == "Go Straight"){
    digitalWrite(IN_1, HIGH);
    digitalWrite(IN_2, LOW);
    digitalWrite(IN_3, HIGH);
    digitalWrite(IN_4, LOW);
    }else if(Command == "Go Back"){
    digitalWrite(IN_1, LOW);
    digitalWrite(IN_2, HIGH);
    digitalWrite(IN_3, LOW);
    digitalWrite(IN_4, HIGH);
    }else if(Command == "Go Right"){
    digitalWrite(IN_1, HIGH);
    digitalWrite(IN_2, LOW);
    digitalWrite(IN_3, LOW);
    digitalWrite(IN_4, LOW);
    }else if(Command == "Go Left"){
    digitalWrite(IN_1, LOW);
    digitalWrite(IN_2, LOW);
    digitalWrite(IN_3, HIGH);
    digitalWrite(IN_4, LOW);
    }else if(Command == "Halt"){
    digitalWrite(IN_1, LOW);
    digitalWrite(IN_2, LOW);
    digitalWrite(IN_3, LOW);
    digitalWrite(IN_4, LOW);
    }
  }


Two-Way Connection Hub Source Code

Download



<?php

require_once "/path/to/vendor"; 
 
use Twilio\TwiML\MessagingResponse;

$response = new MessagingResponse();

/*
Get Variables from MySQL Server

And save variables to an array as shown below.

MySQL -> $data

SELECT * FROM...

.
.
.

*/

$data = array(
	"DATA_1" => "test",
	"DATA_2" => "test",
	"DATA_3" => "test",
	"DATA_4" => "test",
	"DATA_5" => "test",
	"DATA_6" => "test",
	"COMMAND" => "test"
	);

if(isset($_GET['Data_1']) && isset($_GET['Data_2']) && isset($_GET['Data_3']) && isset($_GET['Data_4']) && isset($_GET['Data_5']) && isset($_GET['Data_6'])){
	/*
	If all variables are sent by NodeMCU, save them to the MySQL Database and print the pre-defined command on the database as response to send the command to NodeMCU.
	
	$new = array(
		"DATA_1" => strip_tags(mysqli_real_escape_string($this->conn, $_GET['Data_1'])),
		"DATA_2" => strip_tags(mysqli_real_escape_string($this->conn, $_GET['Data_2']),
		"DATA_3" => strip_tags(mysqli_real_escape_string($this->conn, $_GET['Data_3'])),
		"DATA_4" => strip_tags(mysqli_real_escape_string($this->conn, $_GET['Data_4'])),
		"DATA_5" => strip_tags(mysqli_real_escape_string($this->conn, $_GET['Data_5'])),
		"DATA_6" => strip_tags(mysqli_real_escape_string($this->conn, $_GET['Data_6']))
	);
	
	UPDATE `` SET ...
	
	.
	.
	.
	
	*/
	
	echo $data['COMMAND'];
	exit();
}else{
	/* If a message is transferred by a verified phone to this application, get the body. */
	if(isset($_POST['Body'])){
		switch($_POST['Body']){
		case "Temperature":
		$response->message("Temperature -> ".$data['DATA_1']);
		break;
		case 'Pressure':
		$response->message('Pressure -> '.$data['DATA_2']);
		break;
		case "Altitude":
		$response->message('Altitude -> '.$data['DATA_3']);
		break;
		case 'Date':
		$response->message('Date -> '.$data['DATA_4']);
		break;
		case 'Time':
		$response->message('Time -> '.$data['DATA_5']);
		break;
		case 'Latitude and Longitude':
		$response->message('Latitude and Longitude -> '.$data['DATA_6']);
		break;
		case 'Map Location':
		$response->message('https://www.google.com/maps/search/?api=1&query='.$data['DATA_6']);
		break;
		case 'Go Straight':
		/* Save Command to the database. */ 
		$response->message("Command is transferred to the device.");
		break;
		case 'Go Back':
		/* Save Command to the database. */ 
		$response->message("Command is transferred to the device.");
		break;
		case 'Go Right':
		/* Save Command to the database. */ 
		$response->message("Command is transferred to the device.");
		break;
		case 'Go Left':
		/* Save Command to the database. */ 
		$response->message("Command is transferred to the device.");
		break;
		case 'Halt':
		/* Save Command to the database. */ 
		$response->message("Command is transferred to the device.");
		break;
		case 'Programmed by':
		$response->message('This application is programmed by Kutluhan Aktar and provided by TheAmplituhedron. For more information, please click the link:'."\n".' https://www.theamplituhedron.com/projects/WhatsApp-Mapping-and-Weather-Forecast-Chat-Bot/');
		break;
		case 'About':
		$response->message('https://www.theamplituhedron.com/about/');
		break;
		case 'How r u?':
		$response->message("Thanks for asking."."\n"."Data processing is running just fine 😃");
		break;
		case 'Further information':
		$response->message('https://www.theamplituhedron.com/dashboard/WhatsApp-Two-Way-Connection-Hub/');
		break;		
		case 'Contact':
		$response->message('You can contact me directly at info@theamplituhedron.com');
		break;
		case 'Spidey':
		$message = $response->message('');
        $message->body('Media Feature Testing.'."\n".'Spider-Man');
        $message->media('https://www.theamplituhedron.com/dashboard/WhatsApp-Two-Way-Connection-Hub/Spider.jpg');
		break;	
		case 'Batman':
		$message = $response->message('');
        $message->body('Media Feature Testing.'."\n".'Batman');
        $message->media('https://www.theamplituhedron.com/dashboard/WhatsApp-Two-Way-Connection-Hub/Batman.jpg');
		break;
		case 'Help':
		$response->message('Integrated Keywords:'."\n\n".' Temperature '."\n".' Pressure '."\n".' Altitude '."\n".' Date '."\n".' Time '."\n".' Latitude and Longitude '."\n".' Map Location '."\n".' Go Straight '."\n".' Go Back '."\n".' Go Right '."\n".' Go Left '."\n".' Halt '."\n".' Programmed by '."\n".' About '."\n".' How r u? '."\n".' Further information '."\n".' Contact '."\n".' Spidey '."\n".' Batman '."\n".' Help');
		break;
		default:
		$response->message('Not defined!'."\n".'You have entered: '.$_POST['Body']."\n".'Please enter Help to view all keywords.');
		
	}
		print $response;
		
	}else{
		echo 'Nothing Detected! Check the connection!';
		exit();
	}
}

?>


Schematics

project-image
Schematic - 28.1

Downloads

Zip Folder

Download


Fritzing File

Download