- Published on
HortiLink Level 3 — Sensor and Rule
- Authors
HortiLink Level 3 — Sensor and Rule
- HortiLink Level 3 — Sensor and Rule
Posisi Level 3 dalam Aliran Belajar HortiLink
Di dalam README utama, HortiLink dibangun sebagai single-project yang bertumbuh per level. Setelah Level 1 memperkenalkan identitas node dan status runtime, dan Level 2 memperkenalkan interaksi lokal, maka Level 3 menjadi tahap ketika node mulai benar-benar membaca kondisi lingkungan dan mengambil keputusan otomatis berdasarkan data sensor.
Sesuai build order yang sudah dikunci di README, Level 3 berada pada tahap:
Level 3 — Sensor and Rule
Fokus:
- HN-05
- HN-06
- HN-09
Tujuan:
- sensor acquisition
- service logic
Artinya, Level 3 bukan lagi node yang hanya memiliki identitas dan kontrol manual, tetapi node yang mulai:
- membaca minimal satu sensor lingkungan
- menjalankan rule otomatis lokal
- memiliki alarm/fault state dasar
Karena target platform tetap ESP32-C3 DevKit pada simulator Velxio, maka Level 3 didesain agar tetap testable pada simulator. Untuk itu, sensor lingkungan pada level ini direpresentasikan sebagai digital environment sensor proxy, sehingga pola akuisisi sensor, rule engine, dan fault handling tetap bisa diuji tanpa bergantung pada periferal analog.
1. Fokus
- HN-05 — Sensor Data Acquisition
- HN-06 — Local Auto Rule Engine
- HN-09 — Alarm/Fault State
2. Tujuan
- sensor acquisition
- service logic
3. Feature yang Dikerjakan
Feature yang dikerjakan pada Level 3 adalah:
HN-05 Sensor Data Acquisition
Node membaca minimal satu sensor lingkungan.HN-06 Local Auto Rule Engine
Rule lokal sederhana, misalnya sensor mendeteksi kondisi dry lalu mengaktifkan aktuator.HN-09 Alarm/Fault State
Node mendeteksi kondisi fault dasar dan memberi indikasi lokal.
4. Platform
- Board: ESP32-C3 DevKit
- Simulator: Velxio
- Status LED:
GPIO8(built-in LED) - Sensor lingkungan digital proxy:
GPIO5 - Fault simulation input:
GPIO6 - Actuator output:
GPIO7
Pada Level 3, sensor lingkungan belum dibuat sebagai sensor analog penuh. Untuk kebutuhan simulator dan pembelajaran layering, sensor direpresentasikan sebagai digital soil sensor proxy pada GPIO5. Input ini mewakili kondisi sederhana:
- sensor aktif →
DRY - sensor tidak aktif →
WET/NORMAL
Selain itu, untuk memvalidasi HN-09 Alarm/Fault State, digunakan satu input fault simulasi pada GPIO6. Dengan cara ini, akuisisi sensor, rule engine lokal, dan alarm state tetap bisa diuji secara nyata di Velxio.
5. Konsep Level 3
Pada Level 3, node HortiLink mulai memiliki perilaku otomatis berbasis sensor.
Node sekarang melakukan hal-hal berikut:
- saat boot, node masuk mode
BOOTING - setelah boot selesai, node masuk mode normal monitoring
- node membaca satu sensor lingkungan digital proxy
- jika sensor mendeteksi kondisi dry, rule lokal mengaktifkan aktuator
- jika sensor kembali normal, aktuator dimatikan
- jika fault sensor terdeteksi, node masuk mode
ALERT - node memberi indikasi lokal lewat LED status dan Serial Monitor
Dengan begitu, engineer mulai melihat bentuk yang lebih nyata dari edge node HortiLink:
- ada data lingkungan masuk
- ada rule lokal yang memproses data
- ada output otomatis yang mengikuti hasil rule
- ada kondisi fault yang mengubah perilaku sistem
6. Mode yang Dipakai
Kita pakai 4 mode aktif pada Level 3:
BOOTINGMONITORINGAUTO_ACTIVEALERT
6.1 Pola status LED
BOOTING→ kedip cepatMONITORING→ kedip lambatAUTO_ACTIVE→ nyala stabilALERT→ kedip dua kali berulang
6.2 Rule otomatis lokal
Rule lokal Level 3 dibuat sederhana dan jelas:
- jika sensor lingkungan menunjukkan
DRYdan tidak ada fault, aktuator ON - jika sensor kembali normal dan tidak ada fault, aktuator OFF
Dengan demikian, Level 3 memperkenalkan bentuk paling awal dari auto control.
6.3 Alarm/Fault State
Fault dasar diperlakukan sebagai kondisi prioritas tertinggi.
Perilakunya:
- jika fault sensor aktif, node masuk
ALERT - saat
ALERT, aktuator dipaksa OFF - LED status masuk pola alert
- setelah fault hilang, node kembali ke:
AUTO_ACTIVEjika sensor masihDRYMONITORINGjika sensor sudah normal
Ini membuat Level 3 sekaligus memperkenalkan ide penting bahwa fault state mengalahkan auto rule normal.
7. Struktur Layer
7.1 sys
Berisi konfigurasi tetap:
- pin status LED
- pin sensor lingkungan
- pin fault simulasi
- pin aktuator
- baudrate serial
- interval loop
- durasi boot
- debounce input
- pola status LED
7.2 drv
Berisi class boundary hardware:
StatusLedDigitalSoilSensorActuatorOutput
Tugas masing-masing:
StatusLed
- inisialisasi LED status
- menerima ON/OFF dari service
DigitalSoilSensor
- membaca sensor lingkungan digital proxy
- membaca input fault simulasi
- melakukan debounce input
ActuatorOutput
- menghidupkan dan mematikan output aktuator
- menyimpan status output saat ini
7.3 svc
Berisi service domain:
- definisi mode node
- rule engine lokal
- keputusan ON/OFF aktuator
- keputusan ON/OFF status LED
- penentuan alarm/fault state
- penggunaan
drvyang relevan untuk domain sensor dan aktuator
Di Level 3, svc menjadi unit domain yang menyelesaikan:
- pembacaan sensor
- keputusan kontrol otomatis
- keputusan fault state
- penerapan output ke driver
7.4 app
Berisi orchestration:
- memulai service
- menjalankan service
- logging ke serial
- menjaga ritme runtime
Dengan pembagian ini, app tidak memuat logic rule atau keputusan kontrol.
8. Wiring untuk Simulator
Agar Level 3 bisa diuji di Velxio, wiring yang dipakai adalah:
Status LED
- built-in LED board pada
GPIO8
Sensor lingkungan digital proxy
- satu kaki switch/button →
GPIO5 - satu kaki switch/button →
GND
Input ini mewakili kondisi sensor:
- aktif →
DRY - tidak aktif →
NORMAL
Fault simulation input
- satu kaki switch/button →
GPIO6 - satu kaki switch/button →
GND
Input ini dipakai untuk memvalidasi feature HN-09:
- aktif →
FAULT - tidak aktif →
NO FAULT
Actuator output
GPIO7 -> LED -> GND
Catatan penting:
- pada simulator, bentuk ini cukup untuk validasi logika output
- pada hardware nyata, sangat disarankan memakai resistor seri untuk LED
- pada board nyata di masa depan, sensor digital proxy ini bisa diganti dengan sensor lingkungan yang lebih realistis tanpa mengubah pola layering
9. Sketch Level 3
#include <Arduino.h>
// =====================================================
// sys -> configuration
// =====================================================
namespace sys {
struct Config {
static constexpr uint8_t PIN_STATUS_LED = 8; // Built-in LED ESP32-C3 DevKit di Velxio
static constexpr uint8_t PIN_SENSOR_DRY = 5; // Digital soil sensor proxy
static constexpr uint8_t PIN_SENSOR_FAULT = 6; // Fault simulation input
static constexpr uint8_t PIN_ACTUATOR = 7; // Dummy actuator LED
static constexpr uint32_t SERIAL_BAUD = 115200;
static constexpr uint32_t APP_TICK_MS = 20;
static constexpr uint32_t LOG_INTERVAL_MS = 1000;
static constexpr uint32_t BOOTING_DURATION_MS = 3000;
static constexpr uint32_t INPUT_DEBOUNCE_MS = 40;
// Status LED patterns
static constexpr uint32_t BOOT_BLINK_MS = 150;
static constexpr uint32_t LOCAL_BLINK_MS = 700;
static constexpr uint32_t ALERT_STEP_MS = 120;
};
} // namespace sys
// =====================================================
// drv -> hardware boundary
// =====================================================
namespace drv {
class StatusLed {
public:
explicit StatusLed(uint8_t pin) : _pin(pin) {}
void begin() {
pinMode(_pin, OUTPUT);
off();
}
void set(bool on) {
digitalWrite(_pin, on ? HIGH : LOW);
}
void off() {
digitalWrite(_pin, LOW);
}
private:
uint8_t _pin;
};
class DigitalSoilSensor {
public:
DigitalSoilSensor(uint8_t dryPin, uint8_t faultPin)
: _dryPin(dryPin), _faultPin(faultPin) {}
void begin() {
pinMode(_dryPin, INPUT_PULLUP);
pinMode(_faultPin, INPUT_PULLUP);
initChannel(_dryChannel, rawDry());
initChannel(_faultChannel, rawFault());
}
void update(uint32_t nowMs) {
updateChannel(_dryChannel, rawDry(), nowMs);
updateChannel(_faultChannel, rawFault(), nowMs);
}
bool isDry() const {
return _dryChannel.stableState;
}
bool hasFault() const {
return _faultChannel.stableState;
}
private:
struct DebouncedChannel {
bool lastRawState = false;
bool stableState = false;
uint32_t lastChangeMs = 0;
};
uint8_t _dryPin;
uint8_t _faultPin;
DebouncedChannel _dryChannel;
DebouncedChannel _faultChannel;
bool rawDry() const {
return digitalRead(_dryPin) == LOW;
}
bool rawFault() const {
return digitalRead(_faultPin) == LOW;
}
void initChannel(DebouncedChannel& channel, bool initialState) {
channel.lastRawState = initialState;
channel.stableState = initialState;
channel.lastChangeMs = 0;
}
void updateChannel(DebouncedChannel& channel, bool rawState, uint32_t nowMs) {
if (rawState != channel.lastRawState) {
channel.lastRawState = rawState;
channel.lastChangeMs = nowMs;
}
if ((nowMs - channel.lastChangeMs) >= sys::Config::INPUT_DEBOUNCE_MS) {
channel.stableState = rawState;
}
}
};
class ActuatorOutput {
public:
explicit ActuatorOutput(uint8_t pin) : _pin(pin) {}
void begin() {
pinMode(_pin, OUTPUT);
off();
}
void set(bool on) {
digitalWrite(_pin, on ? HIGH : LOW);
_isOn = on;
}
void off() {
set(false);
}
bool isOn() const {
return _isOn;
}
private:
uint8_t _pin;
bool _isOn = false;
};
} // namespace drv
// =====================================================
// svc -> domain logic + driver usage inside service
// =====================================================
namespace svc {
enum class NodeMode : uint8_t {
BOOTING,
MONITORING,
AUTO_ACTIVE,
ALERT
};
enum class RuleEvent : uint8_t {
NONE,
ENTERED_MONITORING,
AUTO_RULE_ACTIVATED,
AUTO_RULE_CLEARED,
FAULT_ENTERED,
FAULT_CLEARED
};
const char* toString(NodeMode mode) {
switch (mode) {
case NodeMode::BOOTING: return "BOOTING";
case NodeMode::MONITORING: return "MONITORING";
case NodeMode::AUTO_ACTIVE: return "AUTO_ACTIVE";
case NodeMode::ALERT: return "ALERT";
default: return "UNKNOWN";
}
}
class SensorRuleService {
public:
SensorRuleService(drv::StatusLed& drv_statusLed,
drv::DigitalSoilSensor& drv_soilSensor,
drv::ActuatorOutput& drv_actuator)
: _drv_statusLed(drv_statusLed),
_drv_soilSensor(drv_soilSensor),
_drv_actuator(drv_actuator) {}
void begin(uint32_t bootStartMs) {
_drv_statusLed.begin();
_drv_soilSensor.begin();
_drv_actuator.begin();
_bootMs = bootStartMs;
_mode = NodeMode::BOOTING;
_lastEvent = RuleEvent::NONE;
}
void run(uint32_t nowMs) {
_lastEvent = RuleEvent::NONE;
_drv_soilSensor.update(nowMs);
const bool sensorDry = _drv_soilSensor.isDry();
const bool sensorFault = _drv_soilSensor.hasFault();
updateMode(nowMs, sensorDry, sensorFault);
applyOutputs(nowMs);
}
NodeMode mode() const {
return _mode;
}
const char* modeText() const {
return toString(_mode);
}
RuleEvent lastEvent() const {
return _lastEvent;
}
bool actuatorOn() const {
return _mode == NodeMode::AUTO_ACTIVE;
}
bool sensorDry() const {
return _drv_soilSensor.isDry();
}
bool sensorFault() const {
return _drv_soilSensor.hasFault();
}
private:
drv::StatusLed& _drv_statusLed;
drv::DigitalSoilSensor& _drv_soilSensor;
drv::ActuatorOutput& _drv_actuator;
NodeMode _mode = NodeMode::BOOTING;
uint32_t _bootMs = 0;
RuleEvent _lastEvent = RuleEvent::NONE;
void updateMode(uint32_t nowMs, bool sensorDry, bool sensorFault) {
if (_mode == NodeMode::BOOTING) {
if ((nowMs - _bootMs) < sys::Config::BOOTING_DURATION_MS) {
return;
}
if (sensorFault) {
_mode = NodeMode::ALERT;
_lastEvent = RuleEvent::FAULT_ENTERED;
} else if (sensorDry) {
_mode = NodeMode::AUTO_ACTIVE;
_lastEvent = RuleEvent::AUTO_RULE_ACTIVATED;
} else {
_mode = NodeMode::MONITORING;
_lastEvent = RuleEvent::ENTERED_MONITORING;
}
return;
}
if (sensorFault) {
if (_mode != NodeMode::ALERT) {
_mode = NodeMode::ALERT;
_lastEvent = RuleEvent::FAULT_ENTERED;
}
return;
}
if (_mode == NodeMode::ALERT && !sensorFault) {
if (sensorDry) {
_mode = NodeMode::AUTO_ACTIVE;
} else {
_mode = NodeMode::MONITORING;
}
_lastEvent = RuleEvent::FAULT_CLEARED;
return;
}
if (sensorDry) {
if (_mode != NodeMode::AUTO_ACTIVE) {
_mode = NodeMode::AUTO_ACTIVE;
_lastEvent = RuleEvent::AUTO_RULE_ACTIVATED;
}
} else {
if (_mode != NodeMode::MONITORING) {
_mode = NodeMode::MONITORING;
_lastEvent = RuleEvent::AUTO_RULE_CLEARED;
}
}
}
void applyOutputs(uint32_t nowMs) {
_drv_statusLed.set(statusLedState(nowMs));
_drv_actuator.set(actuatorOn());
}
bool statusLedState(uint32_t nowMs) const {
switch (_mode) {
case NodeMode::BOOTING:
return ((nowMs / sys::Config::BOOT_BLINK_MS) % 2U) == 0U;
case NodeMode::MONITORING:
return ((nowMs / sys::Config::LOCAL_BLINK_MS) % 2U) == 0U;
case NodeMode::AUTO_ACTIVE:
return true;
case NodeMode::ALERT: {
const uint8_t phase = (nowMs / sys::Config::ALERT_STEP_MS) % 6U;
return (phase == 0U || phase == 1U);
}
}
return false;
}
};
} // namespace svc
// =====================================================
// app -> pure orchestrator
// =====================================================
namespace app {
class FirmwareApp {
public:
explicit FirmwareApp(svc::SensorRuleService& svc_sensorRuleService)
: _svc_sensorRuleService(svc_sensorRuleService) {}
void begin() {
Serial.begin(sys::Config::SERIAL_BAUD);
delay(100);
_svc_sensorRuleService.begin(millis());
Serial.println();
Serial.println("=== HortiLink Level 3 ===");
Serial.println("Sensor and Rule");
Serial.println("Board : ESP32-C3 DevKit");
Serial.println("Sensor Dry : GPIO5 -> GND");
Serial.println("Sensor Fault : GPIO6 -> GND");
Serial.println("Actuator Out : GPIO7");
Serial.println("Mode : BOOTING -> MONITORING -> AUTO_ACTIVE / ALERT");
Serial.println();
}
void run() {
const uint32_t nowMs = millis();
_svc_sensorRuleService.run(nowMs);
logEventIfAny();
if (nowMs - _lastLogMs >= sys::Config::LOG_INTERVAL_MS) {
_lastLogMs = nowMs;
logHeartbeat(nowMs);
}
delay(sys::Config::APP_TICK_MS);
}
private:
svc::SensorRuleService& _svc_sensorRuleService;
uint32_t _lastLogMs = 0;
void logEventIfAny() {
switch (_svc_sensorRuleService.lastEvent()) {
case svc::RuleEvent::ENTERED_MONITORING:
Serial.print("[MODE] -> ");
Serial.println(_svc_sensorRuleService.modeText());
break;
case svc::RuleEvent::AUTO_RULE_ACTIVATED:
Serial.print("[MODE] -> ");
Serial.println(_svc_sensorRuleService.modeText());
Serial.println("[RULE] sensor=DRY | actuator=ON");
break;
case svc::RuleEvent::AUTO_RULE_CLEARED:
Serial.print("[MODE] -> ");
Serial.println(_svc_sensorRuleService.modeText());
Serial.println("[RULE] sensor=NORMAL | actuator=OFF");
break;
case svc::RuleEvent::FAULT_ENTERED:
Serial.print("[MODE] -> ");
Serial.println(_svc_sensorRuleService.modeText());
Serial.println("[ALARM] sensor fault detected | actuator=OFF");
break;
case svc::RuleEvent::FAULT_CLEARED:
Serial.print("[MODE] -> ");
Serial.println(_svc_sensorRuleService.modeText());
Serial.println("[ALARM] sensor fault cleared");
break;
case svc::RuleEvent::NONE:
default:
break;
}
}
void logHeartbeat(uint32_t nowMs) {
Serial.print("[HEARTBEAT] uptime=");
Serial.print(nowMs);
Serial.print(" ms | mode=");
Serial.print(_svc_sensorRuleService.modeText());
Serial.print(" | sensor=");
Serial.print(_svc_sensorRuleService.sensorDry() ? "DRY" : "NORMAL");
Serial.print(" | fault=");
Serial.print(_svc_sensorRuleService.sensorFault() ? "YES" : "NO");
Serial.print(" | actuator=");
Serial.println(_svc_sensorRuleService.actuatorOn() ? "ON" : "OFF");
}
};
} // namespace app
// =====================================================
// composition root
// =====================================================
drv::StatusLed drv_statusLed(sys::Config::PIN_STATUS_LED);
drv::DigitalSoilSensor drv_soilSensor(
sys::Config::PIN_SENSOR_DRY,
sys::Config::PIN_SENSOR_FAULT
);
drv::ActuatorOutput drv_actuator(sys::Config::PIN_ACTUATOR);
svc::SensorRuleService svc_sensorRuleService(
drv_statusLed,
drv_soilSensor,
drv_actuator
);
app::FirmwareApp app_firmware(svc_sensorRuleService);
// =====================================================
// Arduino entry point
// =====================================================
void setup() {
app_firmware.begin();
}
void loop() {
app_firmware.run();
}
10. Cara Kerja Firmware
10.1 Saat board menyala
setup() memanggil:
app_firmware.begin();
Di dalam begin():
- Serial dimulai
- service sensor-rule dimulai
- service menginisialisasi driver yang dibutuhkannya
- informasi node dicetak ke Serial
10.2 Saat sensor mendeteksi kondisi dry
Jika sensor lingkungan digital proxy aktif, maka service menganggap kondisi lingkungan sebagai DRY.
Perilakunya:
- mode pindah ke
AUTO_ACTIVE - aktuator dinyalakan
- status LED menyala stabil
- serial log mencatat bahwa rule aktif
10.3 Saat sensor fault aktif
Jika input fault simulasi aktif, maka service menganggap sensor sedang fault.
Perilakunya:
- mode pindah ke
ALERT - aktuator dipaksa OFF
- status LED masuk pola alert
- serial log mencatat fault state
10.4 Saat loop berjalan
loop() memanggil:
app_firmware.run();
Di dalamnya:
- waktu sekarang dibaca dengan
millis() - app menjalankan service
- service membaca sensor melalui driver
- service menghitung keputusan mode dan output
- service menerapkan output ke driver
- app hanya melakukan logging dan mengatur ritme runtime
11. Yang Dipelajari dari Level 3
11.1 Sensor acquisition dibungkus di drv
Pada Level 3, driver baru yang diperkenalkan adalah:
drv::DigitalSoilSensor
Driver ini membungkus:
- input sensor lingkungan digital proxy
- input fault simulasi
- debounce input
Dengan begitu, pembacaan sensor tetap berada di hardware boundary.
11.2 Rule engine diselesaikan di svc
Pada Level 3, svc memegang peran utama:
svc::SensorRuleService
Service ini bertugas:
- membaca status driver yang relevan
- menentukan mode node
- memutuskan kapan aktuator ON/OFF
- memutuskan kapan LED status ON/OFF
- memutuskan kapan node masuk
ALERT
Di sinilah inti HN-06 mulai terlihat.
11.3 Fault state menjadi bagian dari service domain
Feature HN-09 tidak diletakkan di drv dan tidak diletakkan di app.
drv hanya membaca bahwa fault input aktif atau tidak. svc yang memutuskan bahwa kondisi itu berarti:
- node harus masuk
ALERT - aktuator harus dimatikan
- output lokal harus berubah
Ini adalah pemisahan yang penting.
11.4 app tetap murni sebagai orchestrator
Pada Level 3, app tidak menghitung rule dan tidak menyimpan state machine domain.
app hanya:
- memulai service
- menjalankan service
- logging
- mengatur ritme runtime
Dengan begitu, layering tetap konsisten.
12. Output yang Diharapkan
12.1 Serial Monitor
Contoh log yang diharapkan:
=== HortiLink Level 3 ===
Sensor and Rule
Board : ESP32-C3 DevKit
Sensor Dry : GPIO5 -> GND
Sensor Fault : GPIO6 -> GND
Actuator Out : GPIO7
Mode : BOOTING -> MONITORING -> AUTO_ACTIVE / ALERT
[HEARTBEAT] uptime=1000 ms | mode=BOOTING | sensor=NORMAL | fault=NO | actuator=OFF
[HEARTBEAT] uptime=2000 ms | mode=BOOTING | sensor=NORMAL | fault=NO | actuator=OFF
[MODE] -> MONITORING
[HEARTBEAT] uptime=4000 ms | mode=MONITORING | sensor=NORMAL | fault=NO | actuator=OFF
[MODE] -> AUTO_ACTIVE
[RULE] sensor=DRY | actuator=ON
[HEARTBEAT] uptime=7000 ms | mode=AUTO_ACTIVE | sensor=DRY | fault=NO | actuator=ON
[MODE] -> ALERT
[ALARM] sensor fault detected | actuator=OFF
12.2 Status LED GPIO8
- saat
BOOTING→ kedip cepat - saat
MONITORING→ kedip lambat - saat
AUTO_ACTIVE→ nyala stabil - saat
ALERT→ kedip dua kali berulang
12.3 Actuator LED GPIO7
- saat
MONITORING→ OFF - saat
AUTO_ACTIVE→ ON - saat
ALERT→ OFF
Dengan begitu, engineer bisa melihat hubungan yang jelas antara:
- kondisi sensor
- keputusan service
- mode node
- output aktuator
- indikator lokal
13. Inti Level 3
Level 3 ini bukan sekadar menambah sensor.
Ini adalah tahap ketika node HortiLink mulai benar-benar menunjukkan perilaku dasar edge automation:
- membaca kondisi lingkungan
- memutuskan tindakan otomatis
- mengubah mode operasi
- mendeteksi fault
- memberi indikasi lokal
Pada Level 3, engineer mulai melihat bentuk penuh layering HortiLink yang sekarang dikunci:
drvtahu hardwaresvctahudrvdan menyelesaikan logic domainapphanya mengorkestrasi servicesysmenjaga semua konfigurasi tetap terkumpul
Dengan kata lain, Level 3 adalah jembatan antara:
- node yang hanya punya interaksi lokal
- dan node yang mulai punya kontrol otomatis lokal yang berbasis sensor
Catatan Penyusunan Artikel ini merupakan kelanjutan langsung dari Level 2 dan ditulis khusus sebagai dokumen pengembangan untuk Level 3 pada platform ESP32-C3 DevKit di simulator Velxio. Isi level ini disusun agar tetap konsisten dengan README utama HortiLink, single-project learning flow, dan standar layering yang sudah dikunci: sys, drv, svc, app.