IoT Dashboard - Bảng điều khiển tối thượng

AI Hunter

Member
Hôm nay chúng ta sẽ vẽ giao diện điều khiển (UI) cho căn nhà. Không cần nói nhiều, nhìn phát biết ngay đèn nào đang bật, quạt nào đang quay.

IoT Dashboard - Bảng điều khiển tối thượng.jpg

1. Nâng cấp Backend (API quản lý thiết bị)​


Trước khi vẽ nút bấm, Backend phải biết trạng thái của các thiết bị.
Mở file backend/server.py và thêm logic quản lý trạng thái đơn giản (giả lập):

Python:
from pydantic import BaseModel

# --- DATA MODEL ---
class DeviceRequest(BaseModel):
    device_id: str
    status: bool

# Lưu trạng thái thiết bị trong RAM (Tắt Server là mất, muốn bền vững thì dùng Database)
# Mặc định: Đèn tắt (False), Quạt tắt (False)
home_state = {
    "light_living_room": False,
    "fan_bedroom": False,
    "pump_garden": False
}

# --- ENDPOINTS ---

# 1. Lấy trạng thái tất cả thiết bị
@app.get("/devices")
async def get_devices():
    return home_state

# 2. Điều khiển thiết bị (Bật/Tắt)
@app.post("/control-device")
async def control_device(req: DeviceRequest):
    if req.device_id in home_state:
        home_state[req.device_id] = req.status
       
        # [TODO] Chỗ này sẽ chèn code gửi lệnh MQTT tới ESP32
        # Ví dụ: mqtt_client.publish(f"home/{req.device_id}", "ON" if req.status else "OFF")
       
        print(f"🔌 Thiết bị {req.device_id} -> {req.status}")
        return {"message": "Success", "state": home_state}
    return {"message": "Device not found"}

Restart Backend: docker-compose restart backend.

2. Code giao diện Dashboard (React Native)​


Để đơn giản hóa (không cần cài thêm bộ điều hướng phức tạp như React Navigation), chúng ta sẽ dùng kỹ thuật "Conditional Rendering" (Render có điều kiện) để chuyển đổi giữa màn hình Chat và Dashboard.

Mở file App.js và sửa lại cấu trúc như sau:

Bước 1: Import thêm Icon và Switch
JavaScript:
import { Switch, ScrollView } from 'react-native'; // Thêm Switch, ScrollView
import {
  Mic, Send, Volume2, VolumeX, Camera, X,
  Home, MessageSquare, Lightbulb, Fan, Droplets, Power // Thêm bộ icon mới
} from 'lucide-react-native';

Bước 2: Tạo Component `DashboardScreen` (Viết ngay bên trên hàm `App`)

JavaScript:
const DashboardScreen = ({ apiUrl, apiToken }) => {
  const [devices, setDevices] = useState({
    light_living_room: false,
    fan_bedroom: false,
    pump_garden: false
  });

  // Lấy trạng thái khi mở màn hình
  useEffect(() => {
    fetchStatus();
  }, []);

  const fetchStatus = async () => {
    try {
      const res = await axios.get(`${apiUrl}/devices`);
      setDevices(res.data);
    } catch (e) {
      console.error("Lỗi tải trạng thái:", e);
    }
  };

  const toggleDevice = async (deviceId) => {
    // 1. Cập nhật UI ngay lập tức cho mượt (Optimistic Update)
    const newState = !devices[deviceId];
    setDevices(prev => ({ ...prev, [deviceId]: newState }));

    // 2. Gửi lệnh về Server
    try {
      await axios.post(`${apiUrl}/control-device`, {
        device_id: deviceId,
        status: newState
      }, { headers: { Authorization: `Bearer ${apiToken}` } });
    } catch (e) {
      console.error("Lỗi điều khiển:", e);
      // Nếu lỗi thì revert lại UI (Optional)
    }
  };

  // Component hiển thị từng thẻ thiết bị
  const DeviceCard = ({ id, name, icon: Icon, isOn }) => (
    <TouchableOpacity
      style={[styles.card, isOn && styles.cardOn]}
      onPress={() => toggleDevice(id)}
      activeOpacity={0.8}
    >
      <View style={styles.cardHeader}>
        <Icon color={isOn ? "#000" : "#00f0ff"} size={32} />
        <Switch
          value={isOn}
          onValueChange={() => toggleDevice(id)}
          trackColor={{ false: "#333", true: "#000" }}
          thumbColor={isOn ? "#fff" : "#555"}
        />
      </View>
      <Text style={[styles.cardTitle, isOn && {color: '#000'}]}>{name}</Text>
      <Text style={[styles.cardStatus, isOn && {color: '#000'}]}>
        {isOn ? "ON" : "OFF"}
      </Text>
    </TouchableOpacity>
  );

  return (
    <ScrollView contentContainerStyle={styles.dashboardContainer}>
      <Text style={styles.sectionTitle}>Living Room</Text>
      <View style={styles.grid}>
        <DeviceCard id="light_living_room" name="Đèn Trần" icon={Lightbulb} isOn={devices.light_living_room} />
        <DeviceCard id="fan_bedroom" name="Quạt Ngủ" icon={Fan} isOn={devices.fan_bedroom} />
      </View>

      <Text style={styles.sectionTitle}>Garden</Text>
      <View style={styles.grid}>
        <DeviceCard id="pump_garden" name="Bơm Nước" icon={Droplets} isOn={devices.pump_garden} />
        {/* Thêm thiết bị ảo để test giao diện */}
        <DeviceCard id="test_device" name="Hệ thống AI" icon={Power} isOn={true} />
      </View>
    </ScrollView>
  );
};

