mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-08-28 23:00:19 +00:00
Develop team (#1)
* draft * Adjust IME candidate scrolling and space behavior * Remove IME buffer label and update remaining counter * Align Meshtastic handling and stabilize chat UI - Align Meshtastic send/receive flow: add routing ACK decoding/log mapping, ack timeouts, channel tracking, and richer RX/TX diagnostics. - Persist and manage PKI peer keys; add PKI backoff on NO_CHANNEL/UNKNOWN_PUBKEY and fall back to PSK when needed. - Improve chat UI state handling: safer app switching with exit callbacks, active app clearing on menu return, and safer chat container cleanup. - Fix Contacts → Compose parent selection to avoid UI tree mismatch; add broadcast compose support and refine action button behavior. - Add UI debug logging for screen/parent validity and child counts to diagnose black-screen/stacking issues. - Apply timezone offset helpers for display-time formatting and reuse in screenshot timestamps. * Add changelog entry and time helpers
This commit is contained in:
@@ -18,6 +18,20 @@ versions.
|
||||
- Meshtastic encryption (AES/PSK) and broader protocol compatibility / Meshtastic 加密(AES/PSK)与更完整协议兼容
|
||||
- Real LoRa field tests, performance, and power tuning / 实机 LoRa 测试、性能与功耗调优
|
||||
|
||||
## [0.1.2-alpha] - 2026-01-21
|
||||
### Added / 新增
|
||||
- Meshtastic routing ACK decoding with reason mapping, ack timeouts, and richer RX/TX diagnostics / Meshtastic 路由 ACK 解码,增加原因映射、ACK 超时和更详细的收发日志
|
||||
- PKI peer key persistence and per-node PKI backoff after NO_CHANNEL/UNKNOWN_PUBKEY / PKI 公钥持久化,对 NO_CHANNEL/UNKNOWN_PUBKEY 加入单节点 PKI 退避
|
||||
- Time helpers for chat timestamps / 聊天时间戳辅助工具
|
||||
|
||||
### Changed / 变更
|
||||
- Chat app switching now calls exit callbacks and clears active app on menu return / Chat 应用切换支持 exit 回调,返回菜单清空 active app
|
||||
- Contacts compose flow supports broadcast channels and avoids UI tree mismatch when chat container exists / Contacts 撰写支持广播频道,在 chat container 存在时避免 UI 树错位
|
||||
- UI timestamp display applies stored timezone offset / UI 显示时间应用已存储时区偏移
|
||||
|
||||
### Fixed / 修复
|
||||
- Chat screen cleanup safety on repeated enter/exit to prevent black-screen stacking / Chat 重复进出时更安全清理,避免黑屏堆叠
|
||||
|
||||
## [0.1.1-alpha] - 2026-01-20
|
||||
### Added / 新增
|
||||
- Mesh protocol factory with explicit Meshtastic/MeshCore selection / Mesh 协议工厂,支持显式选择 Meshtastic/MeshCore
|
||||
|
||||
Binary file not shown.
+2845
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
# Team 实现清单(按你的架构分层)
|
||||
|
||||
目标:参考现有 chat 的分层方式,在 Team 功能上采用同样的分层结构,并显式对齐「解释即架构」的开发思想。以下清单按层拆解,便于逐项落地与验收。
|
||||
|
||||
## 1) Domain(纯模型层)
|
||||
|
||||
- team/domain/team_types.h
|
||||
- 基础类型:TeamId, TeamKey, TeamRole, TeamState, TeamMember, TeamParams
|
||||
- 事件类型枚举:TeamEventType
|
||||
- 事件载体:TeamEvent(包含 event_id/team_id/ts/sender_id/key_id/payload)
|
||||
- team/domain/team_model.h/.cpp
|
||||
- 状态机:Idle/Create/Active/Rotate/Disband
|
||||
- 成员可见性/last_seen/health
|
||||
- 位置降采样策略
|
||||
- 复盘事件生成(纯内存结构,不涉及存储)
|
||||
|
||||
## 2) Ports(接口层)
|
||||
|
||||
- team/ports/i_team_store.h
|
||||
- 读写 PersistentTeamState(单一对象)
|
||||
- load/save/erase
|
||||
- team/ports/i_team_log_store.h
|
||||
- 追加写 TeamEvent(LogRecord)
|
||||
- appendEvent/appendSnapshot(snapshot 可选)
|
||||
- team/ports/i_team_mesh_adapter.h
|
||||
- sendAppData(portnum, payload, len, dest, want_ack)
|
||||
- pollIncomingData 返回 MeshIncomingData
|
||||
- team/ports/i_team_crypto.h
|
||||
- kdf, aeadEncrypt, aeadDecrypt
|
||||
|
||||
## 3) Usecase(用例层)
|
||||
|
||||
- team/usecase/team_service.h/.cpp
|
||||
- createTeam/joinTeam/acceptJoin/confirmJoin
|
||||
- rotateKey/disband/leave
|
||||
- processIncoming:消费 TEAM_*_APP 数据包
|
||||
- 严格遵守持久化时机(成功跃迁后写入,解散开始即删除)
|
||||
- 事件落盘(LogStore)与 EventBus 通知
|
||||
|
||||
## 4) Infra(基础设施层)
|
||||
|
||||
- 存储
|
||||
- team/infra/store/team_flash_store.*:NVS/Preferences 单 blob(含 magic/version/length/crc)
|
||||
- team/infra/store/team_log_store.*:SD append-only LogRecord(AEAD + CRC)
|
||||
- Meshtastic 适配
|
||||
- team/infra/meshtastic/team_mt_adapter.* 或复用 MtAdapter 新增 pollIncomingData/sendAppData
|
||||
- 解析 meshtastic_Data 的 portnum,将非 TEXT 的 payload 入队
|
||||
- 定义 TEAM_*_APP 自定义 portnum(基于 PortNum_PRIVATE_APP + n)
|
||||
- 密码学
|
||||
- team/infra/crypto/team_crypto_*:KDF + AEAD(与 TeamEncrypted 结构匹配)
|
||||
|
||||
## 5) AppContext 与任务流
|
||||
|
||||
- app/app_context.*
|
||||
- 新增 TeamModel, TeamService, TeamStore, TeamLogStore
|
||||
- AppContext::update 调用 team_service_->processIncoming
|
||||
- sys/event_bus.h
|
||||
- 新增 Team 相关事件类型(状态变化、成员变化、指令、诊断)
|
||||
|
||||
## 6) 协议与编码
|
||||
|
||||
- TeamEncrypted 统一 Envelope(version/team_id/key_id/nonce/ciphertext/aad_flags)
|
||||
- TEAM_MGMT_APP
|
||||
- TEAM_ADVERTISE, JOIN_REQUEST, JOIN_ACCEPT, JOIN_CONFIRM, TEAM_STATUS
|
||||
- TEAM_POSITION_APP / TEAM_WAYPOINT_APP
|
||||
- 明文 payload 为 meshtastic Position/Waypoint(再用 TeamEncrypted AEAD)
|
||||
|
||||
## 7) 持久化与日志
|
||||
|
||||
- PersistentTeamState 单对象
|
||||
- identity + secrets + channel + params + log_*
|
||||
- 启动恢复
|
||||
- magic/crc 失败 -> erase -> Idle
|
||||
- LogRecord
|
||||
- AEAD(LogKey) + trailer_crc32
|
||||
- 关键事件即时 flush
|
||||
|
||||
## 8) MVP 优先级(建议)
|
||||
|
||||
1. TeamService + TeamModel(状态机)
|
||||
2. Meshtastic adapter 支持 pollIncomingData/sendAppData
|
||||
3. TEAM_ADVERTISE / JOIN_REQUEST / JOIN_ACCEPT / TEAM_POSITION_APP
|
||||
4. NVS 持久化单对象
|
||||
5. SD LogRecord 追加写(无 snapshot)
|
||||
6. JOIN_CONFIRM / TEAM_STATUS / ROTATE
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
+24
-2
@@ -8,6 +8,7 @@
|
||||
#include "../gps/usecase/gps_service.h"
|
||||
#include "../sys/event_bus.h"
|
||||
#include "../ui/widgets/system_notification.h"
|
||||
#include "../ui/ui_common.h"
|
||||
#include "app_tasks.h"
|
||||
#include <SD.h>
|
||||
|
||||
@@ -27,6 +28,7 @@ bool AppContext::init(TLoRaPagerBoard& board, bool use_mock_adapter, uint32_t di
|
||||
|
||||
// Load configuration
|
||||
config_.load(preferences_);
|
||||
(void)ui_get_timezone_offset_min();
|
||||
|
||||
gps::GpsService::getInstance().begin(
|
||||
board,
|
||||
@@ -75,6 +77,13 @@ bool AppContext::init(TLoRaPagerBoard& board, bool use_mock_adapter, uint32_t di
|
||||
chat_service_ = std::make_unique<chat::ChatService>(
|
||||
*chat_model_, *mesh_adapter_, *chat_store_);
|
||||
|
||||
// Create team service (protocol-only for now)
|
||||
team_crypto_ = std::make_unique<team::infra::TeamCrypto>();
|
||||
team_event_sink_ = std::make_unique<team::infra::TeamEventBusSink>();
|
||||
team_service_ = std::make_unique<team::TeamService>(
|
||||
*team_crypto_, *mesh_adapter_, *team_event_sink_);
|
||||
team_controller_ = std::make_unique<team::TeamController>(*team_service_);
|
||||
|
||||
// Load persisted messages into model (no unread)
|
||||
if (flash_store_)
|
||||
{
|
||||
@@ -91,7 +100,7 @@ bool AppContext::init(TLoRaPagerBoard& board, bool use_mock_adapter, uint32_t di
|
||||
{
|
||||
chat_model_->onIncoming(msg);
|
||||
}
|
||||
chat::ConversationId conv(msg.channel, msg.peer ? msg.peer : msg.from);
|
||||
chat::ConversationId conv(msg.channel, msg.peer);
|
||||
touched.push_back(conv);
|
||||
}
|
||||
for (const auto& conv : touched)
|
||||
@@ -119,6 +128,10 @@ void AppContext::update()
|
||||
{
|
||||
chat_service_->processIncoming();
|
||||
}
|
||||
if (team_service_)
|
||||
{
|
||||
team_service_->processIncoming();
|
||||
}
|
||||
|
||||
// Update UI controller
|
||||
if (ui_controller_)
|
||||
@@ -156,7 +169,16 @@ void AppContext::update()
|
||||
}
|
||||
|
||||
// Show system notification
|
||||
ui::SystemNotification::show(msg_event->text, 10000);
|
||||
ui::SystemNotification::show(msg_event->text, 3000);
|
||||
break;
|
||||
}
|
||||
case sys::EventType::ChatSendResult:
|
||||
{
|
||||
sys::ChatSendResultEvent* result_event = (sys::ChatSendResultEvent*)event;
|
||||
if (chat_service_)
|
||||
{
|
||||
chat_service_->handleSendResult(result_event->msg_id, result_event->success);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case sys::EventType::NodeInfoUpdate:
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include "../chat/domain/chat_model.h"
|
||||
#include "../chat/usecase/chat_service.h"
|
||||
#include "../chat/usecase/contact_service.h"
|
||||
#include "../team/infra/crypto/team_crypto.h"
|
||||
#include "../team/infra/event/team_event_bus_sink.h"
|
||||
#include "../team/usecase/team_controller.h"
|
||||
#include "../team/usecase/team_service.h"
|
||||
|
||||
namespace app
|
||||
{
|
||||
@@ -75,6 +79,14 @@ class AppContext
|
||||
return ui_controller_.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get team controller (UI/state entry points)
|
||||
*/
|
||||
team::TeamController* getTeamController()
|
||||
{
|
||||
return team_controller_.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get configuration
|
||||
*/
|
||||
@@ -140,6 +152,10 @@ class AppContext
|
||||
// Use case
|
||||
std::unique_ptr<chat::ChatService> chat_service_;
|
||||
std::unique_ptr<chat::contacts::ContactService> contact_service_;
|
||||
std::unique_ptr<team::infra::TeamCrypto> team_crypto_;
|
||||
std::unique_ptr<team::infra::TeamEventBusSink> team_event_sink_;
|
||||
std::unique_ptr<team::TeamService> team_service_;
|
||||
std::unique_ptr<team::TeamController> team_controller_;
|
||||
|
||||
// UI
|
||||
std::unique_ptr<chat::ui::UiController> ui_controller_;
|
||||
|
||||
@@ -18,7 +18,7 @@ ChatModel::~ChatModel()
|
||||
|
||||
void ChatModel::onIncoming(const ChatMessage& msg)
|
||||
{
|
||||
ConversationId conv(msg.channel, msg.peer ? msg.peer : msg.from);
|
||||
ConversationId conv(msg.channel, msg.peer);
|
||||
ConversationData& data = getConvData(conv);
|
||||
|
||||
data.messages.append(msg);
|
||||
@@ -41,7 +41,10 @@ void ChatModel::onSendQueued(const ChatMessage& msg)
|
||||
{
|
||||
copy.msg_id = next_msg_id_++;
|
||||
}
|
||||
copy.status = MessageStatus::Queued;
|
||||
if (copy.status != MessageStatus::Failed)
|
||||
{
|
||||
copy.status = MessageStatus::Queued;
|
||||
}
|
||||
|
||||
data.messages.append(copy);
|
||||
data.preview = copy.text;
|
||||
@@ -56,14 +59,13 @@ void ChatModel::onSendResult(MessageId msg_id, bool ok)
|
||||
ConversationData& data = pair.second;
|
||||
for (size_t i = 0; i < data.messages.count(); i++)
|
||||
{
|
||||
const ChatMessage* msg = data.messages.get(i);
|
||||
ChatMessage* msg = data.messages.get(i);
|
||||
if (msg && msg->msg_id == msg_id)
|
||||
{
|
||||
msg->status = ok ? MessageStatus::Sent : MessageStatus::Failed;
|
||||
if (!ok)
|
||||
{
|
||||
ChatMessage failed_msg = *msg;
|
||||
failed_msg.status = MessageStatus::Failed;
|
||||
failed_messages_.append(failed_msg);
|
||||
failed_messages_.append(*msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <Arduino.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace chat
|
||||
{
|
||||
@@ -108,6 +109,7 @@ struct MeshIncomingText
|
||||
{
|
||||
ChannelId channel;
|
||||
NodeId from;
|
||||
NodeId to;
|
||||
MessageId msg_id;
|
||||
uint32_t timestamp;
|
||||
std::string text;
|
||||
@@ -115,6 +117,26 @@ struct MeshIncomingText
|
||||
bool encrypted; // Whether message was encrypted
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Incoming non-text mesh payload
|
||||
*/
|
||||
struct MeshIncomingData
|
||||
{
|
||||
uint32_t portnum;
|
||||
NodeId from;
|
||||
NodeId to;
|
||||
MessageId packet_id;
|
||||
ChannelId channel;
|
||||
uint8_t channel_hash;
|
||||
bool want_response;
|
||||
std::vector<uint8_t> payload;
|
||||
|
||||
MeshIncomingData()
|
||||
: portnum(0), from(0), to(0), packet_id(0),
|
||||
channel(ChannelId::PRIMARY), channel_hash(0),
|
||||
want_response(false) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Mesh configuration
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
#include "meshcore_adapter.h"
|
||||
#include "../../time_utils.h"
|
||||
#include <Arduino.h>
|
||||
#include <cstring>
|
||||
|
||||
@@ -189,6 +190,25 @@ bool MeshCoreAdapter::pollIncomingText(MeshIncomingText* out)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MeshCoreAdapter::sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest, bool want_ack)
|
||||
{
|
||||
(void)channel;
|
||||
(void)portnum;
|
||||
(void)payload;
|
||||
(void)len;
|
||||
(void)dest;
|
||||
(void)want_ack;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MeshCoreAdapter::pollIncomingData(MeshIncomingData* out)
|
||||
{
|
||||
(void)out;
|
||||
return false;
|
||||
}
|
||||
|
||||
void MeshCoreAdapter::applyConfig(const MeshConfig& config)
|
||||
{
|
||||
config_ = config;
|
||||
@@ -263,8 +283,9 @@ void MeshCoreAdapter::handleRawPacket(const uint8_t* data, size_t size)
|
||||
MeshIncomingText incoming;
|
||||
incoming.channel = ChannelId::PRIMARY;
|
||||
incoming.from = 0;
|
||||
incoming.to = 0xFFFFFFFF;
|
||||
incoming.msg_id = next_msg_id_++;
|
||||
incoming.timestamp = millis() / 1000;
|
||||
incoming.timestamp = now_message_timestamp();
|
||||
incoming.text.assign(reinterpret_cast<const char*>(payload), payload_len);
|
||||
incoming.hop_limit = 0;
|
||||
incoming.encrypted = false;
|
||||
@@ -280,4 +301,4 @@ void MeshCoreAdapter::processSendQueue()
|
||||
}
|
||||
|
||||
} // namespace meshcore
|
||||
} // namespace chat
|
||||
} // namespace chat
|
||||
|
||||
@@ -41,6 +41,12 @@ class MeshCoreAdapter : public IMeshAdapter
|
||||
|
||||
bool pollIncomingText(MeshIncomingText* out) override;
|
||||
|
||||
bool sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest = 0, bool want_ack = false) override;
|
||||
|
||||
bool pollIncomingData(MeshIncomingData* out) override;
|
||||
|
||||
void applyConfig(const MeshConfig& config) override;
|
||||
|
||||
bool isReady() const override;
|
||||
@@ -87,4 +93,4 @@ class MeshCoreAdapter : public IMeshAdapter
|
||||
};
|
||||
|
||||
} // namespace meshcore
|
||||
} // namespace chat
|
||||
} // namespace chat
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "mt_adapter.h"
|
||||
#include "../../../sys/event_bus.h"
|
||||
#include "../../domain/contact_types.h"
|
||||
#include "../../../team/protocol/team_portnum.h"
|
||||
#include <Arduino.h>
|
||||
#include <Crypto.h>
|
||||
#include <cmath>
|
||||
@@ -21,6 +22,7 @@
|
||||
#include <Preferences.h>
|
||||
#include <RNG.h>
|
||||
#include <SHA256.h>
|
||||
#include <vector>
|
||||
|
||||
#ifndef LORA_LOG_ENABLE
|
||||
#define LORA_LOG_ENABLE 1
|
||||
@@ -46,6 +48,14 @@ constexpr uint8_t kLoraSyncWord = 0x2b;
|
||||
constexpr uint16_t kLoraPreambleLen = 16;
|
||||
constexpr uint8_t kBitfieldWantResponseMask = 0x02;
|
||||
|
||||
bool allowPkiForPortnum(uint32_t portnum)
|
||||
{
|
||||
return portnum != meshtastic_PortNum_NODEINFO_APP &&
|
||||
portnum != meshtastic_PortNum_ROUTING_APP &&
|
||||
portnum != meshtastic_PortNum_POSITION_APP &&
|
||||
portnum != meshtastic_PortNum_TRACEROUTE_APP;
|
||||
}
|
||||
|
||||
static const char* portName(uint32_t portnum)
|
||||
{
|
||||
switch (portnum)
|
||||
@@ -62,10 +72,61 @@ static const char* portName(uint32_t portnum)
|
||||
return "TELEMETRY";
|
||||
case meshtastic_PortNum_REMOTE_HARDWARE_APP:
|
||||
return "REMOTEHW";
|
||||
case meshtastic_PortNum_ROUTING_APP:
|
||||
return "ROUTING";
|
||||
case meshtastic_PortNum_TRACEROUTE_APP:
|
||||
return "TRACEROUTE";
|
||||
case meshtastic_PortNum_WAYPOINT_APP:
|
||||
return "WAYPOINT";
|
||||
case team::proto::TEAM_MGMT_APP:
|
||||
return "TEAM_MGMT";
|
||||
case team::proto::TEAM_POSITION_APP:
|
||||
return "TEAM_POS";
|
||||
case team::proto::TEAM_WAYPOINT_APP:
|
||||
return "TEAM_WP";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char* routingErrorName(meshtastic_Routing_Error err)
|
||||
{
|
||||
switch (err)
|
||||
{
|
||||
case meshtastic_Routing_Error_NONE:
|
||||
return "NONE";
|
||||
case meshtastic_Routing_Error_NO_ROUTE:
|
||||
return "NO_ROUTE";
|
||||
case meshtastic_Routing_Error_GOT_NAK:
|
||||
return "GOT_NAK";
|
||||
case meshtastic_Routing_Error_TIMEOUT:
|
||||
return "TIMEOUT";
|
||||
case meshtastic_Routing_Error_NO_INTERFACE:
|
||||
return "NO_INTERFACE";
|
||||
case meshtastic_Routing_Error_MAX_RETRANSMIT:
|
||||
return "MAX_RETRANSMIT";
|
||||
case meshtastic_Routing_Error_NO_CHANNEL:
|
||||
return "NO_CHANNEL";
|
||||
case meshtastic_Routing_Error_TOO_LARGE:
|
||||
return "TOO_LARGE";
|
||||
case meshtastic_Routing_Error_NO_RESPONSE:
|
||||
return "NO_RESPONSE";
|
||||
case meshtastic_Routing_Error_DUTY_CYCLE_LIMIT:
|
||||
return "DUTY_CYCLE_LIMIT";
|
||||
case meshtastic_Routing_Error_BAD_REQUEST:
|
||||
return "BAD_REQUEST";
|
||||
case meshtastic_Routing_Error_NOT_AUTHORIZED:
|
||||
return "NOT_AUTHORIZED";
|
||||
case meshtastic_Routing_Error_PKI_FAILED:
|
||||
return "PKI_FAILED";
|
||||
case meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY:
|
||||
return "PKI_UNKNOWN_PUBKEY";
|
||||
case meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY:
|
||||
return "ADMIN_BAD_SESSION_KEY";
|
||||
case meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED:
|
||||
return "ADMIN_PUBLIC_KEY_UNAUTHORIZED";
|
||||
case meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED:
|
||||
return "RATE_LIMIT_EXCEEDED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -354,6 +415,7 @@ using chat::meshtastic::buildWirePacket;
|
||||
using chat::meshtastic::decodeTextMessage;
|
||||
using chat::meshtastic::decryptPayload;
|
||||
using chat::meshtastic::encodeNodeInfoMessage;
|
||||
using chat::meshtastic::encodeAppData;
|
||||
using chat::meshtastic::encodeTextMessage;
|
||||
using chat::meshtastic::PacketHeaderWire;
|
||||
using chat::meshtastic::parseWirePacket;
|
||||
@@ -385,6 +447,7 @@ MtAdapter::MtAdapter(TLoRaPagerBoard& board)
|
||||
config_ = MeshConfig(); // Default config
|
||||
initNodeIdentity();
|
||||
initPkiKeys();
|
||||
loadPkiNodeKeys();
|
||||
updateChannelKeys();
|
||||
}
|
||||
|
||||
@@ -402,6 +465,7 @@ bool MtAdapter::sendText(ChannelId channel, const std::string& text,
|
||||
|
||||
PendingSend pending;
|
||||
pending.channel = channel;
|
||||
pending.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
pending.text = text;
|
||||
pending.msg_id = next_packet_id_++;
|
||||
pending.dest = (peer != 0) ? peer : 0xFFFFFFFF;
|
||||
@@ -434,6 +498,132 @@ bool MtAdapter::pollIncomingText(MeshIncomingText* out)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MtAdapter::sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest, bool want_ack)
|
||||
{
|
||||
if (!ready_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t data_buffer[256];
|
||||
size_t data_size = sizeof(data_buffer);
|
||||
if (!encodeAppData(portnum, payload, len, want_ack, data_buffer, &data_size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t wire_buffer[512];
|
||||
size_t wire_size = sizeof(wire_buffer);
|
||||
|
||||
ChannelId out_channel = channel;
|
||||
uint8_t channel_hash =
|
||||
(out_channel == ChannelId::SECONDARY) ? secondary_channel_hash_ : primary_channel_hash_;
|
||||
const uint8_t* psk =
|
||||
(out_channel == ChannelId::SECONDARY) ? secondary_psk_ : primary_psk_;
|
||||
size_t psk_len =
|
||||
(out_channel == ChannelId::SECONDARY) ? secondary_psk_len_ : primary_psk_len_;
|
||||
uint8_t hop_limit = config_.hop_limit;
|
||||
uint32_t dest_node = (dest != 0) ? dest : 0xFFFFFFFF;
|
||||
bool want_ack_flag = want_ack && (dest_node != 0xFFFFFFFF);
|
||||
MessageId msg_id = next_packet_id_++;
|
||||
|
||||
const uint8_t* out_payload = data_buffer;
|
||||
size_t out_len = data_size;
|
||||
bool pki_disabled = false;
|
||||
if (dest_node != 0xFFFFFFFF)
|
||||
{
|
||||
uint32_t now_ms = millis();
|
||||
auto disable_it = pki_disabled_until_ms_.find(dest_node);
|
||||
if (disable_it != pki_disabled_until_ms_.end())
|
||||
{
|
||||
if (now_ms < disable_it->second)
|
||||
{
|
||||
pki_disabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
pki_disabled_until_ms_.erase(disable_it);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool use_pki = (dest_node != 0xFFFFFFFF) && pki_ready_ &&
|
||||
allowPkiForPortnum(portnum) &&
|
||||
!pki_disabled &&
|
||||
(node_public_keys_.find(dest_node) != node_public_keys_.end());
|
||||
uint8_t pki_buf[256];
|
||||
size_t pki_len = sizeof(pki_buf);
|
||||
if (use_pki && encryptPkiPayload(dest_node, msg_id, data_buffer, data_size, pki_buf, &pki_len))
|
||||
{
|
||||
out_payload = pki_buf;
|
||||
out_len = pki_len;
|
||||
channel_hash = 0; // PKI channel
|
||||
want_ack_flag = true;
|
||||
psk = nullptr;
|
||||
psk_len = 0;
|
||||
}
|
||||
else if (dest_node != 0xFFFFFFFF && out_channel == ChannelId::PRIMARY)
|
||||
{
|
||||
if (pki_disabled)
|
||||
{
|
||||
uint32_t until_ms = pki_disabled_until_ms_[dest_node];
|
||||
LORA_LOG("[LORA] PKI disabled for %08lX until=%lu\n",
|
||||
(unsigned long)dest_node,
|
||||
(unsigned long)until_ms);
|
||||
}
|
||||
auto it = node_last_channel_.find(dest_node);
|
||||
if (it != node_last_channel_.end())
|
||||
{
|
||||
out_channel = it->second;
|
||||
channel_hash =
|
||||
(out_channel == ChannelId::SECONDARY) ? secondary_channel_hash_ : primary_channel_hash_;
|
||||
psk = (out_channel == ChannelId::SECONDARY) ? secondary_psk_ : primary_psk_;
|
||||
psk_len = (out_channel == ChannelId::SECONDARY) ? secondary_psk_len_ : primary_psk_len_;
|
||||
}
|
||||
}
|
||||
|
||||
if (!buildWirePacket(out_payload, out_len, node_id_, msg_id,
|
||||
dest_node, channel_hash, hop_limit, want_ack_flag,
|
||||
psk, psk_len, wire_buffer, &wire_size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!board_.isHardwareOnline(HW_RADIO_ONLINE))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int state = RADIOLIB_ERR_UNSUPPORTED;
|
||||
#if defined(ARDUINO_LILYGO_LORA_SX1262) || defined(ARDUINO_LILYGO_LORA_SX1280)
|
||||
state = board_.radio.transmit(wire_buffer, wire_size);
|
||||
#endif
|
||||
|
||||
bool ok = (state == RADIOLIB_ERR_NONE);
|
||||
LORA_LOG("[LORA] TX app port=%u len=%u ok=%d\n",
|
||||
(unsigned)portnum,
|
||||
(unsigned)wire_size,
|
||||
ok ? 1 : 0);
|
||||
if (ok)
|
||||
{
|
||||
startRadioReceive();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool MtAdapter::pollIncomingData(MeshIncomingData* out)
|
||||
{
|
||||
if (app_receive_queue_.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = app_receive_queue_.front();
|
||||
app_receive_queue_.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MtAdapter::applyConfig(const MeshConfig& config)
|
||||
{
|
||||
config_ = config;
|
||||
@@ -506,6 +696,15 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
header.channel,
|
||||
header.flags,
|
||||
(unsigned)payload_size);
|
||||
const char* channel_kind = "UNKNOWN";
|
||||
if (header.channel == 0) {
|
||||
channel_kind = "PKI";
|
||||
} else if (header.channel == primary_channel_hash_) {
|
||||
channel_kind = "PRIMARY";
|
||||
} else if (header.channel == secondary_channel_hash_) {
|
||||
channel_kind = "SECONDARY";
|
||||
}
|
||||
LORA_LOG("[LORA] RX channel kind=%s hash=0x%02X\n", channel_kind, header.channel);
|
||||
LORA_LOG("[LORA] RX full packet hex: %s\n", full_hex.c_str());
|
||||
if (header.from == node_id_)
|
||||
{
|
||||
@@ -638,6 +837,7 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
std::array<uint8_t, 32> key{};
|
||||
memcpy(key.data(), user.public_key.bytes, 32);
|
||||
node_public_keys_[node_id] = key;
|
||||
savePkiNodeKey(node_id, key.data(), key.size());
|
||||
LORA_LOG("[LORA] PKI key stored for %08lX\n", (unsigned long)node_id);
|
||||
}
|
||||
|
||||
@@ -700,12 +900,71 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded.portnum == meshtastic_PortNum_ROUTING_APP && decoded.payload.size > 0)
|
||||
{
|
||||
meshtastic_Routing routing = meshtastic_Routing_init_default;
|
||||
pb_istream_t rstream =
|
||||
pb_istream_from_buffer(decoded.payload.bytes, decoded.payload.size);
|
||||
if (pb_decode(&rstream, meshtastic_Routing_fields, &routing))
|
||||
{
|
||||
LORA_LOG("[LORA] RX routing from=%08lX req=%08lX dest=%08lX src=%08lX\n",
|
||||
(unsigned long)header.from,
|
||||
(unsigned long)decoded.request_id,
|
||||
(unsigned long)decoded.dest,
|
||||
(unsigned long)decoded.source);
|
||||
if (decoded.request_id != 0 && header.to == node_id_)
|
||||
{
|
||||
bool ok = true;
|
||||
if (routing.which_variant == meshtastic_Routing_error_reason_tag &&
|
||||
routing.error_reason != meshtastic_Routing_Error_NONE)
|
||||
{
|
||||
ok = false;
|
||||
}
|
||||
if (routing.which_variant == meshtastic_Routing_error_reason_tag &&
|
||||
(routing.error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY ||
|
||||
routing.error_reason == meshtastic_Routing_Error_NO_CHANNEL))
|
||||
{
|
||||
uint32_t now_ms = millis();
|
||||
pki_disabled_until_ms_[header.from] = now_ms + PKI_BACKOFF_MS;
|
||||
sendNodeInfoTo(header.from, true);
|
||||
LORA_LOG("[LORA] PKI backoff for %08lX reason=%s until=%lu\n",
|
||||
(unsigned long)header.from,
|
||||
routingErrorName(routing.error_reason),
|
||||
(unsigned long)(now_ms + PKI_BACKOFF_MS));
|
||||
}
|
||||
pending_ack_ms_.erase(decoded.request_id);
|
||||
LORA_LOG("[LORA] RX ack reason=%u (%s)\n",
|
||||
static_cast<unsigned>(routing.error_reason),
|
||||
routingErrorName(routing.error_reason));
|
||||
LORA_LOG("[LORA] RX ack from=%08lX req=%08lX ok=%d\n",
|
||||
(unsigned long)header.from,
|
||||
(unsigned long)decoded.request_id,
|
||||
ok ? 1 : 0);
|
||||
sys::EventBus::publish(
|
||||
new sys::ChatSendResultEvent(decoded.request_id, ok), 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LORA_LOG("[LORA] RX Routing decode fail from=%08lX err=%s\n",
|
||||
(unsigned long)header.from,
|
||||
PB_GET_ERROR(&rstream));
|
||||
}
|
||||
}
|
||||
|
||||
bool want_ack_flag = (header.flags & PACKET_FLAGS_WANT_ACK_MASK) != 0;
|
||||
bool want_response = decoded.want_response ||
|
||||
(decoded.has_bitfield && ((decoded.bitfield & kBitfieldWantResponseMask) != 0));
|
||||
bool to_us = (header.to == node_id_);
|
||||
bool is_text_port = (decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP ||
|
||||
decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP);
|
||||
bool is_nodeinfo_port = (decoded.portnum == meshtastic_PortNum_NODEINFO_APP);
|
||||
ChannelId channel_id =
|
||||
(header.channel == secondary_channel_hash_) ? ChannelId::SECONDARY : ChannelId::PRIMARY;
|
||||
if (header.channel != 0 && header.from != node_id_)
|
||||
{
|
||||
node_last_channel_[header.from] = channel_id;
|
||||
}
|
||||
if ((want_ack_flag || want_response) && to_us && is_text_port)
|
||||
{
|
||||
if (sendRoutingAck(header.from, header.id, header.channel, psk, psk_len))
|
||||
@@ -721,6 +980,24 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
(unsigned long)header.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_text_port && !is_nodeinfo_port && decoded.payload.size > 0)
|
||||
{
|
||||
MeshIncomingData incoming;
|
||||
incoming.portnum = decoded.portnum;
|
||||
incoming.from = header.from;
|
||||
incoming.to = header.to;
|
||||
incoming.packet_id = header.id;
|
||||
incoming.channel = channel_id;
|
||||
incoming.channel_hash = header.channel;
|
||||
incoming.want_response = want_response;
|
||||
incoming.payload.assign(decoded.payload.bytes,
|
||||
decoded.payload.bytes + decoded.payload.size);
|
||||
if (app_receive_queue_.size() < MAX_APP_QUEUE)
|
||||
{
|
||||
app_receive_queue_.push(incoming);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -738,6 +1015,7 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
|
||||
{
|
||||
// Fill in packet info from header
|
||||
incoming.from = header.from;
|
||||
incoming.to = header.to;
|
||||
incoming.msg_id = header.id;
|
||||
if (header.channel == secondary_channel_hash_)
|
||||
{
|
||||
@@ -769,6 +1047,20 @@ void MtAdapter::processSendQueue()
|
||||
|
||||
maybeBroadcastNodeInfo(now);
|
||||
|
||||
for (auto it = pending_ack_ms_.begin(); it != pending_ack_ms_.end();)
|
||||
{
|
||||
if (now - it->second >= ACK_TIMEOUT_MS)
|
||||
{
|
||||
LORA_LOG("[LORA] RX ack timeout req=%08lX\n",
|
||||
(unsigned long)it->first);
|
||||
sys::EventBus::publish(
|
||||
new sys::ChatSendResultEvent(it->first, false), 0);
|
||||
it = pending_ack_ms_.erase(it);
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
if (!send_queue_.empty())
|
||||
{
|
||||
LORA_LOG("[LORA] TX queue pending=%u\n", (unsigned)send_queue_.size());
|
||||
@@ -827,8 +1119,9 @@ bool MtAdapter::sendPacket(const PendingSend& pending)
|
||||
uint8_t wire_buffer[512];
|
||||
size_t wire_size = sizeof(wire_buffer);
|
||||
|
||||
ChannelId channel = pending.channel;
|
||||
uint8_t channel_hash =
|
||||
(pending.channel == ChannelId::SECONDARY) ? secondary_channel_hash_ : primary_channel_hash_;
|
||||
(channel == ChannelId::SECONDARY) ? secondary_channel_hash_ : primary_channel_hash_;
|
||||
uint8_t hop_limit = config_.hop_limit;
|
||||
bool want_ack = false; // Broadcast doesn't need ACK unless PKI direct
|
||||
uint32_t dest = (pending.dest != 0) ? pending.dest : 0xFFFFFFFF;
|
||||
@@ -838,7 +1131,26 @@ bool MtAdapter::sendPacket(const PendingSend& pending)
|
||||
size_t payload_len = data_size;
|
||||
const uint8_t* psk = nullptr;
|
||||
size_t psk_len = 0;
|
||||
bool pki_disabled = false;
|
||||
if (dest != 0xFFFFFFFF)
|
||||
{
|
||||
uint32_t now_ms = millis();
|
||||
auto disable_it = pki_disabled_until_ms_.find(dest);
|
||||
if (disable_it != pki_disabled_until_ms_.end())
|
||||
{
|
||||
if (now_ms < disable_it->second)
|
||||
{
|
||||
pki_disabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
pki_disabled_until_ms_.erase(disable_it);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool use_pki = (dest != 0xFFFFFFFF) && pki_ready_ &&
|
||||
allowPkiForPortnum(pending.portnum) &&
|
||||
!pki_disabled &&
|
||||
(node_public_keys_.find(dest) != node_public_keys_.end());
|
||||
uint8_t pki_buf[256];
|
||||
size_t pki_len = sizeof(pki_buf);
|
||||
@@ -851,8 +1163,25 @@ bool MtAdapter::sendPacket(const PendingSend& pending)
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dest != 0xFFFFFFFF && channel == ChannelId::PRIMARY)
|
||||
{
|
||||
if (pki_disabled)
|
||||
{
|
||||
uint32_t until_ms = pki_disabled_until_ms_[dest];
|
||||
LORA_LOG("[LORA] PKI disabled for %08lX until=%lu\n",
|
||||
(unsigned long)dest,
|
||||
(unsigned long)until_ms);
|
||||
}
|
||||
auto it = node_last_channel_.find(dest);
|
||||
if (it != node_last_channel_.end())
|
||||
{
|
||||
channel = it->second;
|
||||
channel_hash =
|
||||
(channel == ChannelId::SECONDARY) ? secondary_channel_hash_ : primary_channel_hash_;
|
||||
}
|
||||
}
|
||||
// Fallback to PSK
|
||||
if (pending.channel == ChannelId::SECONDARY)
|
||||
if (channel == ChannelId::SECONDARY)
|
||||
{
|
||||
psk = secondary_psk_;
|
||||
psk_len = secondary_psk_len_;
|
||||
@@ -864,6 +1193,15 @@ bool MtAdapter::sendPacket(const PendingSend& pending)
|
||||
}
|
||||
}
|
||||
|
||||
const char* channel_name =
|
||||
(channel == ChannelId::SECONDARY) ? kSecondaryChannelName : kPrimaryChannelName;
|
||||
LORA_LOG("[LORA] TX channel name='%s' hash=0x%02X psk=%u pki=%u dest=%08lX\n",
|
||||
channel_name,
|
||||
channel_hash,
|
||||
(unsigned)psk_len,
|
||||
use_pki ? 1U : 0U,
|
||||
(unsigned long)dest);
|
||||
|
||||
if (!buildWirePacket(payload, payload_len, from_node, pending.msg_id,
|
||||
dest, channel_hash, hop_limit, want_ack,
|
||||
psk, psk_len, wire_buffer, &wire_size))
|
||||
@@ -899,9 +1237,13 @@ bool MtAdapter::sendPacket(const PendingSend& pending)
|
||||
bool ok = (state == RADIOLIB_ERR_NONE);
|
||||
LORA_LOG("[LORA] TX text id=%08lX ch=%u len=%u ok=%d\n",
|
||||
(unsigned long)pending.msg_id,
|
||||
static_cast<unsigned>(pending.channel),
|
||||
static_cast<unsigned>(channel),
|
||||
(unsigned)wire_size,
|
||||
ok ? 1 : 0);
|
||||
if (ok && want_ack)
|
||||
{
|
||||
pending_ack_ms_[pending.msg_id] = millis();
|
||||
}
|
||||
if (ok)
|
||||
{
|
||||
startRadioReceive();
|
||||
@@ -1156,14 +1498,20 @@ void MtAdapter::updateChannelKeys()
|
||||
computeChannelHash(kSecondaryChannelName,
|
||||
(secondary_psk_len_ > 0) ? secondary_psk_ : nullptr,
|
||||
secondary_psk_len_);
|
||||
LORA_LOG("[LORA] channel primary='%s' hash=0x%02X psk=%u\n",
|
||||
std::string primary_psk_hex = toHex(primary_psk_, primary_psk_len_, primary_psk_len_);
|
||||
std::string secondary_psk_hex = toHex(secondary_psk_, secondary_psk_len_, secondary_psk_len_);
|
||||
if (primary_psk_hex.empty()) primary_psk_hex = "-";
|
||||
if (secondary_psk_hex.empty()) secondary_psk_hex = "-";
|
||||
LORA_LOG("[LORA] channel primary='%s' hash=0x%02X psk=%u hex=%s\n",
|
||||
kPrimaryChannelName,
|
||||
primary_channel_hash_,
|
||||
(unsigned)primary_psk_len_);
|
||||
LORA_LOG("[LORA] channel secondary='%s' hash=0x%02X psk=%u\n",
|
||||
(unsigned)primary_psk_len_,
|
||||
primary_psk_hex.c_str());
|
||||
LORA_LOG("[LORA] channel secondary='%s' hash=0x%02X psk=%u hex=%s\n",
|
||||
kSecondaryChannelName,
|
||||
secondary_channel_hash_,
|
||||
(unsigned)secondary_psk_len_);
|
||||
(unsigned)secondary_psk_len_,
|
||||
secondary_psk_hex.c_str());
|
||||
}
|
||||
|
||||
void MtAdapter::startRadioReceive()
|
||||
@@ -1219,6 +1567,92 @@ bool MtAdapter::initPkiKeys()
|
||||
return pki_ready_;
|
||||
}
|
||||
|
||||
void MtAdapter::loadPkiNodeKeys()
|
||||
{
|
||||
Preferences prefs;
|
||||
prefs.begin(kPkiPrefsNs, true);
|
||||
size_t total = prefs.getBytesLength(kPkiPrefsKey);
|
||||
struct PkiKeyEntry {
|
||||
uint32_t node_id;
|
||||
uint8_t key[32];
|
||||
};
|
||||
if (total < sizeof(PkiKeyEntry)) {
|
||||
prefs.end();
|
||||
return;
|
||||
}
|
||||
size_t count = total / sizeof(PkiKeyEntry);
|
||||
if (count > kMaxPkiNodes) {
|
||||
count = kMaxPkiNodes;
|
||||
}
|
||||
std::vector<PkiKeyEntry> entries(count);
|
||||
size_t read = prefs.getBytes(kPkiPrefsKey, entries.data(),
|
||||
entries.size() * sizeof(PkiKeyEntry));
|
||||
prefs.end();
|
||||
|
||||
size_t actual = read / sizeof(PkiKeyEntry);
|
||||
for (size_t i = 0; i < actual; ++i) {
|
||||
if (entries[i].node_id == 0) {
|
||||
continue;
|
||||
}
|
||||
std::array<uint8_t, 32> key{};
|
||||
memcpy(key.data(), entries[i].key, sizeof(entries[i].key));
|
||||
node_public_keys_[entries[i].node_id] = key;
|
||||
}
|
||||
if (actual > 0) {
|
||||
LORA_LOG("[LORA] PKI keys loaded=%u\n", (unsigned)actual);
|
||||
}
|
||||
}
|
||||
|
||||
void MtAdapter::savePkiNodeKey(uint32_t node_id, const uint8_t* key, size_t key_len)
|
||||
{
|
||||
if (node_id == 0 || !key || key_len != 32) {
|
||||
return;
|
||||
}
|
||||
struct PkiKeyEntry {
|
||||
uint32_t node_id;
|
||||
uint8_t key[32];
|
||||
};
|
||||
std::vector<PkiKeyEntry> entries;
|
||||
|
||||
Preferences prefs;
|
||||
prefs.begin(kPkiPrefsNs, false);
|
||||
size_t total = prefs.getBytesLength(kPkiPrefsKey);
|
||||
if (total >= sizeof(PkiKeyEntry)) {
|
||||
size_t count = total / sizeof(PkiKeyEntry);
|
||||
if (count > kMaxPkiNodes) {
|
||||
count = kMaxPkiNodes;
|
||||
}
|
||||
entries.resize(count);
|
||||
prefs.getBytes(kPkiPrefsKey, entries.data(),
|
||||
entries.size() * sizeof(PkiKeyEntry));
|
||||
}
|
||||
|
||||
bool updated = false;
|
||||
for (auto& entry : entries) {
|
||||
if (entry.node_id == node_id) {
|
||||
memcpy(entry.key, key, 32);
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
if (entries.size() < kMaxPkiNodes) {
|
||||
PkiKeyEntry entry{};
|
||||
entry.node_id = node_id;
|
||||
memcpy(entry.key, key, 32);
|
||||
entries.push_back(entry);
|
||||
} else if (!entries.empty()) {
|
||||
entries.back().node_id = node_id;
|
||||
memcpy(entries.back().key, key, 32);
|
||||
}
|
||||
}
|
||||
if (!entries.empty()) {
|
||||
prefs.putBytes(kPkiPrefsKey, entries.data(),
|
||||
entries.size() * sizeof(PkiKeyEntry));
|
||||
}
|
||||
prefs.end();
|
||||
}
|
||||
|
||||
bool MtAdapter::decryptPkiPayload(uint32_t from, uint32_t packet_id,
|
||||
const uint8_t* cipher, size_t cipher_len,
|
||||
uint8_t* out_plain, size_t* out_plain_len)
|
||||
|
||||
@@ -36,6 +36,10 @@ class MtAdapter : public chat::IMeshAdapter
|
||||
bool sendText(ChannelId channel, const std::string& text,
|
||||
MessageId* out_msg_id, NodeId peer = 0) override;
|
||||
bool pollIncomingText(MeshIncomingText* out) override;
|
||||
bool sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest = 0, bool want_ack = false) override;
|
||||
bool pollIncomingData(MeshIncomingData* out) override;
|
||||
void applyConfig(const MeshConfig& config) override;
|
||||
bool isReady() const override;
|
||||
|
||||
@@ -84,7 +88,10 @@ class MtAdapter : public chat::IMeshAdapter
|
||||
std::array<uint8_t, 32> pki_public_key_;
|
||||
std::array<uint8_t, 32> pki_private_key_;
|
||||
std::map<uint32_t, std::array<uint8_t, 32>> node_public_keys_;
|
||||
std::map<uint32_t, ChannelId> node_last_channel_;
|
||||
std::map<uint32_t, uint32_t> pki_disabled_until_ms_;
|
||||
std::map<uint32_t, uint32_t> nodeinfo_last_seen_ms_;
|
||||
std::map<uint32_t, uint32_t> pending_ack_ms_;
|
||||
|
||||
// Raw packet data storage for protocol detection
|
||||
uint8_t last_raw_packet_[256];
|
||||
@@ -94,6 +101,7 @@ class MtAdapter : public chat::IMeshAdapter
|
||||
struct PendingSend
|
||||
{
|
||||
ChannelId channel;
|
||||
uint32_t portnum;
|
||||
std::string text;
|
||||
MessageId msg_id;
|
||||
NodeId dest;
|
||||
@@ -103,12 +111,19 @@ class MtAdapter : public chat::IMeshAdapter
|
||||
|
||||
std::queue<PendingSend> send_queue_;
|
||||
std::queue<MeshIncomingText> receive_queue_;
|
||||
std::queue<MeshIncomingData> app_receive_queue_;
|
||||
|
||||
static constexpr size_t MAX_PACKET_SIZE = 255;
|
||||
static constexpr uint32_t RETRY_DELAY_MS = 1000;
|
||||
static constexpr uint8_t MAX_RETRIES = 1;
|
||||
static constexpr uint32_t NODEINFO_INTERVAL_MS = 3 * 60 * 60 * 1000;
|
||||
static constexpr uint32_t NODEINFO_REPLY_SUPPRESS_MS = 12 * 60 * 60 * 1000;
|
||||
static constexpr uint32_t PKI_BACKOFF_MS = 5 * 60 * 1000;
|
||||
static constexpr size_t MAX_APP_QUEUE = 10;
|
||||
static constexpr uint32_t ACK_TIMEOUT_MS = 15000;
|
||||
static constexpr size_t kMaxPkiNodes = 16;
|
||||
static constexpr const char* kPkiPrefsNs = "chat";
|
||||
static constexpr const char* kPkiPrefsKey = "pki_nodes";
|
||||
|
||||
bool sendPacket(const PendingSend& pending);
|
||||
bool sendNodeInfo();
|
||||
@@ -119,6 +134,8 @@ class MtAdapter : public chat::IMeshAdapter
|
||||
void updateChannelKeys();
|
||||
void startRadioReceive();
|
||||
bool initPkiKeys();
|
||||
void loadPkiNodeKeys();
|
||||
void savePkiNodeKey(uint32_t node_id, const uint8_t* key, size_t key_len);
|
||||
bool decryptPkiPayload(uint32_t from, uint32_t packet_id,
|
||||
const uint8_t* cipher, size_t cipher_len,
|
||||
uint8_t* out_plain, size_t* out_plain_len);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "mt_codec_pb.h"
|
||||
#include "../../time_utils.h"
|
||||
#include "compression/unishox2.h"
|
||||
#include <cstring>
|
||||
|
||||
@@ -111,7 +112,7 @@ bool decodeTextMessage(const uint8_t* buffer, size_t size, MeshIncomingText* out
|
||||
// This will be done in mt_adapter when decoding full packet
|
||||
out->from = 0; // Will be set from packet header
|
||||
out->msg_id = 0; // Will be set from packet header
|
||||
out->timestamp = millis() / 1000;
|
||||
out->timestamp = now_message_timestamp();
|
||||
out->channel = ChannelId::PRIMARY; // Will be set from packet header
|
||||
out->hop_limit = 2;
|
||||
out->encrypted = false;
|
||||
@@ -181,6 +182,44 @@ bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_n
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeAppData(uint32_t portnum, const uint8_t* payload, size_t payload_len,
|
||||
bool want_response, uint8_t* out_buffer, size_t* out_size)
|
||||
{
|
||||
if (!out_buffer || !out_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
meshtastic_Data data = meshtastic_Data_init_default;
|
||||
data.portnum = static_cast<meshtastic_PortNum>(portnum);
|
||||
data.want_response = want_response;
|
||||
data.has_bitfield = true;
|
||||
data.bitfield = 0;
|
||||
|
||||
if (payload_len > sizeof(data.payload.bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
data.payload.size = static_cast<pb_size_t>(payload_len);
|
||||
if (payload_len > 0)
|
||||
{
|
||||
if (!payload)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memcpy(data.payload.bytes, payload, payload_len);
|
||||
}
|
||||
|
||||
pb_ostream_t data_stream = pb_ostream_from_buffer(out_buffer, *out_size);
|
||||
if (!pb_encode(&data_stream, meshtastic_Data_fields, &data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*out_size = data_stream.bytes_written;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeMeshPacket(const meshtastic_MeshPacket& packet, uint8_t* out_buffer, size_t* out_size)
|
||||
{
|
||||
if (!out_buffer || !out_size)
|
||||
|
||||
@@ -67,6 +67,19 @@ bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_n
|
||||
const uint8_t macaddr[6], const uint8_t* public_key, size_t public_key_len,
|
||||
bool want_response, uint8_t* out_buffer, size_t* out_size);
|
||||
|
||||
/**
|
||||
* @brief Encode app payload to Meshtastic Data message
|
||||
* @param portnum Application port number
|
||||
* @param payload Payload bytes
|
||||
* @param payload_len Payload length
|
||||
* @param want_response Request response
|
||||
* @param out_buffer Output buffer
|
||||
* @param out_size Output buffer size (updated with actual size)
|
||||
* @return true if successful
|
||||
*/
|
||||
bool encodeAppData(uint32_t portnum, const uint8_t* payload, size_t payload_len,
|
||||
bool want_response, uint8_t* out_buffer, size_t* out_size);
|
||||
|
||||
/**
|
||||
* @brief Encode MeshPacket to buffer
|
||||
* @param packet MeshPacket structure
|
||||
|
||||
@@ -148,6 +148,28 @@ void FlashStore::clearChannel(ChannelId channel)
|
||||
}
|
||||
}
|
||||
|
||||
bool FlashStore::updateMessageStatus(MessageId msg_id, MessageStatus status)
|
||||
{
|
||||
if (!ready_ || msg_id == 0) return false;
|
||||
|
||||
for (size_t i = 0; i < count_; ++i)
|
||||
{
|
||||
size_t idx = (head_ + kMaxMessages - 1 - i) % kMaxMessages;
|
||||
Record& rec = records_[idx];
|
||||
if (rec.text_len == 0) continue;
|
||||
if (rec.msg_id != msg_id) continue;
|
||||
if (rec.from != 0) continue; // only update outgoing messages
|
||||
uint8_t new_status = static_cast<uint8_t>(status);
|
||||
if (rec.status == new_status) {
|
||||
return true;
|
||||
}
|
||||
rec.status = new_status;
|
||||
persistRecord(static_cast<uint16_t>(idx));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FlashStore::loadFromPrefs()
|
||||
{
|
||||
uint8_t ver = prefs_.getUChar(kKeyVer, 0);
|
||||
|
||||
@@ -29,6 +29,7 @@ class FlashStore : public IChatStore
|
||||
void setUnread(ChannelId channel, int unread) override;
|
||||
int getUnread(ChannelId channel) const override;
|
||||
void clearChannel(ChannelId channel) override;
|
||||
bool updateMessageStatus(MessageId msg_id, MessageStatus status) override;
|
||||
|
||||
std::vector<ChatMessage> loadAll() const;
|
||||
|
||||
|
||||
@@ -168,6 +168,12 @@ void LogStore::clearChannel(ChannelId channel)
|
||||
}
|
||||
}
|
||||
|
||||
bool LogStore::updateMessageStatus(MessageId, MessageStatus)
|
||||
{
|
||||
// Append-only log; no in-place status updates.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LogStore::loadIndex(ChannelId ch)
|
||||
{
|
||||
if (!fs_) return false;
|
||||
|
||||
@@ -43,6 +43,7 @@ class LogStore : public IChatStore
|
||||
void setUnread(ChannelId channel, int unread) override;
|
||||
int getUnread(ChannelId channel) const override;
|
||||
void clearChannel(ChannelId channel) override;
|
||||
bool updateMessageStatus(MessageId msg_id, MessageStatus status) override;
|
||||
|
||||
private:
|
||||
fs::FS* fs_;
|
||||
|
||||
@@ -64,6 +64,26 @@ void RamStore::clearChannel(ChannelId channel)
|
||||
storage.unread_count = 0;
|
||||
}
|
||||
|
||||
bool RamStore::updateMessageStatus(MessageId msg_id, MessageStatus status)
|
||||
{
|
||||
if (msg_id == 0) return false;
|
||||
for (auto& pair : channels_)
|
||||
{
|
||||
ChannelStorage& storage = pair.second;
|
||||
size_t count = storage.messages.count();
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
ChatMessage* msg = storage.messages.get(i);
|
||||
if (!msg) continue;
|
||||
if (msg->msg_id != msg_id) continue;
|
||||
if (msg->from != 0) continue; // only update outgoing messages
|
||||
msg->status = status;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RamStore::ChannelStorage& RamStore::getChannelStorage(ChannelId channel)
|
||||
{
|
||||
auto it = channels_.find(channel);
|
||||
|
||||
@@ -30,6 +30,7 @@ class RamStore : public IChatStore
|
||||
void setUnread(ChannelId channel, int unread) override;
|
||||
int getUnread(ChannelId channel) const override;
|
||||
void clearChannel(ChannelId channel) override;
|
||||
bool updateMessageStatus(MessageId msg_id, MessageStatus status) override;
|
||||
|
||||
private:
|
||||
struct ChannelStorage
|
||||
|
||||
@@ -53,6 +53,14 @@ class IChatStore
|
||||
* @param channel Channel ID
|
||||
*/
|
||||
virtual void clearChannel(ChannelId channel) = 0;
|
||||
|
||||
/**
|
||||
* @brief Update stored message status by message ID
|
||||
* @param msg_id Message ID
|
||||
* @param status New status
|
||||
* @return true if updated
|
||||
*/
|
||||
virtual bool updateMessageStatus(MessageId msg_id, MessageStatus status) = 0;
|
||||
};
|
||||
|
||||
} // namespace chat
|
||||
|
||||
@@ -36,6 +36,27 @@ class IMeshAdapter
|
||||
*/
|
||||
virtual bool pollIncomingText(MeshIncomingText* out) = 0;
|
||||
|
||||
/**
|
||||
* @brief Send app payload data (non-text)
|
||||
* @param channel Channel ID
|
||||
* @param portnum Application port number
|
||||
* @param payload Payload bytes
|
||||
* @param len Payload length
|
||||
* @param dest Destination node (0 for broadcast)
|
||||
* @param want_ack Request ACK if supported
|
||||
* @return true if queued successfully
|
||||
*/
|
||||
virtual bool sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest = 0, bool want_ack = false) = 0;
|
||||
|
||||
/**
|
||||
* @brief Poll for incoming app payload data
|
||||
* @param out Output data (if available)
|
||||
* @return true if data available
|
||||
*/
|
||||
virtual bool pollIncomingData(MeshIncomingData* out) = 0;
|
||||
|
||||
/**
|
||||
* @brief Apply mesh configuration
|
||||
* @param config Configuration to apply
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @file time_utils.h
|
||||
* @brief Time helpers for chat timestamps
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace chat {
|
||||
|
||||
constexpr uint32_t kMinValidEpochSeconds = 1577836800U; // 2020-01-01
|
||||
|
||||
inline bool is_valid_epoch(uint32_t ts)
|
||||
{
|
||||
return ts >= kMinValidEpochSeconds;
|
||||
}
|
||||
|
||||
inline uint32_t now_epoch_seconds()
|
||||
{
|
||||
time_t now = time(nullptr);
|
||||
if (now < 0) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<uint32_t>(now);
|
||||
}
|
||||
|
||||
inline uint32_t now_uptime_seconds()
|
||||
{
|
||||
return static_cast<uint32_t>(millis() / 1000U);
|
||||
}
|
||||
|
||||
inline uint32_t now_message_timestamp()
|
||||
{
|
||||
uint32_t now = now_epoch_seconds();
|
||||
return is_valid_epoch(now) ? now : now_uptime_seconds();
|
||||
}
|
||||
|
||||
} // namespace chat
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
#include "chat_service.h"
|
||||
#include "../time_utils.h"
|
||||
#include "../../sys/event_bus.h"
|
||||
|
||||
namespace chat
|
||||
@@ -22,36 +23,24 @@ MessageId ChatService::sendText(ChannelId channel, const std::string& text, Node
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Try to send via adapter first to get packet ID
|
||||
MessageId msg_id = 0;
|
||||
bool queued = adapter_.sendText(channel, text, &msg_id, peer);
|
||||
|
||||
ChatMessage msg;
|
||||
msg.channel = channel;
|
||||
msg.from = 0; // Local message
|
||||
msg.peer = peer;
|
||||
msg.timestamp = millis() / 1000; // Convert to seconds
|
||||
msg.msg_id = msg_id;
|
||||
msg.timestamp = now_message_timestamp();
|
||||
msg.text = text;
|
||||
msg.status = MessageStatus::Queued;
|
||||
msg.status = queued ? MessageStatus::Queued : MessageStatus::Failed;
|
||||
|
||||
// Queue in model
|
||||
model_.onSendQueued(msg);
|
||||
|
||||
// Try to send via adapter
|
||||
MessageId msg_id = 0;
|
||||
if (adapter_.sendText(channel, text, &msg_id, peer))
|
||||
if (!queued && msg_id != 0)
|
||||
{
|
||||
// Update message ID if adapter provided one
|
||||
if (msg_id != 0)
|
||||
{
|
||||
// Find and update message (simplified - in real impl might need better tracking)
|
||||
const ChatMessage* found = model_.getMessage(msg_id);
|
||||
if (found)
|
||||
{
|
||||
// Message ID already set
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send failed, mark as failed
|
||||
model_.onSendResult(msg.msg_id, false);
|
||||
model_.onSendResult(msg_id, false);
|
||||
}
|
||||
|
||||
// Store message
|
||||
@@ -139,9 +128,13 @@ void ChatService::processIncoming()
|
||||
ChatMessage msg;
|
||||
msg.channel = incoming.channel;
|
||||
msg.from = incoming.from;
|
||||
msg.peer = incoming.from;
|
||||
if (incoming.to == 0xFFFFFFFF || incoming.to == 0) {
|
||||
msg.peer = 0;
|
||||
} else {
|
||||
msg.peer = incoming.from;
|
||||
}
|
||||
msg.msg_id = incoming.msg_id;
|
||||
msg.timestamp = incoming.timestamp ? incoming.timestamp : (millis() / 1000);
|
||||
msg.timestamp = incoming.timestamp ? incoming.timestamp : now_message_timestamp();
|
||||
msg.text = incoming.text;
|
||||
msg.status = MessageStatus::Incoming;
|
||||
|
||||
@@ -158,4 +151,14 @@ void ChatService::processIncoming()
|
||||
}
|
||||
}
|
||||
|
||||
void ChatService::handleSendResult(MessageId msg_id, bool ok)
|
||||
{
|
||||
if (msg_id == 0) return;
|
||||
model_.onSendResult(msg_id, ok);
|
||||
if (!ok)
|
||||
{
|
||||
store_.updateMessageStatus(msg_id, MessageStatus::Failed);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chat
|
||||
|
||||
@@ -72,6 +72,13 @@ class ChatService
|
||||
*/
|
||||
void processIncoming();
|
||||
|
||||
/**
|
||||
* @brief Handle send result (ack/timeout)
|
||||
* @param msg_id Message ID
|
||||
* @param ok true if sent successfully
|
||||
*/
|
||||
void handleSendResult(MessageId msg_id, bool ok);
|
||||
|
||||
/**
|
||||
* @brief Get current channel
|
||||
*/
|
||||
|
||||
+37
-13
@@ -20,6 +20,7 @@ extern "C"
|
||||
extern const lv_image_dsc_t Chat;
|
||||
extern const lv_image_dsc_t Setting;
|
||||
extern const lv_image_dsc_t contact;
|
||||
extern const lv_image_dsc_t team_icon;
|
||||
extern const lv_image_dsc_t shutdown;
|
||||
// Note: img_usb is already declared in images.h (C++ linkage)
|
||||
}
|
||||
@@ -30,11 +31,18 @@ extern "C"
|
||||
|
||||
// Forward declarations for app entry functions (implemented in ui_*.cpp)
|
||||
void ui_gps_enter(lv_obj_t* parent);
|
||||
void ui_gps_exit(lv_obj_t* parent);
|
||||
void ui_chat_enter(lv_obj_t* parent);
|
||||
void ui_chat_exit(lv_obj_t* parent);
|
||||
void ui_contacts_enter(lv_obj_t* parent);
|
||||
void ui_contacts_exit(lv_obj_t* parent);
|
||||
void ui_team_enter(lv_obj_t* parent);
|
||||
void ui_team_exit(lv_obj_t* parent);
|
||||
void ui_setting_enter(lv_obj_t* parent);
|
||||
void ui_setting_exit(lv_obj_t* parent);
|
||||
#ifdef ARDUINO_USB_MODE
|
||||
void ui_usb_enter(lv_obj_t* parent);
|
||||
void ui_usb_exit(lv_obj_t* parent);
|
||||
#endif
|
||||
|
||||
// GPS data access - now provided by GpsService
|
||||
@@ -81,29 +89,37 @@ typedef struct
|
||||
void* user_data;
|
||||
} app_t;
|
||||
|
||||
static app_t* s_active_app = nullptr;
|
||||
|
||||
// App entry functions (implemented in ui_*.cpp files, global scope)
|
||||
|
||||
app_t ui_gps_main = {
|
||||
.setup_func_cb = ui_gps_enter,
|
||||
.exit_func_cb = nullptr,
|
||||
.exit_func_cb = ui_gps_exit,
|
||||
.user_data = nullptr,
|
||||
};
|
||||
|
||||
app_t ui_chat_main = {
|
||||
.setup_func_cb = ui_chat_enter,
|
||||
.exit_func_cb = nullptr,
|
||||
.exit_func_cb = ui_chat_exit,
|
||||
.user_data = nullptr,
|
||||
};
|
||||
|
||||
app_t ui_contacts_main = {
|
||||
.setup_func_cb = ui_contacts_enter,
|
||||
.exit_func_cb = nullptr,
|
||||
.exit_func_cb = ui_contacts_exit,
|
||||
.user_data = nullptr,
|
||||
};
|
||||
|
||||
app_t ui_team_main = {
|
||||
.setup_func_cb = ui_team_enter,
|
||||
.exit_func_cb = ui_team_exit,
|
||||
.user_data = nullptr,
|
||||
};
|
||||
|
||||
app_t ui_setting_main = {
|
||||
.setup_func_cb = ui_setting_enter,
|
||||
.exit_func_cb = nullptr,
|
||||
.exit_func_cb = ui_setting_exit,
|
||||
.user_data = nullptr,
|
||||
};
|
||||
|
||||
@@ -124,21 +140,21 @@ app_t ui_shutdown_main = {
|
||||
#ifdef ARDUINO_USB_MODE
|
||||
app_t ui_usb_main = {
|
||||
.setup_func_cb = ui_usb_enter,
|
||||
.exit_func_cb = nullptr,
|
||||
.exit_func_cb = ui_usb_exit,
|
||||
.user_data = nullptr,
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_USB_MODE
|
||||
const char* kAppNames[6] = {"GPS", "Chat", "Contacts", "USB Mass Storage", "Setting", "Shutdown"};
|
||||
const lv_image_dsc_t* kAppImages[6] = {&gps_icon, &Chat, &contact, &img_usb, &Setting, &shutdown};
|
||||
app_t* kAppFuncs[6] = {&ui_gps_main, &ui_chat_main, &ui_contacts_main, &ui_usb_main, &ui_setting_main, &ui_shutdown_main};
|
||||
#define NUM_APPS 6
|
||||
const char* kAppNames[7] = {"GPS", "Chat", "Contacts", "Team", "USB Mass Storage", "Setting", "Shutdown"};
|
||||
const lv_image_dsc_t* kAppImages[7] = {&gps_icon, &Chat, &contact, &team_icon, &img_usb, &Setting, &shutdown};
|
||||
app_t* kAppFuncs[7] = {&ui_gps_main, &ui_chat_main, &ui_contacts_main, &ui_team_main, &ui_usb_main, &ui_setting_main, &ui_shutdown_main};
|
||||
#define NUM_APPS 7
|
||||
#else
|
||||
const char* kAppNames[5] = {"GPS", "Chat", "Contacts", "Setting", "Shutdown"};
|
||||
const lv_image_dsc_t* kAppImages[5] = {&gps_icon, &Chat, &contact, &Setting, &shutdown};
|
||||
app_t* kAppFuncs[5] = {&ui_gps_main, &ui_chat_main, &ui_contacts_main, &ui_setting_main, &ui_shutdown_main};
|
||||
#define NUM_APPS 5
|
||||
const char* kAppNames[6] = {"GPS", "Chat", "Contacts", "Team", "Setting", "Shutdown"};
|
||||
const lv_image_dsc_t* kAppImages[6] = {&gps_icon, &Chat, &contact, &team_icon, &Setting, &shutdown};
|
||||
app_t* kAppFuncs[6] = {&ui_gps_main, &ui_chat_main, &ui_contacts_main, &ui_team_main, &ui_setting_main, &ui_shutdown_main};
|
||||
#define NUM_APPS 6
|
||||
#endif
|
||||
|
||||
#if LVGL_VERSION_MAJOR == 9
|
||||
@@ -205,9 +221,13 @@ static void create_app(lv_obj_t* parent, const char* name, const lv_image_dsc_t*
|
||||
}
|
||||
if (c == LV_EVENT_CLICKED) {
|
||||
set_default_group(app_g);
|
||||
if (s_active_app && s_active_app != func_cb && s_active_app->exit_func_cb) {
|
||||
(*s_active_app->exit_func_cb)(parent);
|
||||
}
|
||||
if (func_cb->setup_func_cb) {
|
||||
(*func_cb->setup_func_cb)(parent);
|
||||
}
|
||||
s_active_app = func_cb;
|
||||
menu_hidden();
|
||||
} },
|
||||
LV_EVENT_CLICKED, app_fun);
|
||||
@@ -229,6 +249,10 @@ void menu_name_label_event_cb(lv_event_t* e)
|
||||
// App entry functions are implemented in ui_gps.cpp, ui_chat.cpp, ui_setting.cpp
|
||||
|
||||
} // namespace
|
||||
void ui_clear_active_app()
|
||||
{
|
||||
s_active_app = nullptr;
|
||||
}
|
||||
|
||||
// GPS data access - now provided by TLoRaPagerBoard
|
||||
// GPS data collection task is now in TLoRaPagerBoard class
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include "freertos/semphr.h"
|
||||
#include <Arduino.h>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "../team/domain/team_events.h"
|
||||
|
||||
namespace sys
|
||||
{
|
||||
@@ -25,6 +28,14 @@ enum class EventType
|
||||
ChatChannelSwitched, // Channel switched
|
||||
NodeInfoUpdate, // Node info updated (from mesh network)
|
||||
NodeProtocolUpdate, // Node protocol update (from message)
|
||||
TeamAdvertise, // Team advertise received
|
||||
TeamJoinRequest, // Team join request received
|
||||
TeamJoinAccept, // Team join accept received
|
||||
TeamJoinConfirm, // Team join confirm received
|
||||
TeamStatus, // Team status received
|
||||
TeamPosition, // Team position received
|
||||
TeamWaypoint, // Team waypoint received
|
||||
TeamError, // Team protocol error
|
||||
InputEvent, // Input event (keyboard/rotary)
|
||||
SystemTick // System tick (for periodic tasks)
|
||||
};
|
||||
@@ -138,6 +149,94 @@ struct NodeProtocolUpdateEvent : public Event
|
||||
: Event(EventType::NodeProtocolUpdate), node_id(id), timestamp(ts), protocol(proto) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team advertise event
|
||||
*/
|
||||
struct TeamAdvertiseEvent : public Event
|
||||
{
|
||||
team::TeamAdvertiseEvent data;
|
||||
|
||||
explicit TeamAdvertiseEvent(const team::TeamAdvertiseEvent& evt)
|
||||
: Event(EventType::TeamAdvertise), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team join request event
|
||||
*/
|
||||
struct TeamJoinRequestEvent : public Event
|
||||
{
|
||||
team::TeamJoinRequestEvent data;
|
||||
|
||||
explicit TeamJoinRequestEvent(const team::TeamJoinRequestEvent& evt)
|
||||
: Event(EventType::TeamJoinRequest), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team join accept event
|
||||
*/
|
||||
struct TeamJoinAcceptEvent : public Event
|
||||
{
|
||||
team::TeamJoinAcceptEvent data;
|
||||
|
||||
explicit TeamJoinAcceptEvent(const team::TeamJoinAcceptEvent& evt)
|
||||
: Event(EventType::TeamJoinAccept), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team join confirm event
|
||||
*/
|
||||
struct TeamJoinConfirmEvent : public Event
|
||||
{
|
||||
team::TeamJoinConfirmEvent data;
|
||||
|
||||
explicit TeamJoinConfirmEvent(const team::TeamJoinConfirmEvent& evt)
|
||||
: Event(EventType::TeamJoinConfirm), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team status event
|
||||
*/
|
||||
struct TeamStatusEvent : public Event
|
||||
{
|
||||
team::TeamStatusEvent data;
|
||||
|
||||
explicit TeamStatusEvent(const team::TeamStatusEvent& evt)
|
||||
: Event(EventType::TeamStatus), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team position event
|
||||
*/
|
||||
struct TeamPositionEvent : public Event
|
||||
{
|
||||
team::TeamPositionEvent data;
|
||||
|
||||
explicit TeamPositionEvent(const team::TeamPositionEvent& evt)
|
||||
: Event(EventType::TeamPosition), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team waypoint event
|
||||
*/
|
||||
struct TeamWaypointEvent : public Event
|
||||
{
|
||||
team::TeamWaypointEvent data;
|
||||
|
||||
explicit TeamWaypointEvent(const team::TeamWaypointEvent& evt)
|
||||
: Event(EventType::TeamWaypoint), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Team error event
|
||||
*/
|
||||
struct TeamErrorEvent : public Event
|
||||
{
|
||||
team::TeamErrorEvent data;
|
||||
|
||||
explicit TeamErrorEvent(const team::TeamErrorEvent& evt)
|
||||
: Event(EventType::TeamError), data(evt) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Input event
|
||||
*/
|
||||
|
||||
@@ -59,6 +59,21 @@ class RingBuffer
|
||||
return &buffer_[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get mutable element at index (0 = oldest, count-1 = newest)
|
||||
* @param index Index
|
||||
* @return Pointer to element, or nullptr if index out of range
|
||||
*/
|
||||
T* get(size_t index)
|
||||
{
|
||||
if (index >= count_)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
size_t pos = (tail_ + index) % N;
|
||||
return &buffer_[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the newest element
|
||||
* @return Pointer to newest element, or nullptr if empty
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "team_types.h"
|
||||
#include "../protocol/team_mgmt.h"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace team
|
||||
{
|
||||
|
||||
struct TeamEventContext
|
||||
{
|
||||
TeamId team_id{};
|
||||
uint32_t key_id = 0;
|
||||
uint32_t from = 0;
|
||||
uint32_t timestamp = 0;
|
||||
};
|
||||
|
||||
struct TeamAdvertiseEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
team::proto::TeamAdvertise msg;
|
||||
};
|
||||
|
||||
struct TeamJoinRequestEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
team::proto::TeamJoinRequest msg;
|
||||
};
|
||||
|
||||
struct TeamJoinAcceptEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
team::proto::TeamJoinAccept msg;
|
||||
};
|
||||
|
||||
struct TeamJoinConfirmEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
team::proto::TeamJoinConfirm msg;
|
||||
};
|
||||
|
||||
struct TeamStatusEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
team::proto::TeamStatus msg;
|
||||
};
|
||||
|
||||
struct TeamPositionEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
std::vector<uint8_t> payload;
|
||||
};
|
||||
|
||||
struct TeamWaypointEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
std::vector<uint8_t> payload;
|
||||
};
|
||||
|
||||
enum class TeamProtocolError
|
||||
{
|
||||
DecryptFail,
|
||||
DecodeFail,
|
||||
KeyMismatch,
|
||||
UnknownVersion
|
||||
};
|
||||
|
||||
struct TeamErrorEvent
|
||||
{
|
||||
TeamEventContext ctx;
|
||||
TeamProtocolError error;
|
||||
uint32_t portnum = 0;
|
||||
};
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "../protocol/team_wire.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace team
|
||||
{
|
||||
|
||||
constexpr size_t kTeamKeySize = 32;
|
||||
|
||||
using TeamId = std::array<uint8_t, team::proto::kTeamIdSize>;
|
||||
|
||||
struct TeamKeys
|
||||
{
|
||||
TeamId team_id{};
|
||||
uint32_t key_id = 0;
|
||||
std::array<uint8_t, kTeamKeySize> mgmt_key{};
|
||||
std::array<uint8_t, kTeamKeySize> pos_key{};
|
||||
std::array<uint8_t, kTeamKeySize> wp_key{};
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "team_crypto.h"
|
||||
|
||||
#include <ChaChaPoly.h>
|
||||
#include <SHA256.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace team::infra
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr size_t kChaChaTagSize = 16;
|
||||
|
||||
bool sha256Kdf(const uint8_t* key, size_t key_len, const char* info,
|
||||
uint8_t* out, size_t out_len)
|
||||
{
|
||||
if (!key || !info || !out || out_len > 32)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SHA256 hash;
|
||||
hash.reset();
|
||||
hash.update(key, key_len);
|
||||
hash.update(reinterpret_cast<const uint8_t*>(info), strlen(info));
|
||||
uint8_t digest[32];
|
||||
hash.finalize(digest, sizeof(digest));
|
||||
memcpy(out, digest, out_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool chachaEncrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* plain, size_t plain_len,
|
||||
std::vector<uint8_t>& out_cipher)
|
||||
{
|
||||
if (!key || !nonce || !plain)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ChaChaPoly cipher;
|
||||
if (!cipher.setKey(key, key_len))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!cipher.setIV(nonce, nonce_len))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aad_len > 0 && aad)
|
||||
{
|
||||
cipher.addAuthData(aad, aad_len);
|
||||
}
|
||||
|
||||
out_cipher.assign(plain_len + kChaChaTagSize, 0);
|
||||
if (plain_len > 0)
|
||||
{
|
||||
cipher.encrypt(out_cipher.data(), plain, plain_len);
|
||||
}
|
||||
cipher.computeTag(out_cipher.data() + plain_len, kChaChaTagSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool chachaDecrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* cipher, size_t cipher_len,
|
||||
std::vector<uint8_t>& out_plain)
|
||||
{
|
||||
if (!key || !nonce || !cipher || cipher_len < kChaChaTagSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ChaChaPoly cipher_ctx;
|
||||
if (!cipher_ctx.setKey(key, key_len))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!cipher_ctx.setIV(nonce, nonce_len))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aad_len > 0 && aad)
|
||||
{
|
||||
cipher_ctx.addAuthData(aad, aad_len);
|
||||
}
|
||||
|
||||
size_t plain_len = cipher_len - kChaChaTagSize;
|
||||
out_plain.assign(plain_len, 0);
|
||||
if (plain_len > 0)
|
||||
{
|
||||
cipher_ctx.decrypt(out_plain.data(), cipher, plain_len);
|
||||
}
|
||||
|
||||
const uint8_t* tag = cipher + plain_len;
|
||||
if (!cipher_ctx.checkTag(tag, kChaChaTagSize))
|
||||
{
|
||||
out_plain.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TeamCrypto::deriveKey(const uint8_t* key, size_t key_len,
|
||||
const char* info,
|
||||
uint8_t* out, size_t out_len)
|
||||
{
|
||||
return sha256Kdf(key, key_len, info, out, out_len);
|
||||
}
|
||||
|
||||
bool TeamCrypto::aeadEncrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* plain, size_t plain_len,
|
||||
std::vector<uint8_t>& out_cipher)
|
||||
{
|
||||
return chachaEncrypt(key, key_len, nonce, nonce_len,
|
||||
aad, aad_len, plain, plain_len, out_cipher);
|
||||
}
|
||||
|
||||
bool TeamCrypto::aeadDecrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* cipher, size_t cipher_len,
|
||||
std::vector<uint8_t>& out_plain)
|
||||
{
|
||||
return chachaDecrypt(key, key_len, nonce, nonce_len,
|
||||
aad, aad_len, cipher, cipher_len, out_plain);
|
||||
}
|
||||
|
||||
} // namespace team::infra
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../ports/i_team_crypto.h"
|
||||
|
||||
namespace team::infra
|
||||
{
|
||||
|
||||
class TeamCrypto : public team::ITeamCrypto
|
||||
{
|
||||
public:
|
||||
bool deriveKey(const uint8_t* key, size_t key_len,
|
||||
const char* info,
|
||||
uint8_t* out, size_t out_len) override;
|
||||
|
||||
bool aeadEncrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* plain, size_t plain_len,
|
||||
std::vector<uint8_t>& out_cipher) override;
|
||||
|
||||
bool aeadDecrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* cipher, size_t cipher_len,
|
||||
std::vector<uint8_t>& out_plain) override;
|
||||
};
|
||||
|
||||
} // namespace team::infra
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "team_event_bus_sink.h"
|
||||
|
||||
#include "../../../sys/event_bus.h"
|
||||
|
||||
namespace team::infra
|
||||
{
|
||||
|
||||
void TeamEventBusSink::onTeamAdvertise(const TeamAdvertiseEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamAdvertiseEvent(event), 0);
|
||||
}
|
||||
|
||||
void TeamEventBusSink::onTeamJoinRequest(const TeamJoinRequestEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamJoinRequestEvent(event), 0);
|
||||
}
|
||||
|
||||
void TeamEventBusSink::onTeamJoinAccept(const TeamJoinAcceptEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamJoinAcceptEvent(event), 0);
|
||||
}
|
||||
|
||||
void TeamEventBusSink::onTeamJoinConfirm(const TeamJoinConfirmEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamJoinConfirmEvent(event), 0);
|
||||
}
|
||||
|
||||
void TeamEventBusSink::onTeamStatus(const TeamStatusEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamStatusEvent(event), 0);
|
||||
}
|
||||
|
||||
void TeamEventBusSink::onTeamPosition(const TeamPositionEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamPositionEvent(event), 0);
|
||||
}
|
||||
|
||||
void TeamEventBusSink::onTeamWaypoint(const TeamWaypointEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamWaypointEvent(event), 0);
|
||||
}
|
||||
|
||||
void TeamEventBusSink::onTeamError(const TeamErrorEvent& event)
|
||||
{
|
||||
sys::EventBus::publish(new sys::TeamErrorEvent(event), 0);
|
||||
}
|
||||
|
||||
} // namespace team::infra
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../ports/i_team_event_sink.h"
|
||||
|
||||
namespace team::infra
|
||||
{
|
||||
|
||||
class TeamEventBusSink : public team::ITeamEventSink
|
||||
{
|
||||
public:
|
||||
void onTeamAdvertise(const TeamAdvertiseEvent& event) override;
|
||||
void onTeamJoinRequest(const TeamJoinRequestEvent& event) override;
|
||||
void onTeamJoinAccept(const TeamJoinAcceptEvent& event) override;
|
||||
void onTeamJoinConfirm(const TeamJoinConfirmEvent& event) override;
|
||||
void onTeamStatus(const TeamStatusEvent& event) override;
|
||||
void onTeamPosition(const TeamPositionEvent& event) override;
|
||||
void onTeamWaypoint(const TeamWaypointEvent& event) override;
|
||||
void onTeamError(const TeamErrorEvent& event) override;
|
||||
};
|
||||
|
||||
} // namespace team::infra
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace team
|
||||
{
|
||||
|
||||
class ITeamCrypto
|
||||
{
|
||||
public:
|
||||
virtual ~ITeamCrypto() = default;
|
||||
|
||||
virtual bool deriveKey(const uint8_t* key, size_t key_len,
|
||||
const char* info,
|
||||
uint8_t* out, size_t out_len) = 0;
|
||||
|
||||
virtual bool aeadEncrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* plain, size_t plain_len,
|
||||
std::vector<uint8_t>& out_cipher) = 0;
|
||||
|
||||
virtual bool aeadDecrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* nonce, size_t nonce_len,
|
||||
const uint8_t* aad, size_t aad_len,
|
||||
const uint8_t* cipher, size_t cipher_len,
|
||||
std::vector<uint8_t>& out_plain) = 0;
|
||||
};
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "../domain/team_events.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
|
||||
class ITeamEventSink
|
||||
{
|
||||
public:
|
||||
virtual ~ITeamEventSink() = default;
|
||||
|
||||
virtual void onTeamAdvertise(const TeamAdvertiseEvent& event) = 0;
|
||||
virtual void onTeamJoinRequest(const TeamJoinRequestEvent& event) = 0;
|
||||
virtual void onTeamJoinAccept(const TeamJoinAcceptEvent& event) = 0;
|
||||
virtual void onTeamJoinConfirm(const TeamJoinConfirmEvent& event) = 0;
|
||||
virtual void onTeamStatus(const TeamStatusEvent& event) = 0;
|
||||
virtual void onTeamPosition(const TeamPositionEvent& event) = 0;
|
||||
virtual void onTeamWaypoint(const TeamWaypointEvent& event) = 0;
|
||||
virtual void onTeamError(const TeamErrorEvent& event) = 0;
|
||||
};
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,380 @@
|
||||
#include "team_mgmt.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace team::proto
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
struct ByteWriter
|
||||
{
|
||||
explicit ByteWriter(std::vector<uint8_t>& out_ref) : out(out_ref) {}
|
||||
|
||||
void putU8(uint8_t v) { out.push_back(v); }
|
||||
void putU16(uint16_t v)
|
||||
{
|
||||
out.push_back(static_cast<uint8_t>(v & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
|
||||
}
|
||||
void putU32(uint32_t v)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
out.push_back(static_cast<uint8_t>((v >> (8 * i)) & 0xFF));
|
||||
}
|
||||
}
|
||||
void putU64(uint64_t v)
|
||||
{
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
out.push_back(static_cast<uint8_t>((v >> (8 * i)) & 0xFF));
|
||||
}
|
||||
}
|
||||
void putBytes(const uint8_t* data, size_t len)
|
||||
{
|
||||
out.insert(out.end(), data, data + len);
|
||||
}
|
||||
|
||||
std::vector<uint8_t>& out;
|
||||
};
|
||||
|
||||
struct ByteReader
|
||||
{
|
||||
ByteReader(const uint8_t* data_ref, size_t len_ref) : data(data_ref), len(len_ref) {}
|
||||
|
||||
bool getU8(uint8_t* out_val)
|
||||
{
|
||||
if (!ensure(1)) return false;
|
||||
*out_val = data[pos++];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getU16(uint16_t* out_val)
|
||||
{
|
||||
if (!ensure(2)) return false;
|
||||
*out_val = static_cast<uint16_t>(data[pos]) |
|
||||
static_cast<uint16_t>(data[pos + 1] << 8);
|
||||
pos += 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getU32(uint32_t* out_val)
|
||||
{
|
||||
if (!ensure(4)) return false;
|
||||
uint32_t v = 0;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
v |= static_cast<uint32_t>(data[pos + i]) << (8 * i);
|
||||
}
|
||||
*out_val = v;
|
||||
pos += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getU64(uint64_t* out_val)
|
||||
{
|
||||
if (!ensure(8)) return false;
|
||||
uint64_t v = 0;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
v |= static_cast<uint64_t>(data[pos + i]) << (8 * i);
|
||||
}
|
||||
*out_val = v;
|
||||
pos += 8;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getBytes(uint8_t* out_buf, size_t out_len)
|
||||
{
|
||||
if (!ensure(out_len)) return false;
|
||||
memcpy(out_buf, data + pos, out_len);
|
||||
pos += out_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ensure(size_t count) const { return (pos + count) <= len; }
|
||||
|
||||
const uint8_t* data;
|
||||
size_t len;
|
||||
size_t pos = 0;
|
||||
};
|
||||
|
||||
bool encodeTeamParams(const TeamParams& params, ByteWriter& writer)
|
||||
{
|
||||
if (!params.has_params)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
writer.putU32(params.position_interval_ms);
|
||||
writer.putU8(params.precision_level);
|
||||
writer.putU32(params.flags);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamParams(ByteReader& reader, TeamParams* params)
|
||||
{
|
||||
if (!params)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
params->has_params = true;
|
||||
if (!reader.getU32(¶ms->position_interval_ms)) return false;
|
||||
if (!reader.getU8(¶ms->precision_level)) return false;
|
||||
if (!reader.getU32(¶ms->flags)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool encodeTeamMgmtMessage(TeamMgmtType type,
|
||||
const std::vector<uint8_t>& payload,
|
||||
std::vector<uint8_t>& out)
|
||||
{
|
||||
if (payload.size() > 0xFFFF)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out.clear();
|
||||
ByteWriter writer(out);
|
||||
writer.putU8(kTeamMgmtVersion);
|
||||
writer.putU8(static_cast<uint8_t>(type));
|
||||
writer.putU16(0); // reserved
|
||||
writer.putU16(static_cast<uint16_t>(payload.size()));
|
||||
if (!payload.empty())
|
||||
{
|
||||
writer.putBytes(payload.data(), payload.size());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamMgmtMessage(const uint8_t* data, size_t len,
|
||||
uint8_t* out_version,
|
||||
TeamMgmtType* out_type,
|
||||
std::vector<uint8_t>& out_payload)
|
||||
{
|
||||
if (!data || !out_version || !out_type)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ByteReader reader(data, len);
|
||||
uint16_t reserved = 0;
|
||||
uint16_t payload_len = 0;
|
||||
uint8_t type_raw = 0;
|
||||
|
||||
if (!reader.getU8(out_version)) return false;
|
||||
if (!reader.getU8(&type_raw)) return false;
|
||||
if (!reader.getU16(&reserved)) return false;
|
||||
if (!reader.getU16(&payload_len)) return false;
|
||||
if (!reader.ensure(payload_len)) return false;
|
||||
|
||||
out_payload.assign(data + reader.pos, data + reader.pos + payload_len);
|
||||
*out_type = static_cast<TeamMgmtType>(type_raw);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeTeamAdvertise(const TeamAdvertise& input, std::vector<uint8_t>& out)
|
||||
{
|
||||
out.clear();
|
||||
ByteWriter writer(out);
|
||||
writer.putBytes(input.team_id.data(), input.team_id.size());
|
||||
|
||||
uint16_t flags = 0;
|
||||
if (input.has_join_hint) flags |= 0x01;
|
||||
if (input.has_channel_index) flags |= 0x02;
|
||||
if (input.has_expires_at) flags |= 0x04;
|
||||
writer.putU16(flags);
|
||||
|
||||
if (input.has_join_hint) writer.putU32(input.join_hint);
|
||||
if (input.has_channel_index) writer.putU8(input.channel_index);
|
||||
if (input.has_expires_at) writer.putU64(input.expires_at);
|
||||
writer.putU64(input.nonce);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamAdvertise(const uint8_t* data, size_t len, TeamAdvertise* out)
|
||||
{
|
||||
if (!data || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ByteReader reader(data, len);
|
||||
uint16_t flags = 0;
|
||||
|
||||
if (!reader.getBytes(out->team_id.data(), out->team_id.size())) return false;
|
||||
if (!reader.getU16(&flags)) return false;
|
||||
|
||||
out->has_join_hint = (flags & 0x01) != 0;
|
||||
out->has_channel_index = (flags & 0x02) != 0;
|
||||
out->has_expires_at = (flags & 0x04) != 0;
|
||||
|
||||
if (out->has_join_hint && !reader.getU32(&out->join_hint)) return false;
|
||||
if (out->has_channel_index && !reader.getU8(&out->channel_index)) return false;
|
||||
if (out->has_expires_at && !reader.getU64(&out->expires_at)) return false;
|
||||
if (!reader.getU64(&out->nonce)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeTeamJoinRequest(const TeamJoinRequest& input, std::vector<uint8_t>& out)
|
||||
{
|
||||
if (input.has_member_pub && input.member_pub_len > input.member_pub.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out.clear();
|
||||
ByteWriter writer(out);
|
||||
writer.putBytes(input.team_id.data(), input.team_id.size());
|
||||
|
||||
uint16_t flags = 0;
|
||||
if (input.has_member_pub) flags |= 0x01;
|
||||
if (input.has_capabilities) flags |= 0x02;
|
||||
writer.putU16(flags);
|
||||
|
||||
if (input.has_member_pub)
|
||||
{
|
||||
writer.putU8(input.member_pub_len);
|
||||
writer.putBytes(input.member_pub.data(), input.member_pub_len);
|
||||
}
|
||||
if (input.has_capabilities) writer.putU32(input.capabilities);
|
||||
writer.putU64(input.nonce);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamJoinRequest(const uint8_t* data, size_t len, TeamJoinRequest* out)
|
||||
{
|
||||
if (!data || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ByteReader reader(data, len);
|
||||
uint16_t flags = 0;
|
||||
if (!reader.getBytes(out->team_id.data(), out->team_id.size())) return false;
|
||||
if (!reader.getU16(&flags)) return false;
|
||||
|
||||
out->has_member_pub = (flags & 0x01) != 0;
|
||||
out->has_capabilities = (flags & 0x02) != 0;
|
||||
|
||||
if (out->has_member_pub)
|
||||
{
|
||||
uint8_t pub_len = 0;
|
||||
if (!reader.getU8(&pub_len)) return false;
|
||||
if (pub_len > out->member_pub.size()) return false;
|
||||
out->member_pub_len = pub_len;
|
||||
if (!reader.getBytes(out->member_pub.data(), pub_len)) return false;
|
||||
}
|
||||
if (out->has_capabilities && !reader.getU32(&out->capabilities)) return false;
|
||||
if (!reader.getU64(&out->nonce)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeTeamJoinAccept(const TeamJoinAccept& input, std::vector<uint8_t>& out)
|
||||
{
|
||||
if (input.channel_psk_len > input.channel_psk.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out.clear();
|
||||
ByteWriter writer(out);
|
||||
writer.putU8(input.channel_index);
|
||||
writer.putU8(input.channel_psk_len);
|
||||
writer.putBytes(input.channel_psk.data(), input.channel_psk_len);
|
||||
writer.putU32(input.key_id);
|
||||
|
||||
uint16_t flags = 0;
|
||||
if (input.params.has_params) flags |= 0x01;
|
||||
writer.putU16(flags);
|
||||
if (!encodeTeamParams(input.params, writer)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamJoinAccept(const uint8_t* data, size_t len, TeamJoinAccept* out)
|
||||
{
|
||||
if (!data || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ByteReader reader(data, len);
|
||||
uint16_t flags = 0;
|
||||
|
||||
if (!reader.getU8(&out->channel_index)) return false;
|
||||
if (!reader.getU8(&out->channel_psk_len)) return false;
|
||||
if (out->channel_psk_len > out->channel_psk.size()) return false;
|
||||
if (!reader.getBytes(out->channel_psk.data(), out->channel_psk_len)) return false;
|
||||
if (!reader.getU32(&out->key_id)) return false;
|
||||
if (!reader.getU16(&flags)) return false;
|
||||
|
||||
out->params.has_params = (flags & 0x01) != 0;
|
||||
if (out->params.has_params && !decodeTeamParams(reader, &out->params)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeTeamJoinConfirm(const TeamJoinConfirm& input, std::vector<uint8_t>& out)
|
||||
{
|
||||
out.clear();
|
||||
ByteWriter writer(out);
|
||||
writer.putU8(input.ok ? 1 : 0);
|
||||
|
||||
uint16_t flags = 0;
|
||||
if (input.has_capabilities) flags |= 0x01;
|
||||
if (input.has_battery) flags |= 0x02;
|
||||
writer.putU16(flags);
|
||||
|
||||
if (input.has_capabilities) writer.putU32(input.capabilities);
|
||||
if (input.has_battery) writer.putU8(input.battery);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamJoinConfirm(const uint8_t* data, size_t len, TeamJoinConfirm* out)
|
||||
{
|
||||
if (!data || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ByteReader reader(data, len);
|
||||
uint16_t flags = 0;
|
||||
uint8_t ok = 0;
|
||||
if (!reader.getU8(&ok)) return false;
|
||||
out->ok = (ok != 0);
|
||||
if (!reader.getU16(&flags)) return false;
|
||||
|
||||
out->has_capabilities = (flags & 0x01) != 0;
|
||||
out->has_battery = (flags & 0x02) != 0;
|
||||
|
||||
if (out->has_capabilities && !reader.getU32(&out->capabilities)) return false;
|
||||
if (out->has_battery && !reader.getU8(&out->battery)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeTeamStatus(const TeamStatus& input, std::vector<uint8_t>& out)
|
||||
{
|
||||
out.clear();
|
||||
ByteWriter writer(out);
|
||||
writer.putBytes(input.member_list_hash.data(), input.member_list_hash.size());
|
||||
writer.putU32(input.key_id);
|
||||
|
||||
uint16_t flags = 0;
|
||||
if (input.params.has_params) flags |= 0x01;
|
||||
writer.putU16(flags);
|
||||
if (!encodeTeamParams(input.params, writer)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamStatus(const uint8_t* data, size_t len, TeamStatus* out)
|
||||
{
|
||||
if (!data || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ByteReader reader(data, len);
|
||||
uint16_t flags = 0;
|
||||
if (!reader.getBytes(out->member_list_hash.data(), out->member_list_hash.size())) return false;
|
||||
if (!reader.getU32(&out->key_id)) return false;
|
||||
if (!reader.getU16(&flags)) return false;
|
||||
|
||||
out->params.has_params = (flags & 0x01) != 0;
|
||||
if (out->params.has_params && !decodeTeamParams(reader, &out->params)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace team::proto
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include "team_wire.h"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace team::proto
|
||||
{
|
||||
|
||||
constexpr uint8_t kTeamMgmtVersion = 1;
|
||||
constexpr size_t kTeamMemberListHashSize = 32;
|
||||
constexpr size_t kTeamChannelPskSize = 16;
|
||||
|
||||
enum class TeamMgmtType : uint8_t
|
||||
{
|
||||
Advertise = 1,
|
||||
JoinRequest = 2,
|
||||
JoinAccept = 3,
|
||||
JoinConfirm = 4,
|
||||
Status = 5,
|
||||
Rotate = 6,
|
||||
Leave = 7,
|
||||
Disband = 8
|
||||
};
|
||||
|
||||
struct TeamParams
|
||||
{
|
||||
uint32_t position_interval_ms = 0;
|
||||
uint8_t precision_level = 0;
|
||||
uint32_t flags = 0;
|
||||
bool has_params = false;
|
||||
};
|
||||
|
||||
struct TeamAdvertise
|
||||
{
|
||||
std::array<uint8_t, kTeamIdSize> team_id{};
|
||||
uint32_t join_hint = 0;
|
||||
uint8_t channel_index = 0;
|
||||
uint64_t expires_at = 0;
|
||||
uint64_t nonce = 0;
|
||||
bool has_join_hint = false;
|
||||
bool has_channel_index = false;
|
||||
bool has_expires_at = false;
|
||||
};
|
||||
|
||||
struct TeamJoinRequest
|
||||
{
|
||||
std::array<uint8_t, kTeamIdSize> team_id{};
|
||||
std::array<uint8_t, 32> member_pub{};
|
||||
uint8_t member_pub_len = 0;
|
||||
uint32_t capabilities = 0;
|
||||
uint64_t nonce = 0;
|
||||
bool has_member_pub = false;
|
||||
bool has_capabilities = false;
|
||||
};
|
||||
|
||||
struct TeamJoinAccept
|
||||
{
|
||||
uint8_t channel_index = 0;
|
||||
std::array<uint8_t, kTeamChannelPskSize> channel_psk{};
|
||||
uint8_t channel_psk_len = 0;
|
||||
uint32_t key_id = 0;
|
||||
TeamParams params;
|
||||
};
|
||||
|
||||
struct TeamJoinConfirm
|
||||
{
|
||||
bool ok = false;
|
||||
uint32_t capabilities = 0;
|
||||
uint8_t battery = 0;
|
||||
bool has_capabilities = false;
|
||||
bool has_battery = false;
|
||||
};
|
||||
|
||||
struct TeamStatus
|
||||
{
|
||||
std::array<uint8_t, kTeamMemberListHashSize> member_list_hash{};
|
||||
uint32_t key_id = 0;
|
||||
TeamParams params;
|
||||
};
|
||||
|
||||
bool encodeTeamMgmtMessage(TeamMgmtType type,
|
||||
const std::vector<uint8_t>& payload,
|
||||
std::vector<uint8_t>& out);
|
||||
bool decodeTeamMgmtMessage(const uint8_t* data, size_t len,
|
||||
uint8_t* out_version,
|
||||
TeamMgmtType* out_type,
|
||||
std::vector<uint8_t>& out_payload);
|
||||
|
||||
bool encodeTeamAdvertise(const TeamAdvertise& input, std::vector<uint8_t>& out);
|
||||
bool decodeTeamAdvertise(const uint8_t* data, size_t len, TeamAdvertise* out);
|
||||
|
||||
bool encodeTeamJoinRequest(const TeamJoinRequest& input, std::vector<uint8_t>& out);
|
||||
bool decodeTeamJoinRequest(const uint8_t* data, size_t len, TeamJoinRequest* out);
|
||||
|
||||
bool encodeTeamJoinAccept(const TeamJoinAccept& input, std::vector<uint8_t>& out);
|
||||
bool decodeTeamJoinAccept(const uint8_t* data, size_t len, TeamJoinAccept* out);
|
||||
|
||||
bool encodeTeamJoinConfirm(const TeamJoinConfirm& input, std::vector<uint8_t>& out);
|
||||
bool decodeTeamJoinConfirm(const uint8_t* data, size_t len, TeamJoinConfirm* out);
|
||||
|
||||
bool encodeTeamStatus(const TeamStatus& input, std::vector<uint8_t>& out);
|
||||
bool decodeTeamStatus(const uint8_t* data, size_t len, TeamStatus* out);
|
||||
|
||||
} // namespace team::proto
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace team::proto
|
||||
{
|
||||
|
||||
constexpr uint32_t TEAM_MGMT_APP = 300;
|
||||
constexpr uint32_t TEAM_POSITION_APP = 301;
|
||||
constexpr uint32_t TEAM_WAYPOINT_APP = 302;
|
||||
|
||||
} // namespace team::proto
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "team_wire.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace team::proto
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
struct ByteWriter
|
||||
{
|
||||
explicit ByteWriter(std::vector<uint8_t>& out_ref) : out(out_ref) {}
|
||||
|
||||
void putU8(uint8_t v) { out.push_back(v); }
|
||||
void putU16(uint16_t v)
|
||||
{
|
||||
out.push_back(static_cast<uint8_t>(v & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
|
||||
}
|
||||
void putU32(uint32_t v)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
out.push_back(static_cast<uint8_t>((v >> (8 * i)) & 0xFF));
|
||||
}
|
||||
}
|
||||
void putBytes(const uint8_t* data, size_t len)
|
||||
{
|
||||
out.insert(out.end(), data, data + len);
|
||||
}
|
||||
|
||||
std::vector<uint8_t>& out;
|
||||
};
|
||||
|
||||
struct ByteReader
|
||||
{
|
||||
ByteReader(const uint8_t* data_ref, size_t len_ref) : data(data_ref), len(len_ref) {}
|
||||
|
||||
bool getU8(uint8_t* out_val)
|
||||
{
|
||||
if (!ensure(1)) return false;
|
||||
*out_val = data[pos++];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getU16(uint16_t* out_val)
|
||||
{
|
||||
if (!ensure(2)) return false;
|
||||
*out_val = static_cast<uint16_t>(data[pos]) |
|
||||
static_cast<uint16_t>(data[pos + 1] << 8);
|
||||
pos += 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getU32(uint32_t* out_val)
|
||||
{
|
||||
if (!ensure(4)) return false;
|
||||
uint32_t v = 0;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
v |= static_cast<uint32_t>(data[pos + i]) << (8 * i);
|
||||
}
|
||||
*out_val = v;
|
||||
pos += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getBytes(uint8_t* out_buf, size_t out_len)
|
||||
{
|
||||
if (!ensure(out_len)) return false;
|
||||
memcpy(out_buf, data + pos, out_len);
|
||||
pos += out_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool skip(size_t count)
|
||||
{
|
||||
if (!ensure(count)) return false;
|
||||
pos += count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ensure(size_t count) const { return (pos + count) <= len; }
|
||||
|
||||
const uint8_t* data;
|
||||
size_t len;
|
||||
size_t pos = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool encodeTeamEncrypted(const TeamEncrypted& input, std::vector<uint8_t>& out)
|
||||
{
|
||||
if (input.ciphertext.size() > 0xFFFF)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
out.clear();
|
||||
ByteWriter writer(out);
|
||||
writer.putU8(input.version);
|
||||
writer.putU8(input.aad_flags);
|
||||
writer.putU16(0); // reserved
|
||||
writer.putU32(input.key_id);
|
||||
writer.putBytes(input.team_id.data(), input.team_id.size());
|
||||
writer.putBytes(input.nonce.data(), input.nonce.size());
|
||||
writer.putU16(static_cast<uint16_t>(input.ciphertext.size()));
|
||||
if (!input.ciphertext.empty())
|
||||
{
|
||||
writer.putBytes(input.ciphertext.data(), input.ciphertext.size());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decodeTeamEncrypted(const uint8_t* data, size_t len, TeamEncrypted* out)
|
||||
{
|
||||
if (!data || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteReader reader(data, len);
|
||||
uint16_t reserved = 0;
|
||||
uint16_t cipher_len = 0;
|
||||
|
||||
if (!reader.getU8(&out->version)) return false;
|
||||
if (!reader.getU8(&out->aad_flags)) return false;
|
||||
if (!reader.getU16(&reserved)) return false;
|
||||
if (!reader.getU32(&out->key_id)) return false;
|
||||
if (!reader.getBytes(out->team_id.data(), out->team_id.size())) return false;
|
||||
if (!reader.getBytes(out->nonce.data(), out->nonce.size())) return false;
|
||||
if (!reader.getU16(&cipher_len)) return false;
|
||||
if (!reader.ensure(cipher_len)) return false;
|
||||
|
||||
out->ciphertext.assign(data + reader.pos, data + reader.pos + cipher_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace team::proto
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace team::proto
|
||||
{
|
||||
|
||||
constexpr size_t kTeamIdSize = 8;
|
||||
constexpr size_t kTeamNonceSize = 12;
|
||||
constexpr uint8_t kTeamEnvelopeVersion = 1;
|
||||
|
||||
struct TeamEncrypted
|
||||
{
|
||||
uint8_t version = kTeamEnvelopeVersion;
|
||||
uint8_t aad_flags = 0;
|
||||
uint32_t key_id = 0;
|
||||
std::array<uint8_t, kTeamIdSize> team_id{};
|
||||
std::array<uint8_t, kTeamNonceSize> nonce{};
|
||||
std::vector<uint8_t> ciphertext;
|
||||
};
|
||||
|
||||
bool encodeTeamEncrypted(const TeamEncrypted& input, std::vector<uint8_t>& out);
|
||||
bool decodeTeamEncrypted(const uint8_t* data, size_t len, TeamEncrypted* out);
|
||||
|
||||
} // namespace team::proto
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "team_controller.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
|
||||
TeamController::TeamController(TeamService& service)
|
||||
: service_(service)
|
||||
{
|
||||
}
|
||||
|
||||
void TeamController::setKeys(const TeamKeys& keys)
|
||||
{
|
||||
service_.setKeys(keys);
|
||||
}
|
||||
|
||||
void TeamController::clearKeys()
|
||||
{
|
||||
service_.clearKeys();
|
||||
state_ = TeamUiState::Idle;
|
||||
}
|
||||
|
||||
bool TeamController::onCreateTeam(const team::proto::TeamAdvertise& advertise,
|
||||
chat::ChannelId channel)
|
||||
{
|
||||
bool ok = service_.sendAdvertise(advertise, channel);
|
||||
if (ok)
|
||||
{
|
||||
state_ = TeamUiState::Active;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TeamController::onJoinTeam(const team::proto::TeamJoinRequest& join_request,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
bool ok = service_.sendJoinRequest(join_request, channel, dest);
|
||||
if (ok)
|
||||
{
|
||||
state_ = TeamUiState::PendingJoin;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TeamController::onAcceptJoin(const team::proto::TeamJoinAccept& accept,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
bool ok = service_.sendJoinAccept(accept, channel, dest);
|
||||
if (ok)
|
||||
{
|
||||
state_ = TeamUiState::Active;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TeamController::onConfirmJoin(const team::proto::TeamJoinConfirm& confirm,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
bool ok = service_.sendJoinConfirm(confirm, channel, dest);
|
||||
if (ok)
|
||||
{
|
||||
state_ = TeamUiState::Active;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void TeamController::resetUiState()
|
||||
{
|
||||
state_ = TeamUiState::Idle;
|
||||
}
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../chat/domain/chat_types.h"
|
||||
#include "../domain/team_types.h"
|
||||
#include "../protocol/team_mgmt.h"
|
||||
#include "team_service.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
|
||||
enum class TeamUiState
|
||||
{
|
||||
Idle,
|
||||
PendingJoin,
|
||||
Active
|
||||
};
|
||||
|
||||
class TeamController
|
||||
{
|
||||
public:
|
||||
explicit TeamController(TeamService& service);
|
||||
|
||||
void setKeys(const TeamKeys& keys);
|
||||
void clearKeys();
|
||||
|
||||
bool onCreateTeam(const team::proto::TeamAdvertise& advertise,
|
||||
chat::ChannelId channel);
|
||||
bool onJoinTeam(const team::proto::TeamJoinRequest& join_request,
|
||||
chat::ChannelId channel, chat::NodeId dest = 0);
|
||||
bool onAcceptJoin(const team::proto::TeamJoinAccept& accept,
|
||||
chat::ChannelId channel, chat::NodeId dest);
|
||||
bool onConfirmJoin(const team::proto::TeamJoinConfirm& confirm,
|
||||
chat::ChannelId channel, chat::NodeId dest = 0);
|
||||
|
||||
TeamUiState getState() const { return state_; }
|
||||
void resetUiState();
|
||||
|
||||
private:
|
||||
TeamService& service_;
|
||||
TeamUiState state_ = TeamUiState::Idle;
|
||||
};
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,470 @@
|
||||
#include "team_service.h"
|
||||
|
||||
#include "../domain/team_events.h"
|
||||
#include "../protocol/team_mgmt.h"
|
||||
#include "../protocol/team_portnum.h"
|
||||
#include "../protocol/team_wire.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<uint8_t> buildAad(const team::proto::TeamEncrypted& envelope)
|
||||
{
|
||||
std::vector<uint8_t> aad;
|
||||
aad.reserve(1 + 1 + 4 + envelope.team_id.size());
|
||||
aad.push_back(envelope.version);
|
||||
aad.push_back(envelope.aad_flags);
|
||||
|
||||
uint32_t key_id = envelope.key_id;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
aad.push_back(static_cast<uint8_t>((key_id >> (8 * i)) & 0xFF));
|
||||
}
|
||||
|
||||
aad.insert(aad.end(), envelope.team_id.begin(), envelope.team_id.end());
|
||||
return aad;
|
||||
}
|
||||
|
||||
team::TeamEventContext makeContext(const chat::MeshIncomingData& data,
|
||||
const team::proto::TeamEncrypted* envelope)
|
||||
{
|
||||
team::TeamEventContext ctx;
|
||||
if (envelope)
|
||||
{
|
||||
ctx.team_id = envelope->team_id;
|
||||
ctx.key_id = envelope->key_id;
|
||||
}
|
||||
ctx.from = data.from;
|
||||
ctx.timestamp = millis() / 1000;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
team::TeamEventContext makeContext(const chat::MeshIncomingData& data,
|
||||
const team::TeamId& team_id)
|
||||
{
|
||||
team::TeamEventContext ctx;
|
||||
ctx.team_id = team_id;
|
||||
ctx.key_id = 0;
|
||||
ctx.from = data.from;
|
||||
ctx.timestamp = millis() / 1000;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void fillNonce(std::array<uint8_t, team::proto::kTeamNonceSize>& nonce)
|
||||
{
|
||||
for (size_t i = 0; i < nonce.size(); ++i)
|
||||
{
|
||||
nonce[i] = static_cast<uint8_t>(random(0, 256));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TeamService::TeamService(team::ITeamCrypto& crypto,
|
||||
chat::IMeshAdapter& mesh,
|
||||
team::ITeamEventSink& sink)
|
||||
: crypto_(crypto), mesh_(mesh), sink_(sink)
|
||||
{
|
||||
}
|
||||
|
||||
void TeamService::setKeys(const TeamKeys& keys)
|
||||
{
|
||||
keys_ = keys;
|
||||
keys_.valid = true;
|
||||
}
|
||||
|
||||
void TeamService::clearKeys()
|
||||
{
|
||||
keys_ = TeamKeys{};
|
||||
}
|
||||
|
||||
void TeamService::processIncoming()
|
||||
{
|
||||
chat::MeshIncomingData data;
|
||||
while (mesh_.pollIncomingData(&data))
|
||||
{
|
||||
if (data.portnum == team::proto::TEAM_MGMT_APP)
|
||||
{
|
||||
team::proto::TeamEncrypted envelope;
|
||||
std::vector<uint8_t> plain;
|
||||
bool decoded_encrypted =
|
||||
decodeEncryptedPayload(data, keys_.mgmt_key.data(), keys_.mgmt_key.size(),
|
||||
&envelope, plain, false);
|
||||
|
||||
uint8_t version = 0;
|
||||
team::proto::TeamMgmtType type = team::proto::TeamMgmtType::Advertise;
|
||||
std::vector<uint8_t> payload;
|
||||
|
||||
if (decoded_encrypted)
|
||||
{
|
||||
if (!team::proto::decodeTeamMgmtMessage(
|
||||
plain.data(), plain.size(),
|
||||
&version, &type, payload))
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecodeFail, &envelope);
|
||||
continue;
|
||||
}
|
||||
if (version != team::proto::kTeamMgmtVersion)
|
||||
{
|
||||
emitError(data, TeamProtocolError::UnknownVersion, &envelope);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!team::proto::decodeTeamMgmtMessage(
|
||||
data.payload.data(), data.payload.size(),
|
||||
&version, &type, payload))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (version != team::proto::kTeamMgmtVersion)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case team::proto::TeamMgmtType::Advertise:
|
||||
{
|
||||
team::proto::TeamAdvertise msg;
|
||||
if (!team::proto::decodeTeamAdvertise(payload.data(), payload.size(), &msg))
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecodeFail,
|
||||
decoded_encrypted ? &envelope : nullptr);
|
||||
break;
|
||||
}
|
||||
TeamAdvertiseEvent event{makeContext(data, msg.team_id), msg};
|
||||
sink_.onTeamAdvertise(event);
|
||||
break;
|
||||
}
|
||||
case team::proto::TeamMgmtType::JoinRequest:
|
||||
{
|
||||
team::proto::TeamJoinRequest msg;
|
||||
if (!team::proto::decodeTeamJoinRequest(payload.data(), payload.size(), &msg))
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecodeFail,
|
||||
decoded_encrypted ? &envelope : nullptr);
|
||||
break;
|
||||
}
|
||||
TeamJoinRequestEvent event{makeContext(data, msg.team_id), msg};
|
||||
sink_.onTeamJoinRequest(event);
|
||||
break;
|
||||
}
|
||||
case team::proto::TeamMgmtType::JoinAccept:
|
||||
{
|
||||
if (!decoded_encrypted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
team::proto::TeamJoinAccept msg;
|
||||
if (!team::proto::decodeTeamJoinAccept(payload.data(), payload.size(), &msg))
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecodeFail,
|
||||
decoded_encrypted ? &envelope : nullptr);
|
||||
break;
|
||||
}
|
||||
TeamJoinAcceptEvent event{makeContext(data, decoded_encrypted ? &envelope : nullptr), msg};
|
||||
sink_.onTeamJoinAccept(event);
|
||||
break;
|
||||
}
|
||||
case team::proto::TeamMgmtType::JoinConfirm:
|
||||
{
|
||||
if (!decoded_encrypted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
team::proto::TeamJoinConfirm msg;
|
||||
if (!team::proto::decodeTeamJoinConfirm(payload.data(), payload.size(), &msg))
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecodeFail,
|
||||
decoded_encrypted ? &envelope : nullptr);
|
||||
break;
|
||||
}
|
||||
TeamJoinConfirmEvent event{makeContext(data, decoded_encrypted ? &envelope : nullptr), msg};
|
||||
sink_.onTeamJoinConfirm(event);
|
||||
break;
|
||||
}
|
||||
case team::proto::TeamMgmtType::Status:
|
||||
{
|
||||
if (!decoded_encrypted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
team::proto::TeamStatus msg;
|
||||
if (!team::proto::decodeTeamStatus(payload.data(), payload.size(), &msg))
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecodeFail,
|
||||
decoded_encrypted ? &envelope : nullptr);
|
||||
break;
|
||||
}
|
||||
TeamStatusEvent event{makeContext(data, decoded_encrypted ? &envelope : nullptr), msg};
|
||||
sink_.onTeamStatus(event);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (data.portnum == team::proto::TEAM_POSITION_APP)
|
||||
{
|
||||
team::proto::TeamEncrypted envelope;
|
||||
std::vector<uint8_t> plain;
|
||||
if (!decodeEncryptedPayload(data, keys_.pos_key.data(), keys_.pos_key.size(),
|
||||
&envelope, plain, true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TeamPositionEvent event{makeContext(data, &envelope), plain};
|
||||
sink_.onTeamPosition(event);
|
||||
}
|
||||
else if (data.portnum == team::proto::TEAM_WAYPOINT_APP)
|
||||
{
|
||||
team::proto::TeamEncrypted envelope;
|
||||
std::vector<uint8_t> plain;
|
||||
if (!decodeEncryptedPayload(data, keys_.wp_key.data(), keys_.wp_key.size(),
|
||||
&envelope, plain, true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TeamWaypointEvent event{makeContext(data, &envelope), plain};
|
||||
sink_.onTeamWaypoint(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TeamService::sendAdvertise(const team::proto::TeamAdvertise& msg,
|
||||
chat::ChannelId channel)
|
||||
{
|
||||
std::vector<uint8_t> payload;
|
||||
if (!team::proto::encodeTeamAdvertise(msg, payload))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return sendMgmtPlain(team::proto::TeamMgmtType::Advertise, payload, channel, 0);
|
||||
}
|
||||
|
||||
bool TeamService::sendJoinRequest(const team::proto::TeamJoinRequest& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
std::vector<uint8_t> payload;
|
||||
if (!team::proto::encodeTeamJoinRequest(msg, payload))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return sendMgmtPlain(team::proto::TeamMgmtType::JoinRequest, payload, channel, dest);
|
||||
}
|
||||
|
||||
bool TeamService::sendJoinAccept(const team::proto::TeamJoinAccept& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
std::vector<uint8_t> payload;
|
||||
if (!team::proto::encodeTeamJoinAccept(msg, payload))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return sendMgmtEncrypted(team::proto::TeamMgmtType::JoinAccept, payload, channel, dest);
|
||||
}
|
||||
|
||||
bool TeamService::sendJoinConfirm(const team::proto::TeamJoinConfirm& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
std::vector<uint8_t> payload;
|
||||
if (!team::proto::encodeTeamJoinConfirm(msg, payload))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return sendMgmtEncrypted(team::proto::TeamMgmtType::JoinConfirm, payload, channel, dest);
|
||||
}
|
||||
|
||||
bool TeamService::sendStatus(const team::proto::TeamStatus& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
std::vector<uint8_t> payload;
|
||||
if (!team::proto::encodeTeamStatus(msg, payload))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return sendMgmtEncrypted(team::proto::TeamMgmtType::Status, payload, channel, dest);
|
||||
}
|
||||
|
||||
bool TeamService::sendPosition(const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel)
|
||||
{
|
||||
if (!keys_.valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
team::proto::TeamEncrypted envelope;
|
||||
std::vector<uint8_t> wire;
|
||||
if (!encodeEncryptedPayload(payload, keys_.pos_key.data(), keys_.pos_key.size(),
|
||||
&envelope, wire))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return mesh_.sendAppData(channel, team::proto::TEAM_POSITION_APP,
|
||||
wire.data(), wire.size(), 0, false);
|
||||
}
|
||||
|
||||
bool TeamService::sendWaypoint(const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel)
|
||||
{
|
||||
if (!keys_.valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
team::proto::TeamEncrypted envelope;
|
||||
std::vector<uint8_t> wire;
|
||||
if (!encodeEncryptedPayload(payload, keys_.wp_key.data(), keys_.wp_key.size(),
|
||||
&envelope, wire))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return mesh_.sendAppData(channel, team::proto::TEAM_WAYPOINT_APP,
|
||||
wire.data(), wire.size(), 0, false);
|
||||
}
|
||||
|
||||
bool TeamService::decodeEncryptedPayload(const chat::MeshIncomingData& data,
|
||||
const uint8_t* key, size_t key_len,
|
||||
team::proto::TeamEncrypted* envelope,
|
||||
std::vector<uint8_t>& out_plain,
|
||||
bool emit_errors)
|
||||
{
|
||||
if (!envelope)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!team::proto::decodeTeamEncrypted(data.payload.data(),
|
||||
data.payload.size(),
|
||||
envelope))
|
||||
{
|
||||
if (emit_errors)
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecodeFail, nullptr);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (envelope->version != team::proto::kTeamEnvelopeVersion)
|
||||
{
|
||||
if (emit_errors)
|
||||
{
|
||||
emitError(data, TeamProtocolError::UnknownVersion, envelope);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!keys_.valid ||
|
||||
envelope->team_id != keys_.team_id ||
|
||||
envelope->key_id != keys_.key_id)
|
||||
{
|
||||
if (emit_errors)
|
||||
{
|
||||
emitError(data, TeamProtocolError::KeyMismatch, envelope);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> aad = buildAad(*envelope);
|
||||
if (!crypto_.aeadDecrypt(key, key_len,
|
||||
envelope->nonce.data(), envelope->nonce.size(),
|
||||
aad.data(), aad.size(),
|
||||
envelope->ciphertext.data(), envelope->ciphertext.size(),
|
||||
out_plain))
|
||||
{
|
||||
if (emit_errors)
|
||||
{
|
||||
emitError(data, TeamProtocolError::DecryptFail, envelope);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TeamService::encodeEncryptedPayload(const std::vector<uint8_t>& plain,
|
||||
const uint8_t* key, size_t key_len,
|
||||
team::proto::TeamEncrypted* envelope,
|
||||
std::vector<uint8_t>& out_wire)
|
||||
{
|
||||
if (!envelope || !keys_.valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
envelope->version = team::proto::kTeamEnvelopeVersion;
|
||||
envelope->aad_flags = 0;
|
||||
envelope->team_id = keys_.team_id;
|
||||
envelope->key_id = keys_.key_id;
|
||||
fillNonce(envelope->nonce);
|
||||
|
||||
std::vector<uint8_t> aad = buildAad(*envelope);
|
||||
if (!crypto_.aeadEncrypt(key, key_len,
|
||||
envelope->nonce.data(), envelope->nonce.size(),
|
||||
aad.data(), aad.size(),
|
||||
plain.data(), plain.size(),
|
||||
envelope->ciphertext))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return team::proto::encodeTeamEncrypted(*envelope, out_wire);
|
||||
}
|
||||
|
||||
bool TeamService::sendMgmtPlain(const team::proto::TeamMgmtType type,
|
||||
const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
std::vector<uint8_t> wire;
|
||||
if (!team::proto::encodeTeamMgmtMessage(type, payload, wire))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return mesh_.sendAppData(channel, team::proto::TEAM_MGMT_APP,
|
||||
wire.data(), wire.size(), dest, false);
|
||||
}
|
||||
|
||||
bool TeamService::sendMgmtEncrypted(const team::proto::TeamMgmtType type,
|
||||
const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel, chat::NodeId dest)
|
||||
{
|
||||
if (!keys_.valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> mgmt_wire;
|
||||
if (!team::proto::encodeTeamMgmtMessage(type, payload, mgmt_wire))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
team::proto::TeamEncrypted envelope;
|
||||
std::vector<uint8_t> wire;
|
||||
if (!encodeEncryptedPayload(mgmt_wire, keys_.mgmt_key.data(), keys_.mgmt_key.size(),
|
||||
&envelope, wire))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return mesh_.sendAppData(channel, team::proto::TEAM_MGMT_APP,
|
||||
wire.data(), wire.size(), dest, false);
|
||||
}
|
||||
|
||||
void TeamService::emitError(const chat::MeshIncomingData& data,
|
||||
team::TeamProtocolError error,
|
||||
const team::proto::TeamEncrypted* envelope)
|
||||
{
|
||||
TeamErrorEvent event;
|
||||
event.ctx = makeContext(data, envelope);
|
||||
event.error = error;
|
||||
event.portnum = data.portnum;
|
||||
sink_.onTeamError(event);
|
||||
}
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../chat/ports/i_mesh_adapter.h"
|
||||
#include "../domain/team_types.h"
|
||||
#include "../ports/i_team_crypto.h"
|
||||
#include "../ports/i_team_event_sink.h"
|
||||
#include <vector>
|
||||
|
||||
namespace team
|
||||
{
|
||||
|
||||
class TeamService
|
||||
{
|
||||
public:
|
||||
TeamService(team::ITeamCrypto& crypto,
|
||||
chat::IMeshAdapter& mesh,
|
||||
team::ITeamEventSink& sink);
|
||||
|
||||
void setKeys(const TeamKeys& keys);
|
||||
void clearKeys();
|
||||
|
||||
void processIncoming();
|
||||
|
||||
bool sendAdvertise(const team::proto::TeamAdvertise& msg,
|
||||
chat::ChannelId channel);
|
||||
bool sendJoinRequest(const team::proto::TeamJoinRequest& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest = 0);
|
||||
bool sendJoinAccept(const team::proto::TeamJoinAccept& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest);
|
||||
bool sendJoinConfirm(const team::proto::TeamJoinConfirm& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest = 0);
|
||||
bool sendStatus(const team::proto::TeamStatus& msg,
|
||||
chat::ChannelId channel, chat::NodeId dest = 0);
|
||||
bool sendPosition(const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel);
|
||||
bool sendWaypoint(const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel);
|
||||
|
||||
private:
|
||||
team::ITeamCrypto& crypto_;
|
||||
chat::IMeshAdapter& mesh_;
|
||||
team::ITeamEventSink& sink_;
|
||||
TeamKeys keys_{};
|
||||
|
||||
bool decodeEncryptedPayload(const chat::MeshIncomingData& data,
|
||||
const uint8_t* key, size_t key_len,
|
||||
team::proto::TeamEncrypted* envelope,
|
||||
std::vector<uint8_t>& out_plain,
|
||||
bool emit_errors);
|
||||
|
||||
bool encodeEncryptedPayload(const std::vector<uint8_t>& plain,
|
||||
const uint8_t* key, size_t key_len,
|
||||
team::proto::TeamEncrypted* envelope,
|
||||
std::vector<uint8_t>& out_wire);
|
||||
|
||||
bool sendMgmtPlain(const team::proto::TeamMgmtType type,
|
||||
const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel, chat::NodeId dest);
|
||||
bool sendMgmtEncrypted(const team::proto::TeamMgmtType type,
|
||||
const std::vector<uint8_t>& payload,
|
||||
chat::ChannelId channel, chat::NodeId dest);
|
||||
|
||||
void emitError(const chat::MeshIncomingData& data,
|
||||
team::TeamProtocolError error,
|
||||
const team::proto::TeamEncrypted* envelope);
|
||||
};
|
||||
|
||||
} // namespace team
|
||||
@@ -0,0 +1,165 @@
|
||||
#ifdef __has_include
|
||||
#if __has_include("lvgl.h")
|
||||
#ifndef LV_LVGL_H_INCLUDE_SIMPLE
|
||||
#define LV_LVGL_H_INCLUDE_SIMPLE
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
|
||||
#include "lvgl.h"
|
||||
#else
|
||||
#include "lvgl/lvgl.h"
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef LV_ATTRIBUTE_MEM_ALIGN
|
||||
#define LV_ATTRIBUTE_MEM_ALIGN
|
||||
#endif
|
||||
|
||||
#ifndef LV_ATTRIBUTE_IMAGE_TEAM
|
||||
#define LV_ATTRIBUTE_IMAGE_TEAM
|
||||
#endif
|
||||
|
||||
const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMAGE_TEAM uint8_t team_map[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb1, 0x9c, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0x38, 0xef, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x76, 0xd6, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xf4, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x76, 0xd6, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xb1, 0x9c, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0x62, 0x0c, 0x63, 0x90, 0x94, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0x62, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x2b, 0x6b, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0x3b, 0xef, 0x10, 0x84, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x68, 0xac, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0x34, 0xad, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0xa4, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0x34, 0xad, 0x51, 0x8c, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0xa4, 0x49, 0xa4, 0x46, 0xd5, 0x66, 0xd5, 0xe5, 0xed, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xea, 0x8b, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0xcb, 0x83, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00,
|
||||
0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0xcb, 0x83, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63,
|
||||
0xec, 0x62, 0x0c, 0x63, 0x6d, 0x73, 0xb7, 0xde, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0xcb, 0x83, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x59, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0xec, 0x62,
|
||||
0x0c, 0x63, 0x0c, 0x63, 0x15, 0xc6, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0xcb, 0x83, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x73, 0xb5, 0x0c, 0x63, 0x0c, 0x63,
|
||||
0xec, 0x62, 0x0c, 0x63, 0x2f, 0x8c, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0xeb, 0x8b, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xe5, 0xed, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x6b, 0x73, 0x49, 0x9c, 0x07, 0xc5, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63,
|
||||
0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0xd1, 0x9c, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xef, 0x83, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x99, 0xde, 0x0c, 0x63, 0x0c, 0x63, 0xeb, 0x8b, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x86, 0xdd, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6b, 0x73, 0xa8, 0xb4, 0x86, 0xdd, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xa8, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xfa, 0xff, 0xfa, 0xff, 0xfa, 0xff, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62,
|
||||
0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x2f, 0x8c, 0xb7, 0xde, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0x4d, 0x6b, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x8e, 0x73, 0x0c, 0x63, 0xec, 0x8b, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x86, 0xdd, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x74, 0xb5, 0x99, 0xde, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6b, 0x73, 0x6b, 0x73, 0x49, 0x9c, 0x07, 0xc5, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x49, 0x9c, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xb7, 0xde, 0x2f, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00,
|
||||
0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x74, 0xb5, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x0c, 0x63, 0x0c, 0x63, 0x86, 0xdd, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x86, 0xdd, 0x0c, 0x63, 0x0c, 0x63, 0x8c, 0x73, 0x99, 0xde, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x6b, 0x73, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x86, 0xdd, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x74, 0xb5, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0x0c, 0x63, 0x0c, 0x63, 0xe5, 0xed, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x86, 0xdd, 0x0c, 0x63, 0x0c, 0x63, 0x8c, 0x73, 0x99, 0xde, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0xca, 0x8b, 0x07, 0xc5, 0xa8, 0xb4, 0x49, 0x9c, 0x07, 0xc5, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xe5, 0xed, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x74, 0xb5, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x8e, 0x73, 0x0c, 0x63, 0x4a, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0x07, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8d, 0x73, 0x99, 0xde, 0xdd, 0xff, 0x3b, 0xef, 0xf6, 0xc5, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0xca, 0x8b, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x6b, 0x73, 0x6b, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x8d, 0x73, 0x99, 0xde, 0xdd, 0xff, 0xdd, 0xff, 0x30, 0x8c, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x86, 0xdd, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xd2, 0x9c, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x99, 0xde, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x49, 0x9c, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x99, 0xde, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0xf6, 0xc5, 0x74, 0xb5, 0x99, 0xde, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x6b, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xd2, 0x9c, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x07, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x6d, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x44, 0xfe, 0x49, 0x9c, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xd2, 0x9c, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x99, 0xde, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0x44, 0xfe, 0xe5, 0xed, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xf6, 0xc5, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x49, 0x9c, 0x44, 0xfe, 0xca, 0x8b, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x8e, 0x73, 0x30, 0x8c, 0x0c, 0x63, 0x8e, 0x73, 0xd2, 0x9c, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x6b, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0x3b, 0xef, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0x62, 0x0c, 0x63, 0x4a, 0x73, 0x66, 0xb4, 0x66, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x74, 0xb5, 0x99, 0xde, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0xc5, 0xc4, 0xe1, 0xfd, 0xe1, 0xfd, 0x29, 0x9c, 0x0c, 0x63, 0x30, 0x8c, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x66, 0xb4, 0xe1, 0xfd, 0xe1, 0xfd, 0x08, 0x9c, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x74, 0xb5, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0xd2, 0x9c, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0x62, 0x0c, 0x63, 0x4b, 0x73, 0x08, 0x9c, 0x66, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xd2, 0x9c, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x4a, 0x73, 0x66, 0xb4, 0x66, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0xf6, 0xc5, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0xc5, 0xc4, 0xe1, 0xfd, 0xe1, 0xfd, 0x08, 0x9c, 0x0c, 0x63, 0x30, 0x8c, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x66, 0xb4, 0xe1, 0xfd, 0xe1, 0xfd, 0x08, 0x9c, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x74, 0xb5, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0x62, 0x0c, 0x63, 0x2b, 0x6b, 0x66, 0xb4, 0x67, 0xac, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x4a, 0x73, 0xc5, 0xc4, 0x66, 0xb4, 0x0c, 0x63, 0x0c, 0x63, 0xf6, 0xc5, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x74, 0xb5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x3b, 0xef, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x23, 0xdd, 0xe1, 0xfd, 0xe1, 0xfd, 0xca, 0x8b, 0x0c, 0x63, 0xd2, 0x9c, 0xdd, 0xff, 0xf6, 0xc5, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x30, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xdd, 0xff, 0xdd, 0xff, 0xdd, 0xff, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xc5, 0xc4, 0xe1, 0xfd, 0xe1, 0xfd, 0x6b, 0x73, 0x0c, 0x63, 0xd2, 0x9c, 0xdd, 0xff, 0xdd, 0xff, 0x30, 0x8c, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0x3b, 0xef, 0xdd, 0xff, 0xdd, 0xff, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xf6, 0xc5, 0xd2, 0x9c, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0x62, 0x0c, 0x63, 0x2b, 0x6b, 0x08, 0x9c, 0x4a, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0xf6, 0xc5, 0xdd, 0xff, 0xdd, 0xff, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xd2, 0x9c, 0x8e, 0x73, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x8e, 0x73, 0xd2, 0x9c, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x63, 0x0c, 0x63, 0xec, 0x62, 0x0c, 0x63, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x7f, 0x9f, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x9f, 0x9f, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xf3, 0xff, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xef, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xeb, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x20, 0x40, 0x60, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xf7, 0xff, 0xff, 0xdf, 0x9f, 0x60, 0x40, 0x20, 0x00, 0x20, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xe7, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xdb, 0xd7, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xcf, 0xcf, 0xcf, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00, 0x00,
|
||||
0x00, 0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x00,
|
||||
0x40, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x40,
|
||||
0xbf, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3,
|
||||
0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf,
|
||||
0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60,
|
||||
0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xeb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00,
|
||||
0x00, 0x20, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00,
|
||||
0x00, 0x00, 0x20, 0x7f, 0xff, 0xff, 0xdf, 0x7f, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0xdf, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x9f, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xef, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x9f, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xef, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x20, 0xbf, 0xff, 0xdf, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x7f, 0xbf, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x20, 0x9f, 0xbf, 0x9f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x7f, 0xbf, 0x7f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
const lv_image_dsc_t team_icon = {
|
||||
.header.magic = LV_IMAGE_HEADER_MAGIC,
|
||||
.header.cf = LV_COLOR_FORMAT_RGB565A8,
|
||||
.header.flags = 0,
|
||||
.header.w = 64,
|
||||
.header.h = 64,
|
||||
.header.stride = 128,
|
||||
.data_size = sizeof(team_map),
|
||||
.data = team_map,
|
||||
};
|
||||
@@ -13,6 +13,8 @@
|
||||
namespace chat::ui
|
||||
{
|
||||
|
||||
static constexpr size_t kMaxInputBytes = 233;
|
||||
|
||||
struct ChatComposeScreen::Impl
|
||||
{
|
||||
chat::ui::compose::layout::Spec spec;
|
||||
@@ -32,6 +34,12 @@ static void set_btn_label_white(lv_obj_t* btn)
|
||||
ChatComposeScreen::ChatComposeScreen(lv_obj_t* parent, chat::ConversationId conv)
|
||||
: conv_(conv)
|
||||
{
|
||||
lv_obj_t* active = lv_screen_active();
|
||||
if (!active) {
|
||||
Serial.printf("[ChatCompose] WARNING: lv_screen_active() is null\n");
|
||||
} else {
|
||||
Serial.printf("[ChatCompose] init: active=%p parent=%p\n", active, parent);
|
||||
}
|
||||
|
||||
impl_ = new Impl();
|
||||
|
||||
@@ -42,7 +50,7 @@ ChatComposeScreen::ChatComposeScreen(lv_obj_t* parent, chat::ConversationId conv
|
||||
|
||||
lv_textarea_set_placeholder_text(impl_->w.textarea, "");
|
||||
lv_textarea_set_one_line(impl_->w.textarea, false);
|
||||
lv_textarea_set_max_length(impl_->w.textarea, 220);
|
||||
lv_textarea_set_max_length(impl_->w.textarea, kMaxInputBytes);
|
||||
|
||||
lv_obj_add_event_cb(impl_->w.send_btn, on_action_click, LV_EVENT_CLICKED, this);
|
||||
lv_obj_add_event_cb(impl_->w.cancel_btn, on_action_click, LV_EVENT_CLICKED, this);
|
||||
@@ -55,6 +63,13 @@ ChatComposeScreen::ChatComposeScreen(lv_obj_t* parent, chat::ConversationId conv
|
||||
input::bind_textarea_events(impl_->w, this, on_key, on_text_changed);
|
||||
input::setup_default_group_focus(impl_->w);
|
||||
|
||||
if (impl_->w.container && !lv_obj_is_valid(impl_->w.container)) {
|
||||
Serial.printf("[ChatCompose] WARNING: container invalid\n");
|
||||
}
|
||||
if (impl_->w.textarea && !lv_obj_is_valid(impl_->w.textarea)) {
|
||||
Serial.printf("[ChatCompose] WARNING: textarea invalid\n");
|
||||
}
|
||||
|
||||
refresh_len();
|
||||
}
|
||||
|
||||
@@ -154,9 +169,10 @@ void ChatComposeScreen::refresh_len()
|
||||
|
||||
const char* text = lv_textarea_get_text(impl_->w.textarea);
|
||||
size_t len = text ? strlen(text) : 0;
|
||||
size_t remaining = (len < kMaxInputBytes) ? (kMaxInputBytes - len) : 0;
|
||||
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "Len: %u", static_cast<unsigned int>(len));
|
||||
snprintf(buf, sizeof(buf), "Remain: %u", static_cast<unsigned int>(remaining));
|
||||
lv_label_set_text(impl_->w.len_label, buf);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ void create(lv_obj_t* parent, const Spec& spec, Widgets& w) {
|
||||
lv_obj_clear_flag(spacer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
w.len_label = lv_label_create(w.action_bar);
|
||||
lv_label_set_text(w.len_label, "Len: 0");
|
||||
lv_label_set_text(w.len_label, "Remain: 233");
|
||||
}
|
||||
|
||||
} // namespace chat::ui::compose::layout
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
#include "chat_conversation_components.h"
|
||||
|
||||
#include "../ui_common.h"
|
||||
#include "../../../app/app_context.h"
|
||||
#include "chat_conversation_layout.h"
|
||||
#include "chat_conversation_styles.h"
|
||||
#include "chat_conversation_input.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <Arduino.h>
|
||||
#include <ctime>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -19,11 +20,86 @@ namespace ui {
|
||||
// Keep original constant meaning (bubble max width and 70% rule)
|
||||
namespace {
|
||||
constexpr lv_coord_t kBubbleMaxWidth = 322; // same as original
|
||||
constexpr uint32_t kSecondsPerDay = 24U * 60U * 60U;
|
||||
constexpr uint32_t kSecondsPerMonth = 30U * kSecondsPerDay;
|
||||
constexpr uint32_t kSecondsPerYear = 365U * kSecondsPerDay;
|
||||
constexpr uint32_t kMinValidEpochSeconds = 1577836800U; // 2020-01-01
|
||||
} // namespace
|
||||
|
||||
static bool is_valid_epoch_ts(uint32_t ts)
|
||||
{
|
||||
return ts >= kMinValidEpochSeconds;
|
||||
}
|
||||
|
||||
static void format_hms(char* out, size_t out_len, uint32_t seconds)
|
||||
{
|
||||
unsigned h = static_cast<unsigned>(seconds / 3600U);
|
||||
unsigned m = static_cast<unsigned>((seconds / 60U) % 60U);
|
||||
unsigned s = static_cast<unsigned>(seconds % 60U);
|
||||
snprintf(out, out_len, "%02u:%02u:%02u", h, m, s);
|
||||
}
|
||||
|
||||
static void format_message_time(char* out, size_t out_len, uint32_t ts)
|
||||
{
|
||||
if (!out || out_len == 0) return;
|
||||
if (ts == 0) {
|
||||
snprintf(out, out_len, "--");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now_epoch = static_cast<uint32_t>(time(nullptr));
|
||||
bool ts_is_epoch = is_valid_epoch_ts(ts);
|
||||
bool now_is_epoch = is_valid_epoch_ts(now_epoch);
|
||||
uint32_t now_secs = now_is_epoch ? now_epoch : static_cast<uint32_t>(millis() / 1000U);
|
||||
if (ts_is_epoch && !now_is_epoch) {
|
||||
ts_is_epoch = false;
|
||||
}
|
||||
if (now_secs < ts) {
|
||||
now_secs = ts;
|
||||
}
|
||||
uint32_t diff = now_secs - ts;
|
||||
|
||||
if (diff >= kSecondsPerYear) {
|
||||
uint32_t years = diff / kSecondsPerYear;
|
||||
if (years == 0) years = 1;
|
||||
snprintf(out, out_len, "%uy ago", static_cast<unsigned>(years));
|
||||
return;
|
||||
}
|
||||
if (diff >= kSecondsPerMonth) {
|
||||
uint32_t months = diff / kSecondsPerMonth;
|
||||
if (months == 0) months = 1;
|
||||
snprintf(out, out_len, "%um ago", static_cast<unsigned>(months));
|
||||
return;
|
||||
}
|
||||
if (diff >= kSecondsPerDay) {
|
||||
uint32_t days = diff / kSecondsPerDay;
|
||||
if (days == 0) days = 1;
|
||||
snprintf(out, out_len, "%ud ago", static_cast<unsigned>(days));
|
||||
return;
|
||||
}
|
||||
if (ts_is_epoch) {
|
||||
time_t t = ui_apply_timezone_offset(static_cast<time_t>(ts));
|
||||
struct tm* info = gmtime(&t);
|
||||
if (info) {
|
||||
strftime(out, out_len, "%H:%M:%S", info);
|
||||
} else {
|
||||
snprintf(out, out_len, "--");
|
||||
}
|
||||
return;
|
||||
}
|
||||
format_hms(out, out_len, ts % kSecondsPerDay);
|
||||
}
|
||||
|
||||
ChatConversationScreen::ChatConversationScreen(lv_obj_t* parent, chat::ConversationId conv)
|
||||
: conv_(conv)
|
||||
{
|
||||
lv_obj_t* active = lv_screen_active();
|
||||
if (!active) {
|
||||
Serial.printf("[ChatConversation] WARNING: lv_screen_active() is null\n");
|
||||
} else {
|
||||
Serial.printf("[ChatConversation] init: active=%p parent=%p\n", active, parent);
|
||||
}
|
||||
|
||||
// ----- Layout -----
|
||||
auto w = chat::ui::layout::create_conversation_base(parent);
|
||||
container_ = w.root;
|
||||
@@ -48,6 +124,13 @@ ChatConversationScreen::ChatConversationScreen(lv_obj_t* parent, chat::Conversat
|
||||
::ui::widgets::top_bar_set_right_text(top_bar_, "");
|
||||
::ui::widgets::top_bar_set_back_callback(top_bar_, handle_back, this);
|
||||
|
||||
if (container_ && !lv_obj_is_valid(container_)) {
|
||||
Serial.printf("[ChatConversation] WARNING: container invalid\n");
|
||||
}
|
||||
if (msg_list_ && !lv_obj_is_valid(msg_list_)) {
|
||||
Serial.printf("[ChatConversation] WARNING: msg_list invalid\n");
|
||||
}
|
||||
|
||||
// ----- Event (unchanged) -----
|
||||
lv_obj_add_event_cb(reply_btn_, action_event_cb, LV_EVENT_CLICKED, this);
|
||||
|
||||
@@ -124,7 +207,7 @@ void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg)
|
||||
MessageItem item;
|
||||
item.msg = msg;
|
||||
|
||||
// ----- Layout: row + bubble + text -----
|
||||
// ----- Layout: row + bubble + time + text + status -----
|
||||
item.container = chat::ui::layout::create_message_row(msg_list_);
|
||||
chat::ui::conversation::styles::apply_message_row(item.container);
|
||||
|
||||
@@ -147,10 +230,46 @@ void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg)
|
||||
chat::ui::conversation::styles::apply_bubble(bubble, is_self);
|
||||
chat::ui::layout::set_bubble_max_width(bubble, max_bubble_w);
|
||||
|
||||
item.time_label = chat::ui::layout::create_bubble_time(bubble);
|
||||
chat::ui::conversation::styles::apply_bubble_time(item.time_label);
|
||||
char time_buf[16];
|
||||
format_message_time(time_buf, sizeof(time_buf), msg.timestamp);
|
||||
if (conv_.peer == 0) {
|
||||
std::string sender;
|
||||
if (msg.from == 0) {
|
||||
sender = app::AppContext::getInstance().getConfig().short_name;
|
||||
} else {
|
||||
sender = app::AppContext::getInstance().getContactService().getContactName(msg.from);
|
||||
if (sender.empty()) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%04lX",
|
||||
static_cast<unsigned long>(msg.from & 0xFFFF));
|
||||
sender = buf;
|
||||
}
|
||||
}
|
||||
std::string line = sender + " " + time_buf;
|
||||
lv_label_set_text(item.time_label, line.c_str());
|
||||
} else {
|
||||
lv_label_set_text(item.time_label, time_buf);
|
||||
}
|
||||
|
||||
item.text_label = chat::ui::layout::create_bubble_text(bubble);
|
||||
lv_label_set_text(item.text_label, msg.text.c_str());
|
||||
chat::ui::conversation::styles::apply_bubble_text(item.text_label);
|
||||
|
||||
item.status_label = chat::ui::layout::create_bubble_status(bubble);
|
||||
chat::ui::conversation::styles::apply_bubble_status(item.status_label);
|
||||
if (msg.status == MessageStatus::Failed)
|
||||
{
|
||||
lv_label_set_text(item.status_label, "Failed");
|
||||
lv_obj_clear_flag(item.status_label, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
else
|
||||
{
|
||||
lv_label_set_text(item.status_label, "");
|
||||
lv_obj_add_flag(item.status_label, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// Align row based on sender (same behavior)
|
||||
chat::ui::layout::align_message_row(item.container, is_self);
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ public:
|
||||
void setActionCallback(void (*cb)(bool compose, void*), void* user_data);
|
||||
|
||||
lv_obj_t* getObj() const { return container_; }
|
||||
lv_obj_t* getMsgList() const { return msg_list_; }
|
||||
lv_obj_t* getReplyBtn() const { return reply_btn_; }
|
||||
lv_obj_t* getBackBtn() const { return top_bar_.back_btn; }
|
||||
|
||||
chat::ChannelId getChannel() const { return conv_.channel; }
|
||||
|
||||
|
||||
@@ -13,11 +13,67 @@ namespace chat::ui::conversation::input {
|
||||
|
||||
static ChatConversationScreen* s_screen = nullptr;
|
||||
|
||||
namespace {
|
||||
constexpr int kEncoderKeyRotateUp = 19;
|
||||
constexpr int kEncoderKeyRotateDown = 20;
|
||||
constexpr lv_coord_t kScrollStep = 24;
|
||||
}
|
||||
|
||||
static void on_msg_list_key(lv_event_t* e)
|
||||
{
|
||||
auto* screen = static_cast<ChatConversationScreen*>(lv_event_get_user_data(e));
|
||||
if (!screen) return;
|
||||
|
||||
uint32_t key = lv_event_get_key(e);
|
||||
|
||||
if (lv_group_t* g = lv_group_get_default()) {
|
||||
if (key == LV_KEY_ENTER) {
|
||||
lv_group_set_editing(g, !lv_group_get_editing(g));
|
||||
lv_event_stop_processing(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lv_group_get_editing(g)) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_coord_t delta = 0;
|
||||
if (key == LV_KEY_UP || key == kEncoderKeyRotateUp) {
|
||||
delta = -kScrollStep;
|
||||
} else if (key == LV_KEY_DOWN || key == kEncoderKeyRotateDown) {
|
||||
delta = kScrollStep;
|
||||
}
|
||||
if (delta != 0) {
|
||||
if (lv_obj_t* msg_list = screen->getMsgList()) {
|
||||
lv_obj_scroll_by(msg_list, 0, delta, LV_ANIM_OFF);
|
||||
lv_event_stop_processing(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void init(ChatConversationScreen* screen)
|
||||
{
|
||||
s_screen = screen;
|
||||
// v0: no behavior change. Future: rotary scroll / focus reply button etc.
|
||||
CHAT_CONV_INPUT_LOG("[ChatConversationInput] init (v0 no-op)\n");
|
||||
if (!s_screen) {
|
||||
CHAT_CONV_INPUT_LOG("[ChatConversationInput] init (no screen)\n");
|
||||
return;
|
||||
}
|
||||
if (lv_group_t* g = lv_group_get_default()) {
|
||||
if (lv_obj_t* msg_list = s_screen->getMsgList()) {
|
||||
lv_group_add_obj(g, msg_list);
|
||||
lv_group_focus_obj(msg_list);
|
||||
lv_group_set_editing(g, true);
|
||||
lv_obj_add_event_cb(msg_list, on_msg_list_key, LV_EVENT_KEY, s_screen);
|
||||
}
|
||||
if (lv_obj_t* reply_btn = s_screen->getReplyBtn()) {
|
||||
lv_group_add_obj(g, reply_btn);
|
||||
}
|
||||
if (lv_obj_t* back_btn = s_screen->getBackBtn()) {
|
||||
lv_group_add_obj(g, back_btn);
|
||||
}
|
||||
}
|
||||
CHAT_CONV_INPUT_LOG("[ChatConversationInput] init (group focus msg list)\n");
|
||||
}
|
||||
|
||||
void cleanup()
|
||||
|
||||
@@ -134,6 +134,20 @@ lv_obj_t* create_bubble_text(lv_obj_t* bubble_parent)
|
||||
return label;
|
||||
}
|
||||
|
||||
lv_obj_t* create_bubble_time(lv_obj_t* bubble_parent)
|
||||
{
|
||||
lv_obj_t* label = lv_label_create(bubble_parent);
|
||||
lv_obj_set_width(label, LV_SIZE_CONTENT);
|
||||
return label;
|
||||
}
|
||||
|
||||
lv_obj_t* create_bubble_status(lv_obj_t* bubble_parent)
|
||||
{
|
||||
lv_obj_t* label = lv_label_create(bubble_parent);
|
||||
lv_obj_set_width(label, LV_SIZE_CONTENT);
|
||||
return label;
|
||||
}
|
||||
|
||||
void align_message_row(lv_obj_t* row, bool is_self)
|
||||
{
|
||||
// Match original behavior:
|
||||
|
||||
@@ -32,6 +32,12 @@ lv_obj_t* create_bubble(lv_obj_t* row_parent);
|
||||
// Create bubble text label
|
||||
lv_obj_t* create_bubble_text(lv_obj_t* bubble_parent);
|
||||
|
||||
// Create bubble time label
|
||||
lv_obj_t* create_bubble_time(lv_obj_t* bubble_parent);
|
||||
|
||||
// Create bubble status label
|
||||
lv_obj_t* create_bubble_status(lv_obj_t* bubble_parent);
|
||||
|
||||
// Layout-only helpers to align message row left/right
|
||||
void align_message_row(lv_obj_t* row, bool is_self);
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ static lv_style_t s_bubble_base;
|
||||
static lv_style_t s_bubble_self;
|
||||
static lv_style_t s_bubble_other;
|
||||
static lv_style_t s_bubble_text;
|
||||
static lv_style_t s_bubble_time;
|
||||
static lv_style_t s_bubble_status;
|
||||
|
||||
static constexpr lv_coord_t kPadX = 8;
|
||||
static constexpr lv_coord_t kPadY = 6;
|
||||
@@ -110,6 +112,16 @@ void init_once()
|
||||
lv_style_set_text_color(&s_bubble_text, kTextColor);
|
||||
lv_style_set_text_align(&s_bubble_text, LV_TEXT_ALIGN_LEFT);
|
||||
lv_style_set_text_font(&s_bubble_text, &lv_font_noto_cjk_16_2bpp);
|
||||
|
||||
lv_style_init(&s_bubble_time);
|
||||
lv_style_set_text_color(&s_bubble_time, lv_color_hex(0x707070));
|
||||
lv_style_set_text_align(&s_bubble_time, LV_TEXT_ALIGN_LEFT);
|
||||
lv_style_set_text_font(&s_bubble_time, &lv_font_montserrat_12);
|
||||
|
||||
lv_style_init(&s_bubble_status);
|
||||
lv_style_set_text_color(&s_bubble_status, lv_color_hex(0xB00020));
|
||||
lv_style_set_text_align(&s_bubble_status, LV_TEXT_ALIGN_LEFT);
|
||||
lv_style_set_text_font(&s_bubble_status, &lv_font_montserrat_12);
|
||||
}
|
||||
|
||||
void apply_root(lv_obj_t* root)
|
||||
@@ -162,4 +174,16 @@ void apply_bubble_text(lv_obj_t* label)
|
||||
lv_obj_add_style(label, &s_bubble_text, 0);
|
||||
}
|
||||
|
||||
void apply_bubble_time(lv_obj_t* label)
|
||||
{
|
||||
init_once();
|
||||
lv_obj_add_style(label, &s_bubble_time, 0);
|
||||
}
|
||||
|
||||
void apply_bubble_status(lv_obj_t* label)
|
||||
{
|
||||
init_once();
|
||||
lv_obj_add_style(label, &s_bubble_status, 0);
|
||||
}
|
||||
|
||||
} // namespace chat::ui::conversation::styles
|
||||
|
||||
@@ -19,5 +19,7 @@ void apply_reply_label(lv_obj_t* label);
|
||||
void apply_message_row(lv_obj_t* row);
|
||||
void apply_bubble(lv_obj_t* bubble, bool is_self);
|
||||
void apply_bubble_text(lv_obj_t* label);
|
||||
void apply_bubble_time(lv_obj_t* label);
|
||||
void apply_bubble_status(lv_obj_t* label);
|
||||
|
||||
} // namespace chat::ui::conversation::styles
|
||||
|
||||
@@ -25,8 +25,8 @@ static void format_time_hhmm(char out[16], uint32_t ts)
|
||||
snprintf(out, 16, "--:--");
|
||||
return;
|
||||
}
|
||||
time_t t = static_cast<time_t>(ts);
|
||||
struct tm* info = localtime(&t);
|
||||
time_t t = ui_apply_timezone_offset(static_cast<time_t>(ts));
|
||||
struct tm* info = gmtime(&t);
|
||||
if (info) {
|
||||
strftime(out, 16, "%H:%M", info);
|
||||
} else {
|
||||
@@ -38,30 +38,73 @@ static void format_time_hhmm(char out[16], uint32_t ts)
|
||||
|
||||
ChatMessageListScreen::ChatMessageListScreen(lv_obj_t* parent)
|
||||
: container_(nullptr),
|
||||
panel_(nullptr),
|
||||
filter_panel_(nullptr),
|
||||
list_panel_(nullptr),
|
||||
direct_btn_(nullptr),
|
||||
broadcast_btn_(nullptr),
|
||||
list_back_btn_(nullptr),
|
||||
selected_index_(0),
|
||||
filter_mode_(FilterMode::Direct),
|
||||
select_cb_(nullptr),
|
||||
select_cb_user_data_(nullptr),
|
||||
back_cb_(nullptr),
|
||||
back_cb_user_data_(nullptr)
|
||||
{
|
||||
lv_obj_t* active = lv_screen_active();
|
||||
if (!active) {
|
||||
Serial.printf("[ChatMessageList] WARNING: lv_screen_active() is null\n");
|
||||
} else {
|
||||
Serial.printf("[ChatMessageList] init: active=%p parent=%p\n", active, parent);
|
||||
}
|
||||
|
||||
lv_group_t* prev_group = lv_group_get_default();
|
||||
set_default_group(nullptr);
|
||||
|
||||
// ---------- Layout ----------
|
||||
container_ = chat::ui::layout::create_root(parent);
|
||||
auto w = chat::ui::layout::create_layout(parent);
|
||||
container_ = w.root;
|
||||
filter_panel_ = w.filter_panel;
|
||||
list_panel_ = w.list_panel;
|
||||
direct_btn_ = w.direct_btn;
|
||||
broadcast_btn_ = w.broadcast_btn;
|
||||
|
||||
// ---------- Styles ----------
|
||||
chat::ui::message_list::styles::apply_root_container(container_);
|
||||
chat::ui::message_list::styles::apply_filter_panel(filter_panel_);
|
||||
chat::ui::message_list::styles::apply_panel(list_panel_);
|
||||
if (direct_btn_) chat::ui::message_list::styles::apply_filter_btn(direct_btn_);
|
||||
if (broadcast_btn_) chat::ui::message_list::styles::apply_filter_btn(broadcast_btn_);
|
||||
|
||||
// ---------- Top bar (existing widget, unchanged) ----------
|
||||
::ui::widgets::top_bar_init(top_bar_, container_);
|
||||
::ui::widgets::top_bar_set_title(top_bar_, "MESSAGES");
|
||||
::ui::widgets::top_bar_set_right_text(top_bar_, "--:-- --%");
|
||||
::ui::widgets::top_bar_set_back_callback(top_bar_, handle_back, this);
|
||||
if (top_bar_.container) {
|
||||
lv_obj_move_to_index(top_bar_.container, 0);
|
||||
}
|
||||
|
||||
// ---------- Content panel ----------
|
||||
panel_ = chat::ui::layout::create_panel(container_);
|
||||
chat::ui::message_list::styles::apply_panel(panel_);
|
||||
// ---------- Filter events ----------
|
||||
if (direct_btn_) {
|
||||
lv_obj_add_event_cb(direct_btn_, filter_focus_cb, LV_EVENT_FOCUSED, this);
|
||||
lv_obj_add_event_cb(direct_btn_, filter_click_cb, LV_EVENT_CLICKED, this);
|
||||
}
|
||||
if (broadcast_btn_) {
|
||||
lv_obj_add_event_cb(broadcast_btn_, filter_focus_cb, LV_EVENT_FOCUSED, this);
|
||||
lv_obj_add_event_cb(broadcast_btn_, filter_click_cb, LV_EVENT_CLICKED, this);
|
||||
}
|
||||
updateFilterHighlight();
|
||||
|
||||
// ---------- Input layer (explicit, no-op for now) ----------
|
||||
if (container_ && !lv_obj_is_valid(container_)) {
|
||||
Serial.printf("[ChatMessageList] WARNING: container invalid\n");
|
||||
}
|
||||
if (list_panel_ && !lv_obj_is_valid(list_panel_)) {
|
||||
Serial.printf("[ChatMessageList] WARNING: list_panel invalid\n");
|
||||
}
|
||||
|
||||
set_default_group(prev_group);
|
||||
|
||||
// ---------- Input layer ----------
|
||||
chat::ui::message_list::input::init(this);
|
||||
}
|
||||
|
||||
@@ -84,7 +127,8 @@ ChatMessageListScreen::~ChatMessageListScreen()
|
||||
|
||||
void ChatMessageListScreen::setConversations(const std::vector<chat::ConversationMeta>& convs)
|
||||
{
|
||||
rebuildList(convs);
|
||||
convs_ = convs;
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::setSelected(int index)
|
||||
@@ -92,7 +136,16 @@ void ChatMessageListScreen::setSelected(int index)
|
||||
if (index >= 0 && index < static_cast<int>(items_.size()) &&
|
||||
items_[index].btn != nullptr) {
|
||||
selected_index_ = index;
|
||||
lv_group_focus_obj(items_[index].btn);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::setSelectedConversation(const chat::ConversationId& conv)
|
||||
{
|
||||
for (size_t i = 0; i < items_.size(); ++i) {
|
||||
if (items_[i].conv == conv) {
|
||||
setSelected(static_cast<int>(i));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +158,14 @@ chat::ConversationId ChatMessageListScreen::getSelectedConversation() const
|
||||
return chat::ConversationId();
|
||||
}
|
||||
|
||||
lv_obj_t* ChatMessageListScreen::getItemButton(size_t index) const
|
||||
{
|
||||
if (index >= items_.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return items_[index].btn;
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::setChannelSelectCallback(
|
||||
void (*cb)(chat::ChannelId, void*),
|
||||
void* user_data)
|
||||
@@ -127,19 +188,30 @@ void ChatMessageListScreen::setBackCallback(void (*cb)(void*), void* user_data)
|
||||
// ------------------------------------------------
|
||||
// Core logic: rebuild list (behavior unchanged)
|
||||
// ------------------------------------------------
|
||||
void ChatMessageListScreen::rebuildList(const std::vector<chat::ConversationMeta>& convs)
|
||||
void ChatMessageListScreen::rebuildList()
|
||||
{
|
||||
// Same behavior: clear and rebuild
|
||||
lv_obj_clean(panel_);
|
||||
lv_obj_clean(list_panel_);
|
||||
items_.clear();
|
||||
list_back_btn_ = nullptr;
|
||||
|
||||
for (const auto& conv : convs) {
|
||||
std::vector<chat::ConversationMeta> filtered;
|
||||
filtered.reserve(convs_.size());
|
||||
for (const auto& conv : convs_) {
|
||||
if (filter_mode_ == FilterMode::Direct && conv.id.peer != 0) {
|
||||
filtered.push_back(conv);
|
||||
} else if (filter_mode_ == FilterMode::Broadcast && conv.id.peer == 0) {
|
||||
filtered.push_back(conv);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& conv : filtered) {
|
||||
MessageItem item{};
|
||||
item.conv = conv.id;
|
||||
item.unread_count = conv.unread;
|
||||
|
||||
// ----- Layout -----
|
||||
auto w = chat::ui::layout::create_message_item(panel_);
|
||||
auto w = chat::ui::layout::create_message_item(list_panel_);
|
||||
item.btn = w.btn;
|
||||
item.name_label = w.name_label;
|
||||
item.preview_label = w.preview_label;
|
||||
@@ -152,6 +224,7 @@ void ChatMessageListScreen::rebuildList(const std::vector<chat::ConversationMeta
|
||||
chat::ui::message_list::styles::apply_label_preview(item.preview_label);
|
||||
chat::ui::message_list::styles::apply_label_time(item.time_label);
|
||||
chat::ui::message_list::styles::apply_label_unread(item.unread_label);
|
||||
lv_obj_clear_state(item.btn, (lv_state_t)(LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY));
|
||||
|
||||
// ----- Content -----
|
||||
lv_label_set_text(item.name_label, conv.name.c_str());
|
||||
@@ -176,13 +249,28 @@ void ChatMessageListScreen::rebuildList(const std::vector<chat::ConversationMeta
|
||||
}
|
||||
|
||||
if (items_.empty()) {
|
||||
lv_obj_t* placeholder = chat::ui::layout::create_placeholder(panel_);
|
||||
lv_obj_t* placeholder = chat::ui::layout::create_placeholder(list_panel_);
|
||||
chat::ui::message_list::styles::apply_label_placeholder(placeholder);
|
||||
lv_label_set_text(placeholder, "No messages");
|
||||
selected_index_ = -1;
|
||||
} else {
|
||||
}
|
||||
|
||||
// Append back button as the last item in list panel.
|
||||
list_back_btn_ = lv_btn_create(list_panel_);
|
||||
lv_obj_set_size(list_back_btn_, LV_PCT(100), 36);
|
||||
lv_obj_clear_flag(list_back_btn_, LV_OBJ_FLAG_SCROLLABLE);
|
||||
chat::ui::message_list::styles::apply_item_btn(list_back_btn_);
|
||||
lv_obj_clear_state(list_back_btn_, (lv_state_t)(LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY));
|
||||
lv_obj_t* back_label = lv_label_create(list_back_btn_);
|
||||
lv_label_set_text(back_label, "Back");
|
||||
lv_obj_center(back_label);
|
||||
lv_obj_add_event_cb(list_back_btn_, list_back_event_cb, LV_EVENT_CLICKED, this);
|
||||
|
||||
if (!items_.empty()) {
|
||||
setSelected(0);
|
||||
}
|
||||
|
||||
chat::ui::message_list::input::on_ui_refreshed();
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::item_event_cb(lv_event_t* e)
|
||||
@@ -207,6 +295,72 @@ void ChatMessageListScreen::item_event_cb(lv_event_t* e)
|
||||
}
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::list_back_event_cb(lv_event_t* e)
|
||||
{
|
||||
auto* screen =
|
||||
static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
|
||||
if (!screen) {
|
||||
return;
|
||||
}
|
||||
chat::ui::message_list::input::focus_filter();
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::filter_focus_cb(lv_event_t* e)
|
||||
{
|
||||
auto* screen =
|
||||
static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
|
||||
if (!screen) {
|
||||
return;
|
||||
}
|
||||
lv_obj_t* tgt = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
||||
if (tgt == screen->direct_btn_) {
|
||||
screen->setFilterMode(FilterMode::Direct);
|
||||
} else if (tgt == screen->broadcast_btn_) {
|
||||
screen->setFilterMode(FilterMode::Broadcast);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::filter_click_cb(lv_event_t* e)
|
||||
{
|
||||
auto* screen =
|
||||
static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
|
||||
if (!screen) {
|
||||
return;
|
||||
}
|
||||
lv_obj_t* tgt = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
||||
if (tgt == screen->direct_btn_) {
|
||||
screen->setFilterMode(FilterMode::Direct);
|
||||
} else if (tgt == screen->broadcast_btn_) {
|
||||
screen->setFilterMode(FilterMode::Broadcast);
|
||||
}
|
||||
chat::ui::message_list::input::focus_list();
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::updateFilterHighlight()
|
||||
{
|
||||
if (!direct_btn_ || !broadcast_btn_) {
|
||||
return;
|
||||
}
|
||||
lv_obj_clear_state(direct_btn_, LV_STATE_CHECKED);
|
||||
lv_obj_clear_state(broadcast_btn_, LV_STATE_CHECKED);
|
||||
if (filter_mode_ == FilterMode::Direct) {
|
||||
lv_obj_add_state(direct_btn_, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_add_state(broadcast_btn_, LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::setFilterMode(FilterMode mode)
|
||||
{
|
||||
if (filter_mode_ == mode) {
|
||||
return;
|
||||
}
|
||||
filter_mode_ = mode;
|
||||
selected_index_ = -1;
|
||||
updateFilterHighlight();
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
void ChatMessageListScreen::handle_back(void* user_data)
|
||||
{
|
||||
auto* screen = static_cast<ChatMessageListScreen*>(user_data);
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
|
||||
// Set selected item index
|
||||
void setSelected(int index);
|
||||
void setSelectedConversation(const chat::ConversationId& conv);
|
||||
|
||||
// Get selected conversation
|
||||
chat::ConversationId getSelectedConversation() const;
|
||||
@@ -39,13 +40,30 @@ public:
|
||||
|
||||
// Get LVGL root object
|
||||
lv_obj_t* getObj() const { return container_; }
|
||||
lv_obj_t* getDirectButton() const { return direct_btn_; }
|
||||
lv_obj_t* getBroadcastButton() const { return broadcast_btn_; }
|
||||
lv_obj_t* getBackButton() const { return top_bar_.back_btn; }
|
||||
lv_obj_t* getListBackButton() const { return list_back_btn_; }
|
||||
size_t getItemCount() const { return items_.size(); }
|
||||
lv_obj_t* getItemButton(size_t index) const;
|
||||
int getSelectedIndex() const { return selected_index_; }
|
||||
|
||||
private:
|
||||
enum class FilterMode {
|
||||
Direct,
|
||||
Broadcast
|
||||
};
|
||||
|
||||
lv_obj_t* container_ = nullptr;
|
||||
::ui::widgets::TopBar top_bar_{};
|
||||
lv_obj_t* panel_ = nullptr;
|
||||
lv_obj_t* filter_panel_ = nullptr;
|
||||
lv_obj_t* list_panel_ = nullptr;
|
||||
lv_obj_t* direct_btn_ = nullptr;
|
||||
lv_obj_t* broadcast_btn_ = nullptr;
|
||||
lv_obj_t* list_back_btn_ = nullptr;
|
||||
|
||||
int selected_index_ = 0;
|
||||
FilterMode filter_mode_ = FilterMode::Direct;
|
||||
|
||||
void (*select_cb_)(chat::ChannelId, void*) = nullptr;
|
||||
void* select_cb_user_data_ = nullptr;
|
||||
@@ -64,10 +82,16 @@ private:
|
||||
};
|
||||
|
||||
std::vector<MessageItem> items_;
|
||||
std::vector<chat::ConversationMeta> convs_;
|
||||
|
||||
void rebuildList(const std::vector<chat::ConversationMeta>& convs);
|
||||
void rebuildList();
|
||||
void updateFilterHighlight();
|
||||
void setFilterMode(FilterMode mode);
|
||||
|
||||
static void item_event_cb(lv_event_t* e);
|
||||
static void list_back_event_cb(lv_event_t* e);
|
||||
static void filter_focus_cb(lv_event_t* e);
|
||||
static void filter_click_cb(lv_event_t* e);
|
||||
static void handle_back(void* user_data);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,20 +2,17 @@
|
||||
* @file chat_message_list_input.cpp
|
||||
* @brief Input handling for ChatMessageListScreen (explicit layer)
|
||||
*
|
||||
* Current policy (v0):
|
||||
* - Keep behavior identical to the original implementation.
|
||||
* - No event interception or focus changes.
|
||||
*
|
||||
* Future extensions:
|
||||
* - Rotary: up/down to change selected item
|
||||
* - Left/right: optional column switching if more focus zones appear
|
||||
* - Press: trigger current item / open conversation
|
||||
* - Long press: open context menu
|
||||
* Current policy:
|
||||
* - Rotary focuses filter buttons (Direct/Broadcast) or list items.
|
||||
* - Press in filter column moves to list (or back button exits).
|
||||
* - Press in list column returns to filter when Back is focused.
|
||||
* - Back/ESC returns to filter column.
|
||||
*/
|
||||
|
||||
#include <Arduino.h> // keep Arduino first if your build requires it
|
||||
#include "chat_message_list_input.h"
|
||||
#include "chat_message_list_components.h"
|
||||
#include "../ui_common.h"
|
||||
|
||||
#define CHAT_INPUT_DEBUG 1
|
||||
#if CHAT_INPUT_DEBUG
|
||||
@@ -26,28 +23,234 @@
|
||||
|
||||
namespace chat::ui::message_list::input {
|
||||
|
||||
namespace {
|
||||
|
||||
enum class FocusColumn {
|
||||
Filter = 0,
|
||||
List = 1
|
||||
};
|
||||
|
||||
static ChatMessageListScreen* s_screen = nullptr;
|
||||
static lv_group_t* s_group = nullptr;
|
||||
static lv_group_t* s_prev_group = nullptr;
|
||||
static FocusColumn s_col = FocusColumn::Filter;
|
||||
|
||||
static bool is_encoder_active()
|
||||
{
|
||||
lv_indev_t* indev = lv_indev_get_act();
|
||||
return indev && lv_indev_get_type(indev) == LV_INDEV_TYPE_ENCODER;
|
||||
}
|
||||
|
||||
static void group_clear_all(lv_group_t* g)
|
||||
{
|
||||
if (!g) return;
|
||||
lv_group_remove_all_objs(g);
|
||||
}
|
||||
|
||||
static void focus_first_valid(lv_obj_t* obj)
|
||||
{
|
||||
if (!s_group || !obj || !lv_obj_is_valid(obj)) return;
|
||||
lv_group_focus_obj(obj);
|
||||
}
|
||||
|
||||
static void group_add_if_valid(lv_obj_t* obj)
|
||||
{
|
||||
if (!s_group || !obj || !lv_obj_is_valid(obj)) return;
|
||||
lv_group_add_obj(s_group, obj);
|
||||
}
|
||||
|
||||
static void clear_list_focus_states()
|
||||
{
|
||||
if (!s_screen) return;
|
||||
lv_state_t clear_mask = (lv_state_t)(LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
|
||||
size_t count = s_screen->getItemCount();
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (lv_obj_t* btn = s_screen->getItemButton(i)) {
|
||||
if (lv_obj_is_valid(btn)) {
|
||||
lv_obj_clear_state(btn, clear_mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lv_obj_t* back = s_screen->getListBackButton()) {
|
||||
if (lv_obj_is_valid(back)) {
|
||||
lv_obj_clear_state(back, clear_mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void bind_filter_column()
|
||||
{
|
||||
if (!s_group || !s_screen) return;
|
||||
lv_group_focus_freeze(s_group, true);
|
||||
group_clear_all(s_group);
|
||||
clear_list_focus_states();
|
||||
|
||||
if (lv_obj_t* back = s_screen->getBackButton()) {
|
||||
group_add_if_valid(back);
|
||||
}
|
||||
if (lv_obj_t* direct = s_screen->getDirectButton()) {
|
||||
group_add_if_valid(direct);
|
||||
}
|
||||
if (lv_obj_t* broadcast = s_screen->getBroadcastButton()) {
|
||||
group_add_if_valid(broadcast);
|
||||
}
|
||||
|
||||
lv_group_focus_freeze(s_group, false);
|
||||
|
||||
if (lv_obj_t* direct = s_screen->getDirectButton()) {
|
||||
if (lv_obj_has_state(direct, LV_STATE_CHECKED)) {
|
||||
focus_first_valid(direct);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (lv_obj_t* broadcast = s_screen->getBroadcastButton()) {
|
||||
if (lv_obj_has_state(broadcast, LV_STATE_CHECKED)) {
|
||||
focus_first_valid(broadcast);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (lv_obj_t* direct = s_screen->getDirectButton()) {
|
||||
focus_first_valid(direct);
|
||||
} else if (lv_obj_t* broadcast = s_screen->getBroadcastButton()) {
|
||||
focus_first_valid(broadcast);
|
||||
} else if (lv_obj_t* back = s_screen->getBackButton()) {
|
||||
focus_first_valid(back);
|
||||
}
|
||||
}
|
||||
|
||||
static void bind_list_column()
|
||||
{
|
||||
if (!s_group || !s_screen) return;
|
||||
lv_group_focus_freeze(s_group, true);
|
||||
group_clear_all(s_group);
|
||||
|
||||
size_t count = s_screen->getItemCount();
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (lv_obj_t* btn = s_screen->getItemButton(i)) {
|
||||
group_add_if_valid(btn);
|
||||
}
|
||||
}
|
||||
if (lv_obj_t* back = s_screen->getListBackButton()) {
|
||||
group_add_if_valid(back);
|
||||
}
|
||||
|
||||
lv_group_focus_freeze(s_group, false);
|
||||
|
||||
if (count > 0) {
|
||||
int selected = s_screen->getSelectedIndex();
|
||||
if (selected >= 0 && static_cast<size_t>(selected) < count) {
|
||||
focus_first_valid(s_screen->getItemButton(static_cast<size_t>(selected)));
|
||||
} else {
|
||||
focus_first_valid(s_screen->getItemButton(0));
|
||||
}
|
||||
} else if (lv_obj_t* back = s_screen->getListBackButton()) {
|
||||
focus_first_valid(back);
|
||||
} else {
|
||||
s_col = FocusColumn::Filter;
|
||||
bind_filter_column();
|
||||
}
|
||||
}
|
||||
|
||||
static void rebind_by_column()
|
||||
{
|
||||
if (s_col == FocusColumn::Filter) {
|
||||
bind_filter_column();
|
||||
} else {
|
||||
bind_list_column();
|
||||
}
|
||||
}
|
||||
|
||||
static void root_key_event_cb(lv_event_t* e)
|
||||
{
|
||||
if (!is_encoder_active()) return;
|
||||
|
||||
uint32_t key = lv_event_get_key(e);
|
||||
if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE) {
|
||||
s_col = FocusColumn::Filter;
|
||||
rebind_by_column();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key != LV_KEY_ENTER) return;
|
||||
|
||||
lv_obj_t* focused = s_group ? lv_group_get_focused(s_group) : nullptr;
|
||||
if (!focused || !s_screen) return;
|
||||
|
||||
if (s_col == FocusColumn::Filter) {
|
||||
if (focused == s_screen->getBackButton()) {
|
||||
lv_obj_send_event(focused, LV_EVENT_CLICKED, nullptr);
|
||||
return;
|
||||
}
|
||||
s_col = FocusColumn::List;
|
||||
rebind_by_column();
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_col == FocusColumn::List) {
|
||||
if (focused == s_screen->getListBackButton()) {
|
||||
s_col = FocusColumn::Filter;
|
||||
rebind_by_column();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void init(ChatMessageListScreen* screen)
|
||||
{
|
||||
s_screen = screen;
|
||||
if (s_group) {
|
||||
cleanup();
|
||||
}
|
||||
s_group = lv_group_create();
|
||||
s_prev_group = lv_group_get_default();
|
||||
set_default_group(nullptr);
|
||||
s_col = FocusColumn::Filter;
|
||||
rebind_by_column();
|
||||
set_default_group(s_group);
|
||||
|
||||
// v0: No behavior change.
|
||||
// We do not attach LVGL event callbacks here yet,
|
||||
// because the original screen relies on:
|
||||
// - LV_EVENT_CLICKED on list items
|
||||
// - TopBar internal back callback
|
||||
//
|
||||
// This module is the explicit "input ownership" point for future work.
|
||||
if (s_screen && s_screen->getObj()) {
|
||||
lv_obj_add_event_cb(s_screen->getObj(), root_key_event_cb, LV_EVENT_KEY, nullptr);
|
||||
}
|
||||
|
||||
CHAT_INPUT_LOG("[ChatMessageListInput] init (v0 no-op)\n");
|
||||
CHAT_INPUT_LOG("[ChatMessageListInput] init\n");
|
||||
}
|
||||
|
||||
void cleanup()
|
||||
{
|
||||
// v0: No behavior change.
|
||||
s_screen = nullptr;
|
||||
if (s_group) {
|
||||
set_default_group(nullptr);
|
||||
lv_group_del(s_group);
|
||||
s_group = nullptr;
|
||||
}
|
||||
if (s_prev_group) {
|
||||
set_default_group(s_prev_group);
|
||||
}
|
||||
s_prev_group = nullptr;
|
||||
CHAT_INPUT_LOG("[ChatMessageListInput] cleanup\n");
|
||||
}
|
||||
|
||||
void on_ui_refreshed()
|
||||
{
|
||||
if (!s_group) return;
|
||||
rebind_by_column();
|
||||
}
|
||||
|
||||
void focus_filter()
|
||||
{
|
||||
if (!s_group) return;
|
||||
s_col = FocusColumn::Filter;
|
||||
rebind_by_column();
|
||||
}
|
||||
|
||||
void focus_list()
|
||||
{
|
||||
if (!s_group) return;
|
||||
s_col = FocusColumn::List;
|
||||
rebind_by_column();
|
||||
}
|
||||
|
||||
} // namespace chat::ui::input
|
||||
|
||||
@@ -33,5 +33,12 @@ void init(ChatMessageListScreen* screen);
|
||||
*/
|
||||
void cleanup();
|
||||
|
||||
/**
|
||||
* @brief Rebind focus after UI rebuild
|
||||
*/
|
||||
void on_ui_refreshed();
|
||||
void focus_filter();
|
||||
void focus_list();
|
||||
|
||||
} // namespace message_list::input
|
||||
} // namespace chat::ui
|
||||
|
||||
@@ -66,27 +66,79 @@ static void make_non_scrollable(lv_obj_t* obj)
|
||||
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_OFF);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr int kFilterPanelWidth = 80;
|
||||
constexpr int kButtonHeight = 32;
|
||||
constexpr int kPanelGap = 0;
|
||||
constexpr int kScreenEdgePadding = 0;
|
||||
} // namespace
|
||||
|
||||
lv_obj_t* create_root(lv_obj_t* parent)
|
||||
{
|
||||
lv_obj_t* root = lv_obj_create(parent);
|
||||
lv_obj_set_size(root, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(root, 0, 0);
|
||||
lv_obj_set_style_pad_row(root, 3, 0);
|
||||
lv_obj_set_style_pad_all(root, 0, 0); // layout padding only
|
||||
make_non_scrollable(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
lv_obj_t* create_panel(lv_obj_t* parent)
|
||||
static lv_obj_t* create_content(lv_obj_t* parent)
|
||||
{
|
||||
lv_obj_t* content = lv_obj_create(parent);
|
||||
lv_obj_set_size(content, LV_PCT(100), 0);
|
||||
lv_obj_set_flex_grow(content, 1);
|
||||
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(content, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_style_pad_left(content, kScreenEdgePadding, 0);
|
||||
lv_obj_set_style_pad_right(content, kScreenEdgePadding, 0);
|
||||
lv_obj_set_style_pad_top(content, 0, 0);
|
||||
lv_obj_set_style_pad_bottom(content, 0, 0);
|
||||
make_non_scrollable(content);
|
||||
return content;
|
||||
}
|
||||
|
||||
static lv_obj_t* create_filter_panel(lv_obj_t* parent, lv_obj_t** direct_btn, lv_obj_t** broadcast_btn)
|
||||
{
|
||||
lv_obj_t* panel = lv_obj_create(parent);
|
||||
lv_obj_set_size(panel, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_width(panel, kFilterPanelWidth);
|
||||
lv_obj_set_height(panel, LV_PCT(100));
|
||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(panel, 3, LV_PART_MAIN);
|
||||
lv_obj_set_style_margin_right(panel, kPanelGap, LV_PART_MAIN);
|
||||
make_non_scrollable(panel);
|
||||
|
||||
lv_obj_t* direct = lv_btn_create(panel);
|
||||
lv_obj_set_size(direct, LV_PCT(100), kButtonHeight);
|
||||
make_non_scrollable(direct);
|
||||
lv_obj_t* direct_label = lv_label_create(direct);
|
||||
lv_label_set_text(direct_label, "Direct");
|
||||
lv_obj_center(direct_label);
|
||||
|
||||
lv_obj_t* broadcast = lv_btn_create(panel);
|
||||
lv_obj_set_size(broadcast, LV_PCT(100), kButtonHeight);
|
||||
make_non_scrollable(broadcast);
|
||||
lv_obj_t* broadcast_label = lv_label_create(broadcast);
|
||||
lv_label_set_text(broadcast_label, "Broadcast");
|
||||
lv_obj_center(broadcast_label);
|
||||
|
||||
if (direct_btn) *direct_btn = direct;
|
||||
if (broadcast_btn) *broadcast_btn = broadcast;
|
||||
return panel;
|
||||
}
|
||||
|
||||
static lv_obj_t* create_list_panel(lv_obj_t* parent)
|
||||
{
|
||||
lv_obj_t* panel = lv_obj_create(parent);
|
||||
lv_obj_set_height(panel, LV_PCT(100));
|
||||
lv_obj_set_width(panel, 0);
|
||||
lv_obj_set_flex_grow(panel, 1);
|
||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
|
||||
// layout spacing only
|
||||
lv_obj_set_style_pad_row(panel, 2, 0);
|
||||
lv_obj_set_style_pad_row(panel, 3, 0);
|
||||
lv_obj_set_style_pad_left(panel, 5, 0);
|
||||
lv_obj_set_style_pad_right(panel, 5, 0);
|
||||
lv_obj_set_style_pad_all(panel, 3, 0);
|
||||
@@ -95,6 +147,16 @@ lv_obj_t* create_panel(lv_obj_t* parent)
|
||||
return panel;
|
||||
}
|
||||
|
||||
MessageListLayout create_layout(lv_obj_t* parent)
|
||||
{
|
||||
MessageListLayout w{};
|
||||
w.root = create_root(parent);
|
||||
w.content = create_content(w.root);
|
||||
w.filter_panel = create_filter_panel(w.content, &w.direct_btn, &w.broadcast_btn);
|
||||
w.list_panel = create_list_panel(w.content);
|
||||
return w;
|
||||
}
|
||||
|
||||
MessageItemWidgets create_message_item(lv_obj_t* parent)
|
||||
{
|
||||
MessageItemWidgets w{};
|
||||
|
||||
@@ -4,11 +4,20 @@
|
||||
|
||||
namespace chat::ui::layout {
|
||||
|
||||
struct MessageListLayout {
|
||||
lv_obj_t* root;
|
||||
lv_obj_t* content;
|
||||
lv_obj_t* filter_panel;
|
||||
lv_obj_t* list_panel;
|
||||
lv_obj_t* direct_btn;
|
||||
lv_obj_t* broadcast_btn;
|
||||
};
|
||||
|
||||
// root/page
|
||||
lv_obj_t* create_root(lv_obj_t* parent);
|
||||
|
||||
// content panel
|
||||
lv_obj_t* create_panel(lv_obj_t* parent);
|
||||
// split content (filter + list)
|
||||
MessageListLayout create_layout(lv_obj_t* parent);
|
||||
|
||||
// message list item (button with labels)
|
||||
struct MessageItemWidgets {
|
||||
|
||||
@@ -3,13 +3,23 @@
|
||||
|
||||
namespace chat::ui::message_list::styles {
|
||||
|
||||
static constexpr uint32_t kPrimary = 0xEBA341;
|
||||
static constexpr uint32_t kPrimaryLight = 0xF1B65A;
|
||||
static constexpr uint32_t kCardBg = 0xF4C77A;
|
||||
static constexpr uint32_t kSoftBg = 0xF7DCA8;
|
||||
static constexpr uint32_t kTextMain = 0x202020;
|
||||
static constexpr uint32_t kTextMuted = 0x606060;
|
||||
|
||||
static bool inited = false;
|
||||
|
||||
static lv_style_t s_root;
|
||||
static lv_style_t s_panel;
|
||||
static lv_style_t s_filter_panel;
|
||||
|
||||
static lv_style_t s_item_btn;
|
||||
static lv_style_t s_item_btn_focused;
|
||||
static lv_style_t s_filter_btn;
|
||||
static lv_style_t s_filter_btn_checked;
|
||||
|
||||
static lv_style_t s_label_name;
|
||||
static lv_style_t s_label_preview;
|
||||
@@ -32,43 +42,65 @@ void init_once()
|
||||
|
||||
// Panel background
|
||||
lv_style_init(&s_panel);
|
||||
lv_style_set_bg_color(&s_panel, lv_color_hex(0xF5F5F5));
|
||||
lv_style_set_bg_color(&s_panel, lv_color_hex(kSoftBg));
|
||||
lv_style_set_bg_opa(&s_panel, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&s_panel, 0);
|
||||
lv_style_set_pad_all(&s_panel, 3);
|
||||
lv_style_set_radius(&s_panel, 0);
|
||||
|
||||
// Filter panel background
|
||||
lv_style_init(&s_filter_panel);
|
||||
lv_style_set_bg_color(&s_filter_panel, lv_color_hex(kSoftBg));
|
||||
lv_style_set_bg_opa(&s_filter_panel, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&s_filter_panel, 0);
|
||||
lv_style_set_pad_all(&s_filter_panel, 2);
|
||||
lv_style_set_radius(&s_filter_panel, 0);
|
||||
|
||||
// Item button
|
||||
lv_style_init(&s_item_btn);
|
||||
lv_style_set_bg_color(&s_item_btn, lv_color_hex(0xF4C77A));
|
||||
lv_style_set_bg_color(&s_item_btn, lv_color_hex(kCardBg));
|
||||
lv_style_set_bg_opa(&s_item_btn, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&s_item_btn, 1);
|
||||
lv_style_set_border_color(&s_item_btn, lv_color_hex(0xEBA341));
|
||||
lv_style_set_border_color(&s_item_btn, lv_color_hex(kPrimary));
|
||||
lv_style_set_radius(&s_item_btn, 6);
|
||||
|
||||
lv_style_init(&s_item_btn_focused);
|
||||
lv_style_set_bg_color(&s_item_btn_focused, lv_color_hex(0xF1B65A));
|
||||
lv_style_set_outline_width(&s_item_btn_focused, 0);
|
||||
lv_style_set_bg_opa(&s_item_btn_focused, LV_OPA_COVER);
|
||||
lv_style_set_bg_color(&s_item_btn_focused, lv_color_hex(kPrimaryLight));
|
||||
lv_style_set_outline_width(&s_item_btn_focused, 2);
|
||||
lv_style_set_outline_color(&s_item_btn_focused, lv_color_hex(kPrimary));
|
||||
|
||||
// Filter button
|
||||
lv_style_init(&s_filter_btn);
|
||||
lv_style_set_bg_color(&s_filter_btn, lv_color_hex(kCardBg));
|
||||
lv_style_set_bg_opa(&s_filter_btn, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&s_filter_btn, 1);
|
||||
lv_style_set_border_color(&s_filter_btn, lv_color_hex(kPrimary));
|
||||
lv_style_set_radius(&s_filter_btn, 6);
|
||||
|
||||
lv_style_init(&s_filter_btn_checked);
|
||||
lv_style_set_bg_opa(&s_filter_btn_checked, LV_OPA_COVER);
|
||||
lv_style_set_bg_color(&s_filter_btn_checked, lv_color_hex(kPrimary));
|
||||
|
||||
// Labels
|
||||
lv_style_init(&s_label_name);
|
||||
lv_style_set_text_color(&s_label_name, lv_color_hex(0x202020));
|
||||
lv_style_set_text_color(&s_label_name, lv_color_hex(kTextMain));
|
||||
lv_style_set_text_font(&s_label_name, &lv_font_noto_cjk_16_2bpp);
|
||||
|
||||
lv_style_init(&s_label_preview);
|
||||
lv_style_set_text_color(&s_label_preview, lv_color_hex(0x606060));
|
||||
lv_style_set_text_color(&s_label_preview, lv_color_hex(kTextMuted));
|
||||
lv_style_set_text_font(&s_label_preview, &lv_font_noto_cjk_16_2bpp);
|
||||
|
||||
lv_style_init(&s_label_time);
|
||||
lv_style_set_text_color(&s_label_time, lv_color_hex(0x808080));
|
||||
lv_style_set_text_color(&s_label_time, lv_color_hex(kTextMuted));
|
||||
lv_style_set_text_font(&s_label_time, &lv_font_noto_cjk_16_2bpp);
|
||||
|
||||
lv_style_init(&s_label_unread);
|
||||
lv_style_set_text_color(&s_label_unread, lv_color_hex(0xEBA341));
|
||||
lv_style_set_text_color(&s_label_unread, lv_color_hex(kPrimary));
|
||||
lv_style_set_text_font(&s_label_unread, &lv_font_noto_cjk_16_2bpp);
|
||||
|
||||
lv_style_init(&s_label_placeholder);
|
||||
lv_style_set_text_color(&s_label_placeholder, lv_color_hex(0x707070));
|
||||
lv_style_set_text_color(&s_label_placeholder, lv_color_hex(kTextMuted));
|
||||
lv_style_set_text_font(&s_label_placeholder, &lv_font_noto_cjk_16_2bpp);
|
||||
}
|
||||
|
||||
@@ -84,6 +116,12 @@ void apply_panel(lv_obj_t* obj)
|
||||
lv_obj_add_style(obj, &s_panel, 0);
|
||||
}
|
||||
|
||||
void apply_filter_panel(lv_obj_t* obj)
|
||||
{
|
||||
init_once();
|
||||
lv_obj_add_style(obj, &s_filter_panel, 0);
|
||||
}
|
||||
|
||||
void apply_item_btn(lv_obj_t* btn)
|
||||
{
|
||||
init_once();
|
||||
@@ -91,6 +129,13 @@ void apply_item_btn(lv_obj_t* btn)
|
||||
lv_obj_add_style(btn, &s_item_btn_focused, LV_STATE_FOCUSED);
|
||||
}
|
||||
|
||||
void apply_filter_btn(lv_obj_t* btn)
|
||||
{
|
||||
init_once();
|
||||
lv_obj_add_style(btn, &s_filter_btn, LV_PART_MAIN);
|
||||
lv_obj_add_style(btn, &s_filter_btn_checked, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
void apply_label_name(lv_obj_t* label) { init_once(); lv_obj_add_style(label, &s_label_name, 0); }
|
||||
void apply_label_preview(lv_obj_t* label) { init_once(); lv_obj_add_style(label, &s_label_preview, 0); }
|
||||
void apply_label_time(lv_obj_t* label) { init_once(); lv_obj_add_style(label, &s_label_time, 0); }
|
||||
|
||||
@@ -8,9 +8,11 @@ void init_once();
|
||||
// containers
|
||||
void apply_root_container(lv_obj_t* obj);
|
||||
void apply_panel(lv_obj_t* obj);
|
||||
void apply_filter_panel(lv_obj_t* obj);
|
||||
|
||||
// list item
|
||||
void apply_item_btn(lv_obj_t* btn);
|
||||
void apply_filter_btn(lv_obj_t* btn);
|
||||
|
||||
// labels
|
||||
void apply_label_name(lv_obj_t* label);
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
using namespace contacts::ui;
|
||||
|
||||
lv_obj_t* ui_chat_get_container();
|
||||
|
||||
static constexpr int kItemsPerPage = 4;
|
||||
static constexpr int kButtonHeight = 32;
|
||||
static constexpr int kButtonWidth = 80;
|
||||
@@ -37,6 +39,7 @@ static constexpr int kButtonWidth = 80;
|
||||
static lv_group_t* s_compose_group = nullptr;
|
||||
static lv_group_t* s_compose_prev_group = nullptr;
|
||||
static uint32_t s_compose_peer_id = 0;
|
||||
static chat::ChannelId s_compose_channel = chat::ChannelId::PRIMARY;
|
||||
static bool s_refreshing_ui = false;
|
||||
|
||||
// --- Forward declarations (same behavior as original) ---
|
||||
@@ -56,6 +59,7 @@ static void on_action_back_clicked(lv_event_t* e);
|
||||
static void ensure_action_buttons(); // 新增:创建/更新第三列按钮
|
||||
static void on_action_clicked(lv_event_t* e);
|
||||
static const chat::contacts::NodeInfo* get_selected_node();
|
||||
static chat::ChannelId get_selected_channel();
|
||||
static void open_add_edit_modal(bool is_edit);
|
||||
static void open_delete_confirm_modal();
|
||||
static void open_info_modal();
|
||||
@@ -156,6 +160,10 @@ void create_filter_panel(lv_obj_t* parent)
|
||||
lv_obj_add_event_cb(g_contacts_state.nearby_btn, on_filter_focused, LV_EVENT_FOCUSED, nullptr);
|
||||
lv_obj_add_event_cb(g_contacts_state.nearby_btn, on_filter_clicked, LV_EVENT_CLICKED, nullptr);
|
||||
}
|
||||
if (g_contacts_state.broadcast_btn) {
|
||||
lv_obj_add_event_cb(g_contacts_state.broadcast_btn, on_filter_focused, LV_EVENT_FOCUSED, nullptr);
|
||||
lv_obj_add_event_cb(g_contacts_state.broadcast_btn, on_filter_clicked, LV_EVENT_CLICKED, nullptr);
|
||||
}
|
||||
|
||||
// TopBar back button must be reachable by rotary and also respond to press
|
||||
// (Press should exit page, not enter List column)
|
||||
@@ -165,14 +173,17 @@ void create_filter_panel(lv_obj_t* parent)
|
||||
|
||||
// Keep highlight consistent with mode using CHECKED state
|
||||
// (visual-only; does not change behavior)
|
||||
if (g_contacts_state.contacts_btn && g_contacts_state.nearby_btn) {
|
||||
if (g_contacts_state.contacts_btn && g_contacts_state.nearby_btn && g_contacts_state.broadcast_btn) {
|
||||
lv_obj_clear_state(g_contacts_state.contacts_btn, LV_STATE_CHECKED);
|
||||
lv_obj_clear_state(g_contacts_state.nearby_btn, LV_STATE_CHECKED);
|
||||
lv_obj_clear_state(g_contacts_state.broadcast_btn, LV_STATE_CHECKED);
|
||||
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts) {
|
||||
lv_obj_add_state(g_contacts_state.contacts_btn, LV_STATE_CHECKED);
|
||||
} else {
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
|
||||
lv_obj_add_state(g_contacts_state.nearby_btn, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_add_state(g_contacts_state.broadcast_btn, LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,6 +229,7 @@ static void on_filter_focused(lv_event_t* e)
|
||||
ContactsMode new_mode = g_contacts_state.current_mode;
|
||||
if (tgt == g_contacts_state.contacts_btn) new_mode = ContactsMode::Contacts;
|
||||
else if (tgt == g_contacts_state.nearby_btn) new_mode = ContactsMode::Nearby;
|
||||
else if (tgt == g_contacts_state.broadcast_btn) new_mode = ContactsMode::Broadcast;
|
||||
else return;
|
||||
|
||||
if (new_mode != g_contacts_state.current_mode) {
|
||||
@@ -290,6 +302,9 @@ static void on_back_clicked(lv_event_t* /*e*/)
|
||||
|
||||
static const chat::contacts::NodeInfo* get_selected_node()
|
||||
{
|
||||
if (g_contacts_state.current_mode == ContactsMode::Broadcast) {
|
||||
return nullptr;
|
||||
}
|
||||
if (g_contacts_state.selected_index < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -302,6 +317,20 @@ static const chat::contacts::NodeInfo* get_selected_node()
|
||||
return &list[g_contacts_state.selected_index];
|
||||
}
|
||||
|
||||
static chat::ChannelId get_selected_channel()
|
||||
{
|
||||
if (g_contacts_state.current_mode != ContactsMode::Broadcast) {
|
||||
return chat::ChannelId::PRIMARY;
|
||||
}
|
||||
if (g_contacts_state.selected_index < 0) {
|
||||
return chat::ChannelId::PRIMARY;
|
||||
}
|
||||
if (g_contacts_state.selected_index == 1) {
|
||||
return chat::ChannelId::SECONDARY;
|
||||
}
|
||||
return chat::ChannelId::PRIMARY;
|
||||
}
|
||||
|
||||
static void modal_prepare_group()
|
||||
{
|
||||
if (!g_contacts_state.modal_group) {
|
||||
@@ -560,13 +589,18 @@ static void open_chat_compose()
|
||||
return;
|
||||
}
|
||||
const auto* node = get_selected_node();
|
||||
if (!node) {
|
||||
if (g_contacts_state.current_mode != ContactsMode::Broadcast && !node) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_t* parent = g_contacts_state.root
|
||||
? lv_obj_get_parent(g_contacts_state.root)
|
||||
: lv_screen_active();
|
||||
if (lv_obj_t* chat_parent = ui_chat_get_container()) {
|
||||
if (lv_obj_is_valid(chat_parent)) {
|
||||
parent = chat_parent;
|
||||
}
|
||||
}
|
||||
|
||||
s_compose_prev_group = lv_group_get_default();
|
||||
if (!s_compose_group) {
|
||||
@@ -575,7 +609,25 @@ static void open_chat_compose()
|
||||
lv_group_remove_all_objs(s_compose_group);
|
||||
set_default_group(s_compose_group);
|
||||
|
||||
chat::ConversationId conv(chat::ChannelId::PRIMARY, node->node_id);
|
||||
chat::ChannelId channel = chat::ChannelId::PRIMARY;
|
||||
uint32_t peer_id = 0;
|
||||
std::string title;
|
||||
if (g_contacts_state.current_mode == ContactsMode::Broadcast) {
|
||||
channel = get_selected_channel();
|
||||
peer_id = 0;
|
||||
title = "Broadcast";
|
||||
} else {
|
||||
channel = chat::ChannelId::PRIMARY;
|
||||
peer_id = node->node_id;
|
||||
if (g_contacts_state.contact_service) {
|
||||
title = g_contacts_state.contact_service->getContactName(node->node_id);
|
||||
}
|
||||
if (title.empty()) {
|
||||
title = node->display_name;
|
||||
}
|
||||
}
|
||||
|
||||
chat::ConversationId conv(channel, peer_id);
|
||||
g_contacts_state.compose_screen = new chat::ui::ChatComposeScreen(parent, conv);
|
||||
g_contacts_state.compose_screen->setActionCallback(on_compose_action, nullptr);
|
||||
g_contacts_state.compose_screen->setBackCallback(on_compose_back, nullptr);
|
||||
@@ -593,15 +645,9 @@ static void open_chat_compose()
|
||||
}
|
||||
}
|
||||
|
||||
std::string title;
|
||||
if (g_contacts_state.contact_service) {
|
||||
title = g_contacts_state.contact_service->getContactName(node->node_id);
|
||||
}
|
||||
if (title.empty()) {
|
||||
title = node->display_name;
|
||||
}
|
||||
g_contacts_state.compose_screen->setHeaderText(title.c_str(), "RSSI --");
|
||||
s_compose_peer_id = node->node_id;
|
||||
s_compose_peer_id = peer_id;
|
||||
s_compose_channel = channel;
|
||||
|
||||
if (g_contacts_state.root) {
|
||||
lv_obj_add_flag(g_contacts_state.root, LV_OBJ_FLAG_HIDDEN);
|
||||
@@ -624,6 +670,7 @@ static void close_chat_compose()
|
||||
delete g_contacts_state.compose_screen;
|
||||
g_contacts_state.compose_screen = nullptr;
|
||||
s_compose_peer_id = 0;
|
||||
s_compose_channel = chat::ChannelId::PRIMARY;
|
||||
|
||||
if (g_contacts_state.root) {
|
||||
lv_obj_clear_flag(g_contacts_state.root, LV_OBJ_FLAG_HIDDEN);
|
||||
@@ -645,12 +692,11 @@ static void close_chat_compose()
|
||||
|
||||
static void on_compose_action(bool send, void* /*user_data*/)
|
||||
{
|
||||
if (send && g_contacts_state.compose_screen && s_compose_peer_id != 0 &&
|
||||
g_contacts_state.chat_service) {
|
||||
if (send && g_contacts_state.compose_screen && g_contacts_state.chat_service) {
|
||||
std::string text = g_contacts_state.compose_screen->getText();
|
||||
if (!text.empty()) {
|
||||
g_contacts_state.chat_service->sendText(
|
||||
chat::ChannelId::PRIMARY, text, s_compose_peer_id
|
||||
s_compose_channel, text, s_compose_peer_id
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -934,19 +980,28 @@ static void ensure_action_buttons()
|
||||
lv_obj_clear_flag(g_contacts_state.edit_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(g_contacts_state.del_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag (g_contacts_state.add_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(g_contacts_state.info_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.chat_btn, lv_color_hex(0xEBA341), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.edit_btn, lv_color_hex(0xF1B65A), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.del_btn, lv_color_hex(0xEBA341), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.info_btn, lv_color_hex(0xF1B65A), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.action_back_btn, lv_color_hex(0xEBA341), LV_PART_MAIN);
|
||||
} else {
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
|
||||
lv_obj_add_flag (g_contacts_state.edit_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag (g_contacts_state.del_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(g_contacts_state.add_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(g_contacts_state.info_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.chat_btn, lv_color_hex(0xEBA341), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.add_btn, lv_color_hex(0xF1B65A), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.info_btn, lv_color_hex(0xEBA341), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.action_back_btn, lv_color_hex(0xF1B65A), LV_PART_MAIN);
|
||||
} else {
|
||||
lv_obj_add_flag (g_contacts_state.edit_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag (g_contacts_state.del_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag (g_contacts_state.add_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag (g_contacts_state.info_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.chat_btn, lv_color_hex(0xEBA341), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_contacts_state.action_back_btn, lv_color_hex(0xF1B65A), LV_PART_MAIN);
|
||||
}
|
||||
|
||||
/* ---------- enable / disable ---------- */
|
||||
@@ -957,12 +1012,14 @@ static void ensure_action_buttons()
|
||||
set_enabled(g_contacts_state.action_back_btn, true);
|
||||
|
||||
set_enabled(g_contacts_state.chat_btn, has_sel);
|
||||
set_enabled(g_contacts_state.info_btn, has_sel);
|
||||
if (g_contacts_state.current_mode != ContactsMode::Broadcast) {
|
||||
set_enabled(g_contacts_state.info_btn, has_sel);
|
||||
}
|
||||
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts) {
|
||||
set_enabled(g_contacts_state.edit_btn, has_sel);
|
||||
set_enabled(g_contacts_state.del_btn, has_sel);
|
||||
} else {
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
|
||||
set_enabled(g_contacts_state.add_btn, has_sel);
|
||||
}
|
||||
}
|
||||
@@ -1015,6 +1072,20 @@ void refresh_ui()
|
||||
}
|
||||
s_refreshing_ui = true;
|
||||
|
||||
lv_obj_t* active = lv_screen_active();
|
||||
if (!active) {
|
||||
CONTACTS_LOG("[Contacts] WARNING: lv_screen_active() is null\n");
|
||||
} else {
|
||||
CONTACTS_LOG("[Contacts] refresh_ui: active=%p root=%p list_panel=%p\n",
|
||||
active, g_contacts_state.root, g_contacts_state.list_panel);
|
||||
}
|
||||
if (g_contacts_state.root && !lv_obj_is_valid(g_contacts_state.root)) {
|
||||
CONTACTS_LOG("[Contacts] WARNING: root is invalid\n");
|
||||
}
|
||||
if (g_contacts_state.list_panel && !lv_obj_is_valid(g_contacts_state.list_panel)) {
|
||||
CONTACTS_LOG("[Contacts] WARNING: list_panel is invalid\n");
|
||||
}
|
||||
|
||||
lv_obj_clear_flag(g_contacts_state.list_panel, LV_OBJ_FLAG_SCROLLABLE);
|
||||
if (g_contacts_state.sub_container) {
|
||||
lv_obj_clear_flag(g_contacts_state.sub_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
@@ -1042,11 +1113,23 @@ void refresh_ui()
|
||||
g_contacts_state.list_items.clear();
|
||||
|
||||
// Choose list by mode (unchanged)
|
||||
const auto& current_list = (g_contacts_state.current_mode == ContactsMode::Contacts)
|
||||
? g_contacts_state.contacts_list
|
||||
: g_contacts_state.nearby_list;
|
||||
std::vector<chat::contacts::NodeInfo> broadcast_list;
|
||||
const std::vector<chat::contacts::NodeInfo>* current_list = nullptr;
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts) {
|
||||
current_list = &g_contacts_state.contacts_list;
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
|
||||
current_list = &g_contacts_state.nearby_list;
|
||||
} else {
|
||||
chat::contacts::NodeInfo primary{};
|
||||
primary.display_name = "Primary";
|
||||
chat::contacts::NodeInfo secondary{};
|
||||
secondary.display_name = "Secondary";
|
||||
broadcast_list.push_back(primary);
|
||||
broadcast_list.push_back(secondary);
|
||||
current_list = &broadcast_list;
|
||||
}
|
||||
|
||||
g_contacts_state.total_items = current_list.size();
|
||||
g_contacts_state.total_items = current_list->size();
|
||||
|
||||
if (g_contacts_state.selected_index >= static_cast<int>(g_contacts_state.total_items)) {
|
||||
g_contacts_state.selected_index = -1;
|
||||
@@ -1071,13 +1154,15 @@ void refresh_ui()
|
||||
|
||||
// Create list items for current page (structure in layout; status string computed here)
|
||||
for (int i = start_idx; i < end_idx; ++i) {
|
||||
const auto& node = current_list[i];
|
||||
const auto& node = (*current_list)[i];
|
||||
|
||||
std::string status_text;
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts) {
|
||||
status_text = format_time_status(node.last_seen);
|
||||
} else {
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
|
||||
status_text = format_snr(node.snr);
|
||||
} else {
|
||||
status_text = "Channel";
|
||||
}
|
||||
|
||||
lv_obj_t* item = contacts::ui::layout::create_list_item(
|
||||
@@ -1143,14 +1228,19 @@ void refresh_ui()
|
||||
lv_obj_clear_state(g_contacts_state.back_btn, LV_STATE_DISABLED);
|
||||
|
||||
// Update filter highlights (visual-only, using CHECKED state)
|
||||
if (g_contacts_state.contacts_btn != nullptr && g_contacts_state.nearby_btn != nullptr) {
|
||||
if (g_contacts_state.contacts_btn != nullptr &&
|
||||
g_contacts_state.nearby_btn != nullptr &&
|
||||
g_contacts_state.broadcast_btn != nullptr) {
|
||||
lv_obj_clear_state(g_contacts_state.contacts_btn, LV_STATE_CHECKED);
|
||||
lv_obj_clear_state(g_contacts_state.nearby_btn, LV_STATE_CHECKED);
|
||||
lv_obj_clear_state(g_contacts_state.broadcast_btn, LV_STATE_CHECKED);
|
||||
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts) {
|
||||
lv_obj_add_state(g_contacts_state.contacts_btn, LV_STATE_CHECKED);
|
||||
} else {
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
|
||||
lv_obj_add_state(g_contacts_state.nearby_btn, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_add_state(g_contacts_state.broadcast_btn, LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1166,12 +1256,6 @@ void refresh_ui()
|
||||
}
|
||||
s_refreshing_ui = false;
|
||||
|
||||
CONTACTS_LOG("[Contacts] UI refreshed: mode=%d, page=%d/%d, items=%zu\n",
|
||||
(int)g_contacts_state.current_mode,
|
||||
g_contacts_state.current_page + 1,
|
||||
(static_cast<int>(g_contacts_state.total_items) + kItemsPerPage - 1) / kItemsPerPage,
|
||||
g_contacts_state.total_items);
|
||||
|
||||
ensure_action_buttons();
|
||||
contacts_input_on_ui_refreshed();
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ static void bind_filter_column(bool keep_mode_focus)
|
||||
// ② Filter buttons
|
||||
if (g_contacts_state.contacts_btn) lv_group_add_obj(s_group, g_contacts_state.contacts_btn);
|
||||
if (g_contacts_state.nearby_btn) lv_group_add_obj(s_group, g_contacts_state.nearby_btn);
|
||||
if (g_contacts_state.broadcast_btn) lv_group_add_obj(s_group, g_contacts_state.broadcast_btn);
|
||||
|
||||
// Focus preference:
|
||||
// - keep_mode_focus: focus current mode button (better for rotate-to-switch-mode UX)
|
||||
@@ -92,12 +93,16 @@ static void bind_filter_column(bool keep_mode_focus)
|
||||
focus_first_valid(g_contacts_state.contacts_btn);
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby && g_contacts_state.nearby_btn) {
|
||||
focus_first_valid(g_contacts_state.nearby_btn);
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Broadcast && g_contacts_state.broadcast_btn) {
|
||||
focus_first_valid(g_contacts_state.broadcast_btn);
|
||||
} else if (g_contacts_state.top_bar.back_btn) {
|
||||
focus_first_valid(g_contacts_state.top_bar.back_btn);
|
||||
} else if (g_contacts_state.contacts_btn) {
|
||||
focus_first_valid(g_contacts_state.contacts_btn);
|
||||
} else if (g_contacts_state.nearby_btn) {
|
||||
focus_first_valid(g_contacts_state.nearby_btn);
|
||||
} else if (g_contacts_state.broadcast_btn) {
|
||||
focus_first_valid(g_contacts_state.broadcast_btn);
|
||||
}
|
||||
} else {
|
||||
// first entry: prefer current mode button
|
||||
@@ -105,16 +110,19 @@ static void bind_filter_column(bool keep_mode_focus)
|
||||
focus_first_valid(g_contacts_state.contacts_btn);
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby && g_contacts_state.nearby_btn) {
|
||||
focus_first_valid(g_contacts_state.nearby_btn);
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Broadcast && g_contacts_state.broadcast_btn) {
|
||||
focus_first_valid(g_contacts_state.broadcast_btn);
|
||||
} else if (g_contacts_state.contacts_btn) {
|
||||
focus_first_valid(g_contacts_state.contacts_btn);
|
||||
} else if (g_contacts_state.nearby_btn) {
|
||||
focus_first_valid(g_contacts_state.nearby_btn);
|
||||
} else if (g_contacts_state.broadcast_btn) {
|
||||
focus_first_valid(g_contacts_state.broadcast_btn);
|
||||
} else if (g_contacts_state.top_bar.back_btn) {
|
||||
focus_first_valid(g_contacts_state.top_bar.back_btn);
|
||||
}
|
||||
}
|
||||
|
||||
CONTACTS_LOG("[Contacts][Input] bind_filter_column (with topbar back)\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -159,11 +167,13 @@ static void bind_action_column()
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts) {
|
||||
if (g_contacts_state.edit_btn) { lv_group_add_obj(s_group, g_contacts_state.edit_btn); any = true; }
|
||||
if (g_contacts_state.del_btn) { lv_group_add_obj(s_group, g_contacts_state.del_btn); any = true; }
|
||||
} else {
|
||||
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
|
||||
if (g_contacts_state.add_btn) { lv_group_add_obj(s_group, g_contacts_state.add_btn); any = true; }
|
||||
}
|
||||
|
||||
if (g_contacts_state.info_btn) { lv_group_add_obj(s_group, g_contacts_state.info_btn); any = true; }
|
||||
if (g_contacts_state.current_mode != ContactsMode::Broadcast) {
|
||||
if (g_contacts_state.info_btn) { lv_group_add_obj(s_group, g_contacts_state.info_btn); any = true; }
|
||||
}
|
||||
|
||||
// ② Back (added last, not the default focus)
|
||||
if (g_contacts_state.action_back_btn) {
|
||||
|
||||
@@ -218,6 +218,14 @@ void create_filter_panel(lv_obj_t* parent)
|
||||
lv_obj_t* nearby_label = lv_label_create(g_contacts_state.nearby_btn);
|
||||
lv_label_set_text(nearby_label, "Nearby");
|
||||
lv_obj_center(nearby_label);
|
||||
|
||||
g_contacts_state.broadcast_btn = lv_btn_create(g_contacts_state.filter_panel);
|
||||
make_non_scrollable(g_contacts_state.broadcast_btn);
|
||||
style::apply_btn_filter(g_contacts_state.broadcast_btn);
|
||||
lv_obj_set_size(g_contacts_state.broadcast_btn, LV_PCT(100), kButtonHeight);
|
||||
lv_obj_t* broadcast_label = lv_label_create(g_contacts_state.broadcast_btn);
|
||||
lv_label_set_text(broadcast_label, "Broadcast");
|
||||
lv_obj_center(broadcast_label);
|
||||
}
|
||||
|
||||
void create_list_panel(lv_obj_t* parent)
|
||||
|
||||
@@ -32,7 +32,8 @@ namespace ui {
|
||||
|
||||
enum class ContactsMode {
|
||||
Contacts, // Show contacts (nodes with nicknames)
|
||||
Nearby // Show nearby nodes (nodes without nicknames)
|
||||
Nearby, // Show nearby nodes (nodes without nicknames)
|
||||
Broadcast // Show broadcast channels
|
||||
};
|
||||
|
||||
struct ContactsPageState {
|
||||
@@ -45,6 +46,7 @@ struct ContactsPageState {
|
||||
lv_obj_t* filter_panel = nullptr;
|
||||
lv_obj_t* contacts_btn = nullptr;
|
||||
lv_obj_t* nearby_btn = nullptr;
|
||||
lv_obj_t* broadcast_btn = nullptr;
|
||||
|
||||
// Second column: Node list
|
||||
lv_obj_t* list_panel = nullptr;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <cstring>
|
||||
|
||||
#include "../../../app/app_context.h"
|
||||
#include "../../../board/rtc_utils.h"
|
||||
#include "../../../chat/domain/chat_types.h"
|
||||
#include "../../../chat/infra/meshtastic/generated/meshtastic/config.pb.h"
|
||||
#include "../../../chat/infra/meshtastic/mt_region.h"
|
||||
@@ -596,11 +595,9 @@ static void on_option_clicked(lv_event_t* e)
|
||||
}
|
||||
if (payload->item->pref_key && strcmp(payload->item->pref_key, "timezone_offset") == 0)
|
||||
{
|
||||
int delta = payload->value - previous_value;
|
||||
if (delta != 0)
|
||||
{
|
||||
board_adjust_rtc_by_offset_minutes(delta);
|
||||
}
|
||||
ui_set_timezone_offset_min(payload->value);
|
||||
(void)previous_value;
|
||||
restart_now = true;
|
||||
}
|
||||
modal_close();
|
||||
if (restart_now)
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* @file team_page_components.cpp
|
||||
* @brief Team page components
|
||||
*/
|
||||
|
||||
#include "team_page_components.h"
|
||||
#include "team_page_layout.h"
|
||||
#include "team_page_styles.h"
|
||||
#include "team_page_input.h"
|
||||
#include "team_state.h"
|
||||
#include "../../ui_common.h"
|
||||
#include "../../../app/app_context.h"
|
||||
#include "../../../team/protocol/team_mgmt.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void update_top_bar_title(const char* title)
|
||||
{
|
||||
if (!title)
|
||||
{
|
||||
return;
|
||||
}
|
||||
::ui::widgets::top_bar_set_title(g_team_state.top_bar_widget, title);
|
||||
}
|
||||
|
||||
void reset_team_ui_state()
|
||||
{
|
||||
app::AppContext& app_ctx = app::AppContext::getInstance();
|
||||
team::TeamController* controller = app_ctx.getTeamController();
|
||||
if (controller)
|
||||
{
|
||||
controller->resetUiState();
|
||||
}
|
||||
}
|
||||
|
||||
void top_bar_back(void*)
|
||||
{
|
||||
reset_team_ui_state();
|
||||
team_page_destroy();
|
||||
menu_show();
|
||||
}
|
||||
|
||||
void label_set_text(lv_obj_t* btn, const char* text)
|
||||
{
|
||||
lv_obj_t* label = lv_label_create(btn);
|
||||
lv_label_set_text(label, text);
|
||||
lv_obj_center(label);
|
||||
}
|
||||
|
||||
void handle_create(lv_event_t*)
|
||||
{
|
||||
app::AppContext& app_ctx = app::AppContext::getInstance();
|
||||
team::TeamController* controller = app_ctx.getTeamController();
|
||||
if (!controller)
|
||||
{
|
||||
return;
|
||||
}
|
||||
team::proto::TeamAdvertise advertise;
|
||||
controller->onCreateTeam(advertise, chat::ChannelId::PRIMARY);
|
||||
g_team_state.title_override = "Create Team";
|
||||
update_top_bar_title(g_team_state.title_override);
|
||||
team_page_refresh();
|
||||
}
|
||||
|
||||
void handle_join(lv_event_t*)
|
||||
{
|
||||
app::AppContext& app_ctx = app::AppContext::getInstance();
|
||||
team::TeamController* controller = app_ctx.getTeamController();
|
||||
if (!controller)
|
||||
{
|
||||
return;
|
||||
}
|
||||
team::proto::TeamJoinRequest join_request;
|
||||
controller->onJoinTeam(join_request, chat::ChannelId::PRIMARY, 0);
|
||||
g_team_state.title_override = "Join Team";
|
||||
update_top_bar_title(g_team_state.title_override);
|
||||
team_page_refresh();
|
||||
}
|
||||
|
||||
void handle_placeholder(lv_event_t*)
|
||||
{
|
||||
team_page_refresh();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void team_page_create(lv_obj_t* parent)
|
||||
{
|
||||
if (g_team_state.root)
|
||||
{
|
||||
lv_obj_del(g_team_state.root);
|
||||
g_team_state.root = nullptr;
|
||||
}
|
||||
|
||||
style::init_once();
|
||||
|
||||
g_team_state.root = layout::create_root(parent);
|
||||
g_team_state.header = layout::create_header(g_team_state.root);
|
||||
g_team_state.content = layout::create_content(g_team_state.root);
|
||||
g_team_state.state_container = layout::create_state_container(g_team_state.content);
|
||||
g_team_state.idle_container = layout::create_idle_container(g_team_state.state_container);
|
||||
g_team_state.active_container = layout::create_active_container(g_team_state.state_container);
|
||||
|
||||
style::apply_root(g_team_state.root);
|
||||
style::apply_header(g_team_state.header);
|
||||
style::apply_content(g_team_state.content);
|
||||
style::apply_state_container(g_team_state.state_container);
|
||||
style::apply_idle_container(g_team_state.idle_container);
|
||||
style::apply_active_container(g_team_state.active_container);
|
||||
style::apply_status_label(g_team_state.idle_status_label);
|
||||
style::apply_status_label(g_team_state.active_status_label);
|
||||
|
||||
style::apply_button_primary(g_team_state.create_btn);
|
||||
style::apply_button_secondary(g_team_state.join_btn);
|
||||
style::apply_button_secondary(g_team_state.share_btn);
|
||||
style::apply_button_secondary(g_team_state.leave_btn);
|
||||
style::apply_button_secondary(g_team_state.disband_btn);
|
||||
|
||||
::ui::widgets::TopBarConfig cfg;
|
||||
cfg.height = ::ui::widgets::kTopBarHeight;
|
||||
::ui::widgets::top_bar_init(g_team_state.top_bar_widget, g_team_state.header, cfg);
|
||||
update_top_bar_title("Team");
|
||||
::ui::widgets::top_bar_set_back_callback(g_team_state.top_bar_widget, top_bar_back, nullptr);
|
||||
ui_update_top_bar_battery(g_team_state.top_bar_widget);
|
||||
|
||||
label_set_text(g_team_state.create_btn, "Create Team");
|
||||
label_set_text(g_team_state.join_btn, "Join Team");
|
||||
label_set_text(g_team_state.share_btn, "Share Team");
|
||||
label_set_text(g_team_state.leave_btn, "Leave Team");
|
||||
label_set_text(g_team_state.disband_btn, "Disband Team");
|
||||
|
||||
lv_obj_add_event_cb(g_team_state.create_btn, handle_create, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(g_team_state.join_btn, handle_join, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(g_team_state.share_btn, handle_placeholder, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(g_team_state.leave_btn, handle_placeholder, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(g_team_state.disband_btn, handle_placeholder, LV_EVENT_CLICKED, nullptr);
|
||||
|
||||
init_team_input();
|
||||
team_page_refresh();
|
||||
}
|
||||
|
||||
void team_page_destroy()
|
||||
{
|
||||
cleanup_team_input();
|
||||
|
||||
if (g_team_state.root)
|
||||
{
|
||||
lv_obj_del(g_team_state.root);
|
||||
g_team_state.root = nullptr;
|
||||
}
|
||||
g_team_state = TeamPageState{};
|
||||
}
|
||||
|
||||
void team_page_refresh()
|
||||
{
|
||||
app::AppContext& app_ctx = app::AppContext::getInstance();
|
||||
team::TeamController* controller = app_ctx.getTeamController();
|
||||
team::TeamUiState state = controller ? controller->getState() : team::TeamUiState::Idle;
|
||||
const char* title = "Team";
|
||||
|
||||
bool idle = (state == team::TeamUiState::Idle);
|
||||
|
||||
if (idle)
|
||||
{
|
||||
lv_obj_clear_flag(g_team_state.idle_container, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(g_team_state.idle_container, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
lv_obj_add_flag(g_team_state.active_container, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(g_team_state.active_container, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
}
|
||||
else
|
||||
{
|
||||
lv_obj_add_flag(g_team_state.idle_container, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(g_team_state.idle_container, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
lv_obj_clear_flag(g_team_state.active_container, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(g_team_state.active_container, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
}
|
||||
|
||||
if (idle)
|
||||
{
|
||||
lv_label_set_text(g_team_state.idle_status_label, "You are not in a Team");
|
||||
}
|
||||
else
|
||||
{
|
||||
lv_label_set_text(g_team_state.active_status_label, "Team Active");
|
||||
}
|
||||
|
||||
if (state == team::TeamUiState::PendingJoin)
|
||||
{
|
||||
title = "Join Team";
|
||||
}
|
||||
else if (state == team::TeamUiState::Active && g_team_state.title_override)
|
||||
{
|
||||
title = g_team_state.title_override;
|
||||
}
|
||||
update_top_bar_title(title);
|
||||
|
||||
refresh_team_input();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
|
||||
void team_page_create(lv_obj_t* parent)
|
||||
{
|
||||
team::ui::team_page_create(parent);
|
||||
}
|
||||
|
||||
void team_page_destroy()
|
||||
{
|
||||
team::ui::team_page_destroy();
|
||||
}
|
||||
|
||||
void team_page_refresh()
|
||||
{
|
||||
team::ui::team_page_refresh();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @file team_page_components.h
|
||||
* @brief Team page components (public interface)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
void team_page_create(lv_obj_t* parent);
|
||||
void team_page_destroy();
|
||||
void team_page_refresh();
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @file team_page_input.cpp
|
||||
* @brief Team page input handling
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "team_page_input.h"
|
||||
#include "team_state.h"
|
||||
#include "../../ui_common.h"
|
||||
#include "../../../app/app_context.h"
|
||||
|
||||
extern lv_group_t* app_g;
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
namespace
|
||||
{
|
||||
lv_group_t* s_group = nullptr;
|
||||
|
||||
void group_clear_all(lv_group_t* group)
|
||||
{
|
||||
if (!group)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lv_group_remove_all_objs(group);
|
||||
}
|
||||
|
||||
void add_if(lv_obj_t* obj)
|
||||
{
|
||||
if (obj && s_group)
|
||||
{
|
||||
lv_group_add_obj(s_group, obj);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void init_team_input()
|
||||
{
|
||||
extern lv_group_t* app_g;
|
||||
if (!::app_g)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_group = ::app_g;
|
||||
set_default_group(s_group);
|
||||
refresh_team_input();
|
||||
}
|
||||
|
||||
void refresh_team_input()
|
||||
{
|
||||
if (!s_group)
|
||||
{
|
||||
return;
|
||||
}
|
||||
group_clear_all(s_group);
|
||||
|
||||
if (g_team_state.top_bar_widget.back_btn)
|
||||
{
|
||||
lv_group_add_obj(s_group, g_team_state.top_bar_widget.back_btn);
|
||||
}
|
||||
|
||||
app::AppContext& app_ctx = app::AppContext::getInstance();
|
||||
team::TeamController* controller = app_ctx.getTeamController();
|
||||
team::TeamUiState state = controller ? controller->getState() : team::TeamUiState::Idle;
|
||||
|
||||
if (state == team::TeamUiState::Idle)
|
||||
{
|
||||
add_if(g_team_state.create_btn);
|
||||
add_if(g_team_state.join_btn);
|
||||
if (g_team_state.create_btn)
|
||||
{
|
||||
lv_group_focus_obj(g_team_state.create_btn);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
add_if(g_team_state.share_btn);
|
||||
add_if(g_team_state.leave_btn);
|
||||
add_if(g_team_state.disband_btn);
|
||||
if (g_team_state.share_btn)
|
||||
{
|
||||
lv_group_focus_obj(g_team_state.share_btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cleanup_team_input()
|
||||
{
|
||||
if (!s_group)
|
||||
{
|
||||
return;
|
||||
}
|
||||
group_clear_all(s_group);
|
||||
s_group = nullptr;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
|
||||
void init_team_input()
|
||||
{
|
||||
team::ui::init_team_input();
|
||||
}
|
||||
|
||||
void refresh_team_input()
|
||||
{
|
||||
team::ui::refresh_team_input();
|
||||
}
|
||||
|
||||
void cleanup_team_input()
|
||||
{
|
||||
team::ui::cleanup_team_input();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @file team_page_input.h
|
||||
* @brief Team page input handling
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
void init_team_input();
|
||||
void refresh_team_input();
|
||||
void cleanup_team_input();
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* UI Wireframe / Layout Tree
|
||||
* --------------------------------------------------------------------
|
||||
*
|
||||
* Root (COLUMN)
|
||||
* ├─ Header (TopBar)
|
||||
* └─ Content (flex-grow = 1)
|
||||
* └─ StateContainer (COLUMN, centered)
|
||||
* ├─ IdleContainer
|
||||
* │ ├─ IdleStatusLabel
|
||||
* │ ├─ CreateBtn
|
||||
* │ └─ JoinBtn
|
||||
* └─ ActiveContainer
|
||||
* ├─ ActiveStatusLabel
|
||||
* ├─ ViewMapBtn
|
||||
* ├─ ShareBtn
|
||||
* ├─ LeaveBtn
|
||||
* └─ DisbandBtn
|
||||
*
|
||||
* Tree view:
|
||||
* Root(COL)
|
||||
* ├─ Header
|
||||
* └─ Content(COL)
|
||||
* └─ StateContainer(COL)
|
||||
* ├─ IdleContainer(COL) -> IdleStatusLabel, CreateBtn, JoinBtn
|
||||
* └─ ActiveContainer(COL) -> ActiveStatusLabel, ViewMapBtn, ShareBtn, LeaveBtn, DisbandBtn
|
||||
*
|
||||
* ASCII wireframe (layout only):
|
||||
* +----------------------------------------------------------+
|
||||
* | Header (TopBar) |
|
||||
* +----------------------------------------------------------+
|
||||
* | |
|
||||
* | +----------------------------------------------------+ |
|
||||
* | | StateContainer (column, centered) | |
|
||||
* | | | |
|
||||
* | | +------------------------------+ | |
|
||||
* | | | IdleContainer (column) | | |
|
||||
* | | | [StatusLabel] | | |
|
||||
* | | | [CreateBtn] | | |
|
||||
* | | | [JoinBtn] | | |
|
||||
* | | +------------------------------+ | |
|
||||
* | | | |
|
||||
* | | +------------------------------+ | |
|
||||
* | | | ActiveContainer (column) | | |
|
||||
* | | | [StatusLabel] | | |
|
||||
* | | | [ViewMapBtn] | | |
|
||||
* | | | [ShareBtn] | | |
|
||||
* | | | [LeaveBtn] | | |
|
||||
* | | | [DisbandBtn] | | |
|
||||
* | | +------------------------------+ | |
|
||||
* | +----------------------------------------------------+ |
|
||||
* | |
|
||||
* +----------------------------------------------------------+ */
|
||||
|
||||
/**
|
||||
* @file team_page_layout.cpp
|
||||
* @brief Team page layout
|
||||
*/
|
||||
|
||||
#include "team_page_layout.h"
|
||||
#include "team_state.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
namespace layout
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int kButtonWidth = 220;
|
||||
constexpr int kButtonHeight = 32;
|
||||
|
||||
void make_non_scrollable(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_OFF);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
lv_obj_t* create_root(lv_obj_t* parent)
|
||||
{
|
||||
lv_obj_t* root = lv_obj_create(parent);
|
||||
lv_obj_set_size(root, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
|
||||
make_non_scrollable(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
lv_obj_t* create_header(lv_obj_t* root)
|
||||
{
|
||||
lv_obj_t* header = lv_obj_create(root);
|
||||
lv_obj_set_size(header, LV_PCT(100), ::ui::widgets::kTopBarHeight);
|
||||
make_non_scrollable(header);
|
||||
return header;
|
||||
}
|
||||
|
||||
lv_obj_t* create_content(lv_obj_t* root)
|
||||
{
|
||||
lv_obj_t* content = lv_obj_create(root);
|
||||
lv_obj_set_size(content, LV_PCT(100), 0);
|
||||
lv_obj_set_flex_grow(content, 1);
|
||||
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(content, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
|
||||
make_non_scrollable(content);
|
||||
return content;
|
||||
}
|
||||
|
||||
lv_obj_t* create_state_container(lv_obj_t* content)
|
||||
{
|
||||
lv_obj_t* container = lv_obj_create(content);
|
||||
lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
|
||||
make_non_scrollable(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
lv_obj_t* create_idle_container(lv_obj_t* parent)
|
||||
{
|
||||
lv_obj_t* container = lv_obj_create(parent);
|
||||
lv_obj_set_width(container, LV_PCT(100));
|
||||
lv_obj_set_height(container, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);
|
||||
make_non_scrollable(container);
|
||||
|
||||
g_team_state.idle_status_label = lv_label_create(container);
|
||||
g_team_state.create_btn = lv_btn_create(container);
|
||||
g_team_state.join_btn = lv_btn_create(container);
|
||||
|
||||
lv_obj_set_width(g_team_state.idle_status_label, kButtonWidth);
|
||||
lv_obj_set_width(g_team_state.create_btn, kButtonWidth);
|
||||
lv_obj_set_width(g_team_state.join_btn, kButtonWidth);
|
||||
lv_obj_set_height(g_team_state.create_btn, kButtonHeight);
|
||||
lv_obj_set_height(g_team_state.join_btn, kButtonHeight);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
lv_obj_t* create_active_container(lv_obj_t* parent)
|
||||
{
|
||||
lv_obj_t* container = lv_obj_create(parent);
|
||||
lv_obj_set_width(container, LV_PCT(100));
|
||||
lv_obj_set_height(container, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);
|
||||
make_non_scrollable(container);
|
||||
|
||||
g_team_state.active_status_label = lv_label_create(container);
|
||||
g_team_state.share_btn = lv_btn_create(container);
|
||||
g_team_state.leave_btn = lv_btn_create(container);
|
||||
g_team_state.disband_btn = lv_btn_create(container);
|
||||
|
||||
lv_obj_set_width(g_team_state.active_status_label, kButtonWidth);
|
||||
lv_obj_set_width(g_team_state.share_btn, kButtonWidth);
|
||||
lv_obj_set_width(g_team_state.leave_btn, kButtonWidth);
|
||||
lv_obj_set_width(g_team_state.disband_btn, kButtonWidth);
|
||||
lv_obj_set_height(g_team_state.share_btn, kButtonHeight);
|
||||
lv_obj_set_height(g_team_state.leave_btn, kButtonHeight);
|
||||
lv_obj_set_height(g_team_state.disband_btn, kButtonHeight);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
} // namespace layout
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @file team_page_layout.h
|
||||
* @brief Team page layout (structure only)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
namespace layout
|
||||
{
|
||||
|
||||
lv_obj_t* create_root(lv_obj_t* parent);
|
||||
lv_obj_t* create_header(lv_obj_t* root);
|
||||
lv_obj_t* create_content(lv_obj_t* root);
|
||||
lv_obj_t* create_state_container(lv_obj_t* content);
|
||||
lv_obj_t* create_idle_container(lv_obj_t* parent);
|
||||
lv_obj_t* create_active_container(lv_obj_t* parent);
|
||||
|
||||
} // namespace layout
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @file team_page_styles.cpp
|
||||
* @brief Team page styles
|
||||
*/
|
||||
|
||||
#include "team_page_styles.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
namespace style
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr uint32_t kBtnBg = 0xF4C77A;
|
||||
static constexpr uint32_t kBtnBgFoc = 0xEBA341;
|
||||
static constexpr uint32_t kBtnBorder = 0xEBA341;
|
||||
static constexpr uint32_t kTextMain = 0x202020;
|
||||
|
||||
static lv_style_t s_root;
|
||||
static lv_style_t s_header;
|
||||
static lv_style_t s_content;
|
||||
static lv_style_t s_state_container;
|
||||
static lv_style_t s_section;
|
||||
static lv_style_t s_status;
|
||||
static lv_style_t s_btn_basic;
|
||||
static lv_style_t s_btn_focused;
|
||||
static bool s_inited = false;
|
||||
} // namespace
|
||||
|
||||
void init_once()
|
||||
{
|
||||
if (s_inited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s_inited = true;
|
||||
|
||||
lv_style_init(&s_root);
|
||||
lv_style_set_bg_opa(&s_root, LV_OPA_TRANSP);
|
||||
lv_style_set_border_width(&s_root, 0);
|
||||
lv_style_set_pad_all(&s_root, 0);
|
||||
|
||||
lv_style_init(&s_header);
|
||||
lv_style_set_bg_color(&s_header, lv_color_white());
|
||||
lv_style_set_bg_opa(&s_header, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&s_header, 0);
|
||||
lv_style_set_pad_all(&s_header, 0);
|
||||
|
||||
lv_style_init(&s_content);
|
||||
lv_style_set_bg_opa(&s_content, LV_OPA_TRANSP);
|
||||
lv_style_set_border_width(&s_content, 0);
|
||||
lv_style_set_pad_all(&s_content, 0);
|
||||
lv_style_set_pad_top(&s_content, 3);
|
||||
|
||||
lv_style_init(&s_state_container);
|
||||
lv_style_set_bg_opa(&s_state_container, LV_OPA_TRANSP);
|
||||
lv_style_set_border_width(&s_state_container, 0);
|
||||
lv_style_set_pad_all(&s_state_container, 0);
|
||||
lv_style_set_pad_row(&s_state_container, 3);
|
||||
lv_style_set_pad_column(&s_state_container, 0);
|
||||
|
||||
lv_style_init(&s_section);
|
||||
lv_style_set_bg_opa(&s_section, LV_OPA_TRANSP);
|
||||
lv_style_set_border_width(&s_section, 0);
|
||||
lv_style_set_pad_all(&s_section, 0);
|
||||
lv_style_set_pad_row(&s_section, 3);
|
||||
lv_style_set_pad_column(&s_section, 0);
|
||||
|
||||
lv_style_init(&s_status);
|
||||
lv_style_set_text_color(&s_status, lv_color_hex(kTextMain));
|
||||
lv_style_set_text_align(&s_status, LV_TEXT_ALIGN_CENTER);
|
||||
|
||||
lv_style_init(&s_btn_basic);
|
||||
lv_style_set_bg_color(&s_btn_basic, lv_color_hex(kBtnBg));
|
||||
lv_style_set_bg_opa(&s_btn_basic, LV_OPA_COVER);
|
||||
lv_style_set_text_color(&s_btn_basic, lv_color_hex(kTextMain));
|
||||
lv_style_set_border_width(&s_btn_basic, 1);
|
||||
lv_style_set_border_color(&s_btn_basic, lv_color_hex(kBtnBorder));
|
||||
lv_style_set_radius(&s_btn_basic, 12);
|
||||
|
||||
lv_style_init(&s_btn_focused);
|
||||
lv_style_set_bg_color(&s_btn_focused, lv_color_hex(kBtnBgFoc));
|
||||
lv_style_set_bg_opa(&s_btn_focused, LV_OPA_COVER);
|
||||
}
|
||||
|
||||
void apply_root(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_root, 0);
|
||||
}
|
||||
|
||||
void apply_header(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_header, 0);
|
||||
}
|
||||
|
||||
void apply_content(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_content, 0);
|
||||
}
|
||||
|
||||
void apply_state_container(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_state_container, 0);
|
||||
}
|
||||
|
||||
void apply_idle_container(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_section, 0);
|
||||
}
|
||||
|
||||
void apply_active_container(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_section, 0);
|
||||
}
|
||||
|
||||
void apply_status_label(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_status, 0);
|
||||
}
|
||||
|
||||
void apply_button_primary(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_btn_basic, LV_PART_MAIN);
|
||||
lv_obj_add_style(obj, &s_btn_focused, LV_PART_MAIN | LV_STATE_FOCUSED);
|
||||
}
|
||||
|
||||
void apply_button_secondary(lv_obj_t* obj)
|
||||
{
|
||||
lv_obj_add_style(obj, &s_btn_basic, LV_PART_MAIN);
|
||||
lv_obj_add_style(obj, &s_btn_focused, LV_PART_MAIN | LV_STATE_FOCUSED);
|
||||
}
|
||||
|
||||
} // namespace style
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file team_page_styles.h
|
||||
* @brief Team page styles
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
namespace style
|
||||
{
|
||||
|
||||
void init_once();
|
||||
|
||||
void apply_root(lv_obj_t* obj);
|
||||
void apply_header(lv_obj_t* obj);
|
||||
void apply_content(lv_obj_t* obj);
|
||||
void apply_state_container(lv_obj_t* obj);
|
||||
void apply_idle_container(lv_obj_t* obj);
|
||||
void apply_active_container(lv_obj_t* obj);
|
||||
void apply_status_label(lv_obj_t* obj);
|
||||
void apply_button_primary(lv_obj_t* obj);
|
||||
void apply_button_secondary(lv_obj_t* obj);
|
||||
|
||||
} // namespace style
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file team_state.h
|
||||
* @brief Team page UI state
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include "../../widgets/top_bar.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
|
||||
struct TeamPageState
|
||||
{
|
||||
lv_obj_t* root = nullptr;
|
||||
lv_obj_t* page = nullptr;
|
||||
lv_obj_t* header = nullptr;
|
||||
lv_obj_t* content = nullptr;
|
||||
|
||||
lv_obj_t* state_container = nullptr;
|
||||
lv_obj_t* idle_container = nullptr;
|
||||
lv_obj_t* active_container = nullptr;
|
||||
|
||||
lv_obj_t* idle_status_label = nullptr;
|
||||
lv_obj_t* create_btn = nullptr;
|
||||
lv_obj_t* join_btn = nullptr;
|
||||
|
||||
lv_obj_t* active_status_label = nullptr;
|
||||
lv_obj_t* share_btn = nullptr;
|
||||
lv_obj_t* leave_btn = nullptr;
|
||||
lv_obj_t* disband_btn = nullptr;
|
||||
const char* title_override = nullptr;
|
||||
|
||||
::ui::widgets::TopBar top_bar_widget;
|
||||
lv_group_t* group = nullptr;
|
||||
};
|
||||
|
||||
extern TeamPageState g_team_state;
|
||||
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
@@ -11,6 +11,11 @@
|
||||
static lv_obj_t *chat_container = NULL;
|
||||
static std::unique_ptr<chat::ui::UiController> ui_controller = nullptr;
|
||||
|
||||
lv_obj_t* ui_chat_get_container()
|
||||
{
|
||||
return chat_container;
|
||||
}
|
||||
|
||||
static void back_event_handler(lv_event_t *e)
|
||||
{
|
||||
lv_obj_t *obj = (lv_obj_t *)lv_event_get_target(e);
|
||||
@@ -36,6 +41,13 @@ void ui_chat_enter(lv_obj_t *parent)
|
||||
set_default_group(app_g);
|
||||
}
|
||||
|
||||
Serial.printf("[UI Chat] enter: parent=%p active=%p\n",
|
||||
parent, lv_screen_active());
|
||||
if (lv_obj_t* active = lv_screen_active()) {
|
||||
Serial.printf("[UI Chat] active child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(active));
|
||||
}
|
||||
|
||||
// Create container
|
||||
chat_container = lv_obj_create(parent);
|
||||
lv_obj_set_size(chat_container, LV_PCT(100), LV_PCT(100));
|
||||
@@ -45,6 +57,14 @@ void ui_chat_enter(lv_obj_t *parent)
|
||||
lv_obj_set_style_pad_all(chat_container, 0, 0);
|
||||
lv_obj_set_style_radius(chat_container, 0, 0);
|
||||
|
||||
Serial.printf("[UI Chat] container=%p valid=%d\n",
|
||||
chat_container,
|
||||
chat_container ? (lv_obj_is_valid(chat_container) ? 1 : 0) : 0);
|
||||
if (chat_container) {
|
||||
Serial.printf("[UI Chat] container child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(chat_container));
|
||||
}
|
||||
|
||||
// Create UI controller
|
||||
ui_controller = std::unique_ptr<chat::ui::UiController>(
|
||||
new chat::ui::UiController(chat_container, ctx.getChatService()));
|
||||
@@ -57,6 +77,19 @@ void ui_chat_enter(lv_obj_t *parent)
|
||||
|
||||
void ui_chat_exit(lv_obj_t *parent)
|
||||
{
|
||||
Serial.printf("[UI Chat] exit: parent=%p container=%p\n",
|
||||
parent, chat_container);
|
||||
if (lv_obj_t* active = lv_screen_active()) {
|
||||
Serial.printf("[UI Chat] exit active child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(active));
|
||||
}
|
||||
if (chat_container && lv_obj_is_valid(chat_container)) {
|
||||
Serial.printf("[UI Chat] exit container child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(chat_container));
|
||||
}
|
||||
if (chat_container && !lv_obj_is_valid(chat_container)) {
|
||||
chat_container = NULL;
|
||||
}
|
||||
if (chat_container) {
|
||||
lv_obj_clean(chat_container);
|
||||
lv_obj_del(chat_container);
|
||||
|
||||
+39
-1
@@ -14,9 +14,18 @@ extern "C" void lv_draw_buf_destroy(lv_draw_buf_t* draw_buf);
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
#include <Preferences.h>
|
||||
|
||||
extern TLoRaPagerBoard &instance;
|
||||
|
||||
namespace {
|
||||
constexpr const char* kPrefsNs = "settings_v2";
|
||||
constexpr const char* kTimezoneKey = "timezone_offset";
|
||||
|
||||
static bool s_tz_loaded = false;
|
||||
static int s_tz_offset_min = 0;
|
||||
} // namespace
|
||||
|
||||
void set_default_group(lv_group_t *group)
|
||||
{
|
||||
lv_indev_t *cur_drv = NULL;
|
||||
@@ -40,6 +49,7 @@ void set_default_group(lv_group_t *group)
|
||||
|
||||
void menu_show()
|
||||
{
|
||||
ui_clear_active_app();
|
||||
set_default_group(menu_g);
|
||||
lv_tileview_set_tile_by_index(main_screen, 0, 0, LV_ANIM_ON);
|
||||
}
|
||||
@@ -88,6 +98,33 @@ void ui_update_top_bar_battery(ui::widgets::TopBar& bar)
|
||||
ui::widgets::top_bar_set_right_text(bar, battery_buf);
|
||||
}
|
||||
|
||||
int ui_get_timezone_offset_min()
|
||||
{
|
||||
if (!s_tz_loaded) {
|
||||
Preferences prefs;
|
||||
prefs.begin(kPrefsNs, true);
|
||||
s_tz_offset_min = prefs.getInt(kTimezoneKey, 0);
|
||||
prefs.end();
|
||||
s_tz_loaded = true;
|
||||
}
|
||||
return s_tz_offset_min;
|
||||
}
|
||||
|
||||
void ui_set_timezone_offset_min(int offset_min)
|
||||
{
|
||||
s_tz_offset_min = offset_min;
|
||||
s_tz_loaded = true;
|
||||
}
|
||||
|
||||
time_t ui_apply_timezone_offset(time_t utc_seconds)
|
||||
{
|
||||
if (utc_seconds <= 0) {
|
||||
return utc_seconds;
|
||||
}
|
||||
int offset_min = ui_get_timezone_offset_min();
|
||||
return utc_seconds + static_cast<time_t>(offset_min) * 60;
|
||||
}
|
||||
|
||||
bool ui_take_screenshot_to_sd()
|
||||
{
|
||||
#if LV_USE_SNAPSHOT
|
||||
@@ -128,7 +165,8 @@ bool ui_take_screenshot_to_sd()
|
||||
|
||||
char path[64];
|
||||
time_t now = time(nullptr);
|
||||
struct tm* info = localtime(&now);
|
||||
time_t local = ui_apply_timezone_offset(now);
|
||||
struct tm* info = gmtime(&local);
|
||||
if (info) {
|
||||
char ts[20];
|
||||
strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", info);
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
|
||||
#include "lvgl.h"
|
||||
#include "widgets/top_bar.h"
|
||||
#include <ctime>
|
||||
|
||||
// Forward declarations
|
||||
extern lv_obj_t *main_screen;
|
||||
extern lv_group_t *menu_g;
|
||||
|
||||
void menu_show();
|
||||
void ui_clear_active_app();
|
||||
void set_default_group(lv_group_t *group);
|
||||
|
||||
// Menu creation helper (simplified version of factory's create_menu)
|
||||
@@ -25,6 +27,11 @@ void ui_format_battery(int level, bool charging, char* out, size_t out_len);
|
||||
// Update a shared TopBar's right-side battery text from board state
|
||||
void ui_update_top_bar_battery(ui::widgets::TopBar& bar);
|
||||
|
||||
// Timezone offset (minutes) for display-only local time
|
||||
int ui_get_timezone_offset_min();
|
||||
void ui_set_timezone_offset_min(int offset_min);
|
||||
time_t ui_apply_timezone_offset(time_t utc_seconds);
|
||||
|
||||
// Screenshot helper (BMP, RGB565) saved to /sd
|
||||
bool ui_take_screenshot_to_sd();
|
||||
|
||||
|
||||
+46
-10
@@ -9,6 +9,8 @@
|
||||
#include "ui_common.h"
|
||||
#include <ctime>
|
||||
|
||||
extern bool isScreenSleeping();
|
||||
|
||||
namespace chat
|
||||
{
|
||||
namespace ui
|
||||
@@ -208,8 +210,17 @@ void UiController::onChatEvent(sys::Event* event)
|
||||
case sys::EventType::ChatSendResult:
|
||||
{
|
||||
sys::ChatSendResultEvent* result_event = (sys::ChatSendResultEvent*)event;
|
||||
// Update message status in conversation
|
||||
// (Would need to track message indices)
|
||||
if (state_ == State::Conversation && conversation_)
|
||||
{
|
||||
auto messages = service_.getRecentMessages(current_conv_, 50);
|
||||
conversation_->clearMessages();
|
||||
for (const auto& m : messages)
|
||||
{
|
||||
conversation_->addMessage(m);
|
||||
}
|
||||
conversation_->scrollToBottom();
|
||||
}
|
||||
(void)result_event;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -229,6 +240,16 @@ void UiController::onChatEvent(sys::Event* event)
|
||||
void UiController::switchToChannelList()
|
||||
{
|
||||
state_ = State::ChannelList;
|
||||
Serial.printf("[UiController] switchToChannelList: parent=%p active=%p sleeping=%d\n",
|
||||
parent_, lv_screen_active(), isScreenSleeping() ? 1 : 0);
|
||||
if (lv_obj_t* active = lv_screen_active()) {
|
||||
Serial.printf("[UiController] switchToChannelList active child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(active));
|
||||
}
|
||||
if (parent_) {
|
||||
Serial.printf("[UiController] switchToChannelList parent child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(parent_));
|
||||
}
|
||||
|
||||
if (conversation_)
|
||||
{
|
||||
@@ -255,6 +276,17 @@ void UiController::switchToConversation(chat::ConversationId conv)
|
||||
state_ = State::Conversation;
|
||||
current_channel_ = conv.channel;
|
||||
current_conv_ = conv;
|
||||
Serial.printf("[UiController] switchToConversation: parent=%p active=%p sleeping=%d conv_peer=%08lX\n",
|
||||
parent_, lv_screen_active(), isScreenSleeping() ? 1 : 0,
|
||||
(unsigned long)conv.peer);
|
||||
if (lv_obj_t* active = lv_screen_active()) {
|
||||
Serial.printf("[UiController] switchToConversation active child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(active));
|
||||
}
|
||||
if (parent_) {
|
||||
Serial.printf("[UiController] switchToConversation parent child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(parent_));
|
||||
}
|
||||
|
||||
if (channel_list_)
|
||||
{
|
||||
@@ -318,6 +350,17 @@ void UiController::switchToCompose(chat::ConversationId conv)
|
||||
state_ = State::Compose;
|
||||
current_channel_ = conv.channel;
|
||||
current_conv_ = conv;
|
||||
Serial.printf("[UiController] switchToCompose: parent=%p active=%p sleeping=%d conv_peer=%08lX\n",
|
||||
parent_, lv_screen_active(), isScreenSleeping() ? 1 : 0,
|
||||
(unsigned long)conv.peer);
|
||||
if (lv_obj_t* active = lv_screen_active()) {
|
||||
Serial.printf("[UiController] switchToCompose active child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(active));
|
||||
}
|
||||
if (parent_) {
|
||||
Serial.printf("[UiController] switchToCompose parent child count=%u\n",
|
||||
(unsigned)lv_obj_get_child_cnt(parent_));
|
||||
}
|
||||
|
||||
if (channel_list_)
|
||||
{
|
||||
@@ -422,14 +465,7 @@ void UiController::refreshUnreadCounts()
|
||||
}
|
||||
|
||||
channel_list_->setConversations(convs);
|
||||
for (size_t i = 0; i < convs.size(); ++i)
|
||||
{
|
||||
if (convs[i].id == current_conv_)
|
||||
{
|
||||
channel_list_->setSelected(static_cast<int>(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
channel_list_->setSelectedConversation(current_conv_);
|
||||
|
||||
// Update header status (battery only, with icon)
|
||||
channel_list_->updateBatteryFromBoard();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @file ui_team.cpp
|
||||
* @brief Team page implementation
|
||||
*/
|
||||
|
||||
#include "ui_team.h"
|
||||
#include "screens/team/team_page_components.h"
|
||||
#include "screens/team/team_state.h"
|
||||
#include "ui_common.h"
|
||||
#include "../app/app_context.h"
|
||||
|
||||
namespace team
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
TeamPageState g_team_state;
|
||||
} // namespace ui
|
||||
} // namespace team
|
||||
|
||||
void ui_team_enter(lv_obj_t* parent)
|
||||
{
|
||||
team_page_create(parent);
|
||||
}
|
||||
|
||||
void ui_team_exit(lv_obj_t* parent)
|
||||
{
|
||||
(void)parent;
|
||||
app::AppContext& app_ctx = app::AppContext::getInstance();
|
||||
team::TeamController* controller = app_ctx.getTeamController();
|
||||
if (controller)
|
||||
{
|
||||
controller->resetUiState();
|
||||
}
|
||||
team_page_destroy();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @file ui_team.h
|
||||
* @brief Team page entry point
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
/**
|
||||
* @brief Enter Team page
|
||||
* @param parent Parent object (usually main screen)
|
||||
*/
|
||||
void ui_team_enter(lv_obj_t* parent);
|
||||
|
||||
/**
|
||||
* @brief Exit Team page
|
||||
* @param parent Parent object (can be nullptr)
|
||||
*/
|
||||
void ui_team_exit(lv_obj_t* parent);
|
||||
@@ -97,11 +97,6 @@ void ImeWidget::init(lv_obj_t* parent, lv_obj_t* textarea) {
|
||||
self->handle_key(e);
|
||||
}, LV_EVENT_KEY, this);
|
||||
|
||||
buffer_label_ = lv_label_create(container_);
|
||||
lv_label_set_text(buffer_label_, "EN");
|
||||
lv_obj_set_style_text_font(buffer_label_, &lv_font_noto_cjk_16_2bpp, 0);
|
||||
lv_obj_set_style_text_color(buffer_label_, lv_color_hex(0x404040), 0);
|
||||
|
||||
candidates_label_ = lv_label_create(container_);
|
||||
lv_label_set_text(candidates_label_, "");
|
||||
lv_obj_set_style_text_font(candidates_label_, &lv_font_noto_cjk_16_2bpp, 0);
|
||||
@@ -117,7 +112,6 @@ void ImeWidget::detach() {
|
||||
toggle_btn_ = nullptr;
|
||||
toggle_label_ = nullptr;
|
||||
focus_proxy_ = nullptr;
|
||||
buffer_label_ = nullptr;
|
||||
candidates_label_ = nullptr;
|
||||
textarea_ = nullptr;
|
||||
if (s_active_ime == this) {
|
||||
@@ -169,12 +163,12 @@ bool ImeWidget::handle_key(lv_event_t* e) {
|
||||
}
|
||||
} else if (key == LV_KEY_UP || key == LV_KEY_LEFT) {
|
||||
if (ime_.hasBuffer()) {
|
||||
ime_.moveCandidate(-kCandidatesPerPage);
|
||||
ime_.moveCandidate(-1);
|
||||
consumed = true;
|
||||
}
|
||||
} else if (key == LV_KEY_DOWN || key == LV_KEY_RIGHT) {
|
||||
if (ime_.hasBuffer()) {
|
||||
ime_.moveCandidate(kCandidatesPerPage);
|
||||
ime_.moveCandidate(1);
|
||||
consumed = true;
|
||||
}
|
||||
} else if (key == LV_KEY_ENTER) {
|
||||
@@ -188,13 +182,11 @@ bool ImeWidget::handle_key(lv_event_t* e) {
|
||||
}
|
||||
} else if (key == ' ') {
|
||||
if (ime_.hasBuffer()) {
|
||||
ime_.moveCandidate(1);
|
||||
consumed = true;
|
||||
} else {
|
||||
committed = true;
|
||||
committed_text = " ";
|
||||
consumed = true;
|
||||
ime_.reset();
|
||||
}
|
||||
committed = true;
|
||||
committed_text = " ";
|
||||
consumed = true;
|
||||
} else if (key >= 32 && key <= 126) {
|
||||
char c = static_cast<char>(key);
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
@@ -223,7 +215,7 @@ bool ImeWidget::handle_key(lv_event_t* e) {
|
||||
}
|
||||
|
||||
void ImeWidget::refresh_labels() {
|
||||
if (!toggle_label_ || !buffer_label_ || !candidates_label_) return;
|
||||
if (!toggle_label_ || !candidates_label_) return;
|
||||
|
||||
if (textarea_) {
|
||||
if (mode_ == Mode::CN && ime_.hasBuffer()) {
|
||||
@@ -243,24 +235,16 @@ void ImeWidget::refresh_labels() {
|
||||
|
||||
if (mode_ == Mode::EN) {
|
||||
lv_label_set_text(toggle_label_, "EN");
|
||||
lv_label_set_text(buffer_label_, "EN");
|
||||
lv_label_set_text(candidates_label_, "");
|
||||
return;
|
||||
}
|
||||
if (mode_ == Mode::NUM) {
|
||||
lv_label_set_text(toggle_label_, "123");
|
||||
lv_label_set_text(buffer_label_, "123");
|
||||
lv_label_set_text(candidates_label_, "");
|
||||
return;
|
||||
}
|
||||
|
||||
lv_label_set_text(toggle_label_, "CN");
|
||||
if (ime_.hasBuffer()) {
|
||||
std::string buf = "CN: " + ime_.buffer();
|
||||
lv_label_set_text(buffer_label_, buf.c_str());
|
||||
} else {
|
||||
lv_label_set_text(buffer_label_, "CN");
|
||||
}
|
||||
refresh_candidates();
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ private:
|
||||
lv_obj_t* toggle_btn_ = nullptr;
|
||||
lv_obj_t* toggle_label_ = nullptr;
|
||||
lv_obj_t* focus_proxy_ = nullptr;
|
||||
lv_obj_t* buffer_label_ = nullptr;
|
||||
lv_obj_t* candidates_label_ = nullptr;
|
||||
lv_obj_t* textarea_ = nullptr;
|
||||
Mode mode_ = Mode::EN;
|
||||
|
||||
@@ -24,9 +24,9 @@ public:
|
||||
/**
|
||||
* @brief Show a notification
|
||||
* @param text Message text (will be truncated to 15 characters)
|
||||
* @param duration_ms Display duration in milliseconds (default 5000ms)
|
||||
* @param duration_ms Display duration in milliseconds (default 3000ms)
|
||||
*/
|
||||
static void show(const char* text, uint32_t duration_ms = 5000);
|
||||
static void show(const char* text, uint32_t duration_ms = 3000);
|
||||
|
||||
/**
|
||||
* @brief Hide the current notification
|
||||
|
||||
Reference in New Issue
Block a user