diff --git a/.gitignore b/.gitignore
index c585a068..95324a79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,6 @@ Release/*.zip
Release/*.json
Console/build
build/*
+*.svd
+debug.cfg
+debug_custom.json
diff --git a/Beacon.h b/Beacon.h
new file mode 100644
index 00000000..834da22b
--- /dev/null
+++ b/Beacon.h
@@ -0,0 +1,219 @@
+// Copyright (C) 2026, GPS beacon support contributed by GlassOnTin
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#ifndef BEACON_H
+#define BEACON_H
+
+#if HAS_GPS == true
+
+// Beacon interval and timing
+uint32_t beacon_interval_ms = 30000; // Default 30s, configurable via Settings
+#define BEACON_STARTUP_DELAY_MS 10000 // Wait 10s after boot before first beacon
+// BEACON_NO_HOST_TIMEOUT_MS and last_host_activity defined in GPS.h
+bool beacon_enabled = true; // Configurable via Settings
+
+// Beacon interval options (ms) — indexed by roller selection
+const uint32_t beacon_interval_options[] = { 10000, 30000, 60000, 300000, 600000 };
+#define BEACON_INTERVAL_OPTIONS_COUNT 5
+
+// Beacon radio parameters — must match the router's LoRa interface
+#define BEACON_FREQ 868000000
+#define BEACON_BW 125000
+#define BEACON_SF 7
+#define BEACON_CR 5
+#define BEACON_TXP 17
+
+// Beacon IFAC network — must match rnsd's RNodeInterface config
+#define BEACON_NETWORK_NAME "helv4net"
+#define BEACON_PASSPHRASE "R3ticulum-priv8-m3sh"
+
+// Pre-computed RNS destination hash for PLAIN destination "rnlog.beacon"
+// Computed as: SHA256(SHA256("rnlog.beacon")[:10])[:16]
+const uint8_t RNS_DEST_HASH[16] = {
+ 0x18, 0xbc, 0xd8, 0xa3, 0xde, 0xa1, 0x6e, 0xf6,
+ 0x76, 0x5c, 0x6b, 0x27, 0xd0, 0x08, 0xd2, 0x20
+};
+
+// RNS packet header constants (PLAIN destination, HEADER_1, DATA)
+// FLAGS: header_type=0 (HEADER_1), propagation=0 (BROADCAST),
+// destination=0 (PLAIN), packet_type=2 (DATA), transport=0
+#define RNS_FLAGS 0x08
+#define RNS_CONTEXT 0x00
+
+// Beacon state
+bool beacon_mode_active = false;
+uint32_t last_beacon_tx = 0;
+
+// Forward declarations from main firmware
+void lora_receive();
+bool startRadio();
+void setTXPower();
+void setBandwidth();
+void setSpreadingFactor();
+void setCodingRate();
+void beacon_transmit(uint16_t size);
+
+// Diagnostic: track which gate blocks beacon_update()
+// 0=not called, 1=host active, 2=startup delay, 3=radio offline,
+// 4=no gps fix, 5=interval wait, 6=beacon sent
+uint8_t beacon_gate = 0;
+
+void beacon_check_host_activity() {
+ last_host_activity = millis();
+ if (beacon_mode_active) {
+ beacon_mode_active = false;
+ }
+}
+
+void beacon_update() {
+ if (!beacon_enabled) { beacon_gate = 0; return; }
+ // Don't beacon if host has been active recently
+ if (last_host_activity > 0 &&
+ (millis() - last_host_activity < BEACON_NO_HOST_TIMEOUT_MS)) {
+ beacon_gate = 1;
+ return;
+ }
+
+ // Wait for startup delay after boot
+ if (millis() < BEACON_STARTUP_DELAY_MS) {
+ beacon_gate = 2;
+ return;
+ }
+
+ // No point beaconing without a GPS fix — check BEFORE touching
+ // radio params to avoid putting the radio into standby needlessly
+ if (!gps_has_fix) {
+ beacon_gate = 4;
+ return;
+ }
+
+ // Radio must be online — restart if needed
+ if (!radio_online) {
+ lora_freq = (uint32_t)868000000;
+ lora_bw = (uint32_t)125000;
+ lora_sf = 7;
+ lora_cr = 5;
+ lora_txp = 17;
+ if (!startRadio()) {
+ beacon_gate = 3;
+ return;
+ }
+ } else {
+ // Radio is online but may have host/EEPROM params — force beacon settings
+ lora_freq = (uint32_t)868000000;
+ lora_bw = (uint32_t)125000;
+ lora_sf = 7;
+ lora_cr = 5;
+ lora_txp = 17;
+ setTXPower();
+ setBandwidth();
+ setSpreadingFactor();
+ setCodingRate();
+ }
+
+ // Respect beacon interval
+ if (last_beacon_tx > 0 &&
+ (millis() - last_beacon_tx < beacon_interval_ms)) {
+ beacon_gate = 5;
+ return;
+ }
+
+ beacon_mode_active = true;
+ beacon_gate = 6;
+
+ // LXMF path: send proper LXMF message with FIELD_TELEMETRY directly to Sideband
+ if (lxmf_identity_configured) {
+ // Periodic LXMF announce (every 10 minutes)
+ lxmf_announce_if_needed("RNode GPS Tracker");
+
+ // Get Unix timestamp from GPS directly
+ uint32_t timestamp = (uint32_t)(millis() / 1000);
+ #if HAS_GPS == true
+ {
+ extern TinyGPSPlus gps_parser;
+ if (gps_parser.date.isValid() && gps_parser.time.isValid() && gps_parser.date.year() >= 2024) {
+ uint32_t days = 0;
+ uint16_t yr = gps_parser.date.year();
+ uint8_t mo = gps_parser.date.month();
+ for (uint16_t y = 1970; y < yr; y++)
+ days += (y % 4 == 0) ? 366 : 365;
+ static const uint16_t mdays[] = {0,31,59,90,120,151,181,212,243,273,304,334};
+ if (mo >= 1 && mo <= 12) {
+ days += mdays[mo - 1];
+ if (mo > 2 && (yr % 4 == 0)) days++;
+ }
+ days += gps_parser.date.day() - 1;
+ timestamp = days * 86400UL + gps_parser.time.hour() * 3600UL
+ + gps_parser.time.minute() * 60UL + gps_parser.time.second();
+ }
+ }
+ #endif
+
+ lxmf_beacon_send(gps_lat, gps_lon, gps_alt,
+ gps_speed, gps_hdop,
+ timestamp, (int)battery_percent);
+ last_beacon_tx = millis();
+ return;
+ }
+
+ // Legacy path: JSON payload for rnlog collector (no LXMF identity)
+ char json_buf[256];
+ int json_len = snprintf(json_buf, sizeof(json_buf),
+ "{\"lat\":%.6f,\"lon\":%.6f,\"alt\":%.1f,"
+ "\"sat\":%d,\"spd\":%.1f,\"hdop\":%.1f,"
+ "\"bat\":%d,\"fix\":%s}",
+ gps_lat, gps_lon, gps_alt,
+ gps_sats, gps_speed, gps_hdop,
+ (int)battery_percent,
+ gps_has_fix ? "true" : "false");
+
+ if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) return;
+
+ if (beacon_crypto_configured) {
+ // Encrypted SINGLE packet (legacy JSON to rnlog.collector)
+ tbuf[0] = 0x00; // FLAGS: HEADER_1, BROADCAST, SINGLE, DATA
+ tbuf[1] = 0x00; // HOPS
+ memcpy(&tbuf[2], collector_dest_hash, 16);
+ tbuf[18] = 0x00; // CONTEXT_NONE
+
+ int crypto_len = beacon_crypto_encrypt(
+ (uint8_t*)json_buf, json_len,
+ collector_pub_key, collector_identity_hash,
+ &tbuf[19]
+ );
+
+ if (crypto_len > 0 && (19 + crypto_len) <= (int)MTU) {
+ beacon_transmit(19 + crypto_len);
+ lora_receive();
+ last_beacon_tx = millis();
+ }
+ } else {
+ // Fallback: PLAIN beacon (unencrypted, original behavior)
+ tbuf[0] = RNS_FLAGS; // 0x08 = PLAIN DATA
+ tbuf[1] = 0x00;
+ memcpy(&tbuf[2], RNS_DEST_HASH, 16);
+ tbuf[18] = RNS_CONTEXT;
+
+ memcpy(&tbuf[19], json_buf, json_len);
+ if (19 + json_len <= (int)MTU) {
+ beacon_transmit(19 + json_len);
+ lora_receive();
+ last_beacon_tx = millis();
+ }
+ }
+}
+
+#endif
+#endif
diff --git a/BeaconCrypto.h b/BeaconCrypto.h
new file mode 100644
index 00000000..98c3a2bd
--- /dev/null
+++ b/BeaconCrypto.h
@@ -0,0 +1,236 @@
+// Copyright (C) 2026, GPS beacon encryption contributed by GlassOnTin
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#ifndef BEACON_CRYPTO_H
+#define BEACON_CRYPTO_H
+
+#if HAS_GPS == true
+
+#include "sodium/crypto_scalarmult_curve25519.h"
+#include "mbedtls/aes.h"
+#include "mbedtls/md.h"
+#include "esp_random.h"
+
+// State loaded from EEPROM on boot
+bool beacon_crypto_configured = false;
+uint8_t collector_pub_key[32];
+uint8_t collector_identity_hash[16];
+uint8_t collector_dest_hash[16];
+
+// HMAC-SHA256 (single-shot)
+static int hmac_sha256(const uint8_t *key, size_t key_len,
+ const uint8_t *data, size_t data_len,
+ uint8_t *output) {
+ const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
+ return mbedtls_md_hmac(md_info, key, key_len, data, data_len, output);
+}
+
+// RFC 5869 HKDF-SHA256 with info=b"", output 64 bytes
+//
+// Extract: PRK = HMAC-SHA256(key=salt, data=ikm)
+// Expand: T1 = HMAC-SHA256(PRK, 0x01) [1 byte input]
+// T2 = HMAC-SHA256(PRK, T1 || 0x02) [33 bytes input]
+// output = T1 || T2
+static int rns_hkdf(const uint8_t *ikm, size_t ikm_len,
+ const uint8_t *salt, size_t salt_len,
+ uint8_t *output_64) {
+ uint8_t prk[32];
+ int ret = hmac_sha256(salt, salt_len, ikm, ikm_len, prk);
+ if (ret != 0) return ret;
+
+ // T1 = HMAC-SHA256(PRK, 0x01)
+ uint8_t expand_buf[33];
+ expand_buf[0] = 0x01;
+ ret = hmac_sha256(prk, 32, expand_buf, 1, output_64);
+ if (ret != 0) return ret;
+
+ // T2 = HMAC-SHA256(PRK, T1 || 0x02)
+ memcpy(expand_buf, output_64, 32);
+ expand_buf[32] = 0x02;
+ ret = hmac_sha256(prk, 32, expand_buf, 33, output_64 + 32);
+ return ret;
+}
+
+// PKCS7 pad to 16-byte blocks. Returns padded length, or 0 on error.
+static size_t pkcs7_pad(const uint8_t *input, size_t input_len,
+ uint8_t *output, size_t output_size) {
+ uint8_t pad_val = 16 - (input_len % 16);
+ size_t padded_len = input_len + pad_val;
+ if (padded_len > output_size) return 0;
+ memcpy(output, input, input_len);
+ memset(output + input_len, pad_val, pad_val);
+ return padded_len;
+}
+
+// Encrypt beacon payload for RNS SINGLE destination.
+//
+// Output layout: [ephemeral_pub:32][IV:16][ciphertext:var][HMAC:32]
+// Returns total output length, or -1 on error.
+//
+// Crypto pipeline:
+// 1. Generate ephemeral X25519 keypair (libsodium)
+// 2. ECDH shared secret with collector's public key
+// 3. HKDF-SHA256 → signing_key(32) + encryption_key(32)
+// 4. AES-256-CBC encrypt PKCS7-padded plaintext
+// 5. HMAC-SHA256(signing_key, IV || ciphertext)
+static int beacon_crypto_encrypt(const uint8_t *plaintext, size_t pt_len,
+ const uint8_t *peer_pub,
+ const uint8_t *identity_hash,
+ uint8_t *output) {
+ // 1. Generate ephemeral X25519 keypair
+ uint8_t eph_priv[32];
+ esp_fill_random(eph_priv, 32);
+ // Clamp private key per RFC 7748
+ eph_priv[0] &= 248;
+ eph_priv[31] &= 127;
+ eph_priv[31] |= 64;
+
+ // Compute ephemeral public key and write to output
+ if (crypto_scalarmult_curve25519_base(output, eph_priv) != 0) return -1;
+
+ // 2. ECDH shared secret
+ uint8_t ss_bytes[32];
+ if (crypto_scalarmult_curve25519(ss_bytes, eph_priv, peer_pub) != 0) return -1;
+
+ // 3. HKDF-SHA256: derive signing_key(32) + encryption_key(32)
+ uint8_t derived[64];
+ int ret = rns_hkdf(ss_bytes, 32, identity_hash, 16, derived);
+ if (ret != 0) return -1;
+
+ uint8_t *signing_key = derived; // bytes 0-31
+ uint8_t *encryption_key = derived + 32; // bytes 32-63
+
+ // 4. Random IV
+ uint8_t *iv_pos = output + 32; // after ephemeral pubkey
+ esp_fill_random(iv_pos, 16);
+ uint8_t iv_copy[16];
+ memcpy(iv_copy, iv_pos, 16); // AES-CBC modifies IV in-place
+
+ // 5. PKCS7 pad
+ uint8_t padded[512];
+ size_t padded_len = pkcs7_pad(plaintext, pt_len, padded, sizeof(padded));
+ if (padded_len == 0) return -1;
+
+ // 6. AES-256-CBC encrypt
+ uint8_t *ct_pos = output + 32 + 16; // after ephemeral pubkey + IV
+ mbedtls_aes_context aes;
+ mbedtls_aes_init(&aes);
+ ret = mbedtls_aes_setkey_enc(&aes, encryption_key, 256);
+ if (ret != 0) { mbedtls_aes_free(&aes); return -1; }
+ ret = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, padded_len,
+ iv_copy, padded, ct_pos);
+ mbedtls_aes_free(&aes);
+ if (ret != 0) return -1;
+
+ // 7. HMAC-SHA256(signing_key, IV || ciphertext)
+ uint8_t *hmac_pos = ct_pos + padded_len;
+ const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
+ mbedtls_md_context_t md_ctx;
+ mbedtls_md_init(&md_ctx);
+ ret = mbedtls_md_setup(&md_ctx, md_info, 1); // 1 = use HMAC
+ if (ret != 0) { mbedtls_md_free(&md_ctx); return -1; }
+ ret = mbedtls_md_hmac_starts(&md_ctx, signing_key, 32);
+ if (ret != 0) { mbedtls_md_free(&md_ctx); return -1; }
+ ret = mbedtls_md_hmac_update(&md_ctx, iv_pos, 16);
+ if (ret != 0) { mbedtls_md_free(&md_ctx); return -1; }
+ ret = mbedtls_md_hmac_update(&md_ctx, ct_pos, padded_len);
+ if (ret != 0) { mbedtls_md_free(&md_ctx); return -1; }
+ ret = mbedtls_md_hmac_finish(&md_ctx, hmac_pos);
+ mbedtls_md_free(&md_ctx);
+ if (ret != 0) return -1;
+
+ // Total: ephemeral_pub(32) + IV(16) + ciphertext(padded_len) + HMAC(32)
+ return 32 + 16 + (int)padded_len + 32;
+}
+
+// Decrypt incoming RNS Token-format payload.
+//
+// Input layout: [ephemeral_pub:32][IV:16][ciphertext:var][HMAC:32]
+// Returns plaintext length, or -1 on error (HMAC fail, decrypt fail, etc.)
+//
+// Crypto pipeline (reverse of beacon_crypto_encrypt):
+// 1. Extract ephemeral public key
+// 2. ECDH shared secret with OUR private key
+// 3. HKDF-SHA256 → signing_key(32) + encryption_key(32)
+// 4. Verify HMAC-SHA256(signing_key, IV || ciphertext)
+// 5. AES-256-CBC decrypt
+// 6. PKCS7 unpad
+static int beacon_crypto_decrypt(const uint8_t *input, size_t input_len,
+ const uint8_t *our_x25519_sk,
+ const uint8_t *peer_identity_hash,
+ uint8_t *output, size_t output_cap) {
+ // Minimum: ephemeral(32) + IV(16) + 16-byte block + HMAC(32) = 96
+ if (input_len < 96) return -1;
+
+ const uint8_t *eph_pub = input;
+ const uint8_t *iv_pos = input + 32;
+ size_t ct_len = input_len - 32 - 16 - 32; // remove eph + IV + HMAC
+ const uint8_t *ct_pos = input + 32 + 16;
+ const uint8_t *hmac_in = input + input_len - 32;
+
+ if (ct_len == 0 || ct_len % 16 != 0) return -1;
+ if (ct_len > output_cap) return -1;
+
+ // 1. ECDH shared secret
+ uint8_t ss_bytes[32];
+ if (crypto_scalarmult_curve25519(ss_bytes, our_x25519_sk, eph_pub) != 0) return -1;
+
+ // 2. HKDF-SHA256: derive signing_key(32) + encryption_key(32)
+ uint8_t derived[64];
+ int ret = rns_hkdf(ss_bytes, 32, peer_identity_hash, 16, derived);
+ if (ret != 0) return -1;
+
+ uint8_t *signing_key = derived;
+ uint8_t *encryption_key = derived + 32;
+
+ // 3. Verify HMAC-SHA256(signing_key, IV || ciphertext)
+ uint8_t computed_hmac[32];
+ const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
+ mbedtls_md_context_t md_ctx;
+ mbedtls_md_init(&md_ctx);
+ ret = mbedtls_md_setup(&md_ctx, md_info, 1);
+ if (ret == 0) ret = mbedtls_md_hmac_starts(&md_ctx, signing_key, 32);
+ if (ret == 0) ret = mbedtls_md_hmac_update(&md_ctx, iv_pos, 16);
+ if (ret == 0) ret = mbedtls_md_hmac_update(&md_ctx, ct_pos, ct_len);
+ if (ret == 0) ret = mbedtls_md_hmac_finish(&md_ctx, computed_hmac);
+ mbedtls_md_free(&md_ctx);
+ if (ret != 0) return -1;
+
+ if (memcmp(computed_hmac, hmac_in, 32) != 0) return -2; // HMAC mismatch
+
+ // 4. AES-256-CBC decrypt
+ uint8_t iv_copy[16];
+ memcpy(iv_copy, iv_pos, 16);
+ mbedtls_aes_context aes;
+ mbedtls_aes_init(&aes);
+ ret = mbedtls_aes_setkey_dec(&aes, encryption_key, 256);
+ if (ret != 0) { mbedtls_aes_free(&aes); return -1; }
+ ret = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, ct_len,
+ iv_copy, ct_pos, output);
+ mbedtls_aes_free(&aes);
+ if (ret != 0) return -1;
+
+ // 5. PKCS7 unpad
+ uint8_t pad_val = output[ct_len - 1];
+ if (pad_val == 0 || pad_val > 16) return -3; // invalid padding
+ for (size_t i = 0; i < pad_val; i++) {
+ if (output[ct_len - 1 - i] != pad_val) return -3;
+ }
+
+ return (int)(ct_len - pad_val);
+}
+
+#endif
+#endif
diff --git a/Bluetooth.h b/Bluetooth.h
index 616c4b63..7de704d3 100644
--- a/Bluetooth.h
+++ b/Bluetooth.h
@@ -110,11 +110,12 @@ char bt_devname[11];
display_unblank();
if(event == ESP_SPP_SRV_OPEN_EVT) {
bt_state = BT_STATE_CONNECTED;
- cable_state = CABLE_STATE_DISCONNECTED;
+ cable_state = CABLE_STATE_CONNECTED;
}
-
+
if(event == ESP_SPP_CLOSE_EVT ){
bt_state = BT_STATE_ON;
+ if (data_channel == CHANNEL_BT) data_channel = CHANNEL_USB;
}
}
@@ -306,7 +307,7 @@ char bt_devname[11];
display_unblank();
ble_authenticated = false;
if (bt_state != BT_STATE_PAIRING) { bt_state = BT_STATE_CONNECTED; }
- cable_state = CABLE_STATE_DISCONNECTED;
+ cable_state = CABLE_STATE_CONNECTED;
}
void bt_disconnect_callback(BLEServer *server) {
@@ -315,6 +316,7 @@ char bt_devname[11];
display_unblank();
ble_authenticated = false;
bt_state = BT_STATE_ON;
+ if (data_channel == CHANNEL_BT) data_channel = CHANNEL_USB;
}
bool bt_setup_hw() {
@@ -361,7 +363,7 @@ char bt_devname[11];
uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND;
- uint8_t auth_option = ESP_BLE_ONLY_ACCEPT_SPECIFIED_AUTH_ENABLE;
+ uint8_t auth_option = ESP_BLE_ONLY_ACCEPT_SPECIFIED_AUTH_DISABLE;
uint8_t oob_support = ESP_BLE_OOB_DISABLE;
esp_ble_io_cap_t iocap = ESP_IO_CAP_OUT;
@@ -435,7 +437,7 @@ char bt_devname[11];
if (security.sm == 1 && security.lv >= 3) {
// Serial.println("Auth level success");
bt_state = BT_STATE_CONNECTED;
- cable_state = CABLE_STATE_DISCONNECTED;
+ cable_state = CABLE_STATE_CONNECTED;
connection->disconnect();
bt_disable_pairing();
} else {
@@ -462,7 +464,7 @@ char bt_devname[11];
void bt_connect_callback(uint16_t conn_handle) {
// Serial.println("Connect callback");
bt_state = BT_STATE_CONNECTED;
- cable_state = CABLE_STATE_DISCONNECTED;
+ cable_state = CABLE_STATE_CONNECTED;
BLEConnection* conn = Bluefruit.Connection(conn_handle);
conn->requestPHY(BLE_GAP_PHY_2MBPS);
@@ -475,6 +477,7 @@ char bt_devname[11];
if (reason != BLE_GAP_SEC_STATUS_SUCCESS) {
bt_state = BT_STATE_ON;
}
+ if (data_channel == CHANNEL_BT) data_channel = CHANNEL_USB;
}
void bt_update_passkey() {
diff --git a/Boards.h b/Boards.h
index ef2b8c57..33785170 100644
--- a/Boards.h
+++ b/Boards.h
@@ -70,6 +70,11 @@
#define MODEL_DE 0xDE // Xiao ESP32S3 with Wio-SX1262 module, 433 MHz
#define MODEL_DD 0xDD // Xiao ESP32S3 with Wio-SX1262 module, 868 MHz
+ #define PRODUCT_TWATCH_ULT 0xEC
+ #define BOARD_TWATCH_ULT 0x45
+ #define MODEL_D5 0xD5 // LilyGO T-Watch Ultra, 433 MHz
+ #define MODEL_DA 0xDA // LilyGO T-Watch Ultra, 868 MHz
+
#define PRODUCT_T32_10 0xB2
#define BOARD_LORA32_V1_0 0x39
#define MODEL_BA 0xBA // LilyGO T3 v1.0, 433 MHz
@@ -214,9 +219,15 @@
#define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED
#define CONFIG_OFFSET 0
- #define GPS_BAUD_RATE 9600
- #define PIN_GPS_TX 12
- #define PIN_GPS_RX 34
+ #ifndef GPS_BAUD_RATE
+ #define GPS_BAUD_RATE 9600
+ #endif
+ #ifndef PIN_GPS_TX
+ #define PIN_GPS_TX 12
+ #endif
+ #ifndef PIN_GPS_RX
+ #define PIN_GPS_RX 34
+ #endif
#if BOARD_MODEL == BOARD_GENERIC_ESP32
#define HAS_BLUETOOTH true
@@ -397,6 +408,15 @@
#define HAS_SLEEP true
#define HAS_LORA_PA true
#define HAS_LORA_LNA true
+ #define HAS_GPS true
+ #define PIN_GPS_TX 38
+ #define PIN_GPS_RX 39
+ #define PIN_GPS_EN 34
+ #define PIN_GPS_RST 42
+ #define PIN_GPS_PPS 41
+ #define PIN_GPS_STANDBY 40
+ #define GPS_EN_ACTIVE LOW
+ #define GPS_BAUD_RATE 9600
#define PIN_WAKEUP GPIO_NUM_0
#define WAKEUP_LEVEL 0
#define OCP_TUNED 0x18
@@ -632,6 +652,15 @@
#define HAS_INPUT true
#define HAS_SLEEP false
+
+ #define HAS_GPS true
+ #define PIN_GPS_TX 8
+ #define PIN_GPS_RX 9
+ #define PIN_GPS_PPS 6
+ #define PIN_GPS_STANDBY 7
+ #define GPS_BAUD_RATE 9600
+
+ #define HAS_RTC true
#define PMU_IRQ 40
#define I2C_SCL 41
@@ -706,6 +735,94 @@
#endif
#endif
+ #elif BOARD_MODEL == BOARD_TWATCH_ULT
+ #define IS_ESP32S3 true
+ #define MODEM SX1262
+ #define DIO2_AS_RF_SWITCH true
+ #define HAS_BUSY true
+ #define HAS_TCXO true
+
+ #define HAS_DISPLAY true
+ #define HAS_CONSOLE true
+ #define HAS_WIFI true
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_PMU true
+ #define HAS_NP false
+ #define HAS_SD true
+ #define HAS_EEPROM true
+
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+
+ #define HAS_GPS true
+ #define PIN_GPS_TX 43
+ #define PIN_GPS_RX 44
+ #define PIN_GPS_PPS 13
+ #define GPS_BAUD_RATE 38400
+
+ #define HAS_RTC true
+
+ // I2C bus (shared: PMU, touch, IMU, RTC, haptic, GPIO expander)
+ #define PMU_IRQ 7
+ #define I2C_SCL 2
+ #define I2C_SDA 3
+
+ // LoRa SPI (shared bus with NFC and SD)
+ const int pin_cs = 36;
+ const int pin_reset = 47;
+ const int pin_sclk = 35;
+ const int pin_mosi = 34;
+ const int pin_miso = 33;
+ const int pin_tcxo_enable = -1;
+ const int pin_dio = 14;
+ const int pin_busy = 48;
+
+ // Display (CO5300 QSPI)
+ #define DISP_CS 41
+ #define DISP_RST 37
+ #define DISP_SCK 40
+ #define DISP_D0 38
+ #define DISP_D1 39
+ #define DISP_D2 42
+ #define DISP_D3 45
+ #define DISP_TE 6
+ #define DISP_W 410
+ #define DISP_H 502
+
+ // Touch (CST9217, I2C addr 0x1A)
+ // INT and RST managed via XL9555 GPIO expander
+
+ // IMU (BHI260AP, I2C addr 0x28)
+ #define SENSOR_INT 8
+
+ // RTC (PCF85063A, I2C addr 0x51)
+ #define RTC_INT 1
+
+ // NFC (ST25R3916, shared SPI bus)
+ #define NFC_CS 4
+ #define NFC_INT 5
+
+ // SD card (shared SPI bus)
+ const int SD_MISO = 33;
+ const int SD_MOSI = 34;
+ const int SD_CLK = 35;
+ const int SD_CS = 21;
+
+ // Audio (MAX98357A I2S)
+ #define I2S_BCLK 9
+ #define I2S_WCLK 10
+ #define I2S_DOUT 11
+
+ // Buttons
+ const int pin_btn_usr1 = 0;
+
+ // No discrete LEDs on watch
+ const int pin_led_rx = -1;
+ const int pin_led_tx = -1;
+
#else
#error An unsupported ESP32 board was selected. Cannot compile RNode firmware.
#endif
@@ -928,4 +1045,8 @@
#define NP_M 0.15
#endif
+ #ifndef HAS_GPS
+ #define HAS_GPS false
+ #endif
+
#endif
diff --git a/CO5300.h b/CO5300.h
new file mode 100644
index 00000000..1aa938cb
--- /dev/null
+++ b/CO5300.h
@@ -0,0 +1,394 @@
+// CO5300 QSPI AMOLED Display Driver for T-Watch Ultra
+// 410x502 pixels, 16-bit RGB565, QSPI interface
+// Based on LilyGoLib display implementation (MIT license)
+
+#ifndef CO5300_H
+#define CO5300_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+
+#include
+#include "driver/spi_master.h"
+#include "driver/gpio.h"
+
+#define CO5300_CMD_SLPIN 0x10
+#define CO5300_CMD_SLPOUT 0x11
+#define CO5300_CMD_CASET 0x2A
+#define CO5300_CMD_RASET 0x2B
+#define CO5300_CMD_RAMWR 0x2C
+#define CO5300_CMD_MADCTL 0x36
+#define CO5300_CMD_BRIGHTNESS 0x51
+
+#define CO5300_WIDTH 410
+#define CO5300_HEIGHT 502
+#define CO5300_OFFSET_X 22
+#define CO5300_OFFSET_Y 0
+
+#define CO5300_SPI_HOST SPI3_HOST
+#define CO5300_SPI_FREQ_MHZ 45
+#define CO5300_SEND_BUF_SIZE 16384
+
+// Init command table entry
+typedef struct {
+ uint8_t cmd;
+ uint8_t data[20];
+ uint8_t len; // bit 7 = delay 120ms after command
+} co5300_cmd_t;
+
+static const co5300_cmd_t co5300_init_cmds[] = {
+ {0xFE, {0x00}, 0x01},
+ {0xC4, {0x80}, 0x01},
+ {0x3A, {0x55}, 0x01}, // RGB565
+ {0x35, {0x00}, 0x01}, // Tearing effect on
+ {0x53, {0x20}, 0x01}, // Brightness control enable
+ {0x63, {0xFF}, 0x01},
+ {0x2A, {0x00, 0x16, 0x01, 0xAF}, 0x04}, // Column: 22 to 431
+ {0x2B, {0x00, 0x00, 0x01, 0xF5}, 0x04}, // Row: 0 to 501
+ {0x11, {0}, 0x80}, // Sleep out + delay
+ {0x29, {0}, 0x80}, // Display on + delay
+ {0x51, {0x00}, 0x01}, // Brightness = 0
+};
+#define CO5300_INIT_CMD_COUNT (sizeof(co5300_init_cmds) / sizeof(co5300_init_cmds[0]))
+
+static spi_device_handle_t co5300_spi = NULL;
+static bool co5300_ready = false;
+static uint8_t co5300_brightness = 0;
+
+// Send a command with optional data bytes via QSPI (DMA-based)
+static void co5300_write_cmd(uint8_t cmd, uint8_t *data, uint32_t len) {
+ digitalWrite(DISP_CS, LOW);
+ spi_transaction_t t = {};
+ t.flags = SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR;
+ t.cmd = 0x02;
+ t.addr = cmd << 8;
+ if (len > 0 && data) {
+ t.tx_buffer = data;
+ t.length = 8 * len;
+ }
+ spi_device_transmit(co5300_spi, &t);
+ digitalWrite(DISP_CS, HIGH);
+}
+
+// Set the pixel address window
+static void co5300_set_window(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
+ x1 += CO5300_OFFSET_X;
+ x2 += CO5300_OFFSET_X;
+ y1 += CO5300_OFFSET_Y;
+ y2 += CO5300_OFFSET_Y;
+
+ uint8_t caset[] = {highByte(x1), lowByte(x1), highByte(x2), lowByte(x2)};
+ uint8_t raset[] = {highByte(y1), lowByte(y1), highByte(y2), lowByte(y2)};
+ co5300_write_cmd(CO5300_CMD_CASET, caset, 4);
+ co5300_write_cmd(CO5300_CMD_RASET, raset, 4);
+ co5300_write_cmd(CO5300_CMD_RAMWR, NULL, 0);
+}
+
+// Push pixel data to the display (RGB565, DMA-based, blocking)
+void co5300_push_pixels(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *pixels) {
+ if (!co5300_ready) return;
+
+ co5300_set_window(x, y, x + w - 1, y + h - 1);
+
+ uint32_t total = w * h;
+ uint16_t *p = pixels;
+ bool first = true;
+
+ digitalWrite(DISP_CS, LOW);
+ while (total > 0) {
+ uint32_t chunk = (total > CO5300_SEND_BUF_SIZE) ? CO5300_SEND_BUF_SIZE : total;
+ spi_transaction_ext_t t = {};
+ if (first) {
+ t.base.flags = SPI_TRANS_MODE_QIO;
+ t.base.cmd = 0x32;
+ t.base.addr = 0x002C00;
+ first = false;
+ } else {
+ t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
+ SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
+ t.command_bits = 0;
+ t.address_bits = 0;
+ t.dummy_bits = 0;
+ }
+ t.base.tx_buffer = p;
+ t.base.length = chunk * 16;
+ spi_device_transmit(co5300_spi, (spi_transaction_t *)&t);
+ p += chunk;
+ total -= chunk;
+ }
+ digitalWrite(DISP_CS, HIGH);
+}
+
+// --- Async pixel push (display SPI3 is independent from LoRa/SD SPI) ---
+// Queue all DMA transactions on SPI3 and return immediately.
+// co5300_push_wait() blocks until complete.
+// Safe to run LoRa/SD on the other SPI bus while this runs.
+#define CO5300_MAX_ASYNC_TXNS 14
+static spi_transaction_ext_t co5300_async_txns[CO5300_MAX_ASYNC_TXNS];
+static int co5300_async_pending = 0;
+
+void co5300_push_start(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *pixels) {
+ if (!co5300_ready) return;
+
+ // Window setup — blocking but fast (3 small SPI commands)
+ co5300_set_window(x, y, x + w - 1, y + h - 1);
+
+ uint32_t total = w * h;
+ uint16_t *p = pixels;
+ bool first = true;
+ co5300_async_pending = 0;
+
+ digitalWrite(DISP_CS, LOW);
+ while (total > 0 && co5300_async_pending < CO5300_MAX_ASYNC_TXNS) {
+ uint32_t chunk = (total > CO5300_SEND_BUF_SIZE) ? CO5300_SEND_BUF_SIZE : total;
+ int i = co5300_async_pending;
+ memset(&co5300_async_txns[i], 0, sizeof(spi_transaction_ext_t));
+ if (first) {
+ co5300_async_txns[i].base.flags = SPI_TRANS_MODE_QIO;
+ co5300_async_txns[i].base.cmd = 0x32;
+ co5300_async_txns[i].base.addr = 0x002C00;
+ first = false;
+ } else {
+ co5300_async_txns[i].base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
+ SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
+ co5300_async_txns[i].command_bits = 0;
+ co5300_async_txns[i].address_bits = 0;
+ co5300_async_txns[i].dummy_bits = 0;
+ }
+ co5300_async_txns[i].base.tx_buffer = p;
+ co5300_async_txns[i].base.length = chunk * 16;
+ spi_device_queue_trans(co5300_spi, (spi_transaction_t *)&co5300_async_txns[i], portMAX_DELAY);
+ p += chunk;
+ total -= chunk;
+ co5300_async_pending++;
+ }
+ // DMA now running in background on SPI3 — return immediately
+}
+
+void co5300_push_wait() {
+ spi_transaction_t *rtrans;
+ while (co5300_async_pending > 0) {
+ spi_device_get_trans_result(co5300_spi, &rtrans, portMAX_DELAY);
+ co5300_async_pending--;
+ }
+ digitalWrite(DISP_CS, HIGH);
+}
+
+bool co5300_push_done() {
+ if (co5300_async_pending == 0) return true;
+ spi_transaction_t *rtrans;
+ while (co5300_async_pending > 0) {
+ if (spi_device_get_trans_result(co5300_spi, &rtrans, 0) == ESP_OK) {
+ co5300_async_pending--;
+ } else {
+ return false;
+ }
+ }
+ digitalWrite(DISP_CS, HIGH);
+ return true;
+}
+
+// Fill a rectangle with a solid colour
+void co5300_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
+ if (!co5300_ready) return;
+
+ uint32_t total = w * h;
+ uint32_t buf_size = (total > CO5300_SEND_BUF_SIZE) ? CO5300_SEND_BUF_SIZE : total;
+ uint16_t *buf = (uint16_t *)heap_caps_malloc(buf_size * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
+ if (!buf) {
+ buf = (uint16_t *)malloc(buf_size * 2);
+ if (!buf) return;
+ }
+
+ for (uint32_t i = 0; i < buf_size; i++) buf[i] = color;
+
+ co5300_set_window(x, y, x + w - 1, y + h - 1);
+
+ uint32_t remaining = total;
+ bool first = true;
+ digitalWrite(DISP_CS, LOW);
+ while (remaining > 0) {
+ uint32_t chunk = (remaining > buf_size) ? buf_size : remaining;
+ spi_transaction_ext_t t = {};
+ if (first) {
+ t.base.flags = SPI_TRANS_MODE_QIO;
+ t.base.cmd = 0x32;
+ t.base.addr = 0x002C00;
+ first = false;
+ } else {
+ t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
+ SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
+ t.command_bits = 0;
+ t.address_bits = 0;
+ t.dummy_bits = 0;
+ }
+ t.base.tx_buffer = buf;
+ t.base.length = chunk * 16;
+ spi_device_transmit(co5300_spi, (spi_transaction_t *)&t);
+ remaining -= chunk;
+ }
+ digitalWrite(DISP_CS, HIGH);
+
+ heap_caps_free(buf);
+}
+
+// Clear the entire display to black
+void co5300_clear() {
+ co5300_fill_rect(0, 0, CO5300_WIDTH, CO5300_HEIGHT, 0x0000);
+}
+
+void co5300_set_brightness(uint8_t level) {
+ if (!co5300_ready) return;
+ co5300_brightness = level;
+ co5300_write_cmd(CO5300_CMD_BRIGHTNESS, &level, 1);
+}
+
+void co5300_sleep() {
+ if (!co5300_ready) return;
+ co5300_write_cmd(CO5300_CMD_SLPIN, NULL, 0);
+}
+
+void co5300_wakeup() {
+ if (!co5300_ready) return;
+ co5300_write_cmd(CO5300_CMD_SLPOUT, NULL, 0);
+ delay(120);
+}
+
+bool co5300_init() {
+ pinMode(DISP_CS, OUTPUT);
+ digitalWrite(DISP_CS, HIGH);
+
+ // Hardware reset on GPIO 37 (direct pin, not XL9555 expander)
+ pinMode(DISP_RST, OUTPUT);
+ digitalWrite(DISP_RST, HIGH);
+ delay(200);
+ digitalWrite(DISP_RST, LOW);
+ delay(300);
+ digitalWrite(DISP_RST, HIGH);
+ delay(200);
+
+ // Configure QSPI bus
+ spi_bus_config_t bus_cfg = {};
+ bus_cfg.data0_io_num = DISP_D0;
+ bus_cfg.data1_io_num = DISP_D1;
+ bus_cfg.sclk_io_num = DISP_SCK;
+ bus_cfg.data2_io_num = DISP_D2;
+ bus_cfg.data3_io_num = DISP_D3;
+ bus_cfg.data4_io_num = -1;
+ bus_cfg.data5_io_num = -1;
+ bus_cfg.data6_io_num = -1;
+ bus_cfg.data7_io_num = -1;
+ bus_cfg.max_transfer_sz = 0x40000 + 8;
+ bus_cfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS;
+
+ spi_device_interface_config_t dev_cfg = {};
+ dev_cfg.command_bits = 8;
+ dev_cfg.address_bits = 24;
+ dev_cfg.mode = SPI_MODE0;
+ dev_cfg.clock_speed_hz = CO5300_SPI_FREQ_MHZ * 1000 * 1000;
+ dev_cfg.spics_io_num = -1; // CS managed manually
+ dev_cfg.flags = SPI_DEVICE_HALFDUPLEX;
+ dev_cfg.queue_size = 17;
+
+ if (spi_bus_initialize(CO5300_SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO) != ESP_OK) {
+ return false;
+ }
+ if (spi_bus_add_device(CO5300_SPI_HOST, &dev_cfg, &co5300_spi) != ESP_OK) {
+ return false;
+ }
+
+ // Send init sequence (twice, per LilyGoLib pattern)
+ for (int pass = 0; pass < 2; pass++) {
+ for (uint32_t i = 0; i < CO5300_INIT_CMD_COUNT; i++) {
+ co5300_write_cmd(co5300_init_cmds[i].cmd,
+ (uint8_t *)co5300_init_cmds[i].data,
+ co5300_init_cmds[i].len & 0x1F);
+ if (co5300_init_cmds[i].len & 0x80) {
+ delay(120);
+ }
+ }
+ }
+
+ // Set rotation to portrait (default for watch)
+ uint8_t madctl = 0x00; // RGB order, no mirror/swap
+ co5300_write_cmd(CO5300_CMD_MADCTL, &madctl, 1);
+
+ co5300_ready = true;
+
+ // Clear screen to black
+ co5300_clear();
+
+ // Set initial brightness
+ co5300_set_brightness(128);
+
+ return true;
+}
+
+void co5300_end() {
+ if (co5300_spi) {
+ spi_bus_remove_device(co5300_spi);
+ spi_bus_free(CO5300_SPI_HOST);
+ co5300_spi = NULL;
+ }
+ co5300_ready = false;
+}
+
+// ---- Simple text rendering using Adafruit GFX fonts ----
+// Only available when HAS_DISPLAY is true (Display.h includes Adafruit_GFX.h)
+
+#if HAS_DISPLAY
+
+// Draw a single character using Adafruit GFX font at a given position
+// Returns the advance width in pixels
+static uint16_t co5300_draw_char(uint16_t *fb, uint16_t fb_w, uint16_t fb_h,
+ int16_t cx, int16_t cy, char c,
+ uint16_t fg, const GFXfont *font) {
+ if (c < font->first || c > font->last) return 0;
+ GFXglyph *glyph = &font->glyph[c - font->first];
+ uint8_t *bitmap = font->bitmap;
+ uint16_t bo = glyph->bitmapOffset;
+ uint8_t gw = glyph->width, gh = glyph->height;
+ int8_t xo = glyph->xOffset, yo = glyph->yOffset;
+ uint8_t bits = 0, bit = 0;
+
+ for (int16_t yy = 0; yy < gh; yy++) {
+ for (int16_t xx = 0; xx < gw; xx++) {
+ if (!(bit++ & 7)) bits = bitmap[bo++];
+ if (bits & 0x80) {
+ int16_t px = cx + xo + xx;
+ int16_t py = cy + yo + yy;
+ if (px >= 0 && px < fb_w && py >= 0 && py < fb_h) {
+ fb[py * fb_w + px] = fg;
+ }
+ }
+ bits <<= 1;
+ }
+ }
+ return glyph->xAdvance;
+}
+
+// Draw a string using Adafruit GFX font, returns total width
+uint16_t co5300_draw_string(uint16_t *fb, uint16_t fb_w, uint16_t fb_h,
+ int16_t x, int16_t y, const char *str,
+ uint16_t fg, const GFXfont *font) {
+ int16_t cx = x;
+ while (*str) {
+ cx += co5300_draw_char(fb, fb_w, fb_h, cx, y, *str, fg, font);
+ str++;
+ }
+ return cx - x;
+}
+
+// RGB565 colour helpers
+#define CO5300_BLACK 0x0000
+#define CO5300_WHITE 0xFFFF
+#define CO5300_RED 0xF800
+#define CO5300_GREEN 0x07E0
+#define CO5300_BLUE 0x001F
+#define CO5300_CYAN 0x07FF
+#define CO5300_YELLOW 0xFFE0
+#define CO5300_GREY 0x7BEF
+
+#endif // HAS_DISPLAY
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT
+#endif // CO5300_H
diff --git a/Config.h b/Config.h
index dec00639..dee31681 100644
--- a/Config.h
+++ b/Config.h
@@ -164,8 +164,9 @@
// Incoming packet buffer
uint8_t pbuf[MTU];
- // KISS command buffer
- uint8_t cmdbuf[CMD_L];
+ // Response routing globals (channel 0 = USB)
+ volatile uint8_t response_channel = 0;
+ uint8_t data_channel = 0;
// LoRa transmit buffer
uint8_t tbuf[MTU];
diff --git a/DRV2605.h b/DRV2605.h
new file mode 100644
index 00000000..67e2a03f
--- /dev/null
+++ b/DRV2605.h
@@ -0,0 +1,136 @@
+// DRV2605 Haptic Driver — Minimal I2C driver for T-Watch Ultra
+// ERM vibration motor with 117 built-in effects
+// EN pin controlled by XL9555 EXPANDS_DRV_EN (port 0, pin 6)
+
+#ifndef DRV2605_H
+#define DRV2605_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+
+#include
+
+#define DRV2605_ADDR 0x5A
+
+// Registers
+#define DRV2605_STATUS 0x00
+#define DRV2605_MODE 0x01
+#define DRV2605_RTPIN 0x02
+#define DRV2605_LIBRARY 0x03
+#define DRV2605_WAVESEQ1 0x04
+#define DRV2605_GO 0x0C
+#define DRV2605_OVERDRIVE 0x0D
+#define DRV2605_SUSTAINPOS 0x0E
+#define DRV2605_SUSTAINNEG 0x0F
+#define DRV2605_BRAKE 0x10
+#define DRV2605_FEEDBACK 0x1A
+#define DRV2605_CONTROL3 0x1D
+
+// Named effects for watch use cases (ERM Library 1, effects 1-117)
+#define HAPTIC_STRONG_CLICK 1 // Strong Click - 100%
+#define HAPTIC_MEDIUM_CLICK 2 // Strong Click - 60%
+#define HAPTIC_LIGHT_CLICK 3 // Strong Click - 30%
+#define HAPTIC_SHARP_CLICK 4 // Sharp Click - 100%
+#define HAPTIC_SOFT_BUMP 7 // Soft Bump - 100%
+#define HAPTIC_DOUBLE_CLICK 10 // Double Click - 100%
+#define HAPTIC_TRIPLE_CLICK 12 // Triple Click - 100%
+#define HAPTIC_BUZZ 14 // Strong Buzz - 100%
+#define HAPTIC_ALERT 15 // 750ms Alert - 100%
+#define HAPTIC_LONG_ALERT 16 // 1000ms Alert - 100%
+#define HAPTIC_TICK 4 // Sharp Click (subtle tick)
+#define HAPTIC_TRANSITION 47 // Transition Click - 100%
+
+static bool drv2605_ready = false;
+
+static void drv2605_write(uint8_t reg, uint8_t val) {
+ Wire.beginTransmission(DRV2605_ADDR);
+ Wire.write(reg);
+ Wire.write(val);
+ Wire.endTransmission();
+}
+
+static uint8_t drv2605_read(uint8_t reg) {
+ Wire.beginTransmission(DRV2605_ADDR);
+ Wire.write(reg);
+ Wire.endTransmission(false);
+ Wire.requestFrom((uint8_t)DRV2605_ADDR, (uint8_t)1);
+ return Wire.available() ? Wire.read() : 0;
+}
+
+bool drv2605_init() {
+ // Probe device
+ Wire.beginTransmission(DRV2605_ADDR);
+ if (Wire.endTransmission() != 0) return false;
+
+ // Verify chip ID (bits 7:5 of STATUS should be 3 or 7)
+ uint8_t id = drv2605_read(DRV2605_STATUS) >> 5;
+ if (id != 3 && id != 7) return false;
+
+ // Exit standby
+ drv2605_write(DRV2605_MODE, 0x00);
+
+ // Disable real-time playback input
+ drv2605_write(DRV2605_RTPIN, 0x00);
+
+ // Select ERM mode: clear bit 7 of FEEDBACK register
+ uint8_t fb = drv2605_read(DRV2605_FEEDBACK);
+ drv2605_write(DRV2605_FEEDBACK, fb & 0x7F);
+
+ // Enable open-loop drive: set bit 5 of CONTROL3
+ uint8_t ctrl3 = drv2605_read(DRV2605_CONTROL3);
+ drv2605_write(DRV2605_CONTROL3, ctrl3 | 0x20);
+
+ // Select ERM effect library 1
+ drv2605_write(DRV2605_LIBRARY, 1);
+
+ // Clear timing offsets
+ drv2605_write(DRV2605_OVERDRIVE, 0);
+ drv2605_write(DRV2605_SUSTAINPOS, 0);
+ drv2605_write(DRV2605_SUSTAINNEG, 0);
+ drv2605_write(DRV2605_BRAKE, 0);
+
+ // Clear all waveform slots
+ for (uint8_t i = 0; i < 8; i++) {
+ drv2605_write(DRV2605_WAVESEQ1 + i, 0);
+ }
+
+ drv2605_ready = true;
+ return true;
+}
+
+// Play a single effect (1-117 from ERM library)
+void drv2605_play(uint8_t effect) {
+ if (!drv2605_ready) return;
+ drv2605_write(DRV2605_MODE, 0x00); // Internal trigger mode
+ drv2605_write(DRV2605_WAVESEQ1, effect); // Effect in slot 1
+ drv2605_write(DRV2605_WAVESEQ1 + 1, 0); // End sequence
+ drv2605_write(DRV2605_GO, 1); // Start playback
+}
+
+// Play a sequence of up to 8 effects
+void drv2605_sequence(const uint8_t *effects, uint8_t count) {
+ if (!drv2605_ready || count == 0) return;
+ if (count > 8) count = 8;
+ drv2605_write(DRV2605_MODE, 0x00);
+ for (uint8_t i = 0; i < count; i++) {
+ drv2605_write(DRV2605_WAVESEQ1 + i, effects[i]);
+ }
+ if (count < 8) {
+ drv2605_write(DRV2605_WAVESEQ1 + count, 0); // Terminate
+ }
+ drv2605_write(DRV2605_GO, 1);
+}
+
+// Stop any playing effect
+void drv2605_stop() {
+ if (!drv2605_ready) return;
+ drv2605_write(DRV2605_GO, 0);
+}
+
+// Check if an effect is still playing
+bool drv2605_busy() {
+ if (!drv2605_ready) return false;
+ return drv2605_read(DRV2605_GO) & 1;
+}
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT
+#endif // DRV2605_H
diff --git a/Device.h b/Device.h
index e2a1d068..99061fb0 100644
--- a/Device.h
+++ b/Device.h
@@ -83,7 +83,12 @@ bool fw_signature_validated = true;
#define dev_fwhash_addr(a) (a+DEV_FWHASH_OFFSET)
bool device_signatures_ok() {
- return dev_signature_validated && fw_signature_validated;
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ // T-Watch development: skip signature validation for self-signed devices
+ return true;
+ #else
+ return dev_signature_validated && fw_signature_validated;
+ #endif
}
void device_validate_signature() {
@@ -215,7 +220,12 @@ bool device_firmware_ok() {
#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
bool device_init() {
+ #if BOARD_MODEL == BOARD_TWATCH_ULT || BOARD_MODEL == BOARD_TBEAM_S_V1
+ // Dev boards: proceed even if BLE not ready (warm reset race)
+ if (true) {
+ #else
if (bt_ready) {
+ #endif
#if MCU_VARIANT == MCU_ESP32
for (uint8_t i=0; i.
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+ // T-Watch Ultra — LVGL GUI on CO5300 QSPI AMOLED (410x502)
+ #include
+ #include "Fonts/Org_01.h"
+ #include "XL9555.h"
+ #include "CO5300.h"
+
+ // Stubs for variables/functions referenced by other firmware modules
+ bool disp_ext_fb = false;
+ bool display_tx = false;
+ bool recondition_display = false;
+ void ext_fb_enable() { }
+ void ext_fb_disable() { }
+ uint8_t fb[0];
+
+ bool display_blanked = false;
+ uint32_t last_unblank_event = 0;
+ uint32_t display_blanking_timeout = 0;
+
+ // LVGL GUI (must come after variable declarations above)
+ #include "Gui.h"
+
+ void display_unblank() {
+ if (display_blanked) {
+ co5300_wakeup();
+ xl9555_set(EXPANDS_DISP_EN, true);
+ co5300_set_brightness(128);
+ display_blanked = false;
+ if (gui_screen) lv_obj_invalidate(gui_screen);
+ }
+ last_unblank_event = millis();
+ }
+
+ bool display_init() {
+ if (!co5300_init()) return false;
+ co5300_set_brightness(128);
+
+ display_blanking_enabled = true;
+ display_blanking_timeout = 10000;
+
+ if (!gui_init()) return false;
+ return true;
+ }
+
+ void update_display(bool force = false) {
+ if (!co5300_ready) return;
+
+ if (display_blanking_enabled && !display_blanked) {
+ if (millis() - last_unblank_event > display_blanking_timeout) {
+ // Return to watch face before blanking so it's ready on wake
+ if (gui_tileview && gui_tile_watch) {
+ lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_OFF);
+ }
+ co5300_set_brightness(0);
+ co5300_sleep();
+ xl9555_set(EXPANDS_DISP_EN, false);
+ display_blanked = true;
+ return;
+ }
+ }
+ if (display_blanked && !force) return;
+
+ display_updating = true;
+ gui_update();
+ display_updating = false;
+ }
+
+#else
+// Original display path for all other boards
#include "Graphics.h"
#include
@@ -783,6 +852,23 @@ void draw_stat_area() {
if (radio_online) {
draw_waterfall(27, 4);
}
+
+ #if HAS_GPS == true
+ stat_area.setFont(SMALL_FONT);
+ stat_area.setTextSize(1);
+ stat_area.setTextWrap(false);
+ if (gps_has_fix) {
+ stat_area.setTextColor(SSD1306_WHITE);
+ stat_area.fillRect(22, 48, 20, 7, SSD1306_BLACK);
+ stat_area.setCursor(22, 54);
+ stat_area.printf("%ds", gps_sats);
+ } else if (gps_ready) {
+ stat_area.setTextColor(SSD1306_WHITE);
+ stat_area.fillRect(22, 48, 20, 7, SSD1306_BLACK);
+ stat_area.setCursor(22, 54);
+ stat_area.print("gps");
+ }
+ #endif
}
}
@@ -810,7 +896,11 @@ void update_stat_area() {
}
#define START_PAGE 0
-const uint8_t pages = 3;
+#if HAS_GPS == true
+ const uint8_t pages = 6; // 2 diagnostics + 3 GPS + 1 LXMF status
+#else
+ const uint8_t pages = 3;
+#endif
uint8_t disp_page = START_PAGE;
extern char bt_devname[11];
extern char bt_dh[16];
@@ -829,6 +919,127 @@ void draw_disp_area() {
} else {
if (!disp_ext_fb or bt_ssp_pin != 0) {
if (radio_online && display_diagnostics) {
+ if (millis()-last_page_flip >= page_interval) {
+ disp_page = (++disp_page%pages);
+ last_page_flip = millis();
+ }
+
+ #if HAS_GPS == true
+ if (disp_page >= 2 && disp_page <= 4) {
+ // GPS page (pages 2-4, diagnostics on pages 0-1)
+ disp_area.fillRect(0,8,disp_area.width(),56, SSD1306_BLACK);
+ disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1);
+
+ #if HAS_RTC == true
+ if (rtc_ready && rtc_synced) {
+ disp_area.setCursor(2, 13);
+ disp_area.printf("%02d:%02d:%02d", rtc_hour, rtc_minute, rtc_second);
+ disp_area.setCursor(56, 13);
+ disp_area.printf("%02d/%02d/%02d", rtc_day, rtc_month, rtc_year % 100);
+ } else {
+ #endif
+ disp_area.setCursor(2, 13);
+ disp_area.print("On");
+ disp_area.setCursor(14, 13);
+ disp_area.print("@");
+ disp_area.setCursor(21, 13);
+ disp_area.printf("%.1fKbps", (float)lora_bitrate/1000.0);
+ #if HAS_RTC == true
+ }
+ #endif
+
+ // Provisioning flash overlay (3 seconds)
+ if (lxmf_provisioned_at > 0 && (millis() - lxmf_provisioned_at < 3000)) {
+ disp_area.setCursor(2, 27);
+ disp_area.print("PROVISIONED");
+ disp_area.setCursor(2, 39);
+ disp_area.print("");
+ disp_area.setCursor(2, 51);
+ disp_area.print("");
+ } else {
+ if (lxmf_provisioned_at > 0 && (millis() - lxmf_provisioned_at >= 3000)) {
+ lxmf_provisioned_at = 0; // Clear after display
+ }
+
+ if (gps_has_fix) {
+ disp_area.setCursor(2, 27);
+ disp_area.printf("%.5f", gps_lat);
+ disp_area.setCursor(2, 39);
+ disp_area.printf("%.5f", gps_lon);
+ disp_area.setCursor(2, 51);
+ if (beacon_mode_active) {
+ if (lxmf_identity_configured && beacon_crypto_configured) {
+ disp_area.printf("%dsat %.0fm LX", gps_sats, gps_alt);
+ } else {
+ disp_area.printf("%dsat %.0fm BCN", gps_sats, gps_alt);
+ }
+ } else {
+ disp_area.printf("%dsat %.0fm", gps_sats, gps_alt);
+ }
+ } else if (gps_ready) {
+ disp_area.setCursor(2, 27);
+ disp_area.print("GPS searching");
+ disp_area.setCursor(2, 39);
+ disp_area.printf("%d sats", gps_sats);
+ #if HAS_RTC == true
+ if (rtc_ready && rtc_synced) {
+ disp_area.setCursor(2, 51);
+ disp_area.printf("%02d:%02d:%02d UTC", rtc_hour, rtc_minute, rtc_second);
+ }
+ #endif
+ } else {
+ disp_area.setCursor(2, 27);
+ disp_area.print("GPS starting");
+ }
+ }
+ } else if (disp_page == 5) {
+ // LXMF status page
+ disp_area.fillRect(0,8,disp_area.width(),56, SSD1306_BLACK);
+ disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1);
+
+ disp_area.setCursor(2, 13);
+ if (lxmf_identity_configured) {
+ disp_area.print("LXMF Identity");
+ } else {
+ disp_area.print("LXMF: no identity");
+ }
+
+ disp_area.setCursor(2, 27);
+ if (lxmf_identity_configured) {
+ char hash_str[17];
+ for (int i = 0; i < 8; i++) {
+ sprintf(&hash_str[i*2], "%02x", lxmf_source_hash[i]);
+ }
+ hash_str[16] = '\0';
+ disp_area.print(hash_str);
+ }
+
+ disp_area.setCursor(2, 39);
+ if (beacon_crypto_configured) {
+ char tgt_str[24];
+ snprintf(tgt_str, sizeof(tgt_str), "Tgt: %02x%02x%02x%02x",
+ collector_dest_hash[0], collector_dest_hash[1],
+ collector_dest_hash[2], collector_dest_hash[3]);
+ disp_area.print(tgt_str);
+ } else {
+ disp_area.print("Tgt: none");
+ }
+
+ disp_area.setCursor(2, 51);
+ if (lxmf_identity_configured) {
+ uint32_t ann_ago = (lxmf_last_announce > 0) ? (millis() - lxmf_last_announce) / 60000 : 0;
+ uint32_t bcn_ago = (last_beacon_tx > 0) ? (millis() - last_beacon_tx) / 1000 : 0;
+ char timing_str[24];
+ if (lxmf_last_announce > 0) {
+ snprintf(timing_str, sizeof(timing_str), "Ann:%lum Bcn:%lus", (unsigned long)ann_ago, (unsigned long)bcn_ago);
+ } else {
+ snprintf(timing_str, sizeof(timing_str), "Ann:-- Bcn:--");
+ }
+ disp_area.print(timing_str);
+ }
+ } else {
+ #endif
+ // Diagnostics page (original)
disp_area.fillRect(0,8,disp_area.width(),37, SSD1306_BLACK); disp_area.fillRect(0,37,disp_area.width(),27, SSD1306_WHITE);
disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1);
@@ -842,7 +1053,7 @@ void draw_disp_area() {
//disp_area.setCursor(31, 23-1);
disp_area.setCursor(2, 23-1);
disp_area.print("Airtime:");
-
+
disp_area.setCursor(11, 33-1);
if (total_channel_util < 0.099) {
//disp_area.printf("%.1f%%", total_channel_util*100.0);
@@ -869,7 +1080,7 @@ void draw_disp_area() {
disp_area.print("Channel");
disp_area.setCursor(38, 46);
disp_area.print("Load:");
-
+
disp_area.setCursor(11, 57);
if (total_channel_util < 0.099) {
//disp_area.printf("%.1f%%", airtime*100.0);
@@ -889,6 +1100,9 @@ void draw_disp_area() {
disp_area.printf("%.0f%%", longterm_channel_util*100.0);
}
disp_area.drawBitmap(32+2, 50, bm_hg_high, 5, 9, SSD1306_BLACK, SSD1306_WHITE);
+ #if HAS_GPS == true
+ }
+ #endif
} else {
if (device_signatures_ok()) { disp_area.drawBitmap(0, 0, bm_def_lc, disp_area.width(), 23, SSD1306_WHITE, SSD1306_BLACK); }
@@ -1137,3 +1351,5 @@ void ext_fb_enable() {
void ext_fb_disable() {
disp_ext_fb = false;
}
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT (display path selector)
diff --git a/Documentation/BEACON.md b/Documentation/BEACON.md
new file mode 100644
index 00000000..bd723105
--- /dev/null
+++ b/Documentation/BEACON.md
@@ -0,0 +1,337 @@
+# GPS Beacon Mode
+
+RNode devices with GPS hardware (e.g. T-Beam Supreme, Heltec V4) can autonomously transmit GPS position beacons over LoRa when no host computer is connected. This is useful for vehicle tracking, field asset monitoring, or any scenario where the RNode operates standalone on battery power.
+
+## Quick start
+
+Minimum steps to get a beacon transmitting and a receiver collecting data:
+
+```
+1. Build & flash firmware make firmware-tbeam_supreme && make upload-tbeam_supreme
+2. Install rnlog cd rns-collector && pip install .
+3. Provision collector key rnlog provision-lxmf --dest --port /dev/ttyACM0
+4. Provision IFAC key rnlog provision-ifac --name helv4net --passphrase 'R3ticulum-priv8-m3sh' --port /dev/ttyACM0
+5. Start receiver rnlog serve
+6. Disconnect USB & wait RNode enters beacon mode after 15s of no host activity
+```
+
+The rest of this document explains each step, the beacon modes, and how to verify things are working.
+
+## How it works
+
+When no KISS host activity is detected for 15 seconds, the RNode enters beacon mode and transmits a GPS position every 30 seconds. When a host reconnects (e.g. laptop running `rnsd`), beaconing stops automatically and normal RNode operation resumes.
+
+## Beacon modes
+
+There are two beacon paths. The firmware selects automatically based on what has been provisioned.
+
+### LXMF beacon (recommended)
+
+When an LXMF identity and collector key are provisioned, the RNode:
+
+1. Sends RNS **announce** packets (so the receiver learns the device's identity)
+2. Sends **LXMF messages** with `FIELD_TELEMETRY` containing lat, lon, alt, speed, battery
+3. Messages are encrypted per-packet with X25519 ECDH + AES-256-CBC + HMAC
+
+These are proper Reticulum announces and LXMF messages — the receiver can be `rnsd`, Sideband, or any LXMF-aware application.
+
+If IFAC is also provisioned, all packets are tagged with an authentication code before transmission. Receivers with matching `network_name`/`passphrase` accept the packets; others silently drop them.
+
+### Legacy JSON beacon
+
+Without LXMF provisioning, the RNode falls back to sending raw JSON payloads:
+
+```json
+{"lat":51.507400,"lon":-0.127800,"alt":15.0,"sat":8,"spd":0.5,"hdop":1.2,"bat":87,"fix":true}
+```
+
+- **Plaintext**: Sent to well-known PLAIN destination `rnlog.beacon`. Zero configuration needed, but anyone in range can read it.
+- **Encrypted**: When provisioned with `rnlog provision` key, sent as SINGLE packets. Only the matching collector can decrypt.
+
+## Hardware
+
+### Tested boards
+
+| Board | GPS | LoRa | Notes |
+|-------|-----|------|-------|
+| LilyGO T-Beam Supreme S3 | L76K (UART) | SX1262 | Best option. RTC, large battery connector |
+| Heltec LoRa32 V4 | External (UART) | SX1262 | Needs external GPS module |
+
+### Wiring (T-Beam Supreme)
+
+No external wiring needed — GPS and LoRa are integrated. Connect via USB-C for provisioning and flashing.
+
+## Building firmware
+
+### Prerequisites
+
+- Arduino CLI (installed by `make prep-esp32`)
+- Python 3 with `pyserial`
+- USB cable to the device
+
+### Build and flash
+
+```sh
+cd RNode_Firmware
+
+# First time: install Arduino cores and libraries
+make prep-esp32
+
+# Build
+make firmware-tbeam_supreme
+
+# Flash (device must be connected via USB)
+make upload-tbeam_supreme PORT=/dev/ttyACM0
+```
+
+Other board targets: `firmware-tbeam`, `firmware-heltec32_v4`, `firmware-lora32_v21`, etc. Run `make` with no arguments to see available targets.
+
+## Provisioning
+
+Provisioning configures the RNode with cryptographic keys over USB. Keys are stored in NVS (ESP32) or EEPROM and persist across power cycles. You only need to do this once per device.
+
+There are three independent provisioning steps. Each is optional but recommended:
+
+### 1. LXMF identity + collector key
+
+This gives the RNode an LXMF identity (Ed25519 keypair) and tells it where to send telemetry.
+
+```sh
+# Install rnlog if not already done
+cd rns-collector && pip install .
+
+# Provision (requires rnsd running with a path to the Sideband destination)
+rnlog provision-lxmf --dest --port /dev/ttyACM0
+```
+
+What this does:
+- Resolves the Sideband destination hash via Reticulum to get its public key
+- Sends the key material to the RNode via KISS `CMD_BCN_KEY` (0x86)
+- The RNode generates an Ed25519 identity (stored in NVS) and prints its source hash
+- Reads back the RNode's LXMF source hash via `CMD_LXMF_HASH` (0x87)
+
+After provisioning, the OLED display shows an "LXMF Identity" page (page 5) with the source hash and target info.
+
+### 2. IFAC authentication
+
+If your receiver's RNodeInterface uses `network_name` and `passphrase` (IFAC), the beacon must send matching authentication tags or packets will be silently dropped.
+
+```sh
+rnlog provision-ifac \
+ --name helv4net \
+ --passphrase 'R3ticulum-priv8-m3sh' \
+ --port /dev/ttyACM0
+```
+
+The `--name` and `--passphrase` must match the receiver's Reticulum config:
+
+```ini
+# Receiver's ~/.reticulum/config
+[[LoRa 868 MHz]]
+ type = RNodeInterface
+ network_name = helv4net
+ passphrase = R3ticulum-priv8-m3sh
+ ...
+```
+
+What this does:
+- Derives a 64-byte IFAC key: `HKDF-SHA256(SHA256(network_name) + SHA256(passphrase), IFAC_SALT, 64)`
+- Sends to RNode via KISS `CMD_IFAC_KEY` (0x89)
+- The RNode derives an Ed25519 signing keypair from the key and applies IFAC tags to all transmitted packets
+
+### 3. Legacy collector key (alternative to LXMF)
+
+If you don't need LXMF and just want encrypted JSON beacons:
+
+```sh
+rnlog provision
+# Copy the 64-byte hex output, then send via CMD_BCN_KEY — see the Python
+# snippet in the rns-collector README.
+```
+
+## Receiver setup
+
+The receiver is any machine running `rnsd` with an RNode interface on the same LoRa parameters.
+
+### Reticulum config
+
+```ini
+# ~/.reticulum/config
+[reticulum]
+ enable_transport = yes
+
+[interfaces]
+ [[LoRa 868 MHz]]
+ type = RNodeInterface
+ interface_enabled = True
+ port = /dev/ttyACM0
+ frequency = 868000000
+ bandwidth = 125000
+ spreadingfactor = 7
+ txpower = 14
+ # Optional but recommended — must match beacon provisioning:
+ network_name = helv4net
+ passphrase = R3ticulum-priv8-m3sh
+```
+
+### Start the receiver
+
+```sh
+# Start rnsd (if not already running as a service)
+rnsd
+
+# In another terminal, start the telemetry collector
+rnlog serve
+```
+
+Or to also relay GPS data to a Sideband app:
+
+```sh
+rnlog serve --sideband-dest
+```
+
+### Query collected data
+
+```sh
+# Recent readings
+rnlog query -n 10
+
+# Last hour, as JSON
+rnlog -j query -s 1h
+
+# Database summary
+rnlog summary
+
+# Export to CSV
+rnlog export -f csv > telemetry.csv
+```
+
+## Testing and verification
+
+### USB test (device connected)
+
+Trigger a test beacon over USB without waiting for the 30s interval:
+
+```sh
+rnlog test-lxmf --port /dev/ttyACM0
+```
+
+This sends `CMD_LXMF_TEST` (0x88) to the RNode, which immediately transmits an announce + LXMF beacon and emits the pre-encryption plaintext as `CMD_DIAG` frames back over USB. The test tool validates:
+
+- Announce packet structure and Ed25519 signature
+- LXMF message structure and Fernet decryption
+- FIELD_TELEMETRY parsing (lat, lon, alt, speed, battery)
+
+### Over-the-air verification
+
+To verify the receiver is accepting IFAC-authenticated packets:
+
+```python
+import RNS
+
+reticulum = RNS.Reticulum()
+target = bytes.fromhex("YOUR_RNODE_DEST_HASH") # from provision-lxmf output
+
+# Check if the receiver has seen the announce
+identity = RNS.Identity.recall(target)
+if identity:
+ print(f"Identity known: {identity}")
+else:
+ print("Not yet received — trigger a test beacon or wait for next announce")
+```
+
+### Checking the OLED display
+
+The T-Beam Supreme has 6 display pages (cycle with the button):
+
+| Page | Content |
+|------|---------|
+| 0-1 | Radio diagnostics (standard RNode) |
+| 2-4 | GPS (coordinates, satellites, altitude) |
+| 5 | LXMF status (identity hash, target, announce/beacon timing) |
+
+When LXMF is active, page 2 shows `LX` instead of `BCN` next to the satellite count.
+
+## Radio parameters
+
+Beacons use fixed LoRa parameters that must match the receiver's RNode interface:
+
+| Parameter | Value |
+|------------------|---------|
+| Frequency | 868 MHz |
+| Bandwidth | 125 kHz |
+| Spreading Factor | 7 |
+| Coding Rate | 4/5 |
+| TX Power | 17 dBm |
+
+These are set in `Beacon.h`. If your receiver uses different radio parameters, update the `BEACON_*` defines and rebuild.
+
+## Timing
+
+| Parameter | Default | Define |
+|--------------------------|---------|-----------------------------|
+| Beacon interval | 30s | `BEACON_INTERVAL_MS` |
+| Announce interval | 10min | `LXMF_ANNOUNCE_INTERVAL_MS` |
+| Startup delay | 10s | `BEACON_STARTUP_DELAY_MS` |
+| Host inactivity timeout | 15s | `BEACON_NO_HOST_TIMEOUT_MS` |
+
+## Packet sizes
+
+| Mode | Header | Payload | Total |
+|---------------------|--------|----------------------|--------|
+| Plaintext JSON | 19B | ~93B JSON | ~112B |
+| Encrypted JSON | 19B | 32+16+96+32 = 176B | ~195B |
+| LXMF announce | 2B | 183B (+ 8B IFAC tag) | ~193B |
+| LXMF beacon | 2B | ~250B encrypted | ~260B |
+
+All fit within the RNS MTU of 508 bytes.
+
+## Troubleshooting
+
+### Receiver doesn't see any packets
+
+1. **Radio mismatch**: Verify frequency, bandwidth, and spreading factor match between beacon and receiver
+2. **IFAC mismatch**: If the receiver has `network_name`/`passphrase` set, the beacon must be provisioned with matching IFAC key. Packets without valid IFAC are silently dropped — no error is logged
+3. **Range**: LoRa SF7 at 868 MHz has limited range in urban environments. Try line-of-sight first
+
+### `rnlog provision-lxmf` fails to resolve destination
+
+The Sideband destination must be reachable via Reticulum. Ensure `rnsd` is running and has a path to the destination (either direct or via transport nodes).
+
+### `has_path()` returns False but identity is recalled
+
+This is expected. `has_path()` checks the RNS path table (populated by Transport routing), while `Identity.recall()` checks `known_destinations` (populated when any announce is validated). For local interfaces, the path table entry may not be created, but the identity is still usable.
+
+### Device shows "GPS searching" indefinitely
+
+The T-Beam Supreme GPS needs clear sky view for first fix. Cold start takes 30-60 seconds outdoors, longer indoors. The RTC retains time across reboots once synced, speeding subsequent fixes.
+
+## Architecture
+
+```
+┌─────────────────────────┐ LoRa 868 MHz ┌────────────────────────┐
+│ T-Beam Supreme │ ──────────────────────► │ Receiver │
+│ │ IFAC-tagged packets │ (rnsd + RNode) │
+│ GPS ──► Beacon.h │ │ │
+│ ├─ announce │ Announce (185B + 8B IFAC) │ rnsd validates IFAC │
+│ └─ LXMF msg │ LXMF (250B + 8B IFAC) │ ├─ known_destinations │
+│ │ │ └─ LXMF delivery │
+│ Provisioned via USB: │ │ │
+│ - Collector key (64B) │ │ rnlog serve │
+│ - IFAC key (64B) │ │ ├─ SQLite database │
+│ - LXMF identity (NVS) │ │ └─ Sideband relay │
+└─────────────────────────┘ └────────────────────────┘
+```
+
+## Files
+
+| File | Purpose |
+|------------------|-------------------------------------------------------|
+| `Beacon.h` | Beacon state machine, LXMF/JSON path selection |
+| `BeaconCrypto.h` | X25519 ECDH (libsodium), HKDF, AES-256-CBC, HMAC |
+| `LxmfBeacon.h` | LXMF identity, announce construction, beacon messages |
+| `IfacAuth.h` | IFAC key storage (NVS), Ed25519 signing, tag masking |
+| `GPS.h` | GPS parsing (TinyGPS++) |
+| `ROM.h` | EEPROM addresses for beacon key storage |
+| `Framing.h` | KISS command definitions (CMD_BCN_KEY, CMD_IFAC_KEY, etc.) |
+| `Display.h` | OLED display pages including LXMF status |
diff --git a/Documentation/T-Watch-Ultra-Notes.md b/Documentation/T-Watch-Ultra-Notes.md
new file mode 100644
index 00000000..5c4928b8
--- /dev/null
+++ b/Documentation/T-Watch-Ultra-Notes.md
@@ -0,0 +1,73 @@
+# T-Watch Ultra Development Notes
+
+Hard-won lessons from porting RNode firmware to the LilyGo T-Watch Ultra.
+
+## I2C Pin Default Mismatch (Critical)
+
+The generic Arduino ESP32-S3 FQBN (`esp32:esp32:esp32s3`) defines default I2C pins as **SDA=8, SCL=9** (in `variants/esp32s3/pins_arduino.h`). The T-Watch Ultra uses **SDA=3, SCL=2**.
+
+The XPowersLib `XPowersAXP2101` constructor stores the variant's default SDA/SCL internally:
+
+```cpp
+// BROKEN — uses default SDA=8, SCL=9 from variant
+PMU = new XPowersAXP2101(Wire);
+PMU->init(); // Calls Wire.begin(8, 9) — wrong pins!
+
+// CORRECT — pass explicit pins
+PMU = new XPowersAXP2101(Wire, I2C_SDA, I2C_SCL);
+PMU->init(); // Calls Wire.begin(3, 2) — correct pins
+```
+
+**Symptom**: `Wire.begin(3, 2)` returns true, but no I2C device responds. SDA/SCL lines read HIGH (pullups working). Looks like a hardware failure but is entirely a firmware bug.
+
+**Root cause**: `XPowersCommon::begin()` calls `__wire->begin(__sda, __scl)` where `__sda` and `__scl` were set to 8/9 by the constructor defaults. This silently reinitialises the I2C bus on the wrong pins.
+
+This applies to **any ESP32-S3 board using non-default I2C pins with XPowersLib**. Always pass explicit SDA/SCL to the constructor.
+
+## Flash Workflow
+
+The T-Watch Ultra's built-in USB JTAG/serial interface does NOT support esptool auto-reset. Two flash methods:
+
+### Method 1: esptool (faster, for large images)
+1. Hold **BOOT** button, press **RST**, release BOOT → download mode
+2. Flash: `esptool --chip esp32s3 --port /dev/ttyACM4 --baud 921600 write_flash 0x0 bootloader.bin 0x8000 partitions.bin 0xe000 boot_app0.bin 0x10000 app.bin`
+3. Press **RST** to exit download mode (battery keeps ESP32 running after USB cycle)
+
+### Method 2: JTAG via OpenOCD (for small images <2MB)
+```bash
+openocd -f board/esp32s3-builtin.cfg -c "program_esp app.bin 0x10000 verify reset exit"
+```
+Full flash images (16MB factory binaries) exceed JTAG flash stub limits.
+
+## Physical Buttons
+
+- **BOOT**: Left edge of PCB (GPIO 0) — download mode entry
+- **RST**: Right edge of PCB — hardware reset
+- **<< and >|**: Side buttons — media/navigation, NOT power key
+- **Power key**: Connected to AXP2101 PWR_KEY input — controls PMU power on/off
+
+## Deep Sleep Caution
+
+Do NOT call `PMU->enableSleep()` before `esp_deep_sleep_start()`. The AXP2101 retains its sleep mode state across battery-backed resets. If the PMU enters sleep mode, its I2C slave interface may become unresponsive, and the only recovery is a full battery disconnect (including the RTC button cell on the PCB).
+
+Setting GPIO pins to `OPEN_DRAIN` before deep sleep is correct for power savings, but do NOT set the I2C pins (GPIO 2/3) to OPEN_DRAIN — this can leave the bus in a state that's difficult to recover from.
+
+## Display Power Gate
+
+The CO5300 AMOLED display's VCI power is gated by the XL9555 GPIO expander (VC_EN signal, confirmed from schematic sheet 3). The display will retain its last frame buffer content as long as ALDO2 (display power rail) stays on from the battery.
+
+The XL9555 pin mapping for VC_EN needs verification — the BHI260AP GPIO enum values (6, 14, 15) used by LilyGoLib may not directly correspond to XL9555 port pin numbers. The schematic (sheet 3) shows the exact wiring.
+
+## GPS Module
+
+The MIA-M10Q (u-blox) outputs standard NMEA at 38400 baud by default. No vendor-specific init commands needed (unlike the L76K which requires PCAS commands). TinyGPSPlus parses the output without modification.
+
+## RTC
+
+The PCF85063A has time registers at offset 0x04-0x0A (vs PCF8563's 0x02-0x08). Same I2C address (0x51), similar BCD encoding, but different control register layout. The oscillator stop bit is at seconds register bit 7 (same as PCF8563).
+
+## I2C Bus Architecture
+
+All I2C devices share a single bus on GPIO 3 (SDA) and GPIO 2 (SCL) with 2.2K pull-up resistors to VDD3V3. The bus routes through the LCD connector (pins 1-2), meaning the display flex cable must be properly seated for I2C to function.
+
+The I2C bus is used by: AXP2101 (PMU), PCF85063A (RTC), CST9217 (touch), BHI260AP (IMU), DRV2605 (haptic), XL9555 (GPIO expander).
diff --git a/Documentation/photos/twatch_ultra_pcb_battery.jpg b/Documentation/photos/twatch_ultra_pcb_battery.jpg
new file mode 100644
index 00000000..52be3408
Binary files /dev/null and b/Documentation/photos/twatch_ultra_pcb_battery.jpg differ
diff --git a/Documentation/photos/twatch_ultra_pcb_closeup.jpg b/Documentation/photos/twatch_ultra_pcb_closeup.jpg
new file mode 100644
index 00000000..60615d8d
Binary files /dev/null and b/Documentation/photos/twatch_ultra_pcb_closeup.jpg differ
diff --git a/Documentation/photos/twatch_ultra_pcb_overview.jpg b/Documentation/photos/twatch_ultra_pcb_overview.jpg
new file mode 100644
index 00000000..1a361e2b
Binary files /dev/null and b/Documentation/photos/twatch_ultra_pcb_overview.jpg differ
diff --git a/Fonts/montserrat_bold_28.c b/Fonts/montserrat_bold_28.c
new file mode 100644
index 00000000..c7b83078
--- /dev/null
+++ b/Fonts/montserrat_bold_28.c
@@ -0,0 +1,2539 @@
+/*******************************************************************************
+ * Size: 28 px
+ * Bpp: 4
+ * Opts: --bpp 4 --size 28 --no-compress --font /home/ian/Arduino/libraries/lvgl/tests/src/test_files/fonts/Montserrat-Bold.ttf -r 0x20-0x7E --format lvgl -o /home/ian/Code/RNode_Firmware/Fonts/montserrat_bold_28.c --lv-include lvgl.h --lv-font-name montserrat_bold_28
+ ******************************************************************************/
+
+#ifdef LV_LVGL_H_INCLUDE_SIMPLE
+#include "lvgl.h"
+#else
+#include "lvgl.h"
+#endif
+
+#ifndef MONTSERRAT_BOLD_28
+#define MONTSERRAT_BOLD_28 1
+#endif
+
+#if MONTSERRAT_BOLD_28
+
+/*-----------------
+ * BITMAPS
+ *----------------*/
+
+/*Store the image of the glyphs*/
+static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
+ /* U+0020 " " */
+
+ /* U+0021 "!" */
+ 0x8f, 0xff, 0xf9, 0x7f, 0xff, 0xf8, 0x6f, 0xff,
+ 0xf7, 0x4f, 0xff, 0xf6, 0x3f, 0xff, 0xf5, 0x2f,
+ 0xff, 0xf4, 0x1f, 0xff, 0xf3, 0xf, 0xff, 0xf2,
+ 0xf, 0xff, 0xf1, 0xe, 0xff, 0xf0, 0xd, 0xff,
+ 0xe0, 0xb, 0xff, 0xd0, 0x6, 0x99, 0x70, 0x0,
+ 0x0, 0x0, 0x7, 0xde, 0x80, 0x5f, 0xff, 0xf7,
+ 0x9f, 0xff, 0xfb, 0x5f, 0xff, 0xf7, 0x7, 0xee,
+ 0x90,
+
+ /* U+0022 "\"" */
+ 0x9f, 0xff, 0x10, 0xdf, 0xfd, 0x9f, 0xff, 0x0,
+ 0xcf, 0xfc, 0x8f, 0xff, 0x0, 0xcf, 0xfc, 0x8f,
+ 0xff, 0x0, 0xbf, 0xfb, 0x7f, 0xfe, 0x0, 0xbf,
+ 0xfb, 0x6f, 0xfd, 0x0, 0xaf, 0xfa, 0x6f, 0xfd,
+ 0x0, 0xaf, 0xf9, 0x4c, 0xca, 0x0, 0x7c, 0xc7,
+
+ /* U+0023 "#" */
+ 0x0, 0x0, 0x0, 0xff, 0xf2, 0x0, 0xb, 0xff,
+ 0x60, 0x0, 0x0, 0x0, 0x1, 0xff, 0xf0, 0x0,
+ 0xd, 0xff, 0x40, 0x0, 0x0, 0x0, 0x3, 0xff,
+ 0xe0, 0x0, 0xf, 0xff, 0x20, 0x0, 0x0, 0x0,
+ 0x5, 0xff, 0xc0, 0x0, 0x1f, 0xff, 0x0, 0x0,
+ 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf9, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf9, 0x7, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0,
+ 0xd, 0xff, 0x40, 0x0, 0x9f, 0xf8, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0x20, 0x0, 0xbf, 0xf6,
+ 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff, 0x0, 0x0,
+ 0xdf, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xfe,
+ 0x0, 0x0, 0xff, 0xf2, 0x0, 0x0, 0x0, 0x0,
+ 0x5f, 0xfc, 0x0, 0x1, 0xff, 0xf0, 0x0, 0x0,
+ 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xa0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xa0, 0x7f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0,
+ 0xef, 0xf3, 0x0, 0xa, 0xff, 0x80, 0x0, 0x0,
+ 0x0, 0x0, 0xff, 0xf1, 0x0, 0xc, 0xff, 0x60,
+ 0x0, 0x0, 0x0, 0x2, 0xff, 0xf0, 0x0, 0xe,
+ 0xff, 0x40, 0x0, 0x0, 0x0, 0x4, 0xff, 0xd0,
+ 0x0, 0xf, 0xff, 0x20, 0x0, 0x0,
+
+ /* U+0024 "$" */
+ 0x0, 0x0, 0x0, 0x4, 0xff, 0x90, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x4, 0xff, 0x90, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0xff, 0x90,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0xff,
+ 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x16, 0xbf,
+ 0xff, 0xfd, 0xa7, 0x20, 0x0, 0x0, 0x7, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x10, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x4,
+ 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xf7, 0x0,
+ 0xa, 0xff, 0xff, 0x44, 0xff, 0x90, 0x16, 0xc1,
+ 0x0, 0xc, 0xff, 0xfb, 0x4, 0xff, 0x90, 0x0,
+ 0x0, 0x0, 0xb, 0xff, 0xfe, 0x24, 0xff, 0x90,
+ 0x0, 0x0, 0x0, 0x7, 0xff, 0xff, 0xfe, 0xff,
+ 0x90, 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff,
+ 0xff, 0xfa, 0x61, 0x0, 0x0, 0x0, 0x8, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x91, 0x0, 0x0, 0x0,
+ 0x6, 0xbf, 0xff, 0xff, 0xff, 0xfe, 0x10, 0x0,
+ 0x0, 0x0, 0x4, 0xff, 0xff, 0xff, 0xff, 0xa0,
+ 0x0, 0x0, 0x0, 0x4, 0xff, 0x91, 0xdf, 0xff,
+ 0xf0, 0x0, 0x30, 0x0, 0x4, 0xff, 0x90, 0x7f,
+ 0xff, 0xf0, 0x2, 0xfc, 0x61, 0x4, 0xff, 0x91,
+ 0xdf, 0xff, 0xe0, 0x9, 0xff, 0xff, 0xde, 0xff,
+ 0xff, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfc, 0x0, 0x3, 0xbf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0, 0x1,
+ 0x6a, 0xdf, 0xff, 0xfb, 0x72, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4, 0xff, 0x90, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4, 0xff, 0x90, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x4, 0xff, 0x90, 0x0,
+ 0x0, 0x0,
+
+ /* U+0025 "%" */
+ 0x0, 0x8, 0xdf, 0xe9, 0x20, 0x0, 0x0, 0x0,
+ 0x1f, 0xff, 0x40, 0x0, 0x1, 0xdf, 0xff, 0xff,
+ 0xf3, 0x0, 0x0, 0x0, 0xcf, 0xf9, 0x0, 0x0,
+ 0xa, 0xff, 0x81, 0x5f, 0xfd, 0x0, 0x0, 0x7,
+ 0xff, 0xd0, 0x0, 0x0, 0xf, 0xfd, 0x0, 0x9,
+ 0xff, 0x30, 0x0, 0x3f, 0xff, 0x30, 0x0, 0x0,
+ 0x2f, 0xfa, 0x0, 0x6, 0xff, 0x60, 0x0, 0xdf,
+ 0xf7, 0x0, 0x0, 0x0, 0x3f, 0xfa, 0x0, 0x6,
+ 0xff, 0x60, 0x9, 0xff, 0xc0, 0x0, 0x0, 0x0,
+ 0x1f, 0xfd, 0x0, 0x9, 0xff, 0x40, 0x4f, 0xff,
+ 0x10, 0x0, 0x0, 0x0, 0xc, 0xff, 0x70, 0x4f,
+ 0xff, 0x1, 0xef, 0xf5, 0x0, 0x0, 0x0, 0x0,
+ 0x2, 0xff, 0xff, 0xff, 0xf6, 0xa, 0xff, 0xa0,
+ 0x0, 0x32, 0x0, 0x0, 0x0, 0x3c, 0xff, 0xfe,
+ 0x50, 0x6f, 0xfe, 0x10, 0x9f, 0xff, 0xf8, 0x0,
+ 0x0, 0x0, 0x13, 0x20, 0x2, 0xff, 0xf3, 0xc,
+ 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0, 0x0,
+ 0xc, 0xff, 0x80, 0x6f, 0xfb, 0x12, 0xdf, 0xf4,
+ 0x0, 0x0, 0x0, 0x0, 0x8f, 0xfc, 0x0, 0xcf,
+ 0xf2, 0x0, 0x4f, 0xf9, 0x0, 0x0, 0x0, 0x3,
+ 0xff, 0xf2, 0x0, 0xef, 0xf0, 0x0, 0x1f, 0xfc,
+ 0x0, 0x0, 0x0, 0xd, 0xff, 0x60, 0x0, 0xdf,
+ 0xf0, 0x0, 0x1f, 0xfb, 0x0, 0x0, 0x0, 0x9f,
+ 0xfb, 0x0, 0x0, 0xbf, 0xf2, 0x0, 0x4f, 0xf9,
+ 0x0, 0x0, 0x5, 0xff, 0xe1, 0x0, 0x0, 0x5f,
+ 0xfb, 0x22, 0xdf, 0xf3, 0x0, 0x0, 0x1e, 0xff,
+ 0x40, 0x0, 0x0, 0x9, 0xff, 0xff, 0xff, 0x70,
+ 0x0, 0x0, 0xbf, 0xf9, 0x0, 0x0, 0x0, 0x0,
+ 0x5c, 0xfe, 0xb4, 0x0,
+
+ /* U+0026 "&" */
+ 0x0, 0x0, 0x18, 0xce, 0xfe, 0xb6, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff, 0xfc,
+ 0x10, 0x0, 0x0, 0x0, 0x2f, 0xff, 0xff, 0xff,
+ 0xff, 0xf9, 0x0, 0x0, 0x0, 0x8, 0xff, 0xfb,
+ 0x10, 0x2e, 0xff, 0xe0, 0x0, 0x0, 0x0, 0xaf,
+ 0xff, 0x50, 0x0, 0xbf, 0xff, 0x0, 0x0, 0x0,
+ 0x9, 0xff, 0xf9, 0x0, 0x3f, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0x3f, 0xff, 0xf7, 0x7f, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0x0, 0x1, 0xff, 0xff,
+ 0xff, 0xb1, 0x0, 0x0, 0x0, 0x0, 0x5, 0xef,
+ 0xff, 0xff, 0xfc, 0x10, 0x4, 0x95, 0x0, 0x7,
+ 0xff, 0xff, 0xff, 0xff, 0xfd, 0x20, 0xaf, 0xff,
+ 0x14, 0xff, 0xff, 0x40, 0x4e, 0xff, 0xfe, 0x5f,
+ 0xff, 0xd0, 0xbf, 0xff, 0x70, 0x0, 0x2e, 0xff,
+ 0xff, 0xff, 0xf7, 0xe, 0xff, 0xf5, 0x0, 0x0,
+ 0x1c, 0xff, 0xff, 0xfe, 0x0, 0xef, 0xff, 0xb0,
+ 0x0, 0x0, 0x1f, 0xff, 0xff, 0xf0, 0xa, 0xff,
+ 0xff, 0xd7, 0x56, 0x9e, 0xff, 0xff, 0xff, 0xc1,
+ 0x1e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x70, 0x1c, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0x35, 0xff, 0xb0, 0x0, 0x4, 0x9d, 0xef, 0xfd,
+ 0x94, 0x0, 0x3, 0xb0, 0x0,
+
+ /* U+0027 "'" */
+ 0x9f, 0xff, 0x19, 0xff, 0xf0, 0x8f, 0xff, 0x8,
+ 0xff, 0xf0, 0x7f, 0xfe, 0x6, 0xff, 0xd0, 0x6f,
+ 0xfd, 0x4, 0xcc, 0xa0,
+
+ /* U+0028 "(" */
+ 0x0, 0x7, 0xbb, 0xb6, 0x0, 0x2f, 0xff, 0xf2,
+ 0x0, 0xbf, 0xff, 0xa0, 0x2, 0xff, 0xff, 0x20,
+ 0x8, 0xff, 0xfd, 0x0, 0xd, 0xff, 0xf7, 0x0,
+ 0x2f, 0xff, 0xf2, 0x0, 0x5f, 0xff, 0xf0, 0x0,
+ 0x8f, 0xff, 0xc0, 0x0, 0xbf, 0xff, 0x90, 0x0,
+ 0xcf, 0xff, 0x70, 0x0, 0xdf, 0xff, 0x60, 0x0,
+ 0xef, 0xff, 0x50, 0x0, 0xef, 0xff, 0x50, 0x0,
+ 0xdf, 0xff, 0x60, 0x0, 0xcf, 0xff, 0x70, 0x0,
+ 0xbf, 0xff, 0x80, 0x0, 0x9f, 0xff, 0xb0, 0x0,
+ 0x6f, 0xff, 0xe0, 0x0, 0x3f, 0xff, 0xf1, 0x0,
+ 0xe, 0xff, 0xf6, 0x0, 0x9, 0xff, 0xfb, 0x0,
+ 0x3, 0xff, 0xff, 0x10, 0x0, 0xdf, 0xff, 0x70,
+ 0x0, 0x4f, 0xff, 0xe0, 0x0, 0xb, 0xff, 0xf7,
+
+ /* U+0029 ")" */
+ 0x6b, 0xbb, 0x70, 0x0, 0x1f, 0xff, 0xf2, 0x0,
+ 0x9, 0xff, 0xfb, 0x0, 0x2, 0xff, 0xff, 0x20,
+ 0x0, 0xbf, 0xff, 0x80, 0x0, 0x6f, 0xff, 0xe0,
+ 0x0, 0x2f, 0xff, 0xf2, 0x0, 0xe, 0xff, 0xf5,
+ 0x0, 0xb, 0xff, 0xf9, 0x0, 0x8, 0xff, 0xfb,
+ 0x0, 0x7, 0xff, 0xfd, 0x0, 0x6, 0xff, 0xfe,
+ 0x0, 0x5, 0xff, 0xff, 0x0, 0x5, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xfe, 0x0, 0x7, 0xff, 0xfd,
+ 0x0, 0x8, 0xff, 0xfc, 0x0, 0xa, 0xff, 0xf9,
+ 0x0, 0xe, 0xff, 0xf6, 0x0, 0x1f, 0xff, 0xf3,
+ 0x0, 0x5f, 0xff, 0xf0, 0x0, 0xaf, 0xff, 0x90,
+ 0x0, 0xff, 0xff, 0x40, 0x7, 0xff, 0xfd, 0x0,
+ 0xe, 0xff, 0xf4, 0x0, 0x7f, 0xff, 0xb0, 0x0,
+
+ /* U+002A "*" */
+ 0x0, 0x0, 0x3d, 0xd5, 0x0, 0x0, 0x1, 0x0,
+ 0x3f, 0xf5, 0x0, 0x10, 0xc, 0xd4, 0x3f, 0xf5,
+ 0x3c, 0xd0, 0x6f, 0xff, 0xdf, 0xfe, 0xff, 0xf8,
+ 0x18, 0xff, 0xff, 0xff, 0xff, 0xa1, 0x0, 0x2f,
+ 0xff, 0xff, 0xf3, 0x0, 0x6, 0xef, 0xff, 0xff,
+ 0xfe, 0x70, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0xd, 0xe6, 0x3f, 0xf5, 0x5e, 0xe1, 0x2, 0x10,
+ 0x3f, 0xf5, 0x0, 0x30, 0x0, 0x0, 0x3f, 0xf6,
+ 0x0, 0x0,
+
+ /* U+002B "+" */
+ 0x0, 0x0, 0x6, 0xff, 0xf3, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6f, 0xff, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x6, 0xff, 0xf3, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x6f, 0xff, 0x30, 0x0, 0x0, 0x15, 0x55,
+ 0x5c, 0xff, 0xf8, 0x55, 0x55, 0x4, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf1, 0x4f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x14, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0, 0x6, 0xff,
+ 0xf3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff,
+ 0x30, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0x30,
+ 0x0, 0x0, 0x0, 0x0, 0x3, 0x88, 0x81, 0x0,
+ 0x0, 0x0,
+
+ /* U+002C "," */
+ 0x0, 0x2, 0x10, 0x0, 0x3e, 0xff, 0x80, 0xd,
+ 0xff, 0xff, 0x30, 0xff, 0xff, 0xf5, 0xc, 0xff,
+ 0xff, 0x30, 0x2f, 0xff, 0xd0, 0x0, 0xbf, 0xf7,
+ 0x0, 0xf, 0xff, 0x10, 0x4, 0xff, 0xa0, 0x0,
+ 0x9f, 0xf3, 0x0,
+
+ /* U+002D "-" */
+ 0x37, 0x77, 0x77, 0x77, 0x28, 0xff, 0xff, 0xff,
+ 0xf6, 0x8f, 0xff, 0xff, 0xff, 0x68, 0xff, 0xff,
+ 0xff, 0xf6,
+
+ /* U+002E "." */
+ 0x0, 0x14, 0x20, 0x0, 0x4f, 0xff, 0x90, 0xe,
+ 0xff, 0xff, 0x30, 0xff, 0xff, 0xf5, 0xb, 0xff,
+ 0xff, 0x10, 0x1b, 0xfd, 0x40,
+
+ /* U+002F "/" */
+ 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xf0, 0x0,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xa0, 0x0, 0x0,
+ 0x0, 0x0, 0x9f, 0xff, 0x50, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xfe, 0x0, 0x0, 0x0, 0x0, 0x4,
+ 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xf3, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0x80, 0x0,
+ 0x0, 0x0, 0x0, 0xbf, 0xff, 0x20, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0,
+ 0x6, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0, 0xc,
+ 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff,
+ 0xc0, 0x0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0x60,
+ 0x0, 0x0, 0x0, 0x0, 0xdf, 0xff, 0x10, 0x0,
+ 0x0, 0x0, 0x3, 0xff, 0xfb, 0x0, 0x0, 0x0,
+ 0x0, 0x8, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0,
+ 0xe, 0xff, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x4f,
+ 0xff, 0xa0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff,
+ 0x40, 0x0, 0x0, 0x0, 0x0, 0xff, 0xfe, 0x0,
+ 0x0, 0x0, 0x0, 0x5, 0xff, 0xf9, 0x0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xf3, 0x0, 0x0, 0x0,
+ 0x0, 0x1f, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0,
+ 0x6f, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0, 0xbf,
+ 0xff, 0x20, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0030 "0" */
+ 0x0, 0x0, 0x17, 0xce, 0xfe, 0xc7, 0x10, 0x0,
+ 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x70, 0x0, 0x4f, 0xff, 0xff, 0xec, 0xef, 0xff,
+ 0xff, 0x40, 0xd, 0xff, 0xff, 0x60, 0x0, 0x6f,
+ 0xff, 0xfd, 0x4, 0xff, 0xff, 0x80, 0x0, 0x0,
+ 0x8f, 0xff, 0xf4, 0x9f, 0xff, 0xf1, 0x0, 0x0,
+ 0x1, 0xff, 0xff, 0x8c, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0xd, 0xff, 0xfb, 0xdf, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0xbf, 0xff, 0xde, 0xff, 0xfb, 0x0,
+ 0x0, 0x0, 0xb, 0xff, 0xfe, 0xdf, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0xbf, 0xff, 0xdc, 0xff, 0xfd,
+ 0x0, 0x0, 0x0, 0xd, 0xff, 0xfb, 0x9f, 0xff,
+ 0xf1, 0x0, 0x0, 0x1, 0xff, 0xff, 0x84, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x8f, 0xff, 0xf4, 0xd,
+ 0xff, 0xff, 0x60, 0x0, 0x6f, 0xff, 0xfd, 0x0,
+ 0x4f, 0xff, 0xff, 0xec, 0xef, 0xff, 0xff, 0x40,
+ 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x70,
+ 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x0, 0x0, 0x0, 0x17, 0xcf, 0xfe, 0xc7, 0x10,
+ 0x0, 0x0,
+
+ /* U+0031 "1" */
+ 0xcf, 0xff, 0xff, 0xff, 0xac, 0xff, 0xff, 0xff,
+ 0xfa, 0xcf, 0xff, 0xff, 0xff, 0xa7, 0x99, 0x9f,
+ 0xff, 0xfa, 0x0, 0x0, 0xef, 0xff, 0xa0, 0x0,
+ 0xe, 0xff, 0xfa, 0x0, 0x0, 0xef, 0xff, 0xa0,
+ 0x0, 0xe, 0xff, 0xfa, 0x0, 0x0, 0xef, 0xff,
+ 0xa0, 0x0, 0xe, 0xff, 0xfa, 0x0, 0x0, 0xef,
+ 0xff, 0xa0, 0x0, 0xe, 0xff, 0xfa, 0x0, 0x0,
+ 0xef, 0xff, 0xa0, 0x0, 0xe, 0xff, 0xfa, 0x0,
+ 0x0, 0xef, 0xff, 0xa0, 0x0, 0xe, 0xff, 0xfa,
+ 0x0, 0x0, 0xef, 0xff, 0xa0, 0x0, 0xe, 0xff,
+ 0xfa, 0x0, 0x0, 0xef, 0xff, 0xa0,
+
+ /* U+0032 "2" */
+ 0x0, 0x2, 0x7c, 0xdf, 0xfd, 0xa5, 0x0, 0x0,
+ 0x1, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xd2, 0x0,
+ 0x1e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x20,
+ 0xaf, 0xff, 0xff, 0xdc, 0xdf, 0xff, 0xff, 0xa0,
+ 0x7, 0xff, 0x81, 0x0, 0x3, 0xff, 0xff, 0xf0,
+ 0x0, 0x14, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xf0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xf0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xdf, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xff, 0x40,
+ 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xf9, 0x0,
+ 0x0, 0x0, 0x0, 0xb, 0xff, 0xff, 0xb0, 0x0,
+ 0x0, 0x0, 0x1, 0xcf, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x2d, 0xff, 0xff, 0x80, 0x0, 0x0,
+ 0x0, 0x3, 0xef, 0xff, 0xf7, 0x0, 0x0, 0x0,
+ 0x0, 0x4f, 0xff, 0xff, 0x50, 0x0, 0x0, 0x0,
+ 0x6, 0xff, 0xff, 0xfe, 0xaa, 0xaa, 0xaa, 0xa7,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb,
+
+ /* U+0033 "3" */
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xa0, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x9, 0x99, 0x99, 0x99, 0x9f, 0xff,
+ 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff,
+ 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff,
+ 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff,
+ 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xe8, 0x20, 0x0, 0x0, 0x0, 0x0, 0xbf,
+ 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0, 0xb,
+ 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
+ 0x35, 0x56, 0xaf, 0xff, 0xff, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf6, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xff, 0xff, 0x80, 0x2,
+ 0x10, 0x0, 0x0, 0x0, 0x3f, 0xff, 0xf7, 0x0,
+ 0xcf, 0x82, 0x0, 0x0, 0x2d, 0xff, 0xff, 0x30,
+ 0x4f, 0xff, 0xfe, 0xcc, 0xdf, 0xff, 0xff, 0xd0,
+ 0xc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0x0, 0x3c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc2,
+ 0x0, 0x0, 0x2, 0x7b, 0xdf, 0xff, 0xda, 0x50,
+ 0x0, 0x0,
+
+ /* U+0034 "4" */
+ 0x0, 0x0, 0x0, 0x0, 0x4, 0xff, 0xff, 0x70,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0xff,
+ 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xcf, 0xff, 0xe1, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x5f, 0xff, 0xf7, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x2, 0xff, 0xff, 0xb0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd, 0xff,
+ 0xfe, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xf3, 0x1, 0x22, 0x22, 0x0, 0x0,
+ 0x0, 0x6, 0xff, 0xff, 0x70, 0x7, 0xff, 0xfd,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xfb, 0x0, 0x7,
+ 0xff, 0xfd, 0x0, 0x0, 0x1, 0xef, 0xff, 0xe1,
+ 0x0, 0x7, 0xff, 0xfd, 0x0, 0x0, 0xb, 0xff,
+ 0xff, 0xda, 0xaa, 0xaf, 0xff, 0xff, 0xaa, 0xa0,
+ 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf0, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf0, 0x2f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x9, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xfd,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9, 0xff, 0xfd, 0x0, 0x0,
+
+ /* U+0035 "5" */
+ 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0,
+ 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0,
+ 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0,
+ 0x0, 0x9f, 0xff, 0xf9, 0x99, 0x99, 0x99, 0x60,
+ 0x0, 0xbf, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xdf, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xd9, 0x98, 0x62, 0x0, 0x0,
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0x0,
+ 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50,
+ 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0x0, 0x0, 0x0, 0x0, 0x15, 0xcf, 0xff, 0xf8,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xe, 0xff, 0xfc,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0xff, 0xfd,
+ 0x0, 0x40, 0x0, 0x0, 0x0, 0xe, 0xff, 0xfc,
+ 0x7, 0xfb, 0x40, 0x0, 0x1, 0xaf, 0xff, 0xf8,
+ 0xe, 0xff, 0xff, 0xdc, 0xdf, 0xff, 0xff, 0xf2,
+ 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe5, 0x0,
+ 0x0, 0x16, 0xad, 0xef, 0xfe, 0xb6, 0x10, 0x0,
+
+ /* U+0036 "6" */
+ 0x0, 0x0, 0x1, 0x7b, 0xef, 0xfe, 0xc8, 0x30,
+ 0x0, 0x0, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x50, 0x0, 0x2d, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xd0, 0x0, 0x1d, 0xff, 0xff, 0xfd, 0xaa, 0xbe,
+ 0xf5, 0x0, 0x9, 0xff, 0xff, 0xb2, 0x0, 0x0,
+ 0x3, 0x0, 0x1, 0xff, 0xff, 0xb0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf2, 0x1, 0x44,
+ 0x31, 0x0, 0x0, 0xa, 0xff, 0xfd, 0x3c, 0xff,
+ 0xff, 0xfb, 0x30, 0x0, 0xdf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x70, 0xe, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x40, 0xef, 0xff, 0xff,
+ 0x82, 0x2, 0x8f, 0xff, 0xfd, 0xc, 0xff, 0xff,
+ 0x80, 0x0, 0x0, 0x8f, 0xff, 0xf2, 0xaf, 0xff,
+ 0xf3, 0x0, 0x0, 0x3, 0xff, 0xff, 0x36, 0xff,
+ 0xff, 0x40, 0x0, 0x0, 0x3f, 0xff, 0xf2, 0xf,
+ 0xff, 0xfb, 0x0, 0x0, 0xb, 0xff, 0xff, 0x0,
+ 0x6f, 0xff, 0xfd, 0x75, 0x7c, 0xff, 0xff, 0x80,
+ 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
+ 0x0, 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xa1,
+ 0x0, 0x0, 0x0, 0x17, 0xce, 0xff, 0xd9, 0x30,
+ 0x0, 0x0,
+
+ /* U+0037 "7" */
+ 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x74, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf7, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x64, 0xff, 0xff, 0xaa, 0xaa, 0xaa, 0xff,
+ 0xff, 0xf2, 0x4f, 0xff, 0xc0, 0x0, 0x0, 0x2f,
+ 0xff, 0xfb, 0x4, 0xff, 0xfc, 0x0, 0x0, 0x9,
+ 0xff, 0xff, 0x40, 0x2b, 0xbb, 0x80, 0x0, 0x1,
+ 0xff, 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x8f, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xe, 0xff, 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x6, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xdf, 0xff, 0xf0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x5f, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xc, 0xff, 0xff, 0x10, 0x0, 0x0, 0x0,
+ 0x0, 0x4, 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xbf, 0xff, 0xf3, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x2f, 0xff, 0xfb, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0x40, 0x0, 0x0,
+ 0x0, 0x0, 0x1, 0xff, 0xff, 0xd0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x8f, 0xff, 0xf6, 0x0, 0x0,
+ 0x0, 0x0,
+
+ /* U+0038 "8" */
+ 0x0, 0x0, 0x6, 0xad, 0xff, 0xec, 0x83, 0x0,
+ 0x0, 0x0, 0x5, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xa0, 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfc, 0x0, 0x0, 0xef, 0xff, 0xfa, 0x54,
+ 0x7d, 0xff, 0xff, 0x70, 0x3, 0xff, 0xff, 0x70,
+ 0x0, 0x1, 0xef, 0xff, 0xb0, 0x4, 0xff, 0xff,
+ 0x30, 0x0, 0x0, 0xcf, 0xff, 0xb0, 0x1, 0xff,
+ 0xff, 0xa0, 0x0, 0x3, 0xff, 0xff, 0x80, 0x0,
+ 0x8f, 0xff, 0xff, 0xdd, 0xef, 0xff, 0xfe, 0x10,
+ 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0x0, 0x0, 0x3e, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf9, 0x0, 0x3, 0xff, 0xff, 0xfb, 0x87, 0x9e,
+ 0xff, 0xff, 0xa0, 0xb, 0xff, 0xff, 0x40, 0x0,
+ 0x0, 0xbf, 0xff, 0xf2, 0xf, 0xff, 0xfb, 0x0,
+ 0x0, 0x0, 0x3f, 0xff, 0xf6, 0xf, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xf7, 0xd, 0xff,
+ 0xff, 0x20, 0x0, 0x0, 0xaf, 0xff, 0xf5, 0x7,
+ 0xff, 0xff, 0xf8, 0x54, 0x6c, 0xff, 0xff, 0xe0,
+ 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x30, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc3, 0x0, 0x0, 0x0, 0x17, 0xbd, 0xff, 0xec,
+ 0x94, 0x0, 0x0,
+
+ /* U+0039 "9" */
+ 0x0, 0x0, 0x5a, 0xdf, 0xfe, 0xb6, 0x10, 0x0,
+ 0x0, 0x3, 0xdf, 0xff, 0xff, 0xff, 0xfe, 0x50,
+ 0x0, 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x70, 0x0, 0xdf, 0xff, 0xfa, 0x65, 0x7d, 0xff,
+ 0xff, 0x30, 0x3f, 0xff, 0xf6, 0x0, 0x0, 0xb,
+ 0xff, 0xfd, 0x6, 0xff, 0xff, 0x0, 0x0, 0x0,
+ 0x5f, 0xff, 0xf3, 0x5f, 0xff, 0xf3, 0x0, 0x0,
+ 0x9, 0xff, 0xff, 0x72, 0xff, 0xff, 0xe5, 0x10,
+ 0x28, 0xff, 0xff, 0xfa, 0xa, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xb0, 0xb, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfb, 0x0, 0x6, 0xdf,
+ 0xff, 0xff, 0xb2, 0xef, 0xff, 0xa0, 0x0, 0x0,
+ 0x24, 0x43, 0x0, 0x1f, 0xff, 0xf8, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x6, 0xff, 0xff, 0x40, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xef, 0xff, 0xe0, 0x0,
+ 0x15, 0x0, 0x0, 0x4, 0xef, 0xff, 0xf6, 0x0,
+ 0x9, 0xfe, 0xba, 0xbe, 0xff, 0xff, 0xfc, 0x0,
+ 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x10,
+ 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0,
+ 0x0, 0x0, 0x49, 0xce, 0xff, 0xeb, 0x61, 0x0,
+ 0x0, 0x0,
+
+ /* U+003A ":" */
+ 0x1, 0xbf, 0xd4, 0x0, 0xbf, 0xff, 0xf1, 0xf,
+ 0xff, 0xff, 0x50, 0xdf, 0xff, 0xf3, 0x4, 0xff,
+ 0xf9, 0x0, 0x1, 0x42, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0x42, 0x0, 0x4, 0xff, 0xf9, 0x0, 0xef,
+ 0xff, 0xf3, 0xf, 0xff, 0xff, 0x50, 0xbf, 0xff,
+ 0xf1, 0x1, 0xbf, 0xd4, 0x0,
+
+ /* U+003B ";" */
+ 0x1, 0xbf, 0xd4, 0x0, 0xbf, 0xff, 0xf1, 0xf,
+ 0xff, 0xff, 0x50, 0xdf, 0xff, 0xf3, 0x4, 0xff,
+ 0xf9, 0x0, 0x1, 0x42, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x21, 0x0, 0x3, 0xef, 0xf8, 0x0, 0xdf,
+ 0xff, 0xf3, 0xf, 0xff, 0xff, 0x50, 0xcf, 0xff,
+ 0xf3, 0x2, 0xff, 0xfd, 0x0, 0xb, 0xff, 0x70,
+ 0x0, 0xff, 0xf1, 0x0, 0x4f, 0xfa, 0x0, 0x9,
+ 0xff, 0x30, 0x0,
+
+ /* U+003C "<" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0x6c, 0xf1, 0x0,
+ 0x0, 0x0, 0x0, 0x4a, 0xff, 0xff, 0x10, 0x0,
+ 0x0, 0x28, 0xef, 0xff, 0xff, 0xf1, 0x0, 0x16,
+ 0xcf, 0xff, 0xff, 0xfe, 0x94, 0x1, 0xbf, 0xff,
+ 0xff, 0xff, 0xa5, 0x0, 0x0, 0x4f, 0xff, 0xff,
+ 0xc6, 0x10, 0x0, 0x0, 0x4, 0xff, 0xff, 0x70,
+ 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff, 0xff, 0xfa,
+ 0x40, 0x0, 0x0, 0x0, 0x7d, 0xff, 0xff, 0xff,
+ 0xe9, 0x30, 0x0, 0x0, 0x3, 0x9e, 0xff, 0xff,
+ 0xff, 0xd7, 0x0, 0x0, 0x0, 0x5, 0xbf, 0xff,
+ 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0, 0x17, 0xdf,
+ 0xff, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x38,
+ 0xe1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0,
+
+ /* U+003D "=" */
+ 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x4f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x11, 0x55,
+ 0x55, 0x55, 0x55, 0x55, 0x55, 0x50, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x15, 0x55, 0x55,
+ 0x55, 0x55, 0x55, 0x55, 0x4, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf1, 0x4f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x14, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf1,
+
+ /* U+003E ">" */
+ 0x11, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4,
+ 0xfb, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f,
+ 0xff, 0xe9, 0x30, 0x0, 0x0, 0x0, 0x4, 0xff,
+ 0xff, 0xff, 0xd7, 0x10, 0x0, 0x0, 0x5, 0xaf,
+ 0xff, 0xff, 0xff, 0xb5, 0x0, 0x0, 0x0, 0x16,
+ 0xcf, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0, 0x0,
+ 0x17, 0xdf, 0xff, 0xff, 0x10, 0x0, 0x0, 0x0,
+ 0x1, 0x9f, 0xff, 0xf1, 0x0, 0x0, 0x0, 0x5b,
+ 0xff, 0xff, 0xff, 0x10, 0x0, 0x4a, 0xff, 0xff,
+ 0xff, 0xfc, 0x60, 0x19, 0xef, 0xff, 0xff, 0xfd,
+ 0x82, 0x0, 0x4, 0xff, 0xff, 0xff, 0x93, 0x0,
+ 0x0, 0x0, 0x4f, 0xff, 0xb5, 0x0, 0x0, 0x0,
+ 0x0, 0x4, 0xd7, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0,
+
+ /* U+003F "?" */
+ 0x0, 0x0, 0x27, 0xcd, 0xff, 0xeb, 0x70, 0x0,
+ 0x0, 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x1, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf4, 0x0, 0xbf, 0xff, 0xfe, 0xb9, 0xbf, 0xff,
+ 0xff, 0xc0, 0x0, 0x7f, 0xf7, 0x0, 0x0, 0x2e,
+ 0xff, 0xff, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0,
+ 0x9f, 0xff, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xd, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0x0,
+ 0xb, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0xc, 0xff, 0xfe, 0x30, 0x0, 0x0, 0x0, 0x0,
+ 0x7, 0xff, 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xdf, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x3, 0x44, 0x41, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xde, 0xa1, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x3, 0xff, 0xff, 0x90, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x6f, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3, 0xff, 0xff, 0x90, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6, 0xef, 0xa1, 0x0,
+ 0x0, 0x0,
+
+ /* U+0040 "@" */
+ 0x0, 0x0, 0x0, 0x0, 0x37, 0xbd, 0xef, 0xed,
+ 0xc8, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x2c, 0xff, 0xff,
+ 0xfc, 0xba, 0xab, 0xef, 0xff, 0xfe, 0x30, 0x0,
+ 0x0, 0x0, 0x4f, 0xff, 0xfb, 0x40, 0x0, 0x0,
+ 0x0, 0x28, 0xef, 0xff, 0x60, 0x0, 0x0, 0x3f,
+ 0xff, 0xe3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x9f, 0xff, 0x50, 0x0, 0xe, 0xff, 0xd1, 0x0,
+ 0x3a, 0xdf, 0xea, 0x32, 0xff, 0xfa, 0x7f, 0xff,
+ 0x10, 0x8, 0xff, 0xe1, 0x0, 0x8f, 0xff, 0xff,
+ 0xff, 0x9f, 0xff, 0xa0, 0x9f, 0xfa, 0x0, 0xff,
+ 0xf6, 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0xef, 0xf1, 0x5f, 0xfe, 0x0, 0x2f,
+ 0xff, 0xf9, 0x20, 0x3b, 0xff, 0xff, 0xa0, 0x8,
+ 0xff, 0x6a, 0xff, 0x90, 0x8, 0xff, 0xf9, 0x0,
+ 0x0, 0xd, 0xff, 0xfa, 0x0, 0x3f, 0xfa, 0xcf,
+ 0xf5, 0x0, 0xcf, 0xff, 0x20, 0x0, 0x0, 0x5f,
+ 0xff, 0xa0, 0x0, 0xff, 0xcd, 0xff, 0x40, 0xe,
+ 0xff, 0xf0, 0x0, 0x0, 0x2, 0xff, 0xfa, 0x0,
+ 0xf, 0xfd, 0xdf, 0xf4, 0x0, 0xef, 0xff, 0x0,
+ 0x0, 0x0, 0x2f, 0xff, 0xa0, 0x0, 0xff, 0xcc,
+ 0xff, 0x50, 0xc, 0xff, 0xf2, 0x0, 0x0, 0x5,
+ 0xff, 0xfa, 0x0, 0x1f, 0xfb, 0xaf, 0xf9, 0x0,
+ 0x8f, 0xff, 0xa0, 0x0, 0x0, 0xcf, 0xff, 0xa0,
+ 0x5, 0xff, 0x75, 0xff, 0xe0, 0x2, 0xff, 0xff,
+ 0xa2, 0x3, 0xbf, 0xff, 0xfe, 0x22, 0xdf, 0xf2,
+ 0xf, 0xff, 0x50, 0x7, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x8f, 0xfe,
+ 0x10, 0x8, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xff,
+ 0xff, 0xfc, 0x10, 0x0, 0xef, 0xfc, 0x0, 0x3,
+ 0xad, 0xfe, 0xb4, 0x0, 0x3b, 0xff, 0xc7, 0x0,
+ 0x0, 0x3, 0xff, 0xfd, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4,
+ 0xff, 0xff, 0xb4, 0x0, 0x0, 0x0, 0x2, 0x71,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xcf, 0xff,
+ 0xff, 0xcb, 0xab, 0xce, 0xff, 0x70, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6d, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x3, 0x7b, 0xde, 0xff, 0xdb, 0x83,
+ 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0041 "A" */
+ 0x0, 0x0, 0x0, 0x0, 0xb, 0xff, 0xff, 0x30,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xf2, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff,
+ 0xff, 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9, 0xff, 0xfd, 0xff, 0xff, 0x10, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0xff, 0xff, 0x3c,
+ 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x8f, 0xff, 0xc0, 0x6f, 0xff, 0xe0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xe, 0xff, 0xf5, 0x0, 0xef,
+ 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff,
+ 0xfe, 0x0, 0x8, 0xff, 0xfd, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xdf, 0xff, 0x70, 0x0, 0x1f, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xf1,
+ 0x0, 0x0, 0xaf, 0xff, 0xc0, 0x0, 0x0, 0x0,
+ 0xc, 0xff, 0xff, 0x66, 0x66, 0x69, 0xff, 0xff,
+ 0x40, 0x0, 0x0, 0x4, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0xbf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xa0, 0x0, 0xa, 0xff, 0xfe,
+ 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0x20,
+ 0x1, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0,
+ 0x1f, 0xff, 0xf9, 0x0, 0x9f, 0xff, 0xf1, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xf1, 0x1f,
+ 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3,
+ 0xff, 0xff, 0x80,
+
+ /* U+0042 "B" */
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xc8, 0x20,
+ 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x80, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x70, 0xa, 0xff, 0xfe, 0x55,
+ 0x55, 0x57, 0xcf, 0xff, 0xff, 0x0, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xf3, 0xa,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0x4, 0xff, 0xff,
+ 0x40, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x8f,
+ 0xff, 0xf1, 0xa, 0xff, 0xff, 0x33, 0x33, 0x45,
+ 0xaf, 0xff, 0xfa, 0x0, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfd, 0x10, 0xa, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0xaf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0,
+ 0xa, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x27, 0xff,
+ 0xff, 0x90, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0,
+ 0x8, 0xff, 0xff, 0x1a, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x5f, 0xff, 0xf3, 0xaf, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0x3a, 0xff,
+ 0xfe, 0x55, 0x55, 0x56, 0x7c, 0xff, 0xff, 0xf0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf7, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf8, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xeb, 0x71, 0x0, 0x0,
+
+ /* U+0043 "C" */
+ 0x0, 0x0, 0x0, 0x48, 0xce, 0xfe, 0xda, 0x61,
+ 0x0, 0x0, 0x0, 0x4, 0xdf, 0xff, 0xff, 0xff,
+ 0xff, 0xf7, 0x0, 0x0, 0x9, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfc, 0x10, 0xa, 0xff, 0xff,
+ 0xff, 0xed, 0xdf, 0xff, 0xff, 0xf7, 0x6, 0xff,
+ 0xff, 0xfa, 0x30, 0x0, 0x4, 0xcf, 0xf8, 0x0,
+ 0xef, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x85,
+ 0x0, 0x6f, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xe, 0xff, 0xfb,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf,
+ 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6f, 0xff, 0xf8, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xf5, 0x0,
+ 0x0, 0x0, 0x0, 0x86, 0x0, 0x6, 0xff, 0xff,
+ 0xfa, 0x30, 0x0, 0x4, 0xcf, 0xf8, 0x0, 0xa,
+ 0xff, 0xff, 0xff, 0xed, 0xdf, 0xff, 0xff, 0xf8,
+ 0x0, 0x9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x0, 0x0, 0x5, 0xdf, 0xff, 0xff, 0xff,
+ 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0, 0x49, 0xce,
+ 0xfe, 0xda, 0x61, 0x0, 0x0,
+
+ /* U+0044 "D" */
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xed, 0xa5, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x70, 0x0, 0x0, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xc1, 0x0, 0xa,
+ 0xff, 0xff, 0xaa, 0xaa, 0xbc, 0xff, 0xff, 0xff,
+ 0xd0, 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x1,
+ 0x7f, 0xff, 0xff, 0xa0, 0xa, 0xff, 0xfd, 0x0,
+ 0x0, 0x0, 0x0, 0x2f, 0xff, 0xff, 0x30, 0xaf,
+ 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff,
+ 0xf9, 0xa, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xd0, 0xaf, 0xff, 0xd0, 0x0,
+ 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xa, 0xff,
+ 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff,
+ 0xf0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0xa, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xd0, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xf9,
+ 0xa, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x2f,
+ 0xff, 0xff, 0x30, 0xaf, 0xff, 0xd0, 0x0, 0x0,
+ 0x1, 0x7f, 0xff, 0xff, 0xa0, 0xa, 0xff, 0xff,
+ 0xaa, 0xaa, 0xbc, 0xff, 0xff, 0xff, 0xd0, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc1, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x70, 0x0, 0x0, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xfd, 0xa5, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0045 "E" */
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0xaf, 0xff, 0xe9, 0x99, 0x99, 0x99, 0x99, 0x91,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xf7, 0x77, 0x77, 0x77, 0x76, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xe9, 0x99, 0x99, 0x99, 0x99, 0x94,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8,
+
+ /* U+0046 "F" */
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0xaf, 0xff, 0xf9, 0x99, 0x99, 0x99, 0x99, 0x91,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xf9, 0x99, 0x99, 0x99, 0x98, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0047 "G" */
+ 0x0, 0x0, 0x0, 0x48, 0xce, 0xff, 0xdb, 0x72,
+ 0x0, 0x0, 0x0, 0x0, 0x4d, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xa1, 0x0, 0x0, 0x9, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x30, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xdd, 0xef, 0xff, 0xff, 0xb0,
+ 0x6, 0xff, 0xff, 0xfa, 0x30, 0x0, 0x3, 0xaf,
+ 0xfa, 0x0, 0xe, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x4, 0x70, 0x0, 0x6f, 0xff, 0xf8, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff,
+ 0xf1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xcf, 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xb0, 0x0, 0x0, 0x0,
+ 0x0, 0x5c, 0xcc, 0x80, 0xcf, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xb0, 0xaf, 0xff,
+ 0xf1, 0x0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xb0,
+ 0x6f, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xb0, 0xe, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x7f, 0xff, 0xb0, 0x6, 0xff, 0xff, 0xfa,
+ 0x30, 0x0, 0x2, 0xff, 0xff, 0xb0, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xdd, 0xef, 0xff, 0xff, 0xc0,
+ 0x0, 0x9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc0, 0x0, 0x0, 0x4d, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xc4, 0x0, 0x0, 0x0, 0x0, 0x49,
+ 0xce, 0xff, 0xdb, 0x72, 0x0, 0x0,
+
+ /* U+0048 "H" */
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x3, 0xff,
+ 0xff, 0x4a, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0,
+ 0x3f, 0xff, 0xf4, 0xaf, 0xff, 0xd0, 0x0, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0x4a, 0xff, 0xfd, 0x0,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xf4, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x3, 0xff, 0xff, 0x4a,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff,
+ 0xf4, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x3,
+ 0xff, 0xff, 0x4a, 0xff, 0xff, 0xcc, 0xcc, 0xcc,
+ 0xcc, 0xff, 0xff, 0xf4, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x4a, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0xaf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x4a, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x3f,
+ 0xff, 0xf4, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0,
+ 0x3, 0xff, 0xff, 0x4a, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x3f, 0xff, 0xf4, 0xaf, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0x3, 0xff, 0xff, 0x4a, 0xff,
+ 0xfd, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff, 0xf4,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x3, 0xff,
+ 0xff, 0x4a, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0,
+ 0x3f, 0xff, 0xf4, 0xaf, 0xff, 0xd0, 0x0, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0x40,
+
+ /* U+0049 "I" */
+ 0xaf, 0xff, 0xda, 0xff, 0xfd, 0xaf, 0xff, 0xda,
+ 0xff, 0xfd, 0xaf, 0xff, 0xda, 0xff, 0xfd, 0xaf,
+ 0xff, 0xda, 0xff, 0xfd, 0xaf, 0xff, 0xda, 0xff,
+ 0xfd, 0xaf, 0xff, 0xda, 0xff, 0xfd, 0xaf, 0xff,
+ 0xda, 0xff, 0xfd, 0xaf, 0xff, 0xda, 0xff, 0xfd,
+ 0xaf, 0xff, 0xda, 0xff, 0xfd, 0xaf, 0xff, 0xd0,
+
+ /* U+004A "J" */
+ 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0x6f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0x39, 0x99,
+ 0x99, 0x9c, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0,
+ 0x8, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x8,
+ 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff,
+ 0xff, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff,
+ 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0x0,
+ 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0x0, 0x0,
+ 0x0, 0x0, 0x8, 0xff, 0xff, 0x0, 0x0, 0x0,
+ 0x0, 0x8, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0,
+ 0x8, 0xff, 0xff, 0x0, 0x24, 0x0, 0x0, 0xb,
+ 0xff, 0xfe, 0x1, 0xdf, 0x50, 0x0, 0x4f, 0xff,
+ 0xfa, 0xc, 0xff, 0xfd, 0xbc, 0xff, 0xff, 0xf5,
+ 0x2e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3,
+ 0xdf, 0xff, 0xff, 0xff, 0xfc, 0x10, 0x0, 0x5,
+ 0xad, 0xff, 0xea, 0x50, 0x0,
+
+ /* U+004B "K" */
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x1d, 0xff,
+ 0xfe, 0x20, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x1,
+ 0xdf, 0xff, 0xe2, 0x0, 0xaf, 0xff, 0xd0, 0x0,
+ 0x0, 0x1d, 0xff, 0xff, 0x30, 0x0, 0xaf, 0xff,
+ 0xd0, 0x0, 0x1, 0xdf, 0xff, 0xf3, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x1d, 0xff, 0xff, 0x40,
+ 0x0, 0x0, 0xaf, 0xff, 0xd0, 0x1, 0xcf, 0xff,
+ 0xf4, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xd0, 0xc,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0xaf, 0xff,
+ 0xd0, 0xcf, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xec, 0xff, 0xff, 0xd0, 0x0, 0x0,
+ 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0xaf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xd1, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xff, 0x80, 0xbf, 0xff, 0xfb, 0x0,
+ 0x0, 0x0, 0xaf, 0xff, 0xf8, 0x0, 0xd, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0xaf, 0xff, 0xf0, 0x0,
+ 0x2, 0xef, 0xff, 0xf5, 0x0, 0x0, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0x30, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x5, 0xff, 0xff,
+ 0xe1, 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0,
+ 0x8f, 0xff, 0xfc, 0x0, 0xaf, 0xff, 0xd0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0x90,
+
+ /* U+004C "L" */
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0xa,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf,
+ 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff, 0xfd,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0x0, 0xa, 0xff, 0xfd, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xd0, 0x0,
+ 0x0, 0x0, 0x0, 0xa, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xfd, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0,
+ 0x0, 0xa, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0xaa, 0xaa, 0xaa, 0xaa, 0xa6,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xba,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xaf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0,
+
+ /* U+004D "M" */
+ 0xaf, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xaf, 0xff, 0x5a, 0xff, 0xfe, 0x10, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff, 0xf5, 0xaf,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd,
+ 0xff, 0xff, 0x5a, 0xff, 0xff, 0xf4, 0x0, 0x0,
+ 0x0, 0x0, 0x8, 0xff, 0xff, 0xf5, 0xaf, 0xff,
+ 0xff, 0xd0, 0x0, 0x0, 0x0, 0x2, 0xff, 0xff,
+ 0xff, 0x5a, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0,
+ 0x0, 0xbf, 0xff, 0xff, 0xf6, 0xaf, 0xff, 0xff,
+ 0xff, 0x20, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff,
+ 0x6a, 0xff, 0xff, 0xff, 0xfc, 0x0, 0x0, 0x1e,
+ 0xff, 0xff, 0xff, 0xf6, 0xaf, 0xff, 0xae, 0xff,
+ 0xf6, 0x0, 0x9, 0xff, 0xf8, 0xdf, 0xff, 0x6a,
+ 0xff, 0xf9, 0x5f, 0xff, 0xe1, 0x3, 0xff, 0xfd,
+ 0xd, 0xff, 0xf6, 0xaf, 0xff, 0x90, 0xbf, 0xff,
+ 0xa0, 0xcf, 0xff, 0x40, 0xdf, 0xff, 0x6a, 0xff,
+ 0xf9, 0x1, 0xff, 0xff, 0xbf, 0xff, 0xa0, 0xd,
+ 0xff, 0xf6, 0xaf, 0xff, 0x90, 0x7, 0xff, 0xff,
+ 0xff, 0xf1, 0x0, 0xdf, 0xff, 0x6a, 0xff, 0xf9,
+ 0x0, 0xd, 0xff, 0xff, 0xf7, 0x0, 0xd, 0xff,
+ 0xf6, 0xaf, 0xff, 0x90, 0x0, 0x3f, 0xff, 0xfd,
+ 0x0, 0x0, 0xdf, 0xff, 0x6a, 0xff, 0xf9, 0x0,
+ 0x0, 0x9f, 0xff, 0x40, 0x0, 0xd, 0xff, 0xf6,
+ 0xaf, 0xff, 0x90, 0x0, 0x1, 0xef, 0xa0, 0x0,
+ 0x0, 0xdf, 0xff, 0x6a, 0xff, 0xf9, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xf6, 0xaf,
+ 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xdf, 0xff, 0x60,
+
+ /* U+004E "N" */
+ 0xaf, 0xff, 0x70, 0x0, 0x0, 0x0, 0x3, 0xff,
+ 0xff, 0x4a, 0xff, 0xff, 0x50, 0x0, 0x0, 0x0,
+ 0x3f, 0xff, 0xf4, 0xaf, 0xff, 0xff, 0x30, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0x4a, 0xff, 0xff, 0xfe,
+ 0x10, 0x0, 0x0, 0x3f, 0xff, 0xf4, 0xaf, 0xff,
+ 0xff, 0xfc, 0x0, 0x0, 0x3, 0xff, 0xff, 0x4a,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x3f, 0xff,
+ 0xf4, 0xaf, 0xff, 0xff, 0xff, 0xf8, 0x0, 0x3,
+ 0xff, 0xff, 0x4a, 0xff, 0xfe, 0xef, 0xff, 0xf5,
+ 0x0, 0x3f, 0xff, 0xf4, 0xaf, 0xff, 0xc3, 0xff,
+ 0xff, 0xf3, 0x3, 0xff, 0xff, 0x4a, 0xff, 0xfc,
+ 0x5, 0xff, 0xff, 0xe1, 0x3f, 0xff, 0xf4, 0xaf,
+ 0xff, 0xc0, 0x8, 0xff, 0xff, 0xc3, 0xff, 0xff,
+ 0x4a, 0xff, 0xfc, 0x0, 0xb, 0xff, 0xff, 0xdf,
+ 0xff, 0xf4, 0xaf, 0xff, 0xc0, 0x0, 0xd, 0xff,
+ 0xff, 0xff, 0xff, 0x4a, 0xff, 0xfc, 0x0, 0x0,
+ 0x1e, 0xff, 0xff, 0xff, 0xf4, 0xaf, 0xff, 0xc0,
+ 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0x4a, 0xff,
+ 0xfc, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xf4,
+ 0xaf, 0xff, 0xc0, 0x0, 0x0, 0x0, 0x8f, 0xff,
+ 0xff, 0x4a, 0xff, 0xfc, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xf4, 0xaf, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0x0, 0xcf, 0xff, 0x40,
+
+ /* U+004F "O" */
+ 0x0, 0x0, 0x0, 0x38, 0xce, 0xff, 0xdb, 0x61,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x4c, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x91, 0x0, 0x0, 0x0, 0x8,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x40,
+ 0x0, 0x0, 0x9f, 0xff, 0xff, 0xfe, 0xcd, 0xff,
+ 0xff, 0xff, 0xf3, 0x0, 0x6, 0xff, 0xff, 0xfa,
+ 0x30, 0x0, 0x5, 0xdf, 0xff, 0xfe, 0x10, 0xe,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0x90, 0x5f, 0xff, 0xf8, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xdf, 0xff, 0xf0, 0xaf, 0xff, 0xf1,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xf4,
+ 0xcf, 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x2f, 0xff, 0xf7, 0xef, 0xff, 0xb0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1f, 0xff, 0xf8, 0xcf, 0xff,
+ 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff,
+ 0xf7, 0xaf, 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x6f, 0xff, 0xf4, 0x5f, 0xff, 0xf8, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xdf, 0xff, 0xf0, 0xe,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0x90, 0x6, 0xff, 0xff, 0xfa, 0x20, 0x0,
+ 0x5, 0xdf, 0xff, 0xfe, 0x10, 0x0, 0x9f, 0xff,
+ 0xff, 0xfe, 0xdd, 0xff, 0xff, 0xff, 0xf4, 0x0,
+ 0x0, 0x9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x40, 0x0, 0x0, 0x0, 0x4c, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xa1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x48, 0xce, 0xff, 0xdb, 0x71, 0x0, 0x0,
+ 0x0,
+
+ /* U+0050 "P" */
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xdb, 0x61, 0x0,
+ 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x70, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0xaf, 0xff, 0xfa, 0xaa, 0xab,
+ 0xef, 0xff, 0xff, 0x70, 0xaf, 0xff, 0xd0, 0x0,
+ 0x0, 0x5, 0xff, 0xff, 0xe0, 0xaf, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0x8f, 0xff, 0xf2, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x4f, 0xff, 0xf4, 0xaf,
+ 0xff, 0xd0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xf4,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0xbf, 0xff,
+ 0xf1, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x4b, 0xff,
+ 0xff, 0xc0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x30, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf5, 0x0, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfb, 0x20, 0x0, 0xaf, 0xff, 0xfa,
+ 0xaa, 0xa9, 0x85, 0x10, 0x0, 0x0, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf,
+ 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0,
+
+ /* U+0051 "Q" */
+ 0x0, 0x0, 0x0, 0x38, 0xce, 0xff, 0xdb, 0x61,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0xcf, 0xff,
+ 0xff, 0xff, 0xff, 0xf9, 0x10, 0x0, 0x0, 0x0,
+ 0x8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
+ 0x40, 0x0, 0x0, 0x9, 0xff, 0xff, 0xff, 0xec,
+ 0xdf, 0xff, 0xff, 0xff, 0x40, 0x0, 0x6, 0xff,
+ 0xff, 0xfa, 0x20, 0x0, 0x5, 0xdf, 0xff, 0xfe,
+ 0x10, 0x0, 0xef, 0xff, 0xf5, 0x0, 0x0, 0x0,
+ 0x0, 0xbf, 0xff, 0xf9, 0x0, 0x5f, 0xff, 0xf8,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xdf, 0xff, 0xf0,
+ 0xa, 0xff, 0xff, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x6, 0xff, 0xff, 0x40, 0xcf, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff, 0xf7, 0xe,
+ 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+ 0xff, 0xff, 0x80, 0xcf, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2f, 0xff, 0xf7, 0xa, 0xff,
+ 0xff, 0x10, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff,
+ 0xff, 0x40, 0x5f, 0xff, 0xf8, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xdf, 0xff, 0xf0, 0x0, 0xef, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xf9,
+ 0x0, 0x6, 0xff, 0xff, 0xfa, 0x20, 0x0, 0x5,
+ 0xdf, 0xff, 0xfe, 0x10, 0x0, 0x9, 0xff, 0xff,
+ 0xff, 0xed, 0xdf, 0xff, 0xff, 0xff, 0x40, 0x0,
+ 0x0, 0x9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x40, 0x0, 0x0, 0x0, 0x4, 0xcf, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x10, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x48, 0xef, 0xff, 0xff, 0xf2, 0x0,
+ 0x0, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3e,
+ 0xff, 0xff, 0xd7, 0x57, 0xdf, 0x80, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2d, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a,
+ 0xff, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3, 0x9d, 0xef, 0xea, 0x30,
+ 0x0,
+
+ /* U+0052 "R" */
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xdb, 0x61, 0x0,
+ 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x80, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0xaf, 0xff, 0xfa, 0xaa, 0xab,
+ 0xef, 0xff, 0xff, 0x70, 0xaf, 0xff, 0xd0, 0x0,
+ 0x0, 0x5, 0xff, 0xff, 0xe0, 0xaf, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0x8f, 0xff, 0xf2, 0xaf, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x4f, 0xff, 0xf4, 0xaf,
+ 0xff, 0xd0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xf4,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0xbf, 0xff,
+ 0xf1, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x4b, 0xff,
+ 0xff, 0xc0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x30, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf4, 0x0, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x10, 0x0, 0xaf, 0xff, 0xf8,
+ 0x88, 0xaf, 0xff, 0xfc, 0x0, 0x0, 0xaf, 0xff,
+ 0xd0, 0x0, 0x5, 0xff, 0xff, 0x80, 0x0, 0xaf,
+ 0xff, 0xd0, 0x0, 0x0, 0xaf, 0xff, 0xf3, 0x0,
+ 0xaf, 0xff, 0xd0, 0x0, 0x0, 0xe, 0xff, 0xfe,
+ 0x0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x4, 0xff,
+ 0xff, 0xa0, 0xaf, 0xff, 0xd0, 0x0, 0x0, 0x0,
+ 0x9f, 0xff, 0xf5,
+
+ /* U+0053 "S" */
+ 0x0, 0x0, 0x16, 0xbd, 0xef, 0xed, 0xa7, 0x20,
+ 0x0, 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x10, 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x0, 0x4, 0xff, 0xff, 0xfc, 0x99,
+ 0xac, 0xff, 0xf7, 0x0, 0xa, 0xff, 0xff, 0x40,
+ 0x0, 0x0, 0x16, 0xc1, 0x0, 0xc, 0xff, 0xfb,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0xff,
+ 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7,
+ 0xff, 0xff, 0xfa, 0x62, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xcf, 0xff, 0xff, 0xff, 0xea, 0x61, 0x0,
+ 0x0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x91, 0x0, 0x0, 0x0, 0x6, 0xbf, 0xff, 0xff,
+ 0xff, 0xfe, 0x10, 0x0, 0x0, 0x0, 0x0, 0x36,
+ 0xbf, 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0xdf, 0xff, 0xf0, 0x0, 0x30, 0x0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf0, 0x2, 0xfc,
+ 0x61, 0x0, 0x0, 0x1, 0xdf, 0xff, 0xe0, 0x9,
+ 0xff, 0xff, 0xda, 0x99, 0xbf, 0xff, 0xff, 0x80,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
+ 0x0, 0x3, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x90, 0x0, 0x0, 0x1, 0x6a, 0xde, 0xff, 0xdb,
+ 0x72, 0x0, 0x0,
+
+ /* U+0054 "T" */
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf3, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf3, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf3, 0x8a, 0xaa, 0xaa, 0xdf, 0xff,
+ 0xfa, 0xaa, 0xaa, 0xa1, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x9f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x9f, 0xff, 0xe0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x9f, 0xff, 0xe0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xe0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff,
+ 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x9f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x9f, 0xff, 0xe0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x9f, 0xff, 0xe0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xe0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xe0,
+ 0x0, 0x0, 0x0,
+
+ /* U+0055 "U" */
+ 0xdf, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x9, 0xff,
+ 0xfe, 0xdf, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x9,
+ 0xff, 0xfe, 0xdf, 0xff, 0xb0, 0x0, 0x0, 0x0,
+ 0x9, 0xff, 0xfe, 0xdf, 0xff, 0xb0, 0x0, 0x0,
+ 0x0, 0x9, 0xff, 0xfe, 0xdf, 0xff, 0xb0, 0x0,
+ 0x0, 0x0, 0x9, 0xff, 0xfe, 0xdf, 0xff, 0xb0,
+ 0x0, 0x0, 0x0, 0x9, 0xff, 0xfe, 0xdf, 0xff,
+ 0xb0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xfe, 0xdf,
+ 0xff, 0xb0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xfe,
+ 0xdf, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x9, 0xff,
+ 0xfe, 0xdf, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x9,
+ 0xff, 0xfe, 0xdf, 0xff, 0xb0, 0x0, 0x0, 0x0,
+ 0x9, 0xff, 0xfe, 0xcf, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0xa, 0xff, 0xfd, 0xaf, 0xff, 0xf0, 0x0,
+ 0x0, 0x0, 0xe, 0xff, 0xfb, 0x6f, 0xff, 0xf7,
+ 0x0, 0x0, 0x0, 0x5f, 0xff, 0xf7, 0x1f, 0xff,
+ 0xff, 0x70, 0x0, 0x6, 0xff, 0xff, 0xf2, 0x8,
+ 0xff, 0xff, 0xff, 0xdd, 0xff, 0xff, 0xff, 0x90,
+ 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb,
+ 0x0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x80, 0x0, 0x0, 0x0, 0x17, 0xbe, 0xff, 0xeb,
+ 0x71, 0x0, 0x0,
+
+ /* U+0056 "V" */
+ 0x1f, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x9, 0xff, 0xfe, 0x0, 0x9f, 0xff, 0xf4, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xff, 0xff, 0x80, 0x2,
+ 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xf1, 0x0, 0xb, 0xff, 0xff, 0x20, 0x0,
+ 0x0, 0x0, 0xe, 0xff, 0xf9, 0x0, 0x0, 0x4f,
+ 0xff, 0xf9, 0x0, 0x0, 0x0, 0x5, 0xff, 0xff,
+ 0x20, 0x0, 0x0, 0xcf, 0xff, 0xf1, 0x0, 0x0,
+ 0x0, 0xcf, 0xff, 0xb0, 0x0, 0x0, 0x5, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x3f, 0xff, 0xf4, 0x0,
+ 0x0, 0x0, 0xe, 0xff, 0xfe, 0x0, 0x0, 0xb,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x7f, 0xff,
+ 0xf6, 0x0, 0x2, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xff, 0xd0, 0x0, 0x9f, 0xff,
+ 0xe0, 0x0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xff,
+ 0x40, 0x1f, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2f, 0xff, 0xfb, 0x7, 0xff, 0xff, 0x10,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xf2,
+ 0xef, 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x3, 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xc, 0xff, 0xff, 0xff,
+ 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5f, 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7,
+ 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1f, 0xff, 0xfe, 0x0, 0x0,
+ 0x0, 0x0, 0x0,
+
+ /* U+0057 "W" */
+ 0x3f, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0, 0xdf,
+ 0xff, 0xb0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xfc,
+ 0xe, 0xff, 0xfc, 0x0, 0x0, 0x0, 0x2, 0xff,
+ 0xff, 0xf1, 0x0, 0x0, 0x0, 0xe, 0xff, 0xf7,
+ 0x8, 0xff, 0xff, 0x20, 0x0, 0x0, 0x8, 0xff,
+ 0xff, 0xf6, 0x0, 0x0, 0x0, 0x3f, 0xff, 0xf1,
+ 0x3, 0xff, 0xff, 0x70, 0x0, 0x0, 0xd, 0xff,
+ 0xff, 0xfb, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xc0,
+ 0x0, 0xdf, 0xff, 0xc0, 0x0, 0x0, 0x3f, 0xff,
+ 0xff, 0xff, 0x10, 0x0, 0x0, 0xef, 0xff, 0x60,
+ 0x0, 0x8f, 0xff, 0xf2, 0x0, 0x0, 0x8f, 0xff,
+ 0xff, 0xff, 0x60, 0x0, 0x4, 0xff, 0xff, 0x10,
+ 0x0, 0x3f, 0xff, 0xf7, 0x0, 0x0, 0xef, 0xff,
+ 0x9f, 0xff, 0xb0, 0x0, 0x9, 0xff, 0xfc, 0x0,
+ 0x0, 0xd, 0xff, 0xfc, 0x0, 0x3, 0xff, 0xfc,
+ 0x2f, 0xff, 0xf1, 0x0, 0xe, 0xff, 0xf6, 0x0,
+ 0x0, 0x8, 0xff, 0xff, 0x20, 0x9, 0xff, 0xf7,
+ 0xc, 0xff, 0xf6, 0x0, 0x4f, 0xff, 0xf1, 0x0,
+ 0x0, 0x2, 0xff, 0xff, 0x70, 0xe, 0xff, 0xf2,
+ 0x7, 0xff, 0xfb, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0x0, 0x0, 0xdf, 0xff, 0xc0, 0x4f, 0xff, 0xc0,
+ 0x2, 0xff, 0xff, 0x10, 0xff, 0xff, 0x60, 0x0,
+ 0x0, 0x0, 0x8f, 0xff, 0xf2, 0x9f, 0xff, 0x70,
+ 0x0, 0xdf, 0xff, 0x64, 0xff, 0xff, 0x10, 0x0,
+ 0x0, 0x0, 0x2f, 0xff, 0xf7, 0xff, 0xff, 0x10,
+ 0x0, 0x7f, 0xff, 0xba, 0xff, 0xfb, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xfc, 0x0,
+ 0x0, 0x2f, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0,
+ 0x0, 0x0, 0x7, 0xff, 0xff, 0xff, 0xf6, 0x0,
+ 0x0, 0xd, 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0,
+ 0x0, 0x0, 0x2, 0xff, 0xff, 0xff, 0xf1, 0x0,
+ 0x0, 0x7, 0xff, 0xff, 0xff, 0xb0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xdf, 0xff, 0xff, 0xc0, 0x0,
+ 0x0, 0x2, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xff, 0x60, 0x0,
+ 0x0, 0x0, 0xdf, 0xff, 0xff, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2f, 0xff, 0xff, 0x10, 0x0,
+ 0x0, 0x0, 0x7f, 0xff, 0xfb, 0x0, 0x0, 0x0,
+
+ /* U+0058 "X" */
+ 0x3f, 0xff, 0xfe, 0x10, 0x0, 0x0, 0x0, 0xdf,
+ 0xff, 0xe2, 0x7, 0xff, 0xff, 0xa0, 0x0, 0x0,
+ 0x9, 0xff, 0xff, 0x50, 0x0, 0xbf, 0xff, 0xf6,
+ 0x0, 0x0, 0x4f, 0xff, 0xf9, 0x0, 0x0, 0x1e,
+ 0xff, 0xff, 0x20, 0x1, 0xef, 0xff, 0xc0, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0xd0, 0xb, 0xff, 0xff,
+ 0x20, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf9, 0x7f,
+ 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0xb, 0xff,
+ 0xff, 0xff, 0xff, 0x90, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0xef, 0xff, 0xff, 0xfc, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4f, 0xff, 0xff, 0xf2, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xff,
+ 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f,
+ 0xff, 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4, 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0,
+ 0x0, 0x0, 0x1e, 0xff, 0xfe, 0xef, 0xff, 0xe2,
+ 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xf3, 0x4f,
+ 0xff, 0xfc, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff,
+ 0x70, 0x8, 0xff, 0xff, 0x80, 0x0, 0x0, 0x4f,
+ 0xff, 0xfc, 0x0, 0x0, 0xcf, 0xff, 0xf4, 0x0,
+ 0x1, 0xef, 0xff, 0xf1, 0x0, 0x0, 0x2f, 0xff,
+ 0xfe, 0x10, 0xc, 0xff, 0xff, 0x50, 0x0, 0x0,
+ 0x6, 0xff, 0xff, 0xc0, 0x8f, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0xaf, 0xff, 0xf8,
+
+ /* U+0059 "Y" */
+ 0x1f, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0, 0x6,
+ 0xff, 0xff, 0x10, 0x7f, 0xff, 0xf5, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xff, 0x60, 0x0, 0xdf, 0xff,
+ 0xe0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xc0, 0x0,
+ 0x3, 0xff, 0xff, 0x90, 0x0, 0x0, 0x4f, 0xff,
+ 0xf3, 0x0, 0x0, 0x9, 0xff, 0xff, 0x30, 0x0,
+ 0xd, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x1e, 0xff,
+ 0xfc, 0x0, 0x8, 0xff, 0xfe, 0x0, 0x0, 0x0,
+ 0x0, 0x5f, 0xff, 0xf7, 0x2, 0xff, 0xff, 0x40,
+ 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xf1, 0xcf,
+ 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xff,
+ 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x8, 0xff, 0xff, 0xff, 0xf7, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff, 0xfd,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f,
+ 0xff, 0xff, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xff, 0xff, 0xf0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xfc, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf,
+ 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xc, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xb0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff,
+ 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xcf, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+005A "Z" */
+ 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xc0, 0x7a, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xcf, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0xdf, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xc, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xaf, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x8, 0xff, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x6f, 0xff, 0xfe, 0x10, 0x0,
+ 0x0, 0x0, 0x0, 0x4, 0xff, 0xff, 0xf2, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff, 0xff, 0x40,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0xef, 0xff, 0xf6,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff, 0xff,
+ 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff,
+ 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6f,
+ 0xff, 0xff, 0xba, 0xaa, 0xaa, 0xaa, 0xaa, 0xa2,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf3,
+
+ /* U+005B "[" */
+ 0xaf, 0xff, 0xff, 0xfe, 0xaf, 0xff, 0xff, 0xfe,
+ 0xaf, 0xff, 0xff, 0xfe, 0xaf, 0xff, 0xc5, 0x54,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xb0, 0x0, 0xaf, 0xff, 0xb0, 0x0,
+ 0xaf, 0xff, 0xc5, 0x54, 0xaf, 0xff, 0xff, 0xfe,
+ 0xaf, 0xff, 0xff, 0xfe, 0xaf, 0xff, 0xff, 0xfe,
+
+ /* U+005C "\\" */
+ 0xf, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0xa,
+ 0xff, 0xf3, 0x0, 0x0, 0x0, 0x0, 0x5, 0xff,
+ 0xf9, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xfe,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0x40,
+ 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff, 0xa0, 0x0,
+ 0x0, 0x0, 0x0, 0xe, 0xff, 0xf0, 0x0, 0x0,
+ 0x0, 0x0, 0x8, 0xff, 0xf5, 0x0, 0x0, 0x0,
+ 0x0, 0x2, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xdf, 0xff, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x7f, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x1f,
+ 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff,
+ 0xf2, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xf7,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0xff, 0xfd, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0x30, 0x0,
+ 0x0, 0x0, 0x0, 0x5f, 0xff, 0x80, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xe0, 0x0, 0x0, 0x0,
+ 0x0, 0xa, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0,
+ 0x4, 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff,
+ 0xa0, 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xf1,
+ 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xf6, 0x0,
+ 0x0, 0x0, 0x0, 0x2, 0xff, 0xfb,
+
+ /* U+005D "]" */
+ 0x9f, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff,
+ 0x9f, 0xff, 0xff, 0xff, 0x35, 0x59, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x0, 0x6, 0xff, 0xff, 0x0, 0x6, 0xff, 0xff,
+ 0x35, 0x59, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff,
+ 0x9f, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff,
+
+ /* U+005E "^" */
+ 0x0, 0x0, 0x3, 0x88, 0x82, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xcf, 0xff, 0x90, 0x0, 0x0, 0x0,
+ 0x0, 0x3f, 0xff, 0xff, 0x10, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x1,
+ 0xff, 0xf6, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x8f,
+ 0xfa, 0xd, 0xff, 0x50, 0x0, 0x0, 0xe, 0xff,
+ 0x40, 0x7f, 0xfc, 0x0, 0x0, 0x6, 0xff, 0xd0,
+ 0x1, 0xff, 0xf3, 0x0, 0x0, 0xdf, 0xf6, 0x0,
+ 0x9, 0xff, 0xa0, 0x0, 0x4f, 0xff, 0x0, 0x0,
+ 0x3f, 0xff, 0x10, 0xb, 0xff, 0x90, 0x0, 0x0,
+ 0xcf, 0xf8, 0x2, 0xff, 0xf2, 0x0, 0x0, 0x5,
+ 0xff, 0xe0,
+
+ /* U+005F "_" */
+ 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff,
+
+ /* U+0060 "`" */
+ 0x1c, 0xff, 0xfa, 0x0, 0x0, 0x9, 0xff, 0xf9,
+ 0x0, 0x0, 0x6, 0xff, 0xf8, 0x0, 0x0, 0x3,
+ 0xef, 0xf7,
+
+ /* U+0061 "a" */
+ 0x0, 0x27, 0xbe, 0xff, 0xeb, 0x71, 0x0, 0x1,
+ 0xbf, 0xff, 0xff, 0xff, 0xff, 0xe5, 0x0, 0x1f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x8f,
+ 0xfb, 0x76, 0x8d, 0xff, 0xff, 0xc0, 0x1, 0x91,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x2f, 0xff, 0xf4, 0x0, 0x2, 0x56,
+ 0x78, 0x88, 0xff, 0xff, 0x50, 0x3c, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf5, 0x3f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x5b, 0xff, 0xfd, 0x30, 0x0,
+ 0xf, 0xff, 0xf5, 0xef, 0xff, 0x50, 0x0, 0x2,
+ 0xff, 0xff, 0x5d, 0xff, 0xfa, 0x0, 0x1, 0xcf,
+ 0xff, 0xf5, 0x7f, 0xff, 0xfe, 0xbc, 0xff, 0xff,
+ 0xff, 0x50, 0xaf, 0xff, 0xff, 0xff, 0xcc, 0xff,
+ 0xf5, 0x0, 0x5b, 0xef, 0xec, 0x70, 0xcf, 0xff,
+ 0x50,
+
+ /* U+0062 "b" */
+ 0x99, 0x99, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xff, 0xff, 0x62, 0x8d, 0xfe,
+ 0xd8, 0x20, 0x0, 0xf, 0xff, 0xfb, 0xff, 0xff,
+ 0xff, 0xff, 0x80, 0x0, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xa0, 0xf, 0xff, 0xff, 0xfe,
+ 0x98, 0xcf, 0xff, 0xff, 0x50, 0xff, 0xff, 0xf9,
+ 0x0, 0x0, 0x4f, 0xff, 0xfd, 0xf, 0xff, 0xff,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf2, 0xff, 0xff,
+ 0x90, 0x0, 0x0, 0x2, 0xff, 0xff, 0x4f, 0xff,
+ 0xf7, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf6, 0xff,
+ 0xff, 0x90, 0x0, 0x0, 0x2, 0xff, 0xff, 0x4f,
+ 0xff, 0xfe, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf2,
+ 0xff, 0xff, 0xf8, 0x0, 0x0, 0x4f, 0xff, 0xfd,
+ 0xf, 0xff, 0xff, 0xfd, 0x98, 0xcf, 0xff, 0xff,
+ 0x50, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x90, 0xf, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff,
+ 0x80, 0x0, 0xff, 0xff, 0x32, 0x9d, 0xfe, 0xd8,
+ 0x20, 0x0, 0x0,
+
+ /* U+0063 "c" */
+ 0x0, 0x0, 0x3, 0x9d, 0xef, 0xdb, 0x60, 0x0,
+ 0x0, 0x0, 0x1b, 0xff, 0xff, 0xff, 0xff, 0xe3,
+ 0x0, 0x0, 0x2e, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf3, 0x0, 0xd, 0xff, 0xff, 0xe9, 0x8b, 0xff,
+ 0xff, 0xc0, 0x7, 0xff, 0xff, 0x90, 0x0, 0x4,
+ 0xff, 0x81, 0x0, 0xdf, 0xff, 0xc0, 0x0, 0x0,
+ 0x3, 0x10, 0x0, 0xf, 0xff, 0xf7, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xff, 0xff, 0x50, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf7, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xdf, 0xff, 0xc0,
+ 0x0, 0x0, 0x3, 0x10, 0x0, 0x7, 0xff, 0xff,
+ 0x90, 0x0, 0x3, 0xff, 0x81, 0x0, 0xd, 0xff,
+ 0xff, 0xe9, 0x8b, 0xff, 0xff, 0xc0, 0x0, 0x2e,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x0,
+ 0x1b, 0xff, 0xff, 0xff, 0xff, 0xe3, 0x0, 0x0,
+ 0x0, 0x3, 0x9d, 0xef, 0xdb, 0x60, 0x0, 0x0,
+
+ /* U+0064 "d" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x99,
+ 0x93, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf,
+ 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xf5, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xf, 0xff, 0xf5, 0x0, 0x0,
+ 0x6, 0xbe, 0xfe, 0xb4, 0xf, 0xff, 0xf5, 0x0,
+ 0x4, 0xef, 0xff, 0xff, 0xff, 0xaf, 0xff, 0xf5,
+ 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf5, 0x1, 0xef, 0xff, 0xfe, 0x98, 0xbf, 0xff,
+ 0xff, 0xf5, 0x7, 0xff, 0xff, 0x90, 0x0, 0x4,
+ 0xff, 0xff, 0xf5, 0xc, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x6f, 0xff, 0xf5, 0xe, 0xff, 0xf8, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xf5, 0xf, 0xff, 0xf6,
+ 0x0, 0x0, 0x0, 0xf, 0xff, 0xf5, 0xe, 0xff,
+ 0xf8, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf5, 0xc,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xf5,
+ 0x7, 0xff, 0xff, 0x90, 0x0, 0x3, 0xff, 0xff,
+ 0xf5, 0x1, 0xef, 0xff, 0xfe, 0x98, 0xbf, 0xff,
+ 0xff, 0xf5, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf5, 0x0, 0x4, 0xef, 0xff, 0xff,
+ 0xff, 0xad, 0xff, 0xf5, 0x0, 0x0, 0x6, 0xbe,
+ 0xfe, 0xb5, 0xd, 0xff, 0xf5,
+
+ /* U+0065 "e" */
+ 0x0, 0x0, 0x4, 0xad, 0xff, 0xd9, 0x40, 0x0,
+ 0x0, 0x0, 0x2c, 0xff, 0xff, 0xff, 0xff, 0xb1,
+ 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe1, 0x0, 0x1e, 0xff, 0xfe, 0x84, 0x48, 0xff,
+ 0xff, 0xb0, 0x8, 0xff, 0xfe, 0x20, 0x0, 0x2,
+ 0xff, 0xff, 0x30, 0xdf, 0xff, 0x70, 0x0, 0x0,
+ 0x8, 0xff, 0xf8, 0xf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xb1, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfc, 0xf, 0xff, 0xfc, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x50, 0xdf, 0xff, 0xa0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xff, 0xff,
+ 0x60, 0x0, 0x0, 0x3a, 0x0, 0x0, 0xd, 0xff,
+ 0xff, 0xd8, 0x78, 0xcf, 0xfa, 0x0, 0x0, 0x2e,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x0, 0x0,
+ 0x1b, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0,
+ 0x0, 0x3, 0x9c, 0xef, 0xec, 0x82, 0x0, 0x0,
+
+ /* U+0066 "f" */
+ 0x0, 0x0, 0x0, 0x24, 0x54, 0x10, 0x0, 0x0,
+ 0x3d, 0xff, 0xff, 0xf7, 0x0, 0x4, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0xe, 0xff, 0xff, 0xff, 0xe0,
+ 0x0, 0x4f, 0xff, 0xf7, 0x1, 0x30, 0x0, 0x6f,
+ 0xff, 0xe0, 0x0, 0x0, 0xcf, 0xff, 0xff, 0xff,
+ 0xff, 0xa0, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xa0,
+ 0xcf, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x34, 0xcf,
+ 0xff, 0xf4, 0x44, 0x30, 0x0, 0x7f, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0,
+ 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0,
+ 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0,
+ 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0,
+
+ /* U+0067 "g" */
+ 0x0, 0x0, 0x17, 0xce, 0xfe, 0xb5, 0x9, 0xff,
+ 0xf9, 0x0, 0x6, 0xff, 0xff, 0xff, 0xff, 0xca,
+ 0xff, 0xf9, 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf9, 0x4, 0xff, 0xff, 0xfa, 0x65,
+ 0x7d, 0xff, 0xff, 0xf9, 0xb, 0xff, 0xff, 0x30,
+ 0x0, 0x0, 0x9f, 0xff, 0xf9, 0xf, 0xff, 0xf8,
+ 0x0, 0x0, 0x0, 0xf, 0xff, 0xf9, 0x1f, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf9, 0xf,
+ 0xff, 0xf7, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf9,
+ 0xe, 0xff, 0xfe, 0x10, 0x0, 0x0, 0x6f, 0xff,
+ 0xf9, 0x8, 0xff, 0xff, 0xd5, 0x10, 0x28, 0xff,
+ 0xff, 0xf9, 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf9, 0x0, 0x2d, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0, 0x9f, 0xff,
+ 0xff, 0xfe, 0x4d, 0xff, 0xf8, 0x0, 0x0, 0x0,
+ 0x46, 0x76, 0x30, 0xe, 0xff, 0xf7, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff, 0xf4, 0x0,
+ 0xe, 0x81, 0x0, 0x0, 0x1, 0xdf, 0xff, 0xf0,
+ 0x0, 0x8f, 0xff, 0xc8, 0x77, 0xaf, 0xff, 0xff,
+ 0x90, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfd, 0x10, 0x0, 0x7e, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xb1, 0x0, 0x0, 0x0, 0x59, 0xce, 0xff,
+ 0xec, 0x83, 0x0, 0x0,
+
+ /* U+0068 "h" */
+ 0x66, 0x66, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x61, 0x7c, 0xef, 0xd9, 0x30, 0x0,
+ 0xff, 0xff, 0xbe, 0xff, 0xff, 0xff, 0xf7, 0x0,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50,
+ 0xff, 0xff, 0xff, 0xfc, 0xcf, 0xff, 0xff, 0xd0,
+ 0xff, 0xff, 0xfa, 0x0, 0x1, 0xdf, 0xff, 0xf2,
+ 0xff, 0xff, 0xd0, 0x0, 0x0, 0x4f, 0xff, 0xf5,
+ 0xff, 0xff, 0x80, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+
+ /* U+0069 "i" */
+ 0x3, 0xab, 0x70, 0x3f, 0xff, 0xf9, 0x7f, 0xff,
+ 0xfd, 0x4f, 0xff, 0xfa, 0x7, 0xef, 0xb1, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xf, 0xff, 0xf6, 0xf, 0xff, 0xf6, 0xf, 0xff,
+ 0xf6, 0xf, 0xff, 0xf6, 0xf, 0xff, 0xf6, 0xf,
+ 0xff, 0xf6, 0xf, 0xff, 0xf6, 0xf, 0xff, 0xf6,
+ 0xf, 0xff, 0xf6, 0xf, 0xff, 0xf6, 0xf, 0xff,
+ 0xf6, 0xf, 0xff, 0xf6, 0xf, 0xff, 0xf6, 0xf,
+ 0xff, 0xf6,
+
+ /* U+006A "j" */
+ 0x0, 0x0, 0x4, 0xdf, 0xc3, 0x0, 0x0, 0x1,
+ 0xff, 0xff, 0xe0, 0x0, 0x0, 0x4f, 0xff, 0xff,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xb0, 0x0, 0x0,
+ 0x2, 0x9b, 0x81, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xcf, 0xff, 0x90, 0x0, 0x0, 0xc, 0xff,
+ 0xf9, 0x0, 0x0, 0x0, 0xcf, 0xff, 0x90, 0x0,
+ 0x0, 0xc, 0xff, 0xf9, 0x0, 0x0, 0x0, 0xcf,
+ 0xff, 0x90, 0x0, 0x0, 0xc, 0xff, 0xf9, 0x0,
+ 0x0, 0x0, 0xcf, 0xff, 0x90, 0x0, 0x0, 0xc,
+ 0xff, 0xf9, 0x0, 0x0, 0x0, 0xcf, 0xff, 0x90,
+ 0x0, 0x0, 0xc, 0xff, 0xf9, 0x0, 0x0, 0x0,
+ 0xcf, 0xff, 0x90, 0x0, 0x0, 0xc, 0xff, 0xf9,
+ 0x0, 0x0, 0x0, 0xcf, 0xff, 0x90, 0x0, 0x0,
+ 0xc, 0xff, 0xf9, 0x0, 0x0, 0x0, 0xcf, 0xff,
+ 0x90, 0x0, 0x0, 0xf, 0xff, 0xf7, 0x0, 0x88,
+ 0x7c, 0xff, 0xff, 0x40, 0xe, 0xff, 0xff, 0xff,
+ 0xd0, 0x5, 0xff, 0xff, 0xff, 0xe2, 0x0, 0x3a,
+ 0xdf, 0xfd, 0x81, 0x0, 0x0,
+
+ /* U+006B "k" */
+ 0x66, 0x66, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0xbf, 0xff, 0xf7, 0xf, 0xff, 0xf6, 0x0, 0x0,
+ 0xcf, 0xff, 0xf7, 0x0, 0xff, 0xff, 0x60, 0x1,
+ 0xcf, 0xff, 0xf8, 0x0, 0xf, 0xff, 0xf6, 0x1,
+ 0xdf, 0xff, 0xf8, 0x0, 0x0, 0xff, 0xff, 0x62,
+ 0xdf, 0xff, 0xf8, 0x0, 0x0, 0xf, 0xff, 0xf8,
+ 0xef, 0xff, 0xf8, 0x0, 0x0, 0x0, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0xf, 0xff,
+ 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0, 0x0, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0xf,
+ 0xff, 0xff, 0xe6, 0xff, 0xff, 0xf3, 0x0, 0x0,
+ 0xff, 0xff, 0xf2, 0x6, 0xff, 0xff, 0xe1, 0x0,
+ 0xf, 0xff, 0xf8, 0x0, 0x9, 0xff, 0xff, 0xb0,
+ 0x0, 0xff, 0xff, 0x60, 0x0, 0xc, 0xff, 0xff,
+ 0x80, 0xf, 0xff, 0xf6, 0x0, 0x0, 0x1e, 0xff,
+ 0xff, 0x50, 0xff, 0xff, 0x60, 0x0, 0x0, 0x3f,
+ 0xff, 0xff, 0x20,
+
+ /* U+006C "l" */
+ 0x66, 0x66, 0x2f, 0xff, 0xf6, 0xff, 0xff, 0x6f,
+ 0xff, 0xf6, 0xff, 0xff, 0x6f, 0xff, 0xf6, 0xff,
+ 0xff, 0x6f, 0xff, 0xf6, 0xff, 0xff, 0x6f, 0xff,
+ 0xf6, 0xff, 0xff, 0x6f, 0xff, 0xf6, 0xff, 0xff,
+ 0x6f, 0xff, 0xf6, 0xff, 0xff, 0x6f, 0xff, 0xf6,
+ 0xff, 0xff, 0x6f, 0xff, 0xf6, 0xff, 0xff, 0x6f,
+ 0xff, 0xf6, 0xff, 0xff, 0x60,
+
+ /* U+006D "m" */
+ 0xff, 0xff, 0x32, 0x8d, 0xff, 0xc7, 0x0, 0x0,
+ 0x7c, 0xff, 0xda, 0x30, 0x0, 0xff, 0xff, 0x8f,
+ 0xff, 0xff, 0xff, 0xe2, 0x3e, 0xff, 0xff, 0xff,
+ 0xf8, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0xff,
+ 0xff, 0xff, 0xeb, 0xdf, 0xff, 0xff, 0xff, 0xfc,
+ 0xcf, 0xff, 0xff, 0xe0, 0xff, 0xff, 0xf8, 0x0,
+ 0x5, 0xff, 0xff, 0xfe, 0x30, 0x1, 0xcf, 0xff,
+ 0xf3, 0xff, 0xff, 0xc0, 0x0, 0x0, 0xbf, 0xff,
+ 0xf5, 0x0, 0x0, 0x3f, 0xff, 0xf5, 0xff, 0xff,
+ 0x70, 0x0, 0x0, 0x8f, 0xff, 0xf0, 0x0, 0x0,
+ 0xf, 0xff, 0xf7, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x7f, 0xff, 0xe0, 0x0, 0x0, 0xe, 0xff, 0xf7,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x7f, 0xff, 0xe0,
+ 0x0, 0x0, 0xe, 0xff, 0xf7, 0xff, 0xff, 0x60,
+ 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0, 0xe,
+ 0xff, 0xf7, 0xff, 0xff, 0x60, 0x0, 0x0, 0x7f,
+ 0xff, 0xe0, 0x0, 0x0, 0xe, 0xff, 0xf7, 0xff,
+ 0xff, 0x60, 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0,
+ 0x0, 0xe, 0xff, 0xf7, 0xff, 0xff, 0x60, 0x0,
+ 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0, 0xe, 0xff,
+ 0xf7, 0xff, 0xff, 0x60, 0x0, 0x0, 0x7f, 0xff,
+ 0xe0, 0x0, 0x0, 0xe, 0xff, 0xf7, 0xff, 0xff,
+ 0x60, 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0,
+ 0xe, 0xff, 0xf7,
+
+ /* U+006E "n" */
+ 0xff, 0xff, 0x31, 0x7c, 0xef, 0xd9, 0x30, 0x0,
+ 0xff, 0xff, 0x7e, 0xff, 0xff, 0xff, 0xf7, 0x0,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50,
+ 0xff, 0xff, 0xff, 0xfc, 0xcf, 0xff, 0xff, 0xd0,
+ 0xff, 0xff, 0xfa, 0x0, 0x1, 0xdf, 0xff, 0xf2,
+ 0xff, 0xff, 0xd0, 0x0, 0x0, 0x4f, 0xff, 0xf5,
+ 0xff, 0xff, 0x80, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xf6,
+
+ /* U+006F "o" */
+ 0x0, 0x0, 0x4, 0x9d, 0xef, 0xdb, 0x60, 0x0,
+ 0x0, 0x0, 0x2, 0xcf, 0xff, 0xff, 0xff, 0xfe,
+ 0x50, 0x0, 0x0, 0x2e, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf7, 0x0, 0x0, 0xdf, 0xff, 0xfe, 0x98,
+ 0xcf, 0xff, 0xff, 0x40, 0x7, 0xff, 0xff, 0x90,
+ 0x0, 0x4, 0xff, 0xff, 0xd0, 0xd, 0xff, 0xfc,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf2, 0xf, 0xff,
+ 0xf7, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xf5, 0x1f,
+ 0xff, 0xf5, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf6,
+ 0xf, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x1f, 0xff,
+ 0xf5, 0xd, 0xff, 0xfc, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xf2, 0x7, 0xff, 0xff, 0x90, 0x0, 0x4,
+ 0xff, 0xff, 0xc0, 0x0, 0xdf, 0xff, 0xfd, 0x98,
+ 0xbf, 0xff, 0xff, 0x40, 0x0, 0x2e, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x1, 0xbf,
+ 0xff, 0xff, 0xff, 0xfe, 0x40, 0x0, 0x0, 0x0,
+ 0x3, 0x9d, 0xef, 0xdb, 0x50, 0x0, 0x0,
+
+ /* U+0070 "p" */
+ 0xff, 0xff, 0x32, 0x9d, 0xfe, 0xd8, 0x20, 0x0,
+ 0xf, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x80,
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x90, 0xf, 0xff, 0xff, 0xfe, 0x98, 0xcf, 0xff,
+ 0xff, 0x50, 0xff, 0xff, 0xf9, 0x0, 0x0, 0x4f,
+ 0xff, 0xfc, 0xf, 0xff, 0xfe, 0x0, 0x0, 0x0,
+ 0x7f, 0xff, 0xf2, 0xff, 0xff, 0x90, 0x0, 0x0,
+ 0x2, 0xff, 0xff, 0x4f, 0xff, 0xf7, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xf6, 0xff, 0xff, 0xa0, 0x0,
+ 0x0, 0x2, 0xff, 0xff, 0x4f, 0xff, 0xff, 0x0,
+ 0x0, 0x0, 0x7f, 0xff, 0xf2, 0xff, 0xff, 0xf8,
+ 0x0, 0x0, 0x4f, 0xff, 0xfd, 0xf, 0xff, 0xff,
+ 0xfd, 0x98, 0xcf, 0xff, 0xff, 0x50, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0xf, 0xff,
+ 0xfb, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0xff,
+ 0xff, 0x62, 0x8d, 0xfe, 0xd8, 0x20, 0x0, 0xf,
+ 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0,
+
+ /* U+0071 "q" */
+ 0x0, 0x0, 0x6, 0xce, 0xfe, 0xb4, 0xe, 0xff,
+ 0xf4, 0x0, 0x4, 0xef, 0xff, 0xff, 0xff, 0xae,
+ 0xff, 0xf4, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf4, 0x1, 0xef, 0xff, 0xfe, 0x98,
+ 0xcf, 0xff, 0xff, 0xf4, 0x8, 0xff, 0xff, 0x90,
+ 0x0, 0x4, 0xff, 0xff, 0xf4, 0xd, 0xff, 0xfc,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xf4, 0xf, 0xff,
+ 0xf7, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xf4, 0x1f,
+ 0xff, 0xf5, 0x0, 0x0, 0x0, 0xf, 0xff, 0xf4,
+ 0xf, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x1f, 0xff,
+ 0xf4, 0xd, 0xff, 0xfc, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xf4, 0x8, 0xff, 0xff, 0x90, 0x0, 0x4,
+ 0xff, 0xff, 0xf4, 0x1, 0xff, 0xff, 0xfd, 0x98,
+ 0xcf, 0xff, 0xff, 0xf4, 0x0, 0x4f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x4, 0xef,
+ 0xff, 0xff, 0xff, 0xaf, 0xff, 0xf4, 0x0, 0x0,
+ 0x16, 0xce, 0xfe, 0xa4, 0x1f, 0xff, 0xf4, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff,
+ 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1f, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1f, 0xff, 0xf4,
+
+ /* U+0072 "r" */
+ 0xff, 0xff, 0x31, 0x8c, 0xe5, 0xff, 0xff, 0x7f,
+ 0xff, 0xf6, 0xff, 0xff, 0xff, 0xff, 0xf6, 0xff,
+ 0xff, 0xff, 0xfc, 0xb4, 0xff, 0xff, 0xfb, 0x10,
+ 0x0, 0xff, 0xff, 0xe0, 0x0, 0x0, 0xff, 0xff,
+ 0x80, 0x0, 0x0, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0xff, 0xff, 0x60,
+ 0x0, 0x0, 0xff, 0xff, 0x60, 0x0, 0x0, 0xff,
+ 0xff, 0x60, 0x0, 0x0, 0xff, 0xff, 0x60, 0x0,
+ 0x0, 0xff, 0xff, 0x60, 0x0, 0x0, 0xff, 0xff,
+ 0x60, 0x0, 0x0,
+
+ /* U+0073 "s" */
+ 0x0, 0x1, 0x7b, 0xef, 0xfe, 0xc8, 0x30, 0x0,
+ 0x6, 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 0x5,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x0, 0xdf,
+ 0xff, 0xc6, 0x44, 0x69, 0xe9, 0x0, 0xf, 0xff,
+ 0xf2, 0x0, 0x0, 0x0, 0x10, 0x0, 0xff, 0xff,
+ 0xb4, 0x10, 0x0, 0x0, 0x0, 0xa, 0xff, 0xff,
+ 0xff, 0xeb, 0x82, 0x0, 0x0, 0x1c, 0xff, 0xff,
+ 0xff, 0xff, 0xfb, 0x10, 0x0, 0x4, 0x9d, 0xff,
+ 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0, 0x25,
+ 0xbf, 0xff, 0xf1, 0x1, 0x60, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0x20, 0x8f, 0xea, 0x64, 0x45, 0xbf,
+ 0xff, 0xf0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf8, 0x3, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0x0, 0x0, 0x48, 0xce, 0xff, 0xec, 0x82, 0x0,
+ 0x0,
+
+ /* U+0074 "t" */
+ 0x0, 0x38, 0x88, 0x70, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0,
+ 0xcf, 0xff, 0xff, 0xff, 0xff, 0xa0, 0xcf, 0xff,
+ 0xff, 0xff, 0xff, 0xa0, 0xcf, 0xff, 0xff, 0xff,
+ 0xff, 0xa0, 0x34, 0xcf, 0xff, 0xf4, 0x44, 0x30,
+ 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0,
+ 0x0, 0x7f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xf1,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xfd, 0x78, 0xa0,
+ 0x0, 0xd, 0xff, 0xff, 0xff, 0xf2, 0x0, 0x2,
+ 0xef, 0xff, 0xff, 0xf8, 0x0, 0x0, 0x18, 0xdf,
+ 0xfd, 0xa3,
+
+ /* U+0075 "u" */
+ 0x1f, 0xff, 0xf4, 0x0, 0x0, 0x2, 0xff, 0xff,
+ 0x31, 0xff, 0xff, 0x40, 0x0, 0x0, 0x2f, 0xff,
+ 0xf3, 0x1f, 0xff, 0xf4, 0x0, 0x0, 0x2, 0xff,
+ 0xff, 0x31, 0xff, 0xff, 0x40, 0x0, 0x0, 0x2f,
+ 0xff, 0xf3, 0x1f, 0xff, 0xf4, 0x0, 0x0, 0x2,
+ 0xff, 0xff, 0x31, 0xff, 0xff, 0x40, 0x0, 0x0,
+ 0x2f, 0xff, 0xf3, 0x1f, 0xff, 0xf4, 0x0, 0x0,
+ 0x2, 0xff, 0xff, 0x31, 0xff, 0xff, 0x40, 0x0,
+ 0x0, 0x3f, 0xff, 0xf3, 0xf, 0xff, 0xf6, 0x0,
+ 0x0, 0x4, 0xff, 0xff, 0x30, 0xff, 0xff, 0x90,
+ 0x0, 0x0, 0xaf, 0xff, 0xf3, 0xc, 0xff, 0xff,
+ 0x40, 0x0, 0x6f, 0xff, 0xff, 0x30, 0x7f, 0xff,
+ 0xff, 0xdb, 0xef, 0xff, 0xff, 0xf3, 0x1, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x30, 0x3,
+ 0xdf, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xf3, 0x0,
+ 0x0, 0x7c, 0xef, 0xda, 0x30, 0xff, 0xff, 0x30,
+
+ /* U+0076 "v" */
+ 0xf, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x7, 0xff,
+ 0xfc, 0x9, 0xff, 0xfe, 0x0, 0x0, 0x0, 0xd,
+ 0xff, 0xf5, 0x2, 0xff, 0xff, 0x40, 0x0, 0x0,
+ 0x4f, 0xff, 0xe0, 0x0, 0xbf, 0xff, 0xb0, 0x0,
+ 0x0, 0xbf, 0xff, 0x70, 0x0, 0x5f, 0xff, 0xf2,
+ 0x0, 0x2, 0xff, 0xff, 0x10, 0x0, 0xe, 0xff,
+ 0xf8, 0x0, 0x8, 0xff, 0xfa, 0x0, 0x0, 0x7,
+ 0xff, 0xfe, 0x0, 0xe, 0xff, 0xf3, 0x0, 0x0,
+ 0x1, 0xff, 0xff, 0x50, 0x6f, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0xaf, 0xff, 0xb0, 0xdf, 0xff, 0x50,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xf6, 0xff, 0xfe,
+ 0x0, 0x0, 0x0, 0x0, 0xc, 0xff, 0xff, 0xff,
+ 0xf8, 0x0, 0x0, 0x0, 0x0, 0x5, 0xff, 0xff,
+ 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x8f, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1f, 0xff, 0xfd, 0x0, 0x0, 0x0,
+
+ /* U+0077 "w" */
+ 0xcf, 0xff, 0x50, 0x0, 0x0, 0xc, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0xdf, 0xff, 0x6, 0xff, 0xfa,
+ 0x0, 0x0, 0x2, 0xff, 0xff, 0x90, 0x0, 0x0,
+ 0x3f, 0xff, 0xa0, 0x1f, 0xff, 0xf0, 0x0, 0x0,
+ 0x8f, 0xff, 0xff, 0x0, 0x0, 0x8, 0xff, 0xf4,
+ 0x0, 0xbf, 0xff, 0x50, 0x0, 0xe, 0xff, 0xff,
+ 0xf5, 0x0, 0x0, 0xef, 0xfe, 0x0, 0x5, 0xff,
+ 0xfb, 0x0, 0x4, 0xff, 0xff, 0xff, 0xa0, 0x0,
+ 0x4f, 0xff, 0x90, 0x0, 0xf, 0xff, 0xf1, 0x0,
+ 0x9f, 0xff, 0xff, 0xff, 0x10, 0xa, 0xff, 0xf3,
+ 0x0, 0x0, 0x9f, 0xff, 0x60, 0xf, 0xff, 0xd8,
+ 0xff, 0xf6, 0x0, 0xff, 0xfd, 0x0, 0x0, 0x4,
+ 0xff, 0xfc, 0x5, 0xff, 0xf7, 0x3f, 0xff, 0xb0,
+ 0x5f, 0xff, 0x70, 0x0, 0x0, 0xe, 0xff, 0xf1,
+ 0xbf, 0xff, 0x10, 0xdf, 0xff, 0x1b, 0xff, 0xf1,
+ 0x0, 0x0, 0x0, 0x8f, 0xff, 0x8f, 0xff, 0xb0,
+ 0x7, 0xff, 0xf8, 0xff, 0xfb, 0x0, 0x0, 0x0,
+ 0x2, 0xff, 0xff, 0xff, 0xf5, 0x0, 0x1f, 0xff,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0xd, 0xff,
+ 0xff, 0xfe, 0x0, 0x0, 0xcf, 0xff, 0xff, 0xf0,
+ 0x0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xff, 0x90,
+ 0x0, 0x6, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xff, 0xf3, 0x0, 0x0, 0xf,
+ 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0, 0xb,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xe0,
+ 0x0, 0x0, 0x0,
+
+ /* U+0078 "x" */
+ 0x5f, 0xff, 0xf9, 0x0, 0x0, 0xb, 0xff, 0xfe,
+ 0x10, 0x8f, 0xff, 0xf4, 0x0, 0x7, 0xff, 0xff,
+ 0x30, 0x0, 0xcf, 0xff, 0xe1, 0x3, 0xff, 0xff,
+ 0x60, 0x0, 0x1, 0xef, 0xff, 0xb1, 0xef, 0xff,
+ 0xa0, 0x0, 0x0, 0x4, 0xff, 0xff, 0xff, 0xff,
+ 0xd0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0xff,
+ 0xf2, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff,
+ 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd, 0xff,
+ 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x7, 0xff,
+ 0xff, 0xdf, 0xff, 0xf2, 0x0, 0x0, 0x3, 0xff,
+ 0xff, 0x70, 0xdf, 0xff, 0xd0, 0x0, 0x1, 0xef,
+ 0xff, 0xb0, 0x3, 0xff, 0xff, 0xa0, 0x0, 0xbf,
+ 0xff, 0xe1, 0x0, 0x7, 0xff, 0xff, 0x60, 0x8f,
+ 0xff, 0xf4, 0x0, 0x0, 0xc, 0xff, 0xff, 0x30,
+
+ /* U+0079 "y" */
+ 0xf, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x7, 0xff,
+ 0xfb, 0x8, 0xff, 0xfe, 0x0, 0x0, 0x0, 0xd,
+ 0xff, 0xf4, 0x1, 0xff, 0xff, 0x50, 0x0, 0x0,
+ 0x5f, 0xff, 0xd0, 0x0, 0xaf, 0xff, 0xc0, 0x0,
+ 0x0, 0xcf, 0xff, 0x60, 0x0, 0x3f, 0xff, 0xf3,
+ 0x0, 0x2, 0xff, 0xfe, 0x0, 0x0, 0xc, 0xff,
+ 0xfa, 0x0, 0x9, 0xff, 0xf8, 0x0, 0x0, 0x5,
+ 0xff, 0xff, 0x10, 0x1f, 0xff, 0xf1, 0x0, 0x0,
+ 0x0, 0xdf, 0xff, 0x70, 0x7f, 0xff, 0xa0, 0x0,
+ 0x0, 0x0, 0x7f, 0xff, 0xe0, 0xef, 0xff, 0x30,
+ 0x0, 0x0, 0x0, 0xf, 0xff, 0xfb, 0xff, 0xfc,
+ 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0x0, 0x2, 0xff, 0xff,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf,
+ 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x3f, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0,
+ 0x20, 0x0, 0x3f, 0xff, 0xf2, 0x0, 0x0, 0x0,
+ 0x2, 0xfb, 0x89, 0xff, 0xff, 0xa0, 0x0, 0x0,
+ 0x0, 0xa, 0xff, 0xff, 0xff, 0xfe, 0x10, 0x0,
+ 0x0, 0x0, 0x1f, 0xff, 0xff, 0xff, 0xe3, 0x0,
+ 0x0, 0x0, 0x0, 0x2, 0x9d, 0xff, 0xd8, 0x10,
+ 0x0, 0x0, 0x0, 0x0,
+
+ /* U+007A "z" */
+ 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20,
+ 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2, 0xd,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10, 0x34,
+ 0x44, 0x44, 0x4c, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x5, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
+ 0x3, 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x1,
+ 0xef, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0, 0xcf,
+ 0xff, 0xe1, 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff,
+ 0xf3, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xf7, 0x0,
+ 0x0, 0x0, 0x0, 0x4f, 0xff, 0xfe, 0x44, 0x44,
+ 0x44, 0x42, 0xe, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf6, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x60,
+
+ /* U+007B "{" */
+ 0x0, 0x0, 0x29, 0xdf, 0xf8, 0x0, 0x3, 0xff,
+ 0xff, 0xf8, 0x0, 0xd, 0xff, 0xff, 0xf8, 0x0,
+ 0x3f, 0xff, 0xfd, 0x62, 0x0, 0x5f, 0xff, 0xf3,
+ 0x0, 0x0, 0x6f, 0xff, 0xf0, 0x0, 0x0, 0x6f,
+ 0xff, 0xf0, 0x0, 0x0, 0x6f, 0xff, 0xf0, 0x0,
+ 0x0, 0x6f, 0xff, 0xf0, 0x0, 0x0, 0x6f, 0xff,
+ 0xf0, 0x0, 0x0, 0x6f, 0xff, 0xf0, 0x0, 0x36,
+ 0xdf, 0xff, 0xc0, 0x0, 0x8f, 0xff, 0xff, 0x50,
+ 0x0, 0x8f, 0xff, 0xfa, 0x0, 0x0, 0x8f, 0xff,
+ 0xff, 0x90, 0x0, 0x0, 0xaf, 0xff, 0xe0, 0x0,
+ 0x0, 0x6f, 0xff, 0xf0, 0x0, 0x0, 0x6f, 0xff,
+ 0xf0, 0x0, 0x0, 0x6f, 0xff, 0xf0, 0x0, 0x0,
+ 0x6f, 0xff, 0xf0, 0x0, 0x0, 0x6f, 0xff, 0xf0,
+ 0x0, 0x0, 0x5f, 0xff, 0xf2, 0x0, 0x0, 0x3f,
+ 0xff, 0xfd, 0x62, 0x0, 0xe, 0xff, 0xff, 0xf8,
+ 0x0, 0x4, 0xff, 0xff, 0xf8, 0x0, 0x0, 0x29,
+ 0xef, 0xf8,
+
+ /* U+007C "|" */
+ 0x7b, 0xbb, 0x3a, 0xff, 0xf5, 0xaf, 0xff, 0x5a,
+ 0xff, 0xf5, 0xaf, 0xff, 0x5a, 0xff, 0xf5, 0xaf,
+ 0xff, 0x5a, 0xff, 0xf5, 0xaf, 0xff, 0x5a, 0xff,
+ 0xf5, 0xaf, 0xff, 0x5a, 0xff, 0xf5, 0xaf, 0xff,
+ 0x5a, 0xff, 0xf5, 0xaf, 0xff, 0x5a, 0xff, 0xf5,
+ 0xaf, 0xff, 0x5a, 0xff, 0xf5, 0xaf, 0xff, 0x5a,
+ 0xff, 0xf5, 0xaf, 0xff, 0x5a, 0xff, 0xf5, 0xaf,
+ 0xff, 0x5a, 0xff, 0xf5, 0xaf, 0xff, 0x5a, 0xff,
+ 0xf5,
+
+ /* U+007D "}" */
+ 0x9f, 0xfd, 0x91, 0x0, 0x0, 0x9f, 0xff, 0xfe,
+ 0x20, 0x0, 0x9f, 0xff, 0xff, 0xc0, 0x0, 0x37,
+ 0xef, 0xff, 0xf2, 0x0, 0x0, 0x3f, 0xff, 0xf4,
+ 0x0, 0x0, 0xf, 0xff, 0xf5, 0x0, 0x0, 0xf,
+ 0xff, 0xf5, 0x0, 0x0, 0xf, 0xff, 0xf5, 0x0,
+ 0x0, 0xf, 0xff, 0xf5, 0x0, 0x0, 0xf, 0xff,
+ 0xf5, 0x0, 0x0, 0xf, 0xff, 0xf6, 0x0, 0x0,
+ 0xd, 0xff, 0xfd, 0x52, 0x0, 0x5, 0xff, 0xff,
+ 0xf7, 0x0, 0x0, 0xaf, 0xff, 0xf7, 0x0, 0xa,
+ 0xff, 0xff, 0xf7, 0x0, 0xf, 0xff, 0xf9, 0x0,
+ 0x0, 0xf, 0xff, 0xf5, 0x0, 0x0, 0xf, 0xff,
+ 0xf5, 0x0, 0x0, 0xf, 0xff, 0xf5, 0x0, 0x0,
+ 0xf, 0xff, 0xf5, 0x0, 0x0, 0xf, 0xff, 0xf5,
+ 0x0, 0x0, 0x3f, 0xff, 0xf5, 0x0, 0x36, 0xdf,
+ 0xff, 0xf3, 0x0, 0x9f, 0xff, 0xff, 0xd0, 0x0,
+ 0x9f, 0xff, 0xff, 0x30, 0x0, 0x9f, 0xfd, 0x92,
+ 0x0, 0x0,
+
+ /* U+007E "~" */
+ 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x8, 0xef, 0xfa, 0x30, 0x0, 0x6e, 0xe5, 0xa,
+ 0xff, 0xff, 0xff, 0x92, 0x3d, 0xff, 0x32, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xfa,
+ 0x12, 0xaf, 0xff, 0xff, 0xf5, 0x7, 0xcc, 0x20,
+ 0x0, 0x3b, 0xef, 0xc4, 0x0
+};
+
+
+/*---------------------
+ * GLYPH DESCRIPTION
+ *--------------------*/
+
+static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
+ {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
+ {.bitmap_index = 0, .adv_w = 127, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 0, .adv_w = 129, .box_w = 6, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 57, .adv_w = 196, .box_w = 10, .box_h = 8, .ofs_x = 1, .ofs_y = 11},
+ {.bitmap_index = 97, .adv_w = 323, .box_w = 20, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 287, .adv_w = 286, .box_w = 18, .box_h = 26, .ofs_x = 0, .ofs_y = -3},
+ {.bitmap_index = 521, .adv_w = 393, .box_w = 24, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 749, .adv_w = 326, .box_w = 19, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 930, .adv_w = 103, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 11},
+ {.bitmap_index = 950, .adv_w = 160, .box_w = 8, .box_h = 26, .ofs_x = 2, .ofs_y = -5},
+ {.bitmap_index = 1054, .adv_w = 160, .box_w = 8, .box_h = 26, .ofs_x = 0, .ofs_y = -5},
+ {.bitmap_index = 1158, .adv_w = 194, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = 10},
+ {.bitmap_index = 1224, .adv_w = 268, .box_w = 15, .box_h = 13, .ofs_x = 1, .ofs_y = 3},
+ {.bitmap_index = 1322, .adv_w = 117, .box_w = 7, .box_h = 10, .ofs_x = 0, .ofs_y = -4},
+ {.bitmap_index = 1357, .adv_w = 173, .box_w = 9, .box_h = 4, .ofs_x = 1, .ofs_y = 6},
+ {.bitmap_index = 1375, .adv_w = 117, .box_w = 7, .box_h = 6, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 1396, .adv_w = 176, .box_w = 14, .box_h = 26, .ofs_x = -1, .ofs_y = -3},
+ {.bitmap_index = 1578, .adv_w = 304, .box_w = 17, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 1740, .adv_w = 176, .box_w = 9, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 1826, .adv_w = 264, .box_w = 16, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 1978, .adv_w = 265, .box_w = 17, .box_h = 19, .ofs_x = -1, .ofs_y = 0},
+ {.bitmap_index = 2140, .adv_w = 309, .box_w = 20, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 2330, .adv_w = 267, .box_w = 16, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 2482, .adv_w = 285, .box_w = 17, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 2644, .adv_w = 278, .box_w = 17, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 2806, .adv_w = 296, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 2977, .adv_w = 285, .box_w = 17, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 3139, .adv_w = 117, .box_w = 7, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 3192, .adv_w = 117, .box_w = 7, .box_h = 19, .ofs_x = 0, .ofs_y = -4},
+ {.bitmap_index = 3259, .adv_w = 268, .box_w = 15, .box_h = 15, .ofs_x = 1, .ofs_y = 2},
+ {.bitmap_index = 3372, .adv_w = 268, .box_w = 15, .box_h = 10, .ofs_x = 1, .ofs_y = 5},
+ {.bitmap_index = 3447, .adv_w = 268, .box_w = 15, .box_h = 15, .ofs_x = 1, .ofs_y = 2},
+ {.bitmap_index = 3560, .adv_w = 264, .box_w = 17, .box_h = 19, .ofs_x = -1, .ofs_y = 0},
+ {.bitmap_index = 3722, .adv_w = 464, .box_w = 27, .box_h = 24, .ofs_x = 1, .ofs_y = -5},
+ {.bitmap_index = 4046, .adv_w = 343, .box_w = 23, .box_h = 19, .ofs_x = -1, .ofs_y = 0},
+ {.bitmap_index = 4265, .adv_w = 343, .box_w = 19, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 4446, .adv_w = 328, .box_w = 19, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 4627, .adv_w = 370, .box_w = 21, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 4827, .adv_w = 301, .box_w = 16, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 4979, .adv_w = 286, .box_w = 16, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 5131, .adv_w = 345, .box_w = 20, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 5321, .adv_w = 362, .box_w = 19, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 5502, .adv_w = 147, .box_w = 5, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 5550, .adv_w = 242, .box_w = 14, .box_h = 19, .ofs_x = -1, .ofs_y = 0},
+ {.bitmap_index = 5683, .adv_w = 332, .box_w = 20, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 5873, .adv_w = 271, .box_w = 15, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 6016, .adv_w = 428, .box_w = 23, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 6235, .adv_w = 362, .box_w = 19, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 6416, .adv_w = 378, .box_w = 22, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 6625, .adv_w = 328, .box_w = 18, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 6796, .adv_w = 378, .box_w = 23, .box_h = 23, .ofs_x = 1, .ofs_y = -4},
+ {.bitmap_index = 7061, .adv_w = 329, .box_w = 18, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 7232, .adv_w = 286, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 7403, .adv_w = 277, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 7574, .adv_w = 353, .box_w = 18, .box_h = 19, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 7745, .adv_w = 334, .box_w = 23, .box_h = 19, .ofs_x = -1, .ofs_y = 0},
+ {.bitmap_index = 7964, .adv_w = 521, .box_w = 32, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 8268, .adv_w = 320, .box_w = 20, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 8458, .adv_w = 303, .box_w = 21, .box_h = 19, .ofs_x = -1, .ofs_y = 0},
+ {.bitmap_index = 8658, .adv_w = 301, .box_w = 18, .box_h = 19, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 8829, .adv_w = 165, .box_w = 8, .box_h = 26, .ofs_x = 2, .ofs_y = -5},
+ {.bitmap_index = 8933, .adv_w = 176, .box_w = 14, .box_h = 26, .ofs_x = -2, .ofs_y = -3},
+ {.bitmap_index = 9115, .adv_w = 165, .box_w = 8, .box_h = 26, .ofs_x = 0, .ofs_y = -5},
+ {.bitmap_index = 9219, .adv_w = 269, .box_w = 15, .box_h = 12, .ofs_x = 1, .ofs_y = 4},
+ {.bitmap_index = 9309, .adv_w = 224, .box_w = 14, .box_h = 3, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 9330, .adv_w = 269, .box_w = 9, .box_h = 4, .ofs_x = 2, .ofs_y = 17},
+ {.bitmap_index = 9348, .adv_w = 276, .box_w = 15, .box_h = 15, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 9461, .adv_w = 309, .box_w = 17, .box_h = 21, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 9640, .adv_w = 265, .box_w = 17, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 9768, .adv_w = 310, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 9957, .adv_w = 283, .box_w = 17, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 10085, .adv_w = 173, .box_w = 12, .box_h = 21, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 10211, .adv_w = 314, .box_w = 18, .box_h = 20, .ofs_x = 0, .ofs_y = -5},
+ {.bitmap_index = 10391, .adv_w = 310, .box_w = 16, .box_h = 21, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 10559, .adv_w = 135, .box_w = 6, .box_h = 22, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 10625, .adv_w = 138, .box_w = 11, .box_h = 27, .ofs_x = -3, .ofs_y = -5},
+ {.bitmap_index = 10774, .adv_w = 296, .box_w = 17, .box_h = 21, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 10953, .adv_w = 135, .box_w = 5, .box_h = 21, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 11006, .adv_w = 470, .box_w = 26, .box_h = 15, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 11201, .adv_w = 310, .box_w = 16, .box_h = 15, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 11321, .adv_w = 293, .box_w = 18, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 11456, .adv_w = 309, .box_w = 17, .box_h = 20, .ofs_x = 2, .ofs_y = -5},
+ {.bitmap_index = 11626, .adv_w = 309, .box_w = 18, .box_h = 20, .ofs_x = 0, .ofs_y = -5},
+ {.bitmap_index = 11806, .adv_w = 193, .box_w = 10, .box_h = 15, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 11881, .adv_w = 238, .box_w = 15, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 11994, .adv_w = 195, .box_w = 12, .box_h = 19, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 12108, .adv_w = 308, .box_w = 17, .box_h = 15, .ofs_x = 1, .ofs_y = 0},
+ {.bitmap_index = 12236, .adv_w = 268, .box_w = 18, .box_h = 15, .ofs_x = -1, .ofs_y = 0},
+ {.bitmap_index = 12371, .adv_w = 420, .box_w = 27, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 12574, .adv_w = 267, .box_w = 17, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 12702, .adv_w = 268, .box_w = 18, .box_h = 20, .ofs_x = -1, .ofs_y = -5},
+ {.bitmap_index = 12882, .adv_w = 243, .box_w = 15, .box_h = 15, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 12995, .adv_w = 175, .box_w = 10, .box_h = 26, .ofs_x = 1, .ofs_y = -5},
+ {.bitmap_index = 13125, .adv_w = 138, .box_w = 5, .box_h = 26, .ofs_x = 2, .ofs_y = -5},
+ {.bitmap_index = 13190, .adv_w = 175, .box_w = 10, .box_h = 26, .ofs_x = 0, .ofs_y = -5},
+ {.bitmap_index = 13320, .adv_w = 268, .box_w = 15, .box_h = 6, .ofs_x = 1, .ofs_y = 7}
+};
+
+/*---------------------
+ * CHARACTER MAPPING
+ *--------------------*/
+
+
+
+/*Collect the unicode lists and glyph_id offsets*/
+static const lv_font_fmt_txt_cmap_t cmaps[] =
+{
+ {
+ .range_start = 32, .range_length = 95, .glyph_id_start = 1,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
+ }
+};
+
+/*-----------------
+ * KERNING
+ *----------------*/
+
+
+/*Map glyph_ids to kern left classes*/
+static const uint8_t kern_left_class_mapping[] =
+{
+ 0, 0, 1, 2, 0, 3, 4, 5,
+ 2, 6, 7, 8, 9, 10, 9, 10,
+ 11, 12, 0, 13, 14, 15, 16, 17,
+ 18, 19, 12, 20, 20, 0, 0, 0,
+ 21, 22, 23, 24, 25, 22, 26, 27,
+ 28, 29, 29, 30, 31, 32, 29, 29,
+ 22, 33, 34, 35, 3, 36, 30, 37,
+ 37, 38, 39, 40, 41, 42, 43, 0,
+ 44, 0, 45, 46, 47, 48, 49, 50,
+ 51, 45, 52, 52, 53, 48, 45, 45,
+ 46, 46, 54, 55, 56, 57, 51, 58,
+ 58, 59, 58, 60, 41, 0, 0, 9
+};
+
+/*Map glyph_ids to kern right classes*/
+static const uint8_t kern_right_class_mapping[] =
+{
+ 0, 0, 1, 2, 0, 3, 4, 5,
+ 2, 6, 7, 8, 9, 10, 9, 10,
+ 11, 12, 13, 14, 15, 16, 17, 12,
+ 18, 19, 20, 21, 21, 0, 0, 0,
+ 22, 23, 24, 25, 23, 25, 25, 25,
+ 23, 25, 25, 26, 25, 25, 25, 25,
+ 23, 25, 23, 25, 3, 27, 28, 29,
+ 29, 30, 31, 32, 33, 34, 35, 0,
+ 36, 0, 37, 38, 39, 39, 39, 40,
+ 39, 38, 41, 42, 38, 38, 43, 43,
+ 39, 43, 39, 43, 44, 45, 46, 47,
+ 47, 48, 49, 50, 0, 0, 35, 9
+};
+
+/*Kern values between classes*/
+static const int8_t kern_class_values[] =
+{
+ 0, 2, 0, 0, 0, 0, 0, 4,
+ 0, 2, 0, 0, 4, 0, 0, 0,
+ 0, 2, 0, 0, 0, 0, 0, 0,
+ 0, 10, 9, 0, 4, 0, 9, 0,
+ 0, 0, 2, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 2, 20, 0, 11, -8, 0,
+ 0, 9, 0, -25, -27, 2, 20, 8,
+ 6, -18, 2, 18, 0, 17, 4, 12,
+ 0, -19, 0, 0, 6, 0, 0, 0,
+ 0, 0, 0, 27, 6, -2, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, -13, 0, 0,
+ 0, 0, 0, -9, 6, 9, 0, 0,
+ -4, 0, -2, 4, 0, -4, 0, -4,
+ -2, -9, 0, 0, 0, 0, -4, 0,
+ 0, -5, -7, 0, 0, -4, 0, -9,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, -4, -4, -4, 0, 0, -11,
+ 0, -56, 0, 0, -9, -22, 9, 13,
+ 0, 0, -9, 4, 4, 14, 9, -6,
+ 9, 0, 0, -24, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, -15,
+ 0, 0, 3, 0, 12, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ -6, -18, 0, -18, -2, 0, 0, -13,
+ 0, -2, 13, 0, -13, -6, -2, 2,
+ 0, -6, 0, 0, -4, -29, 0, 3,
+ 0, 12, -11, 0, -9, 0, -18, 3,
+ 0, -36, -6, 9, 4, 0, 0, 0,
+ 0, 0, 0, 0, 4, 0, -7, -2,
+ -7, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 16, 0, 4, 0,
+ 0, -9, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, -3, 0, 0, 0,
+ 0, 0, 0, 19, 6, 2, 0, 0,
+ 0, 0, 0, 46, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, -15, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 4, 9,
+ 4, 13, -4, 0, 0, 9, -4, -16,
+ -59, 2, 11, 9, -1, -5, 0, 11,
+ 0, 12, 0, 12, 0, -43, 0, -6,
+ 13, 0, 14, -4, 9, 4, 0, 0,
+ 2, -4, 0, 0, -6, 0, 0, 0,
+ 36, 0, 13, 0, 17, 7, 17, 6,
+ 0, 0, -6, -15, 0, 0, 0, -4,
+ 2, -4, 0, 2, -9, -7, -9, 2,
+ 0, -4, 0, 0, 0, -18, 2, -10,
+ 0, -9, -16, 0, -12, -9, -15, 0,
+ 0, -29, 0, 0, 0, 0, 4, 0,
+ 0, 0, 0, 0, 4, 0, -7, -12,
+ -7, -5, 2, -25, 4, -30, 0, 0,
+ 0, -16, -4, 0, 40, -6, -7, 4,
+ 4, 0, 0, -7, 4, 0, 0, -25,
+ -9, 14, 0, 24, -15, -3, -17, 0,
+ -17, 6, 0, -42, 0, 4, 4, 0,
+ -3, 0, 0, 4, 0, 0, -2, -3,
+ -14, 0, -14, 0, 0, 27, -9, 0,
+ -15, 0, 16, 0, -32, -42, -33, -9,
+ 13, 0, 0, -29, 0, 3, -12, 0,
+ -7, 0, -9, -18, 0, -4, 13, 0,
+ 13, 0, 13, 0, 0, 6, 13, -52,
+ -29, 0, -29, 0, 6, 2, -29, -29,
+ -11, -29, -13, -22, -13, -29, 0, 2,
+ 0, 0, 0, 0, 0, 2, 2, -6,
+ -9, 0, -2, -2, -4, 0, 0, -2,
+ 0, 0, 0, -9, 0, -5, 0, -12,
+ -9, 0, -11, -16, -16, -7, 0, -9,
+ 0, -9, 0, 0, 0, 0, 0, -4,
+ 0, 0, 4, 0, 2, -4, 2, 0,
+ 0, 0, 0, 4, -2, 0, 0, 0,
+ -2, 4, 4, 0, 0, 0, 0, -4,
+ 0, 0, 0, 0, 0, 0, 0, 2,
+ 0, 7, -2, 0, -6, 0, -8, 0,
+ 0, -2, 0, 13, 0, 0, -4, 0,
+ 0, 0, 0, 0, 0, 0, 0, -2,
+ 0, -2, 0, -4, 0, -4, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, -2,
+ -2, 0, -4, -4, 0, 0, 0, 0,
+ 0, 0, 0, 0, -4, 0, -4, -4,
+ -4, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, -3, 0, 0, 0, 0,
+ -4, -7, -4, 0, 0, -13, -2, -13,
+ 9, 0, 0, -9, 4, 9, 11, 0,
+ -11, 0, -3, 0, 0, -19, 4, -2,
+ 4, -25, 4, 0, 0, 2, -24, 0,
+ -25, -5, -39, -2, 0, -22, 0, 9,
+ 12, 0, 7, 0, 0, 0, 0, 0,
+ 1, 0, -10, -7, -10, 0, 0, 0,
+ 0, -4, 0, 0, 0, -4, 0, 0,
+ 0, 0, 0, -2, -2, 0, -2, -5,
+ 0, 0, 0, 0, 0, 0, 0, -4,
+ -4, 0, -4, -6, -3, 0, 0, -4,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, -5, -5, -5, 0,
+ 0, -2, 0, -9, 4, 0, 0, -3,
+ 2, 4, 4, 0, 0, 0, 0, 0,
+ 0, -2, 0, 0, 0, 0, 0, 2,
+ 0, 0, -4, 0, -4, -2, -3, 0,
+ 0, 0, 0, 0, 0, 0, 3, 0,
+ 0, -4, 0, 0, 0, 0, -7, -7,
+ -7, 0, 0, 13, -2, 2, -12, 0,
+ 0, 11, -22, -22, -17, -9, 4, 0,
+ -3, -29, -8, 0, -8, 0, -9, 7,
+ -8, -27, 0, -11, 0, 0, 2, 0,
+ 5, -2, 0, 4, 4, -13, -17, 0,
+ -22, 0, 0, -10, -9, -13, -3, -11,
+ 2, -4, 2, -10, 0, 0, 0, -4,
+ 0, 0, 0, 2, 0, 4, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, -4, 0, -2, 0, 0, -4, 0,
+ -8, -10, -10, 0, 0, -13, 0, 0,
+ 0, 0, 0, 0, 0, -4, 0, 0,
+ 0, 0, -1, -4, -1, 0, 0, 4,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 24, 0, 0, 0, 0, 0, 0, 2,
+ 0, 0, 0, -4, 0, 4, 0, 15,
+ -4, 0, -12, -4, -17, 0, 0, -9,
+ 0, 4, 0, 0, 0, 0, 0, 0,
+ 0, 0, 6, 0, 2, -4, 2, 0,
+ -2, 0, 0, 0, -4, 0, 0, 3,
+ 0, -22, -13, 0, 0, 0, -7, -22,
+ 0, 0, -4, 4, 0, -8, -2, -21,
+ 0, -14, 0, 0, -7, -9, -7, -4,
+ -4, 0, 0, -7, 0, 0, -4, 0,
+ 0, 0, 0, 0, 0, 0, 4, 0,
+ 4, 0, 0, -8, 0, 0, 0, 0,
+ 3, 0, 2, -9, -9, 0, -4, -4,
+ -6, 0, 0, 0, 0, 0, 0, -13,
+ 0, -4, 0, -7, -4, 0, -10, -11,
+ -13, -3, 0, -9, 4, -13, 0, 0,
+ 0, 0, 0, 36, 0, 0, 2, 0,
+ 0, -7, 0, 0, 0, -19, 0, 0,
+ 0, 0, 0, -43, -10, 14, 13, -5,
+ -19, 0, 4, -7, 0, -22, -2, -5,
+ 4, -31, -4, 10, 0, 7, -16, -7,
+ -17, -16, -19, 0, 0, -27, 0, 24,
+ 0, 0, -2, 0, 0, 0, 0, -2,
+ -2, -4, -13, -16, -13, -1, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 2,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ -4, 0, -2, -4, -7, 0, 0, -9,
+ 0, -4, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, -2, 0, -9, 0, 0, 9,
+ -2, 5, 0, -9, 4, -2, 0, -6,
+ -4, 0, -5, -4, -4, 0, -7, -6,
+ 0, 0, -3, -2, -2, -6, -3, 0,
+ 0, -4, 0, 4, -2, 0, -9, 0,
+ 0, 0, 0, -9, 0, -6, 0, -6,
+ 0, -6, 0, 0, 0, 0, 0, 0,
+ 0, 0, -9, 4, 0, -4, 0, -2,
+ -3, -8, -2, -2, -2, 0, -2, -3,
+ 0, 0, 0, 0, 0, 0, -4, -3,
+ -3, 0, 0, 0, 0, 3, -2, 0,
+ -2, 0, 0, 0, 0, -2, -3, -2,
+ -2, -3, -2, -2, 6, 18, 0, 0,
+ -10, 0, -2, 9, 0, -4, -17, -5,
+ 7, 2, 0, -19, -6, 4, -6, 4,
+ 0, 1, -3, -13, 0, -7, 2, 0,
+ 0, -6, 0, 0, 0, 4, 4, -9,
+ -7, 0, -6, 0, 0, -7, -4, -4,
+ 0, -6, 2, -7, 2, -6, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 4,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, -6,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, -2, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, -3, -4, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, -7, 0, 0, -5, 0, 0, -4,
+ -4, 0, 0, 0, 0, -4, 0, 0,
+ 0, 0, 0, -2, 0, 0, 0, 0,
+ 0, -2, 0, 0, 0, 0, -7, 0,
+ -9, 0, 0, 0, -16, 0, 2, -10,
+ 9, 1, -2, -20, 0, 0, -10, -4,
+ 0, -18, -11, -14, 0, 0, -22, -4,
+ -18, -18, -23, 0, -6, 0, 6, 29,
+ -5, 0, -10, 0, 0, 0, -4, -6,
+ -11, -8, -17, -20, -17, -12, 0, 0,
+ -2, 0, 2, 0, 0, -31, 0, 13,
+ 8, -8, -15, 0, 2, -8, 0, -22,
+ -2, -4, 9, -39, -5, 2, 0, 0,
+ -29, -4, -22, -4, -32, 0, 0, -31,
+ 0, 22, 2, 0, -2, 0, 0, 0,
+ 0, 0, -2, -2, -17, -2, -17, 0,
+ 0, 0, 0, 0, -13, 0, -2, 0,
+ -2, -12, -20, 0, 0, -2, -7, -13,
+ -4, 0, -4, 0, 0, 0, 0, -20,
+ -4, -14, -13, -6, -8, -11, -4, -6,
+ 0, -9, -2, -16, -7, 0, -5, 0,
+ 0, -4, -4, 0, 2, 0, -2, -14,
+ -2, 0, 0, -8, 0, 0, 0, 0,
+ 3, 0, 2, -9, 22, 0, -4, -4,
+ -6, 0, 0, 0, 0, 0, 0, -13,
+ 0, -4, 0, -7, -4, 0, -10, -11,
+ -13, -3, 0, -9, 6, 18, 0, 0,
+ 0, 0, 0, 36, 0, 0, 2, 0,
+ 0, -7, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, -1, 0, 0, 0,
+ 0, 0, -2, -9, 0, 0, 0, 0,
+ 0, -2, 0, 0, 0, -4, -4, 0,
+ 0, -9, -4, 0, 0, -9, 0, 6,
+ -2, 0, 0, 0, 0, 0, 0, 0,
+ 2, 0, 0, 0, 0, 0, 9, 6,
+ -2, 0, -12, -4, 0, 13, -16, -15,
+ -9, -9, 18, 10, 4, -38, -4, 9,
+ -4, 0, -4, 9, -4, -16, 0, -4,
+ 4, -5, -6, -13, -6, 0, 0, 13,
+ 9, 0, -12, 0, -25, 0, 0, 19,
+ -5, -17, 2, -5, -14, -14, -14, -4,
+ 4, 0, -7, 0, -11, 0, 6, 14,
+ -12, -17, -18, -11, 13, 0, 2, -34,
+ -5, 4, -8, -4, -12, 0, -10, -17,
+ -7, -7, -6, 0, 0, -12, -11, -4,
+ 0, 13, 12, -4, -25, 0, -25, 0,
+ -2, -9, -16, -27, -2, -15, -8, -14,
+ -8, -14, 0, 0, -5, 0, -9, -2,
+ 0, -4, -9, 0, 6, -16, 4, 0,
+ 0, -25, 0, -4, -10, -8, -4, -13,
+ -11, -16, -10, 0, -13, -4, -12, -4,
+ -13, -4, 0, 0, 2, 20, -8, 0,
+ -13, 0, 0, 0, -4, -9, -12, -13,
+ -15, -19, -15, -7, 9, 0, -7, 0,
+ -22, -3, 4, 9, -15, -17, -9, -16,
+ 16, -4, 2, -43, -9, 9, -10, -8,
+ -17, 0, -13, -19, -3, -4, -6, -4,
+ -11, -13, -2, 0, 0, 13, 15, -2,
+ -29, 0, -27, 0, -4, 16, -17, -32,
+ -9, -16, -19, -22, -19, -16, 0, 0,
+ 0, 0, -3, 0, 0, 4, -3, 9,
+ 2, -7, 9, 0, 0, -8, 0, 0,
+ 0, 0, 2, 2, -3, 0, 0, 0,
+ 0, 0, 0, -4, 0, 0, 0, 0,
+ 6, 13, 1, 0, -3, 0, 0, 0,
+ 0, 0, -2, -2, -3, 0, -3, 0,
+ 2, 6, 0, 0, 0, 0, 6, 2,
+ -2, 0, 19, 0, 10, 2, 2, -7,
+ 0, 9, 0, 0, 0, 6, 0, 0,
+ 0, 0, 9, 0, 12, 2, 15, 0,
+ 0, 13, 0, 15, -2, 0, 0, 0,
+ 0, 45, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, -27, 0, -4, 6, 0,
+ 13, -59, 0, 40, 3, -9, -9, 4,
+ 4, -2, 2, -22, 0, 0, 24, -27,
+ -9, 13, 0, 13, -9, -4, -18, 9,
+ -9, 0, 0, -33, 19, 63, 0, 0,
+ 0, 0, 0, 54, 0, 0, 0, 0,
+ 9, 0, 9, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, -4, 0,
+ 0, -4, -2, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, -2,
+ 9, -11, 0, 0, 2, -4, 0, 4,
+ 54, -9, -3, 12, 11, -11, 4, 0,
+ 0, 4, 4, -8, -13, 24, 13, 34,
+ 0, -4, -4, 20, -2, 9, 0, -58,
+ 15, 0, -4, 0, -11, 0, 0, 51,
+ 0, 4, -9, -11, -7, 15, 9, 6,
+ 0, 0, 0, -12, 0, 0, 0, -11,
+ 0, 0, 0, 0, -9, -2, 0, 0,
+ 0, -9, 0, -3, 0, -20, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, -29, 0, 0, 0, 0, 2, 0,
+ 0, 0, 0, 0, 0, 0, -4, 0,
+ -4, 0, 0, -7, 0, -11, 0, 0,
+ 0, -6, 4, -3, 0, 0, -11, -4,
+ -10, 0, 0, -11, 0, -4, 0, -20,
+ 0, -9, 0, 0, -30, -4, -18, -9,
+ -18, 0, 0, -29, 0, -11, -4, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ -7, -8, -7, -3, 0, 0, 0, 0,
+ -8, 0, -10, 7, -9, 9, 0, -2,
+ -10, -2, -6, -4, 0, -3, -2, -2,
+ 4, -11, -2, 0, 0, 0, -32, -6,
+ -12, 0, -17, 0, -2, -20, -2, 0,
+ 0, -2, -3, 0, -2, 0, 0, 0,
+ 4, 0, -4, -6, -4, -2, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 17, 0, 0, 0, 0, 0,
+ 0, -8, 0, -2, 0, 0, 0, -9,
+ 4, 0, 0, 0, -11, -4, -9, 0,
+ 0, -12, 0, -4, 0, -20, 0, 0,
+ 0, 0, -42, 0, -9, -17, -22, 0,
+ 0, -29, 0, -4, -7, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, -4, -7,
+ -4, -2, 2, 0, 0, 6, -7, 0,
+ 18, 18, -4, -4, -13, 3, 18, 6,
+ 8, -11, 3, 17, 3, 11, 8, 11,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 28, 20, -10, -4, 4,
+ -3, 4, 4, 21, 2, 0, 0, 0,
+ 4, 0, 4, 0, 0, 0, -4, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ -2, 0, 0, 0, 0, 0, 0, 0,
+ 0, 12, 0, 0, 0, 0, -29, -3,
+ -6, -15, -18, 0, 0, -29, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ -4, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, -2, 0, 0, 0, 0, 0,
+ 0, 0, 0, 12, 0, 0, 0, 0,
+ -29, -3, -6, -15, -18, 0, 0, -8,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, -6, 0, 0, 0,
+ -12, 4, 0, -4, 6, 10, 4, -13,
+ 0, 2, -5, 4, 0, 6, 0, 0,
+ 0, 0, -4, 0, -2, -2, -9, 0,
+ -2, -18, 0, 26, -4, 0, -10, 0,
+ 0, 0, -2, -8, 0, -4, -14, -9,
+ -14, -6, 0, 0, -4, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, -2, 0,
+ 0, 0, 0, 0, 0, 0, 0, 12,
+ 0, 0, 0, 0, -29, -3, -6, -15,
+ -18, 0, 0, -29, 0, 0, 0, 0,
+ 0, 0, 0, 22, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, -4, 0,
+ -12, -3, -2, 13, -4, -4, -18, 2,
+ 4, 2, -2, -10, 1, 10, 1, 6,
+ 2, 6, -9, -20, -3, 0, -9, -4,
+ -11, -17, -16, 0, -4, -9, -3, -7,
+ -17, -2, -4, 0, -2, 0, -2, 0,
+ 7, 0, 7, -2, 7, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, -2, -4, -4, 0, 0, -11,
+ 0, -2, 0, -6, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, -27,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, -4, -4, -4, 0,
+ 0, 0, 0, 0, -3, 0, 0, -6,
+ -4, 4, 0, -6, -7, -2, 0, -9,
+ -2, -8, -4, -3, 0, -6, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, -29, 0, 12, 0, 0, -8, 0,
+ 0, 0, 0, 0, -5, 0, -4, 0,
+ -4, 0, 0, 0, -2, 0, -12, 0,
+ 0, 17, -7, -14, -16, 2, 9, 9,
+ 2, -15, 2, 7, 2, 13, 2, 16,
+ -2, -13, 0, 0, -8, 0, 0, -13,
+ -11, 0, 0, -9, 0, -7, -8, 0,
+ -7, 0, 0, -7, 0, -4, 7, 0,
+ -6, -13, -6, -4, 0, 0, -2, 0,
+ -9, 0, 0, 7, -12, 0, 4, -4,
+ 6, 4, 0, -16, 0, -2, -2, 0,
+ -4, 9, -6, 0, 0, 0, -10, -3,
+ -10, 0, -13, 0, 0, -20, 0, 15,
+ -4, 0, -8, 0, 0, 12, 0, -4,
+ 0, -4, -13, 0, -13, -4, 0, 0,
+ 0, 0, -2, 0, 0, 4, -5, 2,
+ 0, 0, -3, -2, 0, -3, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, -26,
+ 0, 8, 0, 0, -3, 0, 0, 0,
+ 0, 0, 1, 0, -4, -4, -4, 0
+};
+
+
+/*Collect the kern class' data in one place*/
+static const lv_font_fmt_txt_kern_classes_t kern_classes =
+{
+ .class_pair_values = kern_class_values,
+ .left_class_mapping = kern_left_class_mapping,
+ .right_class_mapping = kern_right_class_mapping,
+ .left_class_cnt = 60,
+ .right_class_cnt = 50,
+};
+
+/*--------------------
+ * ALL CUSTOM DATA
+ *--------------------*/
+
+#if LVGL_VERSION_MAJOR == 8
+/*Store all the custom data of the font*/
+static lv_font_fmt_txt_glyph_cache_t cache;
+#endif
+
+#if LVGL_VERSION_MAJOR >= 8
+static const lv_font_fmt_txt_dsc_t font_dsc = {
+#else
+static lv_font_fmt_txt_dsc_t font_dsc = {
+#endif
+ .glyph_bitmap = glyph_bitmap,
+ .glyph_dsc = glyph_dsc,
+ .cmaps = cmaps,
+ .kern_dsc = &kern_classes,
+ .kern_scale = 16,
+ .cmap_num = 1,
+ .bpp = 4,
+ .kern_classes = 1,
+ .bitmap_format = 0,
+#if LVGL_VERSION_MAJOR == 8
+ .cache = &cache
+#endif
+};
+
+
+
+/*-----------------
+ * PUBLIC FONT
+ *----------------*/
+
+/*Initialize a public general font descriptor*/
+#if LVGL_VERSION_MAJOR >= 8
+const lv_font_t montserrat_bold_28 = {
+#else
+lv_font_t montserrat_bold_28 = {
+#endif
+ .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
+ .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
+ .line_height = 28, /*The maximum line height required by the font*/
+ .base_line = 5, /*Baseline measured from the bottom of the line*/
+#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
+ .subpx = LV_FONT_SUBPX_NONE,
+#endif
+#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
+ .underline_position = -3,
+ .underline_thickness = 1,
+#endif
+ .dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
+#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
+ .fallback = NULL,
+#endif
+ .user_data = NULL,
+};
+
+
+
+#endif /*#if MONTSERRAT_BOLD_28*/
+
diff --git a/Fonts/montserrat_bold_96.c b/Fonts/montserrat_bold_96.c
new file mode 100644
index 00000000..d3f704a1
--- /dev/null
+++ b/Fonts/montserrat_bold_96.c
@@ -0,0 +1,2647 @@
+/*******************************************************************************
+ * Size: 96 px
+ * Bpp: 4
+ * Opts: --bpp 4 --size 96 --no-compress --font /home/ian/Arduino/libraries/lvgl/tests/src/test_files/fonts/Montserrat-Bold.ttf -r 0x30-0x3A,0x20 --format lvgl -o /home/ian/Code/RNode_Firmware/Fonts/montserrat_bold_96.c --lv-include lvgl.h --lv-font-name montserrat_bold_96
+ ******************************************************************************/
+
+#ifdef LV_LVGL_H_INCLUDE_SIMPLE
+#include "lvgl.h"
+#else
+#include "lvgl.h"
+#endif
+
+#ifndef MONTSERRAT_BOLD_96
+#define MONTSERRAT_BOLD_96 1
+#endif
+
+#if MONTSERRAT_BOLD_96
+
+/*-----------------
+ * BITMAPS
+ *----------------*/
+
+/*Store the image of the glyphs*/
+static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
+ /* U+0020 " " */
+
+ /* U+0030 "0" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x44, 0x43,
+ 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x37, 0xad, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xb7,
+ 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x28, 0xdf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe9, 0x20,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x2, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xb3, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x19, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x10, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4e, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x20, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfb, 0x63, 0x10, 0x12, 0x6a, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc2, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0,
+ 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0xc,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfd, 0x0, 0x0, 0x0, 0x2, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0,
+ 0x0, 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0, 0xe,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x10, 0x0, 0x4, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0xc,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0x0, 0x1, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf2, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf8, 0x0, 0x9f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xa0, 0xb, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf0, 0xf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x11, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x32, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf3, 0x3f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x43, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x52, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf4, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x31, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x6, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf2, 0xf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xd,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0x0, 0xbf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf3, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x80, 0x3, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf5, 0x0, 0xf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10,
+ 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x7, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf8, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40,
+ 0x0, 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xe0, 0x0, 0x0, 0x7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf8, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20,
+ 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0,
+ 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfc, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x71, 0x0, 0x0, 0x0, 0x0, 0x6e, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x75,
+ 0x55, 0x7a, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x20, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf2, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xe4, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x60, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xe7, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x39, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xea, 0x40,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x26, 0x9b, 0xde, 0xff,
+ 0xfe, 0xdb, 0x96, 0x30, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0031 "1" */
+ 0x39, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x96, 0x5f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x5f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x5f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x5f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x78, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x85,
+
+ /* U+0032 "2" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x12, 0x22, 0x21, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2, 0x68, 0xbd, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0xc9, 0x62, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x49, 0xdf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe9, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x7e,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfd, 0x60, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfd, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x2d, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0x10, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0,
+ 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf5, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0, 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,
+ 0x50, 0x0, 0x5, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0xa6, 0x31, 0x0, 0x12,
+ 0x47, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xb0, 0x0, 0x0, 0x1a, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x40, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x2, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0,
+ 0x0, 0x0, 0x4e, 0xff, 0xff, 0xff, 0xff, 0xfc,
+ 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff,
+ 0xff, 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf7, 0x0, 0x0, 0x0,
+ 0x0, 0x4, 0xdf, 0xff, 0xf7, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0x70,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x5, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xc, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+ 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1d, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0xdf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2d,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xe2, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x2e, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x20, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xd1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfd, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xc1, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
+ 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
+ 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdc, 0x0, 0x0,
+ 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, 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x0, 0xd, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0xd, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x0, 0xd, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0xd,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x0, 0xd, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0,
+ 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff,
+
+ /* U+0033 "3" */
+ 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0xb, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0,
+ 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf4, 0x0, 0x0, 0x0, 0xb, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0,
+ 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0xb, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0xbf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0,
+ 0x0, 0x0, 0xb, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0xbf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0,
+ 0x0, 0xb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x40, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xe1, 0x0, 0x0, 0x0,
+ 0xb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x0, 0x79, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1d, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfd, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x5, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x20, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x2, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xdf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xd1, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xe2, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xe, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x72, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
+ 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfd, 0x10, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xe1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0xbb,
+ 0xbb, 0xbb, 0xcc, 0xde, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+ 0x37, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x30, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0,
+ 0x0, 0x0, 0x7e, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x10, 0x0, 0x0, 0xe, 0xff, 0xa2,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0,
+ 0x7, 0xff, 0xff, 0xf9, 0x20, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff,
+ 0xb5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x7f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xa6, 0x10, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x28, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0,
+ 0x0, 0x0, 0xe, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xeb, 0x86, 0x54, 0x33, 0x45, 0x79,
+ 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf9, 0x0, 0x0, 0x7, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x7f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0, 0xe,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0,
+ 0x0, 0x2, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6e, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xe3, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x91, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5b,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2, 0x7c, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xa3, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0x59, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0xa6, 0x10, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x58,
+ 0xab, 0xde, 0xef, 0xff, 0xee, 0xdc, 0x97, 0x52,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0,
+
+ /* U+0034 "4" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x48, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xb, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xe1, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1e, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2e,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x30,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0, 0x0,
+ 0x0, 0x0, 0x2, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x10, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x3, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1e, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xe1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
+ 0xdd, 0xdd, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfd, 0xdd, 0xdd, 0xdd, 0xdd, 0xd3,
+ 0x5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0035 "5" */
+ 0x0, 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x4, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x30, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfb, 0xbb, 0xbb, 0xba, 0x99, 0x86,
+ 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xda, 0x62, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x71, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9,
+ 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe1, 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x20, 0x0, 0x0, 0x0, 0xf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x13, 0x46, 0x9c, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x6c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x10,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0,
+ 0x0, 0x0, 0x5f, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x10, 0x0, 0x0, 0xd, 0xff, 0xc3,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x0, 0x0,
+ 0x5, 0xff, 0xff, 0xfb, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0xdf, 0xff, 0xff, 0xff,
+ 0xc6, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1, 0xaf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x5f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xb6, 0x20, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x28, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0,
+ 0x0, 0x0, 0xd, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xeb, 0x97, 0x54, 0x33, 0x45, 0x79,
+ 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf9, 0x0, 0x0, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0, 0xe,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x6, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0, 0xdf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0,
+ 0x0, 0x1, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5d, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xe3, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xdf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x91, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4b,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0x6b, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xa3, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x48, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0xa6, 0x10, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x58,
+ 0x9b, 0xde, 0xef, 0xff, 0xee, 0xdc, 0x97, 0x52,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0,
+
+ /* U+0036 "6" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x25, 0x7a, 0xcd,
+ 0xef, 0xff, 0xfe, 0xdc, 0xa8, 0x63, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+ 0x5a, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xc7, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4b, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xd8, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x91,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4d, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xaf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x4, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xdd,
+ 0xde, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfd, 0x85, 0x10, 0x0, 0x0, 0x0, 0x14, 0x7a,
+ 0xef, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x92, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x5b, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb,
+ 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4, 0xd4, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xc, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xdf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0,
+ 0x0, 0x0, 0x3, 0x68, 0xab, 0xbb, 0xa9, 0x86,
+ 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x0, 0x0, 0x0, 0x27, 0xcf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x71, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0,
+ 0x4, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfb, 0x0, 0x2b, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xb2, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0,
+ 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf9, 0x9f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0x10, 0x0, 0x0, 0x0,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfd, 0x10, 0x0, 0x0, 0x2, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x10, 0x0,
+ 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x0, 0x0, 0x3, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8,
+ 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x3, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc0, 0x0, 0x2f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe9,
+ 0x52, 0x0, 0x2, 0x47, 0xcf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50, 0x1,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0x60, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x0, 0x1f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x10,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x5, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x70, 0xe, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x0, 0x7f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x5,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x20, 0x1f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x20, 0x9, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf1, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xd0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf2, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0,
+ 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xa0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x70, 0x0, 0x0, 0xbf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x2d, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x0, 0x0, 0x0, 0xa, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb2,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6e, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x0, 0x0, 0x0, 0x1f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfa, 0x51, 0x0, 0x0,
+ 0x0, 0x27, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0, 0x0, 0x0,
+ 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0xdc, 0xcd, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
+ 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xdf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4e, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfa, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x7, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6d, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfb, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3, 0x8d, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x82, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0x47, 0x9c, 0xde, 0xef, 0xfe, 0xdc,
+ 0xa7, 0x51, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0,
+
+ /* U+0037 "7" */
+ 0x6d, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
+ 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
+ 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
+ 0xdd, 0xdd, 0xdd, 0x48, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x8f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x58, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x8f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x58, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf5, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x58, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf5, 0x8f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x58,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf5, 0x8f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x48, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xd0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf6, 0x8, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x90, 0x8, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0,
+ 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfb, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x8f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xd0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x8f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0,
+ 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x80, 0x0, 0x0, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0,
+ 0x8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xe, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x70,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xe, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x2, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x7, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0038 "8" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x13, 0x56, 0x78, 0x88, 0x76,
+ 0x54, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x8b, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x95,
+ 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xb6, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6d, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x81, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1a, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x30, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2d,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2e,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x1d, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0, 0x9,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0,
+ 0x0, 0x0, 0x0, 0x3, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0,
+ 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x85, 0x21, 0x0, 0x14, 0x7d, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf1, 0x0, 0x0, 0x0, 0x2f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xe5, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3, 0xcf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0,
+ 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfd, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x0,
+ 0x0, 0xe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x40, 0x0, 0x1, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6,
+ 0x0, 0x0, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x3, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf9, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xe, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x1,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf7, 0x0, 0x0, 0xf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x0,
+ 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf2, 0x0, 0x0, 0x9, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0,
+ 0x0, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xd1, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x9f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x90, 0x0, 0x0, 0x0, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd2,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x6, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf7, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x5, 0xdf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0x0, 0x0, 0x0, 0x0,
+ 0xc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xb8, 0x54, 0x34, 0x57, 0xaf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x20, 0x0, 0x0, 0x0, 0x0, 0x2f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x3e, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfd, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x90, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xc1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe1, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xd1, 0x0, 0x0,
+ 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xdb, 0xa9, 0xab, 0xcf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xb0, 0x0, 0x0, 0x1f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x94,
+ 0x0, 0x0, 0x0, 0x0, 0x3, 0x8e, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x10, 0x2, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+ 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf7, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf3, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0xd, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x5f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x90,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xb7, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xe9, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0xd, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd4, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x9, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfa, 0x1f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf7, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf3, 0x8, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xcf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0x0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0xaf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
+ 0x93, 0x0, 0x0, 0x0, 0x0, 0x2, 0x7d, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf1, 0x0, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xca, 0xa9,
+ 0x9a, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf6, 0x0, 0x0, 0x7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x0, 0x0, 0x0, 0xb, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x20, 0x0, 0x0,
+ 0x0, 0xc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x30, 0x0, 0x0, 0x0, 0x0, 0x1c, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xb, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfb, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xcf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x4c, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0x70, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc6,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0xad,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0xb6, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x1, 0x47, 0x9b, 0xde,
+ 0xef, 0xff, 0xee, 0xdb, 0x97, 0x52, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+
+ /* U+0039 "9" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x4, 0x79, 0xcd, 0xef, 0xff, 0xed, 0xca,
+ 0x74, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x16, 0xbf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe9, 0x30,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3,
+ 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xe8, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2a, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x81, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1, 0xbf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfb, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x2, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x30,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xcf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xcc,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf9, 0x0, 0x0, 0x0, 0x0, 0xc,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x83, 0x0, 0x0, 0x0, 0x4, 0x9f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x4, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0, 0x0,
+ 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x60, 0x0, 0x0, 0xf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0,
+ 0x4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf4, 0x0, 0x0, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0,
+ 0x0, 0xb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x20, 0x0, 0xef, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf7, 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0, 0x0, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x0, 0x1f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x70, 0xf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa,
+ 0x0, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xc0, 0xb, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x4, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x20, 0xf, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0,
+ 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5,
+ 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x40, 0x3, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x84,
+ 0x21, 0x0, 0x24, 0x8e, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x50, 0x0, 0x2f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf6, 0x0, 0x0, 0x6f, 0xff, 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, 0x0, 0x0,
+ 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, 0xf5, 0x0, 0x0, 0x0, 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, 0x50, 0x0,
+ 0x0, 0x0, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x9f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x6f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20,
+ 0x0, 0x0, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xf8, 0x6, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf1, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd3, 0x0,
+ 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x9f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfd, 0x50, 0x0, 0xa, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x16, 0xaf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfd, 0x83, 0x0, 0x0,
+ 0x0, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x3, 0x58, 0x9a, 0xbb, 0xba, 0x87,
+ 0x41, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xef, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x20, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x3f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2e,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x2e, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4e,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e,
+ 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x1, 0x9f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf5, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x8, 0xff, 0xd6, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x18, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+ 0xff, 0xff, 0xff, 0xb7, 0x52, 0x0, 0x0, 0x0,
+ 0x1, 0x47, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x10, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x8f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xee, 0xdd, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfc, 0x20, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3e, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x7, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0x80, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x6c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc6,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x7b,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xb7, 0x20, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x25, 0x8a, 0xcd,
+ 0xef, 0xff, 0xfe, 0xdc, 0xa8, 0x62, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0,
+
+ /* U+003A ":" */
+ 0x0, 0x0, 0x0, 0x59, 0xbc, 0xba, 0x61, 0x0,
+ 0x0, 0x0, 0x0, 0x6, 0xef, 0xff, 0xff, 0xff,
+ 0xf8, 0x0, 0x0, 0x0, 0xa, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x10, 0x0, 0xa, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0, 0x6, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0,
+ 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf1, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x78, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfb, 0xaf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xcb, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xaf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfb, 0x4f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x60, 0xef, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xf1, 0x6, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x0, 0xb,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x0,
+ 0x0, 0xb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
+ 0x20, 0x0, 0x0, 0x8, 0xff, 0xff, 0xff, 0xff,
+ 0xfa, 0x0, 0x0, 0x0, 0x0, 0x1, 0x7c, 0xef,
+ 0xec, 0x82, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5,
+ 0x9b, 0xcb, 0xa6, 0x10, 0x0, 0x0, 0x0, 0x0,
+ 0x6e, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0,
+ 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc1,
+ 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xc0, 0x0, 0x6f, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x90, 0xe, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x8f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xba, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfc, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xda, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfc, 0x8f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb4, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf6,
+ 0xe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x10, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x90, 0x0, 0xbf, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xd0, 0x0, 0x0, 0xbf, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xd2, 0x0, 0x0, 0x0,
+ 0x8f, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x0, 0x0,
+ 0x0, 0x0, 0x17, 0xce, 0xfe, 0xc8, 0x20, 0x0,
+ 0x0
+};
+
+
+/*---------------------
+ * GLYPH DESCRIPTION
+ *--------------------*/
+
+static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
+ {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
+ {.bitmap_index = 0, .adv_w = 435, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 0, .adv_w = 1043, .box_w = 59, .box_h = 71, .ofs_x = 3, .ofs_y = -1},
+ {.bitmap_index = 2095, .adv_w = 602, .box_w = 30, .box_h = 69, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 3130, .adv_w = 906, .box_w = 54, .box_h = 70, .ofs_x = 0, .ofs_y = 0},
+ {.bitmap_index = 5020, .adv_w = 909, .box_w = 55, .box_h = 69, .ofs_x = -1, .ofs_y = -1},
+ {.bitmap_index = 6918, .adv_w = 1058, .box_w = 64, .box_h = 69, .ofs_x = 2, .ofs_y = -1},
+ {.bitmap_index = 9126, .adv_w = 914, .box_w = 55, .box_h = 69, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 11024, .adv_w = 978, .box_w = 57, .box_h = 70, .ofs_x = 3, .ofs_y = -1},
+ {.bitmap_index = 13019, .adv_w = 952, .box_w = 55, .box_h = 68, .ofs_x = 2, .ofs_y = 0},
+ {.bitmap_index = 14889, .adv_w = 1014, .box_w = 57, .box_h = 71, .ofs_x = 3, .ofs_y = -1},
+ {.bitmap_index = 16913, .adv_w = 978, .box_w = 57, .box_h = 70, .ofs_x = 1, .ofs_y = -1},
+ {.bitmap_index = 18908, .adv_w = 402, .box_w = 19, .box_h = 54, .ofs_x = 3, .ofs_y = -1}
+};
+
+/*---------------------
+ * CHARACTER MAPPING
+ *--------------------*/
+
+
+
+/*Collect the unicode lists and glyph_id offsets*/
+static const lv_font_fmt_txt_cmap_t cmaps[] =
+{
+ {
+ .range_start = 32, .range_length = 1, .glyph_id_start = 1,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
+ },
+ {
+ .range_start = 48, .range_length = 11, .glyph_id_start = 2,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
+ }
+};
+
+/*-----------------
+ * KERNING
+ *----------------*/
+
+
+/*Pair left and right glyphs for kerning*/
+static const uint8_t kern_pair_glyph_ids[] =
+{
+ 2, 3,
+ 2, 4,
+ 2, 5,
+ 2, 9,
+ 4, 6,
+ 5, 4,
+ 5, 5,
+ 5, 7,
+ 5, 9,
+ 6, 3,
+ 6, 5,
+ 6, 9,
+ 6, 10,
+ 6, 11,
+ 6, 12,
+ 7, 4,
+ 7, 5,
+ 7, 7,
+ 7, 9,
+ 8, 9,
+ 9, 2,
+ 9, 3,
+ 9, 5,
+ 9, 6,
+ 9, 7,
+ 9, 8,
+ 9, 10,
+ 9, 12,
+ 11, 3,
+ 11, 4,
+ 11, 5,
+ 11, 9,
+ 12, 9
+};
+
+/* Kerning between the respective left and right glyphs
+ * 4.4 format which needs to scaled with `kern_scale`*/
+static const int8_t kern_pair_values[] =
+{
+ -6, -6, -15, -6, -15, -8, -8, -15,
+ -14, -38, -11, -65, 15, -6, 12, -8,
+ -8, -8, -18, -6, -31, 15, -11, -100,
+ -26, -31, -26, -31, -6, -6, -15, -6,
+ 6
+};
+
+/*Collect the kern pair's data in one place*/
+static const lv_font_fmt_txt_kern_pair_t kern_pairs =
+{
+ .glyph_ids = kern_pair_glyph_ids,
+ .values = kern_pair_values,
+ .pair_cnt = 33,
+ .glyph_ids_size = 0
+};
+
+/*--------------------
+ * ALL CUSTOM DATA
+ *--------------------*/
+
+#if LVGL_VERSION_MAJOR == 8
+/*Store all the custom data of the font*/
+static lv_font_fmt_txt_glyph_cache_t cache;
+#endif
+
+#if LVGL_VERSION_MAJOR >= 8
+static const lv_font_fmt_txt_dsc_t font_dsc = {
+#else
+static lv_font_fmt_txt_dsc_t font_dsc = {
+#endif
+ .glyph_bitmap = glyph_bitmap,
+ .glyph_dsc = glyph_dsc,
+ .cmaps = cmaps,
+ .kern_dsc = &kern_pairs,
+ .kern_scale = 16,
+ .cmap_num = 2,
+ .bpp = 4,
+ .kern_classes = 0,
+ .bitmap_format = 0,
+#if LVGL_VERSION_MAJOR == 8
+ .cache = &cache
+#endif
+};
+
+
+
+/*-----------------
+ * PUBLIC FONT
+ *----------------*/
+
+/*Initialize a public general font descriptor*/
+#if LVGL_VERSION_MAJOR >= 8
+const lv_font_t montserrat_bold_96 = {
+#else
+lv_font_t montserrat_bold_96 = {
+#endif
+ .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
+ .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
+ .line_height = 71, /*The maximum line height required by the font*/
+ .base_line = 1, /*Baseline measured from the bottom of the line*/
+#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
+ .subpx = LV_FONT_SUBPX_NONE,
+#endif
+#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
+ .underline_position = -10,
+ .underline_thickness = 5,
+#endif
+ .dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
+#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
+ .fallback = NULL,
+#endif
+ .user_data = NULL,
+};
+
+
+
+#endif /*#if MONTSERRAT_BOLD_96*/
+
diff --git a/Framing.h b/Framing.h
index 634fd22f..0fc8b368 100644
--- a/Framing.h
+++ b/Framing.h
@@ -47,6 +47,9 @@
#define CMD_STAT_BAT 0x27
#define CMD_STAT_CSMA 0x28
#define CMD_STAT_TEMP 0x29
+ #define CMD_STAT_GPS 0x2A
+ #define CMD_GPS_NMEA 0x2B
+ #define CMD_DIAG 0x2C
#define CMD_BLINK 0x30
#define CMD_RANDOM 0x40
@@ -71,6 +74,11 @@
#define CMD_WIFI_CHN 0x6E
#define CMD_WIFI_IP 0x84
#define CMD_WIFI_NM 0x85
+ #define CMD_BCN_KEY 0x86
+ #define CMD_LXMF_HASH 0x87
+ #define CMD_LXMF_TEST 0x88
+ #define CMD_IFAC_KEY 0x89
+ #define CMD_TRANSPORT_ID 0x8A
#define CMD_BOARD 0x47
#define CMD_PLATFORM 0x48
@@ -115,10 +123,31 @@
#define ERROR_MEMORY_LOW 0x05
#define ERROR_MODEM_TIMEOUT 0x06
- // Serial framing variables
- size_t frame_len;
- bool IN_FRAME = false;
- bool ESCAPE = false;
- uint8_t command = CMD_UNKNOWN;
+ // Channel constants
+ #define CHANNEL_USB 0
+ #define CHANNEL_BT 1
+ #define CHANNEL_WIFI 2
+
+ // Compile-time channel count per platform
+ #if HAS_WIFI == true
+ #define NUM_CHANNELS 3
+ #elif HAS_BLUETOOTH == true || HAS_BLE == true
+ #define NUM_CHANNELS 2
+ #else
+ #define NUM_CHANNELS 1
+ #endif
+
+ // Per-channel parser state
+ typedef struct {
+ bool in_frame;
+ bool escape;
+ uint8_t command;
+ size_t frame_len;
+ uint8_t cmdbuf[CMD_L];
+ #if NUM_CHANNELS > 1
+ uint8_t pktbuf[MTU];
+ uint16_t pkt_len;
+ #endif
+ } ChannelState;
#endif
\ No newline at end of file
diff --git a/GPS.h b/GPS.h
new file mode 100644
index 00000000..d4754a20
--- /dev/null
+++ b/GPS.h
@@ -0,0 +1,199 @@
+// Copyright (C) 2024, Mark Qvist
+// Copyright (C) 2026, GPS support contributed by GlassOnTin
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#ifndef GPS_H
+#define GPS_H
+
+#if HAS_GPS == true
+
+#include
+#include
+
+TinyGPSPlus gps_parser;
+HardwareSerial gps_serial(1);
+
+bool gps_ready = false;
+bool gps_has_fix = false;
+uint8_t gps_sats = 0;
+double gps_lat = 0.0;
+double gps_lon = 0.0;
+double gps_alt = 0.0;
+double gps_speed = 0.0;
+double gps_hdop = 0.0;
+uint32_t gps_last_update = 0;
+uint32_t gps_last_report = 0;
+#define GPS_REPORT_INTERVAL_MS 30000 // Report GPS stats to host every 30s
+
+void kiss_indicate_stat_gps();
+
+// Host activity tracking — shared with Beacon.h
+// GPS telemetry and beacon mode both suppress when host is active.
+#define BEACON_NO_HOST_TIMEOUT_MS 15000
+uint32_t last_host_activity = 0;
+
+void gps_power_on() {
+ #if defined(PIN_GPS_EN)
+ pinMode(PIN_GPS_EN, OUTPUT);
+ #if GPS_EN_ACTIVE == LOW
+ digitalWrite(PIN_GPS_EN, LOW);
+ #else
+ digitalWrite(PIN_GPS_EN, HIGH);
+ #endif
+ #endif
+
+ #if defined(PIN_GPS_RST)
+ // Keep reset HIGH (inactive) to preserve backup RAM (ephemeris/almanac).
+ // This allows warm/hot starts with much faster time-to-fix.
+ pinMode(PIN_GPS_RST, OUTPUT);
+ digitalWrite(PIN_GPS_RST, HIGH);
+ #endif
+
+ #if defined(PIN_GPS_STANDBY)
+ pinMode(PIN_GPS_STANDBY, OUTPUT);
+ digitalWrite(PIN_GPS_STANDBY, HIGH);
+ #endif
+}
+
+void gps_power_off() {
+ #if defined(PIN_GPS_EN)
+ #if GPS_EN_ACTIVE == LOW
+ digitalWrite(PIN_GPS_EN, HIGH);
+ #else
+ digitalWrite(PIN_GPS_EN, LOW);
+ #endif
+ #endif
+}
+
+void gps_setup() {
+ gps_power_on();
+ delay(1000); // Allow GPS module time to boot
+ // PIN_GPS_TX/RX named from ESP32 perspective:
+ // PIN_GPS_RX = ESP32 receives FROM GPS module
+ // PIN_GPS_TX = ESP32 transmits TO GPS module
+ gps_serial.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX);
+ delay(250);
+
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ // MIA-M10Q (u-blox): outputs NMEA at 38400 baud by default.
+ // GGA, GSA, GSV, RMC are enabled out of the box.
+ // No vendor-specific init commands needed.
+ #else
+ // L76K init: force internal antenna (ceramic patch)
+ gps_serial.print("$PCAS15,0*19\r\n");
+ delay(250);
+ // Hot start — use cached ephemeris/almanac if available in L76K backup RAM
+ gps_serial.print("$PCAS10,0*1C\r\n");
+ delay(500);
+ // Enable GPS+GLONASS+BeiDou
+ gps_serial.print("$PCAS04,7*1E\r\n");
+ delay(250);
+ // Output GGA, GSA, GSV, and RMC
+ gps_serial.print("$PCAS03,1,0,1,1,1,0,0,0,0,0,,,0,0*02\r\n");
+ delay(250);
+ // Set navigation mode to Portable (general purpose, works stationary and moving)
+ gps_serial.print("$PCAS11,0*1D\r\n");
+ delay(250);
+ #endif
+
+ gps_ready = true;
+}
+
+// GPS dynamic model options — indexed by roller selection
+const uint8_t gps_model_ubx[] = { 0, 2, 3, 4 }; // Portable, Stationary, Pedestrian, Automotive
+#define GPS_MODEL_OPTIONS_COUNT 4
+uint8_t gps_dynamic_model = 0; // Current model index (default: Portable)
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+void gps_set_dynamic_model(uint8_t model_index) {
+ if (model_index >= GPS_MODEL_OPTIONS_COUNT) return;
+ uint8_t dyn = gps_model_ubx[model_index];
+ gps_dynamic_model = model_index;
+ uint8_t msg[] = {
+ 0xB5, 0x62, 0x06, 0x8A, 0x09, 0x00,
+ 0x00, 0x01, 0x00, 0x00,
+ 0x21, 0x00, 0x11, 0x20,
+ dyn, 0x00, 0x00
+ };
+ uint8_t ck_a = 0, ck_b = 0;
+ for (int i = 2; i < (int)sizeof(msg) - 2; i++) { ck_a += msg[i]; ck_b += ck_a; }
+ msg[sizeof(msg) - 2] = ck_a;
+ msg[sizeof(msg) - 1] = ck_b;
+ gps_serial.write(msg, sizeof(msg));
+}
+#else
+void gps_set_dynamic_model(uint8_t model_index) {
+ if (model_index >= GPS_MODEL_OPTIONS_COUNT) return;
+ gps_dynamic_model = model_index;
+ const char *cmds[] = { "$PCAS11,0*1D\r\n", "$PCAS11,1*1C\r\n",
+ "$PCAS11,2*1F\r\n", "$PCAS11,3*1E\r\n" };
+ gps_serial.print(cmds[model_index]);
+}
+#endif
+
+void gps_update() {
+ if (!gps_ready) return;
+
+ while (gps_serial.available() > 0) {
+ gps_parser.encode(gps_serial.read());
+ }
+
+ if (gps_parser.location.isUpdated()) {
+ gps_has_fix = gps_parser.location.isValid();
+ if (gps_has_fix) {
+ gps_lat = gps_parser.location.lat();
+ gps_lon = gps_parser.location.lng();
+ gps_last_update = millis();
+ }
+ }
+
+ if (gps_parser.altitude.isUpdated() && gps_parser.altitude.isValid()) {
+ gps_alt = gps_parser.altitude.meters();
+ }
+
+ if (gps_parser.speed.isUpdated() && gps_parser.speed.isValid()) {
+ gps_speed = gps_parser.speed.kmph();
+ }
+
+ if (gps_parser.satellites.isUpdated()) {
+ gps_sats = gps_parser.satellites.value();
+ }
+
+ if (gps_parser.hdop.isUpdated()) {
+ gps_hdop = gps_parser.hdop.hdop();
+ }
+
+ // Mark fix as stale after 10 seconds without update
+ if (gps_has_fix && (millis() - gps_last_update > 10000)) {
+ gps_has_fix = false;
+ }
+
+ // Periodically report GPS stats to host (like battery/temp in Power.h).
+ // rnsd parses CMD_STAT_GPS frames and exposes them in interface stats.
+ if (millis() - gps_last_report >= GPS_REPORT_INTERVAL_MS) {
+ response_channel = data_channel;
+ kiss_indicate_stat_gps();
+ gps_last_report = millis();
+ }
+}
+
+void gps_teardown() {
+ gps_serial.end();
+ gps_power_off();
+ gps_ready = false;
+}
+
+#endif
+#endif
diff --git a/Gui.h b/Gui.h
new file mode 100644
index 00000000..13025482
--- /dev/null
+++ b/Gui.h
@@ -0,0 +1,2128 @@
+// R-Watch GUI — LVGL integration for T-Watch Ultimate
+// Tileview navigation with watch face, radio status, GPS, messages, settings
+// Requires: CO5300.h (display), XL9555.h (power control), DRV2605.h (haptic)
+
+#ifndef GUI_H
+#define GUI_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+
+#include
+#include "soc/rtc_cntl_reg.h"
+
+// Custom fonts: generated with lv_font_conv --no-compress from Montserrat Bold
+// IMPORTANT: must use --no-compress or set LV_USE_FONT_COMPRESSED=1 in lv_conf.h
+namespace _f96 {
+ #include "Fonts/montserrat_bold_96.c"
+}
+namespace _f28 {
+ #include "Fonts/montserrat_bold_28.c"
+}
+static const lv_font_t &font_time = _f96::montserrat_bold_96;
+static const lv_font_t &font_mid = _f28::montserrat_bold_28;
+
+// ---------------------------------------------------------------------------
+// Color palette (24-bit hex for lv_color_hex)
+// ---------------------------------------------------------------------------
+#define GUI_COL_BLACK 0x000000
+#define GUI_COL_WHITE 0xFFFFEF // Bone white
+#define GUI_COL_DIM 0x404040 // Dividers, inactive labels
+#define GUI_COL_MID 0x808080 // Secondary text
+#define GUI_COL_AMBER 0xFFA500 // LoRa / radio accent
+#define GUI_COL_TEAL 0x00FFC0 // GPS accent
+#define GUI_COL_BLUE 0x4080FF // BLE accent
+#define GUI_COL_RED 0xFF0000 // Warnings
+#define GUI_COL_GREEN 0x00FF00 // Confirmations
+
+// ---------------------------------------------------------------------------
+// Layout constants (410x502 display)
+// ---------------------------------------------------------------------------
+#define GUI_W CO5300_WIDTH
+#define GUI_H CO5300_HEIGHT
+#define GUI_PAD 24
+
+// Watch face zones (adjusted for 96px time font)
+#define GUI_STATUS_Y 10
+#define GUI_TIME_Y 40
+#define GUI_DATE_Y 150
+#define GUI_RULE1_Y 190
+#define GUI_COMP_Y 200
+#define GUI_COMP_H 90
+#define GUI_RULE2_Y 295
+
+// ---------------------------------------------------------------------------
+// LVGL core objects
+// ---------------------------------------------------------------------------
+static lv_display_t *gui_display = NULL;
+static lv_indev_t *gui_indev = NULL;
+
+// Full-frame double buffer in PSRAM — tear-free rendering.
+// LVGL only re-renders dirty areas within the buffer but always
+// flushes the complete frame (~18ms via DMA SPI, CPU yields).
+// Two buffers: 410*502*2 = 411,640 bytes each (823KB total).
+#define GUI_BUF_LINES GUI_H
+static uint8_t *gui_buf1 = NULL;
+static uint8_t *gui_buf2 = NULL;
+
+// Tileview and tiles
+static lv_obj_t *gui_tileview = NULL;
+static lv_obj_t *gui_tile_watch = NULL;
+static lv_obj_t *gui_tile_radio = NULL;
+static lv_obj_t *gui_tile_gps = NULL;
+static lv_obj_t *gui_tile_msg = NULL;
+static lv_obj_t *gui_tile_set = NULL;
+
+// Convenience alias for display_unblank invalidation
+static lv_obj_t *gui_screen = NULL;
+
+// Watch face widgets
+static lv_obj_t *gui_mode_label = NULL;
+static lv_obj_t *gui_batt_label = NULL;
+static lv_obj_t *gui_time_label = NULL;
+static lv_obj_t *gui_date_label = NULL;
+static lv_obj_t *gui_lora_value = NULL;
+static lv_obj_t *gui_lora_label = NULL;
+static lv_obj_t *gui_gps_value = NULL;
+static lv_obj_t *gui_gps_label = NULL;
+static lv_obj_t *gui_batt_value = NULL; // battery detail in complications
+static lv_obj_t *gui_batt_detail = NULL;
+static lv_obj_t *gui_batt_icon = NULL; // battery bar icon
+static lv_obj_t *gui_batt_fill = NULL; // fill bar inside icon
+static lv_obj_t *gui_batt_cell = NULL; // complication cell (for click event)
+static uint8_t gui_batt_mode = 0; // 0=voltage, 1=percent, 2=time, 3=icon
+#define GUI_BATT_MODES 4
+static lv_obj_t *gui_step_label = NULL; // step count below complications
+
+// Battery time estimation
+static float batt_pct_history[4] = {-1,-1,-1,-1}; // last 4 readings (every 60s)
+static uint32_t batt_hist_last = 0;
+static float batt_rate_pct_hr = 0; // %/hour (positive=charging, negative=discharging)
+
+// Radio status widgets
+static lv_obj_t *gui_radio_freq = NULL;
+static lv_obj_t *gui_radio_params = NULL;
+static lv_obj_t *gui_radio_rssi_bar = NULL;
+static lv_obj_t *gui_radio_rssi_lbl = NULL;
+static lv_obj_t *gui_radio_util = NULL;
+static lv_obj_t *gui_radio_ble = NULL;
+static lv_obj_t *gui_radio_pkts = NULL;
+
+// Radio screen additional widgets
+static lv_obj_t *gui_radio_temp = NULL;
+static lv_obj_t *gui_radio_batt = NULL;
+
+// GPS screen widgets
+static lv_obj_t *gui_gps_coords = NULL;
+static lv_obj_t *gui_gps_fix = NULL;
+static lv_obj_t *gui_gps_alt = NULL;
+static lv_obj_t *gui_gps_beacon = NULL;
+
+// Bubble level widgets
+static lv_obj_t *gui_level_ring = NULL; // outer circle
+static lv_obj_t *gui_level_dot = NULL; // moving bubble
+static lv_obj_t *gui_level_cross_h = NULL; // crosshair horizontal
+static lv_obj_t *gui_level_cross_v = NULL; // crosshair vertical
+static lv_obj_t *gui_level_angle = NULL; // tilt angle text
+#define GUI_LEVEL_SIZE 140 // ring diameter
+#define GUI_LEVEL_DOT 16 // bubble diameter
+#define GUI_LEVEL_Y 340 // vertical position on watch face
+
+// Signal strength view (toggles with bubble level)
+static lv_obj_t *gui_signal_cont = NULL; // container for signal view
+static lv_obj_t *gui_signal_line = NULL; // sparkline graph
+static lv_obj_t *gui_signal_rssi = NULL; // current RSSI text
+static lv_obj_t *gui_signal_dir = NULL; // direction arrow/text
+static bool gui_show_signal = false; // false=bubble level, true=signal
+
+// RSSI history for sparkline
+#define RSSI_HISTORY_LEN 60
+static int16_t rssi_history[RSSI_HISTORY_LEN];
+static uint8_t rssi_history_idx = 0;
+static uint8_t rssi_history_count = 0;
+static lv_point_precise_t rssi_graph_pts[RSSI_HISTORY_LEN];
+
+// GPS+RSSI history for direction finding
+#define DIR_HISTORY_LEN 8
+struct dir_sample_t { double lat, lon; int rssi; };
+static dir_sample_t dir_history[DIR_HISTORY_LEN];
+static uint8_t dir_history_idx = 0;
+static uint8_t dir_history_count = 0;
+
+// Touch input via function pointer (set by .ino after touch init)
+typedef bool (*gui_touch_fn_t)(int16_t *x, int16_t *y);
+static gui_touch_fn_t gui_touch_fn = NULL;
+
+// Data update throttle
+static uint32_t gui_last_data_update = 0;
+#define GUI_DATA_UPDATE_MS 500
+
+// Track current tile for haptic feedback
+static uint8_t gui_last_tile_col = 1;
+static uint8_t gui_last_tile_row = 1;
+static bool gui_was_scrolling = false;
+
+// Frame timing metrics
+static uint32_t gui_frame_count = 0;
+
+// Main loop profiling (set from .ino, read by metrics command)
+uint32_t prof_radio_us = 0;
+uint32_t prof_serial_us = 0;
+uint32_t prof_display_us = 0;
+uint32_t prof_pmu_us = 0;
+uint32_t prof_gps_us = 0;
+uint32_t prof_bt_us = 0;
+uint32_t prof_imu_us = 0;
+uint32_t prof_other_us = 0;
+static uint32_t gui_flush_us_total = 0;
+static uint32_t gui_flush_us_last = 0;
+static uint32_t gui_render_us_last = 0;
+static uint32_t gui_render_start = 0;
+static uint32_t gui_loop_us_last = 0; // time between gui_update() calls
+static uint32_t gui_loop_us_max = 0; // worst case loop time
+static uint32_t gui_last_update_us = 0;
+
+// Remote touch injection
+static int16_t gui_inject_x = -1;
+static int16_t gui_inject_y = -1;
+static bool gui_inject_pressed = false;
+static uint32_t gui_inject_until = 0; // millis() deadline for injected touch
+
+// ---------------------------------------------------------------------------
+// LVGL display flush callback
+// ---------------------------------------------------------------------------
+// Shadow framebuffer for screenshots (RGB565 swapped / big-endian — same as display)
+uint16_t *gui_screenshot_buf = NULL;
+
+// Forward declarations — defined in Display.h / Power.h / .ino after Gui.h is included
+void display_unblank();
+extern float pmu_temperature;
+extern volatile uint32_t imu_step_count;
+// Sensor logger toggle — set by .ino after IMULogger.h is included
+typedef bool (*gui_log_toggle_fn_t)();
+static gui_log_toggle_fn_t gui_log_toggle_fn = NULL;
+// SD file listing and download — set by .ino after SD is available
+typedef void (*gui_list_files_fn_t)();
+static gui_list_files_fn_t gui_list_files_fn = NULL;
+typedef void (*gui_download_file_fn_t)(uint8_t index);
+static gui_download_file_fn_t gui_download_file_fn = NULL;
+static bool gui_imu_logging = false;
+// Forward declarations for IMULogger.h variables (defined later in compilation)
+extern bool imu_logging;
+extern uint32_t imu_log_samples;
+extern uint32_t imu_log_start_ms;
+// Forward declaration for touch logging (defined in IMULogger.h)
+void sensor_log_touch(int16_t x, int16_t y, bool pressed);
+// Forward declarations for filtered accel and noise (defined in .ino)
+extern volatile float imu_ax_f, imu_ay_f, imu_az_f;
+extern volatile float imu_noise;
+// Forward declarations for radio/GPS toggle (defined in .ino / GPS.h / IfacAuth.h)
+bool startRadio();
+void stopRadio();
+void update_radio_lock();
+void gps_power_on();
+void gps_power_off();
+void gps_setup();
+extern bool ifac_configured;
+#ifndef PMU_TEMP_MIN
+#define PMU_TEMP_MIN -30
+#endif
+static volatile bool gui_screenshot_pending = false; // set true to capture next frame
+
+static void gui_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
+ uint16_t x1 = area->x1;
+ uint16_t y1 = area->y1;
+ uint16_t w = area->x2 - area->x1 + 1;
+ uint16_t h = area->y2 - area->y1 + 1;
+ uint16_t *pixels = (uint16_t *)px_map;
+
+ // Copy to shadow framebuffer when screenshot capture is active
+ if (gui_screenshot_buf && gui_screenshot_pending) {
+ for (uint16_t row = 0; row < h; row++) {
+ memcpy(&gui_screenshot_buf[(y1 + row) * GUI_W + x1],
+ &pixels[row * w], w * sizeof(uint16_t));
+ }
+ }
+
+ uint32_t t0 = micros();
+ co5300_push_pixels(x1, y1, w, h, pixels);
+ gui_flush_us_last = micros() - t0;
+ gui_flush_us_total += gui_flush_us_last;
+ gui_frame_count++;
+ lv_display_flush_ready(disp);
+}
+
+// ---------------------------------------------------------------------------
+// LVGL touch input read callback
+// ---------------------------------------------------------------------------
+static void gui_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
+ // Check for injected remote touch first
+ if (gui_inject_pressed && millis() < gui_inject_until) {
+ data->point.x = gui_inject_x;
+ data->point.y = gui_inject_y;
+ data->state = LV_INDEV_STATE_PRESSED;
+ last_unblank_event = millis();
+ return;
+ }
+ gui_inject_pressed = false;
+
+ // Real touch hardware
+ int16_t tx, ty;
+ if (gui_touch_fn && gui_touch_fn(&tx, &ty)) {
+ data->point.x = tx;
+ data->point.y = ty;
+ data->state = LV_INDEV_STATE_PRESSED;
+ last_unblank_event = millis();
+ #if HAS_SD
+ sensor_log_touch(tx, ty, true);
+ #endif
+ } else {
+ data->state = LV_INDEV_STATE_RELEASED;
+ }
+}
+
+void gui_set_touch_handler(gui_touch_fn_t fn) {
+ gui_touch_fn = fn;
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+static void gui_style_black_container(lv_obj_t *obj) {
+ lv_obj_set_style_bg_color(obj, lv_color_hex(GUI_COL_BLACK), 0);
+ lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
+ lv_obj_set_style_border_width(obj, 0, 0);
+ lv_obj_set_style_pad_all(obj, 0, 0);
+ lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
+}
+
+static lv_obj_t *gui_create_rule(lv_obj_t *parent, lv_coord_t y) {
+ static lv_point_precise_t rule_pts[] = {
+ {GUI_PAD, 0}, {GUI_W - GUI_PAD, 0}
+ };
+ lv_obj_t *line = lv_line_create(parent);
+ lv_line_set_points(line, rule_pts, 2);
+ lv_obj_set_style_line_color(line, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_line_width(line, 1, 0);
+ lv_obj_set_pos(line, 0, y);
+ return line;
+}
+
+static lv_obj_t *gui_label(lv_obj_t *parent, const lv_font_t *font,
+ uint32_t color, const char *text) {
+ lv_obj_t *lbl = lv_label_create(parent);
+ lv_obj_set_style_text_font(lbl, font, 0);
+ lv_obj_set_style_text_color(lbl, lv_color_hex(color), 0);
+ lv_label_set_text(lbl, text);
+ return lbl;
+}
+
+static lv_obj_t *gui_label_at(lv_obj_t *parent, const lv_font_t *font,
+ uint32_t color, const char *text,
+ lv_coord_t x, lv_coord_t y) {
+ lv_obj_t *lbl = gui_label(parent, font, color, text);
+ lv_obj_set_pos(lbl, x, y);
+ return lbl;
+}
+
+static void gui_create_complication(lv_obj_t *parent, lv_coord_t x, lv_coord_t w,
+ uint32_t color, const char *label_text,
+ lv_obj_t **value_out, lv_obj_t **label_out) {
+ lv_obj_t *cell = lv_obj_create(parent);
+ lv_obj_remove_style_all(cell);
+ lv_obj_set_size(cell, w, GUI_COMP_H);
+ lv_obj_set_pos(cell, x, 0);
+
+ lv_obj_t *val = gui_label(cell, &font_mid, color, "--");
+ lv_obj_set_width(val, w);
+ lv_obj_set_style_text_align(val, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(val, LV_ALIGN_TOP_MID, 0, 4);
+
+ lv_obj_t *lbl = gui_label(cell, &lv_font_montserrat_14, GUI_COL_DIM, label_text);
+ lv_obj_set_width(lbl, w);
+ lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 46);
+
+ if (value_out) *value_out = val;
+ if (label_out) *label_out = lbl;
+}
+
+// ---------------------------------------------------------------------------
+// Screen: Watch Face (center tile)
+// ---------------------------------------------------------------------------
+static void gui_create_watchface(lv_obj_t *parent) {
+ gui_style_black_container(parent);
+
+ // Status bar
+ gui_mode_label = gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_MID, "IDLE", GUI_PAD, GUI_STATUS_Y);
+ gui_batt_label = gui_label(parent, &lv_font_montserrat_14, GUI_COL_MID, "--%");
+ lv_obj_align(gui_batt_label, LV_ALIGN_TOP_RIGHT, -GUI_PAD, GUI_STATUS_Y);
+
+ // Time (96px custom font — digits and colon only)
+ gui_time_label = gui_label(parent, &font_time, GUI_COL_WHITE, "00:00");
+ lv_obj_set_style_text_letter_space(gui_time_label, 2, 0);
+ lv_obj_set_width(gui_time_label, GUI_W);
+ lv_obj_set_style_text_align(gui_time_label, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_set_pos(gui_time_label, 0, GUI_TIME_Y);
+
+ // Date
+ gui_date_label = gui_label(parent, &font_mid, GUI_COL_MID, "--- -- ---");
+ lv_obj_set_width(gui_date_label, GUI_W);
+ lv_obj_set_style_text_align(gui_date_label, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_set_pos(gui_date_label, 0, GUI_DATE_Y);
+
+ // Rule 1
+ gui_create_rule(parent, GUI_RULE1_Y);
+
+ // Complications
+ lv_obj_t *comp = lv_obj_create(parent);
+ lv_obj_remove_style_all(comp);
+ lv_obj_set_size(comp, GUI_W, GUI_COMP_H);
+ lv_obj_set_pos(comp, 0, GUI_COMP_Y);
+ lv_obj_clear_flag(comp, LV_OBJ_FLAG_SCROLLABLE);
+
+ int cw = (GUI_W - GUI_PAD * 2) / 3;
+
+ // LoRa complication — tap to toggle radio
+ {
+ lv_obj_t *cell = lv_obj_create(comp);
+ lv_obj_remove_style_all(cell);
+ lv_obj_set_size(cell, cw, GUI_COMP_H);
+ lv_obj_set_pos(cell, GUI_PAD, 0);
+ lv_obj_add_flag(cell, LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_clear_flag(cell, (lv_obj_flag_t)(LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SCROLLABLE));
+ lv_obj_add_event_cb(cell, [](lv_event_t *e) {
+ if (radio_online) { stopRadio(); }
+ else {
+ if (lora_freq == 0 || lora_freq == 0xFFFFFFFF) {
+ lora_freq = 868000000; lora_bw = 125000;
+ lora_sf = 7; lora_cr = 5; lora_txp = 17;
+ }
+ startRadio();
+ }
+ }, LV_EVENT_CLICKED, NULL);
+ gui_lora_value = gui_label(cell, &font_mid, GUI_COL_AMBER, "--");
+ lv_obj_set_width(gui_lora_value, cw);
+ lv_obj_set_style_text_align(gui_lora_value, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(gui_lora_value, LV_ALIGN_TOP_MID, 0, 4);
+ gui_lora_label = gui_label(cell, &lv_font_montserrat_14, GUI_COL_DIM, "LoRa");
+ lv_obj_set_width(gui_lora_label, cw);
+ lv_obj_set_style_text_align(gui_lora_label, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(gui_lora_label, LV_ALIGN_TOP_MID, 0, 46);
+ }
+
+ // GPS complication — tap to toggle GPS
+ {
+ lv_obj_t *cell = lv_obj_create(comp);
+ lv_obj_remove_style_all(cell);
+ lv_obj_set_size(cell, cw, GUI_COMP_H);
+ lv_obj_set_pos(cell, GUI_PAD + cw, 0);
+ lv_obj_add_flag(cell, LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_clear_flag(cell, (lv_obj_flag_t)(LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SCROLLABLE));
+ lv_obj_add_event_cb(cell, [](lv_event_t *e) {
+ if (gps_ready) { gps_power_off(); gps_ready = false; }
+ else { gps_power_on(); gps_setup(); }
+ }, LV_EVENT_CLICKED, NULL);
+ gui_gps_value = gui_label(cell, &font_mid, GUI_COL_TEAL, "--");
+ lv_obj_set_width(gui_gps_value, cw);
+ lv_obj_set_style_text_align(gui_gps_value, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(gui_gps_value, LV_ALIGN_TOP_MID, 0, 4);
+ gui_gps_label = gui_label(cell, &lv_font_montserrat_14, GUI_COL_DIM, "GPS");
+ lv_obj_set_width(gui_gps_label, cw);
+ lv_obj_set_style_text_align(gui_gps_label, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(gui_gps_label, LV_ALIGN_TOP_MID, 0, 46);
+ }
+
+ // Battery complication — custom with click-to-cycle and icon bar
+ {
+ int bx = GUI_PAD + cw * 2;
+ gui_batt_cell = lv_obj_create(comp);
+ lv_obj_remove_style_all(gui_batt_cell);
+ lv_obj_set_size(gui_batt_cell, cw, GUI_COMP_H);
+ lv_obj_set_pos(gui_batt_cell, bx, 0);
+ lv_obj_add_flag(gui_batt_cell, LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_clear_flag(gui_batt_cell, (lv_obj_flag_t)(LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SCROLLABLE));
+ lv_obj_add_event_cb(gui_batt_cell, [](lv_event_t *e) {
+ gui_batt_mode = (gui_batt_mode + 1) % GUI_BATT_MODES;
+ }, LV_EVENT_CLICKED, NULL);
+
+ gui_batt_value = gui_label(gui_batt_cell, &font_mid, GUI_COL_WHITE, "--");
+ lv_obj_set_width(gui_batt_value, cw);
+ lv_obj_set_style_text_align(gui_batt_value, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(gui_batt_value, LV_ALIGN_TOP_MID, 0, 4);
+
+ gui_batt_detail = gui_label(gui_batt_cell, &lv_font_montserrat_14, GUI_COL_DIM, "Batt");
+ lv_obj_set_width(gui_batt_detail, cw);
+ lv_obj_set_style_text_align(gui_batt_detail, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_align(gui_batt_detail, LV_ALIGN_TOP_MID, 0, 46);
+
+ // Battery icon bar (hidden until mode 3)
+ int bar_w = cw - 20, bar_h = 24;
+ gui_batt_icon = lv_obj_create(gui_batt_cell);
+ lv_obj_remove_style_all(gui_batt_icon);
+ lv_obj_set_size(gui_batt_icon, bar_w, bar_h);
+ lv_obj_align(gui_batt_icon, LV_ALIGN_TOP_MID, 0, 10);
+ lv_obj_set_style_radius(gui_batt_icon, 4, 0);
+ lv_obj_set_style_border_color(gui_batt_icon, lv_color_hex(GUI_COL_MID), 0);
+ lv_obj_set_style_border_width(gui_batt_icon, 2, 0);
+ lv_obj_set_style_bg_opa(gui_batt_icon, LV_OPA_TRANSP, 0);
+ lv_obj_add_flag(gui_batt_icon, LV_OBJ_FLAG_HIDDEN);
+
+ gui_batt_fill = lv_obj_create(gui_batt_icon);
+ lv_obj_remove_style_all(gui_batt_fill);
+ lv_obj_set_size(gui_batt_fill, bar_w - 6, bar_h - 6);
+ lv_obj_set_pos(gui_batt_fill, 2, 2);
+ lv_obj_set_style_radius(gui_batt_fill, 2, 0);
+ lv_obj_set_style_bg_color(gui_batt_fill, lv_color_hex(GUI_COL_GREEN), 0);
+ lv_obj_set_style_bg_opa(gui_batt_fill, LV_OPA_COVER, 0);
+ }
+
+ // Rule 2
+ gui_create_rule(parent, GUI_RULE2_Y);
+
+ // Step counter below complications
+ gui_step_label = gui_label(parent, &lv_font_montserrat_20, GUI_COL_DIM, "");
+ lv_obj_set_width(gui_step_label, GUI_W);
+ lv_obj_set_style_text_align(gui_step_label, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_set_pos(gui_step_label, 0, GUI_RULE2_Y + 15);
+
+ // Bubble level
+ int lx = (GUI_W - GUI_LEVEL_SIZE) / 2;
+
+ // Outer ring
+ gui_level_ring = lv_obj_create(parent);
+ lv_obj_remove_style_all(gui_level_ring);
+ lv_obj_set_size(gui_level_ring, GUI_LEVEL_SIZE, GUI_LEVEL_SIZE);
+ lv_obj_set_pos(gui_level_ring, lx, GUI_LEVEL_Y);
+ lv_obj_set_style_radius(gui_level_ring, LV_RADIUS_CIRCLE, 0);
+ lv_obj_set_style_border_color(gui_level_ring, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_border_width(gui_level_ring, 1, 0);
+ lv_obj_set_style_bg_opa(gui_level_ring, LV_OPA_TRANSP, 0);
+ lv_obj_clear_flag(gui_level_ring, LV_OBJ_FLAG_SCROLLABLE);
+
+ // Crosshairs
+ static lv_point_precise_t ch_pts[] = {{0,0},{GUI_LEVEL_SIZE,0}};
+ gui_level_cross_h = lv_line_create(gui_level_ring);
+ lv_line_set_points(gui_level_cross_h, ch_pts, 2);
+ lv_obj_set_style_line_color(gui_level_cross_h, lv_color_hex(0x202020), 0);
+ lv_obj_set_style_line_width(gui_level_cross_h, 1, 0);
+ lv_obj_center(gui_level_cross_h);
+
+ static lv_point_precise_t cv_pts[] = {{0,0},{0,GUI_LEVEL_SIZE}};
+ gui_level_cross_v = lv_line_create(gui_level_ring);
+ lv_line_set_points(gui_level_cross_v, cv_pts, 2);
+ lv_obj_set_style_line_color(gui_level_cross_v, lv_color_hex(0x202020), 0);
+ lv_obj_set_style_line_width(gui_level_cross_v, 1, 0);
+ lv_obj_center(gui_level_cross_v);
+
+ // Bubble dot — positioned via lv_obj_set_pos() in update loop,
+ // do NOT use lv_obj_center() as it changes the alignment base
+ gui_level_dot = lv_obj_create(gui_level_ring);
+ lv_obj_remove_style_all(gui_level_dot);
+ lv_obj_set_size(gui_level_dot, GUI_LEVEL_DOT, GUI_LEVEL_DOT);
+ lv_obj_set_style_radius(gui_level_dot, LV_RADIUS_CIRCLE, 0);
+ lv_obj_set_style_bg_color(gui_level_dot, lv_color_hex(GUI_COL_GREEN), 0);
+ lv_obj_set_style_bg_opa(gui_level_dot, LV_OPA_COVER, 0);
+ lv_obj_set_pos(gui_level_dot, GUI_LEVEL_SIZE/2 - GUI_LEVEL_DOT/2,
+ GUI_LEVEL_SIZE/2 - GUI_LEVEL_DOT/2);
+
+ // Angle text below ring
+ gui_level_angle = gui_label(parent, &lv_font_montserrat_14, GUI_COL_DIM, "");
+ lv_obj_set_width(gui_level_angle, GUI_W);
+ lv_obj_set_style_text_align(gui_level_angle, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_set_pos(gui_level_angle, 0, GUI_LEVEL_Y + GUI_LEVEL_SIZE + 5);
+
+ // Make bubble level area clickable to toggle signal view
+ lv_obj_add_flag(gui_level_ring, LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_clear_flag(gui_level_ring, (lv_obj_flag_t)(LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SCROLLABLE));
+ lv_obj_add_event_cb(gui_level_ring, [](lv_event_t *e) {
+ gui_show_signal = !gui_show_signal;
+ if (gui_show_signal) {
+ lv_obj_add_flag(gui_level_ring, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(gui_level_angle, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_clear_flag(gui_signal_cont, LV_OBJ_FLAG_HIDDEN);
+ } else {
+ lv_obj_clear_flag(gui_level_ring, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_clear_flag(gui_level_angle, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(gui_signal_cont, LV_OBJ_FLAG_HIDDEN);
+ }
+ }, LV_EVENT_CLICKED, NULL);
+
+ // Signal strength view (hidden by default)
+ gui_signal_cont = lv_obj_create(parent);
+ lv_obj_remove_style_all(gui_signal_cont);
+ lv_obj_set_size(gui_signal_cont, GUI_W - 2 * GUI_PAD, GUI_LEVEL_SIZE + 20);
+ lv_obj_set_pos(gui_signal_cont, GUI_PAD, GUI_LEVEL_Y);
+ lv_obj_clear_flag(gui_signal_cont, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_add_flag(gui_signal_cont, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(gui_signal_cont, LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_clear_flag(gui_signal_cont, (lv_obj_flag_t)(LV_OBJ_FLAG_GESTURE_BUBBLE));
+ lv_obj_add_event_cb(gui_signal_cont, [](lv_event_t *e) {
+ gui_show_signal = false;
+ lv_obj_clear_flag(gui_level_ring, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_clear_flag(gui_level_angle, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(gui_signal_cont, LV_OBJ_FLAG_HIDDEN);
+ }, LV_EVENT_CLICKED, NULL);
+
+ // RSSI sparkline
+ int graph_w = GUI_W - 2 * GUI_PAD;
+ gui_signal_line = lv_line_create(gui_signal_cont);
+ lv_obj_set_style_line_color(gui_signal_line, lv_color_hex(GUI_COL_AMBER), 0);
+ lv_obj_set_style_line_width(gui_signal_line, 2, 0);
+ lv_obj_set_size(gui_signal_line, graph_w, GUI_LEVEL_SIZE - 30);
+ lv_obj_set_pos(gui_signal_line, 0, 0);
+
+ // Graph border
+ lv_obj_t *graph_border = lv_obj_create(gui_signal_cont);
+ lv_obj_remove_style_all(graph_border);
+ lv_obj_set_size(graph_border, graph_w, GUI_LEVEL_SIZE - 30);
+ lv_obj_set_pos(graph_border, 0, 0);
+ lv_obj_set_style_border_color(graph_border, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_border_width(graph_border, 1, 0);
+ lv_obj_set_style_bg_opa(graph_border, LV_OPA_TRANSP, 0);
+ lv_obj_clear_flag(graph_border, LV_OBJ_FLAG_SCROLLABLE);
+
+ // Current RSSI and direction text
+ gui_signal_rssi = gui_label(gui_signal_cont, &font_mid, GUI_COL_AMBER, "---");
+ lv_obj_set_pos(gui_signal_rssi, 0, GUI_LEVEL_SIZE - 25);
+ gui_signal_dir = gui_label(gui_signal_cont, &lv_font_montserrat_14, GUI_COL_TEAL, "");
+ lv_obj_set_pos(gui_signal_dir, graph_w / 2, GUI_LEVEL_SIZE - 20);
+
+ // Init RSSI history
+ memset(rssi_history, 0, sizeof(rssi_history));
+}
+
+// ---------------------------------------------------------------------------
+// Screen: Radio Status (top tile — swipe down from watch face)
+// ---------------------------------------------------------------------------
+static void gui_create_radio_screen(lv_obj_t *parent) {
+ gui_style_black_container(parent);
+
+ gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "RADIO STATUS", GUI_PAD, 12);
+
+ // Frequency
+ gui_radio_freq = gui_label_at(parent, &font_mid, GUI_COL_AMBER, "--- MHz", GUI_PAD, 40);
+
+ // LoRa parameters
+ gui_radio_params = gui_label_at(parent, &font_mid, GUI_COL_MID, "SF- BW- CR-", GUI_PAD, 80);
+
+ // RSSI
+ gui_create_rule(parent, 115);
+ gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "RSSI", GUI_PAD, 125);
+ gui_radio_rssi_lbl = gui_label(parent, &font_mid, GUI_COL_AMBER, "---");
+ lv_obj_align(gui_radio_rssi_lbl, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 122);
+
+ lv_obj_t *bar = lv_bar_create(parent);
+ lv_obj_set_size(bar, GUI_W - GUI_PAD * 2, 20);
+ lv_obj_set_pos(bar, GUI_PAD, 150);
+ lv_bar_set_range(bar, -140, -40);
+ lv_bar_set_value(bar, -140, LV_ANIM_OFF);
+ lv_obj_set_style_bg_color(bar, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_bg_color(bar, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR);
+ lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0);
+ lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR);
+ gui_radio_rssi_bar = bar;
+
+ // Channel utilization
+ gui_create_rule(parent, 185);
+ gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "CHANNEL", GUI_PAD, 198);
+ gui_radio_util = gui_label(parent, &font_mid, GUI_COL_MID, "-- %");
+ lv_obj_align(gui_radio_util, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 195);
+
+ // BLE status
+ gui_create_rule(parent, 230);
+ gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "BLE", GUI_PAD, 243);
+ gui_radio_ble = gui_label(parent, &font_mid, GUI_COL_BLUE, "---");
+ lv_obj_align(gui_radio_ble, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 240);
+
+ // Packet counts
+ gui_create_rule(parent, 275);
+ gui_radio_pkts = gui_label_at(parent, &font_mid, GUI_COL_MID,
+ "RX: 0 TX: 0", GUI_PAD, 290);
+
+ // Battery and temperature
+ gui_create_rule(parent, 325);
+ gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "BATTERY", GUI_PAD, 338);
+ gui_radio_batt = gui_label(parent, &font_mid, GUI_COL_MID, "---");
+ lv_obj_align(gui_radio_batt, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 335);
+
+ gui_create_rule(parent, 370);
+ gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "TEMPERATURE", GUI_PAD, 383);
+ gui_radio_temp = gui_label(parent, &font_mid, GUI_COL_MID, "---");
+ lv_obj_align(gui_radio_temp, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 380);
+}
+
+// ---------------------------------------------------------------------------
+// Screen: GPS (right tile — swipe right from watch face)
+// ---------------------------------------------------------------------------
+static void gui_create_gps_screen(lv_obj_t *parent) {
+ gui_style_black_container(parent);
+
+ lv_obj_t *gps_title = gui_label(parent, &lv_font_montserrat_14, GUI_COL_DIM, "GPS");
+ lv_obj_set_width(gps_title, GUI_W);
+ lv_obj_set_style_text_align(gps_title, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_set_pos(gps_title, 0, 12);
+
+ // Coordinates — centered for rounded corner clearance
+ gui_gps_coords = gui_label(parent, &font_mid, GUI_COL_TEAL, "-- --");
+ lv_obj_set_width(gui_gps_coords, GUI_W);
+ lv_obj_set_style_text_align(gui_gps_coords, LV_TEXT_ALIGN_CENTER, 0);
+ lv_obj_set_pos(gui_gps_coords, 0, 40);
+
+ // Fix quality
+ gui_create_rule(parent, 110);
+ gui_gps_fix = gui_label_at(parent, &font_mid, GUI_COL_MID,
+ "Sats: -- HDOP: --", GUI_PAD, 125);
+
+ // Altitude and speed
+ gui_create_rule(parent, 160);
+ gui_gps_alt = gui_label_at(parent, &font_mid, GUI_COL_MID,
+ "Alt: -- Spd: --", GUI_PAD, 175);
+
+ // Beacon status
+ gui_create_rule(parent, 215);
+ gui_gps_beacon = gui_label_at(parent, &font_mid, GUI_COL_AMBER,
+ "Beacon: --", GUI_PAD, 230);
+}
+
+// ---------------------------------------------------------------------------
+// Screen: Messages (right tile — swipe left from watch face)
+// ---------------------------------------------------------------------------
+static lv_obj_t *gui_msg_title = NULL;
+static lv_obj_t *gui_msg_empty = NULL;
+static lv_obj_t *gui_msg_cont = NULL;
+#define GUI_MSG_ROWS 8 // max visible message rows
+static lv_obj_t *gui_msg_name[GUI_MSG_ROWS];
+static lv_obj_t *gui_msg_rssi[GUI_MSG_ROWS];
+static lv_obj_t *gui_msg_time[GUI_MSG_ROWS];
+static lv_obj_t *gui_msg_row[GUI_MSG_ROWS];
+static uint32_t gui_msg_last_rebuild = 0;
+
+// Message log types and forward declarations (defined in MessageLog.h)
+#ifndef MSG_LOG_SIZE
+#define MSG_LOG_SIZE 16
+#define MSG_NAME_LEN 24
+#endif
+struct msg_entry_t {
+ uint32_t timestamp;
+ int16_t rssi;
+ int8_t snr;
+ uint8_t pkt_type;
+ uint8_t sender_hash[16];
+ char display_name[MSG_NAME_LEN];
+ char content[64]; // LXMF message content (first 63 chars)
+ uint8_t sender_ed25519_pub[32]; // cached from announce for signature verification
+ uint16_t pkt_len;
+ bool is_announce;
+ bool is_lxmf; // true if decrypted LXMF message
+ bool verified; // signature verified
+ bool has_pubkey; // sender_ed25519_pub is valid
+};
+extern msg_entry_t msg_log[];
+extern uint8_t msg_log_head;
+extern uint8_t msg_log_count;
+extern uint32_t msg_log_last_change;
+
+static void gui_create_msg_screen(lv_obj_t *parent) {
+ gui_style_black_container(parent);
+ gui_msg_title = gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "MESSAGES", GUI_PAD, 12);
+
+ gui_msg_empty = gui_label(parent, &lv_font_montserrat_14, GUI_COL_DIM, "Listening...");
+ lv_obj_align(gui_msg_empty, LV_ALIGN_CENTER, 0, 0);
+
+ // Scrollable message list container
+ gui_msg_cont = lv_obj_create(parent);
+ lv_obj_remove_style_all(gui_msg_cont);
+ lv_obj_set_size(gui_msg_cont, GUI_W, GUI_H - 40);
+ lv_obj_set_pos(gui_msg_cont, 0, 35);
+ lv_obj_set_style_bg_opa(gui_msg_cont, LV_OPA_TRANSP, 0);
+ lv_obj_clear_flag(gui_msg_cont, LV_OBJ_FLAG_SCROLLABLE);
+
+ // Pre-create row widgets
+ for (int i = 0; i < GUI_MSG_ROWS; i++) {
+ int y = i * 55;
+ gui_msg_row[i] = lv_obj_create(gui_msg_cont);
+ lv_obj_remove_style_all(gui_msg_row[i]);
+ lv_obj_set_size(gui_msg_row[i], GUI_W - 2 * GUI_PAD, 50);
+ lv_obj_set_pos(gui_msg_row[i], GUI_PAD, y);
+ lv_obj_add_flag(gui_msg_row[i], LV_OBJ_FLAG_HIDDEN);
+
+ // Display name (left)
+ gui_msg_name[i] = lv_label_create(gui_msg_row[i]);
+ lv_obj_set_style_text_font(gui_msg_name[i], &lv_font_montserrat_14, 0);
+ lv_obj_set_style_text_color(gui_msg_name[i], lv_color_hex(GUI_COL_WHITE), 0);
+ lv_obj_set_pos(gui_msg_name[i], 0, 0);
+ lv_obj_set_width(gui_msg_name[i], GUI_W - 2 * GUI_PAD - 70);
+ lv_label_set_long_mode(gui_msg_name[i], LV_LABEL_LONG_CLIP);
+
+ // RSSI (right)
+ gui_msg_rssi[i] = lv_label_create(gui_msg_row[i]);
+ lv_obj_set_style_text_font(gui_msg_rssi[i], &lv_font_montserrat_14, 0);
+ lv_obj_set_style_text_color(gui_msg_rssi[i], lv_color_hex(GUI_COL_AMBER), 0);
+ lv_obj_set_pos(gui_msg_rssi[i], GUI_W - 2 * GUI_PAD - 65, 0);
+ lv_obj_set_width(gui_msg_rssi[i], 65);
+ lv_obj_set_style_text_align(gui_msg_rssi[i], LV_TEXT_ALIGN_RIGHT, 0);
+
+ // Time ago (below name)
+ gui_msg_time[i] = lv_label_create(gui_msg_row[i]);
+ lv_obj_set_style_text_font(gui_msg_time[i], &lv_font_montserrat_10, 0);
+ lv_obj_set_style_text_color(gui_msg_time[i], lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_pos(gui_msg_time[i], 0, 20);
+
+ // Divider line at bottom of row
+ if (i > 0) {
+ lv_obj_t *div = lv_obj_create(gui_msg_row[i]);
+ lv_obj_remove_style_all(div);
+ lv_obj_set_size(div, GUI_W - 2 * GUI_PAD, 1);
+ lv_obj_set_pos(div, 0, -3);
+ lv_obj_set_style_bg_color(div, lv_color_hex(0x1A1A1A), 0);
+ lv_obj_set_style_bg_opa(div, LV_OPA_COVER, 0);
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Screen: Settings (bottom tile — swipe up from watch face)
+// ---------------------------------------------------------------------------
+static lv_obj_t *gui_set_disp_slider = NULL;
+static lv_obj_t *gui_set_disp_val = NULL;
+static lv_obj_t *gui_set_bcn_roller = NULL;
+static lv_obj_t *gui_set_gps_roller = NULL;
+static lv_obj_t *gui_set_bcn_sw = NULL;
+static lv_obj_t *gui_set_log_sw = NULL;
+static lv_obj_t *gui_set_log_status = NULL;
+
+static void gui_set_disp_cb(lv_event_t *e) {
+ lv_obj_t *slider = (lv_obj_t *)lv_event_get_target(e);
+ int32_t val = lv_slider_get_value(slider);
+ display_blanking_timeout = (uint32_t)val * 1000;
+ char buf[8]; snprintf(buf, sizeof(buf), "%lds", (long)val);
+ lv_label_set_text(gui_set_disp_val, buf);
+ EEPROM.write(config_addr(ADDR_CONF_DISP_TIMEOUT), (uint8_t)val);
+ EEPROM.commit();
+}
+
+static void gui_set_bcn_en_cb(lv_event_t *e) {
+ lv_obj_t *sw = (lv_obj_t *)lv_event_get_target(e);
+ beacon_enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
+ EEPROM.write(config_addr(ADDR_CONF_BCN_EN), beacon_enabled ? 1 : 0);
+ EEPROM.commit();
+}
+
+static void gui_set_bcn_int_cb(lv_event_t *e) {
+ lv_obj_t *roller = (lv_obj_t *)lv_event_get_target(e);
+ uint16_t idx = lv_roller_get_selected(roller);
+ if (idx < BEACON_INTERVAL_OPTIONS_COUNT) {
+ beacon_interval_ms = beacon_interval_options[idx];
+ EEPROM.write(config_addr(ADDR_CONF_BCN_INT), (uint8_t)idx);
+ EEPROM.commit();
+ }
+}
+
+static void gui_set_gps_model_cb(lv_event_t *e) {
+ lv_obj_t *roller = (lv_obj_t *)lv_event_get_target(e);
+ uint16_t idx = lv_roller_get_selected(roller);
+ if (idx < GPS_MODEL_OPTIONS_COUNT) {
+ gps_set_dynamic_model(idx);
+ EEPROM.write(config_addr(ADDR_CONF_GPS_MODEL), (uint8_t)idx);
+ EEPROM.commit();
+ }
+}
+
+static void gui_set_log_cb(lv_event_t *e) {
+ bool on = lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED);
+ if (on) {
+ if (gui_log_toggle_fn) gui_log_toggle_fn();
+ } else {
+ if (gui_log_toggle_fn && imu_logging) gui_log_toggle_fn();
+ }
+}
+
+static void gui_create_settings_screen(lv_obj_t *parent) {
+ gui_style_black_container(parent);
+
+ // Child container for settings content — do NOT set flex on the tile itself
+ lv_obj_t *cont = lv_obj_create(parent);
+ lv_obj_remove_style_all(cont);
+ lv_obj_set_size(cont, GUI_W, GUI_H);
+ lv_obj_set_style_bg_color(cont, lv_color_hex(GUI_COL_BLACK), 0);
+ lv_obj_set_style_bg_opa(cont, LV_OPA_COVER, 0);
+ lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
+
+ // Title
+ gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_DIM, "SETTINGS", GUI_PAD, 12);
+
+ // --- Row 1: Display timeout (y=50) ---
+ gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Display timeout", GUI_PAD, 55);
+ gui_set_disp_slider = lv_slider_create(cont);
+ lv_obj_set_size(gui_set_disp_slider, 180, 12);
+ lv_obj_set_pos(gui_set_disp_slider, GUI_PAD, 80);
+ lv_slider_set_range(gui_set_disp_slider, 5, 60);
+ lv_slider_set_value(gui_set_disp_slider, (int32_t)(display_blanking_timeout / 1000), LV_ANIM_OFF);
+ lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR);
+ lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_WHITE), LV_PART_KNOB);
+ lv_obj_set_style_pad_all(gui_set_disp_slider, 4, LV_PART_KNOB);
+ lv_obj_add_event_cb(gui_set_disp_slider, gui_set_disp_cb, LV_EVENT_VALUE_CHANGED, NULL);
+ char disp_buf[8]; snprintf(disp_buf, sizeof(disp_buf), "%lds", (long)(display_blanking_timeout / 1000));
+ gui_set_disp_val = gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_WHITE, disp_buf, GUI_PAD + 200, 75);
+
+ gui_create_rule(cont, 110);
+
+ // --- Row 2: Beacon enable (y=120) ---
+ gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Beacon", GUI_PAD, 125);
+ gui_set_bcn_sw = lv_switch_create(cont);
+ lv_obj_set_pos(gui_set_bcn_sw, GUI_W - GUI_PAD - 50, 120);
+ lv_obj_set_size(gui_set_bcn_sw, 50, 26);
+ if (beacon_enabled) lv_obj_add_state(gui_set_bcn_sw, LV_STATE_CHECKED);
+ lv_obj_set_style_bg_color(gui_set_bcn_sw, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_bg_color(gui_set_bcn_sw, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR | LV_STATE_CHECKED);
+ lv_obj_add_event_cb(gui_set_bcn_sw, gui_set_bcn_en_cb, LV_EVENT_VALUE_CHANGED, NULL);
+
+ gui_create_rule(cont, 160);
+
+ // --- Row 3: Beacon interval (y=170) ---
+ gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Beacon interval", GUI_PAD, 175);
+ gui_set_bcn_roller = lv_roller_create(cont);
+ lv_roller_set_options(gui_set_bcn_roller, "10s\n30s\n1min\n5min\n10min", LV_ROLLER_MODE_NORMAL);
+ lv_obj_set_pos(gui_set_bcn_roller, GUI_W - GUI_PAD - 100, 170);
+ lv_obj_set_size(gui_set_bcn_roller, 100, 60);
+ lv_obj_set_style_bg_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_BLACK), 0);
+ lv_obj_set_style_text_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_WHITE), 0);
+ lv_obj_set_style_text_font(gui_set_bcn_roller, &lv_font_montserrat_14, 0);
+ lv_obj_set_style_bg_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_AMBER), LV_PART_SELECTED);
+ lv_obj_set_style_text_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_BLACK), LV_PART_SELECTED);
+ // Set initial selection from current beacon_interval_ms
+ for (uint8_t i = 0; i < BEACON_INTERVAL_OPTIONS_COUNT; i++) {
+ if (beacon_interval_options[i] == beacon_interval_ms) {
+ lv_roller_set_selected(gui_set_bcn_roller, i, LV_ANIM_OFF);
+ break;
+ }
+ }
+ lv_obj_add_event_cb(gui_set_bcn_roller, gui_set_bcn_int_cb, LV_EVENT_VALUE_CHANGED, NULL);
+
+ gui_create_rule(cont, 245);
+
+ // --- Row 4: GPS dynamic model (y=255) ---
+ gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "GPS model", GUI_PAD, 260);
+ gui_set_gps_roller = lv_roller_create(cont);
+ lv_roller_set_options(gui_set_gps_roller, "Portable\nStationary\nPedestrian\nAutomotive", LV_ROLLER_MODE_NORMAL);
+ lv_obj_set_pos(gui_set_gps_roller, GUI_W - GUI_PAD - 140, 255);
+ lv_obj_set_size(gui_set_gps_roller, 140, 60);
+ lv_obj_set_style_bg_color(gui_set_gps_roller, lv_color_hex(GUI_COL_BLACK), 0);
+ lv_obj_set_style_text_color(gui_set_gps_roller, lv_color_hex(GUI_COL_WHITE), 0);
+ lv_obj_set_style_text_font(gui_set_gps_roller, &lv_font_montserrat_14, 0);
+ lv_obj_set_style_bg_color(gui_set_gps_roller, lv_color_hex(GUI_COL_AMBER), LV_PART_SELECTED);
+ lv_obj_set_style_text_color(gui_set_gps_roller, lv_color_hex(GUI_COL_BLACK), LV_PART_SELECTED);
+ lv_roller_set_selected(gui_set_gps_roller, gps_dynamic_model, LV_ANIM_OFF);
+ lv_obj_add_event_cb(gui_set_gps_roller, gui_set_gps_model_cb, LV_EVENT_VALUE_CHANGED, NULL);
+
+ // --- Sensor logger ---
+ gui_create_rule(cont, 320);
+ gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Data logger", GUI_PAD, 335);
+ gui_set_log_sw = lv_switch_create(cont);
+ lv_obj_set_pos(gui_set_log_sw, GUI_W - GUI_PAD - 50, 330);
+ lv_obj_set_size(gui_set_log_sw, 50, 26);
+ lv_obj_set_style_bg_color(gui_set_log_sw, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_bg_color(gui_set_log_sw, lv_color_hex(GUI_COL_GREEN), LV_PART_INDICATOR | LV_STATE_CHECKED);
+ lv_obj_add_event_cb(gui_set_log_sw, gui_set_log_cb, LV_EVENT_VALUE_CHANGED, NULL);
+ gui_set_log_status = gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_DIM, "", GUI_PAD, 365);
+}
+
+// ---------------------------------------------------------------------------
+// Tileview change event — haptic feedback
+// ---------------------------------------------------------------------------
+static void gui_tile_change_cb(lv_event_t *e) {
+ lv_obj_t *tv = (lv_obj_t *)lv_event_get_target(e);
+ lv_obj_t *tile = (lv_obj_t *)lv_tileview_get_tile_active(tv);
+ if (!tile) return;
+
+ // Get tile position
+ lv_coord_t col = lv_obj_get_x(tile) / GUI_W;
+ lv_coord_t row = lv_obj_get_y(tile) / GUI_H;
+
+ if (col != gui_last_tile_col || row != gui_last_tile_row) {
+ gui_last_tile_col = col;
+ gui_last_tile_row = row;
+ #if defined(DRV2605_H)
+ if (drv2605_ready) drv2605_play(HAPTIC_TRANSITION);
+ #endif
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Update all screen data from firmware globals
+// ---------------------------------------------------------------------------
+static const char *gui_month_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN",
+ "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
+
+static bool gui_is_scrolling() {
+ if (!gui_tileview) return false;
+ lv_obj_t *tile = (lv_obj_t *)lv_tileview_get_tile_active(gui_tileview);
+ if (!tile) return false;
+ // Check if scroll position doesn't match tile alignment
+ lv_coord_t sx = lv_obj_get_scroll_x(gui_tileview);
+ lv_coord_t sy = lv_obj_get_scroll_y(gui_tileview);
+ lv_coord_t tx = lv_obj_get_x(tile);
+ lv_coord_t ty = lv_obj_get_y(tile);
+ return (sx != tx || sy != ty);
+}
+
+static void gui_update_data() {
+ if (!gui_time_label) return;
+
+ // Skip all GUI updates when display is blanked — no point rendering
+ // to a sleeping display. Saves CPU, SPI writes, and display power.
+ if (display_blanked) return;
+
+ // Skip data updates during scroll animation — frees CPU for rendering
+ if (gui_is_scrolling()) return;
+
+ uint32_t now = millis();
+ if (now - gui_last_data_update < GUI_DATA_UPDATE_MS) return;
+ gui_last_data_update = now;
+
+ // Detect current tile — only update visible screen's labels
+ lv_obj_t *cur_tile = (lv_obj_t *)lv_tileview_get_tile_active(gui_tileview);
+ bool on_watch = (cur_tile == gui_tile_watch);
+ bool on_radio = (cur_tile == gui_tile_radio);
+ bool on_gps = (cur_tile == gui_tile_gps);
+ bool on_settings = (cur_tile == gui_tile_set);
+ bool on_msg = (cur_tile == gui_tile_msg);
+
+ // ---- Watch face ----
+ // Time always updates (cheap, changes rarely)
+ lv_label_set_text_fmt(gui_time_label, "%02d:%02d", rtc_hour, rtc_minute);
+
+ if (!on_watch && !on_radio && !on_gps && !on_settings && !on_msg) return;
+
+ // ---- Watch face details (only when visible) ----
+ if (on_watch) {
+
+ #if HAS_RTC == true
+ if (rtc_year > 0) {
+ const char *mon = (rtc_month >= 1 && rtc_month <= 12) ? gui_month_names[rtc_month - 1] : "---";
+ lv_label_set_text_fmt(gui_date_label, "%d %s %d", rtc_day, mon, rtc_year);
+ }
+ #endif
+
+ // Mode indicator
+ if (bt_state == BT_STATE_CONNECTED) {
+ lv_label_set_text(gui_mode_label, "MODEM");
+ lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_BLUE), 0);
+ }
+ #if HAS_GPS == true
+ else if (beacon_mode_active) {
+ lv_label_set_text(gui_mode_label, "BEACON");
+ lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_AMBER), 0);
+ }
+ #endif
+ else if (radio_online) {
+ lv_label_set_text(gui_mode_label, "RADIO");
+ lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_MID), 0);
+ } else {
+ lv_label_set_text(gui_mode_label, "IDLE");
+ lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_DIM), 0);
+ }
+
+ // Battery
+ if (battery_state == BATTERY_STATE_CHARGING) {
+ lv_label_set_text_fmt(gui_batt_label, "%d%% +", (int)battery_percent);
+ } else {
+ lv_label_set_text_fmt(gui_batt_label, "%d%%", (int)battery_percent);
+ }
+ lv_obj_align(gui_batt_label, LV_ALIGN_TOP_RIGHT, -GUI_PAD, GUI_STATUS_Y);
+
+ // LoRa complication — show RSSI if receiving, TX count if beaconing, noise floor otherwise
+ if (radio_online) {
+ if (last_rssi > -292) {
+ // Received a packet — show RSSI
+ lv_label_set_text_fmt(gui_lora_value, "%d", last_rssi);
+ lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_AMBER), 0);
+ } else if (stat_tx > 0) {
+ // Beacon mode — show TX count
+ lv_label_set_text_fmt(gui_lora_value, "TX:%lu", stat_tx);
+ lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_AMBER), 0);
+ } else if (noise_floor > -292) {
+ // Radio listening — show noise floor
+ lv_label_set_text_fmt(gui_lora_value, "%d", noise_floor);
+ lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_DIM), 0);
+ } else {
+ lv_label_set_text(gui_lora_value, "---");
+ lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_DIM), 0);
+ }
+ lv_label_set_text(gui_lora_label, ifac_configured ? "LoRa" : "NO KEY");
+ lv_obj_set_style_text_color(gui_lora_label,
+ lv_color_hex(ifac_configured ? GUI_COL_DIM : GUI_COL_RED), 0);
+ } else {
+ lv_label_set_text(gui_lora_value, "OFF");
+ lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(0x302000), 0);
+ lv_label_set_text(gui_lora_label, ifac_configured ? "LoRa" : "NO KEY");
+ lv_obj_set_style_text_color(gui_lora_label,
+ lv_color_hex(ifac_configured ? 0x302000 : GUI_COL_RED), 0);
+ }
+
+ // GPS complication — dim when disabled, color by fix quality
+ #if HAS_GPS == true
+ if (!gps_ready) {
+ lv_label_set_text(gui_gps_value, "OFF");
+ lv_obj_set_style_text_color(gui_gps_value, lv_color_hex(0x003020), 0);
+ lv_obj_set_style_text_color(gui_gps_label, lv_color_hex(0x003020), 0);
+ } else if (gps_sats > 0) {
+ lv_label_set_text_fmt(gui_gps_value, "%d sats", gps_sats);
+ uint32_t gps_col = (gps_hdop < 5.0) ? GUI_COL_TEAL :
+ (gps_hdop < 15.0) ? GUI_COL_AMBER : GUI_COL_MID;
+ lv_obj_set_style_text_color(gui_gps_value, lv_color_hex(gps_col), 0);
+ lv_obj_set_style_text_color(gui_gps_label, lv_color_hex(GUI_COL_DIM), 0);
+ } else {
+ lv_label_set_text(gui_gps_value, "no fix");
+ lv_obj_set_style_text_color(gui_gps_value, lv_color_hex(GUI_COL_DIM), 0);
+ lv_obj_set_style_text_color(gui_gps_label, lv_color_hex(GUI_COL_DIM), 0);
+ }
+ #endif
+
+ // Step counter
+ if (gui_step_label) {
+ if (imu_step_count > 0) {
+ lv_label_set_text_fmt(gui_step_label, "%lu steps", imu_step_count);
+ lv_obj_set_style_text_color(gui_step_label, lv_color_hex(GUI_COL_MID), 0);
+ } else {
+ lv_label_set_text(gui_step_label, "");
+ }
+ }
+
+ // Bubble level — spring-damper physics with non-linear sensitivity
+ if (gui_level_dot && imu_az_f != 0) {
+ static float bub_x = 0, bub_y = 0; // current bubble position (pixels)
+ static float vel_x = 0, vel_y = 0; // bubble velocity
+ static uint32_t bub_t = 0; // last update time
+
+ uint32_t now_ms = millis();
+ float dt = (bub_t > 0) ? (now_ms - bub_t) / 1000.0f : 0.016f;
+ if (dt > 0.1f) dt = 0.1f; // clamp for first frame / pauses
+ bub_t = now_ms;
+
+ float max_r = (GUI_LEVEL_SIZE - GUI_LEVEL_DOT) / 2.0f;
+
+ // Tilt vector from accelerometer (radians)
+ float tilt_x = atan2f(imu_ax_f, imu_az_f);
+ float tilt_y = atan2f(imu_ay_f, imu_az_f);
+
+ // Work in polar: magnitude + direction
+ float tilt_r = sqrtf(tilt_x * tilt_x + tilt_y * tilt_y);
+ float tilt_dir_x = (tilt_r > 0.001f) ? tilt_x / tilt_r : 0;
+ float tilt_dir_y = (tilt_r > 0.001f) ? tilt_y / tilt_r : 0;
+
+ // Non-linear radial mapping: tanh compresses large tilts,
+ // amplifies small ones. k=3 → full ring at ~20° tilt
+ float mapped_r = tanhf(tilt_r * 3.0f) * max_r;
+
+ // Project back to cartesian (invert y for natural bubble feel, x matches)
+ float target_x = tilt_dir_x * mapped_r;
+ float target_y = -tilt_dir_y * mapped_r;
+
+ // Adaptive spring-damper: snappy when still, viscous when noisy
+ // noise < 0.001 (desk): spring=80 damp=16 → settles in ~0.15s
+ // noise > 0.01 (hand): spring=30 damp=14 → settles in ~0.4s
+ float noise_t = imu_noise < 0.001f ? 0.0f :
+ imu_noise > 0.01f ? 1.0f :
+ (imu_noise - 0.001f) / 0.009f;
+ float spring = 80.0f - 50.0f * noise_t;
+ float damping = 16.0f - 2.0f * noise_t;
+ const float max_sub = 0.02f;
+ int steps = (int)(dt / max_sub) + 1;
+ float sdt = dt / steps;
+ for (int si = 0; si < steps; si++) {
+ vel_x += (spring * (target_x - bub_x) - damping * vel_x) * sdt;
+ vel_y += (spring * (target_y - bub_y) - damping * vel_y) * sdt;
+ bub_x += vel_x * sdt;
+ bub_y += vel_y * sdt;
+ }
+
+ // Clamp to ring boundary (bubble can't escape the fluid)
+ float dist = sqrtf(bub_x * bub_x + bub_y * bub_y);
+ if (dist > max_r) {
+ bub_x = bub_x * max_r / dist;
+ bub_y = bub_y * max_r / dist;
+ // Kill velocity component along the wall
+ float nx = bub_x / dist, ny = bub_y / dist;
+ float vdot = vel_x * nx + vel_y * ny;
+ if (vdot > 0) { vel_x -= vdot * nx; vel_y -= vdot * ny; }
+ }
+
+ lv_obj_set_pos(gui_level_dot,
+ (int)(GUI_LEVEL_SIZE / 2 - GUI_LEVEL_DOT / 2 + bub_x),
+ (int)(GUI_LEVEL_SIZE / 2 - GUI_LEVEL_DOT / 2 + bub_y));
+
+ // Tilt angle for display and colour
+ float tx = imu_ax_f / 4096.0f, ty = imu_ay_f / 4096.0f;
+ float tilt_deg = atan2f(sqrtf(tx*tx + ty*ty), fabsf(imu_az_f / 4096.0f)) * 57.2958f;
+ uint32_t dot_col = (tilt_deg < 2.0f) ? GUI_COL_GREEN :
+ (tilt_deg < 10.0f) ? GUI_COL_AMBER : GUI_COL_RED;
+ lv_obj_set_style_bg_color(gui_level_dot, lv_color_hex(dot_col), 0);
+
+ lv_label_set_text_fmt(gui_level_angle, "%.1f\xC2\xB0", tilt_deg);
+ lv_obj_set_style_text_color(gui_level_angle, lv_color_hex(dot_col), 0);
+ }
+
+ // Signal strength view — record RSSI and update graph
+ if (gui_signal_cont) {
+ // Record signal level periodically (RSSI if available, noise floor otherwise)
+ static int16_t prev_rssi = -292;
+ static uint32_t last_rssi_record = 0;
+ int16_t current_signal = (last_rssi > -292) ? last_rssi : noise_floor;
+ if (radio_online && millis() - last_rssi_record > 5000 && current_signal > -292) {
+ rssi_history[rssi_history_idx] = current_signal;
+ rssi_history_idx = (rssi_history_idx + 1) % RSSI_HISTORY_LEN;
+ if (rssi_history_count < RSSI_HISTORY_LEN) rssi_history_count++;
+
+ // Record GPS+RSSI for direction finding (only with real RSSI, not noise floor)
+ #if HAS_GPS == true
+ if (gps_has_fix && last_rssi > -292) {
+ dir_history[dir_history_idx] = { gps_lat, gps_lon, last_rssi };
+ dir_history_idx = (dir_history_idx + 1) % DIR_HISTORY_LEN;
+ if (dir_history_count < DIR_HISTORY_LEN) dir_history_count++;
+ }
+ #endif
+
+ last_rssi_record = millis();
+ }
+
+ // Update signal view when visible
+ if (gui_show_signal && rssi_history_count > 0) {
+ // Current signal display
+ if (last_rssi > -292 && radio_online) {
+ lv_label_set_text_fmt(gui_signal_rssi, "%d dBm", last_rssi);
+ uint32_t rssi_col = (last_rssi > -80) ? GUI_COL_GREEN :
+ (last_rssi > -100) ? GUI_COL_AMBER : GUI_COL_RED;
+ lv_obj_set_style_text_color(gui_signal_rssi, lv_color_hex(rssi_col), 0);
+ } else if (noise_floor > -292 && radio_online) {
+ lv_label_set_text_fmt(gui_signal_rssi, "floor %d", noise_floor);
+ lv_obj_set_style_text_color(gui_signal_rssi, lv_color_hex(GUI_COL_DIM), 0);
+ } else {
+ lv_label_set_text(gui_signal_rssi, "no signal");
+ lv_obj_set_style_text_color(gui_signal_rssi, lv_color_hex(GUI_COL_DIM), 0);
+ }
+
+ // Build sparkline points
+ int graph_w = GUI_W - 2 * GUI_PAD;
+ int graph_h = GUI_LEVEL_SIZE - 30;
+ // RSSI range: -130 to -30 dBm mapped to graph height
+ int n = rssi_history_count;
+ for (int i = 0; i < n; i++) {
+ int idx = (rssi_history_idx - n + i + RSSI_HISTORY_LEN) % RSSI_HISTORY_LEN;
+ float x = (float)i / (RSSI_HISTORY_LEN - 1) * graph_w;
+ float norm = (float)(rssi_history[idx] + 130) / 100.0f; // -130→0, -30→1
+ if (norm < 0) norm = 0; if (norm > 1) norm = 1;
+ float y = graph_h * (1.0f - norm);
+ rssi_graph_pts[i] = { (lv_value_precise_t)x, (lv_value_precise_t)y };
+ }
+ lv_line_set_points(gui_signal_line, rssi_graph_pts, n);
+
+ // Direction estimation from GPS+RSSI gradient
+ #if HAS_GPS == true
+ if (dir_history_count >= 3 && gps_has_fix) {
+ // Weighted centroid: stronger signal → weight toward that position
+ // Direction = from current position toward weighted centroid
+ double wlat = 0, wlon = 0, wsum = 0;
+ for (int i = 0; i < dir_history_count; i++) {
+ // Weight: convert RSSI to linear power (higher = closer to source)
+ double w = pow(10.0, (double)dir_history[i].rssi / 20.0);
+ wlat += dir_history[i].lat * w;
+ wlon += dir_history[i].lon * w;
+ wsum += w;
+ }
+ if (wsum > 0) {
+ wlat /= wsum; wlon /= wsum;
+ double dlat = wlat - gps_lat;
+ double dlon = (wlon - gps_lon) * cos(gps_lat * 0.01745329);
+ double bearing = atan2(dlon, dlat) * 57.2958;
+ if (bearing < 0) bearing += 360;
+ const char *dirs[] = {"N","NE","E","SE","S","SW","W","NW"};
+ int di = ((int)(bearing + 22.5) / 45) % 8;
+ lv_label_set_text_fmt(gui_signal_dir, "%s %.0f\xC2\xB0", dirs[di], bearing);
+ lv_obj_set_style_text_color(gui_signal_dir, lv_color_hex(GUI_COL_TEAL), 0);
+ }
+ } else {
+ lv_label_set_text(gui_signal_dir, "");
+ }
+ #endif
+ }
+ }
+
+ // Battery time estimation — sample every 60s
+ {
+ uint32_t bnow = millis();
+ if (battery_percent > 0 && (bnow - batt_hist_last > 60000 || batt_hist_last == 0)) {
+ batt_hist_last = bnow;
+ // Shift history
+ for (int i = 3; i > 0; i--) batt_pct_history[i] = batt_pct_history[i-1];
+ batt_pct_history[0] = battery_percent;
+ // Compute rate from oldest valid to newest (up to 4 min window)
+ int oldest = -1;
+ for (int i = 3; i >= 1; i--) { if (batt_pct_history[i] >= 0) { oldest = i; break; } }
+ if (oldest > 0) {
+ float delta_pct = batt_pct_history[0] - batt_pct_history[oldest];
+ float delta_hr = (oldest * 60.0f) / 3600.0f;
+ batt_rate_pct_hr = delta_pct / delta_hr;
+ }
+ }
+ }
+
+ // Battery complication — tap cycles through modes
+ {
+ bool charging = (battery_state == BATTERY_STATE_CHARGING);
+ bool charged = (battery_state == BATTERY_STATE_CHARGED);
+ uint32_t col = charged || charging ? GUI_COL_GREEN :
+ battery_percent < 15 ? GUI_COL_RED : GUI_COL_WHITE;
+ bool show_icon = (gui_batt_mode == 3);
+
+ // Show/hide text vs icon
+ if (show_icon) {
+ lv_obj_add_flag(gui_batt_value, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_clear_flag(gui_batt_icon, LV_OBJ_FLAG_HIDDEN);
+ } else {
+ lv_obj_clear_flag(gui_batt_value, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(gui_batt_icon, LV_OBJ_FLAG_HIDDEN);
+ }
+
+ if (gui_batt_mode == 0) {
+ // Voltage
+ lv_label_set_text_fmt(gui_batt_value, "%.2fV", battery_voltage);
+ lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(col), 0);
+ lv_label_set_text(gui_batt_detail, charging ? "Charging" : charged ? "Full" : "Batt");
+ } else if (gui_batt_mode == 1) {
+ // Percentage
+ lv_label_set_text_fmt(gui_batt_value, "%d%%", (int)battery_percent);
+ lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(col), 0);
+ lv_label_set_text(gui_batt_detail, charging ? "Charging" : "Battery");
+ } else if (gui_batt_mode == 2) {
+ // Time remaining
+ if (charged) {
+ lv_label_set_text(gui_batt_value, "Full");
+ lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(GUI_COL_GREEN), 0);
+ } else if (batt_rate_pct_hr < -0.5f) {
+ // Discharging: time to 0%
+ float hrs = -battery_percent / batt_rate_pct_hr;
+ if (hrs > 99) lv_label_set_text(gui_batt_value, "99h+");
+ else if (hrs >= 1) lv_label_set_text_fmt(gui_batt_value, "%.0fh%02d", floorf(hrs), (int)((hrs - floorf(hrs))*60));
+ else lv_label_set_text_fmt(gui_batt_value, "%dm", (int)(hrs * 60));
+ lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(col), 0);
+ } else if (batt_rate_pct_hr > 0.5f && charging) {
+ // Charging: time to 100%
+ float hrs = (100.0f - battery_percent) / batt_rate_pct_hr;
+ if (hrs >= 1) lv_label_set_text_fmt(gui_batt_value, "%.0fh%02d", floorf(hrs), (int)((hrs - floorf(hrs))*60));
+ else lv_label_set_text_fmt(gui_batt_value, "%dm", (int)(hrs * 60));
+ lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(GUI_COL_GREEN), 0);
+ } else {
+ lv_label_set_text(gui_batt_value, "---");
+ lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(GUI_COL_DIM), 0);
+ }
+ lv_label_set_text(gui_batt_detail, charging ? "to full" : "remain");
+ } else {
+ // Icon mode — animated bar
+ int bar_inner_w = lv_obj_get_width(gui_batt_icon) - 6;
+ int fill_w = (int)(bar_inner_w * battery_percent / 100.0f);
+ if (fill_w < 2) fill_w = 2;
+
+ // Charging animation: pulse fill width
+ if (charging) {
+ static uint32_t anim_t = 0;
+ float phase = (float)((millis() - anim_t) % 2000) / 2000.0f;
+ float pulse = (sinf(phase * 6.2832f) + 1.0f) / 2.0f;
+ fill_w = (int)(fill_w + (bar_inner_w - fill_w) * pulse * 0.3f);
+ }
+
+ lv_obj_set_width(gui_batt_fill, fill_w);
+ lv_obj_set_style_bg_color(gui_batt_fill, lv_color_hex(col), 0);
+ lv_obj_set_style_border_color(gui_batt_icon, lv_color_hex(col), 0);
+ lv_label_set_text_fmt(gui_batt_detail, "%d%%", (int)battery_percent);
+ }
+ lv_obj_set_style_text_color(gui_batt_detail, lv_color_hex(charging || charged ? GUI_COL_GREEN : GUI_COL_DIM), 0);
+ }
+
+ } // end on_watch
+
+ // ---- Radio status screen (only when visible) ----
+ if (on_radio && gui_radio_freq) {
+ if (lora_freq > 0) {
+ lv_label_set_text_fmt(gui_radio_freq, "%.3f MHz", (float)lora_freq / 1000000.0);
+ } else {
+ lv_label_set_text(gui_radio_freq, "--- MHz");
+ }
+
+ lv_label_set_text_fmt(gui_radio_params, "SF%d BW%lu CR4/%d",
+ lora_sf, lora_bw / 1000, lora_cr);
+
+ if (radio_online && last_rssi > -292) {
+ lv_bar_set_value(gui_radio_rssi_bar, last_rssi, LV_ANIM_ON);
+ lv_label_set_text_fmt(gui_radio_rssi_lbl, "%d dBm", last_rssi);
+ } else {
+ lv_bar_set_value(gui_radio_rssi_bar, -140, LV_ANIM_OFF);
+ lv_label_set_text(gui_radio_rssi_lbl, "---");
+ }
+
+ lv_label_set_text_fmt(gui_radio_util, "%.1f%%",
+ (float)local_channel_util / 100.0);
+
+ if (bt_state == BT_STATE_CONNECTED) {
+ lv_label_set_text(gui_radio_ble, "Connected");
+ lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_BLUE), 0);
+ } else if (bt_state == BT_STATE_ON) {
+ lv_label_set_text(gui_radio_ble, "Advertising");
+ lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_MID), 0);
+ } else {
+ lv_label_set_text(gui_radio_ble, "Off");
+ lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_DIM), 0);
+ }
+
+ lv_label_set_text_fmt(gui_radio_pkts, "RX: %lu TX: %lu", stat_rx, stat_tx);
+
+ // Battery detail
+ if (gui_radio_batt) {
+ lv_label_set_text_fmt(gui_radio_batt, "%.2fV %d%%", battery_voltage, (int)battery_percent);
+ lv_obj_align(gui_radio_batt, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 335);
+ }
+
+ // Temperature
+ if (gui_radio_temp) {
+ if (pmu_temperature > (PMU_TEMP_MIN - 1)) {
+ lv_label_set_text_fmt(gui_radio_temp, "%.1f C", pmu_temperature);
+ } else {
+ lv_label_set_text(gui_radio_temp, "---");
+ }
+ lv_obj_align(gui_radio_temp, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 380);
+ }
+ }
+
+ // ---- GPS screen (only when visible — float formatting is expensive) ----
+ #if HAS_GPS == true
+ if (on_gps && gui_gps_coords) {
+ bool good_fix = (gps_sats >= 4 && gps_hdop < 10.0 && gps_lat != 0.0);
+ bool any_fix = (gps_sats > 0 && gps_lat != 0.0);
+
+ // Coordinates — show when any fix, but dim when HDOP is poor
+ if (any_fix) {
+ lv_label_set_text_fmt(gui_gps_coords, "%.6f\n%.6f", gps_lat, gps_lon);
+ lv_obj_set_style_text_color(gui_gps_coords,
+ lv_color_hex(good_fix ? GUI_COL_TEAL : GUI_COL_MID), 0);
+ } else {
+ lv_label_set_text(gui_gps_coords, "No fix");
+ lv_obj_set_style_text_color(gui_gps_coords, lv_color_hex(GUI_COL_DIM), 0);
+ }
+
+ // Fix quality — color HDOP by quality
+ uint32_t hdop_col = (gps_hdop < 2.0) ? GUI_COL_GREEN :
+ (gps_hdop < 5.0) ? GUI_COL_TEAL :
+ (gps_hdop < 10.0) ? GUI_COL_AMBER : GUI_COL_RED;
+ lv_label_set_text_fmt(gui_gps_fix, "Sats: %d HDOP: %.1f", gps_sats, gps_hdop);
+ lv_obj_set_style_text_color(gui_gps_fix, lv_color_hex(hdop_col), 0);
+
+ // Alt/Speed — suppress speed when HDOP is poor (it's just noise)
+ if (good_fix) {
+ lv_label_set_text_fmt(gui_gps_alt, "Alt: %.0fm Spd: %.1fkm/h", gps_alt, gps_speed);
+ lv_obj_set_style_text_color(gui_gps_alt, lv_color_hex(GUI_COL_MID), 0);
+ } else if (any_fix) {
+ lv_label_set_text_fmt(gui_gps_alt, "Alt: %.0fm Spd: ---", gps_alt);
+ lv_obj_set_style_text_color(gui_gps_alt, lv_color_hex(GUI_COL_DIM), 0);
+ } else {
+ lv_label_set_text(gui_gps_alt, "Alt: --- Spd: ---");
+ lv_obj_set_style_text_color(gui_gps_alt, lv_color_hex(GUI_COL_DIM), 0);
+ }
+
+ if (beacon_mode_active) {
+ lv_label_set_text(gui_gps_beacon, "Beacon: active");
+ lv_obj_set_style_text_color(gui_gps_beacon, lv_color_hex(GUI_COL_AMBER), 0);
+ } else {
+ lv_label_set_text(gui_gps_beacon, "Beacon: off");
+ lv_obj_set_style_text_color(gui_gps_beacon, lv_color_hex(GUI_COL_DIM), 0);
+ }
+ }
+ #endif
+
+ // ---- Settings screen (logger status) ----
+ if (on_settings && gui_set_log_status) {
+ #if HAS_SD
+ if (imu_logging) {
+ uint32_t dur = (millis() - imu_log_start_ms) / 1000;
+ lv_label_set_text_fmt(gui_set_log_status, "%lu samples %lus", imu_log_samples, dur);
+ lv_obj_set_style_text_color(gui_set_log_status, lv_color_hex(GUI_COL_GREEN), 0);
+ if (!lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED))
+ lv_obj_add_state(gui_set_log_sw, LV_STATE_CHECKED);
+ } else {
+ lv_label_set_text(gui_set_log_status, "SD card ready");
+ lv_obj_set_style_text_color(gui_set_log_status, lv_color_hex(GUI_COL_DIM), 0);
+ if (lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED))
+ lv_obj_clear_state(gui_set_log_sw, LV_STATE_CHECKED);
+ }
+ #else
+ lv_label_set_text(gui_set_log_status, "No SD card");
+ #endif
+ }
+
+ // ---- Messages screen ----
+ if (on_msg && gui_msg_cont) {
+ if (msg_log_count == 0) {
+ lv_obj_clear_flag(gui_msg_empty, LV_OBJ_FLAG_HIDDEN);
+ for (int i = 0; i < GUI_MSG_ROWS; i++)
+ lv_obj_add_flag(gui_msg_row[i], LV_OBJ_FLAG_HIDDEN);
+ lv_label_set_text_fmt(gui_msg_title, "MESSAGES");
+ } else {
+ lv_obj_add_flag(gui_msg_empty, LV_OBJ_FLAG_HIDDEN);
+ lv_label_set_text_fmt(gui_msg_title, "MESSAGES (%d)", msg_log_count);
+
+ // Display messages newest-first
+ int shown = 0;
+ for (int i = 0; i < msg_log_count && shown < GUI_MSG_ROWS; i++) {
+ int idx = (msg_log_head - 1 - i + MSG_LOG_SIZE) % MSG_LOG_SIZE;
+ msg_entry_t &m = msg_log[idx];
+
+ lv_obj_clear_flag(gui_msg_row[shown], LV_OBJ_FLAG_HIDDEN);
+
+ // Name — green for verified LXMF, amber for announces, white for data
+ if (m.is_lxmf && m.content[0]) {
+ // LXMF message: show content as primary text
+ lv_label_set_text(gui_msg_name[shown], m.content);
+ lv_obj_set_style_text_color(gui_msg_name[shown],
+ lv_color_hex(m.verified ? GUI_COL_GREEN : GUI_COL_WHITE), 0);
+ } else {
+ lv_label_set_text(gui_msg_name[shown], m.display_name);
+ lv_obj_set_style_text_color(gui_msg_name[shown],
+ lv_color_hex(m.is_announce ? GUI_COL_AMBER : GUI_COL_WHITE), 0);
+ }
+
+ // RSSI with colour coding
+ lv_label_set_text_fmt(gui_msg_rssi[shown], "%d dBm", m.rssi);
+ uint32_t rssi_col = (m.rssi > -80) ? GUI_COL_GREEN :
+ (m.rssi > -100) ? GUI_COL_AMBER : GUI_COL_RED;
+ lv_obj_set_style_text_color(gui_msg_rssi[shown], lv_color_hex(rssi_col), 0);
+
+ // Time ago + sender name for LXMF messages
+ uint32_t ago = (millis() - m.timestamp) / 1000;
+ if (m.is_lxmf) {
+ if (ago < 60) lv_label_set_text_fmt(gui_msg_time[shown], "%s %lus ago", m.display_name, ago);
+ else if (ago < 3600) lv_label_set_text_fmt(gui_msg_time[shown], "%s %lum ago", m.display_name, ago / 60);
+ else lv_label_set_text_fmt(gui_msg_time[shown], "%s %luh ago", m.display_name, ago / 3600);
+ } else {
+ if (ago < 60) lv_label_set_text_fmt(gui_msg_time[shown], "%lus ago %dB", ago, m.pkt_len);
+ else if (ago < 3600) lv_label_set_text_fmt(gui_msg_time[shown], "%lum ago %dB", ago / 60, m.pkt_len);
+ else lv_label_set_text_fmt(gui_msg_time[shown], "%luh ago %dB", ago / 3600, m.pkt_len);
+ }
+
+ shown++;
+ }
+ // Hide unused rows
+ for (int i = shown; i < GUI_MSG_ROWS; i++)
+ lv_obj_add_flag(gui_msg_row[i], LV_OBJ_FLAG_HIDDEN);
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Initialize LVGL and create all screens
+// ---------------------------------------------------------------------------
+bool gui_init() {
+ lv_init();
+
+ // --- Display driver ---
+ gui_display = lv_display_create(GUI_W, GUI_H);
+ if (!gui_display) return false;
+ lv_display_set_color_format(gui_display, LV_COLOR_FORMAT_RGB565_SWAPPED);
+ lv_display_set_flush_cb(gui_display, gui_flush_cb);
+
+ uint32_t buf_size = GUI_W * GUI_BUF_LINES * sizeof(uint16_t);
+ gui_buf1 = (uint8_t *)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
+ gui_buf2 = (uint8_t *)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
+ if (!gui_buf1 || !gui_buf2) {
+ if (gui_buf1) free(gui_buf1);
+ if (gui_buf2) free(gui_buf2);
+ gui_buf2 = NULL;
+ gui_buf1 = (uint8_t *)malloc(buf_size);
+ if (!gui_buf1) return false;
+ }
+ lv_display_set_buffers(gui_display, gui_buf1, gui_buf2, buf_size,
+ LV_DISPLAY_RENDER_MODE_FULL);
+
+ // Shadow framebuffer for screenshots (410*502*2 = 411,640 bytes)
+ gui_screenshot_buf = (uint16_t *)heap_caps_malloc(GUI_W * GUI_H * sizeof(uint16_t),
+ MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
+ if (gui_screenshot_buf) {
+ memset(gui_screenshot_buf, 0, GUI_W * GUI_H * sizeof(uint16_t));
+ Serial.printf("[gui] screenshot buf @ %p (%u bytes)\n",
+ gui_screenshot_buf, GUI_W * GUI_H * 2);
+ }
+
+ // --- Input driver ---
+ gui_indev = lv_indev_create();
+ if (gui_indev) {
+ lv_indev_set_type(gui_indev, LV_INDEV_TYPE_POINTER);
+ lv_indev_set_read_cb(gui_indev, gui_touch_read_cb);
+ lv_indev_set_scroll_throw(gui_indev, 20); // Moderate friction: decelerates into snap within ~1s at 10fps
+ }
+
+ // --- Screen setup ---
+ gui_screen = lv_screen_active();
+ gui_style_black_container(gui_screen);
+
+ // --- Tileview: 3x3 grid, 5 populated tiles ---
+ // (1,0) Radio
+ // (0,1) GPS (1,1) Watch (2,1) Messages
+ // (1,2) Settings
+ gui_tileview = lv_tileview_create(gui_screen);
+ lv_obj_set_style_bg_color(gui_tileview, lv_color_hex(GUI_COL_BLACK), 0);
+ lv_obj_set_style_bg_opa(gui_tileview, LV_OPA_COVER, 0);
+ lv_obj_set_style_border_width(gui_tileview, 0, 0);
+ lv_obj_set_style_pad_all(gui_tileview, 0, 0);
+ lv_obj_set_scrollbar_mode(gui_tileview, LV_SCROLLBAR_MODE_OFF);
+ lv_obj_set_style_anim_duration(gui_tileview, 150, 0); // Snappy 150ms scroll snap
+ lv_obj_clear_flag(gui_tileview, LV_OBJ_FLAG_SCROLL_ELASTIC); // No bounce at tile edges
+ lv_obj_set_size(gui_tileview, GUI_W, GUI_H);
+
+ gui_tile_watch = lv_tileview_add_tile(gui_tileview, 1, 1, LV_DIR_ALL);
+ gui_tile_radio = lv_tileview_add_tile(gui_tileview, 1, 0, LV_DIR_BOTTOM);
+ gui_tile_gps = lv_tileview_add_tile(gui_tileview, 0, 1, LV_DIR_RIGHT);
+ gui_tile_msg = lv_tileview_add_tile(gui_tileview, 2, 1, LV_DIR_LEFT);
+ gui_tile_set = lv_tileview_add_tile(gui_tileview, 1, 2, LV_DIR_TOP);
+
+ // Start on watch face
+ lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_OFF);
+
+ // Haptic feedback on tile change
+ lv_obj_add_event_cb(gui_tileview, gui_tile_change_cb, LV_EVENT_VALUE_CHANGED, NULL);
+
+ // --- Create screen content ---
+ gui_create_watchface(gui_tile_watch);
+ gui_create_radio_screen(gui_tile_radio);
+ gui_create_gps_screen(gui_tile_gps);
+ gui_create_msg_screen(gui_tile_msg);
+ gui_create_settings_screen(gui_tile_set);
+
+ return true;
+}
+
+// ---------------------------------------------------------------------------
+// Screenshot: dump framebuffer as raw RGB565 to a file on SPIFFS,
+// or output dimensions to serial for external tools.
+// Call gui_screenshot() to write /screenshot.raw to SPIFFS (if mounted),
+// or read gui_screenshot_buf directly via debugger.
+// ---------------------------------------------------------------------------
+// ---------------------------------------------------------------------------
+// Remote debug protocol over serial
+// ---------------------------------------------------------------------------
+// Trigger: 3-byte prefix [0x52, 0x57, 0x53] ("RWS") + command byte + optional payload
+//
+// Commands:
+// 'S' (0x53) — Screenshot: captures next frame, responds RWSS + u16 w + u16 h + pixels
+// 'T' (0x54) — Touch inject: reads 5 bytes (u16 x, u16 y, u8 duration_100ms)
+// 'N' (0x4E) — Navigate: reads 2 bytes (u8 col, u8 row) — jump to tile
+// 'M' (0x4D) — Metrics: responds RWSM + JSON stats
+// 'I' (0x49) — Invalidate: force full screen redraw
+// 'L' (0x4C) — Log toggle: start/stop IMU logging to SD card
+// 'F' (0x46) — File list: lists files on SD card
+// 'P' (0x50) — Profile: runs standardized performance test, reports JSON results
+// 'B' (0x42) — Beacon dump: dumps last beacon packet pre/post IFAC
+// 'C' (0x43) — Crypto test: runs IFAC test vectors, reports pass/fail
+
+#define GUI_CMD_PREFIX_LEN 3
+static const uint8_t gui_cmd_prefix[] = {0x52, 0x57, 0x53}; // "RWS"
+static uint8_t gui_cmd_state = 0;
+static uint8_t gui_cmd_id = 0;
+static uint8_t gui_cmd_payload[8];
+static uint8_t gui_cmd_payload_pos = 0;
+static uint8_t gui_cmd_payload_len = 0;
+
+static void gui_cmd_execute();
+
+void gui_process_serial_byte(uint8_t b) {
+ // Match prefix
+ if (gui_cmd_state < GUI_CMD_PREFIX_LEN) {
+ if (b == gui_cmd_prefix[gui_cmd_state]) {
+ gui_cmd_state++;
+ } else {
+ gui_cmd_state = (b == gui_cmd_prefix[0]) ? 1 : 0;
+ }
+ return;
+ }
+
+ // Prefix matched — next byte is command
+ if (gui_cmd_state == GUI_CMD_PREFIX_LEN) {
+ gui_cmd_id = b;
+ gui_cmd_payload_pos = 0;
+ switch (b) {
+ case 'T': gui_cmd_payload_len = 5; break; // x(2) + y(2) + duration(1)
+ case 'N': gui_cmd_payload_len = 2; break; // col(1) + row(1)
+ case 'D': gui_cmd_payload_len = 1; break; // file index(1)
+ default: gui_cmd_payload_len = 0; break; // S, M, I, F, L, X, Z — no payload
+ }
+ gui_cmd_state++;
+ if (gui_cmd_payload_len == 0) {
+ gui_cmd_execute();
+ gui_cmd_state = 0;
+ }
+ return;
+ }
+
+ // Collecting payload
+ if (gui_cmd_payload_pos < gui_cmd_payload_len) {
+ gui_cmd_payload[gui_cmd_payload_pos++] = b;
+ if (gui_cmd_payload_pos >= gui_cmd_payload_len) {
+ gui_cmd_execute();
+ gui_cmd_state = 0;
+ }
+ }
+}
+
+static void gui_cmd_execute() {
+ const uint8_t hdr[] = {'R', 'W', 'S', gui_cmd_id};
+
+ switch (gui_cmd_id) {
+ case 'S': { // Screenshot
+ if (gui_screenshot_buf) {
+ // Unblank and force full redraw so screenshot captures entire screen
+ if (display_blanked) display_unblank();
+ lv_obj_invalidate(lv_screen_active());
+ gui_screenshot_pending = true;
+ // Force full-screen render into screenshot buffer
+ // Advance tick past the refresh period to ensure render fires
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ gui_update_data();
+ lv_timer_handler();
+ gui_screenshot_pending = false;
+ Serial.write(hdr, 4);
+ uint16_t w = GUI_W, h = GUI_H;
+ Serial.write((uint8_t *)&w, 2);
+ Serial.write((uint8_t *)&h, 2);
+ Serial.write((uint8_t *)gui_screenshot_buf, GUI_W * GUI_H * 2);
+ Serial.flush();
+ } else {
+ Serial.write(hdr, 4);
+ uint16_t z = 0;
+ Serial.write((uint8_t *)&z, 2);
+ Serial.write((uint8_t *)&z, 2);
+ Serial.flush();
+ }
+ break;
+ }
+
+ case 'T': { // Touch inject
+ gui_inject_x = gui_cmd_payload[0] | (gui_cmd_payload[1] << 8);
+ gui_inject_y = gui_cmd_payload[2] | (gui_cmd_payload[3] << 8);
+ uint32_t dur = gui_cmd_payload[4] * 100; // duration in 100ms units
+ if (dur == 0) dur = 200;
+ gui_inject_pressed = true;
+ gui_inject_until = millis() + dur;
+ // Unblank display on injected touch
+ if (display_blanked) display_unblank();
+ break;
+ }
+
+ case 'N': { // Navigate to tile
+ uint8_t col = gui_cmd_payload[0];
+ uint8_t row = gui_cmd_payload[1];
+ if (gui_tileview) {
+ // Find tile at position
+ lv_obj_t *target = NULL;
+ if (col == 1 && row == 1) target = gui_tile_watch;
+ else if (col == 1 && row == 0) target = gui_tile_radio;
+ else if (col == 0 && row == 1) target = gui_tile_gps;
+ else if (col == 2 && row == 1) target = gui_tile_msg;
+ else if (col == 1 && row == 2) target = gui_tile_set;
+ if (target) {
+ lv_tileview_set_tile(gui_tileview, target, LV_ANIM_ON);
+ }
+ }
+ if (display_blanked) display_unblank();
+ break;
+ }
+
+ case 'M': { // Metrics
+ Serial.write(hdr, 4);
+ char buf[192];
+ uint32_t avg_flush = gui_frame_count > 0 ? gui_flush_us_total / gui_frame_count : 0;
+ snprintf(buf, sizeof(buf),
+ "{\"build\":\"%s %s\",\"loop\":%lu,\"radio\":%lu,"
+ "\"serial\":%lu,\"disp\":%lu,\"pmu\":%lu,"
+ "\"gps\":%lu,\"bt\":%lu,\"imu\":%lu,"
+ "\"bcn_gate\":%d,\"hw_ready\":%d,"
+ "\"lxmf_id\":%d,\"bcn_crypto\":%d}\n",
+ __DATE__, __TIME__,
+ gui_loop_us_last, prof_radio_us, prof_serial_us,
+ prof_display_us, prof_pmu_us, prof_gps_us,
+ prof_bt_us, prof_imu_us,
+ beacon_gate, hw_ready ? 1 : 0,
+ lxmf_identity_configured ? 1 : 0,
+ beacon_crypto_configured ? 1 : 0);
+ gui_loop_us_max = 0;
+ Serial.write((uint8_t *)buf, strlen(buf));
+ Serial.flush();
+ break;
+ }
+
+ case 'P': { // Standardized performance profile test
+ Serial.write(hdr, 4);
+ if (display_blanked) display_unblank();
+
+ uint32_t p_t0, p_t1;
+ uint32_t p_idle_render = 0, p_idle_flush = 0;
+ uint32_t p_full_render = 0, p_full_flush = 0;
+ uint32_t p_nav_total = 0;
+ uint32_t p_data_update = 0;
+ uint32_t p_frames = 0;
+ uint32_t p_multi_total = 0;
+
+ // Test 1: Idle frame (nothing dirty)
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler(); // clear any pending
+ gui_flush_us_last = 0;
+ gui_frame_count = 0;
+ p_t0 = micros();
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ p_t1 = micros();
+ p_idle_render = p_t1 - p_t0;
+ p_idle_flush = gui_flush_us_last;
+
+ // Test 2: Full invalidation + render
+ lv_obj_invalidate(lv_screen_active());
+ gui_flush_us_last = 0;
+ p_t0 = micros();
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ p_t1 = micros();
+ p_full_render = p_t1 - p_t0;
+ p_full_flush = gui_flush_us_last;
+
+ // Test 3: Data update cycle
+ gui_last_data_update = 0; // force update
+ p_t0 = micros();
+ gui_update_data();
+ p_t1 = micros();
+ p_data_update = p_t1 - p_t0;
+
+ // Test 4: Navigate to each tile and back (5 transitions)
+ p_t0 = micros();
+ lv_tileview_set_tile(gui_tileview, gui_tile_radio, LV_ANIM_OFF);
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ lv_tileview_set_tile(gui_tileview, gui_tile_gps, LV_ANIM_OFF);
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ lv_tileview_set_tile(gui_tileview, gui_tile_msg, LV_ANIM_OFF);
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ lv_tileview_set_tile(gui_tileview, gui_tile_set, LV_ANIM_OFF);
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_OFF);
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ p_t1 = micros();
+ p_nav_total = p_t1 - p_t0;
+
+ // Test 5: Rapid frame burst (10 full frames)
+ p_t0 = micros();
+ for (int i = 0; i < 10; i++) {
+ lv_obj_invalidate(lv_screen_active());
+ lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
+ lv_timer_handler();
+ }
+ p_t1 = micros();
+ p_multi_total = p_t1 - p_t0;
+
+ Serial.printf("{\"test\":\"profile\",\"build\":\"%s %s\","
+ "\"idle_us\":%lu,\"idle_flush_us\":%lu,"
+ "\"full_us\":%lu,\"full_flush_us\":%lu,"
+ "\"data_update_us\":%lu,"
+ "\"nav_5tile_us\":%lu,"
+ "\"burst_10frame_us\":%lu,"
+ "\"avg_frame_us\":%lu,"
+ "\"loop_us\":%lu,"
+ "\"heap\":%lu,\"psram\":%lu}\n",
+ __DATE__, __TIME__,
+ p_idle_render, p_idle_flush,
+ p_full_render, p_full_flush,
+ p_data_update,
+ p_nav_total,
+ p_multi_total, p_multi_total / 10,
+ gui_loop_us_last,
+ (uint32_t)esp_get_free_heap_size(),
+ (uint32_t)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
+ Serial.flush();
+ break;
+ }
+
+ case 'B': { // Beacon packet dump
+ extern uint8_t diag_beacon_pre[];
+ extern uint16_t diag_beacon_pre_len;
+ extern uint8_t diag_beacon_post[];
+ extern uint16_t diag_beacon_post_len;
+
+ Serial.write(hdr, 4);
+ Serial.printf("{\"pre_len\":%d,\"post_len\":%d", diag_beacon_pre_len, diag_beacon_post_len);
+ if (diag_beacon_pre_len > 0) {
+ Serial.printf(",\"pre\":\"");
+ for (int i = 0; i < diag_beacon_pre_len; i++) Serial.printf("%02x", diag_beacon_pre[i]);
+ Serial.printf("\"");
+ }
+ if (diag_beacon_post_len > 0) {
+ Serial.printf(",\"post\":\"");
+ for (int i = 0; i < diag_beacon_post_len; i++) Serial.printf("%02x", diag_beacon_post[i]);
+ Serial.printf("\"");
+ }
+ Serial.println("}");
+ Serial.flush();
+ break;
+ }
+
+ case 'C': { // Crypto test — IFAC test vectors
+ #if HAS_GPS == true
+ Serial.write(hdr, 4);
+
+ // Test vectors from scripts/test_ifac.py
+ const uint8_t tv_key[64] = {0x3a, 0xc2, 0xe0, 0x12, 0xa0, 0x86, 0x04, 0x3c, 0x67, 0xcc, 0xef, 0x40, 0x6a, 0x0b, 0xdb, 0x38, 0xc0, 0x66, 0xb2, 0xee, 0x0a, 0x7f, 0x18, 0x27, 0xfa, 0x1c, 0xb9, 0xdc, 0xcf, 0xbb, 0x8e, 0x9d, 0x53, 0x48, 0xc5, 0x56, 0xf0, 0x8e, 0xed, 0xf3, 0x0b, 0xce, 0x46, 0x2b, 0xb2, 0x09, 0x6b, 0x99, 0x26, 0x08, 0xf4, 0xfc, 0xfd, 0x12, 0x32, 0x4b, 0xb2, 0x45, 0x86, 0x2b, 0x59, 0xd6, 0x11, 0xc7};
+ const uint8_t tv_pk[32] = {0x1a, 0x54, 0x5d, 0x78, 0x34, 0xc3, 0xe1, 0x6c, 0x53, 0x9d, 0xd5, 0xf5, 0x3a, 0xd1, 0x5b, 0x67, 0xae, 0x57, 0x5e, 0x97, 0x06, 0x05, 0x38, 0x5b, 0xeb, 0x76, 0xe9, 0x85, 0x2e, 0xf9, 0xe1, 0xdf};
+ const uint8_t tv_msg[19] = {0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
+ const uint8_t tv_sig[64] = {0x99, 0x80, 0x27, 0x05, 0xaf, 0xde, 0xb0, 0xe6, 0xfe, 0xe5, 0x2b, 0xbc, 0x35, 0x4a, 0x87, 0x93, 0xd8, 0xc2, 0x9c, 0x77, 0x41, 0x6c, 0x5c, 0x54, 0x62, 0x7e, 0x66, 0xc6, 0x50, 0x05, 0xe5, 0x0a, 0x02, 0x48, 0x94, 0x4b, 0xb1, 0x02, 0x5b, 0x3a, 0xaa, 0xa2, 0x9b, 0x26, 0xc4, 0x7f, 0x49, 0x4b, 0xa2, 0x1a, 0xf0, 0xb5, 0xd0, 0x08, 0x8f, 0x9b, 0x49, 0x5b, 0xf2, 0xc7, 0xe1, 0x83, 0x99, 0x01};
+ const uint8_t tv_mask[27] = {0x3b, 0x8f, 0x16, 0xab, 0xe6, 0x0b, 0x8e, 0x35, 0xcb, 0x47, 0x5a, 0x3d, 0x13, 0x00, 0x05, 0xe6, 0x79, 0x79, 0x99, 0x23, 0x35, 0x24, 0x64, 0xd8, 0x4b, 0xf5, 0x3c};
+ const uint8_t tv_result[27] = {0xbb, 0x8f, 0x49, 0x5b, 0xf2, 0xc7, 0xe1, 0x83, 0x99, 0x01, 0x48, 0x09, 0x45, 0x78, 0x9f, 0x5a, 0xa7, 0x89, 0x88, 0x01, 0x06, 0x60, 0x31, 0xbe, 0x3c, 0x7d, 0xa5};
+
+ // Test 1: Keypair derivation
+ uint8_t pk[32], sk[64];
+ crypto_sign_ed25519_seed_keypair(pk, sk, tv_key + 32);
+ bool pk_match = (memcmp(pk, tv_pk, 32) == 0);
+
+ // Test 2: Signature
+ uint8_t sig[64];
+ unsigned long long sig_len;
+ crypto_sign_ed25519_detached(sig, &sig_len, tv_msg, 19, sk);
+ bool sig_match = (memcmp(sig, tv_sig, 64) == 0);
+
+ // Test 3: HKDF
+ uint8_t mask[27];
+ rns_hkdf_var(sig + 56, 8, tv_key, 64, mask, 27);
+ bool mask_match = (memcmp(mask, tv_mask, 27) == 0);
+
+ // Test 4: Full IFAC apply
+ uint8_t pkt[64];
+ memcpy(pkt, tv_msg, 19);
+ // Save original ifac state and substitute test key
+ uint8_t saved_key[64]; bool saved_configured;
+ memcpy(saved_key, ifac_key, 64);
+ saved_configured = ifac_configured;
+ memcpy(ifac_key, tv_key, 64);
+ ifac_derive_keypair();
+ ifac_configured = true;
+ uint16_t result_len = ifac_apply(pkt, 19);
+ bool result_match = (result_len == 27) && (memcmp(pkt, tv_result, 27) == 0);
+ // Restore
+ memcpy(ifac_key, saved_key, 64);
+ if (saved_configured) ifac_derive_keypair();
+ ifac_configured = saved_configured;
+
+ // Report
+ // Dump the live IFAC key for verification
+ Serial.printf("{\"pk\":%s,\"sig\":%s,\"hkdf\":%s,\"ifac\":%s,\"configured\":%s",
+ pk_match ? "true" : "false",
+ sig_match ? "true" : "false",
+ mask_match ? "true" : "false",
+ result_match ? "true" : "false",
+ ifac_configured ? "true" : "false");
+ Serial.printf(",\"live_key\":\"");
+ for (int i = 0; i < 64; i++) Serial.printf("%02x", ifac_key[i]);
+ Serial.printf("\"");
+
+ // Dump actual values on failure for debugging
+ if (!sig_match) {
+ Serial.printf(",\"actual_sig\":\"");
+ for (int i = 0; i < 64; i++) Serial.printf("%02x", sig[i]);
+ Serial.printf("\"");
+ }
+ if (!mask_match) {
+ Serial.printf(",\"actual_mask\":\"");
+ for (int i = 0; i < 27; i++) Serial.printf("%02x", mask[i]);
+ Serial.printf("\"");
+ }
+ if (!result_match) {
+ Serial.printf(",\"actual_result\":\"");
+ for (int i = 0; i < (int)result_len; i++) Serial.printf("%02x", pkt[i]);
+ Serial.printf("\",\"result_len\":%d", result_len);
+ }
+ Serial.println("}");
+ Serial.flush();
+ #else
+ Serial.write(hdr, 4);
+ Serial.println("{\"error\":\"no_gps\"}");
+ Serial.flush();
+ #endif
+ break;
+ }
+
+ case 'I': { // Invalidate — force full redraw
+ if (gui_screen) lv_obj_invalidate(gui_screen);
+ if (display_blanked) display_unblank();
+ break;
+ }
+
+ case 'L': { // Toggle IMU logging
+ Serial.write(hdr, 4);
+ if (gui_log_toggle_fn) {
+ gui_imu_logging = gui_log_toggle_fn();
+ Serial.printf("{\"logging\":%s}\n", gui_imu_logging ? "true" : "false");
+ } else {
+ Serial.println("{\"logging\":false,\"error\":\"not_available\"}");
+ }
+ Serial.flush();
+ break;
+ }
+
+ case 'F': { // List files on SD card
+ Serial.write(hdr, 4);
+ if (gui_list_files_fn) {
+ gui_list_files_fn();
+ } else {
+ Serial.println("{\"error\":\"no_sd\"}");
+ }
+ Serial.flush();
+ break;
+ }
+ case 'D': { // Download file by index
+ Serial.write(hdr, 4);
+ if (gui_download_file_fn) {
+ gui_download_file_fn(gui_cmd_payload[0]);
+ } else {
+ Serial.println("{\"error\":\"no_sd\"}");
+ }
+ Serial.flush();
+ break;
+ }
+ case 'X': { // Hard reset
+ Serial.write(hdr, 4);
+ Serial.println("{\"reset\":true}");
+ Serial.flush();
+ delay(100);
+ ESP.restart();
+ break;
+ }
+ case 'Z': { // Reboot into download mode (no BOOT+RST needed)
+ Serial.write(hdr, 4);
+ Serial.println("{\"bootloader\":true}");
+ Serial.flush();
+ delay(100);
+ #if MCU_VARIANT == MCU_ESP32
+ REG_WRITE(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT);
+ ESP.restart();
+ #endif
+ break;
+ }
+ }
+}
+
+void gui_screenshot_info() {
+ if (gui_screenshot_buf) {
+ Serial.printf("[screenshot] addr=%p size=%u w=%d h=%d\n",
+ gui_screenshot_buf, GUI_W * GUI_H * 2, GUI_W, GUI_H);
+ } else {
+ Serial.println("[screenshot] buffer not allocated");
+ }
+}
+
+// Write screenshot to SD card as raw RGB565 + BMP header
+#if HAS_SD
+#include
+#include "SharedSPI.h"
+bool gui_screenshot_sd(const char *path = "/screenshot.bmp") {
+ if (!gui_screenshot_buf) return false;
+
+ // Acquire shared SPI mutex for SD access
+ if (shared_spi_mutex) xSemaphoreTake(shared_spi_mutex, portMAX_DELAY);
+ SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
+ if (!SD.begin(SD_CS, SPI, 4000000, "/sd", 5)) {
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ Serial.println("[screenshot] SD init failed");
+ return false;
+ }
+
+ File f = SD.open(path, FILE_WRITE);
+ if (!f) {
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ Serial.println("[screenshot] file open failed");
+ return false;
+ }
+
+ // Write BMP header (RGB565 LE, top-down)
+ uint32_t img_size = GUI_W * GUI_H * 2;
+ uint32_t file_size = 14 + 40 + 12 + img_size; // file + info + masks + pixels
+ uint32_t data_offset = 14 + 40 + 12;
+
+ // BMP file header (14 bytes)
+ uint8_t bmp_hdr[14] = {'B', 'M'};
+ memcpy(&bmp_hdr[2], &file_size, 4);
+ memset(&bmp_hdr[6], 0, 4); // reserved
+ memcpy(&bmp_hdr[10], &data_offset, 4);
+ f.write(bmp_hdr, 14);
+
+ // DIB header (BITMAPINFOHEADER, 40 bytes)
+ uint8_t dib[40] = {};
+ uint32_t dib_size = 40;
+ int32_t bmp_w = GUI_W;
+ int32_t bmp_h = -GUI_H; // negative = top-down
+ uint16_t planes = 1;
+ uint16_t bpp = 16;
+ uint32_t compression = 3; // BI_BITFIELDS
+ memcpy(&dib[0], &dib_size, 4);
+ memcpy(&dib[4], &bmp_w, 4);
+ memcpy(&dib[8], &bmp_h, 4);
+ memcpy(&dib[12], &planes, 2);
+ memcpy(&dib[14], &bpp, 2);
+ memcpy(&dib[16], &compression, 4);
+ memcpy(&dib[20], &img_size, 4);
+ f.write(dib, 40);
+
+ // RGB565 bitmasks (R, G, B)
+ uint32_t mask_r = 0xF800, mask_g = 0x07E0, mask_b = 0x001F;
+ f.write((uint8_t *)&mask_r, 4);
+ f.write((uint8_t *)&mask_g, 4);
+ f.write((uint8_t *)&mask_b, 4);
+
+ // Pixel data (RGB565 LE, row by row)
+ // BMP rows must be 4-byte aligned; 410*2=820 bytes per row, 820%4=0, no padding needed
+ f.write((uint8_t *)gui_screenshot_buf, img_size);
+
+ f.close();
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+
+ Serial.printf("[screenshot] saved %s (%u bytes)\n", path, file_size);
+ return true;
+}
+#endif
+
+// ---------------------------------------------------------------------------
+// Main GUI update — called from update_display()
+// ---------------------------------------------------------------------------
+void gui_update() {
+ static uint32_t last_tick = 0;
+ uint32_t now = millis();
+ lv_tick_inc(now - last_tick);
+ last_tick = now;
+
+ // Measure loop interval
+ uint32_t now_us = micros();
+ if (gui_last_update_us > 0) {
+ gui_loop_us_last = now_us - gui_last_update_us;
+ if (gui_loop_us_last > gui_loop_us_max) gui_loop_us_max = gui_loop_us_last;
+ }
+ gui_last_update_us = now_us;
+
+ gui_update_data();
+
+ // Skip LVGL rendering when display is blanked — no SPI traffic to sleeping display
+ if (!display_blanked) {
+ gui_render_start = micros();
+ lv_timer_handler();
+ gui_render_us_last = micros() - gui_render_start;
+ }
+ // After lv_timer_handler, a new DMA may be queued via gui_flush_cb.
+ // It runs in background on SPI3 until the next gui_update() call.
+}
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT
+#endif // GUI_H
diff --git a/IMULogger.h b/IMULogger.h
new file mode 100644
index 00000000..2ecf7f53
--- /dev/null
+++ b/IMULogger.h
@@ -0,0 +1,203 @@
+// Sensor Data Logger — streams all sensor data to SD card as CSV
+// Start/stop via remote debug command 'L' or Settings screen toggle
+//
+// CSV format: timestamp_ms,type,d0,d1,d2,d3,d4,d5,d6,d7,d8
+// Type: A=accel, G=gyro, M=mag, S=step, W=wrist_tilt, P=GPS, T=touch
+// IMU values in raw int16 units, GPS in scaled integers
+// Timestamp is millis() at time of callback
+
+#ifndef IMULOGGER_H
+#define IMULOGGER_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT && HAS_SD
+
+#include
+#include "SharedSPI.h"
+
+// Ring buffer for sensor samples (stored in PSRAM)
+// Tagged samples: type char + up to 9 int32 fields
+struct sensor_sample_t {
+ uint32_t timestamp;
+ char type; // A=accel, G=gyro, M=mag, S=step, W=wrist, P=gps, T=touch
+ int32_t d[9]; // data fields (meaning depends on type)
+};
+
+#define IMU_LOG_BUF_SIZE 512 // samples before flush (~10s at 50Hz)
+static sensor_sample_t *imu_log_buf = NULL;
+static volatile uint32_t imu_log_head = 0; // write position
+static volatile uint32_t imu_log_tail = 0; // read position
+bool imu_logging = false;
+static File imu_log_file;
+uint32_t imu_log_samples = 0;
+uint32_t imu_log_start_ms = 0;
+
+// Push a tagged sample to the ring buffer (safe from callbacks)
+static void sensor_log_push(char type, int32_t d0=0, int32_t d1=0, int32_t d2=0,
+ int32_t d3=0, int32_t d4=0, int32_t d5=0,
+ int32_t d6=0, int32_t d7=0, int32_t d8=0) {
+ if (!imu_logging || !imu_log_buf) return;
+ uint32_t next = (imu_log_head + 1) % IMU_LOG_BUF_SIZE;
+ if (next == imu_log_tail) return; // full
+ sensor_sample_t &s = imu_log_buf[imu_log_head];
+ s.timestamp = millis();
+ s.type = type;
+ s.d[0]=d0; s.d[1]=d1; s.d[2]=d2; s.d[3]=d3; s.d[4]=d4;
+ s.d[5]=d5; s.d[6]=d6; s.d[7]=d7; s.d[8]=d8;
+ imu_log_head = next;
+}
+
+// Sensor callbacks — push individual tagged samples
+void imu_log_accel_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
+ if (size >= 6) {
+ int16_t ax = (int16_t)(data[0] | (data[1] << 8));
+ int16_t ay = (int16_t)(data[2] | (data[3] << 8));
+ int16_t az = (int16_t)(data[4] | (data[5] << 8));
+ sensor_log_push('A', ax, ay, az);
+ }
+}
+
+void imu_log_gyro_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
+ if (size >= 6) {
+ int16_t gx = (int16_t)(data[0] | (data[1] << 8));
+ int16_t gy = (int16_t)(data[2] | (data[3] << 8));
+ int16_t gz = (int16_t)(data[4] | (data[5] << 8));
+ sensor_log_push('G', gx, gy, gz);
+ }
+}
+
+void imu_log_mag_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
+ if (size >= 6) {
+ int16_t mx = (int16_t)(data[0] | (data[1] << 8));
+ int16_t my = (int16_t)(data[2] | (data[3] << 8));
+ int16_t mz = (int16_t)(data[4] | (data[5] << 8));
+ sensor_log_push('M', mx, my, mz);
+ }
+}
+
+// Log step counter event
+void sensor_log_step(uint32_t count) {
+ sensor_log_push('S', (int32_t)count);
+}
+
+// Log wrist tilt event
+void sensor_log_wrist_tilt() {
+ sensor_log_push('W');
+}
+
+// Log GPS fix (call at 1Hz from main loop when logging)
+void sensor_log_gps(double lat, double lon, double alt, double speed, double hdop, uint8_t sats) {
+ sensor_log_push('P',
+ (int32_t)(lat * 1e6), (int32_t)(lon * 1e6), (int32_t)(alt * 10),
+ (int32_t)(speed * 100), (int32_t)(hdop * 100), sats);
+}
+
+// Log touch event
+void sensor_log_touch(int16_t x, int16_t y, bool pressed) {
+ sensor_log_push('T', x, y, pressed ? 1 : 0);
+}
+
+// Forward declaration
+void imu_log_flush();
+
+bool imu_log_start(SensorBHI260AP *bhi) {
+ if (imu_logging || !bhi) return false;
+
+ // Allocate ring buffer in PSRAM
+ if (!imu_log_buf) {
+ imu_log_buf = (sensor_sample_t *)heap_caps_malloc(
+ IMU_LOG_BUF_SIZE * sizeof(sensor_sample_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
+ if (!imu_log_buf) return false;
+ }
+ imu_log_head = 0;
+ imu_log_tail = 0;
+
+ // Init SD (acquire shared SPI mutex)
+ if (shared_spi_mutex) xSemaphoreTake(shared_spi_mutex, portMAX_DELAY);
+ SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
+ bool sd_ok = SD.begin(SD_CS, SPI, 4000000, "/sd", 5);
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ if (!sd_ok) {
+ Serial.println("[imu_log] SD init failed");
+ return false;
+ }
+
+ // Create timestamped filename
+ char fname[32];
+ snprintf(fname, sizeof(fname), "/imu_%lu.csv", millis() / 1000);
+ imu_log_file = SD.open(fname, FILE_WRITE);
+ if (!imu_log_file) {
+ Serial.println("[imu_log] file open failed");
+ SD.end(); SPI.end();
+ return false;
+ }
+
+ // Write CSV header
+ imu_log_file.println("ms,type,d0,d1,d2,d3,d4,d5,d6,d7,d8");
+
+ // Configure sensors at 50Hz
+ bhi->configure(SensorBHI260AP::ACCEL_PASSTHROUGH, 50.0, 0);
+ bhi->onResultEvent(SensorBHI260AP::ACCEL_PASSTHROUGH, imu_log_accel_cb);
+
+ bhi->configure(SensorBHI260AP::GYRO_PASSTHROUGH, 50.0, 0);
+ bhi->onResultEvent(SensorBHI260AP::GYRO_PASSTHROUGH, imu_log_gyro_cb);
+
+ bhi->configure(SensorBHI260AP::MAGNETOMETER_PASSTHROUGH, 25.0, 0);
+ bhi->onResultEvent(SensorBHI260AP::MAGNETOMETER_PASSTHROUGH, imu_log_mag_cb);
+
+ imu_logging = true;
+ imu_log_samples = 0;
+ imu_log_start_ms = millis();
+ Serial.printf("[imu_log] started: %s\n", fname);
+ return true;
+}
+
+void imu_log_stop(SensorBHI260AP *bhi) {
+ if (!imu_logging) return;
+
+ // Disable sensor streams
+ if (bhi) {
+ bhi->configure(SensorBHI260AP::ACCEL_PASSTHROUGH, 0, 0);
+ bhi->configure(SensorBHI260AP::GYRO_PASSTHROUGH, 0, 0);
+ bhi->configure(SensorBHI260AP::MAGNETOMETER_PASSTHROUGH, 0, 0);
+ }
+
+ // Flush remaining samples
+ imu_log_flush();
+
+ uint32_t duration = (millis() - imu_log_start_ms) / 1000;
+ Serial.printf("[imu_log] stopped: %lu samples in %lus (%.1f Hz)\n",
+ imu_log_samples, duration,
+ duration > 0 ? (float)imu_log_samples / duration : 0);
+
+ if (shared_spi_mutex) xSemaphoreTake(shared_spi_mutex, portMAX_DELAY);
+ imu_log_file.close();
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ imu_logging = false;
+}
+
+// Flush ring buffer to SD — call from main loop
+void imu_log_flush() {
+ if (!imu_logging || !imu_log_buf) return;
+ if (shared_spi_mutex && xSemaphoreTake(shared_spi_mutex, pdMS_TO_TICKS(50)) != pdTRUE) return;
+
+ char line[128];
+ uint32_t flushed = 0;
+ while (imu_log_tail != imu_log_head) {
+ sensor_sample_t &s = imu_log_buf[imu_log_tail];
+ int len = snprintf(line, sizeof(line), "%lu,%c,%ld,%ld,%ld,%ld,%ld,%ld,%ld,%ld,%ld\n",
+ s.timestamp, s.type,
+ s.d[0], s.d[1], s.d[2], s.d[3], s.d[4],
+ s.d[5], s.d[6], s.d[7], s.d[8]);
+ imu_log_file.write((uint8_t *)line, len);
+ imu_log_tail = (imu_log_tail + 1) % IMU_LOG_BUF_SIZE;
+ imu_log_samples++;
+ flushed++;
+ }
+ if (flushed > 0) {
+ imu_log_file.flush();
+ }
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+}
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT && HAS_SD
+#endif // IMULOGGER_H
diff --git a/IfacAuth.h b/IfacAuth.h
new file mode 100644
index 00000000..c4289cc7
--- /dev/null
+++ b/IfacAuth.h
@@ -0,0 +1,222 @@
+// Copyright (C) 2026, IFAC authentication contributed by GlassOnTin
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#ifndef IFAC_AUTH_H
+#define IFAC_AUTH_H
+
+#if HAS_GPS == true
+
+#include "sodium/crypto_sign_ed25519.h"
+#include "nvs_flash.h"
+#include "nvs.h"
+#include "mbedtls/sha256.h"
+
+// NVS namespace for IFAC key storage
+#define IFAC_NVS_NAMESPACE "ifac"
+#define IFAC_NVS_KEY "ifac_key"
+
+#define IFAC_SIZE 8
+
+// IFAC state
+bool ifac_configured = false;
+uint8_t ifac_key[64];
+uint8_t ifac_ed25519_pk[32];
+uint8_t ifac_ed25519_sk[64]; // libsodium format: seed(32) + pk(32)
+
+// ---- NVS Storage ----
+
+static bool ifac_nvs_load() {
+ nvs_handle_t handle;
+ if (nvs_open(IFAC_NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) return false;
+
+ size_t key_len = 64;
+ bool ok = (nvs_get_blob(handle, IFAC_NVS_KEY, ifac_key, &key_len) == ESP_OK)
+ && (key_len == 64);
+
+ nvs_close(handle);
+ return ok;
+}
+
+static bool ifac_nvs_save() {
+ nvs_handle_t handle;
+ if (nvs_open(IFAC_NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) return false;
+
+ bool ok = (nvs_set_blob(handle, IFAC_NVS_KEY, ifac_key, 64) == ESP_OK)
+ && (nvs_commit(handle) == ESP_OK);
+
+ nvs_close(handle);
+ return ok;
+}
+
+// ---- Ed25519 Keypair Derivation ----
+// The signing seed is ifac_key[32:64] (last 32 bytes).
+
+static void ifac_derive_keypair() {
+ crypto_sign_ed25519_seed_keypair(ifac_ed25519_pk, ifac_ed25519_sk,
+ ifac_key + 32);
+}
+
+// ---- Variable-Length HKDF-SHA256 ----
+// Matches RNS Cryptography.hkdf() with context=b"".
+// Uses hmac_sha256() from BeaconCrypto.h (must be included first).
+//
+// Extract: PRK = HMAC-SHA256(salt, ikm)
+// Expand: T(i) = HMAC-SHA256(PRK, T(i-1) || counter_byte)
+// counter_byte = (i+1) % 256, starting from i=0
+
+static int rns_hkdf_var(const uint8_t *ikm, size_t ikm_len,
+ const uint8_t *salt, size_t salt_len,
+ uint8_t *output, size_t output_len) {
+ uint8_t prk[32];
+ int ret = hmac_sha256(salt, salt_len, ikm, ikm_len, prk);
+ if (ret != 0) return ret;
+
+ uint8_t prev_block[32];
+ size_t prev_len = 0;
+ size_t written = 0;
+ uint8_t expand_buf[32 + 1]; // max: prev_block(32) + counter(1)
+ int block_idx = 0;
+
+ while (written < output_len) {
+ // Build input: T(i-1) || counter
+ if (prev_len > 0) {
+ memcpy(expand_buf, prev_block, prev_len);
+ }
+ expand_buf[prev_len] = (uint8_t)((block_idx + 1) % 256);
+
+ uint8_t block[32];
+ ret = hmac_sha256(prk, 32, expand_buf, prev_len + 1, block);
+ if (ret != 0) return ret;
+
+ size_t to_copy = output_len - written;
+ if (to_copy > 32) to_copy = 32;
+ memcpy(output + written, block, to_copy);
+ written += to_copy;
+
+ memcpy(prev_block, block, 32);
+ prev_len = 32;
+ block_idx++;
+ }
+
+ return 0;
+}
+
+// ---- Reticulum IFAC_SALT (from Reticulum.py IFAC_SALT constant) ----
+static const uint8_t RNS_IFAC_SALT[32] = {
+ 0xad, 0xf5, 0x4d, 0x88, 0x2c, 0x9a, 0x9b, 0x80, 0x77, 0x1e, 0xb4, 0x99, 0x5d, 0x70, 0x2d, 0x4a,
+ 0x3e, 0x73, 0x33, 0x91, 0xb2, 0xa0, 0xf5, 0x3f, 0x41, 0x6d, 0x9f, 0x90, 0x7e, 0x55, 0xcf, 0xf8,
+};
+
+// ---- Self-provision IFAC key from network_name + passphrase ----
+// Replicates Reticulum.py:821-826 IFAC key derivation:
+// ifac_origin = SHA256(network_name) || SHA256(passphrase)
+// ifac_origin_hash = SHA256(ifac_origin)
+// ifac_key = HKDF(ikm=ifac_origin_hash, salt=IFAC_SALT, length=64)
+static bool ifac_self_provision(const char *network_name, const char *passphrase) {
+ uint8_t name_hash[32], key_hash[32];
+ mbedtls_sha256((const uint8_t *)network_name, strlen(network_name), name_hash, 0);
+ mbedtls_sha256((const uint8_t *)passphrase, strlen(passphrase), key_hash, 0);
+
+ uint8_t ifac_origin[64];
+ memcpy(ifac_origin, name_hash, 32);
+ memcpy(ifac_origin + 32, key_hash, 32);
+
+ uint8_t origin_hash[32];
+ mbedtls_sha256(ifac_origin, 64, origin_hash, 0);
+
+ int ret = rns_hkdf_var(origin_hash, 32, RNS_IFAC_SALT, 32, ifac_key, 64);
+ if (ret != 0) return false;
+
+ ifac_nvs_save();
+ ifac_derive_keypair();
+ ifac_configured = true;
+ return true;
+}
+
+// ---- IFAC Initialization ----
+// Call from setup() after lxmf_init_identity().
+// Loads from NVS if available, otherwise self-provisions from beacon config.
+
+static void ifac_init() {
+ if (ifac_nvs_load()) {
+ ifac_derive_keypair();
+ ifac_configured = true;
+ }
+ // If no key loaded, caller should call ifac_self_provision()
+ // with network_name/passphrase from beacon config.
+}
+
+// ---- Apply IFAC to Outgoing Packet ----
+// Modifies pkt in-place. Returns new size (size + IFAC_SIZE) on success,
+// or original size if IFAC is not configured.
+//
+// Algorithm (from RNS Transport.transmit):
+// 1. sig = Ed25519Sign(pkt, sk) → 64 bytes
+// 2. ifac = sig[56:64] → last 8 bytes
+// 3. mask = HKDF(ikm=ifac, salt=ifac_key, len=size+8)
+// 4. Assemble: new_header(2) + ifac(8) + payload(size-2)
+// 5. Set IFAC flag: byte[0] |= 0x80
+// 6. XOR mask:
+// - byte 0: masked, but preserve 0x80 flag
+// - byte 1: masked
+// - bytes 2..9: NOT masked (IFAC itself)
+// - bytes 10+: masked
+
+static uint16_t ifac_apply(uint8_t *pkt, uint16_t size) {
+ if (!ifac_configured || size < 2) return size;
+
+ uint16_t new_size = size + IFAC_SIZE;
+
+ // 1. Sign the original packet
+ uint8_t signature[64];
+ unsigned long long sig_len_unused;
+ crypto_sign_ed25519_detached(signature, &sig_len_unused,
+ pkt, size, ifac_ed25519_sk);
+
+ // 2. Extract IFAC: last 8 bytes of signature
+ uint8_t ifac[IFAC_SIZE];
+ memcpy(ifac, signature + 64 - IFAC_SIZE, IFAC_SIZE);
+
+ // 3. Generate mask
+ uint8_t mask[MTU + IFAC_SIZE];
+ rns_hkdf_var(ifac, IFAC_SIZE, ifac_key, 64, mask, new_size);
+
+ // 4. Shift payload to make room for IFAC after header
+ // pkt layout before: [hdr0][hdr1][payload...]
+ // pkt layout after: [hdr0|0x80][hdr1][ifac:8][payload...]
+ memmove(pkt + 2 + IFAC_SIZE, pkt + 2, size - 2);
+
+ // 5. Insert IFAC
+ memcpy(pkt + 2, ifac, IFAC_SIZE);
+
+ // 6. Set IFAC flag
+ pkt[0] |= 0x80;
+
+ // 7. Apply mask
+ // byte 0: XOR then force 0x80
+ pkt[0] = (pkt[0] ^ mask[0]) | 0x80;
+ // byte 1: XOR
+ pkt[1] ^= mask[1];
+ // bytes 2..9 (IFAC): NOT masked
+ // bytes 10+: XOR
+ for (uint16_t i = IFAC_SIZE + 2; i < new_size; i++) {
+ pkt[i] ^= mask[i];
+ }
+
+ return new_size;
+}
+
+#endif // HAS_GPS
+#endif // IFAC_AUTH_H
diff --git a/LxmfBeacon.h b/LxmfBeacon.h
new file mode 100644
index 00000000..9cd0db06
--- /dev/null
+++ b/LxmfBeacon.h
@@ -0,0 +1,957 @@
+// Copyright (C) 2026, LXMF beacon support contributed by GlassOnTin
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#ifndef LXMF_BEACON_H
+#define LXMF_BEACON_H
+
+#if HAS_GPS == true
+
+#include "sodium/crypto_sign_ed25519.h"
+#include "sodium/crypto_scalarmult_curve25519.h"
+#include "mbedtls/sha256.h"
+#include "esp_random.h"
+
+// NVS namespace for LXMF identity storage
+#define LXMF_NVS_NAMESPACE "lxmf"
+#define LXMF_NVS_KEY_SEED "ed_seed"
+#define LXMF_NVS_KEY_EDPUB "ed_pub"
+#define LXMF_NVS_KEY_TXID "transport_id"
+
+// LXMF identity state
+bool lxmf_identity_configured = false;
+uint8_t lxmf_ed25519_seed[32]; // Ed25519 private seed
+uint8_t lxmf_ed25519_pk[32]; // Ed25519 public key
+uint8_t lxmf_ed25519_sk[64]; // Ed25519 expanded secret key (seed+pk)
+uint8_t lxmf_x25519_pk[32]; // X25519 public key (derived from Ed25519)
+uint8_t lxmf_x25519_sk[32]; // X25519 private key (derived from Ed25519)
+uint8_t lxmf_identity_hash[16]; // SHA256(x25519_pk + ed25519_pk)[:16]
+uint8_t lxmf_source_hash[16]; // SHA256(name_hash("lxmf","delivery") + identity_hash)[:16]
+
+// Transport node identity for HEADER_2 routing
+bool transport_configured = false;
+uint8_t transport_id[16]; // Transport node's identity hash (16B)
+
+// Announce timing
+#define LXMF_ANNOUNCE_INTERVAL_MS 600000 // 10 minutes
+uint32_t lxmf_last_announce = 0;
+
+// Provisioning display feedback
+uint32_t lxmf_provisioned_at = 0; // millis() when last CMD_BCN_KEY received
+
+// Forward declarations
+void beacon_transmit(uint16_t size);
+void lora_receive();
+
+// ---- SHA-256 helpers ----
+
+static void sha256_once(const uint8_t *data, size_t len, uint8_t *out32) {
+ mbedtls_sha256_context ctx;
+ mbedtls_sha256_init(&ctx);
+ mbedtls_sha256_starts(&ctx, 0);
+ mbedtls_sha256_update(&ctx, data, len);
+ mbedtls_sha256_finish(&ctx, out32);
+ mbedtls_sha256_free(&ctx);
+}
+
+static void sha256_two(const uint8_t *a, size_t a_len,
+ const uint8_t *b, size_t b_len,
+ uint8_t *out32) {
+ mbedtls_sha256_context ctx;
+ mbedtls_sha256_init(&ctx);
+ mbedtls_sha256_starts(&ctx, 0);
+ mbedtls_sha256_update(&ctx, a, a_len);
+ mbedtls_sha256_update(&ctx, b, b_len);
+ mbedtls_sha256_finish(&ctx, out32);
+ mbedtls_sha256_free(&ctx);
+}
+
+// ---- RNS Identity Hash Computation ----
+// identity_hash = SHA256(x25519_pub(32) + ed25519_pub(32))[:16]
+
+static void compute_identity_hash(const uint8_t *x25519_pub, const uint8_t *ed25519_pub,
+ uint8_t *out16) {
+ uint8_t full[32];
+ sha256_two(x25519_pub, 32, ed25519_pub, 32, full);
+ memcpy(out16, full, 16);
+}
+
+// ---- RNS Destination Hash Computation ----
+// dest_hash = SHA256(name_hash + identity_hash)[:16]
+// where name_hash = SHA256(SHA256("lxmf") + SHA256("delivery"))[:10]
+
+static void compute_name_hash(const char *app, const char *aspect, uint8_t *out10) {
+ // RNS: name_hash = SHA256("app.aspect")[:10]
+ char full_name[64];
+ snprintf(full_name, sizeof(full_name), "%s.%s", app, aspect);
+ uint8_t hash[32];
+ sha256_once((const uint8_t*)full_name, strlen(full_name), hash);
+ memcpy(out10, hash, 10);
+}
+
+static void compute_dest_hash(const uint8_t *name_hash10, const uint8_t *identity_hash16,
+ uint8_t *out16) {
+ uint8_t full[32];
+ sha256_two(name_hash10, 10, identity_hash16, 16, full);
+ memcpy(out16, full, 16);
+}
+
+// ---- NVS Identity Storage ----
+
+#include "nvs_flash.h"
+#include "nvs.h"
+
+static bool lxmf_nvs_load_identity() {
+ nvs_handle_t handle;
+ if (nvs_open(LXMF_NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) return false;
+
+ size_t seed_len = 32, pub_len = 32;
+ bool ok = (nvs_get_blob(handle, LXMF_NVS_KEY_SEED, lxmf_ed25519_seed, &seed_len) == ESP_OK)
+ && (nvs_get_blob(handle, LXMF_NVS_KEY_EDPUB, lxmf_ed25519_pk, &pub_len) == ESP_OK)
+ && (seed_len == 32) && (pub_len == 32);
+
+ nvs_close(handle);
+ return ok;
+}
+
+static bool lxmf_nvs_save_identity() {
+ nvs_handle_t handle;
+ if (nvs_open(LXMF_NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) return false;
+
+ bool ok = (nvs_set_blob(handle, LXMF_NVS_KEY_SEED, lxmf_ed25519_seed, 32) == ESP_OK)
+ && (nvs_set_blob(handle, LXMF_NVS_KEY_EDPUB, lxmf_ed25519_pk, 32) == ESP_OK)
+ && (nvs_commit(handle) == ESP_OK);
+
+ nvs_close(handle);
+ return ok;
+}
+
+static bool lxmf_nvs_load_transport_id() {
+ nvs_handle_t handle;
+ if (nvs_open(LXMF_NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) return false;
+ size_t len = 16;
+ bool ok = (nvs_get_blob(handle, LXMF_NVS_KEY_TXID, transport_id, &len) == ESP_OK) && (len == 16);
+ nvs_close(handle);
+ return ok;
+}
+
+static bool lxmf_nvs_save_transport_id() {
+ nvs_handle_t handle;
+ if (nvs_open(LXMF_NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) return false;
+ bool ok = (nvs_set_blob(handle, LXMF_NVS_KEY_TXID, transport_id, 16) == ESP_OK)
+ && (nvs_commit(handle) == ESP_OK);
+ nvs_close(handle);
+ return ok;
+}
+
+// ---- Identity Initialization ----
+// Call from setup(). Loads or generates Ed25519 keypair, derives X25519 keys,
+// computes identity_hash and source_hash (LXMF delivery destination).
+
+static void lxmf_init_identity() {
+ bool loaded = lxmf_nvs_load_identity();
+
+ if (!loaded) {
+ // Generate new Ed25519 keypair
+ crypto_sign_ed25519_keypair(lxmf_ed25519_pk, lxmf_ed25519_sk);
+ // Extract seed from sk (first 32 bytes of libsodium's 64-byte sk)
+ memcpy(lxmf_ed25519_seed, lxmf_ed25519_sk, 32);
+ lxmf_nvs_save_identity();
+ } else {
+ // Validate NVS: regenerate pk from seed and compare
+ uint8_t verify_pk[32], verify_sk[64];
+ crypto_sign_ed25519_seed_keypair(verify_pk, verify_sk, lxmf_ed25519_seed);
+ if (memcmp(verify_pk, lxmf_ed25519_pk, 32) != 0) {
+ // NVS corruption — regenerate and save
+ memcpy(lxmf_ed25519_pk, verify_pk, 32);
+ memcpy(lxmf_ed25519_sk, verify_sk, 64);
+ lxmf_nvs_save_identity();
+ } else {
+ // Reconstruct expanded secret key from seed
+ // libsodium ed25519 sk = seed(32) + pk(32)
+ memcpy(lxmf_ed25519_sk, lxmf_ed25519_seed, 32);
+ memcpy(lxmf_ed25519_sk + 32, lxmf_ed25519_pk, 32);
+ }
+ }
+
+ // Derive X25519 keys from Ed25519
+ crypto_sign_ed25519_pk_to_curve25519(lxmf_x25519_pk, lxmf_ed25519_pk);
+ crypto_sign_ed25519_sk_to_curve25519(lxmf_x25519_sk, lxmf_ed25519_sk);
+
+ // Compute identity hash: SHA256(x25519_pub + ed25519_pub)[:16]
+ compute_identity_hash(lxmf_x25519_pk, lxmf_ed25519_pk, lxmf_identity_hash);
+
+ // Compute source hash (LXMF delivery destination hash)
+ uint8_t name_hash[10];
+ compute_name_hash("lxmf", "delivery", name_hash);
+ compute_dest_hash(name_hash, lxmf_identity_hash, lxmf_source_hash);
+
+ lxmf_identity_configured = true;
+
+ // Load transport node identity (for HEADER_2 routing)
+ transport_configured = lxmf_nvs_load_transport_id();
+}
+
+// ---- Minimal Msgpack Encoder ----
+// Fixed-schema packer: no dynamic allocation, writes directly to output buffer.
+
+struct MsgpackWriter {
+ uint8_t *buf;
+ size_t pos;
+ size_t cap;
+
+ bool ok() const { return pos <= cap; }
+
+ void write_byte(uint8_t b) {
+ if (pos < cap) buf[pos] = b;
+ pos++;
+ }
+
+ void write_bytes(const uint8_t *data, size_t len) {
+ for (size_t i = 0; i < len; i++) write_byte(data[i]);
+ }
+
+ // msgpack fixint (0-127)
+ void pack_uint7(uint8_t v) { write_byte(v & 0x7f); }
+
+ // msgpack uint8
+ void pack_uint8(uint8_t v) { write_byte(0xcc); write_byte(v); }
+
+ // msgpack uint16
+ void pack_uint16(uint16_t v) {
+ write_byte(0xcd);
+ write_byte((v >> 8) & 0xff);
+ write_byte(v & 0xff);
+ }
+
+ // msgpack uint32
+ void pack_uint32(uint32_t v) {
+ write_byte(0xce);
+ write_byte((v >> 24) & 0xff);
+ write_byte((v >> 16) & 0xff);
+ write_byte((v >> 8) & 0xff);
+ write_byte(v & 0xff);
+ }
+
+ // msgpack float64
+ void pack_float64(double v) {
+ write_byte(0xcb);
+ union { double d; uint8_t b[8]; } u;
+ u.d = v;
+ // IEEE 754 big-endian
+ for (int i = 7; i >= 0; i--) write_byte(u.b[i]);
+ }
+
+ // msgpack bin8 (up to 255 bytes)
+ void pack_bin8(const uint8_t *data, uint8_t len) {
+ write_byte(0xc4);
+ write_byte(len);
+ if (len > 0 && data != NULL) write_bytes(data, len);
+ }
+
+ // msgpack fixstr (up to 31 bytes)
+ void pack_fixstr(const char *s, uint8_t len) {
+ write_byte(0xa0 | (len & 0x1f));
+ write_bytes((const uint8_t*)s, len);
+ }
+
+ // msgpack empty string
+ void pack_empty_str() { write_byte(0xa0); }
+
+ // msgpack fixarray header (up to 15 elements)
+ void pack_fixarray(uint8_t n) { write_byte(0x90 | (n & 0x0f)); }
+
+ // msgpack fixmap header (up to 15 entries)
+ void pack_fixmap(uint8_t n) { write_byte(0x80 | (n & 0x0f)); }
+
+ // msgpack false
+ void pack_false() { write_byte(0xc2); }
+
+ // msgpack nil
+ void pack_nil() { write_byte(0xc0); }
+};
+
+// ---- Sideband Telemetry Packing ----
+// Produces Sideband-compatible FIELD_TELEMETRY bytes (Telemeter.packed() format).
+// Format: msgpack map { SID_TIME(0x01): unix_ts, SID_LOCATION(0x02): [...], SID_BATTERY(0x04): [...] }
+//
+// Size budget: LoRa max payload = 255 bytes.
+// With HEADER_2(35) + IFAC(8) + crypto overhead(80) + PKCS7 padding,
+// LXMF plaintext must be ≤ 127 bytes (pads to 128).
+// Fixed: source_hash(16) + signature(64) + payload wrapper(~14) = 94.
+// Max telemetry = 127 - 94 = 33 bytes.
+//
+// Sideband Location.unpack() builds a dict from ALL 7 array elements at once.
+// Short arrays cause IndexError → unpack returns None → no location displayed.
+// We send only 3 elements (lat, lon, alt) to fit the budget, and patch
+// Sideband's Location.unpack() on the phone to handle short arrays.
+//
+// Actual: SID_TIME(6) + SID_LOCATION(22) + SID_BATTERY(5) + map hdr(1) = 34 max.
+// Without battery: 1 + 6 + 22 = 29 bytes.
+
+static size_t lxmf_pack_telemetry(uint8_t *out, size_t out_cap,
+ double lat, double lon, double alt,
+ double speed, double hdop,
+ uint32_t timestamp, int bat_percent) {
+ MsgpackWriter w = { out, 0, out_cap };
+
+ bool has_bat = (bat_percent > 0);
+ w.pack_fixmap(has_bat ? 3 : 2); // SID_TIME + SID_LOCATION [+ SID_BATTERY]
+
+ // SID_TIME = 0x01 → uint32 Unix timestamp (REQUIRED by Sideband)
+ w.pack_uint7(0x01);
+ w.pack_uint32(timestamp);
+
+ // SID_LOCATION = 0x02 → fixarray[3] [lat, lon, alt]
+ // (Sideband patched to handle short arrays; see patch_sideband_location.py)
+ w.pack_uint7(0x02);
+ w.pack_fixarray(3);
+
+ // [0] lat: bin4 (struct.pack("!i", lat*1e6))
+ int32_t lat_i = (int32_t)round(lat * 1e6);
+ uint8_t lat_b[4] = {
+ (uint8_t)((lat_i >> 24) & 0xff), (uint8_t)((lat_i >> 16) & 0xff),
+ (uint8_t)((lat_i >> 8) & 0xff), (uint8_t)(lat_i & 0xff)
+ };
+ w.pack_bin8(lat_b, 4);
+
+ // [1] lon: bin4 (struct.pack("!i", lon*1e6))
+ int32_t lon_i = (int32_t)round(lon * 1e6);
+ uint8_t lon_b[4] = {
+ (uint8_t)((lon_i >> 24) & 0xff), (uint8_t)((lon_i >> 16) & 0xff),
+ (uint8_t)((lon_i >> 8) & 0xff), (uint8_t)(lon_i & 0xff)
+ };
+ w.pack_bin8(lon_b, 4);
+
+ // [2] alt: bin4 (struct.pack("!i", alt*1e2))
+ int32_t alt_i = (int32_t)round(alt * 1e2);
+ uint8_t alt_b[4] = {
+ (uint8_t)((alt_i >> 24) & 0xff), (uint8_t)((alt_i >> 16) & 0xff),
+ (uint8_t)((alt_i >> 8) & 0xff), (uint8_t)(alt_i & 0xff)
+ };
+ w.pack_bin8(alt_b, 4);
+
+ // SID_BATTERY = 0x04 → fixarray[3] [pct, charging, temperature]
+ if (has_bat) {
+ w.pack_uint7(0x04);
+ w.pack_fixarray(3);
+ w.pack_uint7((uint8_t)(bat_percent > 100 ? 100 : bat_percent));
+ w.pack_false(); // not charging
+ w.pack_nil(); // temperature unknown
+ }
+
+ if (!w.ok()) return 0;
+ return w.pos;
+}
+
+// ---- LXMF Message Construction ----
+// Builds the LXMF plaintext (before RNS encryption).
+//
+// LXMF packed format for OPPORTUNISTIC delivery:
+// source_hash(16) + signature(64) + msgpack_payload
+// (dest_hash is omitted; RNS header carries it)
+//
+// msgpack_payload = [timestamp_f64, title_str, content_str, fields_map]
+// fields_map = { 0x02: telemetry_bytes } (FIELD_TELEMETRY = 0x02 in LXMF)
+//
+// Signing (from LXMF LXMessage.pack()):
+// hashed_part = dest_hash + source_hash + msgpack_payload
+// message_hash = SHA256(hashed_part)
+// signed_part = hashed_part + message_hash
+// signature = Ed25519Sign(signed_part, ed25519_sk)
+
+static int lxmf_build_message(uint8_t *out, size_t out_cap,
+ const uint8_t *dest_hash16,
+ double lat, double lon, double alt,
+ double speed, double hdop,
+ uint32_t timestamp, int bat_percent) {
+ // Static buffers to avoid stack overflow (~1KB saved)
+ // Safe because beacon functions are never called concurrently
+ static uint8_t telemetry[128];
+ static uint8_t payload[256];
+ static uint8_t hashed_part[256 + 32];
+ static uint8_t signed_part[256 + 32 + 32];
+
+ // 1. Pack telemetry bytes
+ size_t telem_len = lxmf_pack_telemetry(telemetry, sizeof(telemetry),
+ lat, lon, alt, speed, hdop,
+ timestamp, bat_percent);
+ if (telem_len == 0) return -1;
+
+ // 2. Build msgpack payload: [timestamp, nil, nil, {0x02: telemetry}]
+ MsgpackWriter pw = { payload, 0, sizeof(payload) };
+
+ pw.pack_fixarray(4);
+
+ // timestamp as uint32 (saves 4 bytes vs float64)
+ pw.pack_uint32(timestamp);
+
+ // empty title and content as bin8(0) — must not be nil,
+ // Sideband calls len(content) which fails on None
+ pw.pack_bin8(NULL, 0);
+ pw.pack_bin8(NULL, 0);
+
+ // fields: {FIELD_TELEMETRY(0x02): telemetry_bytes}
+ pw.pack_fixmap(1);
+ pw.pack_uint7(0x02);
+ pw.pack_bin8(telemetry, (uint8_t)telem_len);
+
+ if (!pw.ok()) return -1;
+ size_t payload_len = pw.pos;
+
+ // 3. Compute signature
+ // hashed_part = dest_hash(16) + source_hash(16) + payload
+ size_t hp_len = 16 + 16 + payload_len;
+ if (hp_len > sizeof(hashed_part)) return -1;
+ memcpy(hashed_part, dest_hash16, 16);
+ memcpy(hashed_part + 16, lxmf_source_hash, 16);
+ memcpy(hashed_part + 32, payload, payload_len);
+
+ // message_hash = SHA256(hashed_part) (RNS.Identity.full_hash is single SHA256)
+ uint8_t message_hash[32];
+ sha256_once(hashed_part, hp_len, message_hash);
+
+ // signed_part = hashed_part + message_hash
+ size_t sp_len = hp_len + 32;
+ if (sp_len > sizeof(signed_part)) return -1;
+ memcpy(signed_part, hashed_part, hp_len);
+ memcpy(signed_part + hp_len, message_hash, 32);
+
+ // signature = Ed25519Sign(signed_part)
+ uint8_t signature[64];
+ unsigned long long sig_len_unused;
+ crypto_sign_ed25519_detached(signature, &sig_len_unused,
+ signed_part, sp_len, lxmf_ed25519_sk);
+
+ // 4. Assemble LXMF wire format for OPPORTUNISTIC:
+ // source_hash(16) + signature(64) + payload
+ size_t total = 16 + 64 + payload_len;
+ if (total > out_cap) return -1;
+
+ memcpy(out, lxmf_source_hash, 16);
+ memcpy(out + 16, signature, 64);
+ memcpy(out + 80, payload, payload_len);
+
+ return (int)total;
+}
+
+// ---- Minimal Msgpack Reader ----
+// Read-only cursor over msgpack data. No allocation.
+struct MsgpackReader {
+ const uint8_t *buf;
+ size_t pos;
+ size_t len;
+
+ MsgpackReader(const uint8_t *data, size_t sz) : buf(data), pos(0), len(sz) {}
+ bool has(size_t n) const { return pos + n <= len; }
+ uint8_t peek() const { return has(1) ? buf[pos] : 0xFF; }
+ uint8_t read_u8() { return has(1) ? buf[pos++] : 0; }
+
+ // Read fixarray length (0x90-0x9F)
+ int read_array_len() {
+ uint8_t b = read_u8();
+ if ((b & 0xF0) == 0x90) return b & 0x0F;
+ if (b == 0xDC && has(2)) { uint16_t n = (buf[pos]<<8)|buf[pos+1]; pos+=2; return n; }
+ return -1;
+ }
+
+ // Read uint (fixint, uint8, uint16, uint32)
+ uint32_t read_uint() {
+ uint8_t b = read_u8();
+ if (b <= 0x7F) return b;
+ if (b == 0xCC && has(1)) return buf[pos++];
+ if (b == 0xCD && has(2)) { uint16_t v=(buf[pos]<<8)|buf[pos+1]; pos+=2; return v; }
+ if (b == 0xCE && has(4)) { uint32_t v=(buf[pos]<<24)|(buf[pos+1]<<16)|(buf[pos+2]<<8)|buf[pos+3]; pos+=4; return v; }
+ return 0;
+ }
+
+ // Read binary/string, returns pointer and sets length. Doesn't copy.
+ const uint8_t *read_bin(size_t *out_len) {
+ uint8_t b = read_u8();
+ size_t n = 0;
+ if ((b & 0xE0) == 0xA0) n = b & 0x1F; // fixstr
+ else if (b == 0xC4 && has(1)) n = buf[pos++]; // bin8
+ else if (b == 0xC5 && has(2)) { n=(buf[pos]<<8)|buf[pos+1]; pos+=2; } // bin16
+ else if (b == 0xD9 && has(1)) n = buf[pos++]; // str8
+ else { *out_len = 0; return NULL; }
+ if (!has(n)) { *out_len = 0; return NULL; }
+ const uint8_t *ptr = buf + pos;
+ pos += n;
+ *out_len = n;
+ return ptr;
+ }
+
+ // Skip one msgpack value
+ void skip() {
+ uint8_t b = peek();
+ if (b <= 0x7F || b >= 0xE0 || b == 0xC0 || b == 0xC2 || b == 0xC3) { pos++; return; }
+ if ((b & 0xF0) == 0x90) { int n = read_array_len(); for (int i=0;isource_hash, plaintext, 16);
+ // signature at plaintext+16, 64 bytes (verified later with sender's pubkey)
+ const uint8_t *msgpack_data = plaintext + 16 + 64;
+ size_t msgpack_len = pt_len - 16 - 64;
+
+ MsgpackReader r(msgpack_data, msgpack_len);
+
+ // Expect fixarray(4): [timestamp, title, content, fields]
+ int arr_len = r.read_array_len();
+ if (arr_len < 3) return false;
+
+ // [0] timestamp
+ out->timestamp = r.read_uint();
+
+ // [1] title
+ size_t title_len = 0;
+ const uint8_t *title_ptr = r.read_bin(&title_len);
+ if (title_ptr && title_len > 0) {
+ size_t n = title_len < sizeof(out->title)-1 ? title_len : sizeof(out->title)-1;
+ memcpy(out->title, title_ptr, n);
+ out->title[n] = '\0';
+ } else {
+ out->title[0] = '\0';
+ }
+
+ // [2] content
+ size_t content_len = 0;
+ const uint8_t *content_ptr = r.read_bin(&content_len);
+ if (content_ptr && content_len > 0) {
+ size_t n = content_len < sizeof(out->content)-1 ? content_len : sizeof(out->content)-1;
+ memcpy(out->content, content_ptr, n);
+ out->content[n] = '\0';
+ } else {
+ out->content[0] = '\0';
+ }
+
+ out->verified = false; // caller must verify with sender's pubkey
+ return true;
+}
+
+// Verify LXMF signature using sender's Ed25519 public key
+static bool lxmf_verify_signature(const uint8_t *plaintext, size_t pt_len,
+ const uint8_t *sender_ed25519_pub) {
+ if (pt_len < 16 + 64 + 4) return false;
+ const uint8_t *signature = plaintext + 16;
+ const uint8_t *msgpack_data = plaintext + 16 + 64;
+ size_t msgpack_len = pt_len - 16 - 64;
+
+ // Reconstruct signed_part: dest_hash + source_hash + msgpack + SHA256(same)
+ // dest_hash = our lxmf_source_hash, source_hash = plaintext[0:16]
+ static uint8_t hashed_part[512];
+ size_t hp_len = 16 + 16 + msgpack_len;
+ if (hp_len > sizeof(hashed_part) - 32) return false;
+
+ memcpy(hashed_part, lxmf_source_hash, 16); // dest = us
+ memcpy(hashed_part + 16, plaintext, 16); // source
+ memcpy(hashed_part + 32, msgpack_data, msgpack_len);
+
+ uint8_t message_hash[32];
+ sha256_once(hashed_part, hp_len, message_hash);
+
+ static uint8_t signed_part[512 + 32];
+ memcpy(signed_part, hashed_part, hp_len);
+ memcpy(signed_part + hp_len, message_hash, 32);
+ size_t sp_len = hp_len + 32;
+
+ return crypto_sign_ed25519_verify_detached(signature, signed_part, sp_len,
+ sender_ed25519_pub) == 0;
+}
+
+// ---- RNS Announce Packet Construction ----
+// Builds a complete RNS announce packet in tbuf for transmission.
+//
+// RNS header: flags(1) + hops(1) + dest_hash(16) + context(1) = 19 bytes
+// Announce payload:
+// public_key(64): x25519_pub + ed25519_pub
+// name_hash(10): computed for "lxmf.delivery"
+// random_hash(10): random bytes
+// signature(64): Ed25519Sign(dest_hash + public_key + name_hash + random_hash + app_data)
+// app_data: msgpack string with display name
+
+static int lxmf_build_announce(uint8_t *out, size_t out_cap, const char *display_name) {
+ // Announce FLAGS: header_type=0 (HEADER_1), propagation=0 (BROADCAST),
+ // destination=0 (SINGLE), packet_type=1 (ANNOUNCE), transport=0
+ // Bits: [header_type:2][propagation_type:2][destination_type:2][packet_type:2]
+ // ANNOUNCE packet_type = 1 → 0x01 in low 2 bits
+ // But RNS packs: ifac_flag(1) | header_type(1) | propagation_type(2) | destination_type(1) | packet_type(1) | transport_type(1) | context_flag(1)
+ // Wait, let me use the correct bit layout from RNS:
+ // header byte = (ifac_flag << 7) | (header_type << 6) | (propagation << 4) | (destination << 2) | (packet_type) | transport bit
+ // For announce: ifac=0, header_type=0(HEADER_1), propagation=0(BROADCAST),
+ // destination=0(SINGLE), packet_type=1(ANNOUNCE), transport=0
+ // = 0b00000010 = 0x02
+ // But context_flag is separate: context byte for announce = 0x00 (CONTEXT_NONE)
+
+ // Actually the RNS header is:
+ // byte 0: [ifac_flag:1][header_type:1][propagation_type:2][destination_type:2][packet_type:2]
+ // For HEADER_1 + BROADCAST + SINGLE + ANNOUNCE:
+ // = 0b00_00_00_01 = 0x01
+ out[0] = 0x01; // FLAGS: HEADER_1, BROADCAST, SINGLE, ANNOUNCE
+ out[1] = 0x00; // HOPS
+
+ // dest_hash for our LXMF delivery destination
+ memcpy(&out[2], lxmf_source_hash, 16);
+
+ out[18] = 0x00; // CONTEXT_NONE
+
+ size_t pos = 19;
+
+ // public_key: x25519_pub(32) + ed25519_pub(32)
+ memcpy(&out[pos], lxmf_x25519_pk, 32); pos += 32;
+ memcpy(&out[pos], lxmf_ed25519_pk, 32); pos += 32;
+
+ // name_hash: SHA256("lxmf.delivery")[:10]
+ uint8_t name_hash[10];
+ compute_name_hash("lxmf", "delivery", name_hash);
+ memcpy(&out[pos], name_hash, 10); pos += 10;
+
+ // random_hash: 5 random bytes + 5-byte big-endian Unix timestamp
+ // RNS uses random_hash[5:10] as an "announce emitted" timebase
+ // for ordering announces in the transport node's path table.
+ uint8_t random_hash[10];
+ esp_fill_random(random_hash, 5);
+ uint32_t now_sec = 0;
+ #if HAS_GPS == true
+ // Use GPS time directly (always available when beaconing since gps_has_fix is checked)
+ extern TinyGPSPlus gps_parser;
+ if (gps_parser.date.isValid() && gps_parser.time.isValid() && gps_parser.date.year() >= 2024) {
+ uint32_t days = 0;
+ uint16_t yr = gps_parser.date.year();
+ uint8_t mo = gps_parser.date.month();
+ uint8_t dy = gps_parser.date.day();
+ for (uint16_t y = 1970; y < yr; y++)
+ days += (y % 4 == 0) ? 366 : 365;
+ static const uint16_t mdays[] = {0,31,59,90,120,151,181,212,243,273,304,334};
+ if (mo >= 1 && mo <= 12) {
+ days += mdays[mo - 1];
+ if (mo > 2 && (yr % 4 == 0)) days++;
+ }
+ days += dy - 1;
+ now_sec = days * 86400UL + gps_parser.time.hour() * 3600UL
+ + gps_parser.time.minute() * 60UL + gps_parser.time.second();
+ } else {
+ now_sec = (uint32_t)(millis() / 1000);
+ }
+ #else
+ now_sec = (uint32_t)(millis() / 1000);
+ #endif
+ // 5-byte big-endian timestamp (40-bit, fits Unix time for centuries)
+ random_hash[5] = 0; // high byte always 0 for current era
+ random_hash[6] = (uint8_t)(now_sec >> 24);
+ random_hash[7] = (uint8_t)(now_sec >> 16);
+ random_hash[8] = (uint8_t)(now_sec >> 8);
+ random_hash[9] = (uint8_t)(now_sec);
+ memcpy(&out[pos], random_hash, 10); pos += 10;
+
+ // app_data: raw UTF-8 display name (no msgpack wrapping —
+ // Sideband's display_name_from_app_data() decodes as plain UTF-8)
+ size_t name_len = strlen(display_name);
+ uint8_t app_data[48];
+ size_t app_data_len = name_len;
+ memcpy(app_data, display_name, name_len);
+
+ // signature: Ed25519Sign(dest_hash + public_key + name_hash + random_hash + app_data)
+ // signed_data = dest_hash(16) + public_key(64) + name_hash(10) + random_hash(10) + app_data
+ static uint8_t signed_data[256];
+ size_t sd_len = 0;
+ memcpy(&signed_data[sd_len], lxmf_source_hash, 16); sd_len += 16;
+ memcpy(&signed_data[sd_len], lxmf_x25519_pk, 32); sd_len += 32;
+ memcpy(&signed_data[sd_len], lxmf_ed25519_pk, 32); sd_len += 32;
+ memcpy(&signed_data[sd_len], name_hash, 10); sd_len += 10;
+ memcpy(&signed_data[sd_len], random_hash, 10); sd_len += 10;
+ memcpy(&signed_data[sd_len], app_data, app_data_len); sd_len += app_data_len;
+
+ uint8_t signature[64];
+ unsigned long long sig_len_unused;
+ crypto_sign_ed25519_detached(signature, &sig_len_unused,
+ signed_data, sd_len, lxmf_ed25519_sk);
+
+ memcpy(&out[pos], signature, 64); pos += 64;
+
+ // app_data
+ memcpy(&out[pos], app_data, app_data_len); pos += app_data_len;
+
+ if (pos > out_cap) return -1;
+ return (int)pos;
+}
+
+// ---- Public API ----
+
+// Build and transmit an LXMF telemetry beacon.
+// The LXMF message is encrypted as an RNS SINGLE packet to collector_dest_hash
+// using the same ECDH encryption pipeline from BeaconCrypto.h.
+static void lxmf_beacon_send(double lat, double lon, double alt,
+ double speed, double hdop,
+ uint32_t timestamp, int bat_percent) {
+ if (!lxmf_identity_configured || !beacon_crypto_configured) return;
+
+ // Build LXMF message plaintext
+ uint8_t lxmf_msg[300];
+ int msg_len = lxmf_build_message(lxmf_msg, sizeof(lxmf_msg),
+ collector_dest_hash,
+ lat, lon, alt, speed, hdop,
+ timestamp, bat_percent);
+ if (msg_len <= 0) return;
+
+ // RNS packet: HEADER_2 is required — transport nodes only forward
+ // packets with explicit transport_id. HEADER_1 packets are silently
+ // dropped for transport purposes.
+ int hdr_len;
+ if (transport_configured) {
+ // HEADER_2: flags + hops + transport_id(16) + dest_hash(16) + context
+ tbuf[0] = 0x50; // HEADER_2(1<<6) | TRANSPORT(1<<4) | SINGLE(0) | DATA(0)
+ tbuf[1] = 0x00; // HOPS
+ memcpy(&tbuf[2], transport_id, 16);
+ memcpy(&tbuf[18], collector_dest_hash, 16);
+ tbuf[34] = 0x00; // CONTEXT_NONE
+ hdr_len = 35;
+ } else {
+ // HEADER_1: flags + hops + dest_hash(16) + context
+ tbuf[0] = 0x00; // HEADER_1, BROADCAST, SINGLE, DATA
+ tbuf[1] = 0x00; // HOPS
+ memcpy(&tbuf[2], collector_dest_hash, 16);
+ tbuf[18] = 0x00; // CONTEXT_NONE
+ hdr_len = 19;
+ }
+
+ int crypto_len = beacon_crypto_encrypt(
+ lxmf_msg, msg_len,
+ collector_pub_key, collector_identity_hash,
+ &tbuf[hdr_len]
+ );
+
+ // LoRa FIFO limit is 255 bytes. After IFAC (+8 bytes), the packet
+ // must still fit. Check against 247 (255 - IFAC_SIZE) to prevent
+ // silent truncation that corrupts the IFAC signature.
+ int pre_ifac_size = hdr_len + crypto_len;
+ int max_lora = 255 - 8; // IFAC adds 8 bytes
+ if (crypto_len > 0 && pre_ifac_size <= max_lora && pre_ifac_size <= (int)MTU) {
+ beacon_transmit(pre_ifac_size);
+ lora_receive();
+ }
+}
+
+// Build and transmit an LXMF announce packet.
+static void lxmf_announce_send(const char *display_name) {
+ if (!lxmf_identity_configured) return;
+
+ int pkt_len = lxmf_build_announce(tbuf, MTU, display_name);
+ if (pkt_len > 0) {
+ beacon_transmit(pkt_len);
+ lora_receive();
+ }
+}
+
+// Check if announce is due and send it.
+static void lxmf_announce_if_needed(const char *display_name) {
+ if (!lxmf_identity_configured) return;
+
+ if (lxmf_last_announce == 0 ||
+ (millis() - lxmf_last_announce >= LXMF_ANNOUNCE_INTERVAL_MS)) {
+ lxmf_announce_send(display_name);
+ lxmf_last_announce = millis();
+ }
+}
+
+// ---- CMD_LXMF_TEST: Force-trigger announce + beacon for USB testing ----
+// Emits pre-encryption plaintext as CMD_DIAG KISS frames over serial,
+// then transmits encrypted packets over LoRa.
+
+// KISS framing constants (from Framing.h, repeated here to avoid include-order issues)
+#ifndef LXMF_KISS_FEND
+#define LXMF_KISS_FEND 0xC0
+#define LXMF_KISS_FESC 0xDB
+#define LXMF_KISS_TFEND 0xDC
+#define LXMF_KISS_TFESC 0xDD
+#define LXMF_CMD_DIAG 0x2C
+#endif
+
+// Forward declarations from main firmware
+void serial_write(uint8_t byte);
+bool startRadio();
+void setTXPower();
+void setBandwidth();
+void setSpreadingFactor();
+void setCodingRate();
+
+static void kiss_emit_diag(const uint8_t *data, size_t len) {
+ serial_write(LXMF_KISS_FEND);
+ serial_write(LXMF_CMD_DIAG);
+ for (size_t i = 0; i < len; i++) {
+ uint8_t b = data[i];
+ if (b == LXMF_KISS_FEND) { serial_write(LXMF_KISS_FESC); serial_write(LXMF_KISS_TFEND); }
+ else if (b == LXMF_KISS_FESC) { serial_write(LXMF_KISS_FESC); serial_write(LXMF_KISS_TFESC); }
+ else serial_write(b);
+ }
+ serial_write(LXMF_KISS_FEND);
+}
+
+// Beacon radio parameters (from Beacon.h, repeated to avoid include-order dependency)
+#ifndef LXMF_BEACON_RADIO_FREQ
+#define LXMF_BEACON_RADIO_FREQ 868000000
+#define LXMF_BEACON_RADIO_BW 125000
+#define LXMF_BEACON_RADIO_SF 7
+#define LXMF_BEACON_RADIO_CR 5
+#define LXMF_BEACON_RADIO_TXP 17
+#endif
+
+static void lxmf_test_send() {
+ if (!lxmf_identity_configured) return;
+
+ // Save current LoRa params
+ uint32_t save_freq = lora_freq;
+ uint32_t save_bw = lora_bw;
+ int save_sf = lora_sf;
+ int save_cr = lora_cr;
+ int save_txp = lora_txp;
+
+ // Set beacon LoRa params
+ lora_freq = (uint32_t)LXMF_BEACON_RADIO_FREQ;
+ lora_bw = (uint32_t)LXMF_BEACON_RADIO_BW;
+ lora_sf = LXMF_BEACON_RADIO_SF;
+ lora_cr = LXMF_BEACON_RADIO_CR;
+ lora_txp = LXMF_BEACON_RADIO_TXP;
+
+ if (!radio_online) {
+ startRadio();
+ }
+ if (radio_online) {
+ setTXPower();
+ setBandwidth();
+ setSpreadingFactor();
+ setCodingRate();
+ }
+
+ // 1. Build and emit announce
+ int ann_len = lxmf_build_announce(tbuf, MTU, "RNode GPS Tracker");
+ if (ann_len > 0) {
+ // Emit announce plaintext as CMD_DIAG (pre-IFAC)
+ kiss_emit_diag(tbuf, ann_len);
+ // Transmit over LoRa (beacon_transmit applies IFAC)
+ if (radio_online) {
+ beacon_transmit(ann_len);
+ }
+ }
+
+ // 2. Build and emit LXMF beacon
+ if (beacon_crypto_configured) {
+ // Get Unix timestamp from GPS (always available when beaconing)
+ uint32_t timestamp = (uint32_t)(millis() / 1000);
+ #if HAS_GPS == true
+ {
+ extern TinyGPSPlus gps_parser;
+ if (gps_parser.date.isValid() && gps_parser.time.isValid() && gps_parser.date.year() >= 2024) {
+ uint32_t days = 0;
+ uint16_t yr = gps_parser.date.year();
+ uint8_t mo = gps_parser.date.month();
+ for (uint16_t y = 1970; y < yr; y++)
+ days += (y % 4 == 0) ? 366 : 365;
+ static const uint16_t mdays[] = {0,31,59,90,120,151,181,212,243,273,304,334};
+ if (mo >= 1 && mo <= 12) {
+ days += mdays[mo - 1];
+ if (mo > 2 && (yr % 4 == 0)) days++;
+ }
+ days += gps_parser.date.day() - 1;
+ timestamp = days * 86400UL + gps_parser.time.hour() * 3600UL
+ + gps_parser.time.minute() * 60UL + gps_parser.time.second();
+ }
+ }
+ #endif
+
+ // Build LXMF message plaintext
+ uint8_t lxmf_msg[300];
+ int msg_len = lxmf_build_message(lxmf_msg, sizeof(lxmf_msg),
+ collector_dest_hash,
+ gps_lat, gps_lon, gps_alt,
+ gps_speed, gps_hdop,
+ timestamp, (int)battery_percent);
+
+ if (msg_len > 0) {
+ // Emit LXMF plaintext as CMD_DIAG (pre-encryption)
+ kiss_emit_diag(lxmf_msg, msg_len);
+
+ // Build encrypted RNS packet and transmit
+ int hdr_len;
+ if (transport_configured) {
+ tbuf[0] = 0x50; // HEADER_2 | TRANSPORT | SINGLE | DATA
+ tbuf[1] = 0x00;
+ memcpy(&tbuf[2], transport_id, 16);
+ memcpy(&tbuf[18], collector_dest_hash, 16);
+ tbuf[34] = 0x00;
+ hdr_len = 35;
+ } else {
+ tbuf[0] = 0x00; // HEADER_1 | BROADCAST | SINGLE | DATA
+ tbuf[1] = 0x00;
+ memcpy(&tbuf[2], collector_dest_hash, 16);
+ tbuf[18] = 0x00;
+ hdr_len = 19;
+ }
+
+ int crypto_len = beacon_crypto_encrypt(
+ lxmf_msg, msg_len,
+ collector_pub_key, collector_identity_hash,
+ &tbuf[hdr_len]
+ );
+
+ if (crypto_len > 0 && (hdr_len + crypto_len) <= (int)MTU && radio_online) {
+ beacon_transmit(hdr_len + crypto_len);
+ }
+ }
+ }
+
+ // Restore LoRa params
+ lora_freq = save_freq;
+ lora_bw = save_bw;
+ lora_sf = save_sf;
+ lora_cr = save_cr;
+ lora_txp = save_txp;
+
+ if (radio_online) {
+ setTXPower();
+ setBandwidth();
+ setSpreadingFactor();
+ setCodingRate();
+ }
+
+ lora_receive();
+}
+
+#endif // HAS_GPS
+#endif // LXMF_BEACON_H
diff --git a/Makefile b/Makefile
index 8ba24a9e..947c20a6 100644
--- a/Makefile
+++ b/Makefile
@@ -19,12 +19,27 @@ ARDUINO_ESP_CORE_VER = 2.0.17
# Version 3.2.0 of the Arduino ESP core is based on ESP-IDF v5.4.1
# ARDUINO_ESP_CORE_VER = 3.2.0
+PORT ?= /dev/ttyACM4
+TWATCH_PORT ?= /dev/ttyACM0
+TBEAM_PORT ?= /dev/ttyACM5
+
all: release
clean:
-rm -r ./build
-rm ./Release/rnode_firmware*
+# Stop any process holding the serial port (e.g. rnsd)
+free-port:
+ @if fuser $(PORT) >/dev/null 2>&1; then \
+ echo "Stopping process on $(PORT)..."; \
+ kill $$(fuser $(PORT) 2>/dev/null | tr -d ' :') 2>/dev/null; \
+ sleep 2; \
+ fi
+
+test-kiss: free-port
+ @python3 test_kiss.py $(PORT)
+
prep: prep-avr prep-esp32 prep-samd
prep-avr:
@@ -41,6 +56,8 @@ prep-esp32:
arduino-cli lib install "Adafruit NeoPixel"
arduino-cli lib install "XPowersLib"
arduino-cli lib install "Crypto"
+ arduino-cli lib install "TinyGPSPlus"
+ arduino-cli lib install "lvgl@9.5.0"
prep-samd:
arduino-cli core update-index --config-file arduino-cli.yaml
@@ -95,8 +112,53 @@ firmware-t3s3_sx1280_pa:
firmware-tdeck:
arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3B\""
+# T-Beam Supreme: separate build dir to avoid cross-contamination with T-Watch
+TBEAM_BUILD = build/tbeam
firmware-tbeam_supreme:
- arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x3D"
+ arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x3D" --build-path $(TBEAM_BUILD)
+
+deploy-tbeam_supreme: firmware-tbeam_supreme
+ esptool.py --chip esp32-s3 --port $(TBEAM_PORT) --baud 921600 --before default_reset --after hard_reset \
+ write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x10000 $(TBEAM_BUILD)/RNode_Firmware.ino.bin
+ @sleep 5
+ rnodeconf $(TBEAM_PORT) --firmware-hash $$(./partition_hashes $(TBEAM_BUILD)/RNode_Firmware.ino.bin)
+
+# T-Watch Ultra: separate build dir, 16MB flash, 8MB app partition, LVGL GUI
+# FlashSize=16M is critical — the bootloader embeds flash size and defaults to 4MB.
+# partition_twatch.csv must be copied to the Arduino ESP32 tools/partitions/ directory.
+TWATCH_BUILD = build/twatch
+firmware-twatch_ultra:
+ arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc,FlashSize=16M,PSRAM=enabled" -e --build-property "build.partitions=partition_twatch" --build-property "upload.maximum_size=8388608" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x45" --build-path $(TWATCH_BUILD)
+
+# Quick deploy: bootloader-mode flash (no BOOT+RST needed)
+deploy-twatch_ultra: firmware-twatch_ultra
+ python3 scripts/screenshot.py -p $(TWATCH_PORT) z || true
+ @sleep 2
+ esptool.py --chip esp32-s3 --port $(TWATCH_PORT) --baud 921600 --before no_reset --after hard_reset \
+ write_flash -z --flash_mode dio --flash_freq 80m --flash_size 16MB 0x10000 $(TWATCH_BUILD)/RNode_Firmware.ino.bin
+ @echo "Press RST on watch"
+
+# Full flash including bootloader and partition table (needed after erase or partition changes)
+deploy-twatch_ultra-full: firmware-twatch_ultra
+ @echo "Hold BOOT, press RST, release BOOT, then press Enter..."
+ @read _
+ esptool.py --chip esp32-s3 --port $(TWATCH_PORT) --baud 921600 --before no_reset --after hard_reset \
+ erase_flash
+ @echo "Hold BOOT, press RST, release BOOT, then press Enter..."
+ @read _
+ esptool.py --chip esp32-s3 --port $(TWATCH_PORT) --baud 921600 --before no_reset --after hard_reset \
+ write_flash -z --flash_mode dio --flash_freq 80m --flash_size 16MB \
+ 0x0000 $(TWATCH_BUILD)/RNode_Firmware.ino.bootloader.bin \
+ 0x8000 $(TWATCH_BUILD)/RNode_Firmware.ino.partitions.bin \
+ 0x10000 $(TWATCH_BUILD)/RNode_Firmware.ino.bin
+ @echo "Press RST. Self-provisioning runs automatically on first boot."
+
+# Legacy JTAG upload (keep for backward compat)
+upload-twatch_ultra: firmware-twatch_ultra
+ $(HOME)/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20230921/bin/openocd \
+ -s $(HOME)/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20230921/share/openocd/scripts \
+ -f board/esp32s3-builtin.cfg \
+ -c "program_esp $(TWATCH_BUILD)/RNode_Firmware.ino.bin 0x10000 verify reset exit"
firmware-lora32_v10: check_bt_buffers
arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\""
@@ -214,6 +276,15 @@ upload-heltec32_v4:
@sleep 3
python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
+deploy-heltec32_v4: firmware-heltec32_v4
+ python3 $(HOME)/.arduino15/packages/esp32/tools/esptool_py/4.5.1/esptool.py \
+ --chip esp32s3 --port /dev/ttyACM0 --baud 115200 --no-stub \
+ --before default_reset --after hard_reset write_flash -z \
+ --flash_mode dio --flash_freq 80m --flash_size 8MB \
+ 0x10000 build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin
+ @sleep 3
+ rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
+
upload-tdeck:
arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3
@sleep 1
@@ -221,12 +292,12 @@ upload-tdeck:
@sleep 3
python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
-upload-tbeam_supreme:
- arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3
- @sleep 1
- rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
+upload-tbeam_supreme: free-port
+ arduino-cli upload -p $(PORT) --fqbn esp32:esp32:esp32s3
@sleep 3
- python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
+ rnodeconf $(PORT) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
+ @sleep 3
+ python ./Release/esptool/esptool.py --chip esp32-s3 --port $(PORT) --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
upload-rnode_ng_20:
arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:ttgo-lora32
diff --git a/MessageLog.h b/MessageLog.h
new file mode 100644
index 00000000..a2abf180
--- /dev/null
+++ b/MessageLog.h
@@ -0,0 +1,207 @@
+// MessageLog.h — Received packet log and RNS announce parser
+// Stores recent LoRa packets for the Messages screen viewer
+
+#ifndef MESSAGELOG_H
+#define MESSAGELOG_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+
+#include "mbedtls/sha256.h"
+
+// RNS packet type bits (flags byte, bits 1-0)
+#define RNS_TYPE_DATA 0x00
+#define RNS_TYPE_ANNOUNCE 0x01
+// RNS header type (flags byte, bit 6)
+#define RNS_HEADER_1 0x00
+#define RNS_HEADER_2 0x40
+// IFAC flag (flags byte, bit 7)
+#define RNS_IFAC_FLAG 0x80
+
+// msg_entry_t struct defined in Gui.h (included earlier)
+// MSG_LOG_SIZE and MSG_NAME_LEN also defined there
+#define MSG_DEDUP_MS 60000 // ignore same sender within 60s
+
+msg_entry_t msg_log[MSG_LOG_SIZE];
+uint8_t msg_log_head = 0;
+uint8_t msg_log_count = 0;
+uint32_t msg_log_last_change = 0;
+
+// Find existing entry for this sender (for de-dup / update)
+static int msg_log_find_sender(const uint8_t *hash) {
+ for (int i = 0; i < msg_log_count; i++) {
+ int idx = (msg_log_head - 1 - i + MSG_LOG_SIZE) % MSG_LOG_SIZE;
+ if (memcmp(msg_log[idx].sender_hash, hash, 16) == 0) return idx;
+ }
+ return -1;
+}
+
+// Add or update a message entry
+static void msg_log_add(const uint8_t *sender_hash, const char *name,
+ int16_t rssi, int8_t snr, uint8_t pkt_type,
+ uint16_t pkt_len, bool is_announce) {
+ // De-dup: update existing entry for same sender
+ int existing = msg_log_find_sender(sender_hash);
+ if (existing >= 0) {
+ msg_entry_t &e = msg_log[existing];
+ if (millis() - e.timestamp < MSG_DEDUP_MS && !is_announce) return;
+ // Update existing
+ e.timestamp = millis();
+ e.rssi = rssi;
+ e.snr = snr;
+ e.pkt_type = pkt_type;
+ e.pkt_len = pkt_len;
+ if (is_announce && name[0]) {
+ strncpy(e.display_name, name, MSG_NAME_LEN - 1);
+ e.display_name[MSG_NAME_LEN - 1] = '\0';
+ e.is_announce = true;
+ }
+ msg_log_last_change = millis();
+ return;
+ }
+
+ // New entry
+ msg_entry_t &e = msg_log[msg_log_head];
+ e.timestamp = millis();
+ e.rssi = rssi;
+ e.snr = snr;
+ e.pkt_type = pkt_type;
+ e.pkt_len = pkt_len;
+ e.is_announce = is_announce;
+ memcpy(e.sender_hash, sender_hash, 16);
+ if (name && name[0]) {
+ strncpy(e.display_name, name, MSG_NAME_LEN - 1);
+ e.display_name[MSG_NAME_LEN - 1] = '\0';
+ } else {
+ // Short hex of sender hash
+ snprintf(e.display_name, MSG_NAME_LEN, "%02x%02x%02x..%02x%02x",
+ sender_hash[0], sender_hash[1], sender_hash[2],
+ sender_hash[14], sender_hash[15]);
+ }
+ msg_log_head = (msg_log_head + 1) % MSG_LOG_SIZE;
+ if (msg_log_count < MSG_LOG_SIZE) msg_log_count++;
+ msg_log_last_change = millis();
+}
+
+// Parse an RNS packet and log it
+// Called from main loop after dequeue, before kiss_write_packet
+// pkt points to raw LoRa payload (after split reassembly), len is total length
+static void msg_log_parse_packet(const uint8_t *pkt, uint16_t len,
+ int16_t rssi, int8_t snr) {
+ if (len < 19) return; // too short for RNS header
+
+ uint8_t flags = pkt[0];
+ bool has_ifac = (flags & RNS_IFAC_FLAG) != 0;
+ uint8_t pkt_type = flags & 0x03;
+
+ // IFAC packets: the real header starts after unmasking, but we can
+ // still extract the dest_hash from bytes 2-17 (masked but deterministic position)
+ // For announces, the IFAC is stripped by the receiver's IFAC handler.
+ // For our purposes, we parse what we can.
+
+ int hdr_offset = 0; // where the header starts after IFAC
+ if (has_ifac) {
+ // IFAC-wrapped: bytes 0-1 are masked header, bytes 2-9 are IFAC signature,
+ // rest is masked payload. We can't parse without unmasking.
+ // But we still log the packet with the dest_hash from bytes 2-17
+ // (these are the IFAC sig bytes, not the actual dest_hash)
+ // For now, just log as generic packet with flags info
+ uint8_t generic_hash[16];
+ memcpy(generic_hash, pkt + 2, 16);
+ msg_log_add(generic_hash, NULL, rssi, snr, pkt_type, len, false);
+ return;
+ }
+
+ // Non-IFAC packet — parse RNS header directly
+ const uint8_t *dest_hash = pkt + 2; // bytes 2-17
+
+ if (pkt_type == RNS_TYPE_ANNOUNCE && len > 167) {
+ // ANNOUNCE: extract identity, display name, and cache public keys
+ const uint8_t *pub_keys = pkt + 19; // x25519(32) + ed25519(32)
+ uint8_t identity_hash[32];
+ mbedtls_sha256(pub_keys, 64, identity_hash, 0);
+
+ char name[MSG_NAME_LEN] = {0};
+ int app_data_offset = 19 + 64 + 10 + 10 + 64; // = 167
+ int app_data_len = len - app_data_offset;
+ if (app_data_len > 0 && app_data_len < MSG_NAME_LEN) {
+ memcpy(name, pkt + app_data_offset, app_data_len);
+ name[app_data_len] = '\0';
+ }
+
+ msg_log_add(identity_hash, name, rssi, snr, pkt_type, len, true);
+
+ // Cache sender's ed25519 public key for signature verification
+ int idx = msg_log_find_sender(identity_hash);
+ if (idx >= 0) {
+ memcpy(msg_log[idx].sender_ed25519_pub, pub_keys + 32, 32);
+ msg_log[idx].has_pubkey = true;
+ }
+
+ } else if (pkt_type == RNS_TYPE_DATA && len > 19 + 96) {
+ // DATA packet — check if addressed to us (LXMF delivery)
+ extern uint8_t lxmf_source_hash[16];
+ extern uint8_t lxmf_x25519_sk[32];
+ extern bool lxmf_identity_configured;
+
+ if (lxmf_identity_configured && memcmp(dest_hash, lxmf_source_hash, 16) == 0) {
+ // Addressed to our LXMF delivery destination — try to decrypt
+ const uint8_t *encrypted = pkt + 19; // after RNS header
+ size_t enc_len = len - 19;
+
+ // We need the sender's identity hash for HKDF salt.
+ // For OPPORTUNISTIC messages, we use our OWN identity hash as salt
+ // (the sender encrypted TO us, using our identity hash as salt)
+ extern uint8_t lxmf_identity_hash[16];
+
+ static uint8_t plaintext[512];
+ int pt_len = beacon_crypto_decrypt(encrypted, enc_len,
+ lxmf_x25519_sk,
+ lxmf_identity_hash,
+ plaintext, sizeof(plaintext));
+
+ if (pt_len > 0) {
+ // Decryption succeeded — parse LXMF message
+ LxmfParsed parsed;
+ if (lxmf_parse_received(plaintext, pt_len, &parsed)) {
+ // Look up sender's name and pubkey from announce cache
+ char *sender_name = NULL;
+ uint8_t *sender_pub = NULL;
+ int sender_idx = msg_log_find_sender(parsed.source_hash);
+ if (sender_idx >= 0) {
+ sender_name = msg_log[sender_idx].display_name;
+ if (msg_log[sender_idx].has_pubkey)
+ sender_pub = msg_log[sender_idx].sender_ed25519_pub;
+ }
+
+ // Add as LXMF message entry
+ msg_log_add(parsed.source_hash, sender_name, rssi, snr,
+ pkt_type, len, false);
+
+ // Store content in the new entry
+ int new_idx = msg_log_find_sender(parsed.source_hash);
+ if (new_idx >= 0) {
+ strncpy(msg_log[new_idx].content, parsed.content, 63);
+ msg_log[new_idx].content[63] = '\0';
+ msg_log[new_idx].is_lxmf = true;
+ if (sender_pub) {
+ msg_log[new_idx].verified =
+ lxmf_verify_signature(plaintext, pt_len, sender_pub);
+ }
+ }
+
+ Serial.printf("[lxmf_rx] from %02x%02x: \"%s\"\n",
+ parsed.source_hash[0], parsed.source_hash[1], parsed.content);
+ }
+ }
+ } else {
+ // DATA not for us — log generically
+ msg_log_add(dest_hash, NULL, rssi, snr, pkt_type, len, false);
+ }
+ } else {
+ // Other packet — log with dest_hash
+ msg_log_add(dest_hash, NULL, rssi, snr, pkt_type, len, false);
+ }
+}
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT
+#endif // MESSAGELOG_H
diff --git a/Microphone.h b/Microphone.h
new file mode 100644
index 00000000..2ab9787f
--- /dev/null
+++ b/Microphone.h
@@ -0,0 +1,82 @@
+// SPM1423 PDM Microphone Driver — Minimal audio input for T-Watch Ultra
+// PDM input on I2S_NUM_0 (hardware constraint for PDM on ESP32-S3)
+
+#ifndef MICROPHONE_H
+#define MICROPHONE_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+
+#include "driver/i2s.h"
+
+#define MIC_CLK_PIN 17 // PDM clock (WS pin in I2S terms)
+#define MIC_DAT_PIN 18 // PDM data input
+#define MIC_I2S_PORT I2S_NUM_0 // PDM only works on I2S0
+#define MIC_SAMPLE_RATE 16000 // 16kHz max for PDM mic
+#define MIC_BUF_SIZE 1024
+
+static bool mic_ready = false;
+
+bool mic_init() {
+ i2s_config_t i2s_config = {};
+ i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM);
+ i2s_config.sample_rate = MIC_SAMPLE_RATE;
+ i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
+ i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT;
+ i2s_config.communication_format = I2S_COMM_FORMAT_STAND_PCM_SHORT;
+ i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1;
+ i2s_config.dma_buf_count = 6;
+ i2s_config.dma_buf_len = 512;
+ i2s_config.use_apll = true;
+
+ i2s_pin_config_t pin_config = {};
+ pin_config.bck_io_num = I2S_PIN_NO_CHANGE;
+ pin_config.ws_io_num = MIC_CLK_PIN;
+ pin_config.data_out_num = I2S_PIN_NO_CHANGE;
+ pin_config.data_in_num = MIC_DAT_PIN;
+ pin_config.mck_io_num = I2S_PIN_NO_CHANGE;
+
+ if (i2s_driver_install(MIC_I2S_PORT, &i2s_config, 0, NULL) != ESP_OK) {
+ return false;
+ }
+ if (i2s_set_pin(MIC_I2S_PORT, &pin_config) != ESP_OK) {
+ i2s_driver_uninstall(MIC_I2S_PORT);
+ return false;
+ }
+
+ mic_ready = true;
+ return true;
+}
+
+void mic_end() {
+ if (mic_ready) {
+ i2s_driver_uninstall(MIC_I2S_PORT);
+ mic_ready = false;
+ }
+}
+
+// Read raw audio samples into buffer. Returns bytes read.
+size_t mic_read(int16_t *buf, size_t samples) {
+ if (!mic_ready) return 0;
+ size_t bytes_read = 0;
+ i2s_read(MIC_I2S_PORT, buf, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY);
+ return bytes_read / sizeof(int16_t);
+}
+
+// Get current audio level (RMS of a short sample). Returns 0-32767.
+uint16_t mic_level() {
+ if (!mic_ready) return 0;
+
+ int16_t buf[256];
+ size_t samples = mic_read(buf, 256);
+ if (samples == 0) return 0;
+
+ uint64_t sum_sq = 0;
+ for (size_t i = 0; i < samples; i++) {
+ int32_t s = buf[i];
+ sum_sq += s * s;
+ }
+ return (uint16_t)sqrt((double)sum_sq / samples);
+}
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT
+#endif // MICROPHONE_H
diff --git a/Power.h b/Power.h
index 7ce6d8a7..6455fddc 100644
--- a/Power.h
+++ b/Power.h
@@ -19,7 +19,7 @@
bool pmu_temp_sensor_ready = false;
float pmu_temperature = PMU_TEMP_MIN-1;
-#if BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1
+#if BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TWATCH_ULT
#include
XPowersLibInterface* PMU = NULL;
@@ -36,17 +36,56 @@ float pmu_temperature = PMU_TEMP_MIN-1;
void disablePeripherals() {
if (PMU) {
- // GNSS RTC PowerVDD
- PMU->enablePowerOutput(XPOWERS_VBACKUP);
-
- // LoRa VDD
- PMU->disablePowerOutput(XPOWERS_ALDO2);
-
- // GNSS VDD
- PMU->disablePowerOutput(XPOWERS_ALDO3);
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ PMU->disablePowerOutput(XPOWERS_ALDO3); // LoRa VDD
+ PMU->disablePowerOutput(XPOWERS_BLDO1); // GPS VDD
+ #else
+ // GNSS RTC PowerVDD
+ PMU->enablePowerOutput(XPOWERS_VBACKUP);
+ // LoRa VDD
+ PMU->disablePowerOutput(XPOWERS_ALDO2);
+ // GNSS VDD
+ PMU->disablePowerOutput(XPOWERS_ALDO3);
+ #endif
}
}
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ void power_gps(bool on) { if (PMU) { if (on) PMU->enablePowerOutput(XPOWERS_BLDO1); else PMU->disablePowerOutput(XPOWERS_BLDO1); } }
+ void power_radio(bool on) { if (PMU) { if (on) PMU->enablePowerOutput(XPOWERS_ALDO3); else PMU->disablePowerOutput(XPOWERS_ALDO3); } }
+ void power_nfc(bool on) { if (PMU) { if (on) PMU->enablePowerOutput(XPOWERS_DLDO1); else PMU->disablePowerOutput(XPOWERS_DLDO1); } }
+ void power_speaker(bool on) { if (PMU) { if (on) PMU->enablePowerOutput(XPOWERS_BLDO2); else PMU->disablePowerOutput(XPOWERS_BLDO2); } }
+ void power_sensor(bool on) { if (PMU) { if (on) PMU->enablePowerOutput(XPOWERS_ALDO4); else PMU->disablePowerOutput(XPOWERS_ALDO4); } }
+ void power_sd(bool on) { if (PMU) { if (on) PMU->enablePowerOutput(XPOWERS_ALDO1); else PMU->disablePowerOutput(XPOWERS_ALDO1); } }
+
+ void pmu_prepare_sleep() {
+ if (!PMU) return;
+ PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
+ PMU->enableIRQ(XPOWERS_AXP2101_PKEY_SHORT_IRQ);
+
+ // Disable measurement ADCs
+ PMU->disableBattDetection();
+ PMU->disableVbusVoltageMeasure();
+ PMU->disableBattVoltageMeasure();
+ PMU->disableSystemVoltageMeasure();
+ PMU->disableTSPinMeasure();
+
+ // DO NOT call PMU->enableSleep() — it puts the AXP2101 into a
+ // persistent sleep mode that survives battery-backed resets and
+ // disables its I2C slave interface, bricking the bus.
+
+ // Disable peripheral rails (NOT ALDO2 — causes 600µA anomaly!)
+ power_sd(false);
+ power_radio(false);
+ power_sensor(false);
+ power_gps(false);
+ power_speaker(false);
+ power_nfc(false);
+
+ PMU->clearIrqStatus();
+ }
+ #endif
+
bool pmuInterrupt;
void setPmuFlag()
{
@@ -308,7 +347,7 @@ void measure_battery() {
// }
}
- #elif BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1
+ #elif BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TWATCH_ULT
if (PMU) {
float discharge_current = 0;
float charge_current = 0;
@@ -392,6 +431,7 @@ void measure_battery() {
if (battery_ready) {
pmu_rc++;
if (pmu_rc%PMU_R_INTERVAL == 0) {
+ response_channel = data_channel;
kiss_indicate_battery();
if (pmu_temp_sensor_ready) { kiss_indicate_temperature(); }
}
@@ -643,7 +683,82 @@ bool init_pmu() {
PMU->enableBattVoltageMeasure();
- return true;
+ return true;
+ #elif BOARD_MODEL == BOARD_TWATCH_ULT
+ // Pass explicit SDA/SCL pins to XPowersLib constructor.
+ // The generic esp32s3 FQBN defaults to SDA=8, SCL=9 which are WRONG
+ // for the T-Watch Ultra (SDA=3, SCL=2). XPowersLib's begin() calls
+ // Wire.begin(__sda, __scl) internally, so we must set the correct pins here.
+ if (!PMU) {
+ PMU = new XPowersAXP2101(PMU_WIRE_PORT, I2C_SDA, I2C_SCL);
+ if (!PMU->init()) {
+ delete PMU;
+ PMU = NULL;
+ }
+ }
+
+ if (!PMU) {
+ return false;
+ }
+
+ // Set voltages for all power rails
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); // SD card
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // Display
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // LoRa radio
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 1800); // BHI260AP sensor (1.8V!)
+ PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); // GPS
+ PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); // Speaker
+ PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); // RTC button battery
+
+ // Enable needed rails
+ PMU->enablePowerOutput(XPOWERS_ALDO1); // SD card
+ PMU->enablePowerOutput(XPOWERS_ALDO2); // Display
+ PMU->enablePowerOutput(XPOWERS_ALDO3); // LoRa radio
+ PMU->enablePowerOutput(XPOWERS_ALDO4); // IMU sensor
+ PMU->enablePowerOutput(XPOWERS_BLDO1); // GPS
+ PMU->enablePowerOutput(XPOWERS_BLDO2); // Speaker
+ PMU->enablePowerOutput(XPOWERS_DLDO1); // NFC
+ PMU->enablePowerOutput(XPOWERS_VBACKUP); // RTC battery
+
+ // Disable unused rails
+ PMU->disablePowerOutput(XPOWERS_DCDC2);
+ PMU->disablePowerOutput(XPOWERS_DCDC3);
+ PMU->disablePowerOutput(XPOWERS_DCDC4);
+ PMU->disablePowerOutput(XPOWERS_DCDC5);
+
+ // Protect ESP32-S3 main power
+ PMU->setProtectedChannel(XPOWERS_DCDC1);
+
+ // Configure charging: 4.2V target, 500mA current
+ PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
+ PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
+ PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF);
+
+ // Set power button timing
+ PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
+ PMU->setPowerKeyPressOnTime(XPOWERS_POWERON_128MS);
+
+ // Enable battery and temperature monitoring
+ PMU->enableBattDetection();
+ PMU->enableVbusVoltageMeasure();
+ PMU->enableBattVoltageMeasure();
+ PMU->enableSystemVoltageMeasure();
+ PMU->enableTSPinMeasure();
+
+ // Configure interrupts
+ PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
+ PMU->enableIRQ(XPOWERS_AXP2101_PKEY_SHORT_IRQ |
+ XPOWERS_AXP2101_PKEY_LONG_IRQ |
+ XPOWERS_AXP2101_VBUS_INSERT_IRQ |
+ XPOWERS_AXP2101_VBUS_REMOVE_IRQ |
+ XPOWERS_AXP2101_BAT_CHG_START_IRQ |
+ XPOWERS_AXP2101_BAT_CHG_DONE_IRQ);
+ PMU->clearIrqStatus();
+
+ pinMode(PMU_IRQ, INPUT_PULLUP);
+ attachInterrupt(PMU_IRQ, setPmuFlag, FALLING);
+
+ return true;
#else
return false;
#endif
diff --git a/README.md b/README.md
index 3cd0d0a9..7586774a 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,14 @@ The RNode Firmware supports the following boards:
- Homebrew RNodes based on Adafruit Feather ESP32 boards
- Homebrew RNodes based on generic ESP32 boards
+## Tested Devices (GPS + Beacon branch)
+The following devices have been tested with GPS and beacon support on the `gps-beacon` branch:
+
+| Device | Band | GPS | LoRa | Beacon | Notes |
+|--------|------|-----|------|--------|-------|
+| Heltec LoRa32 v4 | 868 MHz | L76K | SX1262 | Yes | Reference device for GPS+beacon implementation |
+| LilyGo T-Beam Supreme S3 | 868 MHz | L76K | SX1262 | Yes | Requires SPI.begin fix in sx126x.cpp; upload via USB CDC (ttyACM), not CH340 (ttyUSB) |
+
## Supported Transceiver Modules
The RNode Firmware supports all transceiver modules based on Semtech **SX1276**, **SX1278**, **SX1262**, **SX1268** and **SX1280** chips, that have an **SPI interface** and expose the relevant **DIO** interrupt pins from the chip.
diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino
index 5649206a..044cdf05 100644
--- a/RNode_Firmware.ino
+++ b/RNode_Firmware.ino
@@ -17,8 +17,88 @@
#include
#include "Utilities.h"
-FIFOBuffer serialFIFO;
-uint8_t serialBuffer[CONFIG_UART_BUFFER_SIZE+1];
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+ #include "esp_task_wdt.h"
+ #include "XL9555.h"
+ #include "CO5300.h"
+ #include "DRV2605.h"
+
+ // BHI260AP sensor hub — IMU + step counter + wrist wake
+ #include
+ #include
+ #define BOSCH_BHI260_GPIO
+ #include
+ SensorBHI260AP *bhi260 = NULL;
+ bool bhi260_ready = false;
+ volatile uint32_t imu_step_count = 0;
+ volatile bool imu_wrist_tilt = false;
+
+ // Filtered accelerometer for bubble level with adaptive noise tracking
+ volatile float imu_ax_f = 0, imu_ay_f = 0, imu_az_f = 4096;
+ volatile float imu_noise = 0; // running estimate of accel noise (0=still, 1+=noisy)
+ void imu_accel_live_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
+ if (size >= 6) {
+ float ax = (int16_t)(data[0] | (data[1] << 8));
+ float ay = (int16_t)(data[2] | (data[3] << 8));
+ float az = (int16_t)(data[4] | (data[5] << 8));
+ // Noise: EMA of squared deviation from filtered value
+ float dx = ax - imu_ax_f, dy = ay - imu_ay_f, dz = az - imu_az_f;
+ float dev = (dx*dx + dy*dy + dz*dz) / (4096.0f * 4096.0f);
+ imu_noise += 0.1f * (dev - imu_noise);
+ // Adaptive EMA: responsive when quiet, smooth when noisy
+ float alpha = (imu_noise < 0.001f) ? 0.4f :
+ (imu_noise < 0.01f) ? 0.2f : 0.08f;
+ imu_ax_f += alpha * (ax - imu_ax_f);
+ imu_ay_f += alpha * (ay - imu_ay_f);
+ imu_az_f += alpha * (az - imu_az_f);
+ }
+ }
+
+ // MAX98357A I2S speaker + SPM1423 PDM microphone
+ #include "Speaker.h"
+ #include "Microphone.h"
+
+ // Sensor data logger to SD card (must be before callbacks that call sensor_log_*)
+ #include "IMULogger.h"
+
+ // Message log for received packet viewer
+ #include "MessageLog.h"
+
+ // IMU sensor callbacks
+ void imu_step_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
+ if (size >= 4) {
+ imu_step_count = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
+ sensor_log_step(imu_step_count);
+ }
+ }
+
+ void imu_wrist_tilt_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) {
+ imu_wrist_tilt = true;
+ sensor_log_wrist_tilt();
+ }
+
+ // Shared SPI bus mutex (LoRa + SD + NFC)
+ #include "SharedSPI.h"
+ SemaphoreHandle_t shared_spi_mutex = NULL; // definition (declared extern in SharedSPI.h)
+
+ // USB Mass Storage for SD card access (TinyUSB OTG mode only)
+ #if !ARDUINO_USB_MODE
+ #include "USBSD.h"
+ #endif
+
+ // CST9217 capacitive touch panel
+ #include
+ TouchDrvCST92xx touch;
+ bool touch_ready = false;
+ volatile bool touch_irq = false;
+ #define TP_INT 12
+ void IRAM_ATTR touch_isr() { touch_irq = true; }
+#endif
+
+#define CHANNEL_FIFO_SIZE (CONFIG_UART_BUFFER_SIZE / NUM_CHANNELS)
+FIFOBuffer channelFIFO[NUM_CHANNELS];
+uint8_t channelBuffer[NUM_CHANNELS][CHANNEL_FIFO_SIZE + 1];
+ChannelState channel_state[NUM_CHANNELS];
FIFOBuffer16 packet_starts;
uint16_t packet_starts_buf[CONFIG_QUEUE_MAX_LENGTH+1];
@@ -61,9 +141,52 @@ char sbuf[128];
void setup() {
#if MCU_VARIANT == MCU_ESP32
boot_seq();
+
+ // Hardware watchdog — auto-resets on lockup (30s timeout covers
+ // BHI260AP firmware upload which takes ~10s at boot)
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ esp_task_wdt_init(30, true); // 30s timeout, panic on expire
+ esp_task_wdt_add(NULL); // subscribe current task (loopTask)
+ #endif
+
+ // Init shared SPI bus mutex before any SPI users
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ shared_spi_init();
+ #endif
+
EEPROM.begin(EEPROM_SIZE);
Serial.setRxBufferSize(CONFIG_UART_BUFFER_SIZE);
+ // T-Watch Ultra: self-provision if EEPROM is blank (after flash erase)
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ if (EEPROM.read(eeprom_addr(ADDR_PRODUCT)) == 0xFF) {
+ EEPROM.write(eeprom_addr(ADDR_PRODUCT), PRODUCT_TWATCH_ULT);
+ EEPROM.write(eeprom_addr(ADDR_MODEL), MODEL_DA); // 868 MHz
+ EEPROM.write(eeprom_addr(ADDR_HW_REV), 0x01);
+ EEPROM.write(eeprom_addr(ADDR_SERIAL), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_SERIAL+1), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_SERIAL+2), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_SERIAL+3), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_MADE), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_MADE+1), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_MADE+2), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_MADE+3), 0x00);
+ EEPROM.write(eeprom_addr(ADDR_INFO_LOCK), INFO_LOCK_BYTE);
+ // Do NOT write CONF_OK_BYTE — radio config (freq/bw/sf) is not set.
+ // If CONF_OK is set, eeprom_conf_load() loads 0xFFFFFFFF into lora_freq
+ // and startRadio() calls begin(0xFFFFFFFF) which corrupts SX1262 calibration.
+ // Compute and write EEPROM checksum (MD5 of first CHECKSUMMED_SIZE bytes)
+ char chk_data[CHECKSUMMED_SIZE];
+ for (uint8_t i = 0; i < CHECKSUMMED_SIZE; i++)
+ chk_data[i] = EEPROM.read(eeprom_addr(i));
+ unsigned char *chk_hash = MD5::make_hash(chk_data, CHECKSUMMED_SIZE);
+ for (uint8_t i = 0; i < 16; i++)
+ EEPROM.write(eeprom_addr(ADDR_CHKSUM + i), chk_hash[i]);
+ free(chk_hash);
+ EEPROM.commit();
+ }
+ #endif
+
#if BOARD_MODEL == BOARD_TDECK
pinMode(pin_poweron, OUTPUT);
digitalWrite(pin_poweron, HIGH);
@@ -116,11 +239,18 @@ void setup() {
randomSeed(seed_val);
// Initialise serial communication
- memset(serialBuffer, 0, sizeof(serialBuffer));
- fifo_init(&serialFIFO, serialBuffer, CONFIG_UART_BUFFER_SIZE);
+ for (uint8_t ch = 0; ch < NUM_CHANNELS; ch++) {
+ memset(channelBuffer[ch], 0, sizeof(channelBuffer[ch]));
+ fifo_init(&channelFIFO[ch], channelBuffer[ch], CHANNEL_FIFO_SIZE);
+ memset(&channel_state[ch], 0, sizeof(ChannelState));
+ channel_state[ch].command = CMD_UNKNOWN;
+ }
Serial.begin(serial_baudrate);
+ // USB MSC requires TinyUSB mode which adds ~900ms/loop overhead on ESP32-S3.
+ // SD card access uses serial file transfer instead (debug command 'F').
+
#if HAS_NP
led_init();
#endif
@@ -129,7 +259,7 @@ void setup() {
boot_seq();
#endif
- #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4
+ #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 && BOARD_MODEL != BOARD_TWATCH_ULT
// Some boards need to wait until the hardware UART is set up before booting
// the full firmware. In the case of the RAK4631 and Heltec T114, the line below will wait
// until a serial connection is actually established with a master. Thus, it
@@ -145,8 +275,8 @@ void setup() {
#endif
#if HAS_NP == false
- pinMode(pin_led_rx, OUTPUT);
- pinMode(pin_led_tx, OUTPUT);
+ if (pin_led_rx >= 0) pinMode(pin_led_rx, OUTPUT);
+ if (pin_led_tx >= 0) pinMode(pin_led_tx, OUTPUT);
#endif
#if HAS_TCXO == true
@@ -158,7 +288,6 @@ void setup() {
// Initialise buffers
memset(pbuf, 0, sizeof(pbuf));
- memset(cmdbuf, 0, sizeof(cmdbuf));
memset(packet_queue, 0, sizeof(packet_queue));
@@ -250,21 +379,112 @@ void setup() {
display_add_callback(work_while_waiting);
#endif
+ // T-Watch init order: display_init() MUST run BEFORE xl9555_init().
+ // The XL9555 GPIO expander controls the display power gate (EXPANDS_DISP_EN),
+ // but its outputs default HIGH at power-on, so the display is powered before
+ // the expander is explicitly configured. Moving display_init() after xl9555_init()
+ // causes a black screen because the power gate cycling disrupts the CO5300 QSPI
+ // init sequence. Do not reorder.
display_unblank();
disp_ready = display_init();
update_display();
#endif
#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+
#if HAS_PMU == true
pmu_ready = init_pmu();
#endif
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ xl9555_init();
+ xl9555_enable_lora_antenna();
+ xl9555_set(EXPANDS_DRV_EN, true); // Enable haptic motor driver
+ xl9555_set(EXPANDS_DISP_EN, true); // Confirm display power gate on
+ xl9555_set(EXPANDS_TOUCH_RST, true); // Release touch reset
+ delay(100);
+ drv2605_init();
+ if (drv2605_ready) drv2605_play(HAPTIC_SHARP_CLICK); // Boot feedback
+
+ // Init touch panel
+ touch.setPins(-1, TP_INT); // No reset pin (handled by XL9555), INT on GPIO 12
+ if (touch.begin(Wire, 0x1A, I2C_SDA, I2C_SCL)) {
+ touch_ready = true;
+ attachInterrupt(TP_INT, touch_isr, FALLING);
+
+ // Register touch with LVGL GUI
+ #if HAS_DISPLAY == true
+ gui_set_touch_handler([](int16_t *x, int16_t *y) -> bool {
+ if (!touch_ready) return false;
+ return touch.getPoint(x, y, 1) > 0;
+ });
+ #endif
+ }
+
+ // Init speaker (BLDO2 already enabled by PMU init) and microphone
+ speaker_init();
+ mic_init();
+
+ // USB MSC SD card — deferred to main loop (SPI bus needs LoRa init first)
+
+ // BHI260AP init deferred — firmware upload takes ~10s at 1MHz I2C
+ // and blocks serial communication during boot. Will be initialized
+ // lazily from the main loop after radio is up.
+
+ // Beacon timer wakeup: if we woke from deep sleep via timer,
+ // take the fast path — init GPS/LoRa only, transmit, sleep again.
+ // esp_reset_reason() reliably distinguishes deep sleep from cold boot.
+ #if HAS_GPS == true
+ if (esp_reset_reason() == ESP_RST_DEEPSLEEP &&
+ esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER) {
+ beacon_wake_cycle(); // Does not return
+ }
+ #endif
+ #endif
+
#if HAS_BLUETOOTH || HAS_BLE == true
bt_init();
bt_init_ran = true;
#endif
+ #if HAS_RTC == true
+ rtc_setup();
+ #endif
+
+ #if HAS_GPS == true
+ gps_setup();
+ // Load beacon encryption config from EEPROM (config region)
+ if (EEPROM.read(config_addr(ADDR_BCN_OK)) == CONF_OK_BYTE) {
+ for (int i = 0; i < 32; i++)
+ collector_pub_key[i] = EEPROM.read(config_addr(ADDR_BCN_KEY + i));
+ for (int i = 0; i < 16; i++)
+ collector_identity_hash[i] = EEPROM.read(config_addr(ADDR_BCN_IHASH + i));
+ for (int i = 0; i < 16; i++)
+ collector_dest_hash[i] = EEPROM.read(config_addr(ADDR_BCN_DHASH + i));
+ beacon_crypto_configured = true;
+ }
+ // Initialize LXMF identity (load from NVS or generate new)
+ lxmf_init_identity();
+ // Initialize IFAC authentication (load from NVS, or self-provision)
+ ifac_init();
+ if (!ifac_configured) {
+ ifac_self_provision(BEACON_NETWORK_NAME, BEACON_PASSPHRASE);
+ }
+ // Load user settings from config EEPROM
+ uint8_t s_disp = EEPROM.read(config_addr(ADDR_CONF_DISP_TIMEOUT));
+ if (s_disp != 0xFF && s_disp >= 5 && s_disp <= 60)
+ display_blanking_timeout = (uint32_t)s_disp * 1000;
+ uint8_t s_bcn_int = EEPROM.read(config_addr(ADDR_CONF_BCN_INT));
+ if (s_bcn_int != 0xFF && s_bcn_int < BEACON_INTERVAL_OPTIONS_COUNT)
+ beacon_interval_ms = beacon_interval_options[s_bcn_int];
+ uint8_t s_gps_model = EEPROM.read(config_addr(ADDR_CONF_GPS_MODEL));
+ if (s_gps_model != 0xFF && s_gps_model < GPS_MODEL_OPTIONS_COUNT)
+ gps_set_dynamic_model(s_gps_model);
+ uint8_t s_bcn_en = EEPROM.read(config_addr(ADDR_CONF_BCN_EN));
+ if (s_bcn_en != 0xFF)
+ beacon_enabled = (s_bcn_en != 0);
+ #endif
+
if (console_active) {
#if HAS_CONSOLE
console_start();
@@ -448,16 +668,19 @@ void ISR_VECT receive_callback(int packet_size) {
}
if (ready) {
+ stat_rx++;
+
#if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
// We first signal the RSSI of the
// recieved packet to the host.
+ response_channel = data_channel;
kiss_indicate_stat_rssi();
kiss_indicate_stat_snr();
// And then write the entire packet
host_write_len = read_len;
kiss_write_packet(); read_len = 0;
-
+
#else
// Allocate packet struct, but abort if there
// is not enough memory available.
@@ -479,11 +702,12 @@ void ISR_VECT receive_callback(int packet_size) {
free(modem_packet);
}
#endif
- }
+ }
} else {
// In promiscuous mode, raw packets are
// output directly to the host
read_len = 0;
+ stat_rx++;
#if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
last_rssi = LoRa->packetRssi();
@@ -492,6 +716,7 @@ void ISR_VECT receive_callback(int packet_size) {
// We first signal the RSSI of the
// recieved packet to the host.
+ response_channel = data_channel;
kiss_indicate_stat_rssi();
kiss_indicate_stat_snr();
@@ -709,6 +934,7 @@ void update_airtime() {
update_csma_parameters();
#endif
+ response_channel = data_channel;
kiss_indicate_channel_stats();
#endif
}
@@ -749,6 +975,7 @@ void transmit(uint16_t size) {
}
add_airtime(written);
+ stat_tx++;
} else {
led_tx_on(); uint16_t written = 0;
@@ -757,15 +984,80 @@ void transmit(uint16_t size) {
else { LoRa->beginPacket(size); }
for (uint16_t i=0; i < size; i++) { LoRa->write(tbuf[i]); written++; }
LoRa->endPacket(); add_airtime(written);
+ stat_tx++;
}
} else { kiss_indicate_error(ERROR_TXFAILED); led_indicate_error(5); }
}
-void serial_callback(uint8_t sbyte) {
- if (IN_FRAME && sbyte == FEND && command == CMD_DATA) {
- IN_FRAME = false;
+// Transmit raw RNS packet without the 1-byte RNode LoRa header.
+// Used by beacon mode so the receiving RNode passes the packet
+// directly to Reticulum without a spurious header byte.
+// Diagnostic: dump last beacon packet (pre and post IFAC)
+uint8_t diag_beacon_pre[256];
+uint16_t diag_beacon_pre_len = 0;
+uint8_t diag_beacon_post[256];
+uint16_t diag_beacon_post_len = 0;
+
+void beacon_transmit(uint16_t size) {
+ if (radio_online) {
+ // Save pre-IFAC packet for diagnostics
+ if (size <= 256) {
+ memcpy(diag_beacon_pre, tbuf, size);
+ diag_beacon_pre_len = size;
+ }
+
+ #if HAS_GPS == true
+ size = ifac_apply(tbuf, size);
+ #endif
+
+ // Save post-IFAC packet
+ if (size <= 256) {
+ memcpy(diag_beacon_post, tbuf, size);
+ diag_beacon_post_len = size;
+ }
+
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ if (shared_spi_mutex) xSemaphoreTake(shared_spi_mutex, portMAX_DELAY);
+ #endif
+ LoRa->beginPacket();
+ for (uint16_t i = 0; i < size; i++) {
+ LoRa->write(tbuf[i]);
+ }
+ bool tx_ok = LoRa->endPacket();
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ #endif
+ if (!tx_ok) {
+ Serial.println("[beacon] TX failed");
+ }
+ stat_tx++;
+ add_airtime(size);
+ lora_receive();
+ }
+}
+void serial_callback(uint8_t sbyte, uint8_t ch) {
+ ChannelState *cs = &channel_state[ch];
+ if (cs->in_frame && sbyte == FEND && cs->command == CMD_DATA) {
+ cs->in_frame = false;
+
+ #if NUM_CHANNELS > 1
+ if (cs->pkt_len >= MIN_L && !fifo16_isfull(&packet_starts)
+ && queue_height < CONFIG_QUEUE_MAX_LENGTH && queued_bytes + cs->pkt_len <= CONFIG_QUEUE_SIZE) {
+ uint16_t s = queue_cursor;
+ for (uint16_t i = 0; i < cs->pkt_len; i++) {
+ packet_queue[queue_cursor++] = cs->pktbuf[i];
+ if (queue_cursor == CONFIG_QUEUE_SIZE) queue_cursor = 0;
+ }
+ queue_height++;
+ queued_bytes += cs->pkt_len;
+ fifo16_push(&packet_starts, s);
+ fifo16_push(&packet_lengths, cs->pkt_len);
+ }
+ cs->pkt_len = 0;
+ data_channel = ch;
+ #else
if (!fifo16_isfull(&packet_starts) && queued_bytes < CONFIG_QUEUE_SIZE) {
uint16_t s = current_packet_start;
int16_t e = queue_cursor-1; if (e == -1) e = CONFIG_QUEUE_SIZE-1;
@@ -781,47 +1073,57 @@ void serial_callback(uint8_t sbyte) {
current_packet_start = queue_cursor;
}
}
+ #endif
} else if (sbyte == FEND) {
- IN_FRAME = true;
- command = CMD_UNKNOWN;
- frame_len = 0;
- } else if (IN_FRAME && frame_len < MTU) {
+ cs->in_frame = true;
+ cs->command = CMD_UNKNOWN;
+ cs->frame_len = 0;
+ } else if (cs->in_frame && cs->frame_len < MTU) {
// Have a look at the command byte first
- if (frame_len == 0 && command == CMD_UNKNOWN) {
- command = sbyte;
- } else if (command == CMD_DATA) {
- if (bt_state != BT_STATE_CONNECTED) {
+ if (cs->frame_len == 0 && cs->command == CMD_UNKNOWN) {
+ cs->command = sbyte;
+ #if HAS_GPS == true
+ beacon_check_host_activity();
+ #endif
+ } else if (cs->command == CMD_DATA) {
+ if (ch == CHANNEL_USB) {
cable_state = CABLE_STATE_CONNECTED;
}
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
+ }
+ #if NUM_CHANNELS > 1
+ if (cs->pkt_len < MTU) {
+ cs->pktbuf[cs->pkt_len++] = sbyte;
}
+ #else
if (queue_height < CONFIG_QUEUE_MAX_LENGTH && queued_bytes < CONFIG_QUEUE_SIZE) {
queued_bytes++;
packet_queue[queue_cursor++] = sbyte;
if (queue_cursor == CONFIG_QUEUE_SIZE) queue_cursor = 0;
}
+ #endif
}
- } else if (command == CMD_FREQUENCY) {
+ } else if (cs->command == CMD_FREQUENCY) {
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == 4) {
- uint32_t freq = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3];
+ if (cs->frame_len == 4) {
+ uint32_t freq = (uint32_t)cs->cmdbuf[0] << 24 | (uint32_t)cs->cmdbuf[1] << 16 | (uint32_t)cs->cmdbuf[2] << 8 | (uint32_t)cs->cmdbuf[3];
if (freq == 0) {
kiss_indicate_frequency();
@@ -831,20 +1133,20 @@ void serial_callback(uint8_t sbyte) {
kiss_indicate_frequency();
}
}
- } else if (command == CMD_BANDWIDTH) {
+ } else if (cs->command == CMD_BANDWIDTH) {
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == 4) {
- uint32_t bw = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3];
+ if (cs->frame_len == 4) {
+ uint32_t bw = (uint32_t)cs->cmdbuf[0] << 24 | (uint32_t)cs->cmdbuf[1] << 16 | (uint32_t)cs->cmdbuf[2] << 8 | (uint32_t)cs->cmdbuf[3];
if (bw == 0) {
kiss_indicate_bandwidth();
@@ -854,7 +1156,7 @@ void serial_callback(uint8_t sbyte) {
kiss_indicate_bandwidth();
}
}
- } else if (command == CMD_TXPOWER) {
+ } else if (cs->command == CMD_TXPOWER) {
if (sbyte == 0xFF) {
kiss_indicate_txpower();
} else {
@@ -879,7 +1181,7 @@ void serial_callback(uint8_t sbyte) {
if (op_mode == MODE_HOST) setTXPower();
kiss_indicate_txpower();
}
- } else if (command == CMD_SF) {
+ } else if (cs->command == CMD_SF) {
if (sbyte == 0xFF) {
kiss_indicate_spreadingfactor();
} else {
@@ -891,7 +1193,7 @@ void serial_callback(uint8_t sbyte) {
if (op_mode == MODE_HOST) setSpreadingFactor();
kiss_indicate_spreadingfactor();
}
- } else if (command == CMD_CR) {
+ } else if (cs->command == CMD_CR) {
if (sbyte == 0xFF) {
kiss_indicate_codingrate();
} else {
@@ -903,10 +1205,10 @@ void serial_callback(uint8_t sbyte) {
if (op_mode == MODE_HOST) setCodingRate();
kiss_indicate_codingrate();
}
- } else if (command == CMD_IMPLICIT) {
+ } else if (cs->command == CMD_IMPLICIT) {
set_implicit_length(sbyte);
kiss_indicate_implicit_length();
- } else if (command == CMD_LEAVE) {
+ } else if (cs->command == CMD_LEAVE) {
if (sbyte == 0xFF) {
display_unblank();
cable_state = CABLE_STATE_DISCONNECTED;
@@ -915,8 +1217,8 @@ void serial_callback(uint8_t sbyte) {
last_rssi_raw = 0x00;
last_snr_raw = 0x80;
}
- } else if (command == CMD_RADIO_STATE) {
- if (bt_state != BT_STATE_CONNECTED) {
+ } else if (cs->command == CMD_RADIO_STATE) {
+ if (ch == CHANNEL_USB) {
cable_state = CABLE_STATE_CONNECTED;
display_unblank();
}
@@ -929,20 +1231,20 @@ void serial_callback(uint8_t sbyte) {
startRadio();
kiss_indicate_radiostate();
}
- } else if (command == CMD_ST_ALOCK) {
+ } else if (cs->command == CMD_ST_ALOCK) {
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == 2) {
- uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1];
+ if (cs->frame_len == 2) {
+ uint16_t at = (uint16_t)cs->cmdbuf[0] << 8 | (uint16_t)cs->cmdbuf[1];
if (at == 0) {
st_airtime_limit = 0.0;
@@ -952,20 +1254,20 @@ void serial_callback(uint8_t sbyte) {
}
kiss_indicate_st_alock();
}
- } else if (command == CMD_LT_ALOCK) {
+ } else if (cs->command == CMD_LT_ALOCK) {
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == 2) {
- uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1];
+ if (cs->frame_len == 2) {
+ uint16_t at = (uint16_t)cs->cmdbuf[0] << 8 | (uint16_t)cs->cmdbuf[1];
if (at == 0) {
lt_airtime_limit = 0.0;
@@ -975,77 +1277,81 @@ void serial_callback(uint8_t sbyte) {
}
kiss_indicate_lt_alock();
}
- } else if (command == CMD_STAT_RX) {
+ } else if (cs->command == CMD_STAT_RX) {
kiss_indicate_stat_rx();
- } else if (command == CMD_STAT_TX) {
+ } else if (cs->command == CMD_STAT_TX) {
kiss_indicate_stat_tx();
- } else if (command == CMD_STAT_RSSI) {
+ } else if (cs->command == CMD_STAT_RSSI) {
kiss_indicate_stat_rssi();
- } else if (command == CMD_RADIO_LOCK) {
+ #if HAS_GPS == true
+ } else if (cs->command == CMD_STAT_GPS) {
+ kiss_indicate_stat_gps();
+ #endif
+ } else if (cs->command == CMD_RADIO_LOCK) {
update_radio_lock();
kiss_indicate_radio_lock();
- } else if (command == CMD_BLINK) {
+ } else if (cs->command == CMD_BLINK) {
led_indicate_info(sbyte);
- } else if (command == CMD_RANDOM) {
+ } else if (cs->command == CMD_RANDOM) {
kiss_indicate_random(getRandom());
- } else if (command == CMD_DETECT) {
+ } else if (cs->command == CMD_DETECT) {
if (sbyte == DETECT_REQ) {
- if (bt_state != BT_STATE_CONNECTED) cable_state = CABLE_STATE_CONNECTED;
+ if (ch == CHANNEL_USB) cable_state = CABLE_STATE_CONNECTED;
kiss_indicate_detect();
}
- } else if (command == CMD_PROMISC) {
+ } else if (cs->command == CMD_PROMISC) {
if (sbyte == 0x01) {
promisc_enable();
} else if (sbyte == 0x00) {
promisc_disable();
}
kiss_indicate_promisc();
- } else if (command == CMD_READY) {
+ } else if (cs->command == CMD_READY) {
if (!queue_full()) {
kiss_indicate_ready();
} else {
kiss_indicate_not_ready();
}
- } else if (command == CMD_UNLOCK_ROM) {
+ } else if (cs->command == CMD_UNLOCK_ROM) {
if (sbyte == ROM_UNLOCK_BYTE) {
unlock_rom();
}
- } else if (command == CMD_RESET) {
+ } else if (cs->command == CMD_RESET) {
if (sbyte == CMD_RESET_BYTE) {
hard_reset();
}
- } else if (command == CMD_ROM_READ) {
+ } else if (cs->command == CMD_ROM_READ) {
kiss_dump_eeprom();
- } else if (command == CMD_CFG_READ) {
+ } else if (cs->command == CMD_CFG_READ) {
kiss_dump_config();
- } else if (command == CMD_ROM_WRITE) {
+ } else if (cs->command == CMD_ROM_WRITE) {
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == 2) {
- eeprom_write(cmdbuf[0], cmdbuf[1]);
+ if (cs->frame_len == 2) {
+ eeprom_write(cs->cmdbuf[0], cs->cmdbuf[1]);
}
- } else if (command == CMD_FW_VERSION) {
+ } else if (cs->command == CMD_FW_VERSION) {
kiss_indicate_version();
- } else if (command == CMD_PLATFORM) {
+ } else if (cs->command == CMD_PLATFORM) {
kiss_indicate_platform();
- } else if (command == CMD_MCU) {
+ } else if (cs->command == CMD_MCU) {
kiss_indicate_mcu();
- } else if (command == CMD_BOARD) {
+ } else if (cs->command == CMD_BOARD) {
kiss_indicate_board();
- } else if (command == CMD_CONF_SAVE) {
+ } else if (cs->command == CMD_CONF_SAVE) {
eeprom_conf_save();
- } else if (command == CMD_CONF_DELETE) {
+ } else if (cs->command == CMD_CONF_DELETE) {
eeprom_conf_delete();
- } else if (command == CMD_FB_EXT) {
+ } else if (cs->command == CMD_FB_EXT) {
#if HAS_DISPLAY == true
if (sbyte == 0xFF) {
kiss_indicate_fbstate();
@@ -1057,60 +1363,60 @@ void serial_callback(uint8_t sbyte) {
kiss_indicate_fbstate();
}
#endif
- } else if (command == CMD_FB_WRITE) {
+ } else if (cs->command == CMD_FB_WRITE) {
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
#if HAS_DISPLAY
- if (frame_len == 9) {
- uint8_t line = cmdbuf[0];
+ if (cs->frame_len == 9) {
+ uint8_t line = cs->cmdbuf[0];
if (line > 63) line = 63;
int fb_o = line*8;
- memcpy(fb+fb_o, cmdbuf+1, 8);
+ memcpy(fb+fb_o, cs->cmdbuf+1, 8);
}
#endif
- } else if (command == CMD_FB_READ) {
+ } else if (cs->command == CMD_FB_READ) {
if (sbyte != 0x00) { kiss_indicate_fb(); }
- } else if (command == CMD_DISP_READ) {
+ } else if (cs->command == CMD_DISP_READ) {
if (sbyte != 0x00) { kiss_indicate_disp(); }
- } else if (command == CMD_DEV_HASH) {
+ } else if (cs->command == CMD_DEV_HASH) {
#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
if (sbyte != 0x00) {
kiss_indicate_device_hash();
}
#endif
- } else if (command == CMD_DEV_SIG) {
+ } else if (cs->command == CMD_DEV_SIG) {
#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == DEV_SIG_LEN) {
- memcpy(dev_sig, cmdbuf, DEV_SIG_LEN);
+ if (cs->frame_len == DEV_SIG_LEN) {
+ memcpy(dev_sig, cs->cmdbuf, DEV_SIG_LEN);
device_save_signature();
}
#endif
- } else if (command == CMD_FW_UPD) {
+ } else if (cs->command == CMD_FW_UPD) {
if (sbyte == 0x01) {
firmware_update_mode = true;
} else {
firmware_update_mode = false;
}
- } else if (command == CMD_HASHES) {
+ } else if (cs->command == CMD_HASHES) {
#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
if (sbyte == 0x01) {
kiss_indicate_target_fw_hash();
@@ -1122,29 +1428,29 @@ void serial_callback(uint8_t sbyte) {
kiss_indicate_partition_table_hash();
}
#endif
- } else if (command == CMD_FW_HASH) {
+ } else if (cs->command == CMD_FW_HASH) {
#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == DEV_HASH_LEN) {
- memcpy(dev_firmware_hash_target, cmdbuf, DEV_HASH_LEN);
+ if (cs->frame_len == DEV_HASH_LEN) {
+ memcpy(dev_firmware_hash_target, cs->cmdbuf, DEV_HASH_LEN);
device_save_firmware_hash();
}
#endif
- } else if (command == CMD_WIFI_CHN) {
+ } else if (cs->command == CMD_WIFI_CHN) {
#if HAS_WIFI
if (sbyte > 0 && sbyte < 14) { eeprom_update(eeprom_addr(ADDR_CONF_WCHN), sbyte); }
#endif
- } else if (command == CMD_WIFI_MODE) {
+ } else if (cs->command == CMD_WIFI_MODE) {
#if HAS_WIFI
if (sbyte == WR_WIFI_OFF || sbyte == WR_WIFI_STA || sbyte == WR_WIFI_AP) {
wr_conf_save(sbyte);
@@ -1152,73 +1458,163 @@ void serial_callback(uint8_t sbyte) {
wifi_remote_init();
}
#endif
- } else if (command == CMD_WIFI_SSID) {
+ } else if (cs->command == CMD_WIFI_SSID) {
#if HAS_WIFI
- if (sbyte == FESC) { ESCAPE = true; }
+ if (sbyte == FESC) { cs->escape = true; }
else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
if (sbyte == 0x00) {
for (uint8_t i = 0; i<33; i++) {
- if (iframe_len && i<32) { eeprom_update(config_addr(ADDR_CONF_SSID+i), cs->cmdbuf[i]); }
else { eeprom_update(config_addr(ADDR_CONF_SSID+i), 0x00); }
}
}
#endif
- } else if (command == CMD_WIFI_PSK) {
+ } else if (cs->command == CMD_WIFI_PSK) {
#if HAS_WIFI
- if (sbyte == FESC) { ESCAPE = true; }
+ if (sbyte == FESC) { cs->escape = true; }
else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
if (sbyte == 0x00) {
for (uint8_t i = 0; i<33; i++) {
- if (iframe_len && i<32) { eeprom_update(config_addr(ADDR_CONF_PSK+i), cs->cmdbuf[i]); }
else { eeprom_update(config_addr(ADDR_CONF_PSK+i), 0x00); }
}
}
#endif
- } else if (command == CMD_WIFI_IP) {
+ } else if (cs->command == CMD_WIFI_IP) {
#if HAS_WIFI
- if (sbyte == FESC) { ESCAPE = true; }
+ if (sbyte == FESC) { cs->escape = true; }
else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == 4) { for (uint8_t i = 0; i<4; i++) { eeprom_update(config_addr(ADDR_CONF_IP+i), cmdbuf[i]); } }
+ if (cs->frame_len == 4) { for (uint8_t i = 0; i<4; i++) { eeprom_update(config_addr(ADDR_CONF_IP+i), cs->cmdbuf[i]); } }
#endif
- } else if (command == CMD_WIFI_NM) {
+ } else if (cs->command == CMD_WIFI_NM) {
#if HAS_WIFI
- if (sbyte == FESC) { ESCAPE = true; }
+ if (sbyte == FESC) { cs->escape = true; }
else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
- if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
}
- if (frame_len == 4) { for (uint8_t i = 0; i<4; i++) { eeprom_update(config_addr(ADDR_CONF_NM+i), cmdbuf[i]); } }
+ if (cs->frame_len == 4) { for (uint8_t i = 0; i<4; i++) { eeprom_update(config_addr(ADDR_CONF_NM+i), cs->cmdbuf[i]); } }
+ #endif
+ } else if (cs->command == CMD_BCN_KEY) {
+ #if HAS_GPS == true
+ if (sbyte == FESC) { cs->escape = true; }
+ else {
+ if (cs->escape) {
+ if (sbyte == TFEND) sbyte = FEND;
+ if (sbyte == TFESC) sbyte = FESC;
+ cs->escape = false;
+ }
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
+ }
+ // 64 bytes: 32B X25519 pub key + 16B identity hash + 16B dest hash
+ if (cs->frame_len == 64) {
+ for (int i = 0; i < 32; i++)
+ eeprom_update(config_addr(ADDR_BCN_KEY + i), cs->cmdbuf[i]);
+ for (int i = 0; i < 16; i++)
+ eeprom_update(config_addr(ADDR_BCN_IHASH + i), cs->cmdbuf[32 + i]);
+ for (int i = 0; i < 16; i++)
+ eeprom_update(config_addr(ADDR_BCN_DHASH + i), cs->cmdbuf[48 + i]);
+ eeprom_update(config_addr(ADDR_BCN_OK), CONF_OK_BYTE);
+ // Load into RAM immediately
+ memcpy(collector_pub_key, cs->cmdbuf, 32);
+ memcpy(collector_identity_hash, cs->cmdbuf + 32, 16);
+ memcpy(collector_dest_hash, cs->cmdbuf + 48, 16);
+ beacon_crypto_configured = true;
+ lxmf_provisioned_at = millis();
+ kiss_indicate_ready();
+ }
+ #endif
+ } else if (cs->command == CMD_LXMF_HASH) {
+ #if HAS_GPS == true
+ // Return the RNode's LXMF source hash (16 bytes) for display/debugging.
+ // Any byte triggers the response (query command).
+ if (lxmf_identity_configured) {
+ serial_write(FEND);
+ serial_write(CMD_LXMF_HASH);
+ for (int i = 0; i < 16; i++) {
+ uint8_t b = lxmf_source_hash[i];
+ if (b == FEND) { serial_write(FESC); serial_write(TFEND); }
+ else if (b == FESC) { serial_write(FESC); serial_write(TFESC); }
+ else serial_write(b);
+ }
+ serial_write(FEND);
+ }
+ #endif
+ } else if (cs->command == CMD_LXMF_TEST) {
+ #if HAS_GPS == true
+ // Force-trigger LXMF announce + beacon for USB testing.
+ // Emits pre-encryption plaintext as CMD_DIAG frames.
+ lxmf_test_send();
+ #endif
+ } else if (cs->command == CMD_IFAC_KEY) {
+ #if HAS_GPS == true
+ if (sbyte == FESC) { cs->escape = true; }
+ else {
+ if (cs->escape) {
+ if (sbyte == TFEND) sbyte = FEND;
+ if (sbyte == TFESC) sbyte = FESC;
+ cs->escape = false;
+ }
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
+ }
+ // 64 bytes: IFAC key derived from network_name + passphrase
+ if (cs->frame_len == 64) {
+ memcpy(ifac_key, cs->cmdbuf, 64);
+ ifac_nvs_save();
+ ifac_derive_keypair();
+ ifac_configured = true;
+ kiss_indicate_ready();
+ }
#endif
- } else if (command == CMD_BT_CTRL) {
+ } else if (cs->command == CMD_TRANSPORT_ID) {
+ #if HAS_GPS == true
+ if (sbyte == FESC) { cs->escape = true; }
+ else {
+ if (cs->escape) {
+ if (sbyte == TFEND) sbyte = FEND;
+ if (sbyte == TFESC) sbyte = FESC;
+ cs->escape = false;
+ }
+ if (cs->frame_len < CMD_L) cs->cmdbuf[cs->frame_len++] = sbyte;
+ }
+ // 16 bytes: transport node's identity hash
+ if (cs->frame_len == 16) {
+ memcpy(transport_id, cs->cmdbuf, 16);
+ lxmf_nvs_save_transport_id();
+ transport_configured = true;
+ kiss_indicate_ready();
+ }
+ #endif
+ } else if (cs->command == CMD_BT_CTRL) {
#if HAS_BLUETOOTH || HAS_BLE
if (sbyte == 0x00) {
bt_stop();
@@ -1236,101 +1632,101 @@ void serial_callback(uint8_t sbyte) {
}
}
#endif
- } else if (command == CMD_BT_UNPAIR) {
+ } else if (cs->command == CMD_BT_UNPAIR) {
#if HAS_BLE
if (sbyte == 0x01) { bt_debond_all(); }
#endif
- } else if (command == CMD_DISP_INT) {
+ } else if (cs->command == CMD_DISP_INT) {
#if HAS_DISPLAY
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
display_intensity = sbyte;
di_conf_save(display_intensity);
display_unblank();
}
#endif
- } else if (command == CMD_DISP_ADDR) {
+ } else if (cs->command == CMD_DISP_ADDR) {
#if HAS_DISPLAY
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
display_addr = sbyte;
da_conf_save(display_addr);
}
#endif
- } else if (command == CMD_DISP_BLNK) {
+ } else if (cs->command == CMD_DISP_BLNK) {
#if HAS_DISPLAY
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
db_conf_save(sbyte);
display_unblank();
}
#endif
- } else if (command == CMD_DISP_ROT) {
+ } else if (cs->command == CMD_DISP_ROT) {
#if HAS_DISPLAY
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
drot_conf_save(sbyte);
display_unblank();
}
#endif
- } else if (command == CMD_DIS_IA) {
+ } else if (cs->command == CMD_DIS_IA) {
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
dia_conf_save(sbyte);
}
- } else if (command == CMD_DISP_RCND) {
+ } else if (cs->command == CMD_DISP_RCND) {
#if HAS_DISPLAY
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
if (sbyte > 0x00) recondition_display = true;
}
#endif
- } else if (command == CMD_NP_INT) {
+ } else if (cs->command == CMD_NP_INT) {
#if HAS_NP
if (sbyte == FESC) {
- ESCAPE = true;
+ cs->escape = true;
} else {
- if (ESCAPE) {
+ if (cs->escape) {
if (sbyte == TFEND) sbyte = FEND;
if (sbyte == TFESC) sbyte = FESC;
- ESCAPE = false;
+ cs->escape = false;
}
sbyte;
led_set_intensity(sbyte);
@@ -1654,7 +2050,18 @@ void tx_queue_handler() {
void work_while_waiting() { loop(); }
void loop() {
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ esp_task_wdt_reset(); // Feed watchdog
+ uint32_t _prof_t0 = micros(), _prof_t1;
+ #endif
+
if (radio_online) {
+ // Process deferred RX interrupt from main context
+ // (avoids SPI bus contention from ISR)
+ #if MODEM == SX1262
+ if (LoRa->rxPending()) { LoRa->processRxInterrupt(); }
+ #endif
+
#if MCU_VARIANT == MCU_ESP32
modem_packet_t *modem_packet = NULL;
if(modem_packet_queue && xQueueReceive(modem_packet_queue, &modem_packet, 0) == pdTRUE && modem_packet) {
@@ -1665,6 +2072,13 @@ void loop() {
free(modem_packet);
modem_packet = NULL;
+ // Log received packet for message viewer
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ msg_log_parse_packet(pbuf, host_write_len, last_rssi,
+ (int8_t)(last_snr_raw > 127 ? last_snr_raw - 256 : last_snr_raw));
+ #endif
+
+ response_channel = data_channel;
kiss_indicate_stat_rssi();
kiss_indicate_stat_snr();
kiss_write_packet();
@@ -1686,6 +2100,7 @@ void loop() {
last_rssi = LoRa->packetRssi();
last_snr_raw = LoRa->packetSnrRaw();
portEXIT_CRITICAL();
+ response_channel = data_channel;
kiss_indicate_stat_rssi();
kiss_indicate_stat_snr();
kiss_write_packet();
@@ -1699,7 +2114,7 @@ void loop() {
tx_queue_handler();
check_modem_status();
-
+
} else {
if (hw_ready) {
if (console_active) {
@@ -1712,29 +2127,89 @@ void loop() {
} else {
led_indicate_not_ready();
- stopRadio();
+ // Don't call stopRadio() — it calls SPI.end() which kills the bus.
+ // rnsd can still configure the radio via KISS even without hw_ready.
}
}
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ _prof_t1 = micros(); prof_radio_us = _prof_t1 - _prof_t0; _prof_t0 = _prof_t1;
+ #endif
+
#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
buffer_serial();
- if (!fifo_isempty(&serialFIFO)) serial_poll();
+ {
+ bool has_data = false;
+ for (uint8_t ch = 0; ch < NUM_CHANNELS; ch++) {
+ if (!fifo_isempty(&channelFIFO[ch])) { has_data = true; break; }
+ }
+ if (has_data) serial_poll();
+ }
#else
- if (!fifo_isempty_locked(&serialFIFO)) serial_poll();
+ if (!fifo_isempty_locked(&channelFIFO[CHANNEL_USB])) serial_poll();
+ #endif
+
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ _prof_t1 = micros(); prof_serial_us = _prof_t1 - _prof_t0; _prof_t0 = _prof_t1;
#endif
#if HAS_DISPLAY
if (disp_ready && !display_updating) update_display();
#endif
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ _prof_t1 = micros(); prof_display_us = _prof_t1 - _prof_t0; _prof_t0 = _prof_t1;
+ #endif
+
#if HAS_PMU
if (pmu_ready) update_pmu();
#endif
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ _prof_t1 = micros(); prof_pmu_us = _prof_t1 - _prof_t0; _prof_t0 = _prof_t1;
+ #endif
+
+ #if HAS_GPS == true
+ if (gps_ready) {
+ gps_update();
+ if (hw_ready) beacon_update(); // beacon needs provisioned radio
+ #if HAS_RTC == true
+ if (gps_has_fix) rtc_sync_from_gps(gps_parser);
+ #endif
+
+ // Enter beacon sleep cycle when in standalone mode after beacon TX
+ // Don't sleep when on external power (USB) — keeps display and debug active
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ if (beacon_mode_active && beacon_gate == 6 &&
+ battery_state != BATTERY_STATE_CHARGING &&
+ battery_state != BATTERY_STATE_CHARGED &&
+ (last_host_activity == 0 || (millis() - last_host_activity >= BEACON_NO_HOST_TIMEOUT_MS))) {
+ sleep_now();
+ }
+ #endif
+ }
+ #endif
+
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ _prof_t1 = micros(); prof_gps_us = _prof_t1 - _prof_t0; _prof_t0 = _prof_t1;
+ #endif
+
+ #if HAS_RTC == true
+ static uint32_t rtc_last_read = 0;
+ if (rtc_ready && (millis() - rtc_last_read >= 1000)) {
+ rtc_read_time();
+ rtc_last_read = millis();
+ }
+ #endif
+
#if HAS_BLUETOOTH || HAS_BLE == true
if (!console_active && bt_ready) update_bt();
#endif
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ _prof_t1 = micros(); prof_bt_us = _prof_t1 - _prof_t0; _prof_t0 = _prof_t1;
+ #endif
+
#if HAS_WIFI
if (wifi_initialized) update_wifi();
#endif
@@ -1743,6 +2218,175 @@ void loop() {
input_read();
#endif
+ // Touch panel — IRQ-driven display wake (LVGL handles touch input via polling)
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ if (touch_ready && touch_irq) {
+ touch_irq = false;
+ #if HAS_DISPLAY
+ if (display_blanked) display_unblank();
+ #endif
+ }
+
+ // Screenshot: long-press BOOT button (GPIO 0) for 2 seconds
+ #if HAS_SD && HAS_DISPLAY
+ {
+ static uint32_t btn_down_since = 0;
+ static bool btn_action_taken = false;
+ if (digitalRead(0) == LOW) {
+ if (btn_down_since == 0) btn_down_since = millis();
+ // Long press (2s): screenshot to SD
+ if (!btn_action_taken && millis() - btn_down_since > 2000) {
+ btn_action_taken = true;
+ if (drv2605_ready) drv2605_play(HAPTIC_DOUBLE_CLICK);
+ gui_screenshot_sd();
+ }
+ } else {
+ // Short press: navigate home
+ if (btn_down_since > 0 && !btn_action_taken && millis() - btn_down_since > 50) {
+ if (display_blanked) {
+ display_unblank();
+ } else {
+ lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_ON);
+ if (drv2605_ready) drv2605_play(HAPTIC_LIGHT_CLICK);
+ }
+ }
+ btn_down_since = 0;
+ btn_action_taken = false;
+ }
+ }
+ #endif
+ #endif
+
+ // USB MSC SD card mode is toggled on demand via debug command 'D'
+
+ // Deferred BHI260AP init — runs once after boot is complete
+ // Firmware upload takes ~10s and blocks, so we do it after radio is up
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ static uint32_t bhi260_next_try = 5000;
+ if (!bhi260_ready && millis() > bhi260_next_try) {
+ bhi260_next_try = millis() + 10000; // retry every 10s
+ if (bhi260 == NULL) {
+ bhi260 = new SensorBHI260AP();
+ }
+ Wire.setClock(1000000UL);
+ bhi260->setPins(-1);
+ bhi260->setFirmware(bosch_firmware_image, bosch_firmware_size, false);
+ bhi260->setBootFromFlash(false);
+ if (bhi260->begin(Wire, 0x28, I2C_SDA, I2C_SCL)) {
+ bhi260_ready = true;
+ pinMode(SENSOR_INT, INPUT);
+
+ // Enable wrist tilt gesture for display wake
+ bhi260->configure(SensorBHI260AP::WRIST_TILT_GESTURE, 1.0, 0);
+ bhi260->onResultEvent(SensorBHI260AP::WRIST_TILT_GESTURE, imu_wrist_tilt_cb);
+
+ // Always-on accelerometer at 10Hz for bubble level
+ bhi260->configure(SensorBHI260AP::ACCEL_PASSTHROUGH, 10.0, 0);
+ bhi260->onResultEvent(SensorBHI260AP::ACCEL_PASSTHROUGH, imu_accel_live_cb);
+
+ // Register IMU log toggle for remote debug
+ #if HAS_SD && HAS_DISPLAY
+ gui_log_toggle_fn = []() -> bool {
+ if (!imu_logging) {
+ return imu_log_start(bhi260);
+ } else {
+ imu_log_stop(bhi260);
+ return false;
+ }
+ };
+ gui_list_files_fn = []() {
+ if (shared_spi_mutex) xSemaphoreTake(shared_spi_mutex, portMAX_DELAY);
+ SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
+ if (SD.begin(SD_CS, SPI, 4000000, "/sd", 5)) {
+ Serial.print("{\"files\":[");
+ File root = SD.open("/");
+ bool first = true;
+ File f;
+ while ((f = root.openNextFile())) {
+ if (!first) Serial.print(",");
+ Serial.printf("{\"name\":\"%s\",\"size\":%lu}", f.name(), (unsigned long)f.size());
+ first = false;
+ f.close();
+ }
+ root.close();
+ SD.end();
+ Serial.println("]}");
+ } else {
+ Serial.println("{\"error\":\"sd_init_failed\"}");
+ }
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ };
+ gui_download_file_fn = [](uint8_t index) {
+ if (shared_spi_mutex) xSemaphoreTake(shared_spi_mutex, portMAX_DELAY);
+ SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
+ if (SD.begin(SD_CS, SPI, 4000000, "/sd", 5)) {
+ File root = SD.open("/");
+ File f;
+ uint8_t i = 0;
+ while ((f = root.openNextFile())) {
+ if (i == index) {
+ Serial.printf("{\"name\":\"%s\",\"size\":%lu}\n", f.name(), (unsigned long)f.size());
+ uint8_t buf[512];
+ while (f.available()) {
+ int n = f.read(buf, sizeof(buf));
+ Serial.write(buf, n);
+ }
+ f.close();
+ root.close();
+ SD.end();
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ return;
+ }
+ f.close();
+ i++;
+ }
+ root.close();
+ SD.end();
+ Serial.printf("{\"error\":\"index %d not found\"}\n", index);
+ } else {
+ Serial.println("{\"error\":\"sd_init_failed\"}");
+ }
+ if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
+ };
+ #endif
+
+ // Enable step counter (low power, always-on)
+ bhi260->configure(SensorBHI260AP::STEP_COUNTER, 1.0, 0);
+ bhi260->onResultEvent(SensorBHI260AP::STEP_COUNTER, imu_step_cb);
+ }
+ Wire.setClock(400000UL);
+ }
+
+ // Process IMU events and handle wrist wake
+ if (bhi260_ready) {
+ bhi260->update();
+ if (imu_wrist_tilt) {
+ imu_wrist_tilt = false;
+ #if HAS_DISPLAY
+ if (display_blanked) {
+ display_unblank();
+ if (drv2605_ready) drv2605_play(HAPTIC_LIGHT_CLICK);
+ }
+ #endif
+ }
+ #if HAS_SD
+ if (imu_logging) {
+ imu_log_flush();
+ // Log GPS at 1Hz when logging
+ static uint32_t last_gps_log = 0;
+ if (gps_has_fix && millis() - last_gps_log >= 1000) {
+ sensor_log_gps(gps_lat, gps_lon, gps_alt, gps_speed, gps_hdop, gps_sats);
+ last_gps_log = millis();
+ }
+ }
+ #endif
+ }
+ #endif
+
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ _prof_t1 = micros(); prof_imu_us = _prof_t1 - _prof_t0;
+ #endif
+
if (memory_low) {
#if PLATFORM == PLATFORM_ESP32
if (esp_get_free_heap_size() < 8192) {
@@ -1754,8 +2398,105 @@ void loop() {
kiss_indicate_error(ERROR_MEMORY_LOW); memory_low = false;
#endif
}
+
}
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+// Shared deep sleep entry for T-Watch Ultra.
+// Safely shuts down peripherals and enters ESP32 deep sleep.
+// Does not return — device reboots on wake.
+void twatch_enter_deep_sleep(bool beacon_timer) {
+ // 0. Haptic feedback before sleep
+ if (drv2605_ready) {
+ drv2605_play(HAPTIC_SOFT_BUMP);
+ delay(150); // Let the motor spin briefly before powering down
+ }
+
+ // 1. Shut down audio and display before closing buses
+ mic_end();
+ speaker_end();
+ #if HAS_DISPLAY
+ co5300_sleep();
+ #endif
+
+ // 2. Gate display VCI power and disable haptics via XL9555
+ xl9555_sleep_prepare();
+
+ // 3. Disable PMU peripheral rails (no PMU->enableSleep — that bricks I2C!)
+ pmu_prepare_sleep();
+
+ // 4. Close communication buses
+ #if HAS_GPS
+ gps_serial.end();
+ #endif
+ Serial1.end();
+ SPI.end();
+ Wire.end();
+
+ // 5. Reset unused GPIOs to INPUT (minimal leakage)
+ // DO NOT touch I2C pins (GPIO 2/3) — external pullups, and setting
+ // them to OPEN_DRAIN persists across battery-backed resets, bricking I2C.
+ const uint8_t sleep_pins[] = {
+ DISP_D0, DISP_D1, DISP_D2, DISP_D3,
+ DISP_SCK, DISP_CS, DISP_TE, DISP_RST,
+ RTC_INT, NFC_INT, SENSOR_INT, NFC_CS,
+ I2S_BCLK, I2S_WCLK, I2S_DOUT, SD_CS,
+ pin_mosi, pin_miso, pin_sclk, pin_cs,
+ PIN_GPS_TX, PIN_GPS_RX, PIN_GPS_PPS,
+ pin_reset, pin_busy, pin_dio,
+ };
+ for (auto p : sleep_pins) {
+ gpio_reset_pin((gpio_num_t)p); // Resets to INPUT, clears any drive
+ }
+
+ // 6. Configure wakeup sources
+ esp_sleep_enable_ext1_wakeup(1ULL << PMU_IRQ, ESP_EXT1_WAKEUP_ANY_LOW);
+ if (beacon_timer) {
+ esp_sleep_enable_timer_wakeup((uint64_t)beacon_interval_ms * 1000ULL);
+ }
+
+ // 7. Enter deep sleep (does not return)
+ esp_deep_sleep_start();
+}
+
+#if HAS_GPS == true
+// Minimal boot path for beacon timer wakeup.
+// Inits only GPS + LoRa, waits for fix, transmits beacon, sleeps again.
+// Called from setup() on timer wake. Does not return.
+void beacon_wake_cycle() {
+ gps_setup();
+
+ // Load beacon crypto config from EEPROM
+ if (EEPROM.read(config_addr(ADDR_BCN_OK)) == CONF_OK_BYTE) {
+ for (int i = 0; i < 32; i++)
+ collector_pub_key[i] = EEPROM.read(config_addr(ADDR_BCN_KEY + i));
+ for (int i = 0; i < 16; i++)
+ collector_identity_hash[i] = EEPROM.read(config_addr(ADDR_BCN_IHASH + i));
+ for (int i = 0; i < 16; i++)
+ collector_dest_hash[i] = EEPROM.read(config_addr(ADDR_BCN_DHASH + i));
+ beacon_crypto_configured = true;
+ }
+ lxmf_init_identity();
+
+ // Wait for GPS fix (up to 60 seconds for warm start)
+ uint32_t fix_start = millis();
+ while (!gps_has_fix && (millis() - fix_start < 60000)) {
+ gps_update();
+ delay(100);
+ }
+
+ if (gps_has_fix) {
+ last_host_activity = 0;
+ last_beacon_tx = 0;
+ beacon_update();
+ }
+
+ stopRadio();
+ twatch_enter_deep_sleep(true); // Sleep with beacon timer
+}
+#endif
+#endif
+
void sleep_now() {
#if HAS_SLEEP == true
stopRadio(); // TODO: Check this on all platforms
@@ -1782,8 +2523,19 @@ void sleep_now() {
delay(100);
}
#endif
- esp_sleep_enable_ext0_wakeup(PIN_WAKEUP, WAKEUP_LEVEL);
- esp_deep_sleep_start();
+
+ #if BOARD_MODEL == BOARD_TWATCH_ULT
+ #if HAS_GPS == true
+ bool use_beacon_timer = beacon_mode_active;
+ #else
+ bool use_beacon_timer = false;
+ #endif
+ twatch_enter_deep_sleep(use_beacon_timer);
+
+ #else
+ esp_sleep_enable_ext0_wakeup(PIN_WAKEUP, WAKEUP_LEVEL);
+ esp_deep_sleep_start();
+ #endif
#elif PLATFORM == PLATFORM_NRF52
#if BOARD_MODEL == BOARD_HELTEC_T114
npset(0,0,0);
@@ -1847,13 +2599,20 @@ void serial_poll() {
serial_polling = true;
#if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
- while (!fifo_isempty_locked(&serialFIFO)) {
+ while (!fifo_isempty_locked(&channelFIFO[CHANNEL_USB])) {
+ char sbyte = fifo_pop(&channelFIFO[CHANNEL_USB]);
+ response_channel = CHANNEL_USB;
+ serial_callback(sbyte, CHANNEL_USB);
+ }
#else
- while (!fifo_isempty(&serialFIFO)) {
- #endif
- char sbyte = fifo_pop(&serialFIFO);
- serial_callback(sbyte);
+ for (uint8_t ch = 0; ch < NUM_CHANNELS; ch++) {
+ while (!fifo_isempty(&channelFIFO[ch])) {
+ char sbyte = fifo_pop(&channelFIFO[ch]);
+ response_channel = ch;
+ serial_callback(sbyte, ch);
+ }
}
+ #endif
serial_polling = false;
}
@@ -1867,35 +2626,41 @@ void buffer_serial() {
if (!serial_buffering) {
serial_buffering = true;
- uint8_t c = 0;
+ uint8_t c;
- #if HAS_BLUETOOTH || HAS_BLE == true
- while (
- c < MAX_CYCLES &&
- #if HAS_WIFI
- ( (bt_state != BT_STATE_CONNECTED && Serial.available()) || (bt_state == BT_STATE_CONNECTED && SerialBT.available()) || (wr_state >= WR_STATE_ON && wifi_remote_available()) )
- #else
- ( (bt_state != BT_STATE_CONNECTED && Serial.available()) || (bt_state == BT_STATE_CONNECTED && SerialBT.available()) )
- #endif
- )
+ // USB — always read
+ c = 0;
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ while (c < MAX_CYCLES && Serial.available()) {
+ c++;
+ if (!fifo_isfull_locked(&channelFIFO[CHANNEL_USB])) { fifo_push_locked(&channelFIFO[CHANNEL_USB], Serial.read()); }
+ }
#else
- while (c < MAX_CYCLES && Serial.available())
+ while (c < MAX_CYCLES && Serial.available()) {
+ c++;
+ uint8_t sb = Serial.read();
+ #if BOARD_MODEL == BOARD_TWATCH_ULT && HAS_DISPLAY
+ gui_process_serial_byte(sb);
+ #endif
+ if (!fifo_isfull(&channelFIFO[CHANNEL_USB])) { fifo_push(&channelFIFO[CHANNEL_USB], sb); }
+ }
#endif
- {
+
+ #if HAS_BLUETOOTH || HAS_BLE == true
+ c = 0;
+ while (c < MAX_CYCLES && bt_state == BT_STATE_CONNECTED && SerialBT.available()) {
c++;
+ if (!fifo_isfull(&channelFIFO[CHANNEL_BT])) { fifo_push(&channelFIFO[CHANNEL_BT], SerialBT.read()); }
+ }
+ #endif
- #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
- if (!fifo_isfull_locked(&serialFIFO)) { fifo_push_locked(&serialFIFO, Serial.read()); }
- #elif HAS_BLUETOOTH || HAS_BLE == true || HAS_WIFI
- if (bt_state == BT_STATE_CONNECTED) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, SerialBT.read()); } }
- #if HAS_WIFI
- else if (wifi_host_is_connected()) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, wifi_remote_read()); } }
- #endif
- else { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); } }
- #else
- if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); }
- #endif
+ #if HAS_WIFI == true
+ c = 0;
+ while (c < MAX_CYCLES && wifi_host_is_connected() && wifi_remote_available()) {
+ c++;
+ if (!fifo_isfull(&channelFIFO[CHANNEL_WIFI])) { fifo_push(&channelFIFO[CHANNEL_WIFI], wifi_remote_read()); }
}
+ #endif
serial_buffering = false;
}
diff --git a/ROM.h b/ROM.h
index c8834348..8f5f6e59 100644
--- a/ROM.h
+++ b/ROM.h
@@ -58,6 +58,20 @@
#define ADDR_CONF_PSK 0x21
#define ADDR_CONF_IP 0x42
#define ADDR_CONF_NM 0x46
+
+ // Beacon encryption config — stored in config region via config_addr()
+ // Primary region (eeprom_addr) is full past 0xBB; these use config space
+ // after WiFi config (SSID/PSK/IP/NM end at 0x49)
+ #define ADDR_BCN_OK 0x50 // Config valid flag (0x73 = valid) — 1 byte
+ #define ADDR_BCN_KEY 0x51 // Collector X25519 public key — 32 bytes (0x51-0x70)
+ #define ADDR_BCN_IHASH 0x71 // Collector identity hash — 16 bytes (0x71-0x80)
+ #define ADDR_BCN_DHASH 0x81 // Collector dest hash — 16 bytes (0x81-0x90)
+
+ // User settings — stored in config region via config_addr()
+ #define ADDR_CONF_DISP_TIMEOUT 0x91 // Display blank timeout in seconds — 1 byte
+ #define ADDR_CONF_BCN_INT 0x92 // Beacon interval index — 1 byte
+ #define ADDR_CONF_GPS_MODEL 0x93 // GPS dynamic model index — 1 byte
+ #define ADDR_CONF_BCN_EN 0x94 // Beacon enable (0=off, 1=on) — 1 byte
//////////////////////////////////
#endif
diff --git a/RTC.h b/RTC.h
new file mode 100644
index 00000000..99bd87a6
--- /dev/null
+++ b/RTC.h
@@ -0,0 +1,147 @@
+// Copyright (C) 2026, GPS/RTC support contributed by GlassOnTin
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#ifndef RTC_H
+#define RTC_H
+
+#if HAS_RTC == true
+
+#include
+
+// RTC I2C address (shared by PCF8563 and PCF85063A)
+#define RTC_I2C_ADDR 0x51
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+ // PCF85063A registers (T-Watch Ultra)
+ #define RTC_REG_CTRL1 0x00
+ #define RTC_REG_CTRL2 0x01
+ #define RTC_REG_SEC 0x04
+ #define RTC_REG_MIN 0x05
+ #define RTC_REG_HOUR 0x06
+ #define RTC_REG_DAY 0x07
+ #define RTC_REG_WDAY 0x08
+ #define RTC_REG_MON 0x09
+ #define RTC_REG_YEAR 0x0A
+#else
+ // PCF8563 registers (T-Beam Supreme, etc.)
+ #define RTC_REG_CTRL1 0x00
+ #define RTC_REG_CTRL2 0x01
+ #define RTC_REG_SEC 0x02
+ #define RTC_REG_MIN 0x03
+ #define RTC_REG_HOUR 0x04
+ #define RTC_REG_DAY 0x05
+ #define RTC_REG_WDAY 0x06
+ #define RTC_REG_MON 0x07
+ #define RTC_REG_YEAR 0x08
+#endif
+
+bool rtc_ready = false;
+bool rtc_synced = false; // true once GPS time has been written to RTC
+uint32_t rtc_last_sync = 0;
+#define RTC_SYNC_INTERVAL 3600000 // re-sync from GPS every hour
+
+// Cached time from last RTC read
+uint8_t rtc_hour = 0;
+uint8_t rtc_minute = 0;
+uint8_t rtc_second = 0;
+uint8_t rtc_day = 0;
+uint8_t rtc_month = 0;
+uint16_t rtc_year = 0;
+
+static uint8_t bcd_to_dec(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); }
+static uint8_t dec_to_bcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }
+
+void rtc_setup() {
+ // Wire is already initialised by Power.h (PMU init) or Display.h.
+ Wire.beginTransmission(RTC_I2C_ADDR);
+ if (Wire.endTransmission() == 0) {
+ rtc_ready = true;
+
+ // Clear control registers (normal mode, no alarms)
+ Wire.beginTransmission(RTC_I2C_ADDR);
+ Wire.write(RTC_REG_CTRL1);
+ Wire.write(0x00); // normal mode
+ Wire.write(0x00); // control/status 2: no alarms/timer
+ Wire.endTransmission();
+ }
+}
+
+bool rtc_read_time() {
+ if (!rtc_ready) return false;
+
+ Wire.beginTransmission(RTC_I2C_ADDR);
+ Wire.write(RTC_REG_SEC);
+ if (Wire.endTransmission() != 0) return false;
+
+ Wire.requestFrom((uint8_t)RTC_I2C_ADDR, (uint8_t)7);
+ if (Wire.available() < 7) return false;
+
+ uint8_t sec = Wire.read();
+ uint8_t min = Wire.read();
+ uint8_t hour = Wire.read();
+ uint8_t day = Wire.read();
+ Wire.read(); // weekday — skip
+ uint8_t mon = Wire.read();
+ uint8_t year = Wire.read();
+
+ // Check oscillator stop bit (sec register bit 7)
+ // Set on both PCF8563 and PCF85063A when clock integrity is lost
+ if (sec & 0x80) return false;
+
+ rtc_second = bcd_to_dec(sec & 0x7F);
+ rtc_minute = bcd_to_dec(min & 0x7F);
+ rtc_hour = bcd_to_dec(hour & 0x3F);
+ rtc_day = bcd_to_dec(day & 0x3F);
+ rtc_month = bcd_to_dec(mon & 0x1F);
+ rtc_year = 2000 + bcd_to_dec(year);
+
+ return true;
+}
+
+bool rtc_write_time(uint16_t year, uint8_t month, uint8_t day,
+ uint8_t hour, uint8_t minute, uint8_t second) {
+ if (!rtc_ready) return false;
+
+ Wire.beginTransmission(RTC_I2C_ADDR);
+ Wire.write(RTC_REG_SEC);
+ Wire.write(dec_to_bcd(second));
+ Wire.write(dec_to_bcd(minute));
+ Wire.write(dec_to_bcd(hour));
+ Wire.write(dec_to_bcd(day));
+ Wire.write(0x00); // weekday (not used)
+ Wire.write(dec_to_bcd(month));
+ Wire.write(dec_to_bcd(year - 2000));
+ return Wire.endTransmission() == 0;
+}
+
+// Called from gps_update() when GPS has a valid time fix.
+// Syncs RTC from GPS time at most once per RTC_SYNC_INTERVAL.
+void rtc_sync_from_gps(TinyGPSPlus &gps) {
+ if (!rtc_ready) return;
+ if (!gps.date.isValid() || !gps.time.isValid()) return;
+ if (gps.date.year() < 2024) return; // sanity check
+
+ uint32_t now = millis();
+ if (rtc_synced && (now - rtc_last_sync < RTC_SYNC_INTERVAL)) return;
+
+ if (rtc_write_time(gps.date.year(), gps.date.month(), gps.date.day(),
+ gps.time.hour(), gps.time.minute(), gps.time.second())) {
+ rtc_synced = true;
+ rtc_last_sync = now;
+ }
+}
+
+#endif
+#endif
diff --git a/SharedSPI.h b/SharedSPI.h
new file mode 100644
index 00000000..711e7485
--- /dev/null
+++ b/SharedSPI.h
@@ -0,0 +1,24 @@
+// SharedSPI — global mutex for the shared SPI bus (pins MISO=33, MOSI=34, CLK=35)
+//
+// Users of this bus MUST acquire shared_spi_mutex before any SPI transaction
+// and release it after. This prevents race conditions between:
+// - SX1262 LoRa radio (CS=36) — main loop, continuous polling
+// - SD card (CS=21) — USB MSC callbacks (TinyUSB task), IMU logger, screenshots
+// - ST25R3916 NFC (CS=4) — future, not yet implemented
+//
+// The CO5300 display uses a separate SPI3 bus and does NOT need this mutex.
+
+#ifndef SHARED_SPI_H
+#define SHARED_SPI_H
+
+#include
+
+extern SemaphoreHandle_t shared_spi_mutex;
+
+inline void shared_spi_init() {
+ if (!shared_spi_mutex) {
+ shared_spi_mutex = xSemaphoreCreateMutex();
+ }
+}
+
+#endif // SHARED_SPI_H
diff --git a/Speaker.h b/Speaker.h
new file mode 100644
index 00000000..1236c4d8
--- /dev/null
+++ b/Speaker.h
@@ -0,0 +1,110 @@
+// MAX98357A I2S Speaker Driver — Minimal tone generator for T-Watch Ultra
+// Plays simple alert tones via I2S. No WAV files needed.
+
+#ifndef SPEAKER_H
+#define SPEAKER_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT
+
+#include "driver/i2s.h"
+#include
+
+#define SPK_BCLK I2S_BCLK
+#define SPK_WCLK I2S_WCLK
+#define SPK_DOUT I2S_DOUT
+#define SPK_I2S_PORT I2S_NUM_1 // I2S_NUM_0 reserved for PDM microphone
+#define SPK_SAMPLE_RATE 16000
+#define SPK_TONE_BUF_SIZE 512
+
+static bool speaker_ready = false;
+
+bool speaker_init() {
+ i2s_config_t i2s_config = {};
+ i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
+ i2s_config.sample_rate = SPK_SAMPLE_RATE;
+ i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
+ i2s_config.channel_format = I2S_CHANNEL_FMT_ALL_RIGHT;
+ i2s_config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
+ i2s_config.dma_buf_count = 4;
+ i2s_config.dma_buf_len = 512;
+ i2s_config.use_apll = false;
+ i2s_config.tx_desc_auto_clear = true;
+
+ i2s_pin_config_t pin_config = {};
+ pin_config.bck_io_num = SPK_BCLK;
+ pin_config.ws_io_num = SPK_WCLK;
+ pin_config.data_out_num = SPK_DOUT;
+ pin_config.data_in_num = I2S_PIN_NO_CHANGE;
+
+ if (i2s_driver_install(SPK_I2S_PORT, &i2s_config, 0, NULL) != ESP_OK) {
+ return false;
+ }
+ if (i2s_set_pin(SPK_I2S_PORT, &pin_config) != ESP_OK) {
+ i2s_driver_uninstall(SPK_I2S_PORT);
+ return false;
+ }
+
+ speaker_ready = true;
+ return true;
+}
+
+void speaker_end() {
+ if (speaker_ready) {
+ i2s_driver_uninstall(SPK_I2S_PORT);
+ speaker_ready = false;
+ }
+}
+
+// Play a tone at given frequency (Hz) for given duration (ms) at volume (0-100)
+void speaker_tone(uint16_t freq, uint16_t duration_ms, uint8_t volume) {
+ if (!speaker_ready || freq == 0) return;
+
+ float vol = (float)volume / 100.0f;
+ int16_t amplitude = (int16_t)(16000 * vol); // ~half of int16 max
+ uint32_t total_samples = (uint32_t)SPK_SAMPLE_RATE * duration_ms / 1000;
+ int16_t buf[SPK_TONE_BUF_SIZE];
+
+ uint32_t samples_written = 0;
+ while (samples_written < total_samples) {
+ uint32_t chunk = total_samples - samples_written;
+ if (chunk > SPK_TONE_BUF_SIZE) chunk = SPK_TONE_BUF_SIZE;
+
+ for (uint32_t i = 0; i < chunk; i++) {
+ float t = (float)(samples_written + i) / (float)SPK_SAMPLE_RATE;
+ buf[i] = (int16_t)(amplitude * sinf(2.0f * M_PI * freq * t));
+ }
+
+ size_t bytes_written = 0;
+ i2s_write(SPK_I2S_PORT, buf, chunk * sizeof(int16_t), &bytes_written, portMAX_DELAY);
+ samples_written += chunk;
+ }
+
+ // Brief silence to flush DMA buffer
+ memset(buf, 0, sizeof(buf));
+ size_t dummy;
+ i2s_write(SPK_I2S_PORT, buf, sizeof(buf), &dummy, portMAX_DELAY);
+}
+
+// Predefined alert tones
+void speaker_beep() {
+ speaker_tone(1000, 100, 50); // Short 1kHz beep
+}
+
+void speaker_alert() {
+ speaker_tone(800, 200, 70);
+ speaker_tone(1200, 200, 70);
+}
+
+void speaker_success() {
+ speaker_tone(523, 100, 40); // C5
+ speaker_tone(659, 100, 40); // E5
+ speaker_tone(784, 150, 40); // G5
+}
+
+void speaker_error() {
+ speaker_tone(300, 300, 60);
+ speaker_tone(200, 400, 60);
+}
+
+#endif // BOARD_MODEL == BOARD_TWATCH_ULT
+#endif // SPEAKER_H
diff --git a/USBSD.h b/USBSD.h
new file mode 100644
index 00000000..fb08bc00
--- /dev/null
+++ b/USBSD.h
@@ -0,0 +1,112 @@
+// USB Mass Storage — exposes SD card via USB alongside CDC serial
+// Requires USBMode=default (TinyUSB) in the FQBN build flags.
+//
+// SD card and LoRa share the same SPI bus. Rather than mutex-based
+// concurrent access (unreliable due to timing), this uses exclusive
+// mode switching: LoRa sleeps when SD is active, and vice versa.
+// Toggle via remote debug command 'D' or watch UI.
+
+#ifndef USBSD_H
+#define USBSD_H
+
+#if BOARD_MODEL == BOARD_TWATCH_ULT && HAS_SD && !ARDUINO_USB_MODE
+
+#include "USB.h"
+#include "USBMSC.h"
+#include
+
+static USBMSC usb_msc;
+bool usb_sd_ready = false;
+bool usb_sd_mode = false; // true = SD/USB active, LoRa sleeping
+
+static int32_t usb_sd_read(uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize) {
+ if (!usb_sd_ready || !usb_sd_mode) return -1;
+ int32_t result = bufsize;
+ uint32_t sec = lba + (offset / 512);
+ uint32_t cnt = bufsize / 512;
+ for (uint32_t i = 0; i < cnt; i++) {
+ if (!SD.readRAW((uint8_t *)buffer + i * 512, sec + i)) { result = -1; break; }
+ }
+ return result;
+}
+
+static int32_t usb_sd_write(uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) {
+ if (!usb_sd_ready || !usb_sd_mode) return -1;
+ int32_t result = bufsize;
+ uint32_t sec = lba + (offset / 512);
+ uint32_t cnt = bufsize / 512;
+ for (uint32_t i = 0; i < cnt; i++) {
+ if (!SD.writeRAW(buffer + i * 512, sec + i)) { result = -1; break; }
+ }
+ return result;
+}
+
+static bool usb_sd_start_stop(uint8_t power_condition, bool start, bool load_eject) {
+ return true;
+}
+
+// Register USB MSC device (call early in setup, before USB enumeration)
+// Does NOT mount SD yet — call usb_sd_enable() to activate
+void usb_sd_register() {
+ usb_msc.vendorID("RNode");
+ usb_msc.productID("R-Watch SD");
+ usb_msc.productRevision("1.0");
+ usb_msc.onRead(usb_sd_read);
+ usb_msc.onWrite(usb_sd_write);
+ usb_msc.onStartStop(usb_sd_start_stop);
+ usb_msc.mediaPresent(false);
+ usb_msc.begin(0, 512);
+}
+
+// Activate SD mode: sleep LoRa, mount SD, expose via USB
+// Returns true if SD card mounted successfully
+bool usb_sd_enable() {
+ if (usb_sd_mode) return true;
+
+ // Put LoRa radio to sleep (releases SPI bus)
+ if (radio_online) {
+ // The sx126x sleep command is handled by the modem layer
+ // For now, just note that radio will be unavailable
+ }
+
+ // Mount SD card (exclusive SPI access now)
+ SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
+ if (!SD.begin(SD_CS, SPI, 4000000, "/sd", 5)) {
+ SPI.end();
+ return false;
+ }
+
+ uint32_t sectors = SD.numSectors();
+ uint16_t secsize = SD.sectorSize();
+ if (sectors == 0) {
+ SD.end(); SPI.end();
+ return false;
+ }
+
+ usb_msc.mediaPresent(true);
+ usb_msc.begin(sectors, secsize);
+ usb_sd_ready = true;
+ usb_sd_mode = true;
+ Serial.printf("[usb_sd] SD mode ON: %lu sectors (%.1f GB)\n",
+ sectors, (float)sectors * 512 / 1073741824.0);
+ return true;
+}
+
+// Deactivate SD mode: unmount SD, wake LoRa
+void usb_sd_disable() {
+ if (!usb_sd_mode) return;
+
+ usb_msc.mediaPresent(false);
+ usb_sd_ready = false;
+ usb_sd_mode = false;
+
+ // Unmount SD
+ SD.end();
+ SPI.end();
+
+ // LoRa will re-initialize on next main loop cycle
+ Serial.println("[usb_sd] SD mode OFF");
+}
+
+#endif
+#endif
diff --git a/Utilities.h b/Utilities.h
index 8892f59f..2ac5debe 100644
--- a/Utilities.h
+++ b/Utilities.h
@@ -15,6 +15,18 @@
#include "Config.h"
+#if HAS_GPS == true
+ #include "GPS.h"
+ #include "BeaconCrypto.h"
+ #include "IfacAuth.h"
+ #include "LxmfBeacon.h"
+ #include "Beacon.h"
+#endif
+
+#if HAS_RTC == true
+ #include "RTC.h"
+#endif
+
#if HAS_EEPROM
#include
#elif PLATFORM == PLATFORM_NRF52
@@ -250,6 +262,13 @@ uint8_t boot_vector = 0x00;
void led_tx_off() { }
void led_id_on() { }
void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_TWATCH_ULT
+ void led_rx_on() { }
+ void led_rx_off() { }
+ void led_tx_on() { }
+ void led_tx_off() { }
+ void led_id_on() { }
+ void led_id_off() { }
#elif BOARD_MODEL == BOARD_LORA32_V1_0
#if defined(EXTERNAL_LEDS)
void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
@@ -799,26 +818,28 @@ int8_t led_standby_direction = 0;
#endif
void serial_write(uint8_t byte) {
- #if HAS_BLUETOOTH || HAS_BLE == true
- if (bt_state != BT_STATE_CONNECTED) {
- #if HAS_WIFI
- if (wifi_host_is_connected()) { wifi_remote_write(byte); }
- else { Serial.write(byte); }
- #else
- Serial.write(byte);
- #endif
- } else {
+ switch (response_channel) {
+ #if HAS_BLUETOOTH || HAS_BLE == true
+ case CHANNEL_BT:
SerialBT.write(byte);
- #if MCU_VARIANT == MCU_NRF52 && HAS_BLE
- // This ensures that the TX buffer is flushed after a frame is queued in serial.
- // serial_in_frame is used to ensure that the flush only happens at the end of the frame
- if (serial_in_frame && byte == FEND) { SerialBT.flushTXD(); serial_in_frame = false; }
- else if (!serial_in_frame && byte == FEND) { serial_in_frame = true; }
- #endif
- }
- #else
- Serial.write(byte);
- #endif
+ #if MCU_VARIANT == MCU_NRF52 && HAS_BLE
+ // This ensures that the TX buffer is flushed after a frame is queued in serial.
+ // serial_in_frame is used to ensure that the flush only happens at the end of the frame
+ if (serial_in_frame && byte == FEND) { SerialBT.flushTXD(); serial_in_frame = false; }
+ else if (!serial_in_frame && byte == FEND) { serial_in_frame = true; }
+ #endif
+ break;
+ #endif
+ #if HAS_WIFI == true
+ case CHANNEL_WIFI:
+ wifi_remote_write(byte);
+ break;
+ #endif
+ case CHANNEL_USB:
+ default:
+ Serial.write(byte);
+ break;
+ }
}
void escaped_serial_write(uint8_t byte) {
@@ -883,6 +904,65 @@ void kiss_indicate_stat_snr() {
serial_write(FEND);
}
+#if HAS_GPS == true
+void kiss_indicate_stat_gps() {
+ // Report GPS data as a KISS frame:
+ // [fix(1)] [sats(1)] [lat(4)] [lon(4)] [alt(4)] [speed(4)] [hdop(4)]
+ // All floats are IEEE 754 single-precision, big-endian
+ serial_write(FEND);
+ serial_write(CMD_STAT_GPS);
+ escaped_serial_write(gps_has_fix ? 0x01 : 0x00);
+ escaped_serial_write(gps_sats);
+
+ union { float f; uint8_t b[4]; } u;
+
+ u.f = (float)gps_lat;
+ for (int i = 3; i >= 0; i--) escaped_serial_write(u.b[i]);
+
+ u.f = (float)gps_lon;
+ for (int i = 3; i >= 0; i--) escaped_serial_write(u.b[i]);
+
+ u.f = (float)gps_alt;
+ for (int i = 3; i >= 0; i--) escaped_serial_write(u.b[i]);
+
+ u.f = (float)gps_speed;
+ for (int i = 3; i >= 0; i--) escaped_serial_write(u.b[i]);
+
+ u.f = (float)gps_hdop;
+ for (int i = 3; i >= 0; i--) escaped_serial_write(u.b[i]);
+
+ // Diagnostic counters from TinyGPS++ (uint32 big-endian each)
+ uint32_t val;
+
+ val = gps_parser.charsProcessed();
+ escaped_serial_write((val >> 24) & 0xFF);
+ escaped_serial_write((val >> 16) & 0xFF);
+ escaped_serial_write((val >> 8) & 0xFF);
+ escaped_serial_write(val & 0xFF);
+
+ val = gps_parser.passedChecksum();
+ escaped_serial_write((val >> 24) & 0xFF);
+ escaped_serial_write((val >> 16) & 0xFF);
+ escaped_serial_write((val >> 8) & 0xFF);
+ escaped_serial_write(val & 0xFF);
+
+ val = gps_parser.failedChecksum();
+ escaped_serial_write((val >> 24) & 0xFF);
+ escaped_serial_write((val >> 16) & 0xFF);
+ escaped_serial_write((val >> 8) & 0xFF);
+ escaped_serial_write(val & 0xFF);
+
+ val = gps_parser.sentencesWithFix();
+ escaped_serial_write((val >> 24) & 0xFF);
+ escaped_serial_write((val >> 16) & 0xFF);
+ escaped_serial_write((val >> 8) & 0xFF);
+ escaped_serial_write(val & 0xFF);
+
+ serial_write(FEND);
+}
+
+#endif
+
void kiss_indicate_radio_lock() {
serial_write(FEND);
serial_write(CMD_RADIO_LOCK);
@@ -1129,7 +1209,7 @@ void kiss_indicate_fbstate() {
void kiss_indicate_fb() {
serial_write(FEND);
serial_write(CMD_FB_READ);
- #if HAS_DISPLAY
+ #if HAS_DISPLAY && BOARD_MODEL != BOARD_TWATCH_ULT
for (int i = 0; i < 512; i++) {
uint8_t byte = fb[i];
escaped_serial_write(byte);
@@ -1143,7 +1223,7 @@ void kiss_indicate_fb() {
void kiss_indicate_disp() {
serial_write(FEND);
serial_write(CMD_DISP_READ);
- #if HAS_DISPLAY
+ #if HAS_DISPLAY && BOARD_MODEL != BOARD_TWATCH_ULT
uint8_t *da = disp_area.getBuffer();
uint8_t *sa = stat_area.getBuffer();
for (int i = 0; i < 512; i++) { escaped_serial_write(da[i]); }
@@ -1263,12 +1343,20 @@ void updateBitrate() {
}
void setSpreadingFactor() {
- if (radio_online) LoRa->setSpreadingFactor(lora_sf);
+ if (radio_online) {
+ LoRa->standby();
+ LoRa->setSpreadingFactor(lora_sf);
+ lora_receive();
+ }
updateBitrate();
}
void setCodingRate() {
- if (radio_online) LoRa->setCodingRate4(lora_cr);
+ if (radio_online) {
+ LoRa->standby();
+ LoRa->setCodingRate4(lora_cr);
+ lora_receive();
+ }
updateBitrate();
}
@@ -1396,8 +1484,10 @@ void getBandwidth() {
void setBandwidth() {
if (radio_online) {
+ LoRa->standby();
LoRa->setSignalBandwidth(lora_bw);
getBandwidth();
+ lora_receive();
}
}
@@ -1409,8 +1499,10 @@ void getFrequency() {
void setFrequency() {
if (radio_online) {
+ LoRa->standby();
LoRa->setFrequency(lora_freq);
getFrequency();
+ lora_receive();
}
}
@@ -1595,7 +1687,7 @@ bool eeprom_product_valid() {
#if PLATFORM == PLATFORM_AVR
if (rval == PRODUCT_RNODE || rval == PRODUCT_HMBRW) {
#elif PLATFORM == PLATFORM_ESP32
- if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_H32_V4 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_XIAO_S3) {
+ if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_H32_V4 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_XIAO_S3 || rval == PRODUCT_TWATCH_ULT) {
#elif PLATFORM == PLATFORM_NRF52
if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_TECHO || rval == PRODUCT_HMBRW) {
#else
@@ -1633,6 +1725,8 @@ bool eeprom_model_valid() {
if (model == MODEL_DB || model == MODEL_DC) {
#elif BOARD_MODEL == BOARD_XIAO_S3
if (model == MODEL_DD || model == MODEL_DE) {
+ #elif BOARD_MODEL == BOARD_TWATCH_ULT
+ if (model == MODEL_D5 || model == MODEL_DA || model == MODEL_FF || model == MODEL_FE) {
#elif BOARD_MODEL == BOARD_LORA32_V1_0
if (model == MODEL_BA || model == MODEL_BB) {
#elif BOARD_MODEL == BOARD_LORA32_V2_0
@@ -2019,4 +2113,7 @@ void host_disconnected() {
last_rssi = -292;
last_rssi_raw = 0x00;
last_snr_raw = 0x80;
+ #if HAS_WIFI == true
+ if (data_channel == CHANNEL_WIFI) data_channel = CHANNEL_USB;
+ #endif
}
\ No newline at end of file
diff --git a/VISION.md b/VISION.md
new file mode 100644
index 00000000..a1b337cf
--- /dev/null
+++ b/VISION.md
@@ -0,0 +1,232 @@
+# R-Watch: Off-Grid Smart Watch
+
+## Vision
+
+The T-Watch Ultimate port turns the RNode firmware inside out. Where the T-Beam Supreme is a radio modem that gained GPS tracking, the R-Watch is a smart watch that gained off-grid communications. The primary interface is a watch face on your wrist. The LoRa radio, GPS, and Reticulum stack run underneath — always available, never in the way.
+
+The target user puts on a watch in the morning and gets the time, the date, their step count. When they walk beyond cellular range, the same watch becomes a LoRa mesh node: relaying packets for Sideband on their phone, beaconing their location, and buzzing when an LXMF message arrives over the air.
+
+This aligns with the RNode project's core values — sovereignty, self-replication, open hardware — while making Reticulum accessible to people who don't configure radio modems.
+
+## Hardware Platform
+
+**LilyGo T-Watch Ultra**
+
+| Component | Chip | RNode Support |
+|-----------|------|---------------|
+| MCU | ESP32-S3 (dual-core, 16MB flash, 8MB PSRAM) | Existing |
+| LoRa radio | SX1262 (sub-GHz) | Existing (`sx126x.cpp/h`) |
+| GPS | u-blox MIA-M10Q (GPS/GLONASS/BeiDou/Galileo) | Adapt (`GPS.h`, 38400 baud) |
+| PMU | AXP2101 | Existing (`Power.h`) |
+| BLE | 5.0 (ESP32-S3 native) | Existing (`BLESerial.h`) |
+| WiFi | 802.11 b/g/n | Existing |
+| Display | 2.06" AMOLED, 410x502, CO5300 QSPI | **New driver needed** |
+| Touch | CST9217 capacitive (I2C 0x1A) | **New driver needed** |
+| IMU | BHI260AP (I2C 0x28) | **New driver needed** |
+| RTC | PCF85063A (I2C 0x51) | Adapt (`RTC.h`) |
+| NFC | ST25R3916 (SPI) | **New driver needed** |
+| Audio | MAX98357A amp + SPM1423 PDM mic | **New driver needed** |
+| Haptics | DRV2605 (I2C 0x5A) | **New driver needed** |
+| GPIO expander | XL9555 (I2C 0x20) | **New driver needed** |
+| SD card | MicroSD via shared SPI | Existing pattern |
+
+## Use Cases
+
+In priority order:
+
+### 1. Watch Face and Timekeeping
+
+The default screen. Time, date, battery level. The RTC syncs from GPS satellites — no phone or internet needed for accurate time. This is what users see 95% of the time.
+
+### 2. Phone-Connected BLE RNode
+
+A phone running Sideband or NomadNet connects to the watch over BLE. The watch becomes a transparent LoRa modem — identical to plugging a T-Beam into USB, but wireless and on your wrist. The phone handles Reticulum routing; the watch handles the radio. The existing KISS-over-BLE protocol works without modification.
+
+### 3. RNode Status Dashboard
+
+While the radio is active, the watch shows what's happening: link quality, channel utilization, current frequency and spreading factor, BLE connection state. Glanceable, not diagnostic. A signal-strength arc on the watch face periphery, not a terminal dump.
+
+### 4. Standalone GPS Tracker
+
+When no phone has connected for 15 seconds, the watch switches to autonomous mode: GPS beacon transmission and encrypted LXMF telemetry, identical to the T-Beam Supreme's standalone operation. The watch face shows GPS fix status, satellite count, and time since last beacon. Location and battery telemetry reach the Reticulum network without any other device.
+
+### 5. On-Watch Message Notifications
+
+When LXMF messages arrive — either direct over LoRa or relayed from a connected phone — the watch displays them and vibrates. Read them on your wrist. This is new functionality: no existing RNode shows message content.
+
+### 6. Activity Tracking
+
+Step counting from the accelerometer. Basic daily activity. Table-stakes for a device on your wrist, and the hardware supports it.
+
+## UI/UX Philosophy
+
+### Watch Face Is Home
+
+The always-visible screen is a clock. Not a radio diagnostic panel. The 410x502 AMOLED is tall enough to show time prominently with status indicators below: a thin bar for battery, a small icon for BLE connection, a dot for GPS fix, a signal indicator for LoRa activity. Information at a glance without cluttering the time.
+
+### Tall Display Layout
+
+The 410x502 display is a tall rounded rectangle — more vertical space than a typical square watch. Time occupies the upper portion. Status indicators, complications, and secondary information use the lower area. The extra vertical space is an asset for message display and scrollable lists.
+
+### Gesture Navigation
+
+Swipe to navigate between screens. Tap to select. Long-press for context. No reliance on physical buttons for primary navigation. The screen hierarchy:
+
+```
+ [Radio Status]
+ |
+[GPS/Location] -- [Watch Face] -- [Messages]
+ |
+ [Settings]
+```
+
+Swipe left/right/up/down from the watch face to reach each screen. Each screen is designed for glanceable information — two seconds of attention, not ten.
+
+### Dark by Default
+
+AMOLED means black pixels are free. Dark themes with minimal lit pixels extend battery life. Bright elements are reserved for alerts and active indicators.
+
+### Wrist-Raise Wake
+
+The accelerometer detects wrist raise and wakes the display. No button press to check the time. When the wrist drops, the display sleeps. Simple, expected watch behaviour.
+
+## RNode Integration Model
+
+### Dual-Core Architecture
+
+The ESP32-S3 has two cores. Use both:
+
+- **Core 0 — Radio modem**: SX1262 driver, KISS framing, CSMA, packet queues, BLE serial, GPS parsing, beacon logic. This is the existing RNode firmware loop, running as a FreeRTOS task. It must never be starved by display rendering.
+
+- **Core 1 — Watch UI**: Display rendering, touch input, gesture recognition, animations, screen transitions. This is new code, running independently. It reads shared state (radio status, GPS coordinates, battery level, message buffers) from Core 0 via thread-safe queues.
+
+This separation means radio performance is identical to a headless RNode regardless of what the display is doing.
+
+### Operating Modes
+
+| Feature | Phone Connected (BLE) | Standalone |
+|---------|----------------------|------------|
+| Watch face / timekeeping | Yes | Yes |
+| LoRa radio | Host-controlled via KISS | Autonomous beacon |
+| GPS tracking | Reported to phone | Local beacon + LXMF |
+| Message display | Relayed from Sideband | Direct LoRa receive |
+| Message send | Via phone | Future |
+| Activity tracking | Yes | Yes |
+| Provisioning | Via phone or USB | Pre-provisioned |
+
+Mode switching is automatic. When a BLE host connects, the watch becomes a transparent modem. When the host disconnects, the watch enters standalone mode after a 15-second timeout. The watch face reflects the current mode.
+
+## Feature Tiers
+
+### Tier 1 — First Flash
+
+Get the watch running as an RNode with a visible clock.
+
+- Board definition in `Boards.h` with T-Watch Ultimate pin mapping
+- AMOLED display driver (CO5300, 410x502 QSPI)
+- Touch input driver (basic tap and swipe)
+- Watch face: time, date, battery percentage
+- GPS-synced RTC timekeeping
+- BLE RNode modem mode (existing KISS/BLE stack)
+- AXP2101 power management
+- Radio status indicator on watch face (connected / idle / TX / RX)
+- BLE connection indicator
+- Provisioning via `rnodeconf`
+- JTAG flash target in Makefile (OpenOCD, no serial bootloader)
+
+### Tier 2 — Core Watch
+
+Feature parity with T-Beam Supreme, plus watch-native status display.
+
+- Standalone GPS beacon + LXMF encrypted telemetry
+- Radio status screen (frequency, SF, BW, channel utilization, RSSI)
+- GPS status screen (coordinates, satellites, HDOP, fix age, minimap)
+- Wrist-raise display wake via accelerometer
+- Haptic feedback on radio events (message received, beacon sent)
+- Display sleep/wake power management
+- Battery usage optimisation (AMOLED sleep, peripheral power gating)
+
+### Tier 3 — Smart Watch
+
+Features that make it a daily-wear device.
+
+- On-watch LXMF message display with haptic notification
+- Step counter and daily activity tracking
+- Multiple watch face designs
+- Alarm and timer functions
+- Settings screen (LoRa parameters, BLE pairing, display brightness)
+- Audio alerts via speaker for critical events (emergency beacon ACK, low battery)
+
+### Tier 4 — Aspirational
+
+Longer-term possibilities.
+
+- On-watch LXMF message composition (touch keyboard or canned responses)
+- Mesh network visualisation (nearby nodes, link quality graph)
+- Peer discovery and contact list
+- Over-the-air firmware updates via BLE or WiFi
+- Watch face customisation
+
+## Graphics and Artwork
+
+### Framework: LVGL
+
+The UI is built on [LVGL](https://lvgl.io/) (Light and Versatile Graphics Library), which is MIT-licensed and fully compatible with RNode's GPL-3.0. LVGL provides gesture-driven input, smooth animations, and efficient PSRAM-backed rendering on ESP32-S3. It is the standard choice for embedded touchscreen devices.
+
+### Original Artwork
+
+All watch face designs, icons, and graphical assets are original work created for the R-Watch project. LilyGo's factory firmware artwork is not reused — their repositories do not clearly license graphical assets separately from code, making reuse in a GPL project legally ambiguous.
+
+Design inspiration may be drawn from the factory UI's layout and interaction patterns (ideas are not copyrightable), but no bitmap assets, watch face graphics, or icon sets are copied.
+
+### Visual Identity
+
+The R-Watch visual language should:
+
+- Feel like a watch, not an electronics project. Clean typography, considered spacing, purposeful use of colour.
+- Use the AMOLED's strengths: true blacks, vibrant accent colours against dark backgrounds, thin luminous arcs and indicators.
+- Incorporate Reticulum/RNode identity subtly — the mesh network aesthetic should inform the design without dominating it. A watch face, not a dashboard.
+- Prioritise readability at arm's length. Large time digits, high-contrast status indicators, no fine text that requires squinting.
+
+### Asset Licensing
+
+Code is GPL-3.0, consistent with the RNode project. Original graphical assets (watch faces, icons, UI elements) are licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA-4.0), allowing community contribution and remixing while preserving share-alike terms.
+
+## Technical Approach
+
+### What We Reuse
+
+The following existing RNode firmware modules compile for the T-Watch with a new board definition and pin mapping — no architectural changes:
+
+- `sx126x.cpp/h` — SX1262 LoRa driver
+- `GPS.h` — GPS with TinyGPSPlus parser (adapt baud rate for MIA-M10Q: 38400 vs L76K's 9600)
+- `Beacon.h` — GPS beacon transmission
+- `LxmfBeacon.h` — LXMF telemetry messaging
+- `BeaconCrypto.h` — ECDH + AES-256-CBC encryption
+- `BLESerial.h` — BLE GATT serial interface
+- `Bluetooth.h` — BLE pairing and state management
+- `Power.h` — AXP2101 PMU driver (same chip as T-Beam Supreme)
+- `RTC.h` — RTC driver (adapt register map for PCF85063A vs T-Beam's PCF8563)
+- `Config.h` — Configuration state and KISS protocol
+- `Modem.h` — Radio abstraction layer
+- `Framing.h` — KISS framing
+
+### What We Build
+
+- **AMOLED display driver** — CO5300 controller over QSPI (4 data lines). The 410x502 display at 16-bit colour requires DMA-driven QSPI and a PSRAM-backed framebuffer. The UI layer is built on LVGL (MIT-licensed), which handles gesture input and efficient rendering on ESP32-S3 with PSRAM.
+
+- **Touch input system** — Replaces the GPIO button debounce in `Input.h`. Capacitive touch over I2C, with gesture recognition (tap, swipe direction, long-press).
+
+- **Watch UI layer** — The watch face, status screens, message display, and settings. This is the largest piece of new code. It runs on Core 1 and reads shared state from the modem task on Core 0.
+
+- **Board definition** — `BOARD_TWATCH_ULT` in `Boards.h` with pin mapping, feature flags, and display parameters.
+
+- **Makefile target** — `firmware-twatch_ultimate` using `esp32:esp32:esp32s3:CDCOnBoot=cdc`, flashed via OpenOCD JTAG (the T-Watch's native USB doesn't support esptool auto-reset).
+
+## What This Is Not
+
+- **Not Android or WearOS.** This is bare-metal firmware on an ESP32-S3. No app store, no Play Services, no Wear compatibility. The trade-off is sovereignty and radio capability.
+
+- **Not a phone replacement.** The phone running Sideband remains the primary Reticulum host. The watch is the radio and the display, not the router.
+
+- **Not a general-purpose LoRa development board.** It is a watch. The LoRa radio serves the watch's communication features, not the other way around.
diff --git a/XL9555.h b/XL9555.h
new file mode 100644
index 00000000..3c484766
--- /dev/null
+++ b/XL9555.h
@@ -0,0 +1,106 @@
+// XL9555 I2C GPIO Expander - Minimal driver for T-Watch Ultra
+// Two 8-bit ports (0-7 = port 0, 8-15 = port 1)
+// Registers: 0x00/0x01 input, 0x02/0x03 output, 0x06/0x07 direction (1=input, 0=output)
+
+#ifndef XL9555_H
+#define XL9555_H
+
+#include
+
+#define XL9555_ADDR 0x20
+#define XL9555_REG_IN0 0x00
+#define XL9555_REG_IN1 0x01
+#define XL9555_REG_OUT0 0x02
+#define XL9555_REG_OUT1 0x03
+#define XL9555_REG_DIR0 0x06
+#define XL9555_REG_DIR1 0x07
+
+// T-Watch Ultra expander pin assignments (matching LilyGoLib numbering)
+#define EXPANDS_DRV_EN 6 // Port 0, bit 6 — haptic driver enable
+#define EXPANDS_DISP_EN 14 // Port 1, bit 6 — display power gate
+#define EXPANDS_DISP_RST 15 // Port 1, bit 7 — display reset
+#define EXPANDS_TOUCH_RST 16 // Extended pin — touch panel reset
+#define EXPANDS_SD_DET 12 // Port 1, bit 4 — SD card detect (input)
+#define EXPANDS_LORA_RF_SW 11 // Port 1, bit 3 — LoRa RF switch
+
+static bool xl9555_ready = false;
+static uint8_t xl9555_out[2] = {0xFF, 0xFF}; // output register cache
+
+static uint8_t xl9555_read_reg(uint8_t reg) {
+ Wire.beginTransmission(XL9555_ADDR);
+ Wire.write(reg);
+ Wire.endTransmission(false);
+ Wire.requestFrom((uint8_t)XL9555_ADDR, (uint8_t)1);
+ return Wire.available() ? Wire.read() : 0xFF;
+}
+
+static void xl9555_write_reg(uint8_t reg, uint8_t val) {
+ Wire.beginTransmission(XL9555_ADDR);
+ Wire.write(reg);
+ Wire.write(val);
+ Wire.endTransmission();
+}
+
+bool xl9555_init() {
+ Wire.beginTransmission(XL9555_ADDR);
+ if (Wire.endTransmission() != 0) return false;
+
+ // Set output pin directions (0 = output, 1 = input)
+ // Port 0: pin 6 (DRV_EN) as output, rest input
+ uint8_t dir0 = 0xFF & ~(1 << 6);
+ // Port 1: pins 3 (LORA_RF_SW), 6 (DISP_EN), 7 (DISP_RST) as output
+ uint8_t dir1 = 0xFF & ~((1 << 3) | (1 << 6) | (1 << 7));
+
+ xl9555_write_reg(XL9555_REG_DIR0, dir0);
+ xl9555_write_reg(XL9555_REG_DIR1, dir1);
+
+ // Read current output state
+ xl9555_out[0] = xl9555_read_reg(XL9555_REG_OUT0);
+ xl9555_out[1] = xl9555_read_reg(XL9555_REG_OUT1);
+
+ xl9555_ready = true;
+ return true;
+}
+
+void xl9555_set(uint8_t pin, bool value) {
+ if (!xl9555_ready) return;
+ uint8_t port = (pin >= 8) ? 1 : 0;
+ uint8_t bit = pin % 8;
+
+ if (value) {
+ xl9555_out[port] |= (1 << bit);
+ } else {
+ xl9555_out[port] &= ~(1 << bit);
+ }
+ xl9555_write_reg(port == 0 ? XL9555_REG_OUT0 : XL9555_REG_OUT1, xl9555_out[port]);
+}
+
+bool xl9555_get(uint8_t pin) {
+ if (!xl9555_ready) return false;
+ uint8_t port = (pin >= 8) ? 1 : 0;
+ uint8_t bit = pin % 8;
+ uint8_t val = xl9555_read_reg(port == 0 ? XL9555_REG_IN0 : XL9555_REG_IN1);
+ return (val >> bit) & 1;
+}
+
+// Convenience functions for sleep entry
+void xl9555_sleep_prepare() {
+ if (!xl9555_ready) return;
+ xl9555_set(EXPANDS_DISP_RST, false); // hold display in reset
+ xl9555_set(EXPANDS_DRV_EN, false); // disable haptic
+ xl9555_set(EXPANDS_DISP_EN, false); // gate display power
+}
+
+void xl9555_wake_display() {
+ if (!xl9555_ready) return;
+ xl9555_set(EXPANDS_DISP_EN, true); // enable display power
+ xl9555_set(EXPANDS_DISP_RST, false); // reset pulse
+ delay(50);
+ xl9555_set(EXPANDS_DISP_RST, true);
+}
+
+void xl9555_enable_lora_antenna() {
+ xl9555_set(EXPANDS_LORA_RF_SW, true); // select built-in antenna
+}
+
+#endif
diff --git a/arduino-cli.yaml b/arduino-cli.yaml
index 6dd5f8d0..bf0a0d16 100644
--- a/arduino-cli.yaml
+++ b/arduino-cli.yaml
@@ -4,4 +4,4 @@ board_manager:
- https://raw.githubusercontent.com/RAKwireless/RAKwireless-Arduino-BSP-Index/main/package_rakwireless_index.json
- https://github.com/HelTecAutomation/Heltec_nRF52/releases/download/1.7.0/package_heltec_nrf_index.json
- https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
- - http://unsigned.io/arduino/package_unsignedio_UnsignedBoards_index.json
+ # - http://unsigned.io/arduino/package_unsignedio_UnsignedBoards_index.json # 404
diff --git a/flash_parts.sh b/flash_parts.sh
new file mode 100755
index 00000000..587c35e8
--- /dev/null
+++ b/flash_parts.sh
@@ -0,0 +1,144 @@
+#!/bin/bash
+#
+# Workaround for ESP32-S3 rev v0.2 USB-Serial/JTAG controller bug:
+# the USB controller drops after ~80KB of sustained compressed writes.
+#
+# This script splits the firmware into small page-aligned chunks and
+# flashes each with a full device reset between them, keeping each
+# transfer well under the ~80KB compressed limit.
+# Only pauses on failure — successful writes proceed immediately.
+#
+# Usage:
+# ./flash_parts.sh [port] [firmware_bin]
+#
+# Defaults:
+# port = /dev/ttyACM4
+# firmware_bin = build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin
+
+set -uo pipefail
+
+PORT="${1:-/dev/ttyACM4}"
+FIRMWARE="${2:-build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin}"
+BOOTLOADER="build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin"
+PARTITIONS="build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin"
+BOOT_APP0="${HOME}/.arduino15/packages/esp32/hardware/esp32/2.0.17/tools/partitions/boot_app0.bin"
+
+BAUD=460800
+DELAY=1 # seconds between successful writes (USB recovery)
+RETRY_DELAY=8 # seconds to wait before retry on failure
+N_PARTS=16
+FW_BASE=0x10000 # firmware flash offset
+TMPDIR=$(mktemp -d)
+trap 'rm -rf "$TMPDIR"' EXIT
+
+# --- Validate inputs ---
+for f in "$FIRMWARE" "$BOOTLOADER" "$PARTITIONS" "$BOOT_APP0"; do
+ if [ ! -f "$f" ]; then
+ echo "ERROR: Missing file: $f"
+ echo "Run 'make firmware-tbeam_supreme' first."
+ exit 1
+ fi
+done
+
+if [ ! -e "$PORT" ]; then
+ echo "ERROR: Serial port $PORT not found"
+ exit 1
+fi
+
+FW_SIZE=$(stat -c%s "$FIRMWARE")
+echo "Firmware: $FIRMWARE ($FW_SIZE bytes)"
+echo "Port: $PORT"
+echo ""
+
+# --- Split firmware into page-aligned parts ---
+CHUNK_RAW=$(( (FW_SIZE / N_PARTS / 4096) * 4096 ))
+
+python3 -c "
+import sys
+with open('$FIRMWARE', 'rb') as f:
+ data = f.read()
+size = len(data)
+chunk = $CHUNK_RAW
+n = $N_PARTS
+for i in range(n):
+ start = i * chunk
+ end = size if i == n - 1 else start + chunk
+ part = data[start:end]
+ pad = ((len(part) + 4095) // 4096) * 4096
+ part = part.ljust(pad, b'\xff')
+ path = '$TMPDIR/part_{}.bin'.format(i)
+ with open(path, 'wb') as pf:
+ pf.write(part)
+ addr = $FW_BASE + start
+ print('Part {:2d}: 0x{:06x} {:6d} bytes (padded to {})'.format(i, addr, end - start, pad))
+"
+
+echo ""
+
+# --- Flash bootloader + partition table + boot_app0 ---
+echo "=== Flashing bootloader, partitions, boot_app0 ==="
+OUTPUT=$(esptool --chip esp32s3 --port "$PORT" --baud "$BAUD" \
+ --before default-reset --after hard-reset \
+ write-flash -z --flash-mode dio --flash-freq 80m --flash-size 8MB \
+ 0x0 "$BOOTLOADER" \
+ 0x8000 "$PARTITIONS" \
+ 0xe000 "$BOOT_APP0" 2>&1)
+
+if ! echo "$OUTPUT" | grep -q "Hash of data verified"; then
+ echo "Bootloader: FAILED — retrying after ${RETRY_DELAY}s..."
+ sleep "$RETRY_DELAY"
+ OUTPUT=$(esptool --chip esp32s3 --port "$PORT" --baud "$BAUD" \
+ --before default-reset --after hard-reset \
+ write-flash -z --flash-mode dio --flash-freq 80m --flash-size 8MB \
+ 0x0 "$BOOTLOADER" \
+ 0x8000 "$PARTITIONS" \
+ 0xe000 "$BOOT_APP0" 2>&1)
+ if ! echo "$OUTPUT" | grep -q "Hash of data verified"; then
+ echo "Bootloader: FAILED after retry"
+ echo "$OUTPUT" | tail -n 5
+ exit 1
+ fi
+fi
+echo "Bootloader: OK"
+sleep "$DELAY"
+
+# --- Flash each firmware part ---
+FAILED=0
+for i in $(seq 0 $((N_PARTS - 1))); do
+ ADDR=$(printf "0x%06x" $((FW_BASE + i * CHUNK_RAW)))
+
+ echo "=== Part $i/$((N_PARTS - 1)): $ADDR ==="
+ OUTPUT=$(esptool --chip esp32s3 --port "$PORT" --baud "$BAUD" \
+ --before default-reset --after hard-reset \
+ write-flash -z --flash-mode dio --flash-freq 80m --flash-size 8MB \
+ "$ADDR" "$TMPDIR/part_${i}.bin" 2>&1)
+
+ if echo "$OUTPUT" | grep -q "Hash of data verified"; then
+ echo "Part $i: VERIFIED"
+ sleep "$DELAY"
+ else
+ echo "Part $i: FAILED — retrying after ${RETRY_DELAY}s..."
+ sleep "$RETRY_DELAY"
+ OUTPUT=$(esptool --chip esp32s3 --port "$PORT" --baud "$BAUD" \
+ --before default-reset --after hard-reset \
+ write-flash -z --flash-mode dio --flash-freq 80m --flash-size 8MB \
+ "$ADDR" "$TMPDIR/part_${i}.bin" 2>&1)
+
+ if echo "$OUTPUT" | grep -q "Hash of data verified"; then
+ echo "Part $i: VERIFIED (retry)"
+ else
+ echo "Part $i: FAILED after retry"
+ echo "$OUTPUT" | tail -n 5
+ FAILED=1
+ break
+ fi
+ fi
+done
+
+echo ""
+if [ "$FAILED" -eq 0 ]; then
+ echo "=== ALL $N_PARTS PARTS FLASHED AND VERIFIED ==="
+else
+ echo "=== FLASH INCOMPLETE — see errors above ==="
+ exit 1
+fi
diff --git a/partition_hashes b/partition_hashes
index 9d8db4ba..0efb33bb 100755
--- a/partition_hashes
+++ b/partition_hashes
@@ -17,15 +17,9 @@
import os
import sys
-import RNS
-import json
import hashlib
import subprocess
-major_version = None
-minor_version = None
-target_version = None
-
target_file = os.path.join(sys.argv[1])
if sys.argv[1] == "from_device":
@@ -39,7 +33,9 @@ if not from_device:
part_hash = firmware_data[-32:]
if calc_hash == part_hash:
- print(RNS.hexrep(part_hash, delimit=False))
+ print(part_hash.hex())
+ else:
+ sys.exit("ERROR: Embedded hash does not match calculated hash")
else:
try:
diff --git a/partition_twatch.csv b/partition_twatch.csv
new file mode 100644
index 00000000..adc9342e
--- /dev/null
+++ b/partition_twatch.csv
@@ -0,0 +1,9 @@
+# T-Watch Ultimate partition table — 16MB flash, no OTA, large app
+# Requires FlashSize=16M in the Arduino FQBN or the bootloader can't map beyond 4MB.
+# Copy this file to ~/.arduino15/packages/esp32/hardware/esp32//tools/partitions/
+# Name, Type, SubType, Offset, Size, Flags
+nvs, data, nvs, 0x9000, 0x5000,
+otadata, data, ota, 0xe000, 0x2000,
+app0, app, ota_0, 0x10000, 0x800000,
+spiffs, data, spiffs, 0x810000, 0x7E0000,
+coredump, data, coredump,0xFF0000, 0x10000,
diff --git a/scripts/get_partition_hash.py b/scripts/get_partition_hash.py
new file mode 100644
index 00000000..0d90e850
--- /dev/null
+++ b/scripts/get_partition_hash.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+"""Compute the ESP32 partition hash for an RNode firmware binary.
+
+The ESP32 build toolchain appends a SHA256 hash to firmware binaries.
+esp_partition_get_sha256() returns this embedded hash (the last 32 bytes).
+This is different from sha256sum of the entire file.
+
+Usage:
+ python3 get_partition_hash.py
+
+ # Flash and set hash:
+ python3 get_partition_hash.py build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin
+ rnodeconf -H /dev/ttyACM0
+"""
+import hashlib
+import sys
+
+def get_partition_hash(firmware_path):
+ with open(firmware_path, "rb") as f:
+ fw = f.read()
+
+ embedded_hash = fw[-32:]
+ calc_hash = hashlib.sha256(fw[:-32]).digest()
+
+ if calc_hash != embedded_hash:
+ print(f"WARNING: Embedded hash doesn't match SHA256(fw[:-32])", file=sys.stderr)
+ print(f" Calculated: {calc_hash.hex()}", file=sys.stderr)
+ print(f" Embedded: {embedded_hash.hex()}", file=sys.stderr)
+ return None
+
+ return embedded_hash.hex()
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print(f"Usage: {sys.argv[0]} ", file=sys.stderr)
+ sys.exit(1)
+
+ result = get_partition_hash(sys.argv[1])
+ if result:
+ print(result)
+ else:
+ sys.exit(1)
diff --git a/scripts/profile_baseline.json b/scripts/profile_baseline.json
new file mode 100644
index 00000000..073cad50
--- /dev/null
+++ b/scripts/profile_baseline.json
@@ -0,0 +1,18 @@
+{
+ "test": "profile",
+ "build": "Mar 29 2026 10:57:00",
+ "date": "2026-03-29",
+ "device": "T-Watch Ultimate (unprovisioned, hwcdc)",
+ "notes": "Baseline after beacon/stopRadio loop fix. GPS active with fix, radio not provisioned.",
+ "idle_us": 914,
+ "idle_flush_us": 0,
+ "full_us": 79592,
+ "full_flush_us": 37176,
+ "data_update_us": 1939,
+ "nav_5tile_us": 374708,
+ "burst_10frame_us": 801653,
+ "avg_frame_us": 80165,
+ "loop_us": 331,
+ "heap": 7298759,
+ "psram": 7143315
+}
diff --git a/scripts/profile_provisioned.json b/scripts/profile_provisioned.json
new file mode 100644
index 00000000..6e4e3cee
--- /dev/null
+++ b/scripts/profile_provisioned.json
@@ -0,0 +1,27 @@
+{
+ "test": "profile",
+ "build": "Mar 29 2026 11:10:40",
+ "date": "2026-03-29",
+ "device": "T-Watch Ultimate (provisioned 868MHz, hwcdc, radio online)",
+ "notes": "Provisioned via rnodeconf with T-Watch product code. Radio online, GPS active.",
+ "idle_us": 909,
+ "idle_flush_us": 0,
+ "full_us": 82548,
+ "full_flush_us": 37294,
+ "data_update_us": 2030,
+ "nav_5tile_us": 465761,
+ "burst_10frame_us": 799982,
+ "avg_frame_us": 79998,
+ "loop_us": 289,
+ "loop_breakdown": {
+ "radio_us": 6,
+ "serial_us": 7,
+ "display_us": 1,
+ "pmu_us": 2,
+ "gps_us": 10,
+ "bt_us": 2,
+ "imu_us": 217
+ },
+ "heap": 7270887,
+ "psram": 7116347
+}
diff --git a/scripts/screenshot.py b/scripts/screenshot.py
new file mode 100755
index 00000000..d937f054
--- /dev/null
+++ b/scripts/screenshot.py
@@ -0,0 +1,401 @@
+#!/usr/bin/env python3
+"""
+R-Watch Remote Debug Tool
+
+Commands:
+ screenshot [-o file.png] Capture display screenshot
+ metrics Show frame timing and memory stats
+ touch [duration_ms] Inject touch at coordinates
+ swipe Swipe up/down/left/right
+ navigate Jump to: watch, radio, gps, messages, settings
+ invalidate Force full screen redraw
+
+Usage:
+ ./scripts/screenshot.py screenshot
+ ./scripts/screenshot.py metrics
+ ./scripts/screenshot.py touch 200 250
+ ./scripts/screenshot.py swipe down
+ ./scripts/screenshot.py navigate radio
+"""
+
+import argparse
+import struct
+import sys
+import time
+
+WIDTH = 410
+HEIGHT = 502
+PREFIX = b"RWS"
+DEFAULT_PORT = "/dev/ttyACM4"
+
+TILES = {
+ "watch": (1, 1),
+ "radio": (1, 0),
+ "gps": (0, 1),
+ "messages": (2, 1),
+ "settings": (1, 2),
+}
+
+SWIPES = {
+ "down": [(205, 400), (205, 100)], # swipe up on screen → show tile below
+ "up": [(205, 100), (205, 400)],
+ "left": [(350, 250), (60, 250)],
+ "right": [(60, 250), (350, 250)],
+}
+
+
+def get_serial(port):
+ try:
+ import serial
+ except ImportError:
+ sys.exit("pip install pyserial")
+ s = serial.Serial(port, 115200, timeout=2)
+ time.sleep(0.1)
+ s.reset_input_buffer()
+ return s
+
+
+def send_cmd(s, cmd_byte, payload=b""):
+ s.write(PREFIX + bytes([cmd_byte]) + payload)
+ s.flush()
+
+
+def cmd_screenshot(s, output):
+ try:
+ from PIL import Image
+ except ImportError:
+ sys.exit("pip install Pillow")
+
+ send_cmd(s, ord('S'))
+
+ # Scan for response header
+ buf = b""
+ deadline = time.time() + 10
+ while time.time() < deadline:
+ chunk = s.read(max(1, s.in_waiting or 1))
+ if chunk:
+ buf += chunk
+ magic = PREFIX + b"S"
+ idx = buf.find(magic)
+ if idx >= 0 and len(buf) >= idx + 8:
+ break
+ else:
+ sys.exit(f"Timeout ({len(buf)} bytes, no header)")
+
+ hdr = buf[idx:idx + 8]
+ w, h = struct.unpack("H", data, i * 2)[0]
+ r = ((pixel >> 11) & 0x1F) * 255 // 31
+ g = ((pixel >> 5) & 0x3F) * 255 // 63
+ b = (pixel & 0x1F) * 255 // 31
+ pixels[i % w, i // w] = (r, g, b)
+
+ img.save(output)
+ print(f"Saved: {output} ({w}x{h}, {len(data)} bytes)")
+
+
+def cmd_metrics(s):
+ send_cmd(s, ord('M'))
+ buf = b""
+ deadline = time.time() + 3
+ while time.time() < deadline:
+ chunk = s.read(max(1, s.in_waiting or 1))
+ if chunk:
+ buf += chunk
+ magic = PREFIX + b"M"
+ idx = buf.find(magic)
+ if idx >= 0:
+ # Find the JSON after the header
+ json_start = idx + 4
+ nl = buf.find(b"\n", json_start)
+ if nl >= 0:
+ print(buf[json_start:nl].decode())
+ return
+ print(f"Timeout ({len(buf)} bytes)")
+
+
+def cmd_touch(s, x, y, duration_ms=200):
+ dur = max(1, duration_ms // 100)
+ payload = struct.pack("= 0:
+ nl = buf.find(b"\n", idx + 4)
+ if nl >= 0:
+ import json
+ data = json.loads(buf[idx + 4:nl])
+ print(f"Build: {data.get('build', '?')}")
+ print(f"Idle frame: {data['idle_us']:>8} µs (flush: {data['idle_flush_us']} µs)")
+ print(f"Full frame: {data['full_us']:>8} µs (flush: {data['full_flush_us']} µs)")
+ print(f"Data update: {data['data_update_us']:>8} µs")
+ print(f"Nav 5 tiles: {data['nav_5tile_us']:>8} µs ({data['nav_5tile_us']//5} µs/tile)")
+ print(f"Burst 10 frame: {data['burst_10frame_us']:>8} µs ({data['avg_frame_us']} µs/frame)")
+ print(f"Main loop: {data['loop_us']:>8} µs")
+ print(f"Heap free: {data['heap']:>8} bytes")
+ print(f"PSRAM free: {data['psram']:>8} bytes")
+ if save_path:
+ with open(save_path, 'w') as f:
+ json.dump(data, f, indent=2)
+ print(f"Saved: {save_path}")
+ return
+ print(f"Timeout ({len(buf)} bytes)")
+
+
+def cmd_crypto(s):
+ """Run IFAC crypto test vectors on firmware"""
+ send_cmd(s, ord('C'))
+ buf = b""
+ deadline = time.time() + 10
+ while time.time() < deadline:
+ chunk = s.read(max(1, s.in_waiting or 1))
+ if chunk:
+ buf += chunk
+ magic = PREFIX + b"C"
+ idx = buf.find(magic)
+ if idx >= 0:
+ nl = buf.find(b"\n", idx + 4)
+ if nl >= 0:
+ import json
+ data = json.loads(buf[idx + 4:nl])
+ all_pass = True
+ for test, result in data.items():
+ if test in ("pk", "sig", "hkdf", "ifac"):
+ status = "PASS" if result else "FAIL"
+ if not result:
+ all_pass = False
+ print(f" {test:>6}: {status}")
+ if not all_pass:
+ for k, v in data.items():
+ if k.startswith("actual_"):
+ print(f" {k}: {v}")
+ print(f"\n {'ALL PASS' if all_pass else 'FAILED'}")
+ return
+ print(f"Timeout ({len(buf)} bytes)")
+
+
+def cmd_invalidate(s):
+ send_cmd(s, ord('I'))
+ print("Invalidated — full redraw requested")
+
+
+def cmd_files(s):
+ """List files on SD card"""
+ send_cmd(s, ord('F'))
+ buf = b""
+ deadline = time.time() + 10
+ while time.time() < deadline:
+ chunk = s.read(max(1, s.in_waiting or 1))
+ if chunk:
+ buf += chunk
+ magic = PREFIX + b"F"
+ idx = buf.find(magic)
+ if idx >= 0 and b"]}" in buf[idx:]:
+ end = buf.find(b"]}", idx) + 2
+ print(buf[idx + 4:end].decode())
+ return
+ print(f"Timeout ({len(buf)} bytes)")
+
+
+def cmd_download(s, index, output):
+ """Download file from SD card by index"""
+ s.write(PREFIX + b'D' + bytes([index]))
+ s.flush()
+ buf = b""
+ # Wait for header line with filename and size
+ deadline = time.time() + 10
+ while time.time() < deadline:
+ chunk = s.read(max(1, s.in_waiting or 1))
+ if chunk:
+ buf += chunk
+ magic = PREFIX + b"D"
+ idx = buf.find(magic)
+ if idx >= 0:
+ # Find the JSON header line
+ hdr_start = idx + 4
+ nl = buf.find(b"\n", hdr_start)
+ if nl < 0:
+ continue
+ import json
+ info = json.loads(buf[hdr_start:nl])
+ if "error" in info:
+ print(f"Error: {info['error']}")
+ return
+ fname = info["name"]
+ fsize = info["size"]
+ # Read the file data
+ data = buf[nl + 1:]
+ while len(data) < fsize and time.time() < deadline + 30:
+ chunk = s.read(min(4096, fsize - len(data)))
+ if chunk:
+ data += chunk
+ outname = output or fname.lstrip("/")
+ with open(outname, "wb") as f:
+ f.write(data[:fsize])
+ print(f"Downloaded {fname} ({fsize} bytes) → {outname}")
+ return
+ print(f"Timeout ({len(buf)} bytes)")
+
+
+def cmd_simple(s, cmd_char, label):
+ """Send a command, print response, don't wait long"""
+ send_cmd(s, ord(cmd_char))
+ time.sleep(0.5)
+ data = s.read(s.in_waiting or 1)
+ if data:
+ magic = PREFIX + cmd_char.encode()
+ idx = data.find(magic)
+ if idx >= 0:
+ print(data[idx + 4:].decode('ascii', errors='replace').strip())
+ return
+ print(label)
+
+
+def cmd_log(s):
+ send_cmd(s, ord('L'))
+ buf = b""
+ deadline = time.time() + 5
+ while time.time() < deadline:
+ chunk = s.read(max(1, s.in_waiting or 1))
+ if chunk:
+ buf += chunk
+ magic = PREFIX + b"L"
+ idx = buf.find(magic)
+ if idx >= 0:
+ nl = buf.find(b"\n", idx + 4)
+ if nl >= 0:
+ print(buf[idx + 4:nl].decode())
+ return
+ print(f"Timeout ({len(buf)} bytes)")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="R-Watch remote debug")
+ parser.add_argument("-p", "--port", default=DEFAULT_PORT)
+ sub = parser.add_subparsers(dest="command")
+
+ ss = sub.add_parser("screenshot", aliases=["ss"])
+ ss.add_argument("-o", "--output", default="/tmp/watch_screenshot.png")
+
+ sub.add_parser("metrics", aliases=["m"])
+
+ t = sub.add_parser("touch", aliases=["t"])
+ t.add_argument("x", type=int)
+ t.add_argument("y", type=int)
+ t.add_argument("duration", type=int, nargs="?", default=200)
+
+ sw = sub.add_parser("swipe", aliases=["sw"])
+ sw.add_argument("direction", choices=["up", "down", "left", "right"])
+
+ n = sub.add_parser("navigate", aliases=["nav", "n"])
+ n.add_argument("screen", choices=list(TILES.keys()))
+
+ sub.add_parser("invalidate", aliases=["inv"])
+
+ sub.add_parser("crypto", aliases=["c"],
+ help="Run IFAC crypto test vectors on firmware")
+
+ p = sub.add_parser("profile", aliases=["p"],
+ help="Run standardized performance test")
+ p.add_argument("--save", metavar="FILE", help="Save raw JSON to file")
+
+ sub.add_parser("log", aliases=["l"],
+ help="Toggle IMU logging to SD card")
+
+ sub.add_parser("files", aliases=["f"],
+ help="List files on SD card")
+
+ dl = sub.add_parser("download", aliases=["dl"],
+ help="Download file from SD card by index")
+ dl.add_argument("index", type=int, help="File index from 'files' listing")
+ dl.add_argument("-o", "--output", help="Output filename (default: use SD name)")
+
+ sub.add_parser("reset", aliases=["x"],
+ help="Hard reset the device")
+
+ sub.add_parser("bootloader", aliases=["z"],
+ help="Reboot into download mode (no BOOT+RST needed)")
+
+ args = parser.parse_args()
+ if not args.command:
+ parser.print_help()
+ return
+
+ s = get_serial(args.port)
+ try:
+ if args.command in ("screenshot", "ss"):
+ cmd_screenshot(s, args.output)
+ elif args.command in ("metrics", "m"):
+ cmd_metrics(s)
+ elif args.command in ("touch", "t"):
+ cmd_touch(s, args.x, args.y, args.duration)
+ elif args.command in ("swipe", "sw"):
+ cmd_swipe(s, args.direction)
+ elif args.command in ("navigate", "nav", "n"):
+ cmd_navigate(s, args.screen)
+ elif args.command in ("invalidate", "inv"):
+ cmd_invalidate(s)
+ elif args.command in ("crypto", "c"):
+ cmd_crypto(s)
+ elif args.command in ("profile", "p"):
+ cmd_profile(s, getattr(args, 'save', None))
+ elif args.command in ("log", "l"):
+ cmd_log(s)
+ elif args.command in ("files", "f"):
+ cmd_files(s)
+ elif args.command in ("download", "dl"):
+ cmd_download(s, args.index, args.output)
+ elif args.command in ("reset", "x"):
+ cmd_simple(s, 'X', "Reset sent")
+ elif args.command in ("bootloader", "z"):
+ cmd_simple(s, 'Z', "Rebooting into download mode...")
+ finally:
+ s.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/test_ifac.py b/scripts/test_ifac.py
new file mode 100644
index 00000000..ff85a7d5
--- /dev/null
+++ b/scripts/test_ifac.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+"""
+IFAC Crypto Test — compares IfacAuth.h C implementation against Reticulum Python
+
+Tests each stage of the IFAC pipeline:
+1. Ed25519 keypair derivation from seed
+2. Ed25519 signing
+3. HKDF expansion
+4. Full IFAC apply on a test packet
+
+Run standalone to generate test vectors, or with --firmware to
+send test vectors to the watch and compare output.
+
+Usage:
+ python3 scripts/test_ifac.py # generate test vectors
+ python3 scripts/test_ifac.py --firmware # compare with firmware
+"""
+
+import sys
+import os
+import hashlib
+import hmac
+
+# Add RNS to path
+sys.path.insert(0, os.path.expanduser("~/.local/lib/python3.13/site-packages"))
+
+import RNS
+from RNS.Cryptography.pure25519 import ed25519_oop as ed25519
+from RNS.Cryptography.pure25519._ed25519 import sign as ed25519_raw_sign
+
+
+def hkdf_sha256(ikm, salt, length, context=None):
+ """Replicate RNS.Cryptography.hkdf exactly"""
+ return RNS.Cryptography.hkdf(
+ length=length,
+ derive_from=ikm,
+ salt=salt,
+ context=context,
+ )
+
+
+def hkdf_sha256_manual(ikm, salt, output_len):
+ """Manual HKDF-SHA256 matching our C implementation"""
+ # Extract
+ prk = hmac.new(salt, ikm, hashlib.sha256).digest()
+
+ # Expand
+ output = b""
+ prev_block = b""
+ block_idx = 0
+ while len(output) < output_len:
+ expand_input = prev_block + bytes([(block_idx + 1) % 256])
+ block = hmac.new(prk, expand_input, hashlib.sha256).digest()
+ output += block
+ prev_block = block
+ block_idx += 1
+
+ return output[:output_len]
+
+
+def ifac_apply_python(pkt, ifac_key, ifac_size=8):
+ """Replicate Reticulum's Transport.transmit IFAC application"""
+ # Create identity from ifac_key (last 32 bytes = Ed25519 seed)
+ sig_seed = ifac_key[32:]
+ identity = RNS.Identity.from_bytes(ifac_key)
+
+ # 1. Sign the original packet
+ sig = identity.sign(pkt) # Returns 64-byte signature
+ assert len(sig) == 64, f"Signature length: {len(sig)}"
+
+ # 2. Extract IFAC: last 8 bytes of signature
+ ifac = sig[-ifac_size:]
+
+ # 3. Generate mask
+ mask = RNS.Cryptography.hkdf(
+ length=len(pkt) + ifac_size,
+ derive_from=ifac,
+ salt=ifac_key,
+ context=None,
+ )
+
+ # 4. Set IFAC flag + assemble
+ new_header = bytes([pkt[0] | 0x80, pkt[1]])
+ new_raw = new_header + ifac + pkt[2:]
+
+ # 5. Mask
+ masked = bytearray()
+ for i, byte in enumerate(new_raw):
+ if i == 0:
+ masked.append((byte ^ mask[i]) | 0x80)
+ elif i == 1 or i > ifac_size + 1:
+ masked.append(byte ^ mask[i])
+ else:
+ masked.append(byte) # Don't mask IFAC itself
+
+ return bytes(masked)
+
+
+def main():
+ print("=" * 60)
+ print("IFAC Crypto Test Vectors")
+ print("=" * 60)
+
+ # Known IFAC key (helv4net / R3ticulum-priv8-m3sh)
+ network_name = "helv4net"
+ passphrase = "R3ticulum-priv8-m3sh"
+
+ ifac_origin = b""
+ ifac_origin += RNS.Identity.full_hash(network_name.encode("utf-8"))
+ ifac_origin += RNS.Identity.full_hash(passphrase.encode("utf-8"))
+ ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
+
+ ifac_key = hkdf_sha256(ifac_origin_hash, RNS.Reticulum.IFAC_SALT, 64)
+
+ print(f"\n1. IFAC Key Derivation")
+ print(f" Network: {network_name}")
+ print(f" Pass: {passphrase}")
+ print(f" Key: {ifac_key.hex()}")
+
+ # Ed25519 seed = last 32 bytes of ifac_key
+ ed25519_seed = ifac_key[32:]
+ print(f"\n2. Ed25519 Seed (ifac_key[32:64])")
+ print(f" Seed: {ed25519_seed.hex()}")
+
+ # Derive keypair
+ sk = ed25519.SigningKey(ed25519_seed)
+ pk = sk.get_verifying_key()
+ print(f" PK: {pk.to_bytes().hex()}")
+
+ # Also show the full sk_s (seed + pk, 64 bytes — libsodium format)
+ sk_s = sk.sk_s # This is the internal 64-byte representation
+ print(f" SK(64): {sk_s.hex()}")
+
+ # Test signing with a known message
+ test_msg = bytes([0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
+ 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0x77, 0x88, 0x99])
+ print(f"\n3. Ed25519 Signature Test")
+ print(f" Msg: {test_msg.hex()}")
+
+ sig = sk.sign(test_msg)
+ print(f" Sig: {sig.hex()}")
+ print(f" Last 8: {sig[-8:].hex()}")
+
+ # HKDF test
+ test_ikm = sig[-8:]
+ print(f"\n4. HKDF Test")
+ print(f" IKM: {test_ikm.hex()}")
+ print(f" Salt: {ifac_key.hex()}")
+
+ mask_rns = hkdf_sha256(test_ikm, ifac_key, 27)
+ mask_manual = hkdf_sha256_manual(test_ikm, ifac_key, 27)
+ print(f" RNS: {mask_rns.hex()}")
+ print(f" Manual: {mask_manual.hex()}")
+ print(f" Match: {mask_rns == mask_manual}")
+
+ # Full IFAC apply
+ print(f"\n5. Full IFAC Apply")
+ print(f" Input: {test_msg.hex()} ({len(test_msg)} bytes)")
+
+ result = ifac_apply_python(test_msg, ifac_key)
+ print(f" Output: {result.hex()} ({len(result)} bytes)")
+ print(f" Size: {len(test_msg)} -> {len(result)}")
+
+ # Generate C test vector
+ print(f"\n{'=' * 60}")
+ print(f"C Test Vectors (paste into firmware test)")
+ print(f"{'=' * 60}")
+ print(f"const uint8_t test_ifac_key[64] = {{{', '.join(f'0x{b:02x}' for b in ifac_key)}}};")
+ print(f"const uint8_t test_ed25519_seed[32] = {{{', '.join(f'0x{b:02x}' for b in ed25519_seed)}}};")
+ print(f"const uint8_t test_ed25519_pk[32] = {{{', '.join(f'0x{b:02x}' for b in pk.to_bytes())}}};")
+ print(f"const uint8_t test_msg[{len(test_msg)}] = {{{', '.join(f'0x{b:02x}' for b in test_msg)}}};")
+ print(f"const uint8_t test_sig[64] = {{{', '.join(f'0x{b:02x}' for b in sig)}}};")
+ print(f"const uint8_t test_ifac[8] = {{{', '.join(f'0x{b:02x}' for b in sig[-8:])}}};")
+ print(f"const uint8_t test_mask[{len(result)}] = {{{', '.join(f'0x{b:02x}' for b in mask_rns[:len(result)])}}};")
+ print(f"const uint8_t test_result[{len(result)}] = {{{', '.join(f'0x{b:02x}' for b in result)}}};")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/test_lxmf_send.py b/scripts/test_lxmf_send.py
new file mode 100644
index 00000000..97dc89e7
--- /dev/null
+++ b/scripts/test_lxmf_send.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+"""Send an OPPORTUNISTIC LXMF message to the R-Watch via LoRa.
+
+Usage:
+ python3 scripts/test_lxmf_send.py [-p PORT] [-m MESSAGE]
+
+Sends a single encrypted LXMF packet to the watch's destination hash
+via the T-Beam RNode. The watch should decrypt and display it on the
+Messages screen.
+
+Requires: RNS, LXMF (pip install rns lxmf)
+"""
+
+import argparse
+import hashlib
+import os
+import struct
+import time
+
+# Watch identity (from beacon dump)
+WATCH_DEST_HASH = bytes.fromhex("e76d4c209f2e0d591dc4a97864d1cc7e")
+WATCH_X25519_PUB = bytes.fromhex("1c8471b6bbd403c3fd0f8e3b8cd509b428edab2360ef8d0e8a35c8e7d853546f")
+WATCH_ED25519_PUB = bytes.fromhex("6068b81e12ae671b762994333d2b0c34a13637b8e6c15c38164c53ed03c175d1")
+
+# Compute watch's identity hash = SHA256(x25519_pub + ed25519_pub)[:16]
+WATCH_IDENTITY_HASH = hashlib.sha256(WATCH_X25519_PUB + WATCH_ED25519_PUB).digest()[:16]
+
+
+def build_lxmf_opportunistic(message: str, dest_hash: bytes, dest_x25519_pub: bytes,
+ dest_identity_hash: bytes) -> bytes:
+ """Build an OPPORTUNISTIC LXMF packet ready for LoRa transmission."""
+ import RNS
+ from nacl.signing import SigningKey
+ from nacl.public import PrivateKey
+ import nacl.bindings
+
+ # Generate ephemeral sender identity for this message
+ sender_signing_key = SigningKey.generate()
+ sender_verify_key = sender_signing_key.verify_key
+ sender_ed25519_pub = bytes(sender_verify_key)
+ # libsodium ed25519 sk is seed(32) + pub(32) = 64 bytes
+ sender_ed25519_sk_full = bytes(sender_signing_key) + sender_ed25519_pub
+
+ # Derive X25519 keys from Ed25519
+ sender_x25519_pub = nacl.bindings.crypto_sign_ed25519_pk_to_curve25519(sender_ed25519_pub)
+ sender_x25519_sk = nacl.bindings.crypto_sign_ed25519_sk_to_curve25519(sender_ed25519_sk_full)
+
+ # Compute sender's identity hash and source hash
+ sender_identity_hash = hashlib.sha256(sender_x25519_pub + sender_ed25519_pub).digest()[:16]
+ name_hash = hashlib.sha256(b"lxmf" + b"." + b"delivery").digest()[:10]
+ sender_source_hash = hashlib.sha256(name_hash + sender_identity_hash).digest()[:16]
+
+ # Build msgpack payload: [timestamp, title, content, {}]
+ import msgpack
+ timestamp = int(time.time())
+ payload = msgpack.packb([timestamp, b"", message.encode("utf-8"), {}])
+
+ # Build LXMF plaintext: source_hash(16) + signature(64) + payload
+ # Signature covers: dest_hash + source_hash + payload + SHA256(same)
+ hashed_part = dest_hash + sender_source_hash + payload
+ message_hash = hashlib.sha256(hashed_part).digest()
+ signed_part = hashed_part + message_hash
+ signature = sender_signing_key.sign(signed_part).signature
+
+ lxmf_plaintext = sender_source_hash + signature + payload
+ print(f"LXMF plaintext: {len(lxmf_plaintext)} bytes")
+ print(f" source_hash: {sender_source_hash.hex()}")
+ print(f" content: \"{message}\"")
+
+ # Encrypt with Token format: ephemeral_pub(32) + IV(16) + ciphertext + HMAC(32)
+ # ECDH with destination's X25519 public key
+ shared_secret = nacl.bindings.crypto_scalarmult(sender_x25519_sk, dest_x25519_pub)
+
+ # HKDF: derive signing_key(32) + encryption_key(32)
+ derived = RNS.Cryptography.hkdf(
+ length=64,
+ derive_from=shared_secret,
+ salt=dest_identity_hash,
+ context=None
+ )
+ signing_key = derived[:32]
+ encryption_key = derived[32:]
+
+ # Generate ephemeral X25519 keypair for the Token
+ eph_private = PrivateKey.generate()
+ eph_public = bytes(eph_private.public_key)
+ eph_shared = nacl.bindings.crypto_scalarmult(bytes(eph_private), dest_x25519_pub)
+
+ # Re-derive keys using ephemeral (this is what the receiver will do)
+ derived2 = RNS.Cryptography.hkdf(
+ length=64,
+ derive_from=eph_shared,
+ salt=dest_identity_hash,
+ context=None
+ )
+ token_signing_key = derived2[:32]
+ token_encryption_key = derived2[32:]
+
+ # AES-256-CBC encrypt with PKCS7 padding
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+ from cryptography.hazmat.primitives import padding as sym_padding
+
+ iv = os.urandom(16)
+ padder = sym_padding.PKCS7(128).padder()
+ padded = padder.update(lxmf_plaintext) + padder.finalize()
+
+ cipher = Cipher(algorithms.AES(token_encryption_key), modes.CBC(iv))
+ encryptor = cipher.encryptor()
+ ciphertext = encryptor.update(padded) + encryptor.finalize()
+
+ # HMAC-SHA256(signing_key, IV || ciphertext)
+ import hmac as hmac_mod
+ h = hmac_mod.new(token_signing_key, iv + ciphertext, hashlib.sha256)
+ hmac_tag = h.digest()
+
+ # Token: ephemeral_pub(32) + IV(16) + ciphertext + HMAC(32)
+ encrypted = eph_public + iv + ciphertext + hmac_tag
+ print(f"Encrypted payload: {len(encrypted)} bytes")
+
+ # RNS packet: flags(1) + hops(1) + dest_hash(16) + context(1) + encrypted
+ rns_flags = 0x00 # HEADER_1, BROADCAST, SINGLE, DATA
+ rns_hops = 0x00
+ rns_context = 0x00
+
+ packet = bytes([rns_flags, rns_hops]) + dest_hash + bytes([rns_context]) + encrypted
+ print(f"Total RNS packet: {len(packet)} bytes")
+
+ if len(packet) > 255:
+ print(f"WARNING: packet too large for single LoRa frame ({len(packet)} > 255)")
+
+ return packet
+
+
+def send_via_kiss(port: str, packet: bytes):
+ """Send a raw packet via KISS to an RNode."""
+ import serial
+
+ FEND = 0xC0
+ FESC = 0xDB
+ TFEND = 0xDC
+ TFESC = 0xDD
+ CMD_DATA = 0x00
+
+ # KISS escape
+ escaped = bytearray()
+ for b in packet:
+ if b == FEND:
+ escaped += bytes([FESC, TFEND])
+ elif b == FESC:
+ escaped += bytes([FESC, TFESC])
+ else:
+ escaped.append(b)
+
+ frame = bytes([FEND, CMD_DATA]) + bytes(escaped) + bytes([FEND])
+
+ s = serial.Serial(port, 115200, timeout=5)
+ time.sleep(0.5)
+ s.write(frame)
+ s.flush()
+ print(f"Sent {len(frame)} bytes KISS frame to {port}")
+ time.sleep(2)
+ s.close()
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Send LXMF message to R-Watch")
+ parser.add_argument("-p", "--port", default="/dev/ttyACM0",
+ help="T-Beam RNode serial port")
+ parser.add_argument("-m", "--message", default="Hello from test!",
+ help="Message content")
+ args = parser.parse_args()
+
+ print(f"Sending to watch dest: {WATCH_DEST_HASH.hex()}")
+ print(f"Watch identity hash: {WATCH_IDENTITY_HASH.hex()}")
+ print()
+
+ packet = build_lxmf_opportunistic(
+ args.message,
+ WATCH_DEST_HASH,
+ WATCH_X25519_PUB,
+ WATCH_IDENTITY_HASH
+ )
+
+ print()
+ send_via_kiss(args.port, packet)
+ print("Done! Check watch Messages screen.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/sdrconnect_lora_scanner.py b/sdrconnect_lora_scanner.py
new file mode 100644
index 00000000..2c15c262
--- /dev/null
+++ b/sdrconnect_lora_scanner.py
@@ -0,0 +1,668 @@
+#!/usr/bin/env python3
+"""
+SDRconnect Headless WebSocket client for LoRa signal detection at 868 MHz.
+
+Connects to SDRconnect_headless via its WebSocket API (port 5454 by default),
+tunes the RSPduo to 868 MHz, and monitors signal power + spectrum data to
+detect LoRa activity.
+
+Usage:
+ # First, start SDRconnect_headless:
+ # /opt/sdrconnect/SDRconnect_headless --websocket_port=5454
+ #
+ # Then run this script:
+ # python3 sdrconnect_lora_scanner.py
+ # python3 sdrconnect_lora_scanner.py --host 127.0.0.1 --port 5454
+ # python3 sdrconnect_lora_scanner.py --save-spectrum spectrum.png
+ # python3 sdrconnect_lora_scanner.py --iq-capture iq_data.bin --iq-seconds 5
+
+Protocol reference: SDRconnect WebSocket API 1.0.1
+ https://www.sdrplay.com/docs/SDRconnect_WebSocket_API.pdf
+"""
+
+import argparse
+import asyncio
+import json
+import struct
+import sys
+import time
+from collections import deque
+from datetime import datetime
+
+import numpy as np
+
+try:
+ import websockets
+except ImportError:
+ print("ERROR: 'websockets' package required. Install with: pip3 install websockets")
+ sys.exit(1)
+
+
+# --- SDRconnect WebSocket API message helpers ---
+
+def make_msg(event_type: str, property_name: str = "", value: str = "") -> str:
+ """Build a JSON message for the SDRconnect WebSocket API."""
+ return json.dumps({
+ "event_type": event_type,
+ "property": property_name,
+ "value": value
+ })
+
+
+def set_property(prop: str, value) -> str:
+ return make_msg("set_property", prop, str(value))
+
+
+def get_property(prop: str) -> str:
+ return make_msg("get_property", prop)
+
+
+# --- Binary message parser ---
+
+PAYLOAD_TYPE_AUDIO = 1 # Signed 16-bit PCM Stereo @ 48kHz (LRLR)
+PAYLOAD_TYPE_IQ = 2 # Signed 16-bit interleaved IQ (IQIQ)
+PAYLOAD_TYPE_SPECTRUM = 3 # Unsigned 8-bit spectrum FFT bins
+
+
+def parse_binary_message(data: bytes):
+ """
+ Parse a binary WebSocket message from SDRconnect.
+ Returns (payload_type, payload_data).
+
+ Binary messages start with a 2-byte little-endian uint16 payload type:
+ 1 = Signed 16-bit PCM Stereo Audio @ 48kHz (LRLR)
+ 2 = Signed 16-bit interleaved IQ (IQIQ)
+ 3 = Unsigned 8-bit spectrum FFT bins normalised to visible range
+ """
+ if len(data) < 2:
+ return None, None
+ payload_type = struct.unpack_from(' asyncio.Event
+ self._property_values = {} # property name -> latest value
+
+ # Data buffers
+ self.spectrum_bins = deque(maxlen=200) # last N spectrum snapshots
+ self.iq_buffer = bytearray()
+ self.signal_power_history = deque(maxlen=1000)
+ self.signal_snr_history = deque(maxlen=1000)
+
+ # Stats
+ self.spectrum_count = 0
+ self.iq_sample_count = 0
+
+ async def connect(self):
+ """Establish WebSocket connection."""
+ print(f"Connecting to {self.uri} ...")
+ self.ws = await websockets.connect(
+ self.uri,
+ max_size=10 * 1024 * 1024, # 10 MB max message
+ open_timeout=10,
+ )
+ print("Connected.")
+
+ async def close(self):
+ if self.ws:
+ await self.ws.close()
+ print("Connection closed.")
+
+ async def send(self, msg: str):
+ """Send a JSON text message."""
+ await self.ws.send(msg)
+
+ async def send_set_property(self, prop: str, value):
+ await self.send(set_property(prop, value))
+
+ async def send_get_property(self, prop: str):
+ await self.send(get_property(prop))
+
+ async def request_property(self, prop: str, timeout: float = 5.0) -> str:
+ """Send get_property and wait for the response."""
+ evt = asyncio.Event()
+ self._property_events[prop] = evt
+ await self.send_get_property(prop)
+ try:
+ await asyncio.wait_for(evt.wait(), timeout=timeout)
+ except asyncio.TimeoutError:
+ print(f" WARNING: Timeout waiting for property '{prop}'")
+ return None
+ finally:
+ self._property_events.pop(prop, None)
+ return self._property_values.get(prop)
+
+ def _handle_json(self, msg: dict):
+ """Process an incoming JSON message."""
+ event_type = msg.get("event_type", "")
+ prop = msg.get("property", "")
+ value = msg.get("value", "")
+
+ if event_type == "property_changed":
+ self.properties[prop] = value
+ if prop == "signal_power":
+ try:
+ self.signal_power_history.append(
+ (time.time(), float(value))
+ )
+ except (ValueError, TypeError):
+ pass
+ elif prop == "signal_snr":
+ try:
+ self.signal_snr_history.append(
+ (time.time(), float(value))
+ )
+ except (ValueError, TypeError):
+ pass
+ # Wake up anyone waiting for this property
+ if prop in self._property_events:
+ self._property_values[prop] = value
+ self._property_events[prop].set()
+
+ elif event_type == "get_property_response":
+ self._property_values[prop] = value
+ if prop in self._property_events:
+ self._property_events[prop].set()
+
+ def _handle_binary(self, data: bytes):
+ """Process an incoming binary message."""
+ payload_type, payload = parse_binary_message(data)
+
+ if payload_type == PAYLOAD_TYPE_SPECTRUM:
+ bins = np.frombuffer(payload, dtype=np.uint8)
+ self.spectrum_bins.append((time.time(), bins.copy()))
+ self.spectrum_count += 1
+
+ elif payload_type == PAYLOAD_TYPE_IQ:
+ self.iq_buffer.extend(payload)
+ # Count IQ sample pairs (each pair = 4 bytes: I16 + Q16)
+ self.iq_sample_count += len(payload) // 4
+
+ async def receive_loop(self, duration: float = None):
+ """
+ Receive messages in a loop. If duration is set, stop after that
+ many seconds. Otherwise run until cancelled.
+ """
+ start = time.time()
+ try:
+ async for message in self.ws:
+ if isinstance(message, str):
+ try:
+ msg = json.loads(message)
+ self._handle_json(msg)
+ except json.JSONDecodeError:
+ print(f" WARNING: Non-JSON text message: {message[:100]}")
+ elif isinstance(message, bytes):
+ self._handle_binary(message)
+
+ if duration and (time.time() - start) >= duration:
+ break
+ except websockets.ConnectionClosed as e:
+ print(f"Connection closed: {e}")
+
+ # --- High-level commands ---
+
+ async def select_device(self, index: int = 0):
+ """Select device by index."""
+ print(f"Selecting device index {index} ...")
+ await self.send(make_msg("selected_device", "", str(index)))
+ await asyncio.sleep(1)
+
+ async def start_device_streaming(self):
+ """Start the device hardware streaming."""
+ print("Starting device streaming ...")
+ await self.send(make_msg("device_stream_enable", "", "true"))
+ await asyncio.sleep(1)
+
+ async def stop_device_streaming(self):
+ """Stop the device hardware streaming."""
+ await self.send(make_msg("device_stream_enable", "", "false"))
+
+ async def enable_spectrum(self, enable: bool = True):
+ val = "true" if enable else "false"
+ print(f"Spectrum streaming: {val}")
+ await self.send(make_msg("spectrum_enable", "", val))
+
+ async def enable_iq_stream(self, enable: bool = True):
+ val = "true" if enable else "false"
+ print(f"IQ streaming: {val}")
+ await self.send(make_msg("iq_stream_enable", "", val))
+
+ async def tune(self, center_freq: int, vfo_freq: int = None,
+ sample_rate: float = 2e6, bandwidth: int = None):
+ """
+ Tune the radio.
+ center_freq: LO frequency in Hz
+ vfo_freq: VFO frequency in Hz (defaults to center_freq)
+ sample_rate: sample rate in Hz
+ bandwidth: filter bandwidth in Hz
+ """
+ if vfo_freq is None:
+ vfo_freq = center_freq
+
+ print(f"Setting center frequency: {center_freq/1e6:.3f} MHz")
+ await self.send_set_property("device_center_frequency", center_freq)
+ await asyncio.sleep(0.3)
+
+ print(f"Setting VFO frequency: {vfo_freq/1e6:.3f} MHz")
+ await self.send_set_property("device_vfo_frequency", vfo_freq)
+ await asyncio.sleep(0.3)
+
+ print(f"Setting sample rate: {sample_rate/1e6:.1f} MHz")
+ await self.send_set_property("device_sample_rate", int(sample_rate))
+ await asyncio.sleep(0.3)
+
+ if bandwidth:
+ print(f"Setting filter bandwidth: {bandwidth/1e3:.0f} kHz")
+ await self.send_set_property("filter_bandwidth", bandwidth)
+ await asyncio.sleep(0.3)
+
+ async def configure_for_lora_868(self, sample_rate: float = 2e6):
+ """
+ Configure for LoRa monitoring at 868 MHz (EU ISM band).
+
+ LoRa EU868 channels:
+ 868.1 MHz, 868.3 MHz, 868.5 MHz (common 3-channel)
+ 867.1, 867.3, 867.5, 867.7, 867.9 MHz (additional)
+ 869.525 MHz (downlink RX2)
+
+ With 2 MHz sample rate centered at 868.3 MHz, we cover
+ ~867.3 - 869.3 MHz which includes the 3 primary channels.
+ """
+ center = 868_300_000 # 868.3 MHz - center of primary LoRa channels
+ await self.tune(
+ center_freq=center,
+ vfo_freq=center,
+ sample_rate=sample_rate,
+ bandwidth=int(sample_rate),
+ )
+ # Use NFM demodulator (closest to what we want for monitoring)
+ print("Setting demodulator: NFM")
+ await self.send_set_property("demodulator", "NFM")
+ await asyncio.sleep(0.2)
+
+ async def print_device_info(self):
+ """Query and print current device state."""
+ props = [
+ "device_center_frequency", "device_vfo_frequency",
+ "device_sample_rate", "filter_bandwidth",
+ "lna_state", "lna_state_min", "lna_state_max",
+ "demodulator", "started", "can_control",
+ "signal_power", "signal_snr",
+ ]
+ print("\n--- Device State ---")
+ for p in props:
+ val = await self.request_property(p, timeout=3.0)
+ if val is not None:
+ self.properties[p] = val
+ # Format frequency values nicely
+ if "frequency" in p and val and val.isdigit():
+ freq_mhz = int(val) / 1e6
+ print(f" {p}: {val} ({freq_mhz:.3f} MHz)")
+ elif "sample_rate" in p and val:
+ try:
+ sr = float(val)
+ print(f" {p}: {val} ({sr/1e6:.3f} MHz)")
+ except ValueError:
+ print(f" {p}: {val}")
+ else:
+ print(f" {p}: {val}")
+ else:
+ print(f" {p}: (no response)")
+ print("---\n")
+
+
+async def monitor_lora(args):
+ """Main monitoring routine."""
+ client = SDRconnectClient(host=args.host, port=args.port)
+
+ try:
+ await client.connect()
+
+ # Select device (index 0 = first device)
+ await client.select_device(args.device_index)
+
+ # Start hardware streaming
+ await client.start_device_streaming()
+ await asyncio.sleep(1)
+
+ # Tune to 868 MHz LoRa band
+ await client.configure_for_lora_868(sample_rate=args.sample_rate)
+ await asyncio.sleep(1)
+
+ # Query current state
+ # Start receive loop in background to handle property responses
+ recv_task = asyncio.create_task(client.receive_loop())
+ await asyncio.sleep(0.5) # let loop start
+
+ await client.print_device_info()
+
+ # Enable spectrum streaming
+ await client.enable_spectrum(True)
+
+ # Optionally enable IQ streaming
+ if args.iq_capture:
+ await client.enable_iq_stream(True)
+
+ # Cancel the generic receive loop, we'll run our own timed one
+ recv_task.cancel()
+ try:
+ await recv_task
+ except asyncio.CancelledError:
+ pass
+
+ # --- Main monitoring loop ---
+ print(f"\nMonitoring 868 MHz LoRa band for {args.duration} seconds ...")
+ print(f" Center: 868.3 MHz | BW: {args.sample_rate/1e6:.1f} MHz")
+ print(f" Coverage: ~{(868.3 - args.sample_rate/2e6):.1f} - "
+ f"{(868.3 + args.sample_rate/2e6):.1f} MHz")
+ print()
+
+ start_time = time.time()
+ last_report = start_time
+ report_interval = 2.0 # Print status every N seconds
+
+ try:
+ async for message in client.ws:
+ now = time.time()
+ elapsed = now - start_time
+
+ if isinstance(message, str):
+ try:
+ msg = json.loads(message)
+ client._handle_json(msg)
+ except json.JSONDecodeError:
+ pass
+ elif isinstance(message, bytes):
+ client._handle_binary(message)
+
+ # Periodic status report
+ if now - last_report >= report_interval:
+ last_report = now
+ # Get latest signal power/SNR from property_changed events
+ power = client.properties.get("signal_power", "?")
+ snr = client.properties.get("signal_snr", "?")
+ spec_count = client.spectrum_count
+ iq_count = client.iq_sample_count
+
+ ts = datetime.now().strftime("%H:%M:%S")
+ line = (f"[{ts}] t={elapsed:.0f}s | "
+ f"Power: {power} dB | SNR: {snr} dB | "
+ f"Spectrum frames: {spec_count}")
+ if args.iq_capture:
+ line += f" | IQ samples: {iq_count}"
+ print(line)
+
+ # Detect LoRa-like activity: sudden power increases
+ if client.signal_power_history:
+ recent = [p for t, p in client.signal_power_history
+ if now - t < 5.0]
+ if len(recent) >= 2:
+ avg = sum(recent) / len(recent)
+ peak = max(recent)
+ if peak - avg > 6: # 6 dB spike
+ print(f" >>> SIGNAL DETECTED: "
+ f"peak={peak:.1f} dB, avg={avg:.1f} dB, "
+ f"delta={peak-avg:.1f} dB")
+
+ if elapsed >= args.duration:
+ break
+
+ except websockets.ConnectionClosed as e:
+ print(f"Connection closed during monitoring: {e}")
+
+ # --- Post-capture analysis ---
+ print(f"\n--- Capture Summary ---")
+ print(f"Duration: {args.duration} seconds")
+ print(f"Spectrum frames captured: {client.spectrum_count}")
+ print(f"IQ samples captured: {client.iq_sample_count}")
+
+ if client.signal_power_history:
+ powers = [p for _, p in client.signal_power_history]
+ print(f"Signal power: min={min(powers):.1f} dB, "
+ f"max={max(powers):.1f} dB, "
+ f"mean={sum(powers)/len(powers):.1f} dB")
+
+ if client.signal_snr_history:
+ snrs = [s for _, s in client.signal_snr_history]
+ print(f"Signal SNR: min={min(snrs):.1f} dB, "
+ f"max={max(snrs):.1f} dB, "
+ f"mean={sum(snrs)/len(snrs):.1f} dB")
+
+ # Save spectrum plot if requested
+ if args.save_spectrum and client.spectrum_bins:
+ save_spectrum_plot(client, args)
+
+ # Save IQ data if requested
+ if args.iq_capture and client.iq_buffer:
+ save_iq_data(client, args)
+
+ # Analyze spectrum for LoRa signatures
+ if client.spectrum_bins:
+ analyze_spectrum_for_lora(client, args)
+
+ # Cleanup
+ await client.enable_spectrum(False)
+ if args.iq_capture:
+ await client.enable_iq_stream(False)
+ await client.stop_device_streaming()
+
+ finally:
+ await client.close()
+
+
+def save_spectrum_plot(client, args):
+ """Save a waterfall/spectrum plot of captured data."""
+ try:
+ import matplotlib
+ matplotlib.use('Agg')
+ import matplotlib.pyplot as plt
+ except ImportError:
+ print("WARNING: matplotlib not available, skipping spectrum plot")
+ return
+
+ # Build waterfall from spectrum history
+ timestamps = []
+ spectra = []
+ for t, bins in client.spectrum_bins:
+ timestamps.append(t)
+ spectra.append(bins)
+
+ if not spectra:
+ print("No spectrum data to plot.")
+ return
+
+ # Normalize to same length (take the most common length)
+ lengths = [len(s) for s in spectra]
+ target_len = max(set(lengths), key=lengths.count)
+ spectra = [s for s in spectra if len(s) == target_len]
+ if not spectra:
+ return
+
+ waterfall = np.array(spectra, dtype=np.float32)
+ # FFT bins are uint8 normalised to visible range by SDRconnect
+ # Higher values = stronger signal
+
+ sr = args.sample_rate
+ center = 868.3 # MHz
+ freq_axis = np.linspace(center - sr/2e6, center + sr/2e6, target_len)
+ time_axis = np.arange(len(waterfall))
+
+ fig, axes = plt.subplots(2, 1, figsize=(12, 8), gridspec_kw={'height_ratios': [1, 2]})
+
+ # Top: average spectrum
+ avg_spectrum = np.mean(waterfall, axis=0)
+ max_spectrum = np.max(waterfall, axis=0)
+ axes[0].plot(freq_axis, avg_spectrum, 'b-', alpha=0.7, label='Mean')
+ axes[0].plot(freq_axis, max_spectrum, 'r-', alpha=0.5, label='Peak')
+ axes[0].set_xlabel('Frequency (MHz)')
+ axes[0].set_ylabel('Magnitude (0-255)')
+ axes[0].set_title(f'868 MHz LoRa Band Spectrum ({len(waterfall)} frames)')
+ axes[0].legend()
+ axes[0].grid(True, alpha=0.3)
+
+ # Mark LoRa channel frequencies
+ lora_channels = [868.1, 868.3, 868.5]
+ for ch in lora_channels:
+ if freq_axis[0] <= ch <= freq_axis[-1]:
+ axes[0].axvline(x=ch, color='g', linestyle='--', alpha=0.5,
+ label=f'{ch} MHz' if ch == lora_channels[0] else '')
+ axes[0].text(ch, axes[0].get_ylim()[1] * 0.95, f'{ch}',
+ ha='center', va='top', fontsize=8, color='green')
+
+ # Bottom: waterfall
+ extent = [freq_axis[0], freq_axis[-1], len(waterfall), 0]
+ axes[1].imshow(waterfall, aspect='auto', extent=extent,
+ cmap='viridis', interpolation='nearest')
+ axes[1].set_xlabel('Frequency (MHz)')
+ axes[1].set_ylabel('Time (frame #)')
+ axes[1].set_title('Waterfall')
+ for ch in lora_channels:
+ if freq_axis[0] <= ch <= freq_axis[-1]:
+ axes[1].axvline(x=ch, color='r', linestyle='--', alpha=0.5)
+
+ plt.tight_layout()
+ outfile = args.save_spectrum
+ plt.savefig(outfile, dpi=150)
+ print(f"Spectrum plot saved to: {outfile}")
+ plt.close()
+
+
+def save_iq_data(client, args):
+ """Save captured IQ data to a binary file."""
+ outfile = args.iq_capture
+ with open(outfile, 'wb') as f:
+ f.write(bytes(client.iq_buffer))
+ n_samples = len(client.iq_buffer) // 4 # 2 bytes I + 2 bytes Q
+ print(f"IQ data saved to: {outfile} ({len(client.iq_buffer)} bytes, "
+ f"{n_samples} IQ samples)")
+ print(f" Format: signed 16-bit little-endian interleaved IQ (IQIQ)")
+ print(f" To load in Python:")
+ print(f" raw = np.fromfile('{outfile}', dtype=np.int16)")
+ print(f" iq = raw[0::2] + 1j * raw[1::2] # complex IQ")
+
+
+def analyze_spectrum_for_lora(client, args):
+ """Analyze captured spectrum data for LoRa-like signals."""
+ if not client.spectrum_bins:
+ return
+
+ # Get spectra of consistent length
+ lengths = [len(s) for _, s in client.spectrum_bins]
+ target_len = max(set(lengths), key=lengths.count)
+ spectra = [s for _, s in client.spectrum_bins if len(s) == target_len]
+ if not spectra:
+ return
+
+ waterfall = np.array(spectra, dtype=np.float32)
+ sr = args.sample_rate
+ center = 868.3e6
+ n_bins = target_len
+
+ # Map bin indices to frequencies
+ freqs = np.linspace(center - sr/2, center + sr/2, n_bins)
+
+ # LoRa channel frequencies
+ lora_channels = {
+ "EU868 ch1": 868.1e6,
+ "EU868 ch2": 868.3e6,
+ "EU868 ch3": 868.5e6,
+ }
+
+ print(f"\n--- LoRa Channel Analysis ---")
+ print(f"Spectrum bins: {n_bins}, frames: {len(spectra)}")
+
+ # For each LoRa channel, check for activity
+ # LoRa BW is typically 125 kHz or 250 kHz
+ lora_bw = 125e3 # Hz, typical LoRa BW
+
+ avg_spectrum = np.mean(waterfall, axis=0)
+ noise_floor = np.median(avg_spectrum)
+ print(f"Noise floor (median): {noise_floor:.1f}")
+
+ for name, ch_freq in lora_channels.items():
+ # Find bins covering this channel +/- half BW
+ ch_mask = (freqs >= ch_freq - lora_bw) & (freqs <= ch_freq + lora_bw)
+ if not np.any(ch_mask):
+ print(f" {name} ({ch_freq/1e6:.1f} MHz): outside capture range")
+ continue
+
+ ch_avg = np.mean(avg_spectrum[ch_mask])
+ ch_max = np.max(avg_spectrum[ch_mask])
+ ch_peak_frame = np.max(waterfall[:, ch_mask])
+
+ # Check for intermittent signals (LoRa is bursty)
+ ch_per_frame = np.mean(waterfall[:, ch_mask], axis=1)
+ above_noise = ch_per_frame > noise_floor + 10 # 10 units above noise
+ active_frames = np.sum(above_noise)
+ duty_cycle = active_frames / len(spectra) * 100
+
+ delta = ch_avg - noise_floor
+ print(f" {name} ({ch_freq/1e6:.1f} MHz):")
+ print(f" Avg level: {ch_avg:.1f} (delta from noise: {delta:+.1f})")
+ print(f" Peak level: {ch_max:.1f} (peak any frame: {ch_peak_frame:.1f})")
+ print(f" Active frames: {active_frames}/{len(spectra)} "
+ f"({duty_cycle:.1f}% duty cycle)")
+
+ if delta > 15:
+ print(f" ** STRONG signal activity detected **")
+ elif delta > 5:
+ print(f" * Possible signal activity *")
+ else:
+ print(f" (no significant activity)")
+
+ print()
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Monitor 868 MHz LoRa band via SDRconnect WebSocket API",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Prerequisites:
+ 1. Start SDRconnect_headless:
+ /opt/sdrconnect/SDRconnect_headless --websocket_port=5454
+
+ 2. Run this script:
+ python3 %(prog)s --duration 30 --save-spectrum lora_868.png
+
+Examples:
+ # Basic 30-second scan
+ %(prog)s --duration 30
+
+ # Save spectrum plot and IQ data
+ %(prog)s --duration 60 --save-spectrum spectrum.png --iq-capture iq.bin
+
+ # Different host/port
+ %(prog)s --host 192.168.1.100 --port 5454 --duration 30
+ """
+ )
+ parser.add_argument("--host", default="127.0.0.1",
+ help="SDRconnect WebSocket host (default: 127.0.0.1)")
+ parser.add_argument("--port", type=int, default=5454,
+ help="SDRconnect WebSocket port (default: 5454)")
+ parser.add_argument("--duration", type=float, default=30,
+ help="Monitoring duration in seconds (default: 30)")
+ parser.add_argument("--sample-rate", type=float, default=2e6,
+ help="Sample rate in Hz (default: 2000000)")
+ parser.add_argument("--device-index", type=int, default=0,
+ help="Device index to select (default: 0)")
+ parser.add_argument("--save-spectrum", metavar="FILE",
+ help="Save spectrum/waterfall plot to PNG file")
+ parser.add_argument("--iq-capture", metavar="FILE",
+ help="Capture raw IQ data to binary file")
+
+ args = parser.parse_args()
+ asyncio.run(monitor_lora(args))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/sx126x.cpp b/sx126x.cpp
index c67a4dfc..4639576a 100644
--- a/sx126x.cpp
+++ b/sx126x.cpp
@@ -45,6 +45,8 @@
#define OP_RX_TX_FALLBACK_MODE_6X 0x93
#define OP_REGULATOR_MODE_6X 0x96
#define OP_CALIBRATE_IMAGE_6X 0x98
+#define OP_GET_DEVICE_ERRORS_6X 0x17
+#define OP_CLR_DEVICE_ERRORS_6X 0x07
#define MASK_CALIBRATE_ALL 0x7f
@@ -98,6 +100,7 @@
extern SPIClass SPI;
+
#define MAX_PKT_LENGTH 255
sx126x::sx126x() :
@@ -118,14 +121,15 @@ sx126x::sx126x() :
_fifo_rx_addr_ptr(0),
_packet({0}),
_preinit_done(false),
- _onReceive(NULL)
+ _onReceive(NULL),
+ _rx_pending(false)
{ setTimeout(0); }
bool sx126x::preInit() {
pinMode(_ss, OUTPUT);
digitalWrite(_ss, HIGH);
- #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3
+ #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3 || BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TWATCH_ULT
SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs);
#elif BOARD_MODEL == BOARD_TECHO
SPI.setPins(pin_miso, pin_sclk, pin_mosi);
@@ -175,7 +179,6 @@ uint8_t ISR_VECT sx126x::singleTransfer(uint8_t opcode, uint16_t address, uint8_
if (opcode == OP_READ_REGISTER_6X) { SPI.transfer(0x00); }
response = SPI.transfer(value);
SPI.endTransaction();
-
digitalWrite(_ss, HIGH);
return response;
@@ -270,9 +273,23 @@ void sx126x::setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t
buf[4] = crc;
buf[5] = 0x00; // standard IQ setting (no inversion)
buf[6] = 0x00; // unused params
- buf[7] = 0x00;
- buf[8] = 0x00;
+ buf[7] = 0x00;
+ buf[8] = 0x00;
executeOpcode(OP_PACKET_PARAMS_6X, buf, 9);
+
+ // SX1262 errata section 15.4: IQ polarity is inverted compared to
+ // SX1276. Register 0x0736 must be set to correct the polarity.
+ // Standard IQ: set bit 2 (register value 0x0D → keep other bits).
+ // Inverted IQ: clear bit 2 (register value 0x09).
+ // Without this fix, LoRa RX demodulation fails silently.
+ uint8_t iqreg = readRegister(0x0736);
+ if (buf[5] == 0x00) {
+ // Standard IQ: set bit 2
+ writeRegister(0x0736, iqreg | 0x04);
+ } else {
+ // Inverted IQ: clear bit 2
+ writeRegister(0x0736, iqreg & ~0x04);
+ }
}
void sx126x::reset(void) {
@@ -311,7 +328,7 @@ void sx126x::calibrate_image(long frequency) {
int sx126x::begin(long frequency) {
reset();
-
+
if (_busy != -1) { pinMode(_busy, INPUT); }
if (!_preinit_done) { if (!preInit()) { return false; } }
if (_rxen != -1) { pinMode(_rxen, OUTPUT); }
@@ -336,11 +353,17 @@ int sx126x::begin(long frequency) {
setTxPower(2);
enableCrc();
writeRegister(REG_LNA_6X, 0x96); // Set LNA boost
+
+ #if HAS_LORA_PA && LORA_PA_GC1109
+ uint8_t reg8b5 = readRegister(0x08B5);
+ writeRegister(0x08B5, reg8b5 | 0x01);
+ #endif
uint8_t basebuf[2] = {0}; // Set base addresses
executeOpcode(OP_BUFFER_BASE_ADDR_6X, basebuf, 2);
setModulationParams(_sf, _bw, _cr, _ldro);
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+ optimizeModemSensitivity();
#if HAS_LORA_PA
#if LORA_PA_GC1109
@@ -349,25 +372,18 @@ int sx126x::begin(long frequency) {
pinMode(LORA_PA_PWR_EN, OUTPUT);
digitalWrite(LORA_PA_PWR_EN, HIGH);
- // Enable PA LNA and TX standby
+ // CSD: Chip enable - must be HIGH for GC1109 to work
pinMode(LORA_PA_CSD, OUTPUT);
digitalWrite(LORA_PA_CSD, HIGH);
- // Keep PA CPS low until actual
- // transmit. Does it save power?
- // Who knows? Will have to measure.
- // Note from the future: Nope.
- // Power consumption is the same,
- // and turning it on and off is
- // not something that it likes.
- // Keeping it high for now.
+ delay(1); // Allow GC1109 FEM time to power up
+
+ // CPS held HIGH permanently for now. Per GC1109 datasheet CPS is
+ // "don't care" in RX (LNA mode: CSD=1, CTX=0, CPS=X), and HIGH
+ // in TX (PA mode: CSD=1, CTX=1, CPS=1). Keeping it HIGH covers
+ // both modes. CTX is driven by SX1262 DIO2 automatically.
pinMode(LORA_PA_CPS, OUTPUT);
digitalWrite(LORA_PA_CPS, HIGH);
-
- // On Heltec V4, the PA CTX pin
- // is driven by the SX1262 DIO2
- // pin directly, so we do not
- // need to manually raise this.
#endif
#endif
@@ -377,16 +393,6 @@ int sx126x::begin(long frequency) {
void sx126x::end() { sleep(); SPI.end(); _preinit_done = false; }
int sx126x::beginPacket(int implicitHeader) {
- #if HAS_LORA_PA
- #if LORA_PA_GC1109
- // Enable PA CPS for transmit
- // digitalWrite(LORA_PA_CPS, HIGH);
- // Disabled since we're keeping it
- // on permanently as long as the
- // radio is powered up.
- #endif
- #endif
-
standby();
if (implicitHeader) { implicitHeaderMode(); }
else { explicitHeaderMode(); }
@@ -443,20 +449,31 @@ bool sx126x::dcd() {
if ((buf[1] & IRQ_HEADER_DET_MASK_6X) != 0) { header_detected = true; carrier_detected = true; }
else { header_detected = false; }
- if ((buf[1] & IRQ_PREAMBLE_DET_MASK_6X) != 0) {
+ // After a false preamble timeout, ignore new preamble detections
+ // briefly to prevent noise-driven preamble→timeout→preamble cycles
+ // that keep carrier_detected true and block CSMA transmissions.
+ static uint32_t preamble_cooldown_until = 0;
+
+ if ((buf[1] & IRQ_PREAMBLE_DET_MASK_6X) != 0 && now >= preamble_cooldown_until) {
carrier_detected = true;
if (preamble_detected_at == 0) { preamble_detected_at = now; }
if (now - preamble_detected_at > lora_preamble_time_ms + lora_header_time_ms) {
preamble_detected_at = 0;
if (!header_detected) { false_preamble_detected = true; }
- uint8_t clearbuf[2] = {0};
- clearbuf[1] = IRQ_PREAMBLE_DET_MASK_6X;
+ // Clear all IRQ flags except RX_DONE (bit 1) and CRC_ERR (bit 6)
+ // to prevent sticky flags (HeaderErr, HeaderValid, Preamble) from
+ // keeping carrier_detected true and blocking CSMA transmissions.
+ uint8_t clearbuf[2] = {0x03, 0xBD}; // bits 8-9 + 0-7 except 1,6
executeOpcode(OP_CLEAR_IRQ_STATUS_6X, clearbuf, 2);
+ // Cooldown: ignore preamble detections for 2x the preamble+header
+ // time to prevent noise-driven re-detection cycles.
+ preamble_cooldown_until = now + (lora_preamble_time_ms + lora_header_time_ms) * 2;
}
}
- // TODO: Maybe there's a way of unlatching the RSSI
- // status without re-activating receive mode?
+ // After a false preamble, re-enter RX to reset the SX1262 demodulator.
+ // Just clearing IRQ flags is insufficient — the internal demod chain can
+ // get stuck, detecting RF energy but never completing packet decode.
if (false_preamble_detected) { sx126x_modem.receive(); false_preamble_detected = false; }
return carrier_detected;
}
@@ -594,18 +611,6 @@ void sx126x::onReceive(void(*callback)(int)){
}
void sx126x::receive(int size) {
- #if HAS_LORA_PA
- #if LORA_PA_GC1109
- // Disable PA CPS for receive
- // digitalWrite(LORA_PA_CPS, LOW);
- // That turned out to be a bad idea.
- // The LNA goes wonky if it's toggled
- // on and off too quickly. We'll keep
- // it on permanently, as long as the
- // radio is powered up.
- #endif
- #endif
-
if (size > 0) {
implicitHeaderMode();
_payloadLength = size;
@@ -641,9 +646,13 @@ void sx126x::enableTCXO() {
#elif BOARD_MODEL == BOARD_TECHO
uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
#elif BOARD_MODEL == BOARD_HELTEC32_V4
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0xC8, 0x00}; // 800ms timeout
+ #elif BOARD_MODEL == BOARD_TWATCH_ULT
uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
#endif
executeOpcode(OP_DIO3_TCXO_CTRL_6X, buf, 4);
+ delay(10); // Allow TCXO to stabilize
+ waitOnBusy();
#endif
}
@@ -728,8 +737,16 @@ void sx126x::handleLowDataRate() {
else { _ldro = 0x00; lora_low_datarate = false; }
}
-// TODO: Check if there's anything the sx1262 can do here
-void sx126x::optimizeModemSensitivity(){ }
+// SX1262 errata section 15.1: Modulation quality with 500 kHz LoRa BW.
+// Register 0x0889 bit 2 must be cleared for 500 kHz, set for all others.
+void sx126x::optimizeModemSensitivity(){
+ uint8_t reg = readRegister(0x0889);
+ if (getSignalBandwidth() == 500E3) {
+ writeRegister(0x0889, reg & 0xFB); // clear bit 2
+ } else {
+ writeRegister(0x0889, reg | 0x04); // set bit 2
+ }
+}
void sx126x::setSignalBandwidth(long sbw) {
if (sbw <= 7.8E3) { _bw = 0x00; }
@@ -787,6 +804,16 @@ void sx126x::dumpRegisters(Stream& out) {
}
void ISR_VECT sx126x::handleDio0Rise() {
+ // Deferred ISR: no SPI from interrupt context.
+ // SPI operations in ISR can corrupt the bus on ESP32
+ // when the main loop is also accessing SPI.
+ _rx_pending = true;
+}
+
+void sx126x::processRxInterrupt() {
+ if (!_rx_pending) return;
+ _rx_pending = false;
+
uint8_t buf[2];
buf[0] = 0x00;
buf[1] = 0x00;
@@ -794,14 +821,61 @@ void ISR_VECT sx126x::handleDio0Rise() {
executeOpcode(OP_CLEAR_IRQ_STATUS_6X, buf, 2);
if ((buf[1] & IRQ_PAYLOAD_CRC_ERROR_MASK_6X) == 0) {
- _packetIndex = 0;
- uint8_t rxbuf[2] = {0}; // Read packet length
- executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2);
- int packetLength = rxbuf[0];
- if (_onReceive) { _onReceive(packetLength); }
+ if (buf[1] & IRQ_RX_DONE_MASK_6X) {
+ _packetIndex = 0;
+ uint8_t rxbuf[2] = {0};
+ executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2);
+ int packetLength = rxbuf[0];
+ if (_onReceive) { _onReceive(packetLength); }
+ }
}
}
+uint8_t sx126x::getModemStatus() {
+ // GetStatus (0xC0): status byte is returned during the NOP
+ // byte after the opcode, not in the data buffer.
+ waitOnBusy();
+ uint8_t status;
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(OP_STATUS_6X);
+ status = SPI.transfer(0x00);
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+ return status;
+}
+
+void sx126x::getIrqStatus(uint8_t *buf) {
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2);
+}
+
+void sx126x::getDeviceErrors(uint8_t *buf) {
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_DEVICE_ERRORS_6X, buf, 2);
+}
+
+uint32_t sx126x::getActualFrequency() {
+ // Read the 4-byte frequency register at 0x088B-0x088E
+ uint32_t freq_reg = ((uint32_t)readRegister(0x088B) << 24) |
+ ((uint32_t)readRegister(0x088C) << 16) |
+ ((uint32_t)readRegister(0x088D) << 8) |
+ readRegister(0x088E);
+ // Convert: freq_Hz = freq_reg * F_XTAL / 2^25 = freq_reg * 32000000 / 33554432
+ // Simplify: freq_Hz = freq_reg * 0.95367431640625
+ return (uint32_t)((uint64_t)freq_reg * 32000000ULL / 33554432ULL);
+}
+
+uint8_t sx126x::readPublicRegister(uint16_t address) { return readRegister(address); }
+
+uint8_t sx126x::getPacketType() {
+ uint8_t buf = 0;
+ executeOpcodeRead(0x11, &buf, 1); // GetPacketType
+ return buf;
+}
+
void ISR_VECT sx126x::onDio0Rise() { sx126x_modem.handleDio0Rise(); }
void sx126x::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); }
void sx126x::enableCrc() { _crcMode = 1; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
diff --git a/sx126x.h b/sx126x.h
index 068a1bb6..06af36af 100644
--- a/sx126x.h
+++ b/sx126x.h
@@ -84,6 +84,16 @@ class sx126x : public Stream {
void readBuffer(uint8_t* buffer, size_t size);
void setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc);
+ // Deferred ISR processing: call from main loop to handle RX
+ bool rxPending() { return _rx_pending; }
+ void processRxInterrupt();
+ uint8_t getModemStatus();
+ void getIrqStatus(uint8_t *buf);
+ void getDeviceErrors(uint8_t *buf);
+ uint32_t getActualFrequency();
+ uint8_t readPublicRegister(uint16_t address);
+ uint8_t getPacketType();
+
void setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, int ldro);
// deprecated
@@ -138,6 +148,7 @@ class sx126x : public Stream {
uint8_t _packet[255];
bool _preinit_done;
void (*_onReceive)(int);
+ volatile bool _rx_pending;
};
extern sx126x sx126x_modem;
diff --git a/test_kiss.py b/test_kiss.py
new file mode 100644
index 00000000..6013ef7d
--- /dev/null
+++ b/test_kiss.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+
+# Quick KISS protocol smoke test for RNode devices.
+# Sends standard KISS commands over USB serial and verifies responses.
+#
+# Usage:
+# python3 test_kiss.py [port]
+# make test-kiss PORT=/dev/ttyACM4
+
+import serial
+import struct
+import sys
+import time
+
+FEND = 0xC0
+
+def parse_kiss_frames(data):
+ frames = []
+ i = 0
+ while i < len(data):
+ if data[i] == 0xC0:
+ while i < len(data) and data[i] == 0xC0:
+ i += 1
+ frame = bytearray()
+ while i < len(data) and data[i] != 0xC0:
+ frame.append(data[i])
+ i += 1
+ if len(frame) > 0:
+ frames.append(frame)
+ else:
+ i += 1
+ return frames
+
+def wait_for_cmd(ser, target_cmd, timeout=2.0):
+ start = time.time()
+ buf = bytearray()
+ while time.time() - start < timeout:
+ chunk = ser.read(256)
+ if chunk:
+ buf.extend(chunk)
+ for f in parse_kiss_frames(buf):
+ if f[0] == target_cmd:
+ return f
+ else:
+ time.sleep(0.05)
+ return None
+
+def main():
+ port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM4"
+
+ ser = serial.Serial(port, 115200, timeout=0.1)
+ ser.dtr = False
+ time.sleep(0.1)
+ ser.dtr = True
+ time.sleep(4)
+ ser.reset_input_buffer()
+ time.sleep(1)
+ ser.read(8192)
+
+ tests = []
+
+ # Batch detect (same sequence rnodeconf uses)
+ detect_seq = bytes([
+ FEND, 0x08, 0x73, # CMD_DETECT, DETECT_REQ
+ FEND, 0x50, 0x00, # CMD_FW_VERSION
+ FEND, 0x48, 0x00, # CMD_PLATFORM
+ FEND, 0x49, 0x00, # CMD_MCU
+ FEND, 0x47, 0x00, # CMD_BOARD
+ FEND,
+ ])
+ ser.write(detect_seq)
+ time.sleep(1)
+ resp = ser.read(8192)
+ received = {}
+ if resp:
+ for f in parse_kiss_frames(resp):
+ received[f[0]] = f
+
+ checks = [
+ ("DETECT", 0x08, lambda f: f"0x{f[1]:02X}" if len(f) > 1 else "?"),
+ ("FW_VERSION", 0x50, lambda f: f"v{f[1]}.{f[2]:02X}" if len(f) >= 3 else "?"),
+ ("PLATFORM", 0x48, lambda f: f"0x{f[1]:02X}" if len(f) > 1 else "?"),
+ ("MCU", 0x49, lambda f: f"0x{f[1]:02X}" if len(f) > 1 else "?"),
+ ("BOARD", 0x47, lambda f: f"0x{f[1]:02X}" if len(f) > 1 else "?"),
+ ]
+ for name, cmd, fmt in checks:
+ f = received.get(cmd)
+ ok = f is not None
+ detail = fmt(f) if ok else "no response"
+ tests.append((name, ok, detail))
+
+ # Individual queries
+ queries = [
+ ("STAT_RX", 0x21),
+ ("STAT_TX", 0x22),
+ ("STAT_GPS", 0x2A),
+ ]
+ for name, cmd in queries:
+ ser.write(bytes([FEND, cmd, 0x00, FEND]))
+ f = wait_for_cmd(ser, cmd, timeout=2.0)
+ if f:
+ if cmd == 0x2A and len(f) >= 3:
+ detail = f"fix={f[1]}, sats={f[2]}"
+ elif cmd in (0x21, 0x22) and len(f) >= 5:
+ detail = f"count={int.from_bytes(f[1:5], 'big')}"
+ else:
+ detail = f"len={len(f)}"
+ tests.append((name, True, detail))
+ else:
+ tests.append((name, False, "no response"))
+
+ # Periodic stats check
+ ser.reset_input_buffer()
+ time.sleep(4)
+ resp = ser.read(8192)
+ stats = {}
+ if resp:
+ for f in parse_kiss_frames(resp):
+ stats[f[0]] = stats.get(f[0], 0) + 1
+ periodic_ok = len(stats) > 0
+ detail = ", ".join(f"0x{k:02X}({v})" for k, v in sorted(stats.items())) if stats else "none"
+ tests.append(("PERIODIC_STATS", periodic_ok, detail))
+
+ ser.close()
+
+ # Results
+ passed = sum(1 for _, ok, _ in tests if ok)
+ total = len(tests)
+ print(f"KISS smoke test on {port}: {passed}/{total} passed")
+ for name, ok, detail in tests:
+ print(f" [{'PASS' if ok else 'FAIL'}] {name}: {detail}")
+
+ sys.exit(0 if passed == total else 1)
+
+if __name__ == "__main__":
+ main()