espruino / EspruinoTools

JavaScript library of tools for Espruino - used for the Web IDE, CLI, etc.
Apache License 2.0
150 stars 89 forks source link

attempting to send code to Bangle from node script #111

Closed jh3y closed 4 years ago

jh3y commented 4 years ago

Hey @gfwilliams 👋

Playing with Bangle.js and I'd love to be able to connect to it from the CLI and send some code over from a node script.

I tried using espruino directly but I ran into this issue - https://github.com/abandonware/noble/issues/20

If I install @abandonware/noble and use it directly, I can find and connect to the Bangle. But then I'm not sure how I go about uploading expressions or code to it. I tried the following very rough code to get connected;

const noble = require("@abandonware/noble")
const esp = require("espruino")
let BANGLE
noble.startScanning()
noble.on("discover", peripheral => {
  if (
    peripheral.advertisement &&
    peripheral.advertisement.localName &&
    peripheral.advertisement.localName.includes("Bangle")
  ) {
    // DISCOVERED BANGLE
    BANGLE = peripheral
    // Try and connect to Bangle
    BANGLE.connect(() => {
      console.info('CONNECTED', BANGLE)
      esp.init(() => {
        esp.expr(BANGLE.address, "Bangle.buzz()", result => {
          console.info(result)
        })
      })
    })
  }
})

However, espruino is unable to connect. I'm not sure which is or how to grab the correct address in order to connect and upload code/files to the device.

Any help would be much appreciated 👍

@jh3y

gfwilliams commented 4 years ago

Hi! Argh, sorry about this. I believe it may have been fixed on GitHub but I hadn't published an updated package espruino please could you try the latest now and see if it works?

There's some code here that might help you: https://www.espruino.com/Interfacing#node-js-javascript

The odd thing is that what you're doing looks fine. The only thing I can see that's different is that the example code I have there uses noble.stopScanning(); to stop scanning first, and that might affect you.

Also, if you're doing this just to use esp.expr then you literally just need:

var esp = require("espruino");
esp.expr("aa:bb:cc:dd:ee:ff", "Bangle.buzz()", result => {
  console.info(result)
});

The only thing is finding out the bangle's address - which you can get with espruino --list

edit: can confirm - code above works great (at least on Linux)

jh3y commented 4 years ago

Hey @gfwilliams 👋

Thanks for coming back to me 👍 I have tried the latest and I get a little further.

espruino --list doesn't display the watch. But I can still find the watch with noble and connect to it but I can't use espruino.expr with the address from the noble peripheral.

const esp = require("espruino")
let BANGLE
noble.startScanning()
noble.on("discover", peripheral => {
  if (
    peripheral.advertisement &&
    peripheral.advertisement.localName &&
    peripheral.advertisement.localName.includes("Bangle")
  ) {
    // DISCOVERED BANGLE
    BANGLE = peripheral
    console.info('DISCOVERED PERIPHERAL', BANGLE.address)
    noble.stopScanning()
    // Try and connect to Bangle
    esp.expr(BANGLE.address, "Bangle.buzz()", result => {
      console.info(result)
    })
  }
})

I'll get an output like the following:

Port "e6-42-6b-7b-02-17" not found
Unable to connect!
gfwilliams commented 4 years ago

I believe it's because (on Mac again?) the BLE address is formatted differently with dashes rather than colons. Try var addr = BANGLE.address.toString().replace('-',':');

Very odd about espruino --list not working though - that's definitely with the new espruino NPM module? Does 'espruino --list --verbose' give any hints what is wrong?

jh3y commented 4 years ago

Ahh 👍

I wasn't clear enough there, sorry. espruino --list is working now. But the watch seems to be sporadically showing up 😅

I've tinkered further and got this where I can connect via the node module 🙌

const start = () => {
  const BANGLES = []
  Espruino.Core.Serial.getPorts(ports => {
    for (const port of ports) {
      if (port.description && port.description.toLowerCase().includes("bangle"))
        BANGLES.push(port)
    }
    console.info("AVAILABLE BANGLES", BANGLES)
    if (BANGLES.length === 0) {
      console.info("NO BANGLES FOUND, PLEASE TRY AGAIN")
      process.exit(0)
    }
    const BANGLE = BANGLES[0]
    // Once we have a bangle we can connect to it
    Espruino.Core.Serial.open(BANGLE.path, info => {
      console.info("CONNECTED TO BANGLE:", BANGLE, info)
    })
  })
}

