@@ -488,66 +488,50 @@ <h1>PebbleOS Emulator</h1>
488488 // ================================================================
489489 var serialPos = 0 ;
490490
491- function pollSerial ( ) {
492- if ( ! qemu ) return ;
491+ // The PULSE client (module script) pulls new UART2 bytes from here.
492+ window . __readUart2 = function ( ) {
493+ if ( ! qemu ) return null ;
493494 try {
494495 var stat = qemu . FS . stat ( '/tmp/uart2.log' ) ;
495- if ( stat . size <= serialPos ) return ;
496+ if ( stat . size <= serialPos ) return null ;
496497 var stream = qemu . FS . open ( '/tmp/uart2.log' , 'r' ) ;
497498 var chunk = new Uint8Array ( stat . size - serialPos ) ;
498499 qemu . FS . read ( stream , chunk , 0 , chunk . length , serialPos ) ;
499500 qemu . FS . close ( stream ) ;
500501 serialPos = stat . size ;
501- var text = '' ;
502- for ( var i = 0 ; i < chunk . length ; i ++ ) {
503- var b = chunk [ i ] ;
504- if ( ( b >= 32 && b <= 126 ) || b === 9 || b === 10 || b === 13 ) {
505- text += String . fromCharCode ( b ) ;
506- }
507- }
508- if ( text ) log ( text . replace ( / \r / g, '' ) ) ;
502+ return chunk ;
509503 } catch ( e ) {
510- // File doesn't exist until QEMU opens the chardev.
504+ return null ; // file doesn't exist until QEMU opens the chardev
511505 }
512- }
513- setInterval ( pollSerial , 200 ) ;
506+ } ;
514507
515508 // ================================================================
516509 // Console input: pebble_simple_uart exports a browser->guest ring
517510 // for the dbgserial prompt (UART2). Enter sends the line + CRLF.
518511 // ================================================================
519512 var consoleCtrlAddr = 0 ;
520- var consoleInput = document . getElementById ( 'console-input' ) ;
521513
522- function consoleSend ( text ) {
523- if ( ! qemu ) return ;
514+ // Raw byte writer into the dbgserial RX ring; the PULSE client in
515+ // the module script frames its traffic through this.
516+ window . __consoleWriteBytes = function ( bytes ) {
517+ if ( ! qemu ) return false ;
524518 if ( ! consoleCtrlAddr ) {
525- if ( typeof qemu . _pebble_wasm_console_ctrl !== 'function' ) return ;
519+ if ( typeof qemu . _pebble_wasm_console_ctrl !== 'function' ) return false ;
526520 consoleCtrlAddr = Number ( qemu . _pebble_wasm_console_ctrl ( ) ) ;
527- if ( ! consoleCtrlAddr ) return ;
521+ if ( ! consoleCtrlAddr ) return false ;
528522 }
529523 var base = consoleCtrlAddr >> 2 ;
530524 var u32 = qemu . HEAPU32 ;
531525 var buf = u32 [ base ] , size = u32 [ base + 1 ] ;
532526 var head = Atomics . load ( u32 , base + 2 ) ;
533527 var tail = Atomics . load ( u32 , base + 3 ) ;
534- var bytes = [ ] ;
535- for ( var i = 0 ; i < text . length ; i ++ ) bytes . push ( text . charCodeAt ( i ) & 0xff ) ;
536- bytes . push ( 13 , 10 ) ;
537- if ( size - ( head - tail ) < bytes . length ) return ; // ring full
528+ if ( size - ( head - tail ) < bytes . length ) return false ;
538529 for ( var j = 0 ; j < bytes . length ; j ++ ) {
539530 qemu . HEAPU8 [ buf + ( ( head + j ) % size ) ] = bytes [ j ] ;
540531 }
541532 Atomics . store ( u32 , base + 2 , ( head + bytes . length ) >>> 0 ) ;
542- }
543-
544- consoleInput . addEventListener ( 'keydown' , function ( e ) {
545- if ( e . key !== 'Enter' ) return ;
546- var text = consoleInput . value ;
547- consoleInput . value = '' ;
548- log ( '> ' + text ) ;
549- consoleSend ( text ) ;
550- } ) ;
533+ return true ;
534+ } ;
551535
552536 // ================================================================
553537 // Persistence: the 32 MB SPI flash (apps, settings) is saved to
@@ -712,6 +696,8 @@ <h1>PebbleOS Emulator</h1>
712696 import { fetchPbwFromStore } from './store.js' ;
713697 import { AppMessageClient } from './appmessage.js' ;
714698 import { PkjsRuntime , makeIframeSandbox } from './pkjs-runtime.js' ;
699+ import { PulseConsole } from './pulse-console.js' ;
700+ import { NotificationSender , VoiceService } from './phone-extras.js' ;
715701
716702 const input = document . getElementById ( 'install-input' ) ;
717703 const btn = document . getElementById ( 'btn-install' ) ;
@@ -871,9 +857,108 @@ <h1>PebbleOS Emulator</h1>
871857 setInterval ( ( ) => phone . poll ( ) , 20 ) ;
872858 wireSensors ( ) ;
873859 restorePkjsApps ( ) ;
860+ wireConsole ( ) ;
861+ wireNotifications ( ) ;
862+ wireVoice ( ) ;
874863 clearInterval ( attach ) ;
875864 } , 500 ) ;
876865
866+ // ---- PULSE console: readable logs + interactive dbgserial prompt ----
867+ let pulse = null ;
868+ function wireConsole ( ) {
869+ const pad = ( n , w ) => String ( n ) . padStart ( w , '0' ) ;
870+ pulse = new PulseConsole ( ( bytes ) => window . __consoleWriteBytes ( bytes ) , {
871+ onLog : ( r ) => {
872+ const d = new Date ( r . timeMs ) ;
873+ const lvl = r . levelName ? ' <' + r . levelName + '>' : '' ;
874+ window . log ( `[${ pad ( d . getHours ( ) , 2 ) } :${ pad ( d . getMinutes ( ) , 2 ) } :${ pad ( d . getSeconds ( ) , 2 ) } .${ pad ( d . getMilliseconds ( ) , 3 ) } ]${ lvl } ${ r . file } :${ r . line } : ${ r . message } ` ) ;
875+ } ,
876+ onPrompt : ( text , done ) => { if ( text ) window . log ( ' ' + text ) ; } ,
877+ onRaw : ( text ) => window . log ( text ) ,
878+ log : ( m ) => console . log ( '[pulse] ' + m ) ,
879+ } ) ;
880+ setInterval ( ( ) => {
881+ const bytes = window . __readUart2 ( ) ;
882+ if ( bytes ) pulse . feed ( bytes ) ;
883+ } , 100 ) ;
884+
885+ const input = document . getElementById ( 'console-input' ) ;
886+ input . addEventListener ( 'keydown' , ( e ) => {
887+ if ( e . key !== 'Enter' ) return ;
888+ const text = input . value . trim ( ) ;
889+ input . value = '' ;
890+ if ( ! text ) return ;
891+ window . log ( '> ' + text ) ;
892+ if ( ! pulse . promptReady ) window . log ( ' (prompt still connecting — command queued)' ) ;
893+ pulse . command ( text ) ;
894+ } ) ;
895+ }
896+
897+ // ---- notifications ----
898+ function wireNotifications ( ) {
899+ const sender = new NotificationSender ( phone , ( m ) => say ( m ) ) ;
900+ const btn2 = document . getElementById ( 'btn-notify' ) ;
901+ btn2 . disabled = false ;
902+ btn2 . addEventListener ( 'click' , async ( ) => {
903+ btn2 . disabled = true ;
904+ try {
905+ await sender . send ( {
906+ title : document . getElementById ( 'notif-title' ) . value || 'Claude' ,
907+ body : document . getElementById ( 'notif-body' ) . value || 'Hello!' ,
908+ } ) ;
909+ say ( 'Notification sent — check the watch (Reply → Voice tests dictation).' ) ;
910+ } catch ( e ) {
911+ say ( 'Notification failed: ' + e . message ) ;
912+ } finally {
913+ btn2 . disabled = false ;
914+ }
915+ } ) ;
916+ }
917+
918+ // ---- voice dictation: browser speech recognition, typed fallback ----
919+ function wireVoice ( ) {
920+ const SR = window . SpeechRecognition || window . webkitSpeechRecognition ;
921+ let recog = null ;
922+ let transcript = '' ;
923+ let recogDone = null ;
924+
925+ const voice = new VoiceService ( phone , async ( ) => {
926+ if ( recog ) {
927+ try { recog . stop ( ) ; } catch ( e ) { /* already stopped */ }
928+ // give the recognizer a beat to finalize
929+ const settled = await new Promise ( ( res ) => {
930+ recogDone = res ;
931+ setTimeout ( ( ) => res ( transcript ) , 3000 ) ;
932+ } ) ;
933+ recog = null ;
934+ if ( settled && settled . trim ( ) ) return settled ;
935+ }
936+ return window . prompt ( 'Dictation: say what the watch heard' , transcript || 'Hello from the browser' ) ;
937+ } , ( m ) => { console . log ( '[voice] ' + m ) ; if ( window . log ) window . log ( '[voice] ' + m ) ; } ) ;
938+
939+ voice . onSessionStart = ( ) => {
940+ transcript = '' ;
941+ if ( ! SR ) { say ( 'Dictation started — no speech recognition in this browser, you\'ll get a text box.' ) ; return ; }
942+ try {
943+ recog = new SR ( ) ;
944+ recog . continuous = true ;
945+ recog . interimResults = true ;
946+ recog . lang = navigator . language || 'en-US' ;
947+ recog . onresult = ( e ) => {
948+ let t = '' ;
949+ for ( const res of e . results ) t += res [ 0 ] . transcript ;
950+ transcript = t ;
951+ } ;
952+ recog . onend = ( ) => { if ( recogDone ) recogDone ( transcript ) ; } ;
953+ recog . onerror = ( e ) => { console . log ( '[voice] recognition error: ' + e . error ) ; } ;
954+ recog . start ( ) ;
955+ say ( 'Dictation: speak into your microphone now…' ) ;
956+ } catch ( e ) {
957+ recog = null ;
958+ }
959+ } ;
960+ }
961+
877962 // ---- sensors & phone panel: QemuProtocol injections ----
878963 function wireSensors ( ) {
879964 const battSlider = document . getElementById ( 'batt-slider' ) ;
0 commit comments