Upload Arduino IDE SC

This commit is contained in:
DandhiAri 2025-08-07 13:28:18 +07:00
parent 734e5082d9
commit fcef555386
13 changed files with 393 additions and 426 deletions

View File

@ -0,0 +1,393 @@
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP8266WiFi.h>
#include <Firebase_ESP_Client.h>
#include <ArduinoJson.h>
// --- Konfigurasi WiFi ---
#define WIFI_SSID "LMAO"
#define WIFI_PASSWORD "awokawok"
// --- Konfigurasi Firebase ---
#define FIREBASE_HOST "https://meja-iotv0-default-rtdb.firebaseio.com/"
#define FIREBASE_AUTH "AIzaSyC9YQJWMSajJQTlh1tGLRcIeui4rqGvl8E"
FirebaseData firebaseData;
FirebaseConfig config;
FirebaseAuth auth;
LiquidCrystal_I2C lcd(0x27, 16, 2);
// --- Definisi Pin ---
#define TRIG_PIN D5
int echoPins[4] = {D6, D7, D8, D4};
#define BUZZER_PIN D0
#define FORCE_BUTTON_PIN D3
float distances[4];
float lastValidDistances[4];
float lastSensorDistances[4];
// --- Sistem Reservasi ---
bool tableActivationSensorActive = false;
bool tableReserved = false;
bool tableOccupied = false;
bool refreshTriggered = false;
unsigned long reservationSessionStartOrRefreshTime = 0;
unsigned long lastPresenceTime = 0;
String tempLine1 = "";
String tempLine2 = "";
unsigned long messageStartTime = 0;
unsigned long messageDuration = 2000;
bool showTempMessage = false;
String lastLine1 = "";
String lastLine2 = "";
unsigned long lastLCDUpdate = 0;
const unsigned long LCD_UPDATE_INTERVAL = 1000;
const unsigned long RESERVATION_ACTIVE_DURATION = 30 * 60 * 1000;
const unsigned long IDLE_OCCUPIED_TIMEOUT = 10 * 60 * 1000;
const unsigned long WARNING_TIME_10_MIN = 10 * 60 * 1000;
const unsigned long WARNING_TIME_5_MIN = 5 * 60 * 1000;
bool warning10MinTriggered = false;
bool warning5MinTriggered = false;
const float PRESENCE_MIN = 50.0;
const float PRESENCE_MAX = 80.0;
const float MOTION_THRESHOLD = 5.0;
bool presenceDetected = false;
String customerName = "N/A";
bool forceShutdownRequested = false;
unsigned long lastButtonPress = 0;
const unsigned long BUTTON_DEBOUNCE = 200;
unsigned long lastFirebaseUpdate = 0;
const unsigned long FIREBASE_UPDATE_INTERVAL = 5000;
#define TABLE_ID "meja_001"
String deviceID = TABLE_ID;
void showTemporaryMessage(String line1, String line2, int duration = 2000) {
tempLine1 = line1;
tempLine2 = line2;
messageDuration = duration;
showTempMessage = true;
messageStartTime = millis();
lcd.clear();
lcd.setCursor(0, 0); lcd.print(tempLine1);
lcd.setCursor(0, 1); lcd.print(tempLine2);
lastLCDUpdate = millis(); // supaya tidak update terlalu cepat
}
// ==== SETUP ====
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
lcd.init(); lcd.backlight();
pinMode(TRIG_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(FORCE_BUTTON_PIN, INPUT_PULLUP);
for (int i = 0; i < 4; i++) pinMode(echoPins[i], INPUT);
connectWiFi();
if (WiFi.status() == WL_CONNECTED) initFirebase();
showTemporaryMessage("Sistem Siap!", deviceID);
}
// ==== LOOP ====
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWiFi();
if (Firebase.ready() && millis() - lastFirebaseUpdate > FIREBASE_UPDATE_INTERVAL) {
updateFromFirebase();
updateToFirebase();
lastFirebaseUpdate = millis();
}
if (forceShutdownRequested) {
forceShutdown();
if (Firebase.ready()) {
Firebase.RTDB.setInt(&firebaseData, "/" + deviceID + "/force_shutdown", 0);
}
forceShutdownRequested = false;
}
if (digitalRead(FORCE_BUTTON_PIN) == LOW && millis() - lastButtonPress > BUTTON_DEBOUNCE) {
forceShutdown(); lastButtonPress = millis();
}
readSensors();
analyzeActivity();
if (tableActivationSensorActive) {
if (!tableReserved) startReservation();
if (tableReserved) {
checkSessionTimeout();
checkIdle();
checkWarnings();
}
} else if (tableReserved) {
endReservation();
}
displayLCD();
printSerial();
delay(500);
}
// ==== SENSOR & AKTIVITAS ====
float readDistance(int pin) {
float sum = 0; int valid = 0;
for (int i = 0; i < 3; i++) {
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long d = pulseIn(pin, HIGH, 30000);
float dist = d * 0.034 / 2;
if (dist > 2 && dist < 400) { sum += dist; valid++; }
delay(10);
}
return (valid == 0) ? 0 : sum / valid;
}
void readSensors() {
for (int i = 0; i < 4; i++) {
float d = readDistance(echoPins[i]);
if (d > 0) lastValidDistances[i] = d;
distances[i] = lastValidDistances[i];
}
}
void analyzeActivity() {
unsigned long now = millis();
bool motion = false;
for (int i = 0; i < 4; i++) {
float delta = abs(distances[i] - lastSensorDistances[i]);
if (lastSensorDistances[i] > 0 && delta >= MOTION_THRESHOLD) motion = true;
lastSensorDistances[i] = distances[i];
}
if (now - reservationSessionStartOrRefreshTime > 10000) refreshTriggered = false;
if (tableReserved && motion && !refreshTriggered) {
reservationSessionStartOrRefreshTime = now;
warning10MinTriggered = warning5MinTriggered = false;
refreshTriggered = true;
Serial.println("Sesi diperpanjang karena gerakan.");
}
presenceDetected = false;
for (int i = 0; i < 4; i++) {
if (distances[i] >= PRESENCE_MIN && distances[i] <= PRESENCE_MAX) {
presenceDetected = true;
break;
}
}
if (presenceDetected) {
lastPresenceTime = now;
tableOccupied = true;
} else if (now - lastPresenceTime > IDLE_OCCUPIED_TIMEOUT) {
tableOccupied = false;
}
// Tambahkan warning jika terlalu dekat
static unsigned long lastWarningTime = 0;
for (int i = 0; i < 4; i++) {
if (distances[i] > 0 && distances[i] < 10.0 && millis() - lastWarningTime > 5000) {
showTooCloseWarning();
lastWarningTime = millis();
break;
}
}
}
void showTooCloseWarning() {
showTemporaryMessage("!! TERLALU DEKAT !!", "Jarak < 10 cm");
}
// ==== WIFI & FIREBASE ====
void connectWiFi() {
lcd.clear(); lcd.setCursor(0, 0); lcd.print("Koneksi WiFi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) {
delay(500); Serial.print(".");
}
lcd.clear();
if (WiFi.status() == WL_CONNECTED) {
showTemporaryMessage("WiFi Terhubung!", WiFi.localIP().toString());
Serial.println("WiFi IP: " + WiFi.localIP().toString());
} else {
showTemporaryMessage("Gagal WiFi!", "Coba lagi...");
}
}
void initFirebase() {
config.host = FIREBASE_HOST;
config.signer.tokens.legacy_token = FIREBASE_AUTH;
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
Serial.println(Firebase.ready() ? "Firebase Terhubung!" : "Gagal Firebase!");
}
void updateFromFirebase() {
String base = "/" + deviceID;
if (Firebase.RTDB.getInt(&firebaseData, base + "/sensors/table_activation_sensor_active"))
tableActivationSensorActive = firebaseData.intData() == 1;
else
tableActivationSensorActive = false;
if (Firebase.RTDB.getString(&firebaseData, base + "/reserved_by")) {
customerName = firebaseData.stringData();
if (customerName.length() > 8) customerName = customerName.substring(0, 8);
} else customerName = "N/A";
if (Firebase.RTDB.getInt(&firebaseData, base + "/force_shutdown")) {
forceShutdownRequested = firebaseData.intData() == 1;
} else {
forceShutdownRequested = false;
}
}
void updateToFirebase() {
if (!Firebase.ready()) return;
String path = "/" + deviceID;
FirebaseJson json;
json.set("device_id", deviceID);
json.set("mac_address", WiFi.macAddress());
json.set("last_update", millis() / 1000);
json.set("table_occupied", presenceDetected ? 1 : 0);
json.set("time_remaining_minutes", (int)(getRemainingSessionTime() / 60000));
for (int i = 0; i < 4; i++) json.set("sensors/s" + String(i + 1), (int)distances[i]);
json.set("sensors/table_activation_sensor_active", tableActivationSensorActive ? 1 : 0);
Firebase.RTDB.updateNode(&firebaseData, path, &json);
}
// ==== RESERVASI ====
void resetFlags() {
for (int i = 0; i < 4; i++) lastSensorDistances[i] = 0;
warning10MinTriggered = warning5MinTriggered = refreshTriggered = false;
}
void startReservation() {
tableReserved = true;
reservationSessionStartOrRefreshTime = millis();
lastPresenceTime = millis();
resetFlags();
buzzerAlert(2, 500, 150);
Serial.println("Reservasi dimulai oleh " + customerName);
showTemporaryMessage("MEJA " + deviceID.substring(5), "RVSP: " + customerName);
}
void endReservation() {
tableReserved = false;
tableOccupied = false;
resetFlags();
showTemporaryMessage("RESERVASI", "BERAKHIR");
}
void forceShutdown() {
tableActivationSensorActive = false;
endReservation();
Firebase.RTDB.setInt(&firebaseData, "/" + deviceID + "/sensors/table_activation_sensor_active", 0);
Firebase.RTDB.setString(&firebaseData, "/" + deviceID + "/reserved_by", "N/A");
customerName = "N/A";
buzzerAlert(5, 100, 50);
lcd.clear(); lcd.setCursor(0, 0); lcd.print("TERIMA KASIH");
lcd.setCursor(0, 1); lcd.print("SUDAH DATANG!"); delay(3000);
}
unsigned long getRemainingSessionTime() {
if (!tableReserved) return 0;
unsigned long elapsed = millis() - reservationSessionStartOrRefreshTime;
return (elapsed >= RESERVATION_ACTIVE_DURATION) ? 0 : RESERVATION_ACTIVE_DURATION - elapsed;
}
void checkSessionTimeout() {
if (getRemainingSessionTime() == 0) {
tableActivationSensorActive = false;
endReservation();
Firebase.RTDB.setInt(&firebaseData, "/" + deviceID + "/sensors/table_activation_sensor_active", 0);
Firebase.RTDB.setString(&firebaseData, "/" + deviceID + "/reserved_by", "N/A");
}
}
void checkIdle() {
if (millis() - lastPresenceTime >= IDLE_OCCUPIED_TIMEOUT) tableOccupied = false;
else tableOccupied = true;
}
void checkWarnings() {
unsigned long remain = getRemainingSessionTime();
if (remain <= WARNING_TIME_5_MIN && !warning5MinTriggered) {
warning5MinTriggered = true;
buzzerAlert(5, 200, 80);
showWarningLCD("< 5 menit!");
} else if (remain <= WARNING_TIME_10_MIN && !warning10MinTriggered) {
warning10MinTriggered = true;
buzzerAlert(3, 300, 100);
showWarningLCD("< 10 menit!");
}
}
void showWarningLCD(String text) {
showTemporaryMessage("** PERINGATAN **", text);
}
// ==== TAMPILAN ====
void displayLCD() {
if (millis() - lastLCDUpdate < LCD_UPDATE_INTERVAL) return;
if (showTempMessage) {
if (millis() - messageStartTime >= messageDuration) {
showTempMessage = false;
lastLine1 = ""; // paksa refresh ke tampilan normal
lastLine2 = "";
}
return;
}
String line1 = "MEJA " + deviceID.substring(5);
String line2;
if (WiFi.status() != WL_CONNECTED) line2 = "WiFi terputus";
else if (!tableReserved) line2 = "TERSEDIA";
else line2 = "RVSP: " + customerName;
if (line1 != lastLine1 || line2 != lastLine2) {
lcd.clear();
lcd.setCursor(0, 0); lcd.print(line1);
lcd.setCursor(0, 1); lcd.print(line2);
lastLine1 = line1;
lastLine2 = line2;
lastLCDUpdate = millis();
}
}
void printSerial() {
Serial.println("\n=== STATUS " + deviceID + " ===");
Serial.println("WiFi: " + String(WiFi.status() == WL_CONNECTED ? "YA" : "TIDAK"));
Serial.println("Firebase: " + String(Firebase.ready() ? "YA" : "TIDAK"));
Serial.print("Sensor: ");
for (int i = 0; i < 4; i++) Serial.print("S" + String(i+1) + ":" + String(distances[i]) + " ");
Serial.println();
Serial.println("Aktif: " + String(tableActivationSensorActive ? "YA" : "TIDAK"));
Serial.println("Reserved: " + String(tableReserved ? "YA" : "TIDAK"));
Serial.println("Occupied: " + String(tableOccupied ? "YA" : "TIDAK"));
Serial.println("Sisa Waktu: " + String(getRemainingSessionTime() / 60000) + " menit");
Serial.println("Pemesan: " + customerName);
Serial.println("Warning10: " + String(warning10MinTriggered));
Serial.println("Warning5: " + String(warning5MinTriggered));
}
// ==== BUZZER ====
void buzzerAlert(int count, int onTime, int offTime) {
for (int i = 0; i < count; i++) {
digitalWrite(BUZZER_PIN, HIGH); delay(onTime);
digitalWrite(BUZZER_PIN, LOW); delay(offTime);
}
}