Bước 3: Cập nhật hàm `App` chính để chuyển tab

JavaScript:
export default function App() {
  // ... (Các state cũ giữ nguyên: messages, input, permission...) ...
  const [currentTab, setCurrentTab] = useState('chat'); // State để quản lý Tab hiện tại ('chat' hoặc 'home')

  // ... (Giữ nguyên các hàm sendMessage, uploadAudio, takePicture...) ...

  // PHẦN RENDER UI
  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="light-content" />
     
      {/* HEADER */}
      <View style={styles.header}>
        <Text style={styles.headerTitle}>J.A.R.V.I.S</Text>
        {/* Nút Mute cũ vẫn để đây */}
      </View>

      {/* BODY - Thay đổi nội dung dựa theo Tab */}
      <View style={{flex: 1}}>
        {currentTab === 'chat' ? (
           // --- TOÀN BỘ GIAO DIỆN CHAT CŨ BỎ VÀO ĐÂY ---
           <>
             {/* Nếu showCamera thì hiện Camera */}
             {showCamera ? (
                /* ... Code Camera cũ ... */
                <View><Text>Camera Mode (Copy code cũ vào đây)</Text></View>
             ) : (
                <>
                  <FlatList /* ... */ />
                  <View style={styles.inputContainer}> /* ... */ </View>
                </>
             )}
           </>
        ) : (
           // --- GIAO DIỆN DASHBOARD MỚI ---
           <DashboardScreen apiUrl={API_URL} apiToken={API_TOKEN} />
        )}
      </View>

      {/* BOTTOM NAVIGATION BAR */}
      <View style={styles.bottomNav}>
        <TouchableOpacity
          style={styles.navItem}
          onPress={() => setCurrentTab('chat')}
        >
          <MessageSquare color={currentTab === 'chat' ? "#00f0ff" : "#555"} size={28} />
          <Text style={[styles.navText, currentTab === 'chat' && styles.activeNavText]}>Chat</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={styles.navItem}
          onPress={() => setCurrentTab('home')}
        >
          <Home color={currentTab === 'home' ? "#00f0ff" : "#555"} size={28} />
          <Text style={[styles.navText, currentTab === 'home' && styles.activeNavText]}>Home</Text>
        </TouchableOpacity>
      </View>

    </SafeAreaView>
  );
}

Bước 4: Thêm Styles cho Dashboard

JavaScript:
const styles = StyleSheet.create({
  // ... Styles cũ giữ nguyên ...

  // Style Bottom Nav
  bottomNav: {
    flexDirection: 'row',
    borderTopWidth: 1,
    borderTopColor: '#333',
    backgroundColor: '#050505',
    paddingBottom: 20, // Cho dòng iPhone đời mới
    paddingTop: 10,
  },
  navItem: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  navText: {
    color: '#555',
    fontSize: 10,
    marginTop: 4,
  },
  activeNavText: {
    color: '#00f0ff',
    fontWeight: 'bold',
  },

  // Style Dashboard
  dashboardContainer: {
    padding: 20,
  },
  sectionTitle: {
    color: '#fff',
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 15,
    marginTop: 10,
    borderLeftWidth: 3,
    borderLeftColor: '#00f0ff',
    paddingLeft: 10,
  },
  grid: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'space-between',
  },
  card: {
    width: '48%', // Chia đôi màn hình
    backgroundColor: '#1a1a1a',
    borderRadius: 15,
    padding: 15,
    marginBottom: 15,
    borderWidth: 1,
    borderColor: '#333',
  },
  cardOn: {
    backgroundColor: '#00f0ff', // Khi bật thì sáng màu Cyan
    borderColor: '#00f0ff',
    shadowColor: "#00f0ff",
    shadowOffset: { width: 0, height: 0 },
    shadowOpacity: 0.5,
    shadowRadius: 10,
    elevation: 5,
  },
  cardHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 20,
  },
  cardTitle: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
  },
  cardStatus: {
    color: '#888',
    fontSize: 12,
    marginTop: 5,
  }
});

3. Kết quả​

  1. Reload App.
  2. Bạn sẽ thấy bên dưới cùng có 2 nút: ChatHome.
  3. Bấm vào Home.
  4. Một bảng điều khiển hiện ra với các nút: Đèn Trần, Quạt Ngủ, Bơm Nước.
  5. Bấm vào nút Đèn Trần:
  6. Nút chuyển sang màu xanh Cyan rực rỡ (Neon).
  7. Trạng thái chuyển thành "ON".
  8. Server ghi nhận trạng thái mới.

Tổng kết​


Vậy là Jarvis Mobile App đã hoàn thiện 99% các tính năng cơ bản của một Smart Home:
  • Chat Tab: Nơi ra lệnh bằng giọng nói, hỏi đáp, nhận diện hình ảnh.
  • Home Tab: Nơi điều khiển thủ công, xem trạng thái thiết bị.

Chúng ta chỉ còn một mảnh ghép cuối cùng để biến đây thành một hệ thống "bất khả xâm phạm". Đó là **Bảo mật**.
Bạn đâu muốn ai mượn điện thoại cũng vào nghịch tắt đèn nhà bạn, đúng không?
 
Back
Top