the start of structure, using extern and proper imports
This commit is contained in:
parent
74cc87fe65
commit
cdd41bb660
8 changed files with 252 additions and 196 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,7 +3,7 @@
|
||||||
.vscode/c_cpp_properties.json
|
.vscode/c_cpp_properties.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/ipch
|
.vscode/ipch
|
||||||
include/secrets.h
|
src/secrets.cpp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
upload_params.ini
|
upload_params.ini
|
||||||
platformio.ini
|
platformio.ini
|
|
@ -7,10 +7,13 @@
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
Settings settings;
|
|
||||||
|
|
||||||
#ifdef WIFI
|
#ifdef WIFI
|
||||||
#include "WiFi.h"
|
#include "WiFi.h"
|
||||||
|
#ifdef WEBHOOKS
|
||||||
|
// #include "webhooks.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef LOCAL_ACL
|
#ifdef LOCAL_ACL
|
||||||
#include "acl.h"
|
#include "acl.h"
|
||||||
|
@ -24,6 +27,10 @@
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef TINANCE2_BACKEND
|
||||||
|
#include "tinance2.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef WEB_SERVER
|
#ifdef WEB_SERVER
|
||||||
#include <AsyncTCP.h>
|
#include <AsyncTCP.h>
|
||||||
#include "ESPAsyncWebServer.h"
|
#include "ESPAsyncWebServer.h"
|
||||||
|
|
30
include/secrets.h
Normal file
30
include/secrets.h
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#ifndef SECRETS_H
|
||||||
|
#define SECRETS_H
|
||||||
|
|
||||||
|
#ifdef WIFI
|
||||||
|
extern const char* ssid;
|
||||||
|
extern const char* password;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef TINANCE2_BACKEND
|
||||||
|
extern const char* tinance2_url_validatecard;
|
||||||
|
extern const char* tinance2_url_readerinfo;
|
||||||
|
extern const char* tinance2_url_acls;
|
||||||
|
extern const char* tinance2_reader_identifer;
|
||||||
|
extern const char* tinance2_reader_key;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef WEB_SERVER
|
||||||
|
extern const char* http_username;
|
||||||
|
extern const char* http_password;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef WEBHOOKS
|
||||||
|
#ifdef WEBHOOKS_UNLOCK
|
||||||
|
extern const char* webhook_unlock_url;
|
||||||
|
#endif
|
||||||
|
#ifdef WEBHOOKS_LOCK
|
||||||
|
extern const char* webhook_lock_url;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif
|
|
@ -21,4 +21,6 @@ public:
|
||||||
String getDoorMode();
|
String getDoorMode();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extern Settings settings;
|
||||||
|
|
||||||
#endif // SETTINGS_H
|
#endif // SETTINGS_H
|
||||||
|
|
18
include/tinance2.h
Normal file
18
include/tinance2.h
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef tinance2_h
|
||||||
|
#define tinance2_h
|
||||||
|
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
#include "settings.h"
|
||||||
|
#include "secrets.h"
|
||||||
|
#include "hardware.h"
|
||||||
|
#include "buzzer_ctl.h"
|
||||||
|
|
||||||
|
void tinance2SyncTaskFunction(void *parameter);
|
||||||
|
void tinance2authrequest(String fullCardID, String cardID);
|
||||||
|
|
||||||
|
#endif
|
193
src/main.cpp
193
src/main.cpp
|
@ -7,7 +7,6 @@ const unsigned long displayDelay = 1000; // Delay in milliseconds after which th
|
||||||
const unsigned long wifiRebootTimeout = 20000; // Delay before reboot after disconnect
|
const unsigned long wifiRebootTimeout = 20000; // Delay before reboot after disconnect
|
||||||
unsigned int bitCount = 0; // Variable to keep track of the bit count
|
unsigned int bitCount = 0; // Variable to keep track of the bit count
|
||||||
unsigned int maxReaderWaitTime = 9000; // Variable to timeout reader after too long of no data.
|
unsigned int maxReaderWaitTime = 9000; // Variable to timeout reader after too long of no data.
|
||||||
HTTPClient http;
|
|
||||||
|
|
||||||
#ifdef LOCAL_ACL
|
#ifdef LOCAL_ACL
|
||||||
void localAcl(String cardID) {
|
void localAcl(String cardID) {
|
||||||
|
@ -168,202 +167,10 @@ void handleData1Interrupt() {
|
||||||
handleInterrupt(1);
|
handleInterrupt(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef TINANCE2_BACKEND
|
|
||||||
class Tinance2HttpClient {
|
|
||||||
public:
|
|
||||||
Tinance2HttpClient() {}
|
|
||||||
|
|
||||||
std::pair<String, int> sendHttpRequest(String url, String method, String payload) {
|
|
||||||
|
|
||||||
if (!http.begin(url)) {
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Failed to begin HTTP request");
|
|
||||||
#endif
|
|
||||||
http.end();
|
|
||||||
return std::make_pair("", -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the headers
|
|
||||||
http.addHeader("Content-Type", "application/json");
|
|
||||||
http.addHeader("X-Identifier", tinance2_reader_identifer);
|
|
||||||
http.addHeader("X-Access-Key", tinance2_reader_key);
|
|
||||||
|
|
||||||
// Set the HTTP timeout to 5 seconds
|
|
||||||
http.setTimeout(5000);
|
|
||||||
|
|
||||||
// Send the POST request
|
|
||||||
int httpResponseCode;
|
|
||||||
if (method == "POST") {
|
|
||||||
httpResponseCode = http.POST(payload);
|
|
||||||
} else if (method == "GET") {
|
|
||||||
httpResponseCode = http.GET();
|
|
||||||
} else {
|
|
||||||
// Handle invalid method
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Invalid HTTP method");
|
|
||||||
#endif
|
|
||||||
http.end();
|
|
||||||
return std::make_pair("", -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (httpResponseCode <= 0) {
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Request Failed (response less than 0)");
|
|
||||||
#endif
|
|
||||||
http.end();
|
|
||||||
return std::make_pair("", httpResponseCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
String responseBody = http.getString();
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
http.end();
|
|
||||||
|
|
||||||
return std::make_pair(responseBody, httpResponseCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to decode JSON response
|
|
||||||
DynamicJsonDocument decodeJsonResponse(const String& json) {
|
|
||||||
DynamicJsonDocument doc(1024); // Adjust the size as per your JSON data
|
|
||||||
|
|
||||||
DeserializationError error = deserializeJson(doc, json);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.print("Failed to parse JSON: ");
|
|
||||||
Serial.println(error.c_str());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to send the authentication request to Tinance2
|
|
||||||
void tinance2authrequest(String fullCardID, String cardID) {
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("WIFI Status: " + String(WiFi.status()));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Sending Request to Tinance2 for card: " + fullCardID);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Create the JSON payload
|
|
||||||
String payload = "{\"full_card_id\":\"" + String(fullCardID) + "\"}";
|
|
||||||
// Send the HTTP request and get the response
|
|
||||||
Tinance2HttpClient httpClient;
|
|
||||||
std::pair<String, int> responsePair = httpClient.sendHttpRequest(tinance2_url_validatecard, "POST", payload);
|
|
||||||
String response = responsePair.first;
|
|
||||||
int httpResponseCode = responsePair.second;
|
|
||||||
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
// Print the response
|
|
||||||
Serial.println("HTTP Response: " + response);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Process the response
|
|
||||||
if (httpResponseCode != 200 && httpResponseCode != 401 && httpResponseCode != 402 && httpResponseCode != 403) {
|
|
||||||
#ifdef LOCAL_ACL
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Got unexpected http response using offline auth.");
|
|
||||||
#endif
|
|
||||||
localAcl(String(cardID));
|
|
||||||
#endif
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the response code
|
|
||||||
if (httpResponseCode == 200) {
|
|
||||||
Serial.println("Tinance2 Door Access Granted");
|
|
||||||
if (settings.getDoorMode() == "LATCH") {
|
|
||||||
if (!settings.getDoorDisabled()) {
|
|
||||||
unlockDoor(false);
|
|
||||||
delay(RELAY_DELAY);
|
|
||||||
lockDoor(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (settings.getDoorMode() == "TOGGLE") {
|
|
||||||
if (!settings.getDoorDisabled()) {
|
|
||||||
toggleDoor();
|
|
||||||
delay(RELAY_DELAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (httpResponseCode == 400 || httpResponseCode == 401 || httpResponseCode == 402 || httpResponseCode == 403) {
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Tinance2 Door Access Denied");
|
|
||||||
#endif
|
|
||||||
#ifdef BUZZER
|
|
||||||
denied_beep();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Tinance2 Wifi Disconnected using offline processes.");
|
|
||||||
#endif
|
|
||||||
#ifdef LOCAL_ACL
|
|
||||||
localAcl(String(cardID));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef TINANCE2_BACKEND_SYNC
|
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/task.h>
|
|
||||||
|
|
||||||
void tinance2SyncTaskFunction(void *parameter) {
|
|
||||||
while (true) {
|
|
||||||
// Your code here
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("Syncing Tinance2");
|
|
||||||
#endif
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(15000)); // Delay for 15 seconds
|
|
||||||
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
Tinance2HttpClient httpClient;
|
|
||||||
std::pair<String, int> responsePair = httpClient.sendHttpRequest(tinance2_url_readerinfo, "GET", "");
|
|
||||||
String response = responsePair.first;
|
|
||||||
int httpResponseCode = responsePair.second;
|
|
||||||
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
// Print the response
|
|
||||||
Serial.println("HTTP Response: " + response);
|
|
||||||
Serial.println("HTTP Response Code: " + String(httpResponseCode));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Process the response
|
|
||||||
DynamicJsonDocument json = httpClient.decodeJsonResponse(response);
|
|
||||||
|
|
||||||
if (json.containsKey("enabled")) {
|
|
||||||
bool DisableDoor = json["enabled"].as<bool>();
|
|
||||||
settings.setDisableDoor(!DisableDoor);
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("JSON Reader Enabled: " + json["enabled"].as<String>());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.containsKey("mode")) {
|
|
||||||
String doorMode = json["mode"].as<String>();
|
|
||||||
settings.setDoorMode(doorMode);
|
|
||||||
#ifdef SERIAL_DEBUG
|
|
||||||
Serial.println("JSON Reader Mode: " + json["mode"].as<String>());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
// allow reuse (if server supports it)
|
|
||||||
http.setReuse(true);
|
|
||||||
|
|
||||||
#if defined SERIAL_DEBUG || defined SERIAL_ACL
|
#if defined SERIAL_DEBUG || defined SERIAL_ACL
|
||||||
Serial.begin(9600);
|
Serial.begin(9600);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -50,3 +50,5 @@ void Settings::setDoorMode(const String mode) {
|
||||||
String Settings::getDoorMode() {
|
String Settings::getDoorMode() {
|
||||||
return doorMode;
|
return doorMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Settings settings;
|
190
src/tinance2.cpp
Normal file
190
src/tinance2.cpp
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
#include "tinance2.h"
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
|
||||||
|
HTTPClient tinance2_http;
|
||||||
|
|
||||||
|
|
||||||
|
class Tinance2HttpClient {
|
||||||
|
public:
|
||||||
|
Tinance2HttpClient() {}
|
||||||
|
|
||||||
|
std::pair<String, int> sendHttpRequest(String url, String method, String payload) {
|
||||||
|
|
||||||
|
if (!tinance2_http.begin(url)) {
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Failed to begin HTTP request");
|
||||||
|
#endif
|
||||||
|
tinance2_http.end();
|
||||||
|
return std::make_pair("", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the headers
|
||||||
|
tinance2_http.addHeader("Content-Type", "application/json");
|
||||||
|
tinance2_http.addHeader("X-Identifier", tinance2_reader_identifer);
|
||||||
|
tinance2_http.addHeader("X-Access-Key", tinance2_reader_key);
|
||||||
|
|
||||||
|
// Set the HTTP timeout to 5 seconds
|
||||||
|
tinance2_http.setTimeout(5000);
|
||||||
|
|
||||||
|
// Send the POST request
|
||||||
|
int httpResponseCode;
|
||||||
|
if (method == "POST") {
|
||||||
|
httpResponseCode = tinance2_http.POST(payload);
|
||||||
|
} else if (method == "GET") {
|
||||||
|
httpResponseCode = tinance2_http.GET();
|
||||||
|
} else {
|
||||||
|
// Handle invalid method
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Invalid HTTP method");
|
||||||
|
#endif
|
||||||
|
tinance2_http.end();
|
||||||
|
return std::make_pair("", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpResponseCode <= 0) {
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Request Failed (response less than 0)");
|
||||||
|
#endif
|
||||||
|
tinance2_http.end();
|
||||||
|
return std::make_pair("", httpResponseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseBody = tinance2_http.getString();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
tinance2_http.end();
|
||||||
|
|
||||||
|
return std::make_pair(responseBody, httpResponseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to decode JSON response
|
||||||
|
DynamicJsonDocument decodeJsonResponse(const String& json) {
|
||||||
|
DynamicJsonDocument doc(1024); // Adjust the size as per your JSON data
|
||||||
|
|
||||||
|
DeserializationError error = deserializeJson(doc, json);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.print("Failed to parse JSON: ");
|
||||||
|
Serial.println(error.c_str());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to send the authentication request to Tinance2
|
||||||
|
void tinance2authrequest(String fullCardID, String cardID) {
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("WIFI Status: " + String(WiFi.status()));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Sending Request to Tinance2 for card: " + fullCardID);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Create the JSON payload
|
||||||
|
String payload = "{\"full_card_id\":\"" + String(fullCardID) + "\"}";
|
||||||
|
// Send the HTTP request and get the response
|
||||||
|
Tinance2HttpClient httpClient;
|
||||||
|
std::pair<String, int> responsePair = httpClient.sendHttpRequest(tinance2_url_validatecard, "POST", payload);
|
||||||
|
String response = responsePair.first;
|
||||||
|
int httpResponseCode = responsePair.second;
|
||||||
|
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
// Print the response
|
||||||
|
Serial.println("HTTP Response: " + response);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Process the response
|
||||||
|
if (httpResponseCode != 200 && httpResponseCode != 401 && httpResponseCode != 402 && httpResponseCode != 403) {
|
||||||
|
#ifdef LOCAL_ACL
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Got unexpected http response using offline auth.");
|
||||||
|
#endif
|
||||||
|
localAcl(String(cardID));
|
||||||
|
#endif
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the response code
|
||||||
|
if (httpResponseCode == 200) {
|
||||||
|
Serial.println("Tinance2 Door Access Granted");
|
||||||
|
if (settings.getDoorMode() == "LATCH") {
|
||||||
|
if (!settings.getDoorDisabled()) {
|
||||||
|
unlockDoor(false);
|
||||||
|
delay(RELAY_DELAY);
|
||||||
|
lockDoor(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (settings.getDoorMode() == "TOGGLE") {
|
||||||
|
if (!settings.getDoorDisabled()) {
|
||||||
|
toggleDoor();
|
||||||
|
delay(RELAY_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (httpResponseCode == 400 || httpResponseCode == 401 || httpResponseCode == 402 || httpResponseCode == 403) {
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Tinance2 Door Access Denied");
|
||||||
|
#endif
|
||||||
|
#ifdef BUZZER
|
||||||
|
denied_beep();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Tinance2 Wifi Disconnected using offline processes.");
|
||||||
|
#endif
|
||||||
|
#ifdef LOCAL_ACL
|
||||||
|
localAcl(String(cardID));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void tinance2SyncTaskFunction(void *parameter) {
|
||||||
|
while (true) {
|
||||||
|
// Your code here
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("Syncing Tinance2");
|
||||||
|
#endif
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(15000)); // Delay for 15 seconds
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
Tinance2HttpClient httpClient;
|
||||||
|
std::pair<String, int> responsePair = httpClient.sendHttpRequest(tinance2_url_readerinfo, "GET", "");
|
||||||
|
String response = responsePair.first;
|
||||||
|
int httpResponseCode = responsePair.second;
|
||||||
|
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
// Print the response
|
||||||
|
Serial.println("HTTP Response: " + response);
|
||||||
|
Serial.println("HTTP Response Code: " + String(httpResponseCode));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Process the response
|
||||||
|
DynamicJsonDocument json = httpClient.decodeJsonResponse(response);
|
||||||
|
|
||||||
|
if (json.containsKey("enabled")) {
|
||||||
|
bool DisableDoor = json["enabled"].as<bool>();
|
||||||
|
settings.setDisableDoor(!DisableDoor);
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("JSON Reader Enabled: " + json["enabled"].as<String>());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.containsKey("mode")) {
|
||||||
|
String doorMode = json["mode"].as<String>();
|
||||||
|
settings.setDoorMode(doorMode);
|
||||||
|
#ifdef SERIAL_DEBUG
|
||||||
|
Serial.println("JSON Reader Mode: " + json["mode"].as<String>());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue