-
-
I did already see that actually. This would be a perfect use case scenario for having both the GB connection and the web IDE console. Unfortunately that option doesn't show up for me in the gadgetbridge app loader, possibly because I'm using the beta version from the play store. At least my music event logging program seems to accomplish the same thing.
In this post Gordon says:
Connections from Gadgetbridge are now live! In Gadgetbridge, go to the
App Loader (3x3 squares icon below the device name) then tap More...
and Web IDE Remote.My "More..." screen in the app loader looks like this:
-
I took a screenshot of my makeshift log from the bangle 😂 (Hey, whatever works! Right?)
It actually looks a little different than what I'm currently seeing in my music program, somehow it's sending the duration of the new song as the new current position when the new song comes on whereas in my music program it continues counting from the last songs duration. That kind of explains why it's so hard for me to get it to reset properly.
A little more context - The song Mercy was playing first and had a duration of 290. The duration ran out on its own and the phone changed to the next song on its own causing the messages above [13:12:49.424] to start being sent, but with the wrong position data. And twice, once with the old album info!
-
That's a great idea! Like you mentioned, this sort of volume syncing is already built into android for bluetooth speakers and such so we should be able to just make it compatible with that.
I made a few changes to my program, but I haven't figured it out yet. I did write a little helper file to simply log the music messages from gadgetbridge onto the display with timestamps. It seems to send twice as many messages as normal so I'm thinking maybe this is the cause of my problem?
let lines = []; function breakIntoLines(str, maxWidth) { let chunks = str.split(','); // Split by commas let lines = []; let currentLine = ""; chunks.forEach(chunk => { let words = chunk.trim().split(' '); // Split by spaces words.forEach((word, idx) => { let separator = idx === 0 ? "" : " "; let newContent = (currentLine.length > 0 ? separator : '') + word; if (g.stringWidth(currentLine + newContent) <= maxWidth) { currentLine += newContent; } else { if (currentLine.length > 0) { lines.push(currentLine); } currentLine = word; } }); currentLine += ','; }); if (currentLine.length > 0) { lines.push(currentLine); } return lines; } function breakWord(word, maxWidth) { let brokenLines = []; let currentLine = ""; for (let char of word) { if (g.stringWidth(currentLine + char) <= maxWidth) { currentLine += char; } else { brokenLines.push(currentLine); currentLine = char; } } if (currentLine.length > 0) { brokenLines.push(currentLine); } return brokenLines; } function renderLines() { g.clear(); let y = 0; lines.forEach((line, index) => { let sublines = breakIntoLines(line, g.getWidth() - 5); sublines.forEach(subline => { // calculate next Y position before drawing let nextY = y + g.getFontHeight(); // if next Y position is outside of screen, remove oldest line if (nextY > g.getHeight()) { lines.pop(); return; // Skip the current iteration } g.setFont('6x8'); g.drawString(subline, 0, y); y = nextY; }); }); } function formatTimestamp() { const date = new Date(); return `[${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}.${date.getMilliseconds().toString().padStart(3, '0')}]`; } function handleGadgetbridgeMessage(msg) { if (msg.t === 'musicstate' || msg.t === 'musicinfo') { let info = formatTimestamp() + (msg.state !== undefined ? ' ' + msg.state + ',' : '') + (msg.position !== undefined ? ' ' + msg.position : '') + (msg.track !== undefined ? ' ' + msg.track + ',' : '') + (msg.artist !== undefined ? ' ' + msg.artist + ',' : '') + (msg.album !== undefined ? ' ' + msg.album + ',' : '') + (msg.dur !== undefined ? ' ' + msg.dur : ''); lines.unshift(info); // Add the info at the beginning of the array renderLines(); // Draw lines after every new message } }
-
Hi rigrig, I did use fake messages to test and those do work. Sometimes it will actually reset the position to zero with the way the code is now, other times it won't. It's not consistent so it's very difficult for me to pinpoint where the problem is coming from. I'm going to do some more digging in the gadgetbridge log and see if I can decipher how the messages are being sent and when. Thanks for your suggestion.
-
-
I made an attempt at a music control app that shows a track position timer, but it has an issue with the track position timer not always resetting to zero when it's receiving the messages from gadgetbridge for a new song. For the last few days I've been trying to figure out how to get it to reset properly when a new song comes on, but it's been a struggle. Since it uses the boot file to receive the GB messages I need to write to storage every time I make a change and want to test it and I haven't found a way to have the console up when connected to GB.
Also, it is a bit of a battery hog. Either that or it's just from me writing to flash repeatedly, but when using this app my battery does go down pretty fast, it does try to refresh the scrolling text at about 30fps right now which I'm sure doesn't help battery life.
Anyway, I would appreciate any thoughts or suggestions. Thanks
-
-
-
Going from memory, but the phone will show ft starting at 500ft and decrement down 50ft until it reaches 0ft.
On the bangle with the code snippet I posted above in my locale file, it will switch back to 0.3mi starting after 0.1 mi instead of switching to ft like on the the phone, then when it gets to 0.1mi on the watch again it switches to ft but at this point the phone will be at 150ft maybe . It is exactly 3.28084 times more than what it should be due to the meter->ft conversion.
It's also worth mentioning there were some weird instances where it would read one less mile on the watch than the phone and at other times it would match perfectly, albeit with a slight delay in both cases.
-
@Ganblejs I'm also using the beta version of gadgetbridge and I have a custom app loader that opens up normally in the GB app. Do you have chrome installed as well on your phone? I'm thinking it's actually supposed to use chrome within gadgetbridge to display it.
-
There was talk of being able to use your phone's mic for input. Would need some kind of speech to text on the phone then.
In lieu of a mic or speaker, it might be useful to make a predictive text input that contextually predicts chunks of words it thinks you might want to enter into your prompt and present them as a list of options to append to your prompt.
Initially it could start with generic "What, when, why, how, where" etc. and probably would need at least one or two other seed words that are input by the user with a keyboard app to give better context.
Here's the basic thought process on how it might work:
- The Watch: Display the initial list of options. When an option is selected, send the selected word to the server.
- The Server: Receives the selected word from the watch. This selected word is used as a seed to generate new potential words/phrases using an LLM. The server then sends the generated words/phrases back to the watch.
- The Watch: Receives the new words/phrases from the server and displays them as new options. Once the user is satisfied they can send the whole prompt to be answered like normal.
Back on topic... I thought it would be nice for Bangle.js 3 to have an LED indicator that could double as an ambient light sensor. Similar to how the LED is being used on this open source chirp device.
From the description:
Light sensing A simple LED is used as a light sensor. Capacitive properties of diodes are used. LED is driven forward-biased for some time, then it’s driven reverse-biased to charge the internal capacitance. The time it takes for this internal diode capacitance to discharge depends on the amount of light that falls into the diode. Microcontroller timer is used to measure this time and estimate how much ambient light is out there.
- The Watch: Display the initial list of options. When an option is selected, send the selected word to the server.
-
-
If anyone is curious to try out my new Ohm's Law Calculator app you can do so here.
Thank you again Philip, I was really struggling with that menu transition to the layout and back. You rock!
Another challenging task is still on my to do list: Get it to show the formula that was used as an expression with the letter placeholders like so "V = I*R" and then when that menu item is pressed it opens up a formula display page in a large font with the formula shown using the actual values now instead of the letters.
-
@Philip That was it! Thank you so much!
-
Hey, thanks for the reply.
I did try placing layout.render() in different parts of my code but it didn't help.
I'll have to check out that keyboard you helped make. Can't say I've tried using any keyboards on this watch so far, but I like the idea of using the swipe handler to do backspace or maybe as a back button replacement on the input screen in my case.
-
It's far from finished and there may be other bugs and errors, but here is the program I'm making. It's an Ohm's Law Calculator. This problem has been holding me back all day so I would really like to solve it, especially since it seems trivial.
let Layout = require("Layout"); const UNITS = { "Voltage (V)": "Volts", "Current (I)": "Amps", "Resistance (R)": "Ohms", "Power (P)": "Watts", }; const FORMULAS = { 'Voltage (V)': { 'Current (I), Resistance (R)': "{0} * {1}", 'Power (P), Current (I)': "{0} / {1}", 'Power (P), Resistance (R)': "Math.sqrt({0} * {1})" }, 'Current (I)': { 'Voltage (V), Resistance (R)': "{0} / {1}", 'Power (P), Voltage (V)': "{0} / {1}", 'Power (P), Resistance (R)': "Math.sqrt({0} / {1})" }, 'Resistance (R)': { 'Voltage (V), Current (I)': "{0} / {1}", 'Power (P), Current (I)': "{0} / (Math.pow({1}, 2))", 'Power (P), Voltage (V)': "(Math.pow({0}, 2)) / {1}" }, 'Power (P)': { 'Voltage (V), Current (I)': "{0} * {1}", 'Current (I), Resistance (R)': "(Math.pow({0}, 2)) * {1}", 'Voltage (V), Resistance (R)': "(Math.pow({0}, 2)) / {1}" }, }; let lastStringWidth = 0; let calculatedVariable; let selectedVariable; let variableValues = {}; let inputStr = ""; let layout = new Layout({ type: "v", c: [ { type: "txt", font: "6x8:3", label: "", id: "label" }, { type: "h", c: "123".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, fillx: 1, filly: 1 })) }, { type: "h", c: "456".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, fillx: 1, filly: 1 })) }, { type: "h", c: "789".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, fillx: 1, filly: 1 })) }, { type: "h", c: ".0C".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, fillx: 1, filly: 1 })) }, { type: "h", c: [{ type: "btn", font: "6x8:2", label: "Enter", cb: () => { handleEnter(); }, fillx: 1, filly: 1 }] } ] }, { lazy: true }); function clearInputArea() { let label = layout.label; // Calculate rectangle coordinates for clearing let xCenter = g.getWidth() / 2; let halfStringWidth = lastStringWidth / 2; let x1 = xCenter - halfStringWidth; let y1 = label.y; let x2 = xCenter + halfStringWidth; let y2 = label.y + g.getFontHeight(); // Clear the old label area g.clearRect(x1, y1, x2, y2); } function clearTextArea() { let x1 = 0; let y1 = 0; let x2 = g.getWidth(); let y2 = 22; // Clear the old label area g.clearRect(x1, y1, x2, y2); } function clearScreen() { // Except Back Button let x2 = g.getWidth(); let y2 = g.getHeight(); g.clearRect(24, 0, x2, 24); g.clearRect(0, 24, x2, y2); } function showCalculatorInputScreen(variable) { selectedVariable = variable; layout.render(); } function setValue(newStr) { clearTextArea(); inputStr = newStr; layout.label.label = inputStr; layout.render(); lastStringWidth = g.stringWidth(inputStr); } function handleButtonPress(value) { if (value === 'C') { setValue(""); } else { inputStr += value; setValue(inputStr); } } function calculateValue(calculatedVariable, variableValues) { let formulas = FORMULAS[calculatedVariable]; let formulaKeys = Object.keys(formulas); for (let i = 0; i < formulaKeys.length; i++) { let formulaKey = formulaKeys[i]; let variables = formulaKey.split(', '); if (variables.every(variable => variableValues.hasOwnProperty(variable))) { let formula = formulas[formulaKey]; let formulaValues = variables.map(variable => variableValues[variable]); let calculatedValue = eval(formula.replace(/\{(\d+)\}/g, (_, index) => formulaValues[index])); let result = Object.entries(variableValues).map(function (entry) { let variable = entry[0]; let value = entry[1]; return [variable, `${value} ${UNITS[variable]}`]; }); result.push([calculatedVariable, `${calculatedValue.toFixed(2)} ${UNITS[calculatedVariable]}`]); return { formula: formula.replace(/\{(\d+)\}/g, (_, index) => formulaValues[index]), value: calculatedValue.toFixed(2), unit: UNITS[calculatedVariable], result: result, }; } } } (function () { let mainMenu = { '': { 'title': 'Ohm\'s Law Calc' }, '< Back': () => Bangle.showClock() }; Object.keys(UNITS).forEach(unit => { mainMenu[unit] = () => handleUnitSelection(unit); }); function showVariableSelectionMenu() { let variableSelectionMenu = { '': { 'title': 'Select Variable' }, '< Back': () => E.showMenu(mainMenu) }; let variables = Object.keys(UNITS); let remainingVariables = variables.filter(v => v !== calculatedVariable); remainingVariables.forEach(variable => { variableSelectionMenu[variable] = function () { showInputMenu(variable); }; }); E.showMenu(variableSelectionMenu); } function showInputMenu(variable) { let inputMenu = { '': { 'title': variable }, }; E.showMenu(inputMenu); layout.render(); } function handleEnter() { if (calculatedVariable === null) { return; } variableValues[selectedVariable] = parseFloat(inputStr); if (Object.keys(variableValues).length === 1 && variableValues.hasOwnProperty(selectedVariable)) { let temp = variableValues[selectedVariable]; delete variableValues[selectedVariable]; variableValues[Object.keys(variableValues)[0]] = temp; } if (Object.keys(variableValues).length === 2) { let result = calculateValue(calculatedVariable, variableValues); showResultsScreen(result); calculatedVariable = null; variableValues = {}; inputStr = ""; } else { clearScreen(); showVariableSelectionMenu(); } inputStr = ""; setValue(""); } function handleUnitSelection(unit) { calculatedVariable = unit; showVariableSelectionMenu(); } function showResultsScreen(result) { let resultsMenu = { '': { 'title': 'Results' }, '< Back': function () { clearScreen(); E.showMenu(mainMenu); }, 'Formula': result.formula, ['Calculated ' + result.unit]: result.value + ' ' + result.unit }; result.result.forEach(([variable, value]) => { resultsMenu[variable] = value; }); E.showMenu(resultsMenu); } E.showMenu(mainMenu); })();
-
-
There is one thing I noticed that could be improved with regards to the tabs.
If you open a file from storage and click "Copy to Editor" a new tab with its name appears. However if the tab remains open and the file on the device has been updated and reloaded from storage again, it doesn't actually update the open tab with the new file data when clicking "Copy to Editor" again.
Also, ctrl+b doesn't seem to work for beautify anymore, and ctrl+` only switches from terminal to editor but not from editor to terminal.
-
I've been testing this out and there's a pretty big issue with how maps is sending the distance values when using imperial units.
It will send the distance value to bangle.js in meters, which it's supposed to, until it gets to 0.1 miles at which point maps decides to send the distance value directly in feet instead.
Now when the locale file on the bangle.js sees this new value in feet it still thinks it's working with meters and the distance becomes inaccurate as a result.
I've tried getting the log data from gadgetbridge while using navigation, but it only showed one single nav message at the beginning of my drive with a distance of 0 and that's it. The navigation ran the whole trip on my phone and the watch so I don't know why there weren't more.
I was also testing some formatting changes in the locale file to make it use one decimal place at less than 10 miles and convert to feet when less than 0.1 miles (this is about ~500ft or 152.4 meters which is when maps changes from miles to feet). This works great and makes the distances match exactly what is shown on the phone (except the bug with the units switching), but I'm thinking this may not be the right place for this code. Perhaps it would be better placed in the messagegui app and could be made compatible with all locales?
distance: (n) => { var miles = n / 1609.34; var dp = miles < 10 ? 1 : 0; return n < 152.4 ? Math.round(n * 3.28084) + "ft" : (miles < 10 ? miles.toFixed(1) : Math.round(miles)) + "mi"; },
-
-
Since you're having trouble connecting with Bluetooth I would suggest trying to connect to it with different devices (phone, laptop, PC, etc.) by using the web IDE or app loader through a supported browser like chrome (not gadgetbridge).
If you don't see it right away it's possible it's already connected to something else in which case you can hold the button down for 6-10 sec for it to do a full reboot and hopefully show up as available to pair.
-
Yep, I meant to come back here and clarify that as well.
Exactly as Gordon suggested, I had to add square brackets [...] around the
require("Storage").readJSON("shadowclk.json", true)
in thePuck.eval()
call to wrap the resulting data in an array. Then extract the actual data from the array usinglet data = dataArray ? dataArray[0] : null;.
like this:"Puck.eval('[require("Storage").readJSON("shadowclk.json", true)]', (dataArray) => { let data = dataArray ? dataArray[0] : null;
I was always just leaving mine blank at the end of my url.
Adding
.../android.html
fixed it and now the Web IDE Remote option shows up. Thanks!This should help me figure out the track position not resetting properly.