Websocket questions

I am using the Websocket api, have figured out most of the commands (by reading the web page JavaScript).

Normally, while running I get a lot of binary updates, which is the “preview” image at the top of the web page, plus Json updates every second of FPS, memory and a couple of other items.

I can disable these updates by sending {‘sendUpdates’: False}, however, a short period later, the updates start again. I do also have the web page to the pb3 open. Does the web page keep re-enabling updates? Or does it automatically re-enable after a period of time?

Also, is there a list of the binary message types? I’ve figured out 3 of them - byte[0] (0x04,0x05 and 0x09), but I’m guessing there are probably more.

I have to say this was very easy to implement, and everything is working great so far.

Thanks!

I got a lot of information on packet types and formats from the firestorm source at https://github.com/simap/Firestorm. And only @wizard knows for sure, but I’d bet the web page does periodically make sure updates are enabled.

Thanks,

I found the packet types in the firestorm code.

I’ll try my code without the web page being open, and see what happens.

Updates are global currently (hope to change that in the future), and control both the live preview and periodic informational data like FPS. The UI will enable and disable them as a matter of course when working with the UI. It shouldn’t re-enable automatically, though if the web page disconnects and reconnects, it will do that when it reloads everything. Loading the UI with ?min on the url will turn those off (and also disable stored preview loading).

2 Likes

I found with the web page closed, update do not re-enable, so the web page must be refreshing or something.

I am trying to figure out how to upload and download patterns using the websocket.

I have tried the command {'getSources': pid}, which returns some binary data (with header type 0x06) - I’m not sure how this is encoded though, I though it might be base64 encoded, but that doesn’t seem to work.

How do i get the text of the source from the binary data?

And what would be an easy way to load/save patterns using the websocket interface?

I can see the format of the .epe file is defined in:

n.on("click", ".download", function(e) {
    e.preventDefault(),
    e.stopPropagation(),
    saveHandler = function(e) {
        saveHandler = !1,
        blobToBase64(cachedPreviews[t.id], function(a) {
            var n = {
                name: t.name,
                id: t.id,
                sources: e,
                preview: a
            }
              , r = new Blob([JSON.stringify(n, null, 2)],{
                type: "text/plain;charset=utf-8"
            });
            saveAs(r, t.name + ".epe", !1)
        })
    }
    ,
    sendFrame({
        getSources: t.id
    })
}),                       

and I can get the sources, name, id and preview data. Just need to figure out how to get the blob into text (and vice versa for uploading).

Thanks.

Hi @Nick_W,
The sources are compiled in the browser, and the source is only stored on PB and is compressed using lz-string also in the browser.

There isn’t an easy way to get new source code to Pixelblaze outside of the web app/IDE. The app/IDE is where the source is compiled into bytecode for the engine that runs on Pixelblaze.

When you make a change in the editor, compilation happens in the browser, and the core of it is this line:

program = compile(src, {predefinedGlobals: ['pixelCount'], extendedOperators: extendedOperators, constants: constants});

which operates on and produces globals that you can see and poke at in the browser console. This is then packaged up into a payload that includes the bytecode and exports table and sent as CODEDATA (3) frames when live-coding.

Saved patterns consist of additional bits of data like the ID, name, preview jpg, compiled bytecode, export table, and compressed sources. These are sent as SAVEPROGRAMSOURCEFILE (1) frames.

These files are portable within the same major version of Pixelblaze, and Firestorm’s clone command can be used to shuffle these around.

The other websocket binary frame types come in to play when the app loads parts of that stored pattern later. Preview images are sent with THUMBNAILJPG (4) frames when loading the pattern list, and compressed sources are sent when editing/cloning/exporting a pattern using SOURCESDATA (6) frames.

1 Like

Thank you for the help. With this information, I have made a lot of progress.

I can now download, assemble and save an .epe file (once I figured out LZString.decompressFromUint8Array and base64 encoding the preview jpg).

I can also download the binary file representation of the pattern by downloading http://<ip>/p/<pattern_id>

What I am having real trouble with is re-uploading the saved binary file.

The specific part I have a problem with is in putProgramBinary:

form.append('data', binary, {
  filepath: '/p/' + programId,
  contentType: 'application/octet-stream',
});

I’m not a javascript programmer, and I don’t know what the third parameter is supposed to represent (references say filename, but it isn’t a filename).

I’m programming in python, and I have tried this:

    async def load_binary_file(self, filename=None, binary=None):
        try:
            if not binary:
                if not filename: return
                with open(filename, 'rb') as f:
                    binary = f.read()
            programId = filename.replace('.bin','')
            data = aiohttp.FormData()
            data.add_field('data',
                           binary,
                           filename = '/p/' + programId,
                           content_type='application/octet-stream')
            async with aiohttp.ClientSession() as session:
                async with session.post('http://{}/edit'.format(self.ip), data=data) as resp:
                    self.log.info(resp.status)
                    await resp.text()
            if resp.status == 200:
                self.log.info('file: {} loaded'.format(filename))
        except Exception as e:
            self.log.exception(e)

Which gives me a 200 response, but no pattern is loaded on the pb.

Any idea where I am going wrong? is the whole post supposed to be application/octet-stream? in which case I need to convert everything to bytes.

if I could understand the javascript FormData(), I’m sure I could figure it out.

Thanks.

OK, figured it out but OMG was it a pain.

This is the only way I could get the binary file to upload:

    async def load_binary_file(self, filename=None, binary=None):
        try:
            if not binary:
                if not filename: return
                with open(filename, 'rb') as f:
                    binary = f.read()
            programId = filename.replace('.bin','')                        
            form = aiohttp.MultipartWriter('form-data')
            part = form.append(binary)
            part.headers[aiohttp.hdrs.CONTENT_DISPOSITION] = 'form-data; name="data"; filename="/p/{}"'.format(programId)
            part.headers[aiohttp.hdrs.CONTENT_TYPE] = 'application/octet-stream'
            async with aiohttp.ClientSession() as session:
                async with session.post('http://{}/edit'.format(self.ip), data=form) as resp:
                    self.log.info(resp.status)
                    await resp.text()
            if resp.status == 200:
                self.log.info('file: {} loaded'.format(filename))
        except Exception as e:
            self.log.exception(e)

This works, where FormData() does not, because aiohttp encodes filenames like this:

filename="%2Fp%2FShzpfL53tXQDBLbRv"; filename*=utf-8\'\'%2Fp%2FShzpfL53tXQDBLbRv

and pixelblaze only accepts plain strings for filename - ie:

filename="/p/ShzpfL53tXQDBLbRv"

So the pixelblaze server doesn’t seem to support RFC6266 properly. This did take a ridiculous amount of time to track down though.

So, if anyone else needs to upload the pattern binary files (same ones as used in cloning), here is how you do it (in python anyway) - and beware of the gotcha!

@Nick_W,
Very interesting find! And even more curious. Doing the same kind of upload with curl works, as does the nodejs stuff. Perhaps that was using a non-utf8 encoding, and skip urlencoding?

BTW what are you building?

I think Python ‘s aiohttp is being too clever and is URL encoding file names (and I don’t think it is supposed to do that), so it would probably work as expected without the encoding.

So curl and node probably don’t URL encode the file name.

Unfortunately, the only workaround I could find was to build the content-disposition header manually.

As to what I’m building, I have an existing APA102 controller which I built in 2016. It controls 1440 pixels which light the garden.

This works fine, but the patterns are all compiled into the firmware (running on an M0). If I want a different pattern, I have to recompile and re-upload. It’s all controlled via MQTT (WiFi controller connected to the M0).

I was inspired by @zranger1 Python library, and have just finished an asynchronous MQTT interface in Python 3.6. Fully compatible (ie it uses the same command interface) with Pixelblaze-Client, but using the asyncio framework.

I haven’t got a good name for it yet, but it receives commands over MQTT, and sends them to one or more Pixelblaze’s. Data from the PB is also published to a topic of your choice. Includes discovery etc.

The idea is that I can drop the PB in place of the M0, and still control it using the existing MQTT framework (speed, brightness, pattern selection, etc). The advantage is of course, that patterns can be written and tested, uploaded/downloaded and so on very easily.

I wanted to have a library of pattern files, so if a pattern was selected, that isn’t on the pb, it can be uploaded automatically, and then set to be the current pattern. And it works!

I should have the library up on GitHub this weekend.

4 Likes

@Nick_W, this is awesome! Would you mind if I borrowed the pattern management features for pixelblaze-client?

1 Like

Sure, a lot of the code is copied from Pixelblaze-Client anyway. I have implemented some extra commands and functions, but it should be easy to adapt to a synchronous version.

I’ll try to get a beta version up on GitHub this weekend.

2 Likes