@geastack/core 0.1.42 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ #pragma once
2
+ #include <array>
3
+ #include <cstddef>
4
+ #include <cstdint>
5
+
6
+ namespace gea::platform::espnow {
7
+ struct Datagram {
8
+ std::array<std::uint8_t, 6> source{};
9
+ std::array<std::uint8_t, 1470> bytes{};
10
+ std::size_t size = 0;
11
+ std::int64_t receivedUs = 0;
12
+ int rssi = 0;
13
+ // Raw 32-bit receive clock in microseconds, not the esp_timer epoch. The
14
+ // SDK qualifies its precision only without modem/light sleep. Zero is a
15
+ // valid timestamp; absence of RX metadata is represented separately.
16
+ std::uint32_t radioTimestampUs = 0;
17
+ bool radioTimestampValid = false;
18
+ };
19
+ struct Statistics {
20
+ std::uint32_t sent = 0, failed = 0, received = 0, dropped = 0;
21
+ std::uint32_t maximumSendUs = 0;
22
+ // Last completed datagram, published before canSend() becomes true. The
23
+ // single sending task may read these after canSend() for its prior send.
24
+ std::uint64_t lastSendStartedUs = 0, lastSendCompletedUs = 0;
25
+ std::uint32_t lastSendRate = 0, lastSendStatus = 0;
26
+ // Optional diagnostic copy of the SDK callback's 24-byte Action MAC header.
27
+ // Validity is separate: zero fields are valid values, and Retry=0 does not
28
+ // prove that the hardware made only one transmission attempt.
29
+ std::uint16_t lastSendFrameControl = 0, lastSendDuration = 0,
30
+ lastSendSequenceControl = 0;
31
+ bool lastSendMacValid = false;
32
+ };
33
+ } // namespace gea::platform::espnow
34
+ extern "C" {
35
+ bool gea_espnow_open(int channel, int rate) __attribute__((weak));
36
+ void gea_espnow_close() __attribute__((weak));
37
+ bool gea_espnow_is_open() __attribute__((weak));
38
+ bool gea_espnow_can_send() __attribute__((weak));
39
+ bool gea_espnow_capture_tx_header(bool enabled) __attribute__((weak));
40
+ bool gea_espnow_set_max_tx_power(int quarterDbm) __attribute__((weak));
41
+ int gea_espnow_get_max_tx_power() __attribute__((weak));
42
+ bool gea_espnow_peer(const std::uint8_t *mac, const std::uint8_t *key)
43
+ __attribute__((weak));
44
+ int gea_espnow_send(const std::uint8_t *mac, const void *data, std::size_t size)
45
+ __attribute__((weak));
46
+ bool gea_espnow_receive(gea::platform::espnow::Datagram *)
47
+ __attribute__((weak));
48
+ void gea_espnow_wake() __attribute__((weak));
49
+ void gea_espnow_wait(unsigned ms) __attribute__((weak));
50
+ void gea_espnow_statistics(gea::platform::espnow::Statistics *)
51
+ __attribute__((weak));
52
+ }
53
+ namespace gea::platform::espnow {
54
+ class EspNow {
55
+ public:
56
+ // Channel zero borrows the running Wi-Fi radio without changing its channel.
57
+ // Otherwise claims an idle radio. Rate is 24/36/54 Mbps (OFDM), 65 (HT20
58
+ // MCS7), or 115 on ESP32-S31 (HE20 MCS9 short GI, nominal 114.7 Mbps).
59
+ // Unsupported rates/protocols fail; a borrowed radio is never reconfigured.
60
+ static bool open(int channel, int rate = 54) {
61
+ return gea_espnow_open && gea_espnow_open(channel, rate);
62
+ }
63
+ static bool isOpen() { return gea_espnow_is_open && gea_espnow_is_open(); }
64
+ static bool canSend() { return gea_espnow_can_send && gea_espnow_can_send(); }
65
+ // Native diagnostics only; default off and reset on radio close/open.
66
+ // Enable only on an owned radio. Copies metadata, never changes the frame.
67
+ static bool captureTxHeader(bool enabled) {
68
+ return gea_espnow_capture_tx_header &&
69
+ gea_espnow_capture_tx_header(enabled);
70
+ }
71
+ // Quarter-dBm cap, 8..84. Only an owned radio may be changed; the SDK
72
+ // quantizes the request. Read back the actual cap, or -1 if unavailable.
73
+ static bool setMaxTxPower(int quarterDbm) {
74
+ return gea_espnow_set_max_tx_power &&
75
+ gea_espnow_set_max_tx_power(quarterDbm);
76
+ }
77
+ static int getMaxTxPower() {
78
+ return gea_espnow_get_max_tx_power ? gea_espnow_get_max_tx_power() : -1;
79
+ }
80
+ static void close() {
81
+ if (gea_espnow_close)
82
+ gea_espnow_close();
83
+ }
84
+ static bool addPeer(const std::uint8_t *mac,
85
+ const std::uint8_t *key = nullptr) {
86
+ return gea_espnow_peer && gea_espnow_peer(mac, key);
87
+ }
88
+ // Copies one datagram; length on admission, zero when busy, -1 on error.
89
+ // MAC completion is NOT an application acknowledgement.
90
+ static int send(const std::uint8_t *mac, const void *bytes,
91
+ std::size_t size) {
92
+ return gea_espnow_send ? gea_espnow_send(mac, bytes, size) : -1;
93
+ }
94
+ static bool receive(Datagram &packet) {
95
+ return gea_espnow_receive && gea_espnow_receive(&packet);
96
+ }
97
+ // One native consumer. Wakes for RX or TX completion; never call from the UI.
98
+ static void wake() {
99
+ if (gea_espnow_wake)
100
+ gea_espnow_wake();
101
+ }
102
+ static void wait(unsigned ms) {
103
+ if (gea_espnow_wait)
104
+ gea_espnow_wait(ms);
105
+ }
106
+ static Statistics statistics() {
107
+ Statistics s{};
108
+ if (gea_espnow_statistics)
109
+ gea_espnow_statistics(&s);
110
+ return s;
111
+ }
112
+ };
113
+ } // namespace gea::platform::espnow
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ #pragma once
3
+
4
+ #include <cstddef>
5
+ #include <cstdint>
6
+
7
+ namespace gea::platform::sensors {
8
+ struct MagnetometerReading {
9
+ // Compensated whole microtesla. BMM150 overflow sentinels are preserved;
10
+ // saturatedAxes identifies axes that must not be used as field strength.
11
+ std::int16_t x = 0, y = 0, z = 0;
12
+ bool valid = false;
13
+ std::uint8_t saturatedAxes = 0; // X=1, Y=2, Z=4.
14
+ std::uint64_t timestampUs = 0;
15
+ };
16
+ } // namespace gea::platform::sensors
17
+
18
+ extern "C" {
19
+ bool gea_magnetometer_open() __attribute__((weak));
20
+ void gea_magnetometer_close() __attribute__((weak));
21
+ unsigned gea_magnetometer_count() __attribute__((weak));
22
+ bool gea_magnetometer_read(unsigned,
23
+ gea::platform::sensors::MagnetometerReading *)
24
+ __attribute__((weak));
25
+ bool gea_magnetometer_read_all(gea::platform::sensors::MagnetometerReading *,
26
+ std::size_t) __attribute__((weak));
27
+ }
28
+
29
+ namespace gea::platform::sensors {
30
+ class Magnetometer {
31
+ public:
32
+ // Idempotent. No bus or pins are claimed until open is called.
33
+ static bool open() {
34
+ return gea_magnetometer_open && gea_magnetometer_open();
35
+ }
36
+ static void close() {
37
+ if (gea_magnetometer_close)
38
+ gea_magnetometer_close();
39
+ }
40
+ static unsigned count() {
41
+ return gea_magnetometer_count ? gea_magnetometer_count() : 0;
42
+ }
43
+ static bool read(unsigned index, MagnetometerReading &sample) {
44
+ sample = {};
45
+ return gea_magnetometer_read && gea_magnetometer_read(index, &sample);
46
+ }
47
+ // Reads the board's sensors back-to-back under one driver lock, with a
48
+ // shared completion timestamp. This is not hardware-triggered sampling.
49
+ // False if capacity is insufficient or any sensor failed; inspect valid
50
+ // on individual readings. The output is cleared even when unsupported.
51
+ static bool readAll(MagnetometerReading *samples, std::size_t capacity) {
52
+ if (!samples)
53
+ return false;
54
+ for (std::size_t i = 0; i < capacity; ++i)
55
+ samples[i] = {};
56
+ return gea_magnetometer_read_all &&
57
+ gea_magnetometer_read_all(samples, capacity);
58
+ }
59
+ };
60
+ } // namespace gea::platform::sensors
package/include/runtime.h CHANGED
@@ -67,6 +67,20 @@ public:
67
67
  // calls it, so a target that does not care never sees a difference.
