1+ // For info on interfacing with Gadgetbridge, see https://www.espruino.com/Gadgetbridge
2+ const Debug = false ; // Set to true to show debugging into
3+ const Layout = require ( "Layout" ) ;
4+ const PrimaryFont = "Vector:18" ;
5+
6+ const buttonPadding = 10 ;
7+
8+ const Command = {
9+ next : "next" ,
10+ pause : "pause" ,
11+ play : "play" ,
12+ previous : "previous" ,
13+ volumeup : "volumeup" ,
14+ volumedown : "volumedown" ,
15+ } ;
16+
17+ const PlaybackState = {
18+ paused : "pause" ,
19+ playing : "play"
20+ } ;
21+
22+ /**
23+ * Format elapsed time in minutes and seconds.
24+ * @param {* } time Elapsed time
25+ * @returns Time string
26+ */
27+ function formatTime ( time ) {
28+ let minute = 0 , second = 0 ;
29+ if ( time ) {
30+ minute = Math . floor ( time / 60 ) ;
31+ second = time % 60 ;
32+ }
33+ let minuteStr = minute . toString ( ) , secondStr = second . toString ( ) ;
34+
35+ if ( minute < 10 ) minuteStr = `0${ minute } ` ;
36+ if ( second < 10 ) secondStr = `0${ second } ` ;
37+
38+ return `${ minuteStr } :${ secondStr } ` ;
39+ }
40+
41+ /**
42+ * Global playback state tracker.
43+ * Follows the syntax {t:"musicstate", state:"play/pause",position,shuffle,repeat}
44+ */
45+ let appState = { t : "musicstate" , state : PlaybackState . paused , position : 0 , shuffle : 0 , repeat : 0 } ;
46+
47+ /**
48+ * Define the screen layout.
49+ */
50+ let layout = new Layout ( {
51+ type : "v" , c : [
52+ { type : "txt" , id : "title" , halign : - 1 , fillx : 0 , col : g . fg , font : PrimaryFont , label : "Track N/A" } ,
53+ { type : "txt" , id : "artist" , halign : - 1 , fillx : 0 , col : g . fg , font : PrimaryFont , label : "Artist N/A" } ,
54+ {
55+ type : "h" , c : [
56+ { type : "txt" , id : "elapsed" , halign : - 1 , fillx : 1 , col : g . fg , font : PrimaryFont , label : formatTime ( 0 ) } ,
57+ { type : "txt" , id : "timeSplitter" , halign : 0 , fillx : 1 , col : g . fg , font : PrimaryFont , label : " - " } ,
58+ { type : "txt" , id : "duration" , halign : 1 , fillx : 1 , col : g . fg , font : PrimaryFont , label : formatTime ( 0 ) }
59+ ]
60+ } ,
61+ {
62+ type : "h" , c : [
63+ { type : "btn" , id : Command . previous , font : PrimaryFont , col : g . fg2 , bgCol : g . bg2 , pad : buttonPadding , label : "|<<" , cb : l => sendCommand ( Command . previous , true ) } ,
64+ { type : "btn" , id : "playpause" , font : PrimaryFont , col : g . fg2 , bgCol : g . bg2 , pad : buttonPadding , label : " > " , cb : l => sendCommand ( appState . state === PlaybackState . paused ? Command . play : Command . pause , true ) } ,
65+ { type : "btn" , id : Command . next , font : PrimaryFont , col : g . fg2 , bgCol : g . bg2 , pad : buttonPadding , label : ">>|" , cb : l => sendCommand ( Command . next , true ) }
66+ ]
67+ } ,
68+ ]
69+ } , { lazy : true } ) ;
70+
71+ /// Set up the app
72+ function initialize ( ) {
73+ // Detect whether we're using an emulator.
74+ if ( typeof Bluetooth === "undefined" || typeof Bluetooth . println === "undefined" ) { // emulator!
75+ Bluetooth = {
76+ println : ( line ) => { console . log ( "Bluetooth:" , line ) ; } ,
77+ } ;
78+ }
79+
80+ // Set up listeners for swiping
81+ Bangle . on ( 'swipe' , function ( directionLR , directionUD ) {
82+ switch ( directionLR ) {
83+ case - 1 : // Left
84+ sendCommand ( Command . previous , true ) ;
85+ break ;
86+ case 1 : // Right
87+ sendCommand ( Command . next , true ) ;
88+ break ;
89+ }
90+
91+ switch ( directionUD ) {
92+ case - 1 : // Up
93+ sendCommand ( Command . volumeup , true ) ;
94+ break ;
95+ case 1 : // Down
96+ sendCommand ( Command . volumedown , true ) ;
97+ break ;
98+ }
99+ } ) ;
100+
101+ // Eat music events (๑ᵔ⤙ᵔ๑)
102+ Bangle . on ( "message" , ( type , message ) => {
103+ if ( type . includes ( "music" ) && ! message . handled ) {
104+ processMusicEvent ( message ) ;
105+ message . handled = true ;
106+ }
107+ } ) ;
108+
109+ // Toggle play/pause if the button is pressed
110+ setWatch ( function ( ) {
111+ sendCommand ( appState . state === PlaybackState . paused ? Command . play : Command . pause , true ) ;
112+ } , BTN , { edge : "falling" , debounce : 50 , repeat : true } ) ;
113+
114+ // Goad Gadgetbridge into sending us the current track info
115+ sendCommand ( Command . volumeup , false ) ;
116+ sendCommand ( Command . volumedown , false ) ;
117+
118+ // Render the screen
119+ g . clear ( ) ;
120+ draw ( ) ;
121+ }
122+
123+ function draw ( ) {
124+ layout . update ( ) ;
125+ layout . render ( ) ;
126+ }
127+
128+ // Track how long the current song has been running.
129+ let elapsedTimer ;
130+ let position = 0 ;
131+ function updateTime ( ) {
132+ position ++ ;
133+ layout . elapsed . label = formatTime ( position ) ;
134+ draw ( ) ;
135+
136+ if ( Debug ) console . log ( "Tick" ) ;
137+ }
138+
139+ function clearTimer ( ) {
140+ position = 0 ;
141+ if ( elapsedTimer ) {
142+ clearInterval ( elapsedTimer ) ;
143+ elapsedTimer = undefined ;
144+ }
145+ }
146+
147+ /**
148+ * Send a command via Bluetooth back to Gadgetbridge.
149+ * @param {Command } command Which command to execute
150+ * @param {true|false } buzz Whether to vibrate the motor
151+ */
152+ function sendCommand ( command , buzz ) {
153+ if ( buzz ) Bangle . buzz ( 50 ) ;
154+ Bangle . musicControl ( command ) ;
155+ }
156+
157+ function processMusicEvent ( event ) {
158+ if ( Debug ) console . log ( "State: " + event . state ) ;
159+ if ( Debug ) console . log ( "Position: " + event . position ) ;
160+
161+ position = event . position ;
162+
163+ switch ( event . state ) {
164+ case PlaybackState . playing :
165+ if ( Debug ) console . log ( "Playing" ) ;
166+ appState . state = event . state ;
167+ elapsedTimer = setInterval ( updateTime , 1000 ) ;
168+ layout . playpause . label = " || " ;
169+ break ;
170+ case PlaybackState . paused :
171+ if ( Debug ) console . log ( "Paused" ) ;
172+ appState . state = event . state ;
173+ clearTimer ( ) ;
174+ layout . playpause . label = " > " ;
175+ break ;
176+ case PlaybackState . previous :
177+ case PlaybackState . next :
178+ // Reset position
179+ position = 0 ;
180+ appState . state = PlaybackState . playing ;
181+ break ;
182+ }
183+
184+ // Re-render track info on song change
185+ if ( event . track != layout . title . label ) {
186+ position = 0 ;
187+ layout . title . label = event ? event . track : "Track N/A" ;
188+ layout . artist . label = event ? event . artist : "Artist N/A" ;
189+ layout . duration . label = formatTime ( event . dur ) ;
190+ }
191+
192+ draw ( ) ;
193+ if ( Debug ) layout . debug ( ) ;
194+ }
195+
196+ // Start the app and set up listeners
197+ initialize ( ) ;
0 commit comments