View File

@ -1,49 +0,0 @@
<?php
use App\Livewire\Auth\Login;
use App\Models\User;
use Livewire\Livewire;
test('login screen can be rendered', function () {
$response = $this->get('/login');
$response->assertStatus(200);
});
test('users can authenticate using the login screen', function () {
$user = User::factory()->create();
$response = Livewire::test(Login::class)
->set('email', $user->email)
->set('password', 'password')
->call('login');
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
$this->assertAuthenticated();
});
test('users can not authenticate with invalid password', function () {
$user = User::factory()->create();
$response = Livewire::test(Login::class)
->set('email', $user->email)
->set('password', 'wrong-password')
->call('login');
$response->assertHasErrors('email');
$this->assertGuest();
});
test('users can logout', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/logout');
$response->assertRedirect('/');
$this->assertGuest();
});

View File

@ -1,47 +0,0 @@
<?php
use App\Models\User;
use Illuminate\Auth\Events\Verified;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;
test('email verification screen can be rendered', function () {
$user = User::factory()->unverified()->create();
$response = $this->actingAs($user)->get('/verify-email');
$response->assertStatus(200);
});
test('email can be verified', function () {
$user = User::factory()->unverified()->create();
Event::fake();
$verificationUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
['id' => $user->id, 'hash' => sha1($user->email)]
);
$response = $this->actingAs($user)->get($verificationUrl);
Event::assertDispatched(Verified::class);
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
});
test('email is not verified with invalid hash', function () {
$user = User::factory()->unverified()->create();
$verificationUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
['id' => $user->id, 'hash' => sha1('wrong-email')]
);
$this->actingAs($user)->get($verificationUrl);
expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
});