esp.init(start)

What API functions do I need to use in order to package an app and upload it to the Bangle? I have code upload working using Core.CodeWriter.writeToEspruino 👍

gfwilliams commented 4 years ago

I don't really see why using Espruino.Core.Serial.open is somehow working for you when .expr didn't - it's basically using exactly the same code in each case.

The sporadic appearance of Bangle.js is really strange though. You could try modifying https://github.com/espruino/EspruinoTools/blob/gh-pages/core/serial_noble.js#L83 to a higher timeout and see if that helps.

In terms of upload, assuming you can get it working you're best off just doing ./espruino -d Bangle yourcode.js since this should be doing exactly what you are trying to do now (getPorts, find Bangle by name, then Serial.open). But if for some reason you want to reimplement it you're best off looking there to see what happens.

BUT direct app loading on Bangle.js isn't implemented in the tools yet. Basically you can upload to RAM (the default), or to 'boot code' which is where the bootloader is stored, but there's no support (yet) for uploading to a named file in Storage. That'll be coming very soon, but until then you basically just need to do this between getting the code to write and writing it:

code = 'require("Storage").write("appfile",'+JSON.stringify(code)+');\n'

That could be as easy as running:

espruino --board BANGLEJS myfile.js -o temp.js
# add require("Storage") to temp.js
espruino -d Bangle temp.js
jh3y commented 4 years ago

The issue with that though is that I want to send the code from within a script, not by using the CLI app.

I've been able to do this and have code working 👍

 Espruino.Core.Serial.open(BANGLE.path, info => {
      console.info("CONNECTED TO BANGLE:", BANGLE, info)
      Espruino.Core.CodeWriter.init()
      Espruino.Core.CodeWriter.writeToEspruino("Bangle.beep();", () => {
        console.info("BANGLE DID A BEEP")
      })
    })

It's just a way of packaging this up and using it however the BangleApps repo is doing it. As a last resort, I could just wrap the code being sent with the code from the badge workshop 👍

gfwilliams commented 4 years ago

Ahh, ok - well if you want to literally just connect and send commands, https://www.espruino.com/Interfacing#node-js-javascript would be an easy start I guess. The actual communication method for Bangle.js is called 'Nordic UART' and is used in loads of places, including Adafruit's BLE devices - so there are some packages that handle it already: https://www.npmjs.com/search?q=nordic%20uart

There's also esp.sendCode(port, code, callback) which does what you're doing, but it doesn't maintain a connection, so yeah - if you're trying to keep a connection open I think what you're doing is the best.

One thing you might be interested in is transformForEspruino - which will potentially do things like minification and will also automatically load any modules referenced by the code: https://github.com/espruino/EspruinoTools/blob/gh-pages/index.js#L143

But again if you're sending single commands, what you're doing is fine.

I'm not 100% sure what you have planned, but there might be something to be said for making BangleApps an NPM package and putting some simple Node.js code in for loading apps via CLI... All it'd require is basically creating a new comms.js that used the code from https://www.espruino.com/Interfacing#node-js-javascript

jh3y commented 4 years ago

Hey @gfwilliams 👋

Thanks for the info! I'm looking to create a CLI tool like create-bangle-app (think create-react-app but for Bangle.js 😄) so keeping the connection open is perfect for "dev" mode. I have this working now with fs.watchFile so that when I make a save to a file it uploads file content as string to the bangle with Core.CodeWriter.writeToEspruino(fileContentString).

Oooo. I'll certainly take a look at transformForEspruino 👍

So comms.js is where the "magic" is happening in the BangleApps repo for uploading an app to the watch?

gfwilliams commented 4 years ago

Nice - sounds like a great idea to help people get started.

There is the -w option in the CLI to watch a file and upload it on change (it stays connected and also turns the console into a REPL into the watch). Just from the point of view of reducing duplication (since the REPL might be a handy addition for you too), what's actually missing from the CLI tool itself? Just a way of instantiating it using the espruino package?

