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:
vicliu
2026-01-21 04:36:27 +08:00
committed by GitHub
parent 6327339f90
commit e466060fbd
88 changed files with 7637 additions and 193 deletions
+14
View File
@@ -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
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2845
View File
File diff suppressed because it is too large Load Diff
+85
View File
@@ -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
- 追加写 TeamEventLogRecord
- appendEvent/appendSnapshotsnapshot 可选)
- 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 LogRecordAEAD + 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 统一 Envelopeversion/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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+24 -2
View File
@@ -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:
+16
View File
@@ -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_;
+8 -6
View File
@@ -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;
}
+22
View File
@@ -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
*/
+23 -2
View File
@@ -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
+7 -1
View File
@@ -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
+441 -7
View File
@@ -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)
+17
View File
@@ -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);
+40 -1
View File
@@ -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)
+13
View File
@@ -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
+22
View File
@@ -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);
+1
View File
@@ -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;
+6
View File
@@ -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;
+1
View File
@@ -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_;
+20
View File
@@ -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);
+1
View File
@@ -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
+8
View File
@@ -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
+21
View File
@@ -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
+41
View File
@@ -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
+26 -23
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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
+99
View File
@@ -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
*/
+15
View File
@@ -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
+76
View File
@@ -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
+24
View File
@@ -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
+139
View File
@@ -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
+28
View File
@@ -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
+32
View File
@@ -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
+23
View File
@@ -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
+380
View File
@@ -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(&params->position_interval_ms)) return false;
if (!reader.getU8(&params->precision_level)) return false;
if (!reader.getU32(&params->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
+107
View File
@@ -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
+12
View File
@@ -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
+140
View File
@@ -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
+28
View File
@@ -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
+71
View File
@@ -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
+43
View File
@@ -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
+470
View File
@@ -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
+68
View File
@@ -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
+165
View File
@@ -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);
}
+1 -1
View File
@@ -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);
};
+221 -18
View File
@@ -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{};
+11 -2
View File
@@ -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)
+3 -1
View File
@@ -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();
+117
View File
@@ -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();
}
+10
View File
@@ -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();
+168
View File
@@ -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
+26
View File
@@ -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
+137
View File
@@ -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
+31
View File
@@ -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
+44
View File
@@ -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
+33
View File
@@ -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
View File
@@ -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);
+7
View File
@@ -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
View File
@@ -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();
+35
View File
@@ -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();
}
+20
View File
@@ -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);
+7 -23
View File
@@ -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();
}
-1
View File
@@ -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;
+2 -2
View File
@@ -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