View File

@ -1,39 +0,0 @@
<?php
use App\Livewire\Auth\ConfirmPassword;
use App\Models\User;
use Livewire\Livewire;
test('confirm password screen can be rendered', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/confirm-password');
$response->assertStatus(200);
});
test('password can be confirmed', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = Livewire::test(ConfirmPassword::class)
->set('password', 'password')
->call('confirmPassword');
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
});
test('password is not confirmed with invalid password', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = Livewire::test(ConfirmPassword::class)
->set('password', 'wrong-password')
->call('confirmPassword');
$response->assertHasErrors(['password']);
});

View File

@ -1,68 +0,0 @@
<?php
use App\Livewire\Auth\ForgotPassword;
use App\Livewire\Auth\ResetPassword;
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;
use Illuminate\Support\Facades\Notification;
use Livewire\Livewire;
test('reset password link screen can be rendered', function () {
$response = $this->get('/forgot-password');
$response->assertStatus(200);
});
test('reset password link can be requested', function () {
Notification::fake();
$user = User::factory()->create();
Livewire::test(ForgotPassword::class)
->set('email', $user->email)
->call('sendPasswordResetLink');
Notification::assertSentTo($user, ResetPasswordNotification::class);
});
test('reset password screen can be rendered', function () {
Notification::fake();
$user = User::factory()->create();
Livewire::test(ForgotPassword::class)
->set('email', $user->email)
->call('sendPasswordResetLink');
Notification::assertSentTo($user, ResetPasswordNotification::class, function ($notification) {
$response = $this->get('/reset-password/'.$notification->token);
$response->assertStatus(200);
return true;
});
});
test('password can be reset with valid token', function () {
Notification::fake();
$user = User::factory()->create();
Livewire::test(ForgotPassword::class)
->set('email', $user->email)
->call('sendPasswordResetLink');
Notification::assertSentTo($user, ResetPasswordNotification::class, function ($notification) use ($user) {
$response = Livewire::test(ResetPassword::class, ['token' => $notification->token])
->set('email', $user->email)
->set('password', 'password')
->set('password_confirmation', 'password')
->call('resetPassword');
$response
->assertHasNoErrors()
->assertRedirect(route('login', absolute: false));
return true;
});
});