If so, it might be that I could split out the CLI parsing from the tool around about here and then just expose a function that you could hand an object to instead of having to poke into the depths of the tool.

But yeah, in BangleApps the code that works out what to send over the BLE connection (and handles responses) is in https://github.com/espruino/BangleApps/blob/master/comms.js - and then the code that actually sends it via Web Bluetooth is in https://github.com/espruino/EspruinoWebTools/blob/master/puck.js

So what would be needed is a way to swap out the Puck object with something that handled comms over Noble.

jh3y commented 4 years ago

Hey @gfwilliams 👋

Thanks for pointing me at those resources 👍

I've dug in a little further and got something pretty much there 🙌 AppInfo.getFiles was what I was looking for then using Core.Serial to write it to the device.

However, I'm a little stuck on one piece. Although I can fire a script and upload an app to the device or override it. I'm having trouble uploading the associated icon with it.

From digging into AppInfo, am I right in thinking a stringified version of the following is what should be sent over.

json

require('Storage')
  .write("+hellow","{
    "name": "Hello World!",
    "icon": "*hellow",
    "src": "-hellow",
    "files": "+hellow,-hellow,*hellow"
  }");

js

require('Storage')
  .write("-hellow","g.clear()\nE.showMessage('HELLO WORLD!')");

img

require('Storage')
  .write("*hellow","require(\"heatshrink\").decompress(atob(\"<IMG>"))\n");

As mentioned, I can upload the app and see it on the menu. It just doesn't have an icon with it.

Thanks for all your help!

MaBecker commented 4 years ago

I believe it's because (on Mac again?) the BLE address is formatted differently with dashes rather than colons. Try var addr = BANGLE.address.toString().replace('-',':');

Very odd about espruino --list not working though - that's definitely with the new espruino NPM module? Does 'espruino --list --verbose' give any hints what is wrong?

Running on Mac OS 10.14.6 and node v8.16.2 and on my desk is a Pixl.js and a Bangle.js

espruino --list Espruino Command-line Tool 0.1.30

PORTS: c2-92-a6-15-c3-ec (Bangle.js c3ec)

espruino --list --verbose 0.1.30 Espruino Command-line Tool 0.1.30

...... 0.1.30 Espruino Command-line Tool 0.1.30

Noble: Disable Web Bluetooth as we have Noble instead Noble: Starting scan Noble: Found device:
Noble: Found device: d0-d2-b0-26-82-01 d0-d2-b0-26-82-01 Noble: Found device: d0-d2-b0-26-82-01 d0-d2-b0-26-82-01 Noble: Found device:
Noble: Found device:
Noble: Found UART device: Bangle.js c3ec c2-92-a6-15-c3-ec Noble: Found device:
Noble: Found device: d0-4f-7e-49-e6-5c d0-4f-7e-49-e6-5c Noble: Found device: d0-4f-7e-49-e6-5c d0-4f-7e-49-e6-5c PORTS: c2-92-a6-15-c3-ec (Bangle.js c3ec) .....

Just to confirm

gfwilliams commented 4 years ago

Ahh, interesting - thanks! So it's different on Mac. I should probably replace - with : in serial_noble then.

For uploads: What should happen is the evaluate flag is checked in AppInfo.getFiles: https://github.com/espruino/BangleApps/blob/master/appinfo.js#L39

If that's the case, the data is sent as-is, so unstringified. Basically it's then executed by Bangle.js before the upload, so the uncompressed data is written to Storage.

It may be that your JSON doesn't have the evaluate field in it - it should look like this: https://github.com/espruino/BangleApps/blob/master/README.md#developing-your-own-app

jh3y commented 4 years ago

Hey @gfwilliams 👋

Thanks for that! That's the piece I was missing. I wasn't evaluating it 🤦‍♂ It works great now. The icon is uploaded with the code and all is well 👍

Thank you so much for your help with this.

I think we can close the issue. It went off on a slight tangent 😄

Thanks again so much for your help.

gfwilliams commented 4 years ago

No worries - glad you got it working!