mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 10:19:57 +00:00
Single LED (P0.28) doubles as heartbeat, message flash, shutdown flash and LoRa TX indicator, all sharing the same physical pin. Adds one PWM channel on that pin (heartbeat-pwm-led/lora-tx-pwm-led aliases) and a single 0-100 brightness knob (get/set led) covering every one of those events at once, deliberately RAM-only: resets to a 10% default on every reboot regardless of CLI changes made in a previous session. The existing persisted on/off gate (set leds on|off) is untouched. Boards without heartbeat-pwm-led/lora-tx-pwm-led aliases keep their current plain-GPIO behavior unchanged.
482 lines
15 KiB
C
482 lines
15 KiB
C
/*
|
||
* ZephCore - UI Common
|
||
* Copyright (c) 2025 ZephCore
|
||
* SPDX-License-Identifier: MIT
|
||
*
|
||
* Shared ui_task.h implementations that are identical across all UI variants.
|
||
* Compiled for every build that includes any UI (button, joystick, or future).
|
||
*/
|
||
|
||
#include "ui_task.h"
|
||
|
||
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
|
||
#include "buzzer.h"
|
||
#endif
|
||
|
||
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
|
||
#include "display.h"
|
||
#endif
|
||
|
||
#include <zephyr_poweroff.h>
|
||
#include "led_gate.h" /* shared with the LoRa TX LED */
|
||
|
||
#include <zephyr/drivers/gpio.h>
|
||
#include <zephyr/drivers/pwm.h>
|
||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||
#include <zephyr/kernel.h>
|
||
#include <string.h>
|
||
|
||
#include <zephyr/logging/log.h>
|
||
LOG_MODULE_REGISTER(ui_led, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
|
||
|
||
/* ========== Input axis flip ==========
|
||
* Shared by both UI variants. Written from the mesh/CLI thread, read from the
|
||
* input callback; a plain bool is enough. */
|
||
|
||
static bool input_flipped;
|
||
|
||
void zephcore_input_set_flipped(bool flipped)
|
||
{
|
||
input_flipped = flipped;
|
||
}
|
||
|
||
bool zephcore_input_is_flipped(void)
|
||
{
|
||
return input_flipped;
|
||
}
|
||
|
||
uint16_t zephcore_input_map_code(uint16_t code)
|
||
{
|
||
if (!input_flipped) {
|
||
return code;
|
||
}
|
||
|
||
switch (code) {
|
||
case INPUT_KEY_UP: return INPUT_KEY_DOWN;
|
||
case INPUT_KEY_DOWN: return INPUT_KEY_UP;
|
||
case INPUT_KEY_LEFT: return INPUT_KEY_RIGHT;
|
||
case INPUT_KEY_RIGHT: return INPUT_KEY_LEFT;
|
||
default: return code;
|
||
}
|
||
}
|
||
|
||
/* ========== Startup Chime ========== */
|
||
|
||
void ui_play_startup_chime(void)
|
||
{
|
||
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
|
||
if (!buzzer_is_quiet()) {
|
||
buzzer_play(MELODY_STARTUP);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/* ========== LED Heartbeat ========== */
|
||
/*
|
||
* Heartbeat on led0 (or led1). The pulse widens while there are unread
|
||
* messages; with both LEDs present, companions light led1 for unread.
|
||
*/
|
||
|
||
/* PWM path (brightness-capable, shared with the LoRa TX LED via the same
|
||
* DT_ALIAS(heartbeat_pwm_led)/DT_ALIAS(lora_tx_pwm_led) pair when a board's
|
||
* heartbeat and TX indicator are the same physical LED, e.g. T096) takes
|
||
* priority over the plain digital led0/led1 fallback. Brightness comes from
|
||
* helpers/led_gate.h — one dimmer for every event on this LED, not a
|
||
* heartbeat-only one. */
|
||
#if DT_NODE_EXISTS(DT_ALIAS(heartbeat_pwm_led))
|
||
static const struct pwm_dt_spec s_heartbeat_led_pwm =
|
||
PWM_DT_SPEC_GET(DT_ALIAS(heartbeat_pwm_led));
|
||
#define HAS_HEARTBEAT_LED_PWM 1
|
||
#define HAS_HEARTBEAT_LED 0
|
||
#elif DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios)
|
||
static const struct gpio_dt_spec s_heartbeat_led =
|
||
GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
|
||
#define HAS_HEARTBEAT_LED_PWM 0
|
||
#define HAS_HEARTBEAT_LED 1
|
||
#elif DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios)
|
||
static const struct gpio_dt_spec s_heartbeat_led =
|
||
GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
|
||
#define HAS_HEARTBEAT_LED_PWM 0
|
||
#define HAS_HEARTBEAT_LED 1
|
||
#else
|
||
#define HAS_HEARTBEAT_LED_PWM 0
|
||
#define HAS_HEARTBEAT_LED 0
|
||
#endif
|
||
|
||
/* "Some heartbeat LED exists, PWM or plain GPIO" — used where the on/off
|
||
* write differs but everything else (work queue plumbing, readiness check)
|
||
* is identical either way. */
|
||
#define HAS_ANY_HEARTBEAT_LED (HAS_HEARTBEAT_LED_PWM || HAS_HEARTBEAT_LED)
|
||
|
||
#if HAS_HEARTBEAT_LED_PWM
|
||
static inline bool heartbeat_led_is_ready(void)
|
||
{
|
||
return pwm_is_ready_dt(&s_heartbeat_led_pwm);
|
||
}
|
||
static inline void heartbeat_led_write(bool on)
|
||
{
|
||
uint32_t pulse = on ? (uint32_t)((uint64_t)s_heartbeat_led_pwm.period *
|
||
zephcore_led_brightness_pct() / 100)
|
||
: 0;
|
||
pwm_set_pulse_dt(&s_heartbeat_led_pwm, pulse);
|
||
}
|
||
#elif HAS_HEARTBEAT_LED
|
||
static inline bool heartbeat_led_is_ready(void)
|
||
{
|
||
return gpio_is_ready_dt(&s_heartbeat_led);
|
||
}
|
||
static inline void heartbeat_led_write(bool on)
|
||
{
|
||
gpio_pin_set_dt(&s_heartbeat_led, on ? 1 : 0);
|
||
}
|
||
#endif
|
||
|
||
/* Second LED for unread-message indication. Repeaters use led1 for LoRa TX
|
||
* (via lora-tx-led alias) — no offline queue, so this is companion-only. */
|
||
#if HAS_ANY_HEARTBEAT_LED && DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) && \
|
||
DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) && !defined(ZEPHCORE_REPEATER)
|
||
static const struct gpio_dt_spec s_msg_led =
|
||
GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
|
||
#define HAS_MSG_LED 1
|
||
#else
|
||
#define HAS_MSG_LED 0
|
||
#endif
|
||
|
||
#define LED_CYCLE_MS 4000 /* Total heartbeat period */
|
||
#define LED_ON_MS 20 /* Normal pulse width */
|
||
#define LED_ON_MSG_MS 200 /* Pulse width when unread messages */
|
||
|
||
#if HAS_ANY_HEARTBEAT_LED
|
||
static struct k_work_delayable s_led_on_work;
|
||
static struct k_work_delayable s_led_off_work;
|
||
|
||
/*
|
||
* Weak: returns current unread message count for pulse-width adaptation.
|
||
* Overridden by ui_task.c (button UI) to read from ui_state.
|
||
* Joystick UI leaves this at 0 — it drives its own message display.
|
||
*/
|
||
__attribute__((weak)) uint16_t ui_led_get_msg_count(void) { return 0; }
|
||
|
||
/*
|
||
* Does the heartbeat LED light on this pass? The cycle keeps running in
|
||
* every mode, LEDS_HB_OFF included, because the unread indicator is lit from
|
||
* inside it.
|
||
*/
|
||
static bool hb_should_light(uint16_t msg_count)
|
||
{
|
||
switch (zephcore_leds_hb_mode()) {
|
||
case LEDS_HB_OFF: return false;
|
||
case LEDS_HB_UNREAD: return msg_count > 0; /* dark unless there is news */
|
||
default: return true; /* LEDS_HB_ALL, LEDS_HB_HB */
|
||
}
|
||
}
|
||
|
||
/* Pulse width for this pass. LEDS_HB_HB is the "liveness tick only" mode, so
|
||
* it never widens even when messages are waiting. */
|
||
static uint16_t hb_pulse_ms(uint16_t msg_count)
|
||
{
|
||
if (zephcore_leds_hb_mode() == LEDS_HB_HB) {
|
||
return LED_ON_MS;
|
||
}
|
||
return (msg_count > 0) ? LED_ON_MSG_MS : LED_ON_MS;
|
||
}
|
||
|
||
static void led_off_work_handler(struct k_work *work)
|
||
{
|
||
ARG_UNUSED(work);
|
||
/* Yield the pin if radio activity is holding it (shared-pin boards only;
|
||
* everywhere else this always reads false). Clearing here would blank the
|
||
* LED in the middle of a transmit. Routed through heartbeat_led_write()
|
||
* rather than a direct gpio_pin_set_dt(), since s_heartbeat_led doesn't
|
||
* even exist on PWM-capable boards (see HAS_HEARTBEAT_LED_PWM above). */
|
||
if (!zephcore_led_radio_holds_pin()) {
|
||
heartbeat_led_write(false);
|
||
}
|
||
#if HAS_MSG_LED
|
||
gpio_pin_set_dt(&s_msg_led, 0);
|
||
#endif
|
||
uint16_t on_ms = hb_pulse_ms(ui_led_get_msg_count());
|
||
|
||
k_work_reschedule(&s_led_on_work, K_MSEC(LED_CYCLE_MS - on_ms));
|
||
}
|
||
|
||
static void led_on_work_handler(struct k_work *work)
|
||
{
|
||
ARG_UNUSED(work);
|
||
uint16_t mc = ui_led_get_msg_count();
|
||
uint16_t on_ms = hb_pulse_ms(mc);
|
||
|
||
if (!zephcore_leds_disabled()) {
|
||
if (hb_should_light(mc) && !zephcore_led_radio_holds_pin()) {
|
||
heartbeat_led_write(true);
|
||
}
|
||
#if HAS_MSG_LED
|
||
/* The unread LED is a separate pin, so it is governed by the mode
|
||
* but not by the radio's hold on the heartbeat pin. LEDS_HB_HB is
|
||
* the liveness-only mode and deliberately suppresses it. */
|
||
if (mc > 0 && zephcore_leds_hb_mode() != LEDS_HB_OFF &&
|
||
zephcore_leds_hb_mode() != LEDS_HB_HB) {
|
||
gpio_pin_set_dt(&s_msg_led, 1);
|
||
}
|
||
#endif
|
||
}
|
||
k_work_reschedule(&s_led_off_work, K_MSEC(on_ms));
|
||
}
|
||
#endif /* HAS_ANY_HEARTBEAT_LED */
|
||
|
||
/*
|
||
* Weak: called after s_leds_disabled changes so each UI variant can sync its
|
||
* own display state. Overridden by ui_task.c (button UI) to update
|
||
* ui_state->leds_disabled so the LEDs page shows the correct toggle state.
|
||
*/
|
||
__attribute__((weak)) void ui_led_on_disabled_changed(bool disabled) { ARG_UNUSED(disabled); }
|
||
|
||
void ui_led_heartbeat_init(void)
|
||
{
|
||
#if HAS_ANY_HEARTBEAT_LED
|
||
if (heartbeat_led_is_ready()) {
|
||
#if HAS_HEARTBEAT_LED
|
||
gpio_pin_configure_dt(&s_heartbeat_led, GPIO_OUTPUT_INACTIVE);
|
||
#else
|
||
heartbeat_led_write(false);
|
||
#endif
|
||
k_work_init_delayable(&s_led_on_work, led_on_work_handler);
|
||
k_work_init_delayable(&s_led_off_work, led_off_work_handler);
|
||
k_work_reschedule(&s_led_on_work, K_NO_WAIT);
|
||
LOG_INF("LED heartbeat started");
|
||
}
|
||
#if HAS_MSG_LED
|
||
if (gpio_is_ready_dt(&s_msg_led)) {
|
||
gpio_pin_configure_dt(&s_msg_led, GPIO_OUTPUT_INACTIVE);
|
||
LOG_INF("msg LED ready");
|
||
}
|
||
#endif
|
||
#endif
|
||
}
|
||
|
||
void ui_set_heartbeat_led(bool enabled)
|
||
{
|
||
#if HAS_ANY_HEARTBEAT_LED
|
||
if (enabled && !zephcore_leds_disabled()) {
|
||
if (heartbeat_led_is_ready()) {
|
||
k_work_reschedule(&s_led_on_work, K_NO_WAIT);
|
||
}
|
||
} else {
|
||
k_work_cancel_delayable(&s_led_on_work);
|
||
k_work_cancel_delayable(&s_led_off_work);
|
||
heartbeat_led_write(false);
|
||
#if HAS_MSG_LED
|
||
gpio_pin_set_dt(&s_msg_led, 0);
|
||
#endif
|
||
}
|
||
#else
|
||
(void)enabled;
|
||
#endif
|
||
}
|
||
|
||
/*
|
||
* Strong override of the weak hook in led_gate.c: react to a gate change from
|
||
* anywhere (UI toggle, "set leds", boot). Stops or restarts the heartbeat cycle
|
||
* and refreshes the UI's LED page. The gate flag itself is already set by the
|
||
* time we get here — do NOT call back into ui_set_leds_disabled() from here.
|
||
*/
|
||
void zephcore_leds_ui_sync(bool disabled)
|
||
{
|
||
#if HAS_ANY_HEARTBEAT_LED
|
||
if (disabled) {
|
||
k_work_cancel_delayable(&s_led_on_work);
|
||
k_work_cancel_delayable(&s_led_off_work);
|
||
heartbeat_led_write(false);
|
||
#if HAS_MSG_LED
|
||
gpio_pin_set_dt(&s_msg_led, 0);
|
||
#endif
|
||
} else if (!k_work_delayable_is_pending(&s_led_on_work) &&
|
||
!k_work_delayable_is_pending(&s_led_off_work)) {
|
||
/* Restart heartbeat only if it was stopped (avoids spurious pulse) */
|
||
if (heartbeat_led_is_ready()) {
|
||
k_work_reschedule(&s_led_on_work, K_NO_WAIT);
|
||
}
|
||
}
|
||
#else
|
||
(void)disabled;
|
||
#endif
|
||
ui_led_on_disabled_changed(disabled);
|
||
}
|
||
|
||
/* UI-facing spelling of the same thing. Kept because the UI toggle pages and
|
||
* the companion boot path call it by this name; the gate is what actually
|
||
* governs every LED. */
|
||
void ui_set_leds_disabled(bool disabled)
|
||
{
|
||
zephcore_leds_set_disabled(disabled);
|
||
}
|
||
|
||
/* Flash the heartbeat LED immediately on message receipt.
|
||
* Cancels the current cycle, pulses at LED_ON_MSG_MS width, then the
|
||
* work chain resumes the normal heartbeat automatically.
|
||
* No-op when LEDs are disabled or hardware is absent. */
|
||
void ui_led_flash_msg(void)
|
||
{
|
||
#if HAS_ANY_HEARTBEAT_LED
|
||
if (!zephcore_leds_disabled() && heartbeat_led_is_ready()) {
|
||
k_work_cancel_delayable(&s_led_on_work);
|
||
k_work_cancel_delayable(&s_led_off_work);
|
||
heartbeat_led_write(true);
|
||
k_work_reschedule(&s_led_off_work, K_MSEC(LED_ON_MSG_MS));
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/* Flash the heartbeat LED 3 times on shutdown.
|
||
* Used as a visual power-off indicator when the buzzer is muted.
|
||
* Suppressed by "set leds off" — a node the user asked to keep dark stays dark
|
||
* even at power-off. */
|
||
void ui_led_flash_shutdown(void)
|
||
{
|
||
#if HAS_ANY_HEARTBEAT_LED
|
||
if (!zephcore_leds_disabled() && heartbeat_led_is_ready()) {
|
||
for (int i = 0; i < 3; i++) {
|
||
heartbeat_led_write(true);
|
||
k_sleep(K_MSEC(100));
|
||
heartbeat_led_write(false);
|
||
if (i < 2) {
|
||
k_sleep(K_MSEC(100));
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/* ========== Battery refresh ==========
|
||
* Lazy: render path calls ui_refresh_battery(); ADC only fires when the
|
||
* cached reading is older than UI_BATT_REFRESH_MS. Telemetry / stats paths
|
||
* read fresh directly via their own callbacks — this gate only governs the
|
||
* local display. */
|
||
#define UI_BATT_REFRESH_MS 30000
|
||
|
||
static uint16_t (*s_batt_provider)(void);
|
||
/* The board's own percentage: its discharge curve, or the fuel gauge. */
|
||
static uint8_t (*s_batt_pct_provider)(void);
|
||
static uint32_t s_batt_last_read_ms;
|
||
static bool s_batt_ever_read;
|
||
|
||
void ui_set_battery_provider(uint16_t (*mv)(void), uint8_t (*pct)(void))
|
||
{
|
||
s_batt_provider = mv;
|
||
s_batt_pct_provider = pct;
|
||
}
|
||
|
||
void ui_refresh_battery(void)
|
||
{
|
||
if (!s_batt_provider) {
|
||
return;
|
||
}
|
||
uint32_t now = k_uptime_get_32();
|
||
if (s_batt_ever_read && (now - s_batt_last_read_ms) < UI_BATT_REFRESH_MS) {
|
||
return;
|
||
}
|
||
uint16_t mv = s_batt_provider();
|
||
ui_set_battery(mv, (mv && s_batt_pct_provider) ? s_batt_pct_provider() : 0);
|
||
s_batt_last_read_ms = k_uptime_get_32();
|
||
s_batt_ever_read = true;
|
||
}
|
||
|
||
/* Forget the freshness timestamp so the next ui_refresh_battery() call is
|
||
* guaranteed to sample the ADC. Use when entering a state where a fresh
|
||
* reading matters (e.g. just woke the screen from sleep). */
|
||
void ui_invalidate_battery_cache(void)
|
||
{
|
||
s_batt_ever_read = false;
|
||
s_batt_last_read_ms = 0;
|
||
}
|
||
|
||
/* ========== Power-off: the UI's part ==========
|
||
* zephcore_power_off() (adapters/board/zephyr_poweroff.c) runs this first,
|
||
* then powers the board down. */
|
||
void ui_before_power_off(void)
|
||
{
|
||
/* Stop heartbeat LED cycle (cancels both works). */
|
||
ui_set_heartbeat_led(false);
|
||
|
||
/* Display off — content stays visible on EPD, blanks on OLED. */
|
||
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
|
||
mc_display_off();
|
||
#endif
|
||
}
|
||
|
||
/* ========== Low-battery warning screen ==========
|
||
* The policy itself is app/PowerPolicy.cpp. */
|
||
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
|
||
void ui_show_low_battery(bool hold)
|
||
{
|
||
/* Wake the panel (OLED may be blanked by auto-off; EPD is always
|
||
* visible). Centre two lines; the message persists on e-paper after
|
||
* power is cut, so e-paper needs no hold delay. */
|
||
mc_display_on();
|
||
mc_display_clear();
|
||
|
||
const char *l1 = "Low Battery";
|
||
const char *l2 = "Shutting Down";
|
||
uint16_t w = mc_display_width();
|
||
uint8_t fw = mc_display_font_width();
|
||
uint8_t fh = mc_display_font_height();
|
||
int x1 = (fw && w) ? ((int)w - (int)strlen(l1) * fw) / 2 : 0;
|
||
int x2 = (fw && w) ? ((int)w - (int)strlen(l2) * fw) / 2 : 0;
|
||
if (x1 < 0) x1 = 0;
|
||
if (x2 < 0) x2 = 0;
|
||
int y1 = (int)mc_display_height() / 2 - (int)fh;
|
||
if (y1 < 0) y1 = 0;
|
||
|
||
mc_display_text(x1, y1, l1, false);
|
||
mc_display_text(x2, y1 + fh + 2, l2, false);
|
||
mc_display_finalize();
|
||
|
||
/* OLED blanks the instant power drops, so hold long enough to read it.
|
||
* EPD keeps the image with no power, so skip the delay. The deferred-
|
||
* poweroff (grace) path passes hold=false: it must NOT block the main
|
||
* thread, because that thread has to service the app's message fetch
|
||
* during the grace window — the grace timer provides the on-screen dwell
|
||
* instead. */
|
||
if (hold && !mc_display_is_epd()) {
|
||
k_sleep(K_MSEC(3000));
|
||
}
|
||
}
|
||
#else
|
||
void ui_show_low_battery(bool hold)
|
||
{
|
||
ARG_UNUSED(hold);
|
||
}
|
||
#endif
|
||
|
||
/* ========== Shared splash logo ==========
|
||
* 128×13 ZephCore wordmark, MSB-first row-major (Adafruit XBM/drawBitmap
|
||
* format). Used by both UI variants' splash renders via mc_display_xbm(). */
|
||
const uint8_t zephcore_logo[] = {
|
||
0x00, 0x01, 0xff, 0x7f, 0xe7, 0xf8, 0x70, 0x70,
|
||
0x3c, 0x01, 0xe0, 0x7f, 0xc3, 0xff, 0x00, 0x00,
|
||
0x00, 0x01, 0xff, 0x7f, 0xe7, 0xfc, 0x70, 0x70,
|
||
0xff, 0x07, 0xf8, 0x7f, 0xe3, 0xff, 0x00, 0x00,
|
||
0x00, 0x01, 0xff, 0x7f, 0xe7, 0xfe, 0x70, 0x71,
|
||
0xff, 0x0f, 0xfc, 0x7f, 0xf3, 0xff, 0x00, 0x00,
|
||
0x00, 0x00, 0x0e, 0x70, 0x07, 0x0e, 0x70, 0x71,
|
||
0xc7, 0x8e, 0x1c, 0x70, 0x73, 0x80, 0x00, 0x00,
|
||
0x00, 0x00, 0x1c, 0x70, 0x07, 0x0e, 0x70, 0x73,
|
||
0x83, 0x1c, 0x0e, 0x70, 0x73, 0x80, 0x00, 0x00,
|
||
0x00, 0x00, 0x38, 0x7f, 0xe7, 0xfe, 0x7f, 0xf3,
|
||
0x80, 0x1c, 0x0e, 0x7f, 0xf3, 0xff, 0x00, 0x00,
|
||
0x00, 0x00, 0x78, 0x7f, 0xe7, 0xfc, 0x7f, 0xf3,
|
||
0x80, 0x1c, 0x0e, 0x7f, 0xe3, 0xff, 0x00, 0x00,
|
||
0x00, 0x00, 0x70, 0x7f, 0xe7, 0xf8, 0x7f, 0xf3,
|
||
0x80, 0x1c, 0x0e, 0x7f, 0x83, 0xff, 0x00, 0x00,
|
||
0x00, 0x00, 0xe0, 0x70, 0x07, 0x00, 0x70, 0x73,
|
||
0x83, 0x1c, 0x0e, 0x73, 0xc3, 0x80, 0x00, 0x00,
|
||
0x00, 0x01, 0xc0, 0x70, 0x07, 0x00, 0x70, 0x71,
|
||
0xc7, 0x8e, 0x1c, 0x71, 0xe3, 0x80, 0x00, 0x00,
|
||
0x00, 0x03, 0xff, 0x7f, 0xe7, 0x00, 0x70, 0x71,
|
||
0xff, 0x0f, 0xfc, 0x70, 0xe3, 0xff, 0x00, 0x00,
|
||
0x00, 0x03, 0xff, 0x7f, 0xe7, 0x00, 0x70, 0x70,
|
||
0xff, 0x07, 0xf8, 0x70, 0xf3, 0xff, 0x00, 0x00,
|
||
0x00, 0x03, 0xff, 0x7f, 0xe7, 0x00, 0x70, 0x70,
|
||
0x3c, 0x01, 0xe0, 0x70, 0x7b, 0xff, 0x00, 0x00,
|
||
};
|