View File

@ -1,25 +0,0 @@
<?php
use App\Livewire\Auth\Register;
use Livewire\Livewire;
test('registration screen can be rendered', function () {
$response = $this->get('/register');
$response->assertStatus(200);
});
test('new users can register', function () {
$response = Livewire::test(Register::class)
->set('name', 'Test User')
->set('email', 'test@example.com')
->set('password', 'password')
->set('password_confirmation', 'password')
->call('register');
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
$this->assertAuthenticated();
});

View File

@ -1,13 +0,0 @@
<?php
use App\Models\User;
test('guests are redirected to the login page', function () {
$this->get('/dashboard')->assertRedirect('/login');
});
test('authenticated users can visit the dashboard', function () {
$this->actingAs($user = User::factory()->create());
$this->get('/dashboard')->assertStatus(200);
});

View File

@ -1,7 +0,0 @@
<?php
test('returns a successful response', function () {
$response = $this->get('/');
$response->assertStatus(200);
});

View File

@ -1,40 +0,0 @@
<?php
use App\Livewire\Settings\Password;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Livewire\Livewire;
test('password can be updated', function () {
$user = User::factory()->create([
'password' => Hash::make('password'),
]);
$this->actingAs($user);
$response = Livewire::test(Password::class)
->set('current_password', 'password')
->set('password', 'new-password')
->set('password_confirmation', 'new-password')
->call('updatePassword');
$response->assertHasNoErrors();
expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue();
});
test('correct password must be provided to update password', function () {
$user = User::factory()->create([
'password' => Hash::make('password'),
]);
$this->actingAs($user);
$response = Livewire::test(Password::class)
->set('current_password', 'wrong-password')
->set('password', 'new-password')
->set('password_confirmation', 'new-password')
->call('updatePassword');
$response->assertHasErrors(['current_password']);
});