68
68
  static bool boot(const RuntimeOptions &options);
69
69
 
70
+ // Pause UI event/frame callbacks at a safe event-loop boundary. A request
71
+ // made before boot is retained; uiPaused() becomes true only after boot and
72
+ // the current callback/frame (including its locks) have returned. Calls do
73
+ // not wait and may be made from another task. uiPaused() acknowledges the
74
+ // current request, so callers that need quiescence must wait for it.
75
+ //
76
+ // Paused input is discarded and gestures are reset. Resuming requests a fresh
77
+ // frame. Platform tickers, device control, native services and previously
78
+ // submitted display DMA keep running; this does not suspend any task. A
79
+ // headless build has no UI loop, so a pause is acknowledged immediately.
80
+ static void requestUiPause(bool paused);
81
+ static bool uiPauseRequested();
82
+ static bool uiPaused();
83
+
70
84
  // Hands the runtime a block of internal RAM to hold for the display and free
71
85
  // immediately before Display::init(). For an app whose native boot hook has
72
86
  // a phase that must not see less RAM than it actually has, followed by one
package/index.d.ts CHANGED
@@ -103,9 +103,20 @@ export interface GeaIntrinsicElements {
103
103
  'virtual-list': NativeVirtualListProps
104
104
  }
105
105
 
