hackish: real-time random watch face

Posted on
  • There's a random clock app, but the way it works is that you have to reload the bootloader manually to actually flush the new random face. That isn't what I want -- I want a real-time random watch face that will show me a different face every time I wake up the display.

    This has proven to be rather more difficult than I expected, but I have finally come up with something that, while hackish, is actually functional. I figured I'd share it here instead of making a PR, because I'm pretty sure it would never actually go into the code as-is. Anyway,

    The easier of the changes was modifying the settings app to allow a random clock. This was accomplished by simply adding an else case to the existing code:

    if (clockApps.length === 0) {
      clockMenu["No Clocks Found"] = () => { };
    } else {
      clockMenu[`${settings.clock === " random" ? "* " : ""}[random]`] = () => {
        if (settings.clock !== " random") {
          settings.clock = " random";
          updateSettings();
          showMainMenu();
        }
      };
    }
    

    NB: I used " random" because the clockApp is a string name of the application set, and I think adding a leading space ensures that there won't be a collision, but I would appreciate guidance if I'm wrong about that.

    The bootloader part was a little more tricky (and I'm still not 100% satisfied, though I'd say it does work about 90% of the way I'd like it to). First off I set about refactoring the code just a bit to make selecting a random clock easier.

    const clockApp = (require("Storage").readJSON("setting.js­on",1) || {}).clock;
    const evaluateCurrentClockApp = () => {
      const allClockApps = () => (
        require("Storage").list(/\.info$/)
          .map((file) => {
            const app = require("Storage").readJSON(file,1);
            if (app && app.type == "clock") {
              return app;
            }
          })
          .filter((x) => x)
      );
      const clockNotFound = `
        E.showMessage("No Clock Found");
        setWatch(() => {
          Bangle.showLauncher();
        }, BTN2, {repeat:false,edge:"falling"});)
      `;
      const currentClockApp = () => {
        if (" random" === clockApp) {
          const apps = allClockApps();
          return apps.length > 0
            ? require("Storage").read(apps[Math.floor(­Math.random() * apps.length)].src)
            : clockNotFound;
        } else if (clockApp) {
          return require("Storage").read(clockApp);
        }
        // Fallback: first clock app we discover on the system
        const [discoveredApp] = allClockApps().sort((a, b) => a.sortorder - b.sortorder);
        return discoveredApp
          ? require("Storage").read(discoveredApp.sr­c)
          // Fallback: not found
          : clockNotFound;
      };
      delete evaluateCurrentClockApp;
      eval(currentClockApp());
    };
    

    Instead of running

          eval(clockApp);
          delete clockApp;
    

    to enter the clock app as before, we just: evaluateCurrentClockApp();.

    I was also made aware of the fact that all code in the bootloader persists through the passing of execution to the clock -- that is why delete evaluateCurrentClockApp; is at the end of the function. I think it's pretty crazy that you can delete a function that is currently in calling scope, but it actually works and I verified by trace() in the web IDE that it does indeed clean up the memory.

    So this is fairly straightforward, but we end up at the original problem: this runs once when the bootloader is executed, but then the selected "random" clock stays as the watch face unless the bootloader is manually run again (e.g. long press BTN3).

    I tried a couple solutions to this problem, but the one that seems the most promising is this: set an LCD power off listener in the bootloader. When the LCD is powered off and the current clock app is set as random, reload the bootloader:

    Bangle.on("lcdPower", function onLcdPower(on) {
      if (!on && " random" === clockApp) {
        Bangle.removeListener("lcdPower", onLcdPower);
        load();
      }
    });
    

    This actually works fairly well for what it does... the only problem is that reloading the bootloader turns the LCD back on, so you can probably guess what happens next: a never-ending loop of the LCD going off, coming back on with a random clock face, going off, coming back on, etc. Still, I was kinda happy when it happened :)

    After this happened, I tried this:

    Bangle.setLCDPower(0);
    Bangle.on("lcdPower", function onLcdPower(on) {
      if (!on && " random" === clockApp) {
        Bangle.removeListener("lcdPower", onLcdPower);
        load();
      }
    });
    

    This actually solves the never-ending wake loop, but it introduces another problem: the LCD screen is off upon initial execution. It's honestly a small price to pay for a true random clock face in my opinion, but I wanted to do better. This is also when I really noticed how after the LCD turns off there is a flicker due to reloading the bootloader, before my LCD power off occurs. Annoying, but acceptable (for me).

  • So this brings me to the final hackish """solution""" I came up with for keeping the display on during initial bootloading:

    if (require("Storage").read(".bootrn")) {
      require("Storage").erase(".bootrn");
      Bangle.setLCDPower(0);
    }
    Bangle.on("lcdPower", function onLcdPower(on) {
      if (!on && " random" === clockApp) {
        Bangle.removeListener("lcdPower", onLcdPower);
        require("Storage").write(".bootrn", "1");
        load();
      }
    });
    

    When powering off, a 1-byte file is written to .bootrn and when bootloading, the existence of that file is checked. I don't need to tell you that this is an ugly hack... but it works :)

    So anyway this was my journey thus far in trying to accomplish a real-time random clock face.

    Question:

    Is there any way less invasive than calling load() to accomplish this, as I have? The entire flicker hack can be removed if so. I tried things to get around this, e.g. wrapping the clock/child application in an IIFE, evaluating it, and then deleteing the function. Nothing besides load() seemed to reliably do the trick without a bunch of memory leakage.

    Also, the limit for this forum is kinda low. I'm only replying to continue the thread like this because I ran out of characters, lol.

    Anyways, I'll attach the bootloader.js and settings.js in case anyone wants to mess around with it.

    P.S. I was scared to load a different bootloader to my watch but I went for it!


    2 Attachments

  • Nice! And yes, sorry for not pulling in the bootloader change as it was.

    Did you try const [discoveredApp] = allClockApps().sort((a, b) => a.sortorder - b.sortorder);? Because Espruino doesn't support destructuring so I'd have thought this might error.

    Is there any way less invasive than calling load()

    Actually, I think you should be able to call load("clock_app_name.js") which at least doesn't require any bootloader mods at all - so it could go in a Widget...

    Bangle.on("lcdPower", function onLcdPower(on) {
      if (!on && " random" === clockApp) {
        // use your previous bootloader code to find a random `clockApp`
        load(clockApp); //< clockApp is the filename, not the file contents
      }
    });
    

    But yes, that forces the screen on. I guess potentially we could have some kind of firmware change that allows it to stay off... Other option is just to load the random clock on the lcdPower on event?

    One option is actually just to modify the clock apps themselves. If each clock app provided a unload() function that unregistered any event handlers, it would be pretty easy to call unload() and then eval() a new clock app.

    Worst case you could detect if a clock didn't implement unload() and then do the load(clockFilename) command.

    P.S. I was scared to load a different bootloader to my watch but I went for it!

    Maybe the word 'bootloader' is overly scary! You can do what you want in JS and you'll always be able to recover the Bangle with 'install default apps' in the App Loader, so go for it!

  • Espruino doesn't support destructuring

    Oooh, yeah I did discover that because in my PR I had const {clock: clockApp} = ... and it died. I missed that one.

    call load("clock_app_name.js")

    It seems this is essentially having the same effect?

    load the random clock on the lcdPower on event?

    I did try that first! The thing is, it's better to have a little bit of flicker when the watch is going to sleep than when the watch is waking up. 99% of the time in the Real World™ I won't even see the flicker when the watch goes off, because I got what I needed and am not looking anymore. On the other hand, I will see it flicker every time before showing the clock, and that will (and did) drive me nuts :)

    it would be pretty easy to call unload() and then eval() a new clock app

    I did think about an approach like this but it's pretty intrusive in that 1. every clock app needs to conform and 2. it seems rather trivial to screw this up in any given clock app (even if there is an unload function), leading to leaks...

    Thanks for responding :) I think the tl;dr for now is that what I have is the best that it can be without other deeper changes (e.g. firmware). I'm just happy I was able to get a random watch face!

  • Hey Gordon, would you mind just pointing me towards a link where I could start if I theoretically had brass balls and wanted to try modifying my own firmware?

  • It seems this is essentially having the same effect?

    It still reboots, however it's a lot tidier than having the code in the bootloader (IMO).

    it seems rather trivial to screw this up in any given clock app (even if there is an unload function), leading to leaks...

    Yes.. That's what led me to the current situation of full reboots for each app - it's just way more stable when you don't completely trust an app.

    However in most cases with clocks I think it'd be pretty possible, and it would be quite neat and would enable features like swiping to new clock faces.

    modifying my own firmware?

    This is the best link: https://github.com/espruino/Espruino/blo­b/master/README_Building.md

    It's actually pretty straightforward - especially on Linux or Windows (with WSL installed). The bootloader is designed with a watchdog to try and recover if a bad binary gets flashed to it, so it's not too painful to develop (although I develop with SWD wires attached as it makes it much quicker to flash new firmwares)

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

hackish: real-time random watch face

Posted by Avatar for cha0s @cha0s

Actions