View File

@ -1,76 +0,0 @@
<?php
use App\Livewire\Settings\Profile;
use App\Models\User;
use Livewire\Livewire;
test('profile page is displayed', function () {
$this->actingAs($user = User::factory()->create());
$this->get('/settings/profile')->assertOk();
});
test('profile information can be updated', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = Livewire::test(Profile::class)
->set('name', 'Test User')
->set('email', 'test@example.com')
->call('updateProfileInformation');
$response->assertHasNoErrors();
$user->refresh();
expect($user->name)->toEqual('Test User');
expect($user->email)->toEqual('test@example.com');
expect($user->email_verified_at)->toBeNull();
});
test('email verification status is unchanged when email address is unchanged', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = Livewire::test(Profile::class)
->set('name', 'Test User')
->set('email', $user->email)
->call('updateProfileInformation');
$response->assertHasNoErrors();
expect($user->refresh()->email_verified_at)->not->toBeNull();
});
test('user can delete their account', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = Livewire::test('settings.delete-user-form')
->set('password', 'password')
->call('deleteUser');
$response
->assertHasNoErrors()
->assertRedirect('/');
expect($user->fresh())->toBeNull();
expect(auth()->check())->toBeFalse();
});
test('correct password must be provided to delete account', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = Livewire::test('settings.delete-user-form')
->set('password', 'wrong-password')
->call('deleteUser');
$response->assertHasErrors(['password']);
expect($user->fresh())->not->toBeNull();
});

View File

@ -1,47 +0,0 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/
pest()->extend(Tests\TestCase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/
function something()
{
// ..
}

View File

@ -1,10 +0,0 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}

View File

@ -1,5 +0,0 @@
<?php
test('that true is true', function () {
expect(true)->toBeTrue();
});