106
+ export interface WindowEventListenerOptions {
107
+ capture?: boolean
108
+ once?: boolean
109
+ }
110
+ export interface GeaWindowEventMap extends GeaEventMap {
111
+ blur: Event
112
+ resize: Event
113
+ }
114
+ /** Native app-window event authority. No global input posting or browser Window privileges. */
106
115
  export interface GeaWindow {
107
116
  readonly innerWidth: number
108
117
  readonly innerHeight: number
118
+ addEventListener<K extends keyof GeaWindowEventMap>(type: K, listener: (event: GeaWindowEventMap[K]) => void, options?: boolean | WindowEventListenerOptions): void
119
+ removeEventListener<K extends keyof GeaWindowEventMap>(type: K, listener: (event: GeaWindowEventMap[K]) => void, options?: boolean | WindowEventListenerOptions): void
109
120
  }
110
121
 
111
122
  declare global {
@@ -140,6 +151,10 @@ declare global {
140
151
  /** @gea-host-no-property-writes */
141
152
  function requestAnimationFrame(callback: (timestampMs: int) => void): number
142
153
 
154
+ // Shares the native Promise-job FIFO; app replacement retires pending jobs.
155
+ /** @gea-host-no-property-writes */
156
+ function queueMicrotask(callback: () => void): void
157
+
143
158
  // Native one-shot / repeating timers (gea::host::setTimeout/setInterval,
144
159
  // engine/../host/include/host/timers.h), NOT Node's `NodeJS.Timeout` (there is
145
160
  // no `dom` or `node` lib here, so without this declaration a `setTimeout`
@@ -339,6 +354,7 @@ export interface DataAttributes {
339
354
  }
340
355
 
341
356
  export interface Event {
357
+ readonly defaultPrevented: boolean
342
358
  readonly type: string
343
359
  readonly target: EventTarget
344
360
  readonly currentTarget: EventTarget
@@ -354,12 +370,30 @@ export interface EventTarget {
354
370
  addEventListener(type: string, listener: (event: Event) => void): void
355
371
  }
356
372
 
373
+ /** Live child collection; iterable and bracket protocols are not published yet. */
374
+ export interface NodeList {
375
+ readonly length: number
376
+ item(index: number): Node | null
377
+ }
378
+
357
379
  export interface Node extends EventTarget {
358
- readonly nodeType?: number
380
+ /** True only for a live allocation in the current mounted document subtree. */
381
+ readonly isConnected: boolean
382
+ readonly nodeType: number
383
+ readonly parentNode: Node | null
384
+ /** The current first child, including text/comments; null when empty. */
385
+ readonly firstChild: Node | null
386
+ readonly childNodes: NodeList
359
387
  appendChild(child: Node): void
388
+ /** Move a live node before a direct child; null appends. Return the inserted node. */
389
+ insertBefore(child: Node, reference: Node | null): Node
390
+ /** Detach a direct live child and return that same allocation, without retiring it. */
391
+ removeChild(child: Node): Node
360
392
  remove(): void
361
393
  }
362
394
 
395
+ export interface FocusOptions { preventScroll?: boolean }
396
+
363
397
  export interface ScrollIntoViewOptions {
364
398
  behavior?: 'auto' | 'instant' | 'smooth'
365
399
  block?: 'start' | 'center' | 'end' | 'nearest'
@@ -374,6 +408,12 @@ export interface DOMTokenList {
374
408
  }
375
409
 
376
410
  export interface Element extends Node {
411
+ readonly lastElementChild: Element | null
412
+ /** Bounded native selector grammar; malformed/unsupported selectors refuse. */
413
+ matches(selector: string): boolean
414
+ closest<E extends Element = Element>(selector: string): E | null
415
+ querySelector(selector: string): Element | null
416
+ querySelectorAll(selector: string): readonly Element[]
377
417
  className?: string
378
418
  readonly children: readonly Element[]
379
419
  scrollLeft?: number
@@ -395,10 +435,24 @@ export interface Element extends Node {
395
435
  // `blur()` clears it again (and does nothing when this element was not the
396
436
  // one focused). Backed by `NodeHandle::focus()`/`blur()` over
397
437
  // `Tree::setActiveInput` (engine/ui/node.cpp).
398
- focus(): void
438
+ focus(options?: FocusOptions): void
399
439
  blur(): void
400
440
  }
401
441
 
442
+ /** Minimal native text input backed by a live NodeHandle allocation.
443
+ * Value uses the native value attribute; HTML dirty-value/defaultValue semantics
444
+ * and checked/type/selection APIs are not declared by this carrier. */
445
+ export interface InputElement extends Element {
446
+ disabled: boolean
447
+ value: string
448
+ }
449
+
450
+ /** Native button attributes backed by the live node allocation. */
451
+ export interface ButtonElement extends Element {
452
+ disabled: boolean
453
+ value: string
454
+ }
455
+
402
456
  export interface VirtualListElement extends Element {
403
457
  scrollTop: number
404
458
  /**
@@ -412,15 +466,22 @@ export interface VirtualListElement extends Element {
412
466
  }
413
467
 
414
468
  export interface Document {
469
+ addEventListener<K extends keyof GeaEventMap>(type: K, listener: (event: GeaEventMap[K]) => void, options?: boolean | WindowEventListenerOptions): void
470
+ removeEventListener<K extends keyof GeaEventMap>(type: K, listener: (event: GeaEventMap[K]) => void, options?: boolean | WindowEventListenerOptions): void
415
471
  readonly body: Element
472
+ /** Actual connected native focus, null when no owned responder is focused. */
473
+ readonly activeElement: Element | null
416
474
  getElementById(id: string): Element | null
417
475
  querySelector(selector: string): Element | null
418
476
  querySelectorAll(selector: string): readonly Element[]
477
+ createElement(tag: 'input'): InputElement
478
+ createElement(tag: 'button'): ButtonElement
419
479
  createElement(tag: 'audio'): HTMLAudioElement
420
480
  createElement(tag: 'video'): HTMLVideoElement
421
481
  createElement(tag: 'virtual-list'): VirtualListElement
422
482
  createElement(tag: string): Element
423
483
  createTextNode(text: string): Node
484
+ createComment(text: string): Node
424
485
  createDocumentFragment(): Node
425
486
  }
426
487
 
@@ -435,6 +496,10 @@ export interface PointerEvent extends Event {
435
496
  // finger is 2, etc. (web convention, mirrored by the device touch runtime).
436
497
  // Multi-touch handlers route gestures by this id.
437
498
  readonly pointerId: number
499
+ /** Changed button: primary 0, auxiliary 1, secondary 2; -1 for no known edge. */
500
+ readonly button: number
501
+ /** Held-button bitmask for the producing pointer gesture; zero when released/unknown. */
502
+ readonly buttons: number
438
503
  readonly x: number
439
504
  readonly y: number
440
505
  readonly clientX: number
@@ -502,7 +567,7 @@ export interface RotaryEvent extends Event {
502
567
  * `KeyEvent`; `input` carries an `InputEvent`. Both are dispatched off-tree by
503
568
  * the runtime (`dispatch_key_input`, and `virtual_keyboard.cpp`'s synthetic
504
569
  * pair). Hardware button releases dispatch `keyup` with the same KeyEvent.
505
- * `pointercancel` remains unsupported by the native event dispatcher.
570
+ * Native cancellation names its event `pointercancel`; `touchcancel` is a listener alias.
506
571
  */
507
572
  export interface GeaEventMap {
508
573
  click: PressEvent
@@ -512,7 +577,12 @@ export interface GeaEventMap {
512
577
  pointerdown: PointerEvent
513
578
  pointermove: PointerEvent
514
579
  pointerup: PointerEvent
580
+ pointercancel: PointerEvent
581
+ touchcancel: PointerEvent
515
582
  input: InputEvent
583
+ focusin: Event
584
+ focusout: Event
585
+ change: InputEvent
516
586
  keydown: KeyEvent
517
587
  keyup: KeyEvent
518
588
  rotary: RotaryEvent
@@ -524,6 +594,15 @@ export type PointerEventHandler = (event: PointerEvent) => void
524
594
  export type RotaryEventHandler = (event: RotaryEvent) => void
525
595
 
526
596
  export interface NativeTouchEventAttributes {
597
+ /** Bare JSX spellings use the same native listener publication as onX. */
598
+ touchstart?: TouchEventHandler
599
+ touchend?: TouchEventHandler
600
+ touchmove?: TouchEventHandler
601
+ touchcancel?: PointerEventHandler
602
+ pointerdown?: PointerEventHandler
603
+ pointermove?: PointerEventHandler
604
+ pointerup?: PointerEventHandler
605
+ pointercancel?: PointerEventHandler
527
606
  onTouchStart?: TouchEventHandler
528
607
  onTouchEnd?: TouchEventHandler
529
608
  onTouchMove?: TouchEventHandler
@@ -538,9 +617,18 @@ export interface NativeTouchEventAttributes {
538
617
  onPointerDown?: PointerEventHandler
539
618
  onPointerMove?: PointerEventHandler
540
619
  onPointerUp?: PointerEventHandler
620
+ onPointerCancel?: PointerEventHandler
541
621
  }
542
622
 
543
623
  export interface NativeEventAttributes extends NativeTouchEventAttributes {
624
+ click?: PressHandler
625
+ input?: (event: InputEvent) => void
626
+ /** Committed user value change; distinct from intermediate input. */
627
+ change?: (event: InputEvent) => void
628
+ onChange?: (event: InputEvent) => void
629
+ keydown?: (event: KeyEvent) => void
630
+ keyup?: (event: KeyEvent) => void
631
+ scroll?: (event: Event) => void
544
632
  /** Text services and app-owned editors may dispatch input on ordinary views. */
545
633
  onInput?: (event: InputEvent) => void
546
634
  onClick?: PressHandler
@@ -551,6 +639,7 @@ export interface NativeEventAttributes extends NativeTouchEventAttributes {
551
639
  }
552
640
 
553
641
  export interface InputEventTarget extends Element {
642
+ disabled: boolean
554
643
  value: string
555
644
  }
556
645
 
@@ -559,9 +648,19 @@ export interface InputEvent {
559
648
  currentTarget: InputEventTarget
560
649
  }
561
650
 
562
- export interface KeyEvent {
563
- keyCode: number
564
- which: number
651
+ export interface KeyEvent extends Event {
652
+ readonly target: Element
653
+ readonly currentTarget: Element
654
+ readonly keyCode: number
655
+ readonly which: number
656
+ readonly key: string
657
+ readonly code: string
658
+ readonly altKey: boolean
659
+ readonly ctrlKey: boolean
660
+ readonly metaKey: boolean
661
+ readonly shiftKey: boolean
662
+ readonly repeat: boolean
663
+ readonly isComposing: boolean
565
664
  }
566
665
 
567
666
  export interface ViewProps extends DataAttributes {
@@ -1349,6 +1448,32 @@ export interface UartController {
1349
1448
 
1350
1449
  export declare const Uart: UartController
1351
1450
 
1451
+ /** ESP-NOW v2 datagrams; addresses are six-byte MACs. Unsupported targets return false/-1. */
1452
+ export interface EspNowController {
1453
+ /** Integer quarter-dBm cap, 8..84; fails on closed/borrowed radios. The SDK quantizes it. */
1454
+ setMaxTxPower(quarterDbm: number): boolean
1455
+ /** Actual cap in quarter-dBm, or -1 when unavailable. */
1456
+ getMaxTxPower(): number
1457
+ /**
1458
+ * Channel 0 borrows running Wi-Fi. Other channels claim an idle radio.
1459
+ * Default 54; rates 24/36/54 use OFDM, 65 uses HT20 MCS7.
1460
+ * ESP32-S31 also accepts 115 for HE20 MCS9 short GI (nominal 114.7 Mbps).
1461
+ * Unsupported rates/protocols return false without changing a borrowed radio.
1462
+ */
1463
+ open(channel: number, rateMbps?: number): boolean
1464
+ isOpen(): boolean
1465
+ /** True when the previous radio send has completed. */
1466
+ canSend(): boolean
1467
+ close(): void
1468
+ /** Optional key is a 16-byte LMK; omit only for unencrypted traffic. */
1469
+ addPeer(address: Uint8Array, key?: Uint8Array): boolean
1470
+ /** Whole datagram admission: bytes, 0 when busy, -1 for invalid/unavailable. */
1471
+ send(address: Uint8Array, bytes: Uint8Array): number
1472
+ /** source must be 6 bytes, buffer at least 1470. Returns received length, 0, or -1. */
1473
+ receive(source: Uint8Array, buffer: Uint8Array): number
1474
+ }
1475
+ export declare const EspNow: EspNowController
1476
+
1352
1477
  export interface LedController {
1353
1478
  // A WS2812/NeoPixel strand on one pin — what "the onboard LED" is on most
1354
1479
  // modern ESP32 modules.
@@ -1550,6 +1675,29 @@ export interface AccelerometerController {
1550
1675
 
1551
1676
  export declare const Accelerometer: AccelerometerController
1552
1677
 
1678
+ export interface MagnetometerReading {
1679
+ /** Compensated whole microtesla; saturated axes retain their sensor sentinel. */
1680
+ x: number
1681
+ y: number
1682
+ z: number
1683
+ /** False for an unavailable sensor or failed I2C read. */
1684
+ valid: boolean
1685
+ /** Saturated-axis bits: X=1, Y=2, Z=4. Those values are not field strength. */
1686
+ saturatedAxes: number
1687
+ timestampUs: number
1688
+ }
1689
+
1690
+ export interface MagnetometerController {
1691
+ /** Initializes the board's sensors. Idempotent; false when unavailable. */
1692
+ open(): boolean
1693
+ close(): void
1694
+ /** Number of board-defined sensors, whether open or closed. */
1695
+ count(): number
1696
+ /** Board-defined sensor index, 0 to count() - 1. */
1697
+ read(index: number): MagnetometerReading
1698
+ }
1699
+ export declare const Magnetometer: MagnetometerController
1700
+
1553
1701
  export type CameraFacing = 'front' | 'back' | 'external'
1554
1702
  export type CameraFlashMode = 'off' | 'on' | 'auto' | 'torch'
1555
1703
 
@@ -1814,7 +1962,7 @@ export interface InputElementProps {
1814
1962
  keyup?: (keyCode: number) => void
1815
1963
  }
1816
1964
 
1817
- export interface NativeInputElementProps extends InputElementProps, DataAttributes {
1965
+ export interface NativeInputElementProps extends Omit<InputElementProps, 'input' | 'keydown' | 'keyup'>, NativeEventAttributes, DataAttributes {
1818
1966
  id?: string
1819
1967
  ref?: GeaElement | null
1820
1968
  }
@@ -1829,7 +1977,7 @@ export interface TextAreaElementProps {
1829
1977
  onBlur?: () => void
1830
1978
  }
1831
1979
 
1832
- export interface NativeTextAreaElementProps extends TextAreaElementProps, DataAttributes {
1980
+ export interface NativeTextAreaElementProps extends TextAreaElementProps, NativeEventAttributes, DataAttributes {
1833
1981
  id?: string
1834
1982
  disabled?: boolean
1835
1983
  readOnly?: boolean
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geastack/core",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "GeaStack app API, reactivity headers, runtime sources, and type declarations.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -66,8 +66,11 @@
66
66
  "@geajs/core": "1.5.1",
67
67
  "@geajs/vite-plugin": "1.5.0",
68
68
  "@geastack/compiler": "^1.0.29",
69
- "@geastack/geatsc-plugin-gea": "0.1.23",
69
+ "@geastack/geatsc-plugin-gea": "0.1.24",
70
70
  "opentype.js": "^2.0.0",
71
71
  "vite": "^8.0.0"
72
+ },
73
+ "devDependencies": {
74
+ "jsdom": "^26.1.0"
72
75
  }
73
76
  }
package/runtime/host.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  // fails the representation coverage guard for the whole `Geolocation` literal
7
7
  // below. lib.dom's GeolocationPositionError also carries PERMISSION_DENIED /
8
8
  // POSITION_UNAVAILABLE / TIMEOUT, which our host object never sets.
9
- import type { UartOptions } from '../index'
10
9
  import type {
10
+ UartOptions,
11
+ MagnetometerReading,
11
12
  CanvasRenderingContext2D,
12
13
  GeolocationCoordinates,
13
14
  GeolocationPosition,
@@ -355,6 +356,12 @@ declare const __gea_Gpio: {
355
356
  write(pin: number, level: boolean): boolean
356
357
  read(pin: number): boolean
357
358
  }
359
+ declare const __gea_Magnetometer: {
360
+ open(): boolean
361
+ close(): void
362
+ count(): number
363
+ read(index: number, values: Float64Array): boolean
364
+ }
358
365
  declare const __gea_Uart: {
359
366
  open(port: number, tx: number, rx: number, baud: number, rxBytes: number, txBytes: number): boolean
360
367
  isOpen(port: number): boolean
@@ -364,6 +371,17 @@ declare const __gea_Uart: {
364
371
  available(port: number): number
365
372
  writable(port: number): number
366
373
  }
374
+ declare const __gea_EspNow: {
375
+ open(channel: number, rate: number): boolean
376
+ setMaxTxPower(quarterDbm: number): boolean
377
+ getMaxTxPower(): number
378
+ isOpen(): boolean
379
+ canSend(): boolean
380
+ close(): void
381
+ addPeer(address: Uint8Array, key: Uint8Array): boolean
382
+ send(address: Uint8Array, bytes: Uint8Array): number
383
+ receive(source: Uint8Array, bytes: Uint8Array): number
384
+ }
367
385
  declare const __gea_Led: {
368
386
  // A WS2812/NeoPixel strand on one pin — what "the onboard LED" is on most
369
387
  // modern ESP32 modules. set() is the one-call default (attach if needed,
@@ -1066,6 +1084,48 @@ export const Uart = {
1066
1084
  return typeof __gea_Uart !== 'undefined' ? __gea_Uart.writable(port) : -1
1067
1085
  },
1068
1086
  }
1087
+ export const Magnetometer = {
1088
+ open(): boolean {
1089
+ return typeof __gea_Magnetometer !== 'undefined' && __gea_Magnetometer.open()
1090
+ },
1091
+ close(): void {
1092
+ if (typeof __gea_Magnetometer !== 'undefined') __gea_Magnetometer.close()
1093
+ },
1094
+ count(): number {
1095
+ return typeof __gea_Magnetometer !== 'undefined' ? __gea_Magnetometer.count() : 0
1096
+ },
1097
+ read(index: number): MagnetometerReading {
1098
+ const values = new Float64Array(5)
1099
+ const valid = typeof __gea_Magnetometer !== 'undefined' && __gea_Magnetometer.read(index, values)
1100
+ return { x: values[0], y: values[1], z: values[2], valid, saturatedAxes: values[3], timestampUs: values[4] }
1101
+ },
1102
+ }
1103
+
1104
+ export const EspNow = {
1105
+ // 115 selects S31 HE20 MCS9 short GI (114.7 Mbps); target validation owns
1106
+ // availability. Other rates and the default retain their existing meaning.
1107
+ open(channel: number, rateMbps: number = 54): boolean {
1108
+ return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.open(channel, rateMbps)
1109
+ },
1110
+ setMaxTxPower(quarterDbm: number): boolean {
1111
+ return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.setMaxTxPower(quarterDbm)
1112
+ },
1113
+ getMaxTxPower(): number {
1114
+ return typeof __gea_EspNow !== 'undefined' ? __gea_EspNow.getMaxTxPower() : -1
1115
+ },
1116
+ isOpen(): boolean { return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.isOpen() },
1117
+ canSend(): boolean { return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.canSend() },
1118
+ close(): void { if (typeof __gea_EspNow !== 'undefined') __gea_EspNow.close() },
1119
+ addPeer(address: Uint8Array, key: Uint8Array = new Uint8Array(0)): boolean {
1120
+ return typeof __gea_EspNow !== 'undefined' && __gea_EspNow.addPeer(address, key)
1121
+ },
1122
+ send(address: Uint8Array, bytes: Uint8Array): number {
1123
+ return typeof __gea_EspNow !== 'undefined' ? __gea_EspNow.send(address, bytes) : -1
1124
+ },
1125
+ receive(source: Uint8Array, bytes: Uint8Array): number {
1126
+ return typeof __gea_EspNow !== 'undefined' ? __gea_EspNow.receive(source, bytes) : -1
1127
+ },
1128
+ }
1069
1129
  export const Led = {
1070
1130
  set(pin: number, r: number, g: number, b: number): boolean {
1071
1131
  return __gea_Led.set(pin, r, g, b)
@@ -59,8 +59,11 @@ export type {
59
59
  export type {
60
60
  UartOptions,
61
61
  UartController,
62
+ EspNowController,
62
63
  AccelerometerController,
63
64
  AccelerometerReading,
65
+ MagnetometerController,
66
+ MagnetometerReading,
64
67
  AppsController,
65
68
  AudioBuffer,
66
69
  AudioBufferSourceNode,
@@ -130,6 +133,8 @@ export type {
130
133
  InputElementProps,
131
134
  InputEvent,
132
135
  InputEventTarget,
136
+ InputElement,
137
+ ButtonElement,
133
138
  KeyEvent,
134
139
  MediaDevices,
135
140
  MediaRecorder,