Fix constrained firmware builds and CI checks

This commit is contained in:
mikecarper committed 2026-11-03 00:37:33 -08:00
1 parent dbaf951359
commit cf5dfeb87b
30 files changed
+839 -44

No files matched your search

+13 -2
View File
@@ -52,12 +52,12 @@ jobs:
--firmware-version v1.18.0.1-halo-keymind-cascade-dev
--radio-preset usa-cascade-fixed --profile cascade --require-ota
- name: Verify linked Companion crypto, retained Reader and ARM stack use
- name: Verify linked Companion crypto, Reader exclusion and ARM stack use
run: |
python3 -m pip install unicorn==2.1.4
python3 -B test/test_cc310_shared_scratch.py -v
python3 -B scripts/check_nrf52_companion_crypto.py .pio/build/RAK_4631_companion_radio_full/firmware.elf
python3 -B scripts/check_nrf52_companion_reader.py .pio/build/RAK_4631_companion_radio_full/firmware.elf
python3 -B scripts/check_nrf52_companion_reader.py --expect-disabled .pio/build/RAK_4631_companion_radio_full/firmware.elf
python3 -B scripts/check_nrf52_companion_ota_workspace.py .pio/build/RAK_4631_companion_radio_full/firmware.elf
- name: Qualify XIAO Full Companion with Meshtext and direct OTA
@@ -293,6 +293,14 @@ jobs:
run: pio run -e muzi_base_repeater -e muzi_base_room_server -e muzi_base_companion_radio_usb -e muzi_base_companion_radio_ble
- name: Qualify Muzi Full Companion
run: bash build.sh build-firmware muzi_base_companion_radio_full --firmware-version v1.18.0.2-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade
- name: Upload universal build failure logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: universal-lora-build-logs
path: out/build-logs/*.log
if-no-files-found: ignore
retention-days: 7
headless-kiss-smoke:
runs-on: ubuntu-latest
@@ -1168,6 +1176,8 @@ jobs:
- name: Verify WiFi, ESP-NOW and combined wireless controls
run: |
python3 -B test/test_wireless_control.py -v
python3 -B test/test_stm32_wireless_specialization.py -v
python3 -B test/test_companion_no_wireless.py -v
python3 -B test/test_serial_wifi_sessions.py -v
python3 -B test/test_companion_wifi_listener.py -v
python3 -B test/test_wifi_compatibility.py -v
@@ -1404,6 +1414,7 @@ jobs:
python3 -B test/test_webconfig_ui.py
python3 -B test/test_webconfig_regulators.py -v
python3 -B test/test_webconfig_gzip.py
python3 -B test/test_compact_elegant_ota.py -v
python3 -B test/test_webconfig_secret_masking.py
python3 -B test/test_webconfig_station_ownership.py -v
python3 -B test/test_mqtt_canonical_wifi.py -v
@@ -21,7 +21,14 @@
#include "ESPAsyncWebServer.h"
#include "FS.h"
#include "elegantWebpage.h"
#if defined(ESP32) && defined(MESH_COMPACT_ELEGANT_OTA) && MESH_COMPACT_ELEGANT_OTA
// Keep the full AsyncElegantOTA upload protocol; only its page is smaller.
#include <helpers/esp32/StaticHtml.h>
#define ELEGANT_HTML MESH_HTML_ELEGANT_OTA_COMPACT
#define ELEGANT_HTML_SIZE MESH_HTML_ELEGANT_OTA_COMPACT_LEN
#else
#include "elegantWebpage.h"
#endif
class AsyncElegantOtaClass{
+4 -2
View File
@@ -477,8 +477,10 @@ these are crypto function frames only, excluding callers, interrupts and other
tasks. The loop stack stays at 8 KiB. A full firmware RAM check still applies.
CI runs `scripts/check_nrf52_companion_crypto.py` on the actual Full RAK ELF.
It checks the 960-byte table and retained Reader corpus, executes two RFC 8032
vectors, and rejects crypto function stack use above 2 KiB. Host tests compare
It checks the 960-byte table, executes two RFC 8032 vectors, and rejects crypto
function stack use above 2 KiB. Reader is now disabled by default in Companion
builds; an explicit `COMPANION_FEATURE_READER=1` retains the implementation and
corpus. Host tests compare
1,791 scalars and 128 keypair/signature/shared-secret cases against the original
implementation, plus scalar boundary/random and sanitizer tests. Clean Full
builds also cover T1000-E and T096. These builds are synthetic coverage for
+3 -1
View File
@@ -256,7 +256,9 @@ for the state machine and limitations.
## Commands
ESP32 and nRF52 Full Companions include an offline World English Bible lookup:
Custom ESP32 and nRF52 Full Companion USB builds can opt into an offline World
English Bible lookup with `COMPANION_FEATURE_READER=1`. Standard builds leave
it disabled to preserve firmware space:
```text
get reader 3:16
+11
View File
@@ -3672,6 +3672,16 @@ static bool companionNativeUsbLightSleepReady() {
bool handleCompanionWirelessCommand(const char* command, char* reply, size_t size,
CompanionWirelessSource source) {
#if defined(STM32_PLATFORM) && !defined(BLE_PIN_CODE) && !defined(WIFI_SSID) \
&& !defined(WITH_BRIDGE) && !defined(WITH_MQTT_BRIDGE) \
&& !defined(WITH_UDP_BRIDGE) && !defined(WITH_ESPNOW_BRIDGE) \
&& !defined(WITH_WEBCONFIG) && !defined(ETHERNET_ENABLED) \
&& !MESH_ENABLE_EMERGENCY_POWER \
&& (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW)
// This build has no wireless transport whose state can change.
(void)source;
return mesh::wireless::handleNoServices(command, reply, size);
#else
#if defined(BLE_PIN_CODE)
const auto wireless_command = mesh::wireless::parse(command);
if (reply && size && companion_bluetooth_off_at != 0
@@ -3700,6 +3710,7 @@ bool handleCompanionWirelessCommand(const char* command, char* reply, size_t siz
}
}
return mesh::wireless::control().handle(command, reply, size, millis(), requester);
#endif
}
#if COMPANION_RTC_TERMINAL_INPUT
+2 -7
View File
@@ -1739,12 +1739,6 @@ public:
movePage(-1);
return true;
}
#if COMPANION_FEATURE_READER
if (key == KEY_ENTER && isRadioPage()) {
_task->showReader();
return true;
}
#endif
if (key == KEY_NEXT || key == KEY_RIGHT) {
movePage(1);
if (_page == HomePage::RECENT) {
@@ -3157,7 +3151,8 @@ char UITask::handleLongPress(char c) {
// The radio page's reader gesture, including exit, takes precedence over
// the global early-boot rescue gesture just like the WiFi setup page.
if (_display != NULL && isReaderActive()) { closeReader(); return 0; }
if (_display != NULL && curr == home && static_cast<HomeScreen*>(home)->isRadioPage()) {
if (c == KEY_ENTER && _display != NULL && curr == home
&& static_cast<HomeScreen*>(home)->isRadioPage()) {
showReader();
return 0;
}
+1 -3
View File
@@ -43,8 +43,6 @@ def check(path):
if elf.symbols.get("base", (0, 0))[1] != 960:
raise ValueError("Companion must retain the 960-byte Ed25519 base table")
reader = "_ZN4mesh5bible9generatedL10readerDataE"
if reader not in elf.symbols:
raise ValueError("Companion Reader corpus is missing")
path_cache = elf.symbols.get("_ZZN4mesh18contactPathStorageEvE4pool")
if path_cache and elf.sections[path_cache[2]][1] != 8: # SHT_NOBITS
raise ValueError("Contact path cache has a flash load image; expected zero-initialized storage")
@@ -102,7 +100,7 @@ def check(path):
return {
"passed": True, "elf_sha256": hashlib.sha256(elf.data).hexdigest(),
"base_table_bytes": elf.symbols["base"][1],
"reader_data_bytes": elf.symbols[reader][1], "vectors": rows,
"reader_data_bytes": elf.symbols.get(reader, (0, 0))[1], "vectors": rows,
"zero_initialized_path_cache_bytes": path_cache[1] if path_cache else 0,
"stack_limit_bytes": 2048,
"scope": "Crypto function frames only; excludes caller/ISR/task stacks and physical timing.",
+16 -1
View File
@@ -23,6 +23,19 @@ ROOT = Path(__file__).resolve().parents[1]
TYPOGRAPHY = str.maketrans({'\u00a0':' ', '\u2013':'-', '\u2014':'-', '\u2018':"'", '\u2019':"'", '\u201c':'"', '\u201d':'"'})
def check_disabled(path):
elf = FirmwareElf(path)
if elf.unpack('H', 18)[0] != 40:
raise ValueError('Expected ARM ELF')
retained = [name for name in elf.symbols
if 'readerData' in name or 'replyVerse' in name
or 'handleReaderCommand' in name]
if retained:
raise ValueError('Reader code or corpus unexpectedly linked: ' + repr(retained))
return {'passed': True, 'reader': 'disabled',
'elf_sha256': hashlib.sha256(elf.data).hexdigest()}
def check(path, every_page=False):
elf = FirmwareElf(path)
if elf.unpack('H', 18)[0] != 40:
@@ -132,8 +145,10 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('elf')
parser.add_argument('--every-page', action='store_true')
parser.add_argument('--expect-disabled', action='store_true')
args = parser.parse_args()
try:
print(json.dumps(check(args.elf, args.every_page), indent=2))
print(json.dumps(check_disabled(args.elf) if args.expect_disabled
else check(args.elf, args.every_page), indent=2))
except (ValueError, OSError) as error:
parser.exit(1, f'FAILED: {error}\n')
+10 -1
View File
@@ -118,7 +118,16 @@ def main():
"#pragma once", "#include <stdint.h>", "#include <pgmspace.h>"]
for page in sorted(Path("webui/pages").glob("*.html")):
symbol = "MESH_HTML_" + page.stem.upper()
compressed = compress_page(page.read_bytes())
page_bytes = page.read_bytes()
if page.name == "elegant_ota_compact.html":
# Inline the pinned, locally vendored MD5 implementation: the OTA
# page must work without internet and the server verifies its hash.
marker = b"/* MESH_SPARK_MD5 */"
if page_bytes.count(marker) != 1:
raise ValueError("compact OTA page must contain one MD5 marker")
vendor = Path("webui/vendor/spark-md5-3.0.2.min.js").read_bytes()
page_bytes = page_bytes.replace(marker, vendor)
compressed = compress_page(page_bytes)
static_lines.append("const uint32_t %s_LEN = %d;" % (symbol, len(compressed)))
append_byte_array(static_lines, symbol, compressed)
static_header = Path("src/helpers/esp32/StaticHtml.h")
+12
View File
@@ -3750,8 +3750,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
#if MESH_ENABLE_TX_BEACONS
if (handleBeaconCommand(command, reply, 157)) return;
#endif
#if defined(STM32_PLATFORM) && defined(STM32WLxx) && defined(USE_STM32WL) \
&& !defined(WITH_WEBCONFIG) && !defined(WITH_MQTT_BRIDGE) \
&& !defined(WITH_UDP_BRIDGE) && !defined(WITH_ESPNOW_BRIDGE) \
&& !defined(WITH_BRIDGE) && !defined(ETHERNET_ENABLED) \
&& !defined(BLE_PIN_CODE) && !defined(WIFI_SSID) \
&& !MESH_ENABLE_EMERGENCY_POWER \
&& (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW)
// The integrated STM32WL target has no 2.4 GHz services. Keep the same
// unsupported-command replies without retaining the state machine.
if (mesh::wireless::handleNoServices(command, reply, 160)) return;
#else
if (mesh::wireless::control().handle(command, reply, 160, millis(),
_callbacks->wirelessCommandSource(sender_timestamp))) return;
#endif
// Infrastructure callers can already have reflected a three-byte CLI
// request prefix into the same 160-byte reply. Reserve it for bulk pages.
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
+1 -6
View File
@@ -1,12 +1,7 @@
#pragma once
#ifndef COMPANION_FEATURE_READER
#if defined(COMPANION_RADIO_FULL) && defined(ENABLE_USB_INTERFACE) \
&& (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM))
#define COMPANION_FEATURE_READER 1
#else
#define COMPANION_FEATURE_READER 0
#endif
#define COMPANION_FEATURE_READER 0
#endif
#ifndef COMPANION_READER_COMPACT_STREAM
+18
View File
@@ -47,6 +47,24 @@ inline Command parse(const char* s) {
return {scope, Action::Invalid};
}
// Match Control::handle when the build has no wireless services, without
// retaining the state-transition machinery on flash-constrained devices.
inline bool handleNoServices(const char* text, char* reply, size_t size) {
const auto command = parse(text);
if (command.action == Action::None || !reply || !size) return false;
if (command.action == Action::Invalid) {
snprintf(reply, size, MESH_CLI_REPLY_TEXT("Error: use set wifi/espnow on|off [force]; set 2.4ghz on [all|force]|off [force]", "Error: set wifi/espnow on|off [force]; set 2.4ghz on [all|force]|off [force]"));
} else if (command.action == Action::Get) {
if (command.scope == All)
snprintf(reply, size, "wifi=unavailable, bluetooth=unavailable, espnow=unavailable");
else
snprintf(reply, size, "%s unavailable", command.scope == WiFi ? "wifi" : "espnow");
} else {
snprintf(reply, size, MESH_CLI_REPLY_TEXT("Error: requested wireless service unavailable in this build/boot", "Error: wireless service unavailable this build/boot"));
}
return true;
}
class Backend {
public:
virtual uint8_t available() const = 0; // Services usable in this boot.
+267
View File
@@ -3,6 +3,273 @@
#pragma once
#include <stdint.h>
#include <pgmspace.h>
const uint32_t MESH_HTML_ELEGANT_OTA_COMPACT_LEN = 4211;
const uint8_t MESH_HTML_ELEGANT_OTA_COMPACT[] PROGMEM = {
0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0xff,0x94,0x52,0x55,0xa2,0xac,0x36,
0x18,0x7e,0x9f,0x55,0xa4,0xa9,0x0d,0xb7,0xc8,0xe8,0x91,0x19,0xa0,0xee,0xee,0x2e,
0x21,0xf9,0x19,0xd2,0x03,0x84,0x9b,0x84,0x91,0x4b,0x8f,0x6c,0xa6,0xb6,0x8e,0xee,
0xa4,0x2b,0xe9,0x8f,0x5d,0x7b,0xeb,0xc3,0x97,0xfc,0xee,0xe1,0x33,0x42,0x71,0x7b,
0xaa,0x80,0x64,0xb6,0xc8,0xe3,0x49,0xd8,0x7d,0x61,0x06,0x4c,0xc4,0x61,0x01,0x96,
0x11,0x9e,0x31,0x6d,0xc0,0x46,0xb4,0xb6,0xa9,0x77,0x41,0x07,0x69,0xc9,0x0a,0x88,
0xe8,0x5e,0xc2,0xa1,0x52,0xda,0x52,0xc2,0x55,0x69,0xa1,0x44,0xab,0x83,0x14,0x36,
0x8b,0x04,0xec,0x25,0x07,0xaf,0x63,0x5c,0x59,0x4a,0x2b,0x59,0xee,0x19,0xce,0x72,
0x88,0xe6,0x14,0xb3,0x58,0x69,0x73,0x88,0x3f,0x04,0x93,0xbd,0xae,0x34,0x90,0x8f,
0xbf,0x78,0x35,0x0c,0x7a,0x59,0x68,0xec,0x09,0xbf,0x49,0xa2,0xc4,0xa9,0x49,0x31,
0xea,0x66,0x7e,0x56,0x1d,0x89,0x61,0xa5,0xf1,0x0c,0x68,0x99,0x6e,0x0b,0x76,0xec,
0x03,0x6f,0x96,0x17,0x1a,0x0a,0xe4,0xf5,0x4e,0x96,0x9b,0x05,0xd2,0x84,0xd5,0x56,
0x6d,0x2b,0x26,0x84,0x2c,0x77,0x9b,0x19,0x99,0xb7,0xfa,0x5c,0x96,0xe0,0x65,0x20,
0x77,0x19,0xc6,0xf2,0x57,0xeb,0xeb,0x49,0x2a,0x21,0x17,0xd8,0x53,0x33,0xb8,0xb6,
0x66,0x64,0x76,0x9d,0xd4,0xd6,0xaa,0x12,0xcb,0xad,0x6a,0xdb,0xa7,0x96,0x65,0x86,
0x29,0xed,0xa0,0x69,0xc6,0xc0,0xfe,0x1a,0x1d,0xba,0xe0,0xd7,0x95,0x56,0x3b,0x0d,
0xc6,0x34,0x7d,0x45,0xf3,0xd9,0xec,0xf9,0xeb,0xc9,0xb3,0xc6,0x32,0x5b,0xa3,0x2c,
0x93,0x16,0x3c,0x53,0x31,0x0e,0x9b,0x4a,0xe3,0x38,0x34,0xab,0xb6,0x6a,0x0f,0x3a,
0xcd,0xd5,0xa1,0xe3,0x36,0xac,0x3c,0x1d,0x30,0x07,0x5c,0x4f,0xc2,0xa0,0xef,0x3c,
0x0c,0xfa,0xe1,0xb7,0x03,0x68,0xf7,0x31,0x7f,0x6a,0x4c,0x28,0x08,0x2b,0x22,0x45,
0x44,0xa5,0xc0,0x89,0x4b,0x7b,0xa2,0xf1,0x67,0xe8,0x81,0x85,0x91,0x7e,0xec,0x64,
0x54,0xfc,0x7b,0xf7,0x47,0x18,0x54,0x18,0x64,0x6c,0x38,0x0e,0x73,0xd8,0x41,0x29,
0xe2,0x2f,0x2b,0xc1,0x2c,0x90,0x76,0xf1,0x61,0x30,0xc8,0x26,0x61,0xce,0x12,0xc0,
0xe5,0x77,0x03,0xe8,0x74,0x11,0xd5,0x18,0x58,0xd1,0x61,0xdd,0xad,0x88,0x92,0x3d,
0xcb,0x6b,0x64,0x52,0xa9,0x8b,0x03,0xd3,0x28,0xe0,0x19,0xf0,0x2b,0x10,0x31,0x79,
0x6b,0x10,0x61,0xc8,0x36,0xd2,0xff,0x8f,0x98,0x83,0x39,0x19,0x0b,0x05,0x6d,0x63,
0x8d,0xcc,0x18,0x2d,0x0c,0xc6,0x36,0xc6,0xc0,0x24,0x55,0xba,0x77,0xa3,0xf1,0xbb,
0x05,0xdb,0x01,0x99,0xfa,0x89,0x2c,0x89,0xd2,0xa4,0xfd,0xfd,0xdd,0x03,0xe7,0xa1,
0x73,0xa2,0xc7,0x32,0xa4,0x18,0x7c,0xba,0x82,0x46,0x9a,0x71,0x0e,0x15,0xde,0x6f,
0xeb,0xe8,0x0e,0xde,0xb4,0x75,0xc3,0x64,0xfd,0xfa,0x07,0xf3,0x9e,0xa1,0x5d,0x98,
0xba,0xca,0x15,0x13,0x14,0xc7,0xd9,0xfe,0x61,0xd0,0xeb,0xd0,0x63,0x3c,0x8b,0xce,
0x6a,0x64,0x28,0xc1,0xd3,0x8d,0x28,0xde,0xc8,0xc3,0x96,0x91,0xca,0xa4,0xc0,0x75,
0x61,0x77,0xa3,0xd9,0xb8,0xde,0xfe,0x88,0x28,0xd1,0x2a,0x87,0x87,0x5c,0xdc,0x2f,
0xd4,0x70,0x2d,0x2b,0x1b,0x07,0xf7,0x08,0x5e,0x97,0xbe,0xf2,0x0a,0xb1,0x26,0x4b,
0x7f,0xe6,0x2f,0x5c,0xf2,0x6a,0x29,0xf4,0x3f,0x7f,0x93,0xd7,0x75,0xfd,0xc0,0x25,
0xd3,0x0f,0xdf,0xfd,0x82,0x7c,0xfc,0x19,0xf9,0xfa,0x8b,0xb7,0x3e,0xf9,0xc0,0xd9,
0x12,0x03,0x40,0x0e,0x90,0xd4,0x32,0xd8,0xe3,0xca,0x95,0xf6,0xc9,0xbd,0x60,0x32,
0x4d,0xeb,0x92,0x5b,0xa9,0xca,0x69,0xca,0xb8,0x55,0xfa,0xe4,0x34,0x32,0x9d,0xb6,
0xdd,0xaa,0x94,0xc0,0xb1,0x52,0xda,0x9a,0x28,0x8a,0xa8,0x4a,0x7e,0x05,0x6e,0xa9,
0xd3,0x14,0x4a,0xd4,0x39,0xf8,0xa3,0x6a,0xf0,0x9a,0x3a,0xd7,0x90,0x1b,0x20,0x8f,
0x7c,0x05,0xa4,0xb2,0x84,0xd6,0x75,0xcc,0x40,0x5f,0x78,0xa1,0x17,0xfa,0xac,0x10,
0x4e,0xd3,0xd3,0x63,0xda,0xde,0xbf,0xd9,0x33,0x4d,0x76,0xb9,0x4a,0xb6,0x56,0x9f,
0x9a,0x96,0x88,0x0e,0x12,0x6b,0x3d,0x5c,0x73,0x66,0x79,0x36,0x05,0xa7,0x17,0x1a,
0xc8,0xd3,0xeb,0x96,0xf2,0x3f,0x6f,0x47,0xf0,0xe1,0x1b,0xeb,0xc7,0x0a,0xb9,0x76,
0x1e,0x35,0x55,0x97,0x7d,0x1a,0xcc,0x47,0x6b,0xac,0xcf,0x58,0x2d,0xb1,0x8d,0x6d,
0x9b,0x87,0x09,0xb1,0x5c,0x44,0x0f,0x4d,0x99,0x9b,0x38,0x8d,0x06,0x5b,0xeb,0x92,
0xb0,0x97,0x92,0x17,0x56,0x8b,0xcb,0xd5,0xe5,0xd9,0xf9,0xe2,0x72,0x7d,0xed,0x66,
0x70,0xfc,0x99,0x67,0x3a,0xfa,0x1e,0x97,0xe6,0xd2,0x39,0x62,0x81,0x58,0x22,0x56,
0x88,0x35,0xe2,0x0c,0x71,0x8e,0xb8,0x40,0x5c,0x22,0x18,0x22,0x41,0x70,0x84,0x40,
0x00,0x22,0xa5,0x3f,0x6e,0xc7,0x74,0x84,0x17,0xe5,0xf4,0xbe,0x8b,0x49,0xdd,0xa3,
0x6b,0x5c,0xeb,0x34,0x2c,0xea,0x0a,0x9a,0x0e,0xaf,0x7b,0xdf,0x71,0x7b,0xf2,0x88,
0x5a,0x67,0x3b,0x54,0x36,0x68,0xc3,0xd0,0xfc,0xc6,0xe2,0x38,0x5e,0x2e,0x3c,0x83,
0x75,0x5f,0x3f,0x0c,0x8b,0xc7,0xc0,0x4f,0x3c,0x07,0xf4,0xba,0x72,0xba,0x71,0xb2,
0xe8,0xf8,0xfd,0xec,0x47,0x37,0xc1,0x6f,0xfe,0xa3,0xcb,0xf1,0x5b,0xfc,0xc7,0xe1,
0x54,0x18,0x47,0x10,0xc3,0xc0,0x6e,0xf4,0x7f,0x6c,0x86,0x83,0x4e,0x42,0x86,0xc1,
0xb4,0xa0,0x71,0xed,0x59,0x65,0xc0,0xb0,0xfc,0xb9,0x76,0x5c,0xf6,0xf3,0x2a,0xcb,
0xf3,0xae,0xd4,0x78,0x54,0xea,0xd3,0xf2,0x0b,0xdd,0x16,0x92,0x4a,0x31,0x64,0x1b,
0x58,0x5d,0xe5,0x91,0xa6,0xf8,0xdf,0x64,0xfc,0xb4,0x54,0x60,0x7d,0x01,0x48,0x95,
0x47,0xa1,0x06,0x0b,0x32,0x37,0x9b,0xb2,0x0f,0xce,0xa7,0x20,0xf4,0xf3,0xee,0xf7,
0xad,0x0d,0x77,0xf1,0xa8,0x69,0x29,0x00,0xdb,0x02,0x94,0x0a,0x8f,0x4e,0x15,0x1e,
0x0c,0x58,0x82,0x0a,0x5a,0xf9,0xa4,0xb3,0xd0,0xcf,0xbb,0xc1,0x13,0xb9,0xc1,0xa3,
0xd1,0xd3,0x01,0xd6,0x05,0x28,0x75,0x1e,0x8d,0x8a,0x78,0xec,0xe7,0xa6,0x95,0x73,
0xde,0x78,0x6b,0x95,0xf0,0x58,0x7e,0xdf,0xc6,0x70,0x15,0x13,0x8a,0x9a,0x0c,0xfe,
0x63,0xa4,0x4e,0x4c,0x9b,0x59,0x63,0x28,0x0c,0x77,0x13,0x02,0xc3,0x07,0x3a,0xda,
0x55,0x8c,0x06,0xbc,0x94,0x61,0x52,0xfb,0x35,0x9e,0xc4,0x36,0x97,0xfc,0x41,0x25,
0x3c,0xe2,0xbc,0xfa,0x9f,0x47,0x7b,0x21,0x5c,0x91,0x59,0x31,0xf4,0x58,0x6f,0x60,
0x22,0x4a,0x52,0x1e,0x83,0xbc,0x17,0x34,0x84,0x05,0xa2,0x98,0x8a,0xa2,0x97,0x5a,
0x90,0x55,0xca,0xd4,0x93,0xbd,0x21,0x82,0x48,0x4c,0xe1,0x43,0x50,0xf5,0x42,0x59,
0x2a,0x54,0x11,0x53,0x10,0xa8,0x97,0x32,0xb9,0x4c,0x31,0x40,0x2f,0x54,0x91,0x92,
0xc2,0x79,0xea,0x01,0xdf,0x41,0x49,0xea,0x24,0x9e,0x3c,0x04,0x41,0x7a,0x29,0x89,
0x02,0x84,0xb1,0x47,0x7b,0xc1,0x88,0x89,0x98,0x8b,0xc6,0xa8,0xc7,0x18,0xc4,0x4d,
0x4c,0xb8,0xfe,0x46,0x5d,0x6f,0x97,0x8f,0xaf,0x03,0xd5,0x0b,0x6e,0x51,0x6e,0xa0,
0x27,0xc9,0x0e,0x52,0xbc,0x93,0x2e,0xb7,0xf3,0xc7,0xd7,0xe5,0x67,0x3f,0xe4,0x65,
0x04,0x17,0x7e,0x9a,0xea,0x20,0xc9,0x3b,0xe9,0x7c,0x3b,0x7d,0x7c,0x1d,0x24,0xf4,
0xe6,0x2a,0x81,0x08,0xc8,0x4b,0xa4,0x87,0x28,0xdf,0x45,0xa7,0xdb,0xf5,0xe3,0xeb,
0x21,0xa2,0x5e,0x12,0x92,0x15,0x42,0xfc,0xf2,0xd0,0xe1,0xe1,0xdf,0x3d,0xd6,0x2b,
0x08,0x66,0xe9,0x85,0xa1,0x07,0xd4,0x9b,0x24,0xc1,0x29,0x65,0xca,0xb1,0x5e,0xee,
0xa4,0x91,0x22,0x36,0xe5,0x68,0x2f,0x25,0x33,0x89,0xd4,0x9c,0x72,0xaa,0x37,0xf3,
0x54,0x75,0x95,0x9c,0x72,0xb4,0x17,0x08,0x95,0x24,0x5e,0x34,0x04,0x49,0x2f,0x64,
0x88,0x4b,0x39,0x66,0x9e,0xa3,0x6f,0xb8,0x98,0xc0,0x08,0x53,0x10,0xa4,0x17,0x54,
0xd5,0x13,0xea,0x31,0x24,0x71,0x2f,0x83,0x92,0x44,0xea,0xd0,0x13,0xbd,0x21,0xc4,
0x84,0x53,0x43,0xa6,0x20,0x70,0x2f,0x14,0xbb,0x53,0x84,0xe8,0x9f,0xa2,0xfd,0xb2,
0xff,0xcc,0x4d,0x22,0xcd,0xf2,0x29,0xd1,0x43,0x92,0x6f,0x92,0xfd,0xbc,0x3f,0x18,
0xd9,0x8b,0x89,0xd9,0x42,0xd5,0xe5,0x09,0x01,0x0e,0x09,0xde,0x24,0xfb,0x69,0x7f,
0x66,0x83,0x94,0x22,0x21,0x73,0x7e,0x39,0xfc,0x70,0xf8,0x9b,0x63,0xbf,0xee,0x07,
0x42,0x7b,0x89,0x09,0x95,0x99,0xbf,0x08,0xf2,0x20,0xd4,0x2f,0x02,0xf4,0x82,0x09,
0x55,0x31,0x39,0x4d,0x14,0xda,0x1b,0x38,0x38,0x4b,0xc4,0x66,0x8a,0xe8,0x05,0x33,
0xad,0x70,0x19,0x1a,0xa8,0x17,0xa8,0x34,0x84,0x5c,0x69,0xa4,0x90,0xde,0x3c,0xc1,
0x51,0x08,0x9d,0x28,0xa8,0x97,0x58,0x9a,0x04,0x33,0x8f,0x10,0xd2,0x2b,0x98,0x8d,
0x51,0x51,0x23,0x84,0xf7,0x16,0x4e,0x5c,0xc8,0x9a,0x08,0xaa,0x97,0x2b,0x89,0xab,
0x66,0x4c,0x00,0xe0,0x5e,0xca,0x48,0x58,0x8a,0x8d,0x04,0xb0,0xde,0x4c,0x28,0x94,
0x8d,0x69,0x44,0xe0,0x5e,0x55,0x26,0x92,0x6e,0xf8,0xc3,0x70,0xd9,0x3f,0xcf,0xb7,
0x7b,0xde,0xdf,0x77,0x45,0xa5,0x0b,0xa5,0xea,0x93,0xe1,0x07,0xc3,0x5f,0x8c,0xf3,
0xfe,0x79,0xba,0xdd,0xe3,0xfe,0x69,0x15,0xec,0x59,0x50,0xbc,0x49,0xe8,0x90,0xf0,
0x4b,0x72,0xda,0x3f,0xaf,0xb7,0x7b,0xdf,0xcf,0x2f,0xa7,0x70,0x31,0x2d,0xb2,0x97,
0xc6,0x0e,0x4d,0xbc,0x34,0xd7,0xfd,0xf3,0x72,0xbb,0x07,0xfe,0x9d,0xab,0x85,0x8a,
0x92,0xd9,0xcb,0x83,0x87,0x07,0xf8,0x1d,0x04,0xee,0x0d,0x41,0xa4,0x69,0x16,0x18,
0x92,0xa4,0x17,0xb2,0xb4,0xd2,0x9d,0x7c,0x4a,0x3a,0x56,0x6f,0x30,0x96,0xa9,0x07,
0xbd,0x98,0x4c,0x8b,0x39,0xaa,0xa6,0xa2,0xbc,0x83,0x32,0x44,0x20,0x62,0x35,0x04,
0xc1,0x7a,0x09,0x39,0x10,0xaa,0x53,0x8f,0xf7,0x82,0x39,0xa1,0x52,0x92,0xc6,0x22,
0xe9,0x0d,0x42,0x05,0x83,0xeb,0x98,0xf4,0x18,0x83,0x19,0x0b,0x05,0x4d,0x45,0xe8,
0x05,0x30,0x31,0x48,0xa2,0xa6,0x26,0xee,0x2d,0x90,0x91,0xc1,0x56,0x53,0x52,0xf5,
0x12,0x15,0x4d,0x33,0xc3,0xbf,0x40,0xff,0xb1,0x63,0x1d,0xbc,0x8d,0x83,0x50,0xf8,
0xaf,0x5c,0x37,0x14,0xea,0x7a,0x65,0x9d,0x4d,0xa7,0xc6,0x4d,0x8d,0x1b,0x5a,0x91,
0x75,0xca,0x6c,0xc8,0x39,0xb8,0x8a,0x5d,0x35,0x55,0xea,0xff,0x7e,0xef,0x61,0xec,
0xac,0xdb,0x7b,0x75,0x04,0xf8,0x00,0xbd,0xef,0xc1,0x07,0x0a,0x1f,0xbe,0x7a,0x44,
0x87,0x61,0xa1,0x5b,0x4e,0x24,0xba,0x0c,0x0b,0xdd,0x72,0x23,0xd1,0x63,0x58,0xe8,
0x96,0x17,0x89,0x3e,0x83,0x02,0x5a,0x2b,0x4f,0xa9,0x6e,0xfc,0x96,0xa4,0xfa,0x19,
0x65,0x5a,0xa9,0x68,0x47,0x5c,0x06,0xe0,0x3d,0x10,0x29,0xec,0x40,0x86,0x75,0x3f,
0x90,0x4c,0xf8,0x74,0x6e,0xfa,0xdb,0x12,0x32,0x8d,0x44,0x6a,0xa1,0x75,0x76,0x99,
0xf4,0x07,0xe7,0x19,0x91,0x94,0x91,0x55,0x80,0x39,0x34,0x0c,0x9b,0x9b,0xb0,0x4b,
0xf1,0xf6,0xd8,0xc4,0x3d,0xc0,0x5d,0x9f,0xe6,0xe6,0x11,0x68,0x62,0xad,0x53,0x7d,
0xd3,0x99,0x4e,0x3b,0x77,0xa4,0xf3,0x35,0x84,0x3b,0x6d,0x19,0x31,0x02,0x9f,0xcc,
0x89,0x34,0x35,0x5d,0x77,0x23,0xc3,0x07,0x1b,0x5e,0xf4,0x49,0x12,0x4e,0xb9,0x5a,
0x4a,0xa4,0x56,0x3c,0x50,0x57,0xe0,0x00,0xa2,0x79,0x31,0x10,0x6d,0xa7,0xe1,0xb9,
0xf8,0x0a,0x69,0x79,0xfc,0xc8,0x6d,0x40,0xcb,0x6b,0x36,0x5a,0xfc,0xa8,0x82,0x7d,
0x5e,0xa2,0x4d,0x20,0xcc,0xcd,0xe4,0xac,0x23,0x63,0x9e,0x4d,0xae,0x79,0x9c,0xf0,
0x51,0x99,0x06,0xa6,0x10,0x0a,0x85,0x79,0xd4,0x75,0x22,0xe6,0xc9,0xab,0x23,0xf1,
0x72,0xdb,0xac,0xf4,0xa6,0x8b,0xcf,0x7d,0x75,0x45,0xe4,0x51,0xdd,0xe7,0x92,0x52,
0x9a,0xa7,0x62,0x1d,0xa7,0x41,0x11,0xaa,0x22,0x1c,0x60,0x4c,0xd1,0xb6,0xf9,0x47,
0x7f,0xa3,0xa5,0x25,0x35,0xf3,0x80,0x8e,0x43,0xe7,0x38,0xbb,0x58,0xd3,0xfb,0x75,
0x15,0x84,0x21,0x91,0x7b,0x7e,0x18,0x7a,0x34,0x5f,0x1e,0xe5,0xb8,0xcd,0x45,0x4f,
0x20,0x87,0x44,0x9e,0xd4,0x6a,0x1b,0x69,0xe1,0x0c,0xba,0x14,0xd4,0xa9,0xaf,0x04,
0x8c,0x84,0x9d,0xe7,0xb0,0x50,0x42,0x1d,0x36,0x03,0x2c,0xe1,0xdf,0xca,0x92,0x97,
0x45,0xa2,0xb0,0x87,0xd6,0x44,0x3b,0x28,0xc7,0xc4,0x3a,0x3c,0xa5,0xc4,0x9a,0xdb,
0xbc,0x99,0xd3,0xdd,0x63,0x48,0x3f,0x11,0xd7,0xe8,0xf2,0x3e,0x56,0x19,0x81,0x49,
0xe8,0x0e,0xc0,0xf0,0x60,0x24,0x57,0x61,0x47,0xc3,0xf7,0x70,0x5c,0x74,0x40,0xc7,
0x8f,0x44,0x9c,0x98,0x7a,0x2d,0x12,0xb0,0x39,0xef,0x25,0x6c,0xc4,0xa2,0xa1,0x55,
0xa9,0xac,0xaa,0x55,0x89,0xce,0xaf,0x15,0x4c,0x49,0x07,0xe5,0xa1,0xab,0x4b,0xaa,
0xe9,0x08,0xac,0x87,0xea,0x74,0xad,0x97,0x3e,0x54,0x83,0xdb,0x07,0xaf,0xa5,0xca,
0x9a,0xe7,0x1a,0xb4,0x2b,0x35,0x75,0xbe,0xbf,0x9a,0xf0,0x88,0xfe,0x97,0x50,0x29,
0xa1,0x29,0x78,0x72,0x44,0x15,0xe2,0x49,0xc5,0xf6,0x36,0x1f,0xeb,0xcc,0xc6,0x90,
0xd9,0x38,0xf4,0x83,0xb1,0x4e,0x2c,0x65,0xc2,0x58,0x77,0x6d,0x75,0x72,0x32,0x3e,
0x6c,0x32,0x7f,0x1f,0x42,0xb1,0x55,0x10,0xa1,0xbc,0x8c,0xb2,0x88,0x80,0x01,0x66,
0x45,0x80,0xe5,0x0b,0x74,0x66,0xad,0xec,0xd0,0x0c,0x17,0x4e,0x93,0xc1,0x5a,0x75,
0x3b,0xce,0xac,0x71,0x22,0x15,0xd9,0xde,0xa6,0x39,0x6c,0x07,0x76,0xeb,0xeb,0x71,
0x7b,0x34,0x88,0xe3,0x64,0x9b,0xd2,0x2d,0x21,0xb6,0x6b,0x7d,0xdf,0xf1,0x6d,0xb7,
0xd3,0xed,0xf9,0x5d,0xb7,0xd3,0xa8,0x77,0x5b,0x0d,0xa7,0xd5,0x6f,0xa1,0xaf,0xd1,
0xe8,0xd5,0x5a,0x2e,0xb8,0xad,0x6b,0x1e,0xe5,0x8c,0xdf,0x15,0x84,0xe2,0xf4,0x56,
0x90,0xd9,0x7e,0xbd,0x06,0xd6,0x04,0x65,0xe4,0xce,0xd4,0xf8,0x44,0xe3,0xf8,0xdd,
0x13,0x51,0x53,0xc2,0x60,0xac,0x55,0x2b,0x09,0x83,0xf4,0x97,0x54,0xc0,0x8b,0x79,
0x79,0xbe,0xf0,0x6d,0xb5,0x90,0x2f,0x6e,0x86,0xc3,0xc1,0x14,0x39,0x56,0x06,0x2a,
0xb8,0xb7,0x5b,0x4b,0x7d,0x16,0x38,0xd6,0x59,0x82,0x73,0xac,0x34,0x96,0x3d,0x30,
0x66,0x49,0xc5,0x92,0xce,0x17,0x2e,0x67,0xdc,0x99,0x5c,0x13,0x30,0xbc,0xcd,0xf9,
0x44,0xf6,0xb1,0x80,0xff,0x7b,0x1b,0x75,0x20,0x87,0xd8,0x17,0xda,0x95,0xf1,0xfa,
0xbc,0x93,0x8d,0x40,0x66,0x33,0x84,0x99,0x39,0xd2,0xf6,0xbb,0xe2,0x8d,0x6b,0xc1,
0x51,0x1b,0xf8,0x7e,0x5f,0xe1,0x54,0xac,0x98,0x65,0x2f,0x3d,0x31,0xe1,0x7a,0xef,
0xbd,0x37,0x16,0xe4,0x98,0x1c,0x06,0x07,0xc4,0x76,0x7f,0x57,0xde,0xf3,0x65,0x99,
0xd1,0x48,0x48,0x68,0x61,0x5b,0xda,0x55,0x5b,0x9a,0x5e,0x85,0x98,0x65,0xab,0xce,
0x6e,0x56,0xd8,0x04,0x57,0x87,0x08,0x65,0x0c,0x37,0xab,0xca,0x85,0x2f,0x4a,0x65,
0x3c,0x4a,0x51,0x31,0xcf,0xea,0x60,0x63,0x47,0xf0,0x3b,0xc4,0x67,0x83,0xc3,0xfb,
0xa0,0x88,0x24,0x46,0x78,0xcd,0xc1,0xe7,0x45,0x1a,0x29,0x59,0x8a,0x06,0xc8,0xc0,
0x8b,0x23,0x2e,0xd4,0x7f,0x92,0xbb,0x5f,0x97,0x4d,0x95,0x70,0xf9,0x19,0x75,0x12,
0x25,0x98,0xce,0xba,0x7a,0x46,0x02,0x44,0x57,0x94,0xad,0xdc,0x00,0x27,0x39,0x9e,
0x83,0xdd,0x56,0x7f,0xac,0x27,0x96,0x42,0x63,0xd0,0x1d,0x58,0x46,0x00,0xb2,0x26,
0x81,0x43,0xe2,0x1a,0xb9,0x70,0x00,0xc4,0x42,0x4b,0xa2,0xc8,0x29,0x4c,0xb4,0xc5,
0x42,0x12,0xb4,0x1f,0x04,0xcc,0x41,0x36,0xf0,0x5d,0x6a,0x41,0xcd,0xc5,0xd8,0x42,
0x6d,0x23,0x89,0x60,0xf0,0xc6,0x9d,0x99,0xbc,0x11,0xe5,0x53,0x31,0xfb,0x09,0x52,
0xb8,0x92,0xef,0x12,0xd6,0x5f,0xbf,0x69,0xf0,0x3a,0x75,0xed,0x4d,0x73,0x0d,0x7e,
0xbd,0x5d,0x0f,0x04,0xaf,0x85,0xdc,0x86,0x04,0x15,0x15,0x18,0x8a,0xd7,0x49,0xbc,
0xe4,0x63,0x5e,0x24,0xd0,0x2a,0x9f,0x3e,0xba,0x79,0xb9,0x5c,0x2c,0xcb,0x82,0x17,
0x0a,0x4a,0xbb,0x1f,0xbe,0x18,0x2e,0x0d,0xf0,0x80,0xd2,0xf4,0x8d,0x6d,0x05,0x00,
0xf0,0x09,0xf6,0xe9,0x4d,0xad,0x57,0xc7,0xe5,0x00,0xa0,0xbb,0xcc,0x06,0xa0,0x1c,
0x30,0xa3,0xf6,0x38,0x94,0x78,0xd7,0x3c,0x08,0xcf,0xfc,0x6c,0xa8,0x50,0x03,0x06,
0xa4,0x3f,0x32,0xe8,0xf6,0x7c,0x2d,0xbb,0x2b,0xd6,0x79,0x40,0x98,0x22,0x4d,0xa3,
0x51,0x6c,0x04,0xf9,0xe6,0x29,0x69,0x47,0x5c,0x75,0x70,0x50,0x25,0x0e,0x30,0x36,
0x2f,0x2b,0x1e,0x41,0xbc,0x5c,0xe6,0x3b,0xe0,0xfe,0x3c,0xf7,0xfb,0x04,0xb6,0xdc,
0x92,0x94,0x45,0x12,0x0b,0x5e,0xc0,0x5f,0x4b,0xe5,0x7a,0x3c,0xcb,0xaa,0x5a,0xf8,
0x35,0x87,0xfd,0x74,0xc0,0x7a,0x15,0xaf,0x9b,0x5c,0x38,0x16,0x21,0x44,0x2b,0x81,
0x26,0x44,0xc3,0x5a,0x61,0x21,0x61,0xca,0xf8,0xe9,0xc3,0x66,0x87,0x41,0x10,0x39,
0xe6,0xbb,0x7c,0x58,0x38,0x64,0x53,0xa2,0x35,0x55,0x24,0x37,0xa4,0x82,0x9d,0x7a,
0xfe,0xa4,0xbc,0x94,0x15,0x71,0xb5,0xa3,0x7a,0x1a,0xac,0x90,0x82,0x48,0x5c,0x9e,
0x94,0x55,0x9c,0xc0,0x86,0x8e,0x93,0x6d,0xe9,0xf1,0x6d,0xf0,0xf8,0xb6,0xf2,0xf8,
0xda,0xd7,0xe1,0xf6,0x24,0xfa,0x86,0xed,0x49,0xac,0x60,0xd9,0xd4,0xf3,0x71,0xdb,
0xe7,0x10,0x93,0x0e,0x98,0x50,0xac,0xbf,0x61,0xd8,0xf0,0xd8,0xea,0x48,0xff,0xd1,
0x8e,0x2f,0x09,0x5a,0x2e,0xd3,0x2f,0xb7,0xa0,0xa2,0xca,0x62,0x55,0x71,0x34,0x13,
0x5b,0xe9,0x75,0x53,0xef,0x90,0x0e,0x56,0x19,0x8d,0x4a,0xd9,0x27,0x91,0x68,0x87,
0xf4,0x1e,0x5b,0xb9,0x69,0x92,0x18,0xa0,0x56,0xe1,0x20,0x4d,0xc2,0x34,0x94,0xd5,
0xdb,0xfe,0x5a,0x91,0x7f,0x8f,0x66,0x9e,0x44,0x7a,0x35,0xa4,0x45,0xb2,0x94,0x59,
0xd6,0x3d,0xd3,0xca,0x45,0x83,0xad,0x4d,0xdd,0x3d,0xae,0x29,0x0c,0xf7,0xcc,0x46,
0x68,0xf3,0xb8,0x9e,0xeb,0x93,0xbe,0x21,0xd4,0x7b,0xe4,0x37,0xdb,0xd1,0x20,0xe4,
0xd1,0xb6,0xe8,0x18,0xb7,0x8a,0xb7,0x28,0x6e,0x1c,0xaa,0x3a,0x7c,0x3a,0x4a,0xf9,
0xc7,0x3b,0x01,0x62,0xea,0x1d,0xe3,0x14,0x48,0xb4,0x87,0x1e,0x2b,0x48,0x14,0xe8,
0x3a,0x19,0xc4,0x24,0x62,0xf9,0xb9,0x49,0x19,0x8c,0x6a,0xcb,0x7c,0x78,0x4e,0x24,
0x4d,0x68,0xcf,0x4d,0x0e,0x58,0x05,0x82,0x26,0xec,0xec,0xb4,0xeb,0x33,0x11,0x81,
0x19,0x23,0xd9,0xf1,0x01,0xe7,0xb3,0x4e,0x60,0x80,0x8a,0x46,0x90,0x93,0x1d,0xf5,
0x8f,0xf4,0x14,0x34,0xa4,0xc7,0x78,0x54,0xb4,0x25,0x21,0x9c,0x3d,0x14,0x65,0x7a,
0xc0,0x5d,0x6a,0x45,0xf9,0x0b,0x25,0x9a,0x1a,0xe1,0xe8,0x20,0x01,0x39,0xd7,0xcb,
0xab,0x76,0x79,0xd1,0xc1,0xb5,0xa3,0xba,0x5c,0x91,0xc5,0xa8,0x6b,0x02,0x5a,0xa7,
0x2f,0xb6,0x35,0x68,0x1d,0x3f,0x1e,0xa9,0x6a,0xca,0x21,0x86,0xcb,0x1d,0x5b,0xd3,
0x94,0xe7,0x5c,0xf0,0x91,0xa1,0x17,0xaa,0x15,0x23,0x91,0x59,0x31,0xd2,0xf4,0x92,
0x57,0x81,0x64,0xc8,0xdb,0x11,0x45,0xe7,0x62,0x95,0xa5,0xd6,0x13,0xd7,0x49,0x1e,
0x4b,0x8c,0x94,0xc7,0x7e,0x36,0xb1,0x85,0xfe,0xff,0x1f,0x53,0x5c,0x05,0x2c,0x33,
0xe2,0x62,0xa7,0x1c,0xfb,0x94,0x6b,0xe4,0xd2,0x0e,0xd8,0xc1,0xda,0x8c,0xb0,0x2e,
0xb1,0x81,0x3a,0xec,0xb7,0xc5,0x48,0x72,0xc4,0xa7,0x0c,0xbd,0xc5,0x7c,0x9d,0xf5,
0xb0,0x65,0xda,0x26,0x20,0x9c,0xef,0x4f,0xf7,0x69,0x37,0xb5,0xad,0x63,0x82,0x61,
0xeb,0xb4,0xa2,0x5e,0xe8,0x43,0xfa,0x52,0x5c,0x55,0x56,0xe9,0x1c,0x3a,0x93,0x98,
0x60,0x55,0x5d,0xdc,0x07,0x3c,0x5f,0x54,0x0d,0x67,0xbd,0x35,0xd6,0xe9,0x3a,0x00,
0x6c,0x9d,0x17,0x4e,0xde,0x55,0xcc,0x75,0x80,0xee,0x15,0xf4,0xad,0xb7,0xa9,0xe0,
0xa2,0x2a,0xd7,0xe7,0xdd,0x23,0x98,0x51,0x80,0xea,0x08,0x35,0x9b,0xbe,0x6b,0x03,
0x37,0xd5,0x87,0x6d,0x7a,0xfa,0x5e,0x74,0xdc,0x4e,0x94,0x9d,0xa4,0x17,0x41,0x6e,
0xfe,0xbf,0x0d,0xc8,0xb5,0xd1,0xd1,0xcd,0xe8,0x80,0xf5,0xff,0xe5,0xde,0xe4,0x48,
0x38,0xdc,0xa7,0xe8,0x32,0x43,0x2e,0x1e,0xd2,0xd8,0x82,0x24,0xce,0x73,0x69,0x47,
0x16,0x9a,0xe6,0x12,0xf5,0x3d,0x3e,0x98,0x6d,0xe7,0x1a,0xe5,0x08,0xe1,0x86,0x1b,
0x9b,0xc5,0xb0,0xef,0x09,0xcc,0x6c,0x53,0xb6,0x6a,0xae,0x83,0xbd,0xd3,0x56,0xc7,
0x27,0x1e,0xc3,0xc2,0x39,0x6d,0x6f,0xb8,0x23,0x0e,0x52,0x70,0x3a,0xdb,0x70,0xd3,
0xeb,0xa5,0xe0,0x14,0x75,0xb0,0x78,0x4f,0xf5,0x55,0x95,0xc3,0x09,0x32,0x58,0x64,
0xa7,0xac,0xc7,0x8e,0x09,0x57,0x2c,0x3c,0x83,0x03,0xa3,0x34,0x05,0xa8,0xa7,0x01,
0xcf,0xd8,0xd7,0x2f,0x67,0x46,0x23,0x14,0x49,0x8e,0x3c,0x46,0x69,0x99,0x34,0x0b,
0x5e,0x08,0x8c,0x9f,0xab,0x39,0xc7,0x8f,0x97,0x76,0x6e,0xa6,0x63,0x0f,0x67,0x1f,
0x3d,0xe6,0xd3,0xdc,0xe2,0x30,0x18,0xcd,0x39,0x7a,0x2c,0x54,0x54,0x69,0x18,0x71,
0x18,0x9e,0xf6,0x81,0xb0,0x9e,0x69,0x1c,0x06,0xd5,0x10,0x86,0xf8,0xa2,0x4c,0xb9,
0x85,0xf0,0x7b,0xc3,0xc1,0x20,0x3c,0xe7,0x38,0xdb,0x77,0x11,0xc2,0xc4,0x93,0x3a,
0xbd,0xb6,0x06,0x48,0xdf,0x12,0x72,0xfb,0xcc,0x04,0x61,0x75,0xa9,0xa9,0xb1,0xf6,
0xd6,0xf0,0xf4,0x4a,0xca,0x05,0x69,0x56,0xc7,0x1b,0x39,0x4f,0x23,0x5c,0x0d,0x25,
0x0d,0x67,0x0d,0xab,0x1d,0x0a,0x40,0x15,0x4f,0x8a,0x61,0xee,0xf9,0x26,0x4c,0x2e,
0x40,0xa9,0x73,0xb0,0x56,0x36,0x67,0x55,0xf5,0x16,0xa0,0x50,0xfc,0xab,0x4a,0x95,
0xa6,0x0e,0x04,0xdf,0x16,0x97,0xd5,0x84,0xb4,0xda,0x0b,0xb5,0x90,0xb3,0x38,0xaf,
0x39,0x23,0x32,0x1c,0x0f,0x2d,0xde,0x7a,0x23,0xa7,0x73,0xd7,0xf5,0x18,0xaf,0xe7,
0xef,0x25,0x71,0x32,0xe7,0x53,0xaf,0x28,0xd7,0x6a,0xb0,0x02,0xf7,0x56,0x2c,0x10,
0x73,0x5e,0x18,0x87,0x83,0x09,0x97,0x65,0x51,0x73,0xe9,0xf8,0x11,0xce,0x60,0x7e,
0xa2,0x97,0x82,0xf2,0x23,0x13,0xf3,0xaa,0xdc,0x1a,0x5d,0xad,0x2a,0x50,0xd1,0xbb,
0xa9,0xe8,0x8e,0x9a,0x22,0xde,0x84,0xfa,0x8c,0xca,0x78,0x26,0x14,0x35,0xda,0x6f,
0x35,0xd0,0x95,0x86,0x3d,0xc0,0x4d,0x4b,0xa6,0xb8,0x0d,0x3a,0xb7,0xd5,0x80,0x75,
0x4c,0xd0,0xe2,0x07,0x59,0xba,0xbf,0xef,0xe9,0xf9,0x66,0x8f,0x4d,0xbc,0xd1,0x9f,
0x7f,0x8c,0xbc,0x89,0x01,0x98,0xc7,0x55,0x8a,0x23,0xc5,0x00,0x76,0xf5,0xf1,0x83,
0x6f,0xbf,0xf1,0xb4,0x40,0x34,0x0c,0x6a,0x05,0xfc,0x49,0x85,0xf0,0xae,0x58,0xc3,
0xd2,0xae,0x35,0xc2,0x95,0xe4,0xa3,0x42,0xa7,0x2c,0x12,0xe8,0xdc,0x1f,0xa3,0x6e,
0x7a,0x51,0xb4,0x62,0x06,0x45,0x14,0x4d,0xf8,0xb1,0xfe,0xf2,0x4b,0xeb,0x0b,0xbc,
0x77,0x0c,0x5b,0x7f,0xc0,0x0a,0xa8,0x21,0xff,0x0b,0xea,0x6c,0x17,0x5c,0xa5,0xa2,
0xc5,0xa3,0xe8,0x1e,0xc5,0xa3,0xa2,0x2c,0xf8,0x62,0x09,0xb2,0x64,0x38,0xcd,0x1c,
0xb4,0x4e,0x09,0x57,0x2d,0x3f,0x0c,0xf8,0x81,0xe4,0xe8,0xcb,0x8c,0xa9,0x1a,0xce,
0x06,0xe5,0xe5,0xc8,0x31,0x89,0xa3,0xde,0x0d,0xca,0x2e,0xd8,0x57,0xe4,0xc6,0x58,
0xa6,0xe2,0x94,0x57,0x12,0x02,0x47,0xb1,0x71,0xc2,0x5c,0xae,0x97,0xb3,0x19,0x76,
0xd5,0xb3,0x7e,0x32,0x6f,0x8a,0x8f,0xd1,0x37,0x3f,0x7c,0x03,0x73,0x47,0x44,0x55,
0x47,0x34,0x55,0x7d,0x9d,0xd8,0x01,0x0d,0x38,0x47,0x32,0xce,0xcd,0x2a,0x0d,0x40,
0x9f,0x25,0xcc,0x9e,0x34,0xf1,0x2e,0xc7,0x79,0xd2,0xe4,0xb1,0xc0,0xb9,0x76,0xa9,
0x44,0xdd,0x2c,0x60,0xa0,0xdd,0x23,0x0c,0x12,0x0f,0x3c,0xc3,0x31,0xa6,0x8d,0x67,
0x8c,0x51,0x2f,0x97,0x4d,0x9e,0x82,0x45,0x85,0x84,0x85,0x7e,0xcc,0x47,0xb5,0x34,
0x14,0x4f,0xa5,0xd3,0xd0,0xba,0x2b,0x87,0x18,0x56,0x39,0x43,0x8b,0x6f,0x82,0x20,
0xca,0xd5,0x23,0xb4,0x97,0x3a,0xc9,0xd2,0x46,0x40,0x17,0x9f,0x01,0xdd,0x34,0xb1,
0x90,0x0c,0x33,0x89,0x7a,0x36,0xed,0x83,0x28,0x51,0xc3,0x80,0x20,0x84,0x5f,0xac,
0x78,0x40,0x39,0xee,0x81,0x0d,0xc6,0x8e,0xdf,0x55,0xa4,0x95,0xd5,0x82,0xfc,0x01,
0x1f,0xae,0xc4,0x22,0x96,0x5e,0x82,0x08,0xa8,0x85,0x5c,0x7e,0x71,0xf7,0xce,0x0d,
0x21,0x96,0x8f,0x68,0x09,0x68,0x10,0xae,0xc4,0xd3,0x92,0x7b,0xe0,0x68,0xcf,0x47,
0x55,0xf0,0x1b,0x63,0xa1,0xbd,0x89,0x11,0xe3,0xa3,0x38,0x3e,0x7e,0x6a,0xf1,0x15,
0x8f,0xa0,0x04,0xa0,0xb1,0xf7,0xe0,0xfe,0xe3,0x27,0x9e,0xaf,0x6b,0x14,0xd8,0x50,
0x6f,0x6f,0x65,0x62,0x7e,0xb9,0xe2,0x32,0x9b,0xc0,0xd5,0x11,0x46,0x81,0x4b,0x82,
0xfa,0x08,0x18,0xba,0xed,0x0b,0xae,0xb1,0xb3,0x19,0x7d,0x52,0x85,0x17,0x6f,0x81,
0x1b,0x81,0x29,0xc9,0xf6,0x9c,0xd0,0x91,0x2f,0x02,0xaa,0xb2,0x69,0xfd,0x81,0xa4,
0x79,0xfa,0x15,0x8c,0xd7,0xaf,0x2b,0x1f,0x94,0x22,0xce,0x59,0xd8,0x17,0x5c,0x34,
0xb0,0x8f,0xa1,0xe5,0x4d,0xba,0x74,0x27,0xde,0x17,0x90,0xfd,0x2b,0x47,0x72,0x13,
0x1b,0x6e,0x9c,0x69,0x00,0xd5,0x12,0xa3,0x08,0xe6,0x4b,0xcf,0x7b,0xea,0xe7,0x2b,
0x12,0x10,0x1f,0x9f,0xb3,0x43,0x19,0x7e,0x29,0x15,0x9c,0xad,0x2c,0xcf,0x81,0xee,
0x46,0x59,0x8a,0xc0,0x9b,0x2a,0x51,0x46,0x33,0x28,0x3d,0x10,0x99,0xe3,0x1b,0x4f,
0x9e,0x3c,0x00,0xa1,0xba,0x64,0x27,0x1e,0x9b,0x5a,0x8b,0xba,0x44,0x3f,0x01,0x95,
0x58,0xd8,0x23,0xeb,0x50,0x52,0x74,0x99,0x85,0x94,0x57,0x52,0xb6,0xa4,0x2c,0x0a,
0xae,0xee,0xa0,0x8b,0x14,0x48,0x89,0x6a,0x27,0xf0,0x7a,0x89,0xc7,0x1b,0x65,0x25,
0x0e,0x21,0x9e,0xc4,0x45,0xc2,0x73,0xa0,0xdf,0x8b,0x5f,0x63,0xa4,0x61,0xd4,0xa9,
0xf8,0x22,0x00,0xd3,0x41,0x4d,0x0e,0xec,0xa9,0xb4,0xc4,0xff,0x2e,0xd6,0x76,0x75,
0xc2,0x18,0x55,0x0f,0x53,0x94,0x44,0x7e,0xfb,0x56,0xa8,0xcd,0x26,0x5f,0xe5,0xa3,
0x2c,0x53,0x8c,0x31,0x4a,0x25,0x43,0x1e,0xd9,0x23,0xf3,0x15,0xc3,0x8f,0xe6,0x2c,
0xb5,0x8e,0x3f,0x77,0x03,0xff,0xd1,0x8f,0x43,0xfd,0x05,0x90,0x09,0x2d,0xa4,0x1f,
0x35,0x00,0x00,
};
const uint32_t MESH_HTML_LIGHTWEIGHT_OTA_LEN = 619;
const uint8_t MESH_HTML_LIGHTWEIGHT_OTA[] PROGMEM = {
0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0xff,0x74,0x52,0xc5,0x9a,0xdb,0x30,
+1 -1
View File
@@ -11,7 +11,7 @@
/* nRF52's Arduino recipe defaults to -Ofast, which more than doubles this
* decoder. Keep the size policy local to tinf instead of changing MeshCore. */
#pragma GCC push_options
#if defined(NRF52_PLATFORM)
#if defined(NRF52_PLATFORM) && __GNUC__ >= 12
#pragma GCC optimize ("Oz")
#else
#pragma GCC optimize ("Os")
+3 -3
View File
@@ -1481,10 +1481,10 @@
}
],
"noncompact_tokens_sha256": {
"src/helpers/CommonCLI.cpp": "e060c15118b94db76a41391b1fee872759ddcf308576b60eb1ae6ddec9444b8a",
"examples/simple_repeater/MyMesh.cpp": "bf614d2a4c672d33cbfbf2d4338f522c21e877abcfa7566f0e39a9fb6eecbbd9",
"src/helpers/CommonCLI.cpp": "c39dce89a0222f32d3da3f66f9c130491c93cf1e14e7eefeccb5b57dd63e9612",
"examples/simple_repeater/MyMesh.cpp": "c67ad3818404b7f1bd3fb99cd44aefac788f9c069342eb3124c693a066d03ca4",
"src/helpers/RadioProfileCLI.cpp": "8e3d293cb522717b13901c27d7fc471f5c370710919fb3d74d35449bc497552d",
"src/helpers/WirelessControl.h": "0944bfdefb7003b47af52104b4c7447d7e5e65d54b3396790878c5b49e472913",
"src/helpers/WirelessControl.h": "d96f79fbc5736d9e08d4839c76b4af5c6cfccab5d3c2f01226cdbfeed1da56c5",
"src/helpers/PowerSavingDiagnostics.h": "18b78f5780046ea8c772c4ce6c337741ae0c027039d9399c94c5f4205d114000",
"src/helpers/FloodAdvertCLI.h": "0f378e4d0f6c3b9b79991068a4b4208139484d2144ee1d7c5094d39043463be4",
"src/helpers/LedSettings.cpp": "df0681234ca1337148ad59caa935857bc692f98bf18603ca6d099c0382028400",
+155
View File
@@ -0,0 +1,155 @@
"""Exercise the compact OTA page against the existing ElegantOTA wire format."""
from email import policy
from email.parser import BytesParser
import gzip
import hashlib
import http.server
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import threading
import unittest
from test_webconfig_ui_runtime import BROWSER, run_browser
ROOT = Path(__file__).resolve().parents[1]
VENDOR = ROOT / "webui/vendor/spark-md5-3.0.2.min.js"
HEADER = ROOT / "src/helpers/esp32/StaticHtml.h"
SYMBOL = "MESH_HTML_ELEGANT_OTA_COMPACT"
def embedded_page():
source = HEADER.read_text(encoding="utf-8")
length = int(re.search(SYMBOL + r"_LEN = (\d+);", source).group(1))
array = source.split("const uint8_t " + SYMBOL + "[] PROGMEM = {", 1)[1].split("};", 1)[0]
data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", array))
if len(data) != length:
raise AssertionError("compressed OTA page has a bad length")
return gzip.decompress(data).decode("utf-8")
class CompactElegantOtaTests(unittest.TestCase):
def test_vendored_incremental_md5_vectors(self):
self.assertEqual(hashlib.sha256(VENDOR.read_bytes()).hexdigest(),
"d80e84c820cc5587a0ba3c8a20652099ea3fa7fc43944e812e56d449c1d9f1c9")
node = shutil.which("node")
if not node:
self.skipTest("Node is unavailable")
script = r'''
const SparkMD5 = require(process.argv[1]);
const crypto = require('crypto');
if (SparkMD5.hash('') !== 'd41d8cd98f00b204e9800998ecf8427e' ||
SparkMD5.hash('abc') !== '900150983cd24fb0d6963f7d28e17f72') process.exit(1);
for (const size of [0, 3, 64, 262144, 700123]) {
const input = Buffer.alloc(size);
for (let i=0;i<size;i++) input[i] = i % 251;
const digest = new SparkMD5.ArrayBuffer();
for (let i=0;i<size;i+=65537) {
const b=input.subarray(i,Math.min(size,i+65537));
digest.append(b.buffer.slice(b.byteOffset,b.byteOffset+b.byteLength));
}
if (digest.end() !== crypto.createHash('md5').update(input).digest('hex')) process.exit(1);
}
'''
subprocess.run([node, "-e", script, str(VENDOR)], check=True, timeout=30)
def test_only_tbeam_observers_select_the_page(self):
source = (ROOT / "arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h").read_text()
self.assertIn("MESH_COMPACT_ELEGANT_OTA", source)
self.assertIn("#include \"elegantWebpage.h\"", source)
target = (ROOT / "variants/lilygo_tbeam_universal/platformio.ini").read_text()
selected = re.findall(r"\[env:([^]]+)\]([^\[]*)", target)
enabled = [name for name, section in selected if "-D MESH_COMPACT_ELEGANT_OTA=1" in section]
self.assertEqual(enabled, ["Tbeam_repeater_observer_mqtt", "Tbeam_room_server_observer_mqtt"])
self.assertEqual(embedded_page().count("MESH_SPARK_MD5"), 0)
self.assertIn("SparkMD5", embedded_page())
@unittest.skipUnless(BROWSER, "Chromium-family browser is unavailable")
def test_real_browser_streams_firmware_and_filesystem_with_correct_md5(self):
payload = bytes(i % 251 for i in range(700123)) # three FileReader chunks
requests = []
page = embedded_page().replace("</body>", r'''
<script>
window.addEventListener('load',function(){
var bytes=new Uint8Array(700123);for(var i=0;i<bytes.length;i++)bytes[i]=i%251;
var input=document.getElementById('file'),button=document.getElementById('upload');
function send(kind){
document.querySelector('input[value="'+kind+'"]').checked=true;
var selection=new DataTransfer();selection.items.add(new File([bytes],kind+'.bin'));
input.files=selection.files;button.click();
}
var phase=0;send('firmware');
document.body.setAttribute('data-test-busy',button.disabled&&input.disabled&&document.querySelector('input[value="filesystem"]').disabled?'yes':'no');
var timer=setInterval(function(){
var text=document.getElementById('status').textContent;
if(phase===2){
if(text.indexOf('Upload failed (HTTP 500)')<0)return;
clearInterval(timer);document.body.setAttribute('data-test-done',!button.disabled&&!input.disabled&&!document.querySelector('input[value="filesystem"]').disabled?'yes':'no');
document.body.setAttribute('data-test-identity',document.getElementById('identity').textContent);return;
}
if(text.indexOf('Update complete')<0)return;
if(phase++===0){send('filesystem');return;}
send('firmware');
},30);
});
</script></body>''')
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
body = (b'{"id":"test node","hardware":"ESP32"}' if self.path == "/update/identity"
else page.encode("utf-8"))
self.send_response(200)
self.send_header("Content-Type", "application/json" if self.path == "/update/identity" else "text/html")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
message = BytesParser(policy=policy.default).parsebytes(
b"Content-Type: " + self.headers["Content-Type"].encode("ascii")
+ b"\r\nMIME-Version: 1.0\r\n\r\n" + body)
parts = {p.get_param("name", header="Content-Disposition"): p for p in message.iter_parts()}
requests.append((self.path, parts["MD5"].get_payload(decode=True).decode(),
[(name, part.get_filename(), part.get_payload(decode=True))
for name, part in parts.items() if name != "MD5"]))
failure = len(requests) == 3
response = b"invalid image" if failure else b"OK"
self.send_response(500 if failure else 200)
self.send_header("Content-Length", str(len(response)))
self.end_headers()
self.wfile.write(response)
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
with tempfile.TemporaryDirectory(prefix="meshcore-ota-browser-") as folder:
command = [BROWSER, "--headless=new", "--disable-gpu", "--no-first-run",
"--user-data-dir=" + folder, "--virtual-time-budget=15000", "--dump-dom",
"http://127.0.0.1:%d/update" % server.server_port]
if os.name != "nt" and hasattr(os, "geteuid") and os.geteuid() == 0:
command.insert(1, "--no-sandbox")
result = run_browser(command)
self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace"))
self.assertIn('data-test-busy="yes"', result.stdout.decode(errors="replace"))
self.assertIn('data-test-done="yes"', result.stdout.decode(errors="replace"))
self.assertIn('data-test-identity="test node · ESP32"', result.stdout.decode(errors="replace"))
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
digest = hashlib.md5(payload).hexdigest()
self.assertEqual(requests, [("/update", digest, [(name, name, payload)])
for name in ("firmware", "filesystem", "firmware")])
if __name__ == "__main__":
unittest.main()
+8 -1
View File
@@ -22,7 +22,7 @@ HARNESS = r'''
#define UI_HAS_JOYSTICK 0
#define UI_WIFI_SETUP_HOME_PAGE 0
#define MESH_DEBUG_PRINTLN(...)
constexpr char KEY_ENTER='e', KEY_NEXT='n', KEY_PREV='p', KEY_SELECT='s', KEY_UP='u', KEY_DOWN='d';
constexpr char KEY_ENTER='e', KEY_NEXT='n', KEY_PREV='p', KEY_SELECT='s', KEY_UP='u', KEY_DOWN='d', KEY_LEFT='l', KEY_RIGHT='r';
constexpr int BUTTON_EVENT_CLICK=1, BUTTON_EVENT_LONG_PRESS=2, BUTTON_EVENT_DOUBLE_CLICK=3;
constexpr int BUTTON_EVENT_TRIPLE_CLICK=4, BUTTON_EVENT_QUADRUPLE_CLICK=5;
uint32_t now_ms=0;
@@ -156,6 +156,13 @@ void visible() {
event(ui, BUTTON_EVENT_LONG_PRESS);
assert(ui.reader_closes==1 && ui.curr==ui.home);
ui.home_screen.radio_page=true;
#ifdef UI_HAS_NAV_INPUT
event(ui, BUTTON_EVENT_CLICK);
assert(ui.curr==ui.home && ui.reader_opens==0);
#endif
assert(ui.handleLongPress(KEY_LEFT)==KEY_LEFT);
assert(ui.handleLongPress(KEY_RIGHT)==KEY_RIGHT);
assert(ui.curr==ui.home && ui.reader_opens==0);
event(ui, BUTTON_EVENT_LONG_PRESS);
assert(ui.reader_opens==1 && ui.curr==ui.reader);
#endif
+132
View File
@@ -0,0 +1,132 @@
"""Check the compact no-wireless Companion path against the full controller."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <helpers/WirelessControl.h>
#include "examples/companion_radio/CompanionWireless.h"
#include <cassert>
#include <cstring>
#include <string>
#include <vector>
struct Unavailable : mesh::wireless::Backend {
unsigned sets = 0;
uint8_t available() const override { return 0; }
uint8_t enabled() const override { return 0; }
uint8_t clients() const override { return 0; }
mesh::wireless::Result set(uint8_t, bool) override {
++sets;
return mesh::wireless::Result::Failed;
}
};
@WRAPPER@
int main() {
using namespace mesh::wireless;
static Unavailable backend;
Control reference;
reference.begin(backend);
control().begin(backend);
const char* commands[] = {
"", "get wifi", "get espnow", "get 2.4ghz", "get wifi.status",
"set wifi on", "set wifi off", "set wifi off force", "set wifi on force",
"set espnow on", "set espnow off", "set espnow off force",
"set 2.4ghz on", "set 2.4ghz off", "set 2.4ghz off force",
"set 2.4ghz on all", "set 2.4ghz on force", "set 2.4ghz on all force",
" get wifi\t", "set\twifi on", "get wifi junk", "set wifi",
"set wifi off force extra", "set espnow.channel 3", "get bluetooth",
"set bluetooth off", "set 2.4ghzx on", "SET wifi on", "reset wifi"
};
const CompanionWirelessSource sources[] = {
CompanionWirelessSource::Other, CompanionWirelessSource::Usb,
CompanionWirelessSource::Network, CompanionWirelessSource::Framed
};
const uint8_t requesters[] = {0, Independent, WiFi, Bluetooth};
for (const char* command : commands) for (size_t size = 0; size <= 160; ++size)
for (unsigned route = 0; route < 4; ++route) {
char expected[160], actual[160];
memset(expected, 0x5a, sizeof(expected));
memset(actual, 0x5a, sizeof(actual));
const bool original = reference.handle(command, expected, size, 500, requesters[route]);
const bool compact = handleCompanionWirelessCommand(command, actual, size, sources[route]);
assert(original == compact);
assert(memcmp(expected, actual, sizeof(expected)) == 0);
reference.service(1000);
control().service(1000);
assert(!reference.pending() && !control().pending());
assert(!reference.masterOff() && !control().masterOff());
assert(!reference.blocked(All) && !control().blocked(All));
assert(backend.sets == 0);
}
for (const char* command : {"get wifi", "set wifi on", "unknown"}) {
assert(reference.handle(command, nullptr, 160, 500, Independent)
== handleCompanionWirelessCommand(command, nullptr, 160, CompanionWirelessSource::Usb));
}
}
'''
class NoWirelessCompanionTest(unittest.TestCase):
def test_stm32_wrapper_matches_full_controller(self):
wrapper = body((ROOT / "examples/companion_radio/main.cpp").read_text(),
"bool handleCompanionWirelessCommand(")
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "no_wireless.cpp"
source.write_text(HARNESS.replace("@WRAPPER@", wrapper))
for compact in (0, 1):
binary = Path(directory) / f"check-{compact}"
command = [os.environ.get("CXX", "g++"), "-std=c++17", "-O1",
"-Wall", "-Wextra", "-Werror", "-DSTM32_PLATFORM=1",
f"-DMESH_CLI_COMPACT_REPLIES={compact}",
"-I", str(ROOT / "src"), "-I", str(ROOT),
str(source), "-o", str(binary)]
if os.name != "nt":
command[1:1] = ["-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
subprocess.run(command, check=True, timeout=60)
subprocess.run([str(binary)], check=True, timeout=30)
def test_wireless_capabilities_keep_the_full_controller(self):
wrapper = body((ROOT / "examples/companion_radio/main.cpp").read_text(),
"bool handleCompanionWirelessCommand(")
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "guard.cpp"
source.write_text('#include <helpers/WirelessControl.h>\n'
'#include "examples/companion_radio/CompanionWireless.h"\n'
+ wrapper)
for flags, compact in (
(["-DSTM32_PLATFORM=1"], True),
(["-DSTM32_PLATFORM=1", "-DMESH_PRIMARY_ESPNOW=0"], True),
(["-DSTM32_PLATFORM=1", "-DBLE_PIN_CODE=123456"], False),
(["-DSTM32_PLATFORM=1", "-DWIFI_SSID=1"], False),
(["-DSTM32_PLATFORM=1", "-DMESH_PRIMARY_ESPNOW=1"], False),
(["-DSTM32_PLATFORM=1", "-DWITH_BRIDGE=1"], False),
(["-DSTM32_PLATFORM=1", "-DWITH_MQTT_BRIDGE=1"], False),
(["-DSTM32_PLATFORM=1", "-DWITH_UDP_BRIDGE=1"], False),
(["-DSTM32_PLATFORM=1", "-DWITH_ESPNOW_BRIDGE=1"], False),
(["-DSTM32_PLATFORM=1", "-DWITH_WEBCONFIG=1"], False),
(["-DSTM32_PLATFORM=1", "-DETHERNET_ENABLED=1"], False),
(["-DSTM32_PLATFORM=1", "-DMESH_ENABLE_EMERGENCY_POWER=1"], False),
([], False),
):
with self.subTest(flags=flags):
result = subprocess.run(
[os.environ.get("CXX", "g++"), "-std=c++17", "-E", "-P",
*flags, "-I", str(ROOT / "src"), "-I", str(ROOT), str(source)],
check=True, capture_output=True, text=True, timeout=60)
self.assertEqual("return mesh::wireless::handleNoServices(" in result.stdout, compact)
self.assertEqual("return mesh::wireless::control().handle(" in result.stdout, not compact)
if __name__ == "__main__":
unittest.main()
+13 -7
View File
@@ -78,7 +78,7 @@ class CompanionReaderTest(unittest.TestCase):
"#pragma once\n#include <stdint.h>\n#include <stddef.h>\n#include <stdio.h>\n#include <string.h>\n"
"class Stream { public: virtual size_t write(const uint8_t*,size_t);size_t print(const char*); };\n")
flags = ["-mcpu=cortex-m4", "-mthumb", "-Oz", "-fno-inline-small-functions",
"-fstack-usage", "-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1",
"-fstack-usage", "-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DCOMPANION_FEATURE_READER=1",
"-DENABLE_USB_INTERFACE=1", "-I" + str(directory),
"-I" + str(ROOT / "src"), "-I" + str(FIXTURE)]
if compact:
@@ -239,7 +239,7 @@ class CompanionReaderTest(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="meshcore-reader-") as directory:
directory = Path(directory)
for platform, compact in (("ESP32_PLATFORM", False), ("NRF52_PLATFORM", False), ("NRF52_PLATFORM", True)):
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"]
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"]
if compact:
flags.append("-DCOMPANION_READER_COMPACT_STREAM=1")
obj, binary = directory / "tinf.o", directory / "reader.exe"
@@ -264,7 +264,7 @@ class CompanionReaderTest(unittest.TestCase):
self.skipTest("host G++ required")
for platform, compact, expected in (("NRF52_PLATFORM", False, 3), ("NRF52_PLATFORM", True, 0),
("ESP32_PLATFORM", True, None)):
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"]
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"]
if compact:
flags.append("-DCOMPANION_READER_COMPACT_STREAM=1")
result = self.run_checked([cxx, "-E", "-P", *flags,
@@ -280,6 +280,8 @@ class CompanionReaderTest(unittest.TestCase):
self.skipTest("host G++ required")
for flags in ([], ["-DESP32_PLATFORM=1"], ["-DNRF52_PLATFORM=1"],
["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1"],
["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"],
["-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"],
["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1",
"-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=0"]):
result = self.run_checked([cxx, "-E", "-P", *flags,
@@ -448,7 +450,7 @@ int main() {
generated = directory / "dispatch.cpp"
generated.write_text(harness)
for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM"):
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"]
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"]
obj, binary = directory / "tinf.o", directory / "dispatch.exe"
self.run_checked([cc, "-std=c11", "-Os", *flags, "-c",
str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)])
@@ -465,7 +467,7 @@ int main() {
self.skipTest("host GCC and G++ required")
with tempfile.TemporaryDirectory(prefix="meshcore-reader-reader-") as directory:
directory = Path(directory)
flags = ["-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"]
flags = ["-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"]
obj, binary = directory / "tinf.o", directory / "reader.exe"
self.run_checked([cc, "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"),
"-o", str(obj)])
@@ -582,7 +584,7 @@ int main() {
generated = directory / "interlock.cpp"
generated.write_text(harness)
for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM"):
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"]
flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"]
obj, binary = directory / "tinf.o", directory / "interlock.exe"
self.run_checked([cc, "-std=c11", "-Os", *flags, "-c",
str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)])
@@ -594,7 +596,11 @@ int main() {
def test_reader_button_routing_and_profile_gate(self):
ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text(encoding="utf-8")
long_press = ui[ui.index("char UITask::handleLongPress("):]
home = extract_braced(ui, "class HomeScreen")
short_press = extract_braced(home, "bool handleInput(char c) override")
self.assertNotIn("showReader(", short_press)
long_press = extract_braced(ui, "char UITask::handleLongPress(")
self.assertRegex(long_press, r"if\s*\(c\s*==\s*KEY_ENTER[\s\S]*?isRadioPage\(\)\)\s*\{\s*showReader\(\)")
self.assertLess(long_press.index("isReaderActive()"), long_press.index("enterCLIRescue()"))
self.assertLess(long_press.index("isRadioPage()"), long_press.index("enterCLIRescue()"))
self.assertIn("#if COMPANION_FEATURE_READER\n#include \"ReaderScreen.h\"", ui)
+6 -3
View File
@@ -2,12 +2,13 @@
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
from cpp_source import body, _uncommented
from test_display_power import FS_STUB
@@ -349,9 +350,11 @@ class LR2021CalibrationTests(unittest.TestCase):
def test_all_cli_roles_load_before_init_and_service_pending(self):
for role in ("simple_repeater", "simple_room_server", "simple_sensor", "companion_radio"):
source = (ROOT / "examples" / role / "main.cpp").read_text()
setup = body(source, "void setup()")
setup = _uncommented(body(source, "void setup()"))
with self.subTest(role=role):
self.assertLess(setup.index("loadLR2021CalibrationSettings("), setup.index("radio_init()"))
init = re.search(r"\bwhile\s*\([^\n;]*\bradio_init\s*\(\s*\)", setup)
self.assertIsNotNone(init, "startup must initialize the radio")
self.assertLess(setup.index("loadLR2021CalibrationSettings("), init.start())
self.assertIn("loopLR2021CalibrationSettings(radio_driver)", source)
self.assertIn("#if defined(USE_LR2021)", source)
+1 -1
View File
@@ -31,7 +31,7 @@ class PocketReaderTest(unittest.TestCase):
self.assertIsNotNone(font)
selected = directory / "font.c"
selected.write_text('#include "u8g2_fonts.h"\n' + font.group())
flags = ["-g", "-O1", "-DHELTEC_MESH_POCKET=1", "-DCOMPANION_RADIO_FULL=1",
flags = ["-g", "-O1", "-DHELTEC_MESH_POCKET=1", "-DCOMPANION_RADIO_FULL=1", "-DCOMPANION_FEATURE_READER=1",
"-DNRF52_PLATFORM=1", "-DENABLE_USB_INTERFACE=1", "-DUI_POCKET_GUI=1",
"-DUI_SMALL_MESSAGE_FONT=0", "-DUI_BUTTON_READER_HINT=1", "-DUI_READER_TOUCH_BAR=0",
"-I" + str(FIXTURE), "-I" + str(ROOT / "src"), "-I" + str(ROOT),
@@ -0,0 +1,46 @@
"""The STM32WL no-service shortcut must not mask any wireless owner."""
from pathlib import Path
import shutil
import subprocess
import unittest
ROOT = Path(__file__).resolve().parents[1]
class Stm32WirelessSpecializationTest(unittest.TestCase):
def test_only_plain_stm32wl_uses_no_service_handler(self):
compiler = shutil.which("gcc")
if not compiler:
self.skipTest("host GCC required")
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
anchor = source.index("if (handleBeaconCommand(command, reply, 157))")
start = source.index("#if defined(STM32_PLATFORM) && defined(STM32WLxx)", anchor)
end = source.index("#endif", start) + len("#endif")
branch = source[start:end]
base = ("STM32_PLATFORM", "STM32WLxx", "USE_STM32WL")
cases = [(base, True),
(base[:-1], False),
(("ESP32_PLATFORM",), False),
(base + ("WITH_WEBCONFIG",), False),
(base + ("WITH_MQTT_BRIDGE",), False),
(base + ("WITH_UDP_BRIDGE",), False),
(base + ("WITH_ESPNOW_BRIDGE",), False),
(base + ("WITH_BRIDGE",), False),
(base + ("ETHERNET_ENABLED",), False),
(base + ("BLE_PIN_CODE=123456",), False),
(base + ("WIFI_SSID=custom",), False),
(base + ("MESH_PRIMARY_ESPNOW=1",), False),
(base + ("MESH_ENABLE_EMERGENCY_POWER=1",), False)]
for defines, expected in cases:
with self.subTest(defines=defines):
result = subprocess.run([compiler, "-E", "-P", "-x", "c", "-",
*("-D" + value for value in defines)],
input=branch, text=True, capture_output=True,
check=True)
self.assertEqual("handleNoServices" in result.stdout, expected)
self.assertEqual("control().handle" in result.stdout, not expected)
if __name__ == "__main__":
unittest.main()
+6 -1
View File
@@ -50,7 +50,12 @@ class WebConfigGzipTest(unittest.TestCase):
self.assertIn("gzip, 1000 iterations", header)
blobs = arrays(header)
for page in (ROOT / "webui/pages").glob("*.html"):
self.assertEqual(gzip.decompress(blobs["MESH_HTML_" + page.stem.upper()]), page.read_bytes())
expected = page.read_bytes()
if page.name == "elegant_ota_compact.html":
marker = b"/* MESH_SPARK_MD5 */"
self.assertEqual(expected.count(marker), 1)
expected = expected.replace(marker, (ROOT / "webui/vendor/spark-md5-3.0.2.min.js").read_bytes())
self.assertEqual(gzip.decompress(blobs["MESH_HTML_" + page.stem.upper()]), expected)
def test_loader_matches_legacy_android_browser_syntax(self):
blobs = arrays((ROOT / "src/helpers/esp32/WebConfigHtml.h").read_text())
+5 -3
View File
@@ -1,7 +1,9 @@
# Offline Reader lookup
ESP32 and nRF52 **Full Companion** USB terminals (and the ESP32 TCP terminal)
include an embedded 879-verse corpus from the World English Bible:
Reader is disabled in standard Companion builds to preserve flash space. A
custom ESP32 or nRF52 **Full Companion** USB build can enable it with
`COMPANION_FEATURE_READER=1`. The USB terminal (and ESP32 TCP terminal) then
includes an embedded 879-verse corpus from the World English Bible:
```text
get reader 3:16
@@ -23,7 +25,7 @@ last verse of a range. An optional page number refers to the first verse in
that command: `get reader 21:17 2`. No radio request can access the reader.
No WiFi, phone, API key, filesystem
space or bootloader update is needed. Reduced/ordinary Companion and other
roles omit the corpus. `COMPANION_FEATURE_READER=0` excludes it from a Full build.
roles omit the corpus; the opt-in requires a Full Companion USB build.
## On-device Reader
@@ -158,6 +158,7 @@ build_flags =
-fno-partial-inlining
-mno-target-align
-D MESH_UNIVERSAL_LTO=1
-D MESH_COMPACT_ELEGANT_OTA=1 ; full authenticated OTA protocol, smaller page
-D ADVERT_NAME='"T-Beam MQTT Observer"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
@@ -201,6 +202,7 @@ build_flags =
-fno-partial-inlining
-mno-target-align
-D MESH_UNIVERSAL_LTO=1
-D MESH_COMPACT_ELEGANT_OTA=1 ; full authenticated OTA protocol, smaller page
-D ADVERT_NAME='"T-Beam Room Observer"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
+53
View File
@@ -0,0 +1,53 @@
<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>MeshCore OTA</title><style>
body{font:16px sans-serif;max-width:38rem;margin:2rem auto;padding:0 1rem;line-height:1.45}
fieldset{margin:1rem 0}button,input{font:inherit}button{padding:.5rem 1rem}progress{width:100%}
#status{white-space:pre-wrap;overflow-wrap:anywhere}
</style></head><body>
<h1>MeshCore OTA</h1><p id="identity">Reading device identity…</p>
<fieldset><legend>Update type</legend>
<label><input type="radio" name="type" value="firmware" checked> Firmware</label>
<label><input type="radio" name="type" value="filesystem"> Filesystem</label></fieldset>
<label for="file">Image (.bin or .bin.gz)</label><br><input id="file" type="file" accept=".bin,.bin.gz"><br>
<button type="button" id="upload">Upload</button>
<progress id="progress" max="100" value="0" hidden></progress><p id="status" role="status"></p>
<script>/* spark-md5 3.0.2, André Cruz, (MIT OR WTFPL); see webui/vendor. */
/* MESH_SPARK_MD5 */
</script><script>
(function(){
var input=document.getElementById('file'),button=document.getElementById('upload');
var status=document.getElementById('status'),progress=document.getElementById('progress');
var modes=document.querySelectorAll('input[name="type"]');
function setBusy(busy){button.disabled=busy;input.disabled=busy;for(var i=0;i<modes.length;i++)modes[i].disabled=busy;}
function finish(message){status.textContent=message;setBusy(false);}
fetch('/update/identity',{cache:'no-store'}).then(function(response){
if(!response.ok)throw Error('Identity unavailable');return response.json();
}).then(function(identity){
document.getElementById('identity').textContent=(identity.id||'MeshCore')+' · '+(identity.hardware||'ESP32');
}).catch(function(){document.getElementById('identity').textContent='Device identity unavailable';});
button.onclick=function(){
var file=input.files&&input.files[0];
if(!file||!file.size){finish('Select a nonempty image.');return;}
var type=document.querySelector('input[name="type"]:checked').value;
var md5=new SparkMD5.ArrayBuffer(),reader=new FileReader(),offset=0,chunk=262144;
setBusy(true);progress.hidden=false;progress.value=0;
status.textContent='Calculating checksum…';
reader.onerror=function(){finish('Could not read the selected file.');};
reader.onload=function(event){
md5.append(event.target.result);offset+=event.target.result.byteLength;
if(offset<file.size){readNext();return;}
var form=new FormData(),request=new XMLHttpRequest();
form.append('MD5',md5.end());form.append(type,file,type);
request.open('POST','/update');request.withCredentials=true;
request.upload.onprogress=function(event){if(event.lengthComputable){progress.value=Math.round(event.loaded*100/event.total);status.textContent='Uploading '+progress.value+'%';}};
request.onload=function(){finish(request.status===200?'Update complete; the device will reboot.':'Upload failed (HTTP '+request.status+'): '+request.responseText);};
request.onerror=function(){finish('Upload failed; check the connection and retry.');};
request.onabort=function(){finish('Upload cancelled.');};
request.send(form);
};
function readNext(){reader.readAsArrayBuffer(file.slice(offset,Math.min(file.size,offset+chunk)));}
readNext();
};
})();
</script></body></html>
+7
View File
@@ -0,0 +1,7 @@
`spark-md5-3.0.2.min.js` is SparkMD5 3.0.2 by André Cruz, from
https://github.com/satazor/js-spark-md5/tree/v3.0.2. Its package metadata
offers either WTFPL or MIT; MeshCore uses the MIT option. The upstream WTFPL
license is also retained alongside the file.
SHA-256: `d80e84c820cc5587a0ba3c8a20652099ea3fa7fc43944e812e56d449c1d9f1c9`.
The OTA page embeds this file locally; it never downloads scripts at runtime.
+13
View File
@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2015 André Cruz <amdfcruz@gmail.com>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015 André Cruz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
(function(factory){if(typeof exports==="object"){module.exports=factory()}else if(typeof define==="function"&&define.amd){define(factory)}else{var glob;try{glob=window}catch(e){glob=self}glob.SparkMD5=factory()}})(function(undefined){"use strict";var add32=function(a,b){return a+b&4294967295},hex_chr=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];function cmn(q,a,b,x,s,t){a=add32(add32(a,q),add32(x,t));return add32(a<<s|a>>>32-s,b)}function md5cycle(x,k){var a=x[0],b=x[1],c=x[2],d=x[3];a+=(b&c|~b&d)+k[0]-680876936|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[1]-389564586|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[2]+606105819|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[3]-1044525330|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[4]-176418897|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[5]+1200080426|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[6]-1473231341|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[7]-45705983|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[8]+1770035416|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[9]-1958414417|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[10]-42063|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[11]-1990404162|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[12]+1804603682|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[13]-40341101|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[14]-1502002290|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[15]+1236535329|0;b=(b<<22|b>>>10)+c|0;a+=(b&d|c&~d)+k[1]-165796510|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[6]-1069501632|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[11]+643717713|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[0]-373897302|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[5]-701558691|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[10]+38016083|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[15]-660478335|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[4]-405537848|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[9]+568446438|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[14]-1019803690|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[3]-187363961|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[8]+1163531501|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[13]-1444681467|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[2]-51403784|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[7]+1735328473|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[12]-1926607734|0;b=(b<<20|b>>>12)+c|0;a+=(b^c^d)+k[5]-378558|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[8]-2022574463|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[11]+1839030562|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[14]-35309556|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[1]-1530992060|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[4]+1272893353|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[7]-155497632|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[10]-1094730640|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[13]+681279174|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[0]-358537222|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[3]-722521979|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[6]+76029189|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[9]-640364487|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[12]-421815835|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[15]+530742520|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[2]-995338651|0;b=(b<<23|b>>>9)+c|0;a+=(c^(b|~d))+k[0]-198630844|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[7]+1126891415|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[14]-1416354905|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[5]-57434055|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[12]+1700485571|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[3]-1894986606|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[10]-1051523|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[1]-2054922799|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[8]+1873313359|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[15]-30611744|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[6]-1560198380|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[13]+1309151649|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[4]-145523070|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[11]-1120210379|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[2]+718787259|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[9]-343485551|0;b=(b<<21|b>>>11)+c|0;x[0]=a+x[0]|0;x[1]=b+x[1]|0;x[2]=c+x[2]|0;x[3]=d+x[3]|0}function md5blk(s){var md5blks=[],i;for(i=0;i<64;i+=4){md5blks[i>>2]=s.charCodeAt(i)+(s.charCodeAt(i+1)<<8)+(s.charCodeAt(i+2)<<16)+(s.charCodeAt(i+3)<<24)}return md5blks}function md5blk_array(a){var md5blks=[],i;for(i=0;i<64;i+=4){md5blks[i>>2]=a[i]+(a[i+1]<<8)+(a[i+2]<<16)+(a[i+3]<<24)}return md5blks}function md51(s){var n=s.length,state=[1732584193,-271733879,-1732584194,271733878],i,length,tail,tmp,lo,hi;for(i=64;i<=n;i+=64){md5cycle(state,md5blk(s.substring(i-64,i)))}s=s.substring(i-64);length=s.length;tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(i=0;i<length;i+=1){tail[i>>2]|=s.charCodeAt(i)<<(i%4<<3)}tail[i>>2]|=128<<(i%4<<3);if(i>55){md5cycle(state,tail);for(i=0;i<16;i+=1){tail[i]=0}}tmp=n*8;tmp=tmp.toString(16).match(/(.*?)(.{0,8})$/);lo=parseInt(tmp[2],16);hi=parseInt(tmp[1],16)||0;tail[14]=lo;tail[15]=hi;md5cycle(state,tail);return state}function md51_array(a){var n=a.length,state=[1732584193,-271733879,-1732584194,271733878],i,length,tail,tmp,lo,hi;for(i=64;i<=n;i+=64){md5cycle(state,md5blk_array(a.subarray(i-64,i)))}a=i-64<n?a.subarray(i-64):new Uint8Array(0);length=a.length;tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]Line truncated