| @@ -233,7 +233,7 @@ public: | |||
| scaleX = (float) (grid->getNumColumns()) / activeBlock->getWidth(); | |||
| scaleY = (float) (grid->getNumRows()) / activeBlock->getHeight(); | |||
| setLEDProgram (grid); | |||
| setLEDProgram (*activeBlock); | |||
| } | |||
| // Make the on screen Lighpad component visible | |||
| @@ -286,7 +286,7 @@ private: | |||
| { | |||
| // Switch to canvas mode and set the LEDGrid program | |||
| currentMode = canvas; | |||
| setLEDProgram (activeBlock->getLEDGrid()); | |||
| setLEDProgram (*activeBlock); | |||
| } | |||
| } | |||
| @@ -325,7 +325,7 @@ private: | |||
| { | |||
| // Switch to colour palette mode and set the LEDGrid program | |||
| currentMode = colourPalette; | |||
| setLEDProgram (activeBlock->getLEDGrid()); | |||
| setLEDProgram (*activeBlock); | |||
| } | |||
| stopTimer(); | |||
| @@ -350,7 +350,7 @@ private: | |||
| } | |||
| /** Sets the LEDGrid Program for the selected mode */ | |||
| void setLEDProgram (LEDGrid* grid) | |||
| void setLEDProgram (Block& block) | |||
| { | |||
| canvasProgram = nullptr; | |||
| colourPaletteProgram = nullptr; | |||
| @@ -358,10 +358,10 @@ private: | |||
| if (currentMode == canvas) | |||
| { | |||
| // Create a new BitmapLEDProgram for the LEDGrid | |||
| canvasProgram = new BitmapLEDProgram (*grid); | |||
| canvasProgram = new BitmapLEDProgram (block); | |||
| // Set the LEDGrid program | |||
| grid->setProgram (canvasProgram); | |||
| block.setProgram (canvasProgram); | |||
| // Redraw any previously drawn LEDs | |||
| redrawLEDs(); | |||
| @@ -369,10 +369,10 @@ private: | |||
| else if (currentMode == colourPalette) | |||
| { | |||
| // Create a new DrumPadGridProgram for the LEDGrid | |||
| colourPaletteProgram = new DrumPadGridProgram (*grid); | |||
| colourPaletteProgram = new DrumPadGridProgram (block); | |||
| // Set the LEDGrid program | |||
| grid->setProgram (colourPaletteProgram); | |||
| block.setProgram (colourPaletteProgram); | |||
| // Setup the grid layout | |||
| colourPaletteProgram->setGridFills (layout.numColumns, | |||
| @@ -28,7 +28,7 @@ public: | |||
| // If this is a Lightpad then set the grid program to be blank | |||
| if (auto grid = block->getLEDGrid()) | |||
| grid->setProgram (new BitmapLEDProgram(*grid)); | |||
| block->setProgram (new BitmapLEDProgram (*block)); | |||
| // If this is a Lightpad then redraw it at 25Hz | |||
| if (block->getType() == Block::lightPadBlock) | |||
| @@ -145,7 +145,7 @@ public: | |||
| scaleX = static_cast<float> (grid->getNumColumns() - 1) / activeBlock->getWidth(); | |||
| scaleY = static_cast<float> (grid->getNumRows() - 1) / activeBlock->getHeight(); | |||
| setLEDProgram (grid); | |||
| setLEDProgram (*activeBlock); | |||
| } | |||
| break; | |||
| @@ -227,7 +227,7 @@ private: | |||
| currentMode = waveformSelectionMode; | |||
| // Set the LEDGrid program to the new mode | |||
| setLEDProgram (activeBlock->getLEDGrid()); | |||
| setLEDProgram (*activeBlock); | |||
| } | |||
| #if JUCE_IOS | |||
| @@ -259,15 +259,15 @@ private: | |||
| } | |||
| /** Sets the LEDGrid Program for the selected mode */ | |||
| void setLEDProgram (LEDGrid* grid) | |||
| void setLEDProgram (Block& block) | |||
| { | |||
| if (currentMode == waveformSelectionMode) | |||
| { | |||
| // Create a new WaveshapeProgram for the LEDGrid | |||
| waveshapeProgram = new WaveshapeProgram (*grid); | |||
| waveshapeProgram = new WaveshapeProgram (block); | |||
| // Set the LEDGrid program | |||
| grid->setProgram (waveshapeProgram); | |||
| block.setProgram (waveshapeProgram); | |||
| // Initialise the program | |||
| waveshapeProgram->setWaveshapeType (static_cast<uint8> (waveshapeMode)); | |||
| @@ -276,10 +276,16 @@ private: | |||
| else if (currentMode == playMode) | |||
| { | |||
| // Create a new DrumPadGridProgram for the LEDGrid | |||
| gridProgram = new DrumPadGridProgram (*grid); | |||
| gridProgram = new DrumPadGridProgram (block); | |||
| // Set the LEDGrid program | |||
| grid->setProgram (gridProgram); | |||
| auto error = block.setProgram (gridProgram); | |||
| if (error.failed()) | |||
| { | |||
| DBG (error.getErrorMessage()); | |||
| jassertfalse; | |||
| } | |||
| // Setup the grid layout | |||
| gridProgram->setGridFills (layout.numColumns, | |||
| @@ -3,15 +3,15 @@ | |||
| /** | |||
| A Program to draw moving waveshapes onto the LEDGrid | |||
| */ | |||
| class WaveshapeProgram : public LEDGrid::Program | |||
| class WaveshapeProgram : public Block::Program | |||
| { | |||
| public: | |||
| WaveshapeProgram (LEDGrid& lg) : Program (lg) {} | |||
| WaveshapeProgram (Block& b) : Program (b) {} | |||
| /** Sets the waveshape type to display on the grid */ | |||
| void setWaveshapeType (uint8 type) | |||
| { | |||
| ledGrid.setDataByte (0, type); | |||
| block.setDataByte (0, type); | |||
| } | |||
| /** Generates the Y coordinates for 1.5 cycles of each of the four waveshapes and stores them | |||
| @@ -74,22 +74,19 @@ public: | |||
| // Store the values for each of the waveshapes at the correct offsets in the shared data heap | |||
| for (uint8 i = 0; i < 45; ++i) | |||
| { | |||
| ledGrid.setDataByte (sineWaveOffset + i, sineWaveY[i]); | |||
| ledGrid.setDataByte (squareWaveOffset + i, squareWaveY[i]); | |||
| ledGrid.setDataByte (sawWaveOffset + i, sawWaveY[i]); | |||
| ledGrid.setDataByte (triangleWaveOffset + i, triangleWaveY[i]); | |||
| block.setDataByte (sineWaveOffset + i, sineWaveY[i]); | |||
| block.setDataByte (squareWaveOffset + i, squareWaveY[i]); | |||
| block.setDataByte (sawWaveOffset + i, sawWaveY[i]); | |||
| block.setDataByte (triangleWaveOffset + i, triangleWaveY[i]); | |||
| } | |||
| } | |||
| uint32 getHeapSize() override | |||
| { | |||
| return totalDataSize; | |||
| } | |||
| String getLittleFootProgram() override | |||
| { | |||
| return R"littlefoot( | |||
| #heapsize: 256 | |||
| int yOffset; | |||
| int min (int a, int b) | |||
| @@ -173,8 +170,6 @@ private: | |||
| static constexpr uint32 sawWaveOffset = 91; // 1 byte * 45 | |||
| static constexpr uint32 triangleWaveOffset = 136; // 1 byte * 45 | |||
| static constexpr uint32 totalDataSize = triangleWaveOffset + 45; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WaveshapeProgram) | |||
| }; | |||
| @@ -0,0 +1,323 @@ | |||
| /** Returns the smaller of two integer values. */ | |||
| int min (int a, int b); | |||
| /** Returns the smaller of two floating point values. */ | |||
| float min (float a, float b); | |||
| /** Returns the larger of two integer values. */ | |||
| int max (int a, int b); | |||
| /** Returns the larger of two floating point values. */ | |||
| float max (float a, float b); | |||
| /** Constrains an integer value to keep it within a given range. | |||
| @param lowerLimit the minimum value to return | |||
| @param upperLimit the maximum value to return | |||
| @param valueToConstrain the value to try to return | |||
| @returns the closest value to valueToConstrain which lies between lowerLimit | |||
| and upperLimit (inclusive) | |||
| */ | |||
| int clamp (int lowerLimit, int upperLimit, int valueToConstrain); | |||
| /** Constrains a floating point value to keep it within a given range. | |||
| @param lowerLimit the minimum value to return | |||
| @param upperLimit the maximum value to return | |||
| @param valueToConstrain the value to try to return | |||
| @returns the closest value to valueToConstrain which lies between lowerLimit | |||
| and upperLimit (inclusive) | |||
| */ | |||
| float clamp (float lowerLimit, float upperLimit, float valueToConstrain); | |||
| /** Returns the absolute value of an integer value. */ | |||
| int abs (int arg); | |||
| /** Returns the absolute value of a floating point value. */ | |||
| float abs (float arg); | |||
| /** Remaps a value from a source range to a target range. | |||
| @param value the value within the source range to map | |||
| @param sourceMin the minimum value of the source range | |||
| @param sourceMax the maximum value of the source range | |||
| @param destMin the minumum value of the destination range | |||
| @param destMax the maximum value of the destination range | |||
| @returns the original value mapped to the destination range | |||
| */ | |||
| float map (float value, float sourceMin, float sourceMax, float destMin, float destMax); | |||
| /** Remaps a value from a source range to the range 0 - 1.0. | |||
| @param value the value within the source range to map | |||
| @param sourceMin the minimum value of the source range | |||
| @param sourceMax the maximum value of the source range | |||
| @returns the original value mapped to the range 0 - 1.0 | |||
| */ | |||
| float map (float value, float sourceMin, float sourceMax); | |||
| /** Performs a modulo operation (can cope with the dividend being negative). | |||
| The divisor must be greater than zero. | |||
| */ | |||
| int mod (int dividend, int divisor); | |||
| /** Returns the number of milliseconds since a fixed event (usually system startup). | |||
| This returns a monotonically increasing value which it unaffected by changes to the | |||
| system clock. It should be accurate to within a few millisecseconds. | |||
| */ | |||
| int getMillisecondCounter(); | |||
| /** Returns the current firmware version. */ | |||
| int getFirmwareVersion(); | |||
| /** Logs an integer value to the console. */ | |||
| void log (int data); | |||
| /** Logs a hexadecimal value to the console. */ | |||
| void logHex (int data); | |||
| /** Returns the length of time spent in the function call in milliseconds. */ | |||
| int getTimeInCurrentFunctionCall(); | |||
| /** Returns the battery level between 0 and 1.0. */ | |||
| float getBatteryLevel(); | |||
| /** Returns true if the battery is charging. */ | |||
| bool isBatteryCharging(); | |||
| /** Returns true if this block is directly connected to the application, | |||
| as opposed to only being connected to a different block via a connection port. | |||
| */ | |||
| bool isMasterBlock(); | |||
| /** Returns true if this block is directly connected to the host computer. */ | |||
| bool isConnectedToHost(); | |||
| /** Sets whether status overlays should be displayed. */ | |||
| void setStatusOverlayActive (bool active); | |||
| /** Returns the number of blocks in the current topology. */ | |||
| int getNumBlocksInTopology(); | |||
| /** Returns the ID of the block at a given index in the topology. */ | |||
| int getBlockIDForIndex (int index); | |||
| /** Returns the ID of the block connected to a specified port. */ | |||
| int getBlockIDOnPort (int port); | |||
| /** Returns the port number that is connected to the master block. Returns 0xFF if there is no path. */ | |||
| int getPortToMaster(); | |||
| /** Returns the block type of the block with this ID. */ | |||
| int getBlockTypeForID (int blockID); | |||
| /** Sends a message to the block with the specified ID. This will be processed in the handleMessage() callback. | |||
| @param blockID the ID of the block to send this message to | |||
| @param data0 the first chunk of data to send | |||
| @param data1 the second chunk of data to send | |||
| @param data2 the third chunk of data to send | |||
| */ | |||
| void sendMessageToBlock (int blockID, int data0, int data1, int data2); | |||
| /** Combines a set of 8-bit ARGB values into a 32-bit colour and returns the result. */ | |||
| int makeARGB (int alpha, int red, int green, int blue); | |||
| /** Blends the overlaid ARGB colour onto the base one and returns the new colour. */ | |||
| int blendARGB (int baseColour, int overlaidColour); | |||
| /** Sets a pixel to a specified colour with full alpha. | |||
| @param rgb the colour to use (0xff...) | |||
| @param x the x coordinate of the pixel to fill | |||
| @param y the y coordinate of the pixel to fill | |||
| */ | |||
| void fillPixel (int rgb, int x, int y); | |||
| /** Blends a the current pixel colour with a specified colour. | |||
| @param argb the colour to use | |||
| @param x the x coordinate of the pixel to blend | |||
| @param y the y coordinate of the pixel to blend | |||
| */ | |||
| void blendPixel (int argb, int x, int y); | |||
| /** Fills a rectangle on the display with a specified colour. | |||
| @param rgb the colour to use (0xff...) | |||
| @param x the x coordinate of the rectangle to draw | |||
| @param y the y coordinate of the rectangle to draw | |||
| @param width the width of the rectangle to draw | |||
| @param height the height of the rectangle to draw | |||
| */ | |||
| void fillRect (int rgb, int x, int y, int width, int height); | |||
| /** Blends a rectangle on the display with a specified colour. | |||
| @param argb the colour to use | |||
| @param x the x coordinate of the rectangle to blend | |||
| @param y the y coordinate of the rectangle to blend | |||
| @param width the width of the rectangle to blend | |||
| @param height the height of the rectangle to blend | |||
| */ | |||
| void blendRect (int argb, int x, int y, int width, int height); | |||
| /** Draws a rectangle on the block display with four corner colours blended together. | |||
| @param colourNW the colour to use in the north west corner of the rectangle | |||
| @param colourNE the colour to use in the north east corner of the rectangle | |||
| @param colourSE the colour to use in the south east corner of the rectangle | |||
| @param colourSW the colour to use in the south west corner of the rectangle | |||
| @param x the x coordinate of the rectangle | |||
| @param y the y coordinate of the rectangle | |||
| @param width the width of the rectangle | |||
| @param height the height of the rectangle | |||
| */ | |||
| void blendGradientRect (int colourNW, int colourNE, int colourSE, int colourSW, int x, int y, int width, int height); | |||
| /** Draws a pressure point at a given centre LED with a specified colour and pressure. | |||
| @param argb the colour to use | |||
| @param touchX the x position of the touch | |||
| @param touchY the y position of the touch | |||
| @param touchZ the pressure value of the touch | |||
| */ | |||
| void addPressurePoint (int argb, float touchX, float touchY, float touchZ); | |||
| /** Draws the pressure map on the block display. */ | |||
| void drawPressureMap(); | |||
| /** Fades the pressure map on the block display. */ | |||
| void fadePressureMap(); | |||
| /** Draws a number on the block display. | |||
| @param value the number to draw between 0 and 99 | |||
| @param colour the colour to use | |||
| @param x the x coordinate to use | |||
| @param y the y coordinate to use | |||
| */ | |||
| void drawNumber (int value, int colour, int x, int y); | |||
| /** Clears the display setting all the LEDs to black. */ | |||
| void clearDisplay(); | |||
| /** Clears the display setting all the LEDs to a specified colour. */ | |||
| void clearDisplay (int rgb); | |||
| /** Sends a 1-byte short midi message. */ | |||
| void sendMIDI (int byte0); | |||
| /** Sends a 2-byte short midi message. */ | |||
| void sendMIDI (int byte0, int byte1); | |||
| /** Sends a 3-byte short midi message. */ | |||
| void sendMIDI (int byte0, int byte1, int byte2); | |||
| /** Sends a key-down message. | |||
| @param channel the midi channel, in the range 0 to 15 | |||
| @param noteNumber the key number, in the range 0 to 127 | |||
| @param velocity the velocity, in the range 0 to 127 | |||
| */ | |||
| void sendNoteOn (int channel, int noteNumber, int velocity); | |||
| /** Sends a key-up message. | |||
| @param channel the midi channel, in the range 0 to 15 | |||
| @param noteNumber the key number, in the range 0 to 127 | |||
| @param velocity the velocity, in the range 0 to 127 | |||
| */ | |||
| void sendNoteOff (int channel, int noteNumber, int velocity); | |||
| /** Sends an aftertouch message. | |||
| @param channel the midi channel, in the range 0 to 15 | |||
| @param noteNumber the key number, in the range 0 to 127 | |||
| @param level the amount of aftertouch, in the range 0 to 127 | |||
| */ | |||
| void sendAftertouch (int channel, int noteNumber, int level); | |||
| /** Sends a controller message. | |||
| @param channel the midi channel, in the range 0 to 15 | |||
| @param controller the type of controller | |||
| @param value the controller value | |||
| */ | |||
| void sendCC (int channel, int controller, int value); | |||
| /** Sends a pitch bend message. | |||
| @param channel the midi channel, in the range 0 to 15 | |||
| @param position the wheel position, in the range 0 to 16383 | |||
| */ | |||
| void sendPitchBend (int channel, int position); | |||
| /** Sends a channel-pressure change event. | |||
| @param channel the midi channel, in the range 0 to 15 | |||
| @param pressure the pressure, in the range 0 to 127 | |||
| */ | |||
| void sendChannelPressure (int channel, int pressure); | |||
| //============================================================================== | |||
| /** Called when a button is pushed. | |||
| @param index the index of the button that was pushed | |||
| */ | |||
| void handleButtonDown (int index); | |||
| /** Called when a button is released. | |||
| @param index the index of the button that was released | |||
| */ | |||
| void handleButtonUp (int index); | |||
| /** Called when a touch event starts. | |||
| @param index the touch index, which will stay constant for each finger as it is tracked | |||
| @param x the X position of this touch on the device, in logical units starting from 0 (left) | |||
| @param y the Y position of this touch on the device, in logical units starting from 0 (top) | |||
| @param z the current pressure of this touch, in the range 0.0 (no pressure) to 1.0 (very hard) | |||
| @param vz the rate at which pressure is currently changing, measured in units/second | |||
| */ | |||
| void touchStart (int index, float x, float y, float z, float vz); | |||
| /** Called when a touch event moves. | |||
| @param index the touch index, which will stay constant for each finger as it is tracked | |||
| @param x the X position of this touch on the device, in logical units starting from 0 (left) | |||
| @param y the Y position of this touch on the device, in logical units starting from 0 (top) | |||
| @param z the current pressure of this touch, in the range 0.0 (no pressure) to 1.0 (very hard) | |||
| @param vz the rate at which pressure is currently changing, measured in units/second | |||
| */ | |||
| void touchMove (int index, float x, float y, float z, float vz); | |||
| /** Called when a touch event ends. | |||
| @param index the touch index, which will stay constant for each finger as it is tracked | |||
| @param x the X position of this touch on the device, in logical units starting from 0 (left) | |||
| @param y the Y position of this touch on the device, in logical units starting from 0 (top) | |||
| @param z the current pressure of this touch, in the range 0.0 (no pressure) to 1.0 (very hard) | |||
| @param vz the rate at which pressure is currently changing, measured in units/second | |||
| */ | |||
| void touchEnd (int index, float x, float y, float z, float vz); | |||
| /** Called when a program is loaded onto the block and is about to start. Do any setup here. */ | |||
| void initialise(); | |||
| /** Called when a block receives a MIDI message. */ | |||
| void handleMIDI (int byte0, int byte1, int byte2); | |||
| /** Called when a block receives a message. | |||
| @see sendMessageToBlock | |||
| */ | |||
| void handleMessage (int data0, int data1, int data2); | |||
| @@ -65,9 +65,16 @@ Block::~Block() {} | |||
| void Block::addDataInputPortListener (DataInputPortListener* listener) { dataInputPortListeners.add (listener); } | |||
| void Block::removeDataInputPortListener (DataInputPortListener* listener) { dataInputPortListeners.remove (listener); } | |||
| void Block::addProgramEventListener (ProgramEventListener* listener) { programEventListeners.add (listener); } | |||
| void Block::removeProgramEventListener (ProgramEventListener* listener) { programEventListeners.remove (listener); } | |||
| bool Block::ConnectionPort::operator== (const ConnectionPort& other) const noexcept { return edge == other.edge && index == other.index; } | |||
| bool Block::ConnectionPort::operator!= (const ConnectionPort& other) const noexcept { return ! operator== (other); } | |||
| Block::Program::Program (Block& b) : block (b) {} | |||
| Block::Program::~Program() {} | |||
| //============================================================================== | |||
| TouchSurface::TouchSurface (Block& b) : block (b) {} | |||
| TouchSurface::~TouchSurface() {} | |||
| @@ -91,9 +98,6 @@ void ControlButton::removeListener (Listener* l) { listeners.remove (l); | |||
| LEDGrid::LEDGrid (Block& b) : block (b) {} | |||
| LEDGrid::~LEDGrid() {} | |||
| LEDGrid::Program::Program (LEDGrid& l) : ledGrid (l) {} | |||
| LEDGrid::Program::~Program() {} | |||
| LEDGrid::Renderer::~Renderer() {} | |||
| //============================================================================== | |||
| @@ -184,6 +184,94 @@ public: | |||
| /** Returns a list of the connectors that this device has. */ | |||
| virtual juce::Array<ConnectionPort> getPorts() const = 0; | |||
| //============================================================================== | |||
| /** A program that can be loaded onto a block. */ | |||
| struct Program | |||
| { | |||
| /** Creates a Program for the corresponding LEDGrid. */ | |||
| Program (Block&); | |||
| /** Destructor. */ | |||
| virtual ~Program(); | |||
| /** Returns the LittleFoot program to execute on the BLOCKS device. */ | |||
| virtual juce::String getLittleFootProgram() = 0; | |||
| Block& block; | |||
| }; | |||
| /** Sets the Program to run on this block. | |||
| The supplied Program's lifetime will be managed by this class, so do not | |||
| use the Program in other places in your code. | |||
| */ | |||
| virtual juce::Result setProgram (Program*) = 0; | |||
| /** Returns a pointer to the currently loaded program. */ | |||
| virtual Program* getProgram() const = 0; | |||
| //============================================================================== | |||
| /** A message that can be sent to the currently loaded program. */ | |||
| struct ProgramEventMessage | |||
| { | |||
| int32 values[3]; | |||
| }; | |||
| /** Sends a message to the currently loaded program. | |||
| To receive the message the program must provide a littlefoot function called | |||
| handleMessage with the following form: | |||
| @code | |||
| void handleMessage (int param1, int param2) | |||
| { | |||
| // Do something with the two integer parameters that the app has sent... | |||
| } | |||
| @endcode | |||
| */ | |||
| virtual void sendProgramEvent (const ProgramEventMessage&) = 0; | |||
| /** Interface for objects listening to custom program events. */ | |||
| struct ProgramEventListener | |||
| { | |||
| virtual ~ProgramEventListener() {} | |||
| /** Called whenever a message from a block is received. */ | |||
| virtual void handleProgramEvent (Block& source, const ProgramEventMessage&) = 0; | |||
| }; | |||
| /** Adds a new listener for custom program events from the block. */ | |||
| virtual void addProgramEventListener (ProgramEventListener*); | |||
| /** Removes a listener for custom program events from the block. */ | |||
| virtual void removeProgramEventListener (ProgramEventListener*); | |||
| //============================================================================== | |||
| /** Returns the size of the data block that setDataByte and other functions can write to. */ | |||
| virtual uint32 getMemorySize() = 0; | |||
| /** Sets a single byte on the littlefoot heap. */ | |||
| virtual void setDataByte (size_t offset, uint8 value) = 0; | |||
| /** Sets multiple bytes on the littlefoot heap. */ | |||
| virtual void setDataBytes (size_t offset, const void* data, size_t num) = 0; | |||
| /** Sets multiple bits on the littlefoot heap. */ | |||
| virtual void setDataBits (uint32 startBit, uint32 numBits, uint32 value) = 0; | |||
| /** Gets a byte from the littlefoot heap. */ | |||
| virtual uint8 getDataByte (size_t offset) = 0; | |||
| /** Sets the current program as the block's default state. */ | |||
| virtual void saveProgramAsDefault() = 0; | |||
| //============================================================================== | |||
| /** Allows the user to provide a function that will receive log messages from the block. */ | |||
| virtual void setLogger (std::function<void(const String&)> loggingCallback) = 0; | |||
| /** Sends a firmware update packet to a block, and waits for a reply. Returns an error code. */ | |||
| virtual bool sendFirmwareUpdatePacket (const uint8* data, uint8 size, | |||
| std::function<void (uint8)> packetAckCallback) = 0; | |||
| //============================================================================== | |||
| /** Interface for objects listening to input data port. */ | |||
| struct DataInputPortListener | |||
| @@ -194,10 +282,10 @@ public: | |||
| virtual void handleIncomingDataPortMessage (Block& source, const void* messageData, size_t messageSize) = 0; | |||
| }; | |||
| /** Adds a new listener of data input port. */ | |||
| /** Adds a new listener for the data input port. */ | |||
| virtual void addDataInputPortListener (DataInputPortListener*); | |||
| /** Removes a listener of data input port. */ | |||
| /** Removes a listener for the data input port. */ | |||
| virtual void removeDataInputPortListener (DataInputPortListener*); | |||
| /** Sends a message to the block. */ | |||
| @@ -214,6 +302,7 @@ protected: | |||
| Block (const juce::String& serialNumberToUse); | |||
| juce::ListenerList<DataInputPortListener> dataInputPortListeners; | |||
| juce::ListenerList<ProgramEventListener> programEventListeners; | |||
| private: | |||
| //============================================================================== | |||
| @@ -73,95 +73,32 @@ public: | |||
| /** Returns the number of rows in the LED grid. */ | |||
| virtual int getNumRows() const = 0; | |||
| /** A program that can be loaded onto an LEDGrid. | |||
| This class facilitates the execution of a LittleFoot program on a BLOCKS | |||
| device with an LEDGrid. | |||
| */ | |||
| struct Program | |||
| { | |||
| /** Creates a Program for the corresponding LEDGrid. */ | |||
| Program (LEDGrid&); | |||
| /** Destructor. */ | |||
| virtual ~Program(); | |||
| /** Returns the LittleFoot program to execute on the BLOCKS device. */ | |||
| virtual juce::String getLittleFootProgram() = 0; | |||
| /** Sets the size of the shared area of memory used to communicate with | |||
| the host computer. | |||
| */ | |||
| virtual uint32 getHeapSize() = 0; | |||
| LEDGrid& ledGrid; | |||
| }; | |||
| /** Sets the Program to run on this LEDGrid. | |||
| The supplied Program's lifetime will be managed by this class, so do not | |||
| use the Program in other places in your code. | |||
| */ | |||
| virtual juce::Result setProgram (Program*) = 0; | |||
| /** Returns a pointer to the currently loaded program. */ | |||
| virtual Program* getProgram() const = 0; | |||
| /** A message that can be sent to the currently loaded program. */ | |||
| struct ProgramEventMessage | |||
| { | |||
| int32 values[2]; | |||
| }; | |||
| /** Sends a message to the currently loaded program. | |||
| To receive the message the program must provide a function called | |||
| handleMessage with the following form: | |||
| @code | |||
| void handleMessage (int param1, int param2) | |||
| { | |||
| // Do something with the two integer parameters that the app has sent... | |||
| } | |||
| @endcode | |||
| */ | |||
| virtual void sendProgramEvent (const ProgramEventMessage&) = 0; | |||
| /** Sets a single byte on the heap. */ | |||
| virtual void setDataByte (size_t offset, uint8 value) = 0; | |||
| /** Sets multiple bytes on the heap. */ | |||
| virtual void setDataBytes (size_t offset, const void* data, size_t num) = 0; | |||
| /** Sets multiple bits on the heap. */ | |||
| virtual void setDataBits (uint32 startBit, uint32 numBits, uint32 value) = 0; | |||
| /** Gets a byte from the heap. */ | |||
| virtual uint8 getDataByte (size_t offset) = 0; | |||
| /** Sets the current program as the block's default state. */ | |||
| virtual void saveProgramAsDefault() = 0; | |||
| //============================================================================== | |||
| struct Renderer | |||
| struct Renderer : public juce::ReferenceCountedObject | |||
| { | |||
| virtual ~Renderer(); | |||
| virtual void renderLEDGrid (LEDGrid&) = 0; | |||
| /** The Renderer class is reference-counted, so always use a Renderer::Ptr when | |||
| you are keeping references to them. | |||
| */ | |||
| using Ptr = juce::ReferenceCountedObjectPtr<Renderer>; | |||
| }; | |||
| /** Set the visualiser that will create visuals for this block (nullptr for none). | |||
| Note that the LEDGrid will NOT take ownership of this object, so the caller | |||
| must ensure that it doesn't get deleted while in use here. | |||
| */ | |||
| void setRenderer (Renderer* newRenderer) noexcept { renderer = newRenderer; } | |||
| void setRenderer (Renderer::Ptr newRenderer) noexcept { renderer = newRenderer; } | |||
| /** Returns the visualiser currently attached to this block (nullptr for none). */ | |||
| Renderer* getRenderer() const noexcept { return renderer; } | |||
| Renderer::Ptr getRenderer() const noexcept { return renderer; } | |||
| /** The device that this LEDGrid belongs to. */ | |||
| Block& block; | |||
| private: | |||
| Renderer* renderer = nullptr; | |||
| Renderer::Ptr renderer; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LEDGrid) | |||
| }; | |||
| @@ -39,9 +39,10 @@ using namespace juce; | |||
| */ | |||
| struct Compiler | |||
| { | |||
| Compiler() {} | |||
| Compiler() = default; | |||
| /** | |||
| /** Gives the compiler a zero-terminated list of native function prototypes to | |||
| use when parsing function calls. | |||
| */ | |||
| void addNativeFunctions (const char* const* functionPrototypes) | |||
| { | |||
| @@ -49,7 +50,8 @@ struct Compiler | |||
| nativeFunctions.add (NativeFunction (*functionPrototypes, nullptr)); | |||
| } | |||
| /** | |||
| /** Tells the compiler to use the list of native function prototypes from | |||
| this littlefoot::Runner object. | |||
| */ | |||
| template <typename RunnerType> | |||
| void addNativeFunctions (const RunnerType& runner) | |||
| @@ -58,20 +60,22 @@ struct Compiler | |||
| nativeFunctions.add (runner.getNativeFunction (i)); | |||
| } | |||
| /** | |||
| /** Compiles a littlefoot program. | |||
| If there's an error, this returns it, otherwise the compiled bytecode is | |||
| placed in the compiledObjectCode member. | |||
| */ | |||
| Result compile (const String& sourceCode, uint32 heapSizeBytesRequired) | |||
| Result compile (const String& sourceCode, uint32 defaultHeapSize) | |||
| { | |||
| try | |||
| { | |||
| SyntaxTreeBuilder stb (sourceCode); | |||
| SyntaxTreeBuilder stb (sourceCode, nativeFunctions, defaultHeapSize); | |||
| stb.compile(); | |||
| stb.simplify(); | |||
| compiledObjectCode.clear(); | |||
| CodeGenerator codeGen (compiledObjectCode, nativeFunctions, stb.functions); | |||
| codeGen.generateCode (stb.blockBeingParsed, (heapSizeBytesRequired + 3) & ~3u); | |||
| CodeGenerator codeGen (compiledObjectCode, stb); | |||
| codeGen.generateCode (stb.blockBeingParsed, stb.heapSizeRequired); | |||
| return Result::ok(); | |||
| } | |||
| catch (String error) | |||
| @@ -80,7 +84,14 @@ struct Compiler | |||
| } | |||
| } | |||
| /** | |||
| /** After a successful compilation, this returns the finished Program. */ | |||
| Program getCompiledProgram() const noexcept | |||
| { | |||
| return Program (compiledObjectCode.begin(), (uint32) compiledObjectCode.size()); | |||
| } | |||
| /** After a successful call to compile(), this contains the bytecode generated. | |||
| A littlefoot::Program object can be created directly from this array. | |||
| */ | |||
| Array<uint8> compiledObjectCode; | |||
| @@ -102,7 +113,7 @@ private: | |||
| X(return_, "return") X(true_, "true") X(false_, "false") | |||
| #define LITTLEFOOT_OPERATORS(X) \ | |||
| X(semicolon, ";") X(dot, ".") X(comma, ",") \ | |||
| X(semicolon, ";") X(dot, ".") X(comma, ",") X(hash, "#") \ | |||
| X(openParen, "(") X(closeParen, ")") X(openBrace, "{") X(closeBrace, "}") \ | |||
| X(openBracket, "[") X(closeBracket, "]") X(colon, ":") X(question, "?") \ | |||
| X(equals, "==") X(assign, "=") X(notEquals, "!=") X(logicalNot, "!") \ | |||
| @@ -354,7 +365,8 @@ private: | |||
| //============================================================================== | |||
| struct SyntaxTreeBuilder : private TokenIterator | |||
| { | |||
| SyntaxTreeBuilder (const String& code) : TokenIterator (code) {} | |||
| SyntaxTreeBuilder (const String& code, const Array<NativeFunction>& nativeFns, uint32 defaultHeapSize) | |||
| : TokenIterator (code), nativeFunctions (nativeFns), heapSizeRequired (defaultHeapSize) {} | |||
| void compile() | |||
| { | |||
| @@ -362,6 +374,12 @@ private: | |||
| while (currentType != Token::eof) | |||
| { | |||
| if (matchIf (Token::hash)) | |||
| { | |||
| parseCompilerDirective(); | |||
| continue; | |||
| } | |||
| if (! matchesAnyTypeOrVoid()) | |||
| throwErrorExpecting ("a global variable or function"); | |||
| @@ -377,7 +395,7 @@ private: | |||
| if (type == Type::void_) | |||
| location.throwError ("A variable type cannot be 'void'"); | |||
| int arraySize = matchIf (Token::openBracket) ? parseArraySize() : 0; | |||
| int arraySize = matchIf (Token::openBracket) ? parseIntegerLiteral() : 0; | |||
| if (arraySize > 0) | |||
| location.throwError ("Arrays not yet implemented!"); | |||
| @@ -399,9 +417,29 @@ private: | |||
| f->block->simplify (*this); | |||
| } | |||
| Function* findFunction (FunctionID functionID) const noexcept | |||
| { | |||
| for (auto f : functions) | |||
| if (f->functionID == functionID) | |||
| return f; | |||
| return nullptr; | |||
| } | |||
| NativeFunction* findNativeFunction (FunctionID functionID) const noexcept | |||
| { | |||
| for (auto& f : nativeFunctions) | |||
| if (f.functionID == functionID) | |||
| return &f; | |||
| return nullptr; | |||
| } | |||
| //============================================================================== | |||
| BlockPtr blockBeingParsed = nullptr; | |||
| Array<Function*> functions; | |||
| const Array<NativeFunction>& nativeFunctions; | |||
| uint32 heapSizeRequired; | |||
| template <typename Type, typename... Args> | |||
| Type* allocate (Args... args) { auto o = new Type (args...); allAllocatedObjects.add (o); return o; } | |||
| @@ -410,10 +448,23 @@ private: | |||
| OwnedArray<AllocatedObject> allAllocatedObjects; | |||
| //============================================================================== | |||
| void parseCompilerDirective() | |||
| { | |||
| auto name = parseIdentifier(); | |||
| if (name == "heapsize") | |||
| { | |||
| match (Token::colon); | |||
| heapSizeRequired = (((uint32) parseIntegerLiteral()) + 3) & ~3u; | |||
| return; | |||
| } | |||
| location.throwError ("Unknown compiler directive"); | |||
| } | |||
| void parseFunctionDeclaration (Type returnType, const String& name) | |||
| { | |||
| auto f = allocate<Function>(); | |||
| functions.add (f); | |||
| while (matchesAnyType()) | |||
| { | |||
| @@ -431,6 +482,12 @@ private: | |||
| match (Token::closeParen); | |||
| f->functionID = createFunctionID (name, returnType, f->getArgumentTypes()); | |||
| if (findFunction (f->functionID) != nullptr || findNativeFunction (f->functionID) != nullptr) | |||
| location.throwError ("Duplicate function declaration"); | |||
| functions.add (f); | |||
| f->block = parseBlock (true); | |||
| f->returnType = returnType; | |||
| @@ -443,12 +500,11 @@ private: | |||
| } | |||
| } | |||
| int parseArraySize() | |||
| int parseIntegerLiteral() | |||
| { | |||
| auto e = parseExpression(); | |||
| e->simplify (*this); | |||
| if (auto literal = dynamic_cast<LiteralValue*> (e)) | |||
| if (auto literal = dynamic_cast<LiteralValue*> (e->simplify (*this))) | |||
| { | |||
| if (literal->value.isInt() || literal->value.isInt64()) | |||
| { | |||
| @@ -459,7 +515,7 @@ private: | |||
| } | |||
| } | |||
| location.throwError ("An array size must be a constant integer"); | |||
| location.throwError ("Expected an integer constant"); | |||
| return 0; | |||
| } | |||
| @@ -618,8 +674,8 @@ private: | |||
| { | |||
| if (currentType == Token::identifier) return parseSuffixes (allocate<Identifier> (location, blockBeingParsed, parseIdentifier())); | |||
| if (matchIf (Token::openParen)) return parseSuffixes (matchCloseParen (parseExpression())); | |||
| if (matchIf (Token::true_)) return parseSuffixes (allocate<LiteralValue> (location, blockBeingParsed, (int) 1)); | |||
| if (matchIf (Token::false_)) return parseSuffixes (allocate<LiteralValue> (location, blockBeingParsed, (int) 0)); | |||
| if (matchIf (Token::true_)) return parseSuffixes (allocate<LiteralValue> (location, blockBeingParsed, true)); | |||
| if (matchIf (Token::false_)) return parseSuffixes (allocate<LiteralValue> (location, blockBeingParsed, false)); | |||
| if (currentType == Token::literal) | |||
| { | |||
| @@ -835,12 +891,12 @@ private: | |||
| //============================================================================== | |||
| struct CodeGenerator | |||
| { | |||
| CodeGenerator (Array<uint8>& output, const Array<NativeFunction>& nativeFns, const Array<Function*>& fns) | |||
| : outputCode (output), nativeFunctions (nativeFns), functions (fns) {} | |||
| CodeGenerator (Array<uint8>& output, const SyntaxTreeBuilder& stb) | |||
| : outputCode (output), syntaxTree (stb) {} | |||
| void generateCode (BlockPtr outerBlock, uint32 heapSizeBytesRequired) | |||
| { | |||
| for (auto f : functions) | |||
| for (auto f : syntaxTree.functions) | |||
| { | |||
| f->address = createMarker(); | |||
| f->unwindAddress = createMarker(); | |||
| @@ -848,16 +904,19 @@ private: | |||
| emit ((int16) 0); // checksum | |||
| emit ((int16) 0); // size | |||
| emit ((int16) functions.size()); | |||
| emit ((int16) syntaxTree.functions.size()); | |||
| emit ((int16) outerBlock->variables.size()); | |||
| emit ((int16) heapSizeBytesRequired); | |||
| for (auto f : functions) | |||
| for (auto f : syntaxTree.functions) | |||
| emit (f->functionID, f->address); | |||
| for (auto f : functions) | |||
| auto codeStart = outputCode.size(); | |||
| for (auto f : syntaxTree.functions) | |||
| f->emit (*this); | |||
| removeJumpsToNextInstruction (codeStart); | |||
| resolveMarkers(); | |||
| Program::writeInt16 (outputCode.begin() + 2, (int16) outputCode.size()); | |||
| @@ -868,39 +927,92 @@ private: | |||
| //============================================================================== | |||
| Array<uint8>& outputCode; | |||
| const Array<NativeFunction>& nativeFunctions; | |||
| const Array<Function*>& functions; | |||
| const SyntaxTreeBuilder& syntaxTree; | |||
| struct Marker { int index = 0; }; | |||
| struct MarkerAndAddress { int markerIndex, address; }; | |||
| struct MarkerAndAddress { Marker marker; int address; }; | |||
| int nextMarker = 0; | |||
| Array<MarkerAndAddress> markersToResolve, resolvedMarkers; | |||
| Marker createMarker() noexcept { Marker m; m.index = ++nextMarker; return m; } | |||
| void attachMarker (Marker m) { resolvedMarkers.add ({ m.index, outputCode.size() }); } | |||
| void attachMarker (Marker m) { resolvedMarkers.add ({ m, outputCode.size() }); } | |||
| int getResolvedMarkerAddress (int markerIndex) const | |||
| int getResolvedMarkerAddress (Marker marker) const | |||
| { | |||
| for (auto m : resolvedMarkers) | |||
| if (m.markerIndex == markerIndex) | |||
| if (m.marker.index == marker.index) | |||
| return m.address; | |||
| jassertfalse; | |||
| return 0; | |||
| } | |||
| Marker getMarkerAtAddress (int address) const noexcept | |||
| { | |||
| for (auto m : markersToResolve) | |||
| if (m.address == address) | |||
| return m.marker; | |||
| jassertfalse; | |||
| return {}; | |||
| } | |||
| void resolveMarkers() | |||
| { | |||
| for (auto m : markersToResolve) | |||
| Program::writeInt16 (outputCode.begin() + m.address, (int16) getResolvedMarkerAddress (m.markerIndex)); | |||
| Program::writeInt16 (outputCode.begin() + m.address, (int16) getResolvedMarkerAddress (m.marker)); | |||
| } | |||
| void removeCode (int address, int size) | |||
| { | |||
| outputCode.removeRange (address, size); | |||
| for (int i = markersToResolve.size(); --i >= 0;) | |||
| { | |||
| auto& m = markersToResolve.getReference (i); | |||
| if (m.address >= address + size) | |||
| m.address -= size; | |||
| else if (m.address >= address) | |||
| markersToResolve.remove (i); | |||
| } | |||
| for (auto& m : resolvedMarkers) | |||
| if (m.address >= address + size) | |||
| m.address -= size; | |||
| } | |||
| void removeJumpsToNextInstruction (int address) | |||
| { | |||
| while (address < outputCode.size()) | |||
| { | |||
| auto op = (OpCode) outputCode.getUnchecked (address); | |||
| auto opSize = 1 + Program::getNumExtraBytesForOpcode (op); | |||
| if (op == OpCode::jump) | |||
| { | |||
| auto marker = getMarkerAtAddress (address + 1); | |||
| if (marker.index != 0) | |||
| { | |||
| if (getResolvedMarkerAddress (marker) == address + opSize) | |||
| { | |||
| removeCode (address, opSize); | |||
| continue; | |||
| } | |||
| } | |||
| } | |||
| address += opSize; | |||
| } | |||
| } | |||
| Marker breakTarget, continueTarget; | |||
| //============================================================================== | |||
| void emit (OpCode op) { emit ((int8) op); } | |||
| void emit (Marker m) { markersToResolve.add ({ m.index, outputCode.size() }); emit ((int16) 0); } | |||
| void emit (Marker m) { markersToResolve.add ({ m, outputCode.size() }); emit ((int16) 0); } | |||
| void emit (int8 value) { outputCode.add ((uint8) value); } | |||
| void emit (int16 value) { uint8 d[2]; Program::writeInt16 (d, value); outputCode.insertArray (-1, d, (int) sizeof (d)); } | |||
| void emit (int32 value) { uint8 d[4]; Program::writeInt32 (d, value); outputCode.insertArray (-1, d, (int) sizeof (d)); } | |||
| @@ -955,7 +1067,7 @@ private: | |||
| index += stackDepth; | |||
| if (index == 0) | |||
| emit ((OpCode) ((int) OpCode::dup)); | |||
| emit (OpCode::dup); | |||
| else if (index < 8) | |||
| emit ((OpCode) ((int) OpCode::dupOffset_01 + index - 1)); | |||
| else if (index >= 128) | |||
| @@ -966,25 +1078,6 @@ private: | |||
| emitCast (sourceType, requiredType, location); | |||
| } | |||
| //============================================================================== | |||
| Function* findFunction (FunctionID functionID) const noexcept | |||
| { | |||
| for (auto f : functions) | |||
| if (f->functionID == functionID) | |||
| return f; | |||
| return nullptr; | |||
| } | |||
| NativeFunction* findNativeFunction (FunctionID functionID) const noexcept | |||
| { | |||
| for (auto& f : nativeFunctions) | |||
| if (f.functionID == functionID) | |||
| return &f; | |||
| return nullptr; | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| @@ -1385,9 +1478,7 @@ private: | |||
| else if (fn->returnType != Type::void_) | |||
| location.throwError ("Cannot return a value from a void function"); | |||
| if (parentBlock->statements.getLast() != this) | |||
| cg.emit (OpCode::jump, fn->unwindAddress); | |||
| cg.emit (OpCode::jump, fn->unwindAddress); | |||
| return; | |||
| } | |||
| @@ -1798,7 +1889,7 @@ private: | |||
| auto functionID = getFunctionID (cg); | |||
| if (auto fn = cg.findFunction (functionID)) | |||
| if (auto fn = cg.syntaxTree.findFunction (functionID)) | |||
| { | |||
| emitArgs (cg, fn->getArgumentTypes(), stackDepth); | |||
| cg.emit (OpCode::call, fn->address); | |||
| @@ -1806,7 +1897,7 @@ private: | |||
| return; | |||
| } | |||
| if (auto nativeFn = cg.findNativeFunction (functionID)) | |||
| if (auto nativeFn = cg.syntaxTree.findNativeFunction (functionID)) | |||
| { | |||
| emitArgs (cg, getArgTypesFromFunctionName (nativeFn->nameAndArguments), stackDepth); | |||
| cg.emit (OpCode::callNative, nativeFn->functionID); | |||
| @@ -1836,10 +1927,10 @@ private: | |||
| auto functionID = getFunctionID (cg); | |||
| if (auto fn = cg.findFunction (functionID)) | |||
| if (auto fn = cg.syntaxTree.findFunction (functionID)) | |||
| return fn->returnType; | |||
| if (auto nativeFn = cg.findNativeFunction (functionID)) | |||
| if (auto nativeFn = cg.syntaxTree.findNativeFunction (functionID)) | |||
| return nativeFn->returnType; | |||
| if (auto b = findBuiltInFunction (functionID)) | |||
| @@ -52,16 +52,35 @@ struct LittleFootRemoteHeap | |||
| void clear() noexcept | |||
| { | |||
| zeromem (targetData, sizeof (targetData)); | |||
| invalidateData(); | |||
| } | |||
| void resetDeviceStateToUnknown() | |||
| { | |||
| invalidateData(); | |||
| messagesSent.clear(); | |||
| resetDataRangeToUnknown (0, ImplementationClass::maxBlockSize); | |||
| } | |||
| void resetDataRangeToUnknown (size_t offset, size_t size) noexcept | |||
| { | |||
| auto* latestState = getLatestExpectedDataState(); | |||
| for (size_t i = 0; i < size; ++i) | |||
| latestState[offset + i] = unknownByte; | |||
| } | |||
| void setByte (size_t offset, uint8 value) noexcept | |||
| { | |||
| if (offset < blockSize) | |||
| { | |||
| if (targetData [offset] != value) | |||
| if (targetData[offset] != value) | |||
| { | |||
| targetData [offset] = value; | |||
| invalidateData(); | |||
| targetData[offset] = value; | |||
| needsSyncing = true; | |||
| if (programStateKnown && offset < programSize) | |||
| programStateKnown = false; | |||
| } | |||
| } | |||
| else | |||
| @@ -100,37 +119,31 @@ struct LittleFootRemoteHeap | |||
| return 0; | |||
| } | |||
| void invalidateData() | |||
| void invalidateData() noexcept | |||
| { | |||
| dataHasChanged = true; | |||
| needsSyncing = true; | |||
| programStateKnown = false; | |||
| } | |||
| void sendChanges (ImplementationClass& bi) | |||
| bool isFullySynced() const noexcept | |||
| { | |||
| if (dataHasChanged && messagesSent.isEmpty()) | |||
| return ! needsSyncing; | |||
| } | |||
| void sendChanges (ImplementationClass& bi, bool forceSend) | |||
| { | |||
| if ((needsSyncing && messagesSent.isEmpty()) || forceSend) | |||
| { | |||
| for (;;) | |||
| for (int maxChanges = 30; --maxChanges >= 0;) | |||
| { | |||
| uint16 data[ImplementationClass::maxBlockSize]; | |||
| uint32 packetIndex; | |||
| if (messagesSent.isEmpty()) | |||
| { | |||
| for (uint32 i = 0; i < blockSize; ++i) | |||
| data[i] = deviceState[i]; | |||
| packetIndex = lastPacketIndexReceived; | |||
| } | |||
| else | |||
| { | |||
| auto& lastPacket = messagesSent.getReference (messagesSent.size() - 1); | |||
| auto* latestState = getLatestExpectedDataState(); | |||
| for (uint32 i = 0; i < blockSize; ++i) | |||
| data[i] = lastPacket.resultDataState[i]; | |||
| for (uint32 i = 0; i < blockSize; ++i) | |||
| data[i] = latestState[i]; | |||
| packetIndex = lastPacket.packetIndex; | |||
| } | |||
| uint32 packetIndex = messagesSent.isEmpty() ? lastPacketIndexReceived | |||
| : messagesSent.getLast()->packetIndex; | |||
| packetIndex = (packetIndex + 1) & ImplementationClass::maxPacketCounter; | |||
| @@ -141,14 +154,14 @@ struct LittleFootRemoteHeap | |||
| } | |||
| } | |||
| for (auto& m : messagesSent) | |||
| for (auto* m : messagesSent) | |||
| { | |||
| if (m.dispatchTime >= Time::getCurrentTime() - RelativeTime::milliseconds (250)) | |||
| if (m->dispatchTime >= Time::getCurrentTime() - RelativeTime::milliseconds (250)) | |||
| break; | |||
| m.dispatchTime = Time::getCurrentTime(); | |||
| bi.sendMessageToDevice (m.packet); | |||
| //DBG ("Sending packet " << (int) m.packetIndex << " - " << m.packet.size() << " bytes, device " << bi.getDeviceIndex()); | |||
| m->dispatchTime = Time::getCurrentTime(); | |||
| bi.sendMessageToDevice (m->packet); | |||
| //DBG ("Sending packet " << (int) m->packetIndex << " - " << m->packet.size() << " bytes, device " << bi.getDeviceIndex()); | |||
| if (getTotalSizeOfMessagesSent() > 200) | |||
| break; | |||
| @@ -157,7 +170,7 @@ struct LittleFootRemoteHeap | |||
| void handleACKFromDevice (ImplementationClass& bi, uint32 packetIndex) noexcept | |||
| { | |||
| //DBG ("ACK " << (int) packetIndex); | |||
| //DBG ("ACK " << (int) packetIndex << " device " << (int) bi.getDeviceIndex()); | |||
| if (lastPacketIndexReceived != packetIndex) | |||
| { | |||
| @@ -165,7 +178,7 @@ struct LittleFootRemoteHeap | |||
| for (int i = messagesSent.size(); --i >= 0;) | |||
| { | |||
| auto& m = messagesSent.getReference(i); | |||
| auto& m = *messagesSent.getUnchecked(i); | |||
| if (m.packetIndex == packetIndex) | |||
| { | |||
| @@ -174,10 +187,10 @@ struct LittleFootRemoteHeap | |||
| messagesSent.removeRange (0, i + 1); | |||
| dumpStatus(); | |||
| sendChanges (bi); | |||
| sendChanges (bi, false); | |||
| if (messagesSent.isEmpty()) | |||
| dataHasChanged = false; | |||
| needsSyncing = false; | |||
| return; | |||
| } | |||
| @@ -191,17 +204,16 @@ struct LittleFootRemoteHeap | |||
| { | |||
| if (! programStateKnown) | |||
| { | |||
| programStateKnown = true; | |||
| uint8 deviceMemory[ImplementationClass::maxBlockSize]; | |||
| bool anyUnknowns = false; | |||
| for (size_t i = 0; i < blockSize; ++i) | |||
| { | |||
| anyUnknowns = (deviceState[i] > 255); | |||
| deviceMemory[i] = (uint8) deviceState[i]; | |||
| } | |||
| programLoaded = ! anyUnknowns && littlefoot::Program (deviceMemory, (uint32) blockSize).checksumMatches(); | |||
| programStateKnown = true; | |||
| littlefoot::Program prog (deviceMemory, (uint32) blockSize); | |||
| programLoaded = prog.checksumMatches(); | |||
| programSize = prog.getProgramSize(); | |||
| } | |||
| return programLoaded; | |||
| @@ -214,15 +226,13 @@ struct LittleFootRemoteHeap | |||
| private: | |||
| uint16 deviceState[ImplementationClass::maxBlockSize]; | |||
| uint8 targetData[ImplementationClass::maxBlockSize] = { 0 }; | |||
| bool dataHasChanged = true, programStateKnown = true, programLoaded = false; | |||
| uint32 programSize = 0; | |||
| bool needsSyncing = true, programStateKnown = true, programLoaded = false; | |||
| void resetDeviceStateToUnknown() | |||
| uint16* getLatestExpectedDataState() noexcept | |||
| { | |||
| invalidateData(); | |||
| messagesSent.clear(); | |||
| for (uint32 i = 0; i < ImplementationClass::maxBlockSize; ++i) | |||
| deviceState[i] = unknownByte; | |||
| return messagesSent.isEmpty() ? deviceState | |||
| : messagesSent.getLast()->resultDataState; | |||
| } | |||
| struct ChangeMessage | |||
| @@ -233,16 +243,16 @@ private: | |||
| uint16 resultDataState[ImplementationClass::maxBlockSize]; | |||
| }; | |||
| Array<ChangeMessage> messagesSent; | |||
| OwnedArray<ChangeMessage> messagesSent; | |||
| uint32 lastPacketIndexReceived = 0; | |||
| int getTotalSizeOfMessagesSent() const noexcept | |||
| { | |||
| int total = 0; | |||
| for (auto& m : messagesSent) | |||
| if (m.dispatchTime != Time()) | |||
| total += m.packet.size(); | |||
| for (auto* m : messagesSent) | |||
| if (m->dispatchTime != Time()) | |||
| total += m->packet.size(); | |||
| return total; | |||
| } | |||
| @@ -280,6 +290,8 @@ private: | |||
| Diff (uint16* current, const uint8* target, size_t blockSizeToUse) | |||
| : newData (target), blockSize (blockSizeToUse) | |||
| { | |||
| ranges.ensureStorageAllocated ((int) blockSize); | |||
| for (int i = 0; i < (int) blockSize; ++i) | |||
| ranges.add ({ i, 1, newData[i] == current[i], false }); | |||
| @@ -290,7 +302,7 @@ private: | |||
| bool createChangeMessage (const ImplementationClass& bi, | |||
| const uint16* currentState, | |||
| Array<ChangeMessage>& messagesCreated, | |||
| OwnedArray<ChangeMessage>& messagesCreated, | |||
| uint32 nextPacketIndex) | |||
| { | |||
| if (ranges.isEmpty()) | |||
| @@ -301,8 +313,7 @@ private: | |||
| if (deviceIndex < 0) | |||
| return false; | |||
| messagesCreated.add ({}); | |||
| auto& message = messagesCreated.getReference (messagesCreated.size() - 1); | |||
| auto& message = *messagesCreated.add (new ChangeMessage()); | |||
| message.packetIndex = nextPacketIndex; | |||
| @@ -366,27 +377,27 @@ private: | |||
| void coalesceUniformRegions() | |||
| { | |||
| for (int i = 0; i < ranges.size() - 1; ++i) | |||
| for (int i = ranges.size(); --i > 0;) | |||
| { | |||
| auto& r1 = ranges.getReference (i); | |||
| auto r2 = ranges.getReference (i + 1); | |||
| auto& r1 = ranges.getReference (i - 1); | |||
| auto r2 = ranges.getReference (i); | |||
| if (r1.isSkipped == r2.isSkipped | |||
| && (r1.isSkipped || newData[r1.index] == newData[r2.index])) | |||
| { | |||
| r1.length += r2.length; | |||
| ranges.remove (i + 1); | |||
| i = jmax (0, i - 2); | |||
| ranges.remove (i); | |||
| i = jmin (ranges.size() - 1, i + 1); | |||
| } | |||
| } | |||
| } | |||
| void coalesceSequences() | |||
| { | |||
| for (int i = 0; i < ranges.size() - 1; ++i) | |||
| for (int i = ranges.size(); --i > 0;) | |||
| { | |||
| auto& r1 = ranges.getReference (i); | |||
| auto r2 = ranges.getReference (i + 1); | |||
| auto& r1 = ranges.getReference (i - 1); | |||
| auto r2 = ranges.getReference (i); | |||
| if (! (r1.isSkipped || r2.isSkipped) | |||
| && (r1.isMixed || r1.length == 1) | |||
| @@ -396,8 +407,8 @@ private: | |||
| { | |||
| r1.length += r2.length; | |||
| r1.isMixed = true; | |||
| ranges.remove (i + 1); | |||
| i = jmax (0, i - 2); | |||
| ranges.remove (i); | |||
| i = jmin (ranges.size() - 1, i + 1); | |||
| } | |||
| } | |||
| } | |||
| @@ -44,11 +44,11 @@ namespace littlefoot | |||
| #define LITTLEFOOT_DUMP_PROGRAM 0 | |||
| #endif | |||
| using int8 = char; | |||
| using int8 = signed char; | |||
| using uint8 = unsigned char; | |||
| using int16 = short; | |||
| using int16 = signed short; | |||
| using uint16 = unsigned short; | |||
| using int32 = int; | |||
| using int32 = signed int; | |||
| using uint32 = unsigned int; | |||
| using FunctionID = int16; | |||
| @@ -180,8 +180,8 @@ struct NativeFunction | |||
| jassert (nameAndArgTypes[slash + 1] != 0); // The slash must be followed by a return type character | |||
| jassert (juce::String (nameAndArgTypes).substring (0, slash).containsOnly ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_")); | |||
| jassert (! juce::String ("0123456789").containsChar (nameAndArgTypes[0])); | |||
| jassert (juce::String (nameAndArgTypes).substring (slash + 1).containsOnly ("vif")); | |||
| jassert (juce::String (nameAndArgTypes).substring (slash + 2).containsOnly ("if")); // arguments must only be of these types | |||
| jassert (juce::String (nameAndArgTypes).substring (slash + 1).containsOnly ("vifb")); | |||
| jassert (juce::String (nameAndArgTypes).substring (slash + 2).containsOnly ("ifb")); // arguments must only be of these types | |||
| for (; nameAndArgTypes[i] != 0; ++i) | |||
| if (i != slash + 1) | |||
| @@ -247,13 +247,6 @@ struct Program | |||
| return calculateChecksum() == getStoredChecksum(); | |||
| } | |||
| uint32 getProgramSize() const noexcept | |||
| { | |||
| auto size = (uint16) readInt16 (programStart + 2); | |||
| return size < programHeaderSize ? programHeaderSize | |||
| : (size > maxProgramSize ? maxProgramSize : size); | |||
| } | |||
| uint32 getNumFunctions() const noexcept | |||
| { | |||
| return (uint16) readInt16 (programStart + 4); | |||
| @@ -286,10 +279,11 @@ struct Program | |||
| : getFunctionStartAddress (functionIndex); | |||
| } | |||
| /** Returns the number of global variables the program uses */ | |||
| uint16 getNumGlobals() const noexcept | |||
| uint32 getProgramSize() const noexcept | |||
| { | |||
| return (uint16) readInt16 (programStart + 6); | |||
| auto size = (uint16) readInt16 (programStart + 2); | |||
| return size < programHeaderSize ? programHeaderSize | |||
| : (size > maxProgramSize ? maxProgramSize : size); | |||
| } | |||
| /** Returns the number of bytes of heap space the program needs */ | |||
| @@ -298,6 +292,17 @@ struct Program | |||
| return (uint16) readInt16 (programStart + 8); | |||
| } | |||
| /** Returns the number of global variables the program uses */ | |||
| uint16 getNumGlobals() const noexcept | |||
| { | |||
| return (uint16) readInt16 (programStart + 6); | |||
| } | |||
| uint32 getTotalSpaceNeeded() const noexcept | |||
| { | |||
| return getProgramSize() + getHeapSizeBytes(); | |||
| } | |||
| #if JUCE_DEBUG | |||
| //============================================================================== | |||
| /** Prints the assembly code for a given function. */ | |||
| @@ -307,7 +312,7 @@ struct Program | |||
| << " (" << juce::String::toHexString (getFunctionID (functionIndex)) << ")" << juce::newLine; | |||
| if (auto codeStart = getFunctionStartAddress (functionIndex)) | |||
| if (auto codeEnd = getFunctionEndAddress (functionIndex)) | |||
| if (auto codeEnd = getFunctionEndAddress (functionIndex)) | |||
| for (auto prog = codeStart; prog < codeEnd;) | |||
| out << getOpDisassembly (prog) << juce::newLine; | |||
| } | |||
| @@ -339,19 +344,41 @@ struct Program | |||
| /** Calls dumpFunctionDisassembly() for all functions. */ | |||
| void dumpAllFunctions (juce::OutputStream& out) const | |||
| { | |||
| DBG ("Program size: " << (int) getProgramSize() << " bytes"); | |||
| if (programStart != nullptr) | |||
| { | |||
| DBG ("Program size: " << (int) getProgramSize() << " bytes"); | |||
| for (uint32 i = 0; i < getNumFunctions(); ++i) | |||
| dumpFunctionDisassembly (out, i); | |||
| for (uint32 i = 0; i < getNumFunctions(); ++i) | |||
| dumpFunctionDisassembly (out, i); | |||
| } | |||
| } | |||
| #endif | |||
| /** For a given op code, this returns the number of program bytes that follow it. */ | |||
| static uint8 getNumExtraBytesForOpcode (OpCode op) noexcept | |||
| { | |||
| switch (op) | |||
| { | |||
| #define LITTLEFOOT_OP(name) case OpCode::name: return 0; | |||
| #define LITTLEFOOT_OP_INT8(name) case OpCode::name: return 1; | |||
| #define LITTLEFOOT_OP_INT16(name) case OpCode::name: return 2; | |||
| #define LITTLEFOOT_OP_INT32(name) case OpCode::name: return 4; | |||
| LITTLEFOOT_OPCODES (LITTLEFOOT_OP, LITTLEFOOT_OP_INT8, LITTLEFOOT_OP_INT16, LITTLEFOOT_OP_INT32) | |||
| #undef LITTLEFOOT_OP | |||
| #undef LITTLEFOOT_OP_INT8 | |||
| #undef LITTLEFOOT_OP_INT16 | |||
| #undef LITTLEFOOT_OP_INT32 | |||
| default: jassertfalse; return 0; | |||
| } | |||
| } | |||
| //============================================================================== | |||
| static float intToFloat (int32 value) noexcept { float v; copyFloatMem (&v, &value); return v; } | |||
| static int32 floatToInt (float value) noexcept { int32 v; copyFloatMem (&v, &value); return v; } | |||
| static int16 readInt16 (const uint8* d) noexcept { return (int16) (d[0] + (((uint16) d[1]) << 8)); } | |||
| static int32 readInt32 (const uint8* d) noexcept { return (int32) ((uint32) (uint16) readInt16 (d) + (((uint32) (uint16) readInt16 (d + 2)) << 16)); } | |||
| static int16 readInt16 (const uint8* d) noexcept { return (int16) (d[0] | (((uint16) d[1]) << 8)); } | |||
| static int32 readInt32 (const uint8* d) noexcept { return (int32) (d[0] | (((uint32) d[1]) << 8) | (((uint32) d[2]) << 16) | (((uint32) d[3]) << 24)); } | |||
| static void writeInt16 (uint8* d, int16 v) noexcept { d[0] = (uint8) v; d[1] = (uint8) (v >> 8); } | |||
| static void writeInt32 (uint8* d, int32 v) noexcept { d[0] = (uint8) v; d[1] = (uint8) (v >> 8); d[2] = (uint8) (v >> 16); d[3] = (uint8) (v >> 24); } | |||
| @@ -416,6 +443,16 @@ struct Runner | |||
| allMemory[i] = 0; | |||
| } | |||
| /** Clears all the non-program data. */ | |||
| void clearHeapAndGlobals() noexcept | |||
| { | |||
| auto* start = getProgramAndDataStart() + program.getProgramSize(); | |||
| auto* end = allMemory + sizeof (allMemory); | |||
| for (auto m = start; m < end; ++m) | |||
| *m = 0; | |||
| } | |||
| /** Return codes from a function call */ | |||
| enum class ErrorCode : uint8 | |||
| { | |||
| @@ -429,6 +466,23 @@ struct Runner | |||
| unknownFunction | |||
| }; | |||
| /** Returns a text description for an error code */ | |||
| static const char* getErrorDescription (ErrorCode e) noexcept | |||
| { | |||
| switch (e) | |||
| { | |||
| case ErrorCode::ok: return "OK"; | |||
| case ErrorCode::executionTimedOut: return "Timed-out"; | |||
| case ErrorCode::unknownInstruction: return "Illegal instruction"; | |||
| case ErrorCode::stackOverflow: return "Stack overflow"; | |||
| case ErrorCode::stackUnderflow: return "Stack underflow"; | |||
| case ErrorCode::illegalAddress: return "Illegal access"; | |||
| case ErrorCode::divisionByZero: return "Division by zero"; | |||
| case ErrorCode::unknownFunction: return "Unknown function"; | |||
| default: return "Unknown error"; | |||
| } | |||
| } | |||
| /** Calls one of the functions in the program, by its textual signature. */ | |||
| ErrorCode callFunction (const char* functionSignature) noexcept | |||
| { | |||
| @@ -467,15 +521,17 @@ struct Runner | |||
| /** */ | |||
| bool isProgramValid() const noexcept { return heapStart != nullptr; } | |||
| /** */ | |||
| /** Sets a byte of data. */ | |||
| void setDataByte (uint32 index, uint8 value) noexcept | |||
| { | |||
| if (index < programAndHeapSpace) | |||
| { | |||
| if (index < program.getProgramSize()) | |||
| auto& dest = getProgramAndDataStart()[index]; | |||
| if (index < program.getProgramSize() && dest != value) | |||
| heapStart = nullptr; // force a re-initialise of the memory layout when the program changes | |||
| getProgramAndDataStart()[index] = value; | |||
| dest = value; | |||
| } | |||
| } | |||
| @@ -561,7 +617,7 @@ struct Runner | |||
| if (prog.getFunctionID (i) == function) | |||
| { | |||
| programCounter = prog.getFunctionStartAddress (i); | |||
| functionEnd = prog.getFunctionEndAddress (i); | |||
| programEnd = r.getProgramHeapStart(); | |||
| tos = *--stack = 0; | |||
| return; | |||
| } | |||
| @@ -599,7 +655,7 @@ struct Runner | |||
| for (;;) | |||
| { | |||
| if (programCounter >= functionEnd) | |||
| if (programCounter >= programEnd) | |||
| return error; | |||
| if ((++opsPerformed & 63) == 0 && hasTimedOut()) | |||
| @@ -628,7 +684,7 @@ struct Runner | |||
| //============================================================================== | |||
| Runner* runner; | |||
| const uint8* programCounter; | |||
| const uint8* functionEnd; | |||
| const uint8* programEnd; | |||
| const uint8* programBase; | |||
| uint8* heapStart; | |||
| int32* stack; | |||
| @@ -646,7 +702,7 @@ struct Runner | |||
| int16 readProgram16() noexcept { auto v = Program::readInt16 (programCounter); programCounter += sizeof (int16); return v; } | |||
| int32 readProgram32() noexcept { auto v = Program::readInt32 (programCounter); programCounter += sizeof (int32); return v; } | |||
| void setError (ErrorCode e) noexcept { error = e; programCounter = functionEnd; jassert (error == ErrorCode::ok); } | |||
| void setError (ErrorCode e) noexcept { error = e; programCounter = programEnd; jassert (error == ErrorCode::ok); } | |||
| bool checkStackUnderflow() noexcept { if (stack <= stackEnd) return true; setError (ErrorCode::stackUnderflow); return false; } | |||
| bool flushTopToStack() noexcept { if (--stack < stackStart) { setError (ErrorCode::stackOverflow); return false; } *stack = tos; return true; } | |||
| @@ -52,6 +52,7 @@ enum class MessageFromDevice | |||
| { | |||
| deviceTopology = 0x01, | |||
| packetACK = 0x02, | |||
| firmwareUpdateACK = 0x03, | |||
| touchStart = 0x10, | |||
| touchMove = 0x11, | |||
| @@ -62,7 +63,11 @@ enum class MessageFromDevice | |||
| touchEndWithVelocity = 0x15, | |||
| controlButtonDown = 0x20, | |||
| controlButtonUp = 0x21 | |||
| controlButtonUp = 0x21, | |||
| programEventMessage = 0x28, | |||
| logMessage = 0x30 | |||
| }; | |||
| /** Messages that the host may send to a device. */ | |||
| @@ -70,7 +75,8 @@ enum class MessageFromHost | |||
| { | |||
| deviceCommandMessage = 0x01, | |||
| sharedDataChange = 0x02, | |||
| programEventMessage = 0x03 | |||
| programEventMessage = 0x03, | |||
| firmwareUpdatePacket = 0x04 | |||
| }; | |||
| @@ -224,15 +230,18 @@ using ByteCountMany = IntegerWithBitSize<8>; | |||
| using ByteValue = IntegerWithBitSize<8>; | |||
| using ByteSequenceContinues = IntegerWithBitSize<1>; | |||
| static constexpr uint32 numProgramMessageInts = 2; | |||
| using FirmwareUpdateACKCode = IntegerWithBitSize<7>; | |||
| using FirmwareUpdatePacketSize = IntegerWithBitSize<7>; | |||
| static constexpr uint32 numProgramMessageInts = 3; | |||
| static constexpr uint32 apiModeHostPingTimeoutMs = 5000; | |||
| static constexpr uint32 padBlockProgramAndHeapSize = 3200; | |||
| static constexpr uint32 padBlockProgramAndHeapSize = 7200; | |||
| static constexpr uint32 padBlockStackSize = 800; | |||
| static constexpr uint32 controlBlockProgramAndHeapSize = 1500; | |||
| static constexpr uint32 controlBlockStackSize = 500; | |||
| static constexpr uint32 controlBlockProgramAndHeapSize = 3000; | |||
| static constexpr uint32 controlBlockStackSize = 800; | |||
| //============================================================================== | |||
| @@ -251,6 +260,8 @@ enum BitSizes | |||
| programEventMessage = MessageType::bits + 32 * numProgramMessageInts, | |||
| packetACK = MessageType::bits + PacketCounter::bits, | |||
| firmwareUpdateACK = MessageType::bits + FirmwareUpdateACKCode::bits, | |||
| controlButtonMessage = typeDeviceAndTime + ControlButtonID::bits, | |||
| }; | |||
| @@ -258,18 +269,62 @@ enum BitSizes | |||
| // These are the littlefoot functions provided for use in BLOCKS programs | |||
| static constexpr const char* ledProgramLittleFootFunctions[] = | |||
| { | |||
| "min/iii", | |||
| "min/fff", | |||
| "max/iii", | |||
| "max/fff", | |||
| "clamp/iiii", | |||
| "clamp/ffff", | |||
| "abs/ii", | |||
| "abs/ff", | |||
| "map/ffffff", | |||
| "map/ffff", | |||
| "mod/iii", | |||
| "getRandomFloat/f", | |||
| "getRandomInt/ii", | |||
| "getMillisecondCounter/i", | |||
| "getFirmwareVersion/i", | |||
| "log/vi", | |||
| "logHex/vi", | |||
| "getTimeInCurrentFunctionCall/i", | |||
| "getBatteryLevel/f", | |||
| "isBatteryCharging/b", | |||
| "isMasterBlock/b", | |||
| "isConnectedToHost/b", | |||
| "setStatusOverlayActive/vb", | |||
| "getNumBlocksInTopology/i", | |||
| "getBlockIDForIndex/ii", | |||
| "getBlockIDOnPort/ii", | |||
| "getPortToMaster/i", | |||
| "getBlockTypeForID/ii", | |||
| "sendMessageToBlock/viiii", | |||
| "sendMessageToHost/viii", | |||
| "makeARGB/iiiii", | |||
| "blendARGB/iii", | |||
| "setLED/viii", | |||
| "blendLED/viii", | |||
| "fillPixel/viii", | |||
| "blendPixel/viii", | |||
| "fillRect/viiiii", | |||
| "blendRect/viiiii", | |||
| "sendMIDI/vi", | |||
| "sendMIDI/vii", | |||
| "sendMIDI/viii", | |||
| "blendGradientRect/viiiiiiii", | |||
| "addPressurePoint/vifff", | |||
| "drawPressureMap/v", | |||
| "fadePressureMap/v", | |||
| "enableDebug/viii", | |||
| "drawNumber/viiii", | |||
| "clearDisplay/v", | |||
| "clearDisplay/vi", | |||
| "sendMIDI/vi", | |||
| "sendMIDI/vii", | |||
| "sendMIDI/viii", | |||
| "sendNoteOn/viii", | |||
| "sendNoteOff/viii", | |||
| "sendAftertouch/viii", | |||
| "sendCC/viii", | |||
| "sendPitchBend/vii", | |||
| "sendChannelPressure/vii", | |||
| "setChannelRange/vbii", | |||
| "assignChannel/ii", | |||
| "deassignChannel/vii", | |||
| "getControlChannel/i", | |||
| "useMPEDuplicateFilter/vb", | |||
| nullptr | |||
| }; | |||
| @@ -218,6 +218,20 @@ struct HostPacketBuilder | |||
| return true; | |||
| } | |||
| bool addFirmwareUpdatePacket (const uint8* packetData, uint8 size) | |||
| { | |||
| if (! data.hasCapacity (MessageType::bits + FirmwareUpdatePacketSize::bits + 7 * size)) | |||
| return false; | |||
| writeMessageType (MessageFromHost::firmwareUpdatePacket); | |||
| data << FirmwareUpdatePacketSize (size); | |||
| for (uint8 i = 0; i < size; ++i) | |||
| data << IntegerWithBitSize<7> ((uint32) packetData[i]); | |||
| return true; | |||
| } | |||
| //============================================================================== | |||
| private: | |||
| Packed7BitArrayBuilder<maxPacketBytes> data; | |||
| @@ -78,7 +78,10 @@ struct HostPacketDecoder | |||
| case MessageFromDevice::touchEndWithVelocity: return handleTouchWithVelocity (handler, reader, deviceIndex, packetTimestamp, false, true); | |||
| case MessageFromDevice::controlButtonDown: return handleButtonDownOrUp (handler, reader, deviceIndex, packetTimestamp, true); | |||
| case MessageFromDevice::controlButtonUp: return handleButtonDownOrUp (handler, reader, deviceIndex, packetTimestamp, false); | |||
| case MessageFromDevice::programEventMessage: return handleCustomMessage (handler, reader, deviceIndex, packetTimestamp); | |||
| case MessageFromDevice::packetACK: return handlePacketACK (handler, reader, deviceIndex); | |||
| case MessageFromDevice::firmwareUpdateACK: return handleFirmwareUpdateACK (handler, reader, deviceIndex); | |||
| case MessageFromDevice::logMessage: return handleLogMessage (handler, reader, deviceIndex); | |||
| default: | |||
| jassertfalse; // got an invalid message type, could be a corrupt packet, or a | |||
| @@ -217,6 +220,24 @@ struct HostPacketDecoder | |||
| return true; | |||
| } | |||
| static bool handleCustomMessage (Handler& handler, Packed7BitArrayReader& reader, | |||
| TopologyIndex deviceIndex, PacketTimestamp packetTimestamp) | |||
| { | |||
| if (reader.getRemainingBits() < BitSizes::programEventMessage - MessageType::bits) | |||
| { | |||
| jassertfalse; // not enough data available for this message type! | |||
| return false; | |||
| } | |||
| int32 data[numProgramMessageInts] = {}; | |||
| for (uint32 i = 0; i < numProgramMessageInts; ++i) | |||
| data[i] = (int32) reader.read<IntegerWithBitSize<32>>().get(); | |||
| handler.handleCustomMessage (deviceIndex, packetTimestamp.get(), data); | |||
| return true; | |||
| } | |||
| static bool handlePacketACK (Handler& handler, Packed7BitArrayReader& reader, TopologyIndex deviceIndex) | |||
| { | |||
| if (reader.getRemainingBits() < BitSizes::packetACK - MessageType::bits) | |||
| @@ -228,4 +249,30 @@ struct HostPacketDecoder | |||
| handler.handlePacketACK (deviceIndex, reader.read<PacketCounter>()); | |||
| return true; | |||
| } | |||
| static bool handleFirmwareUpdateACK (Handler& handler, Packed7BitArrayReader& reader, TopologyIndex deviceIndex) | |||
| { | |||
| if (reader.getRemainingBits() < FirmwareUpdateACKCode::bits) | |||
| { | |||
| jassertfalse; // not enough data available for this message type! | |||
| return false; | |||
| } | |||
| handler.handleFirmwareUpdateACK (deviceIndex, reader.read<FirmwareUpdateACKCode>()); | |||
| return true; | |||
| } | |||
| static bool handleLogMessage (Handler& handler, Packed7BitArrayReader& reader, TopologyIndex deviceIndex) | |||
| { | |||
| String message; | |||
| while (reader.getRemainingBits() >= 7) | |||
| { | |||
| uint32 c = reader.read<IntegerWithBitSize<7>>(); | |||
| message << (char) c; | |||
| } | |||
| handler.handleLogMessage (deviceIndex, message); | |||
| return true; | |||
| } | |||
| }; | |||
| @@ -134,6 +134,21 @@ struct PhysicalTopologySource::Internal | |||
| if (midiInput != nullptr) | |||
| midiInput->stop(); | |||
| if (interprocessLock != nullptr) | |||
| interprocessLock->exit(); | |||
| } | |||
| bool lockAgainstOtherProcesses (const String& midiInName, const String& midiOutName) | |||
| { | |||
| interprocessLock.reset (new juce::InterProcessLock ("blocks_sdk_" | |||
| + File::createLegalFileName (midiInName) | |||
| + "_" + File::createLegalFileName (midiOutName))); | |||
| if (interprocessLock->enter (500)) | |||
| return true; | |||
| interprocessLock = nullptr; | |||
| return false; | |||
| } | |||
| struct Listener | |||
| @@ -189,6 +204,7 @@ struct PhysicalTopologySource::Internal | |||
| private: | |||
| juce::ListenerList<Listener> listeners; | |||
| std::unique_ptr<juce::InterProcessLock> interprocessLock; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MIDIDeviceConnection) | |||
| }; | |||
| @@ -215,13 +231,16 @@ struct PhysicalTopologySource::Internal | |||
| { | |||
| std::unique_ptr<MIDIDeviceConnection> dev (new MIDIDeviceConnection()); | |||
| dev->midiInput.reset (juce::MidiInput::openDevice (pair.inputIndex, dev.get())); | |||
| dev->midiOutput.reset (juce::MidiOutput::openDevice (pair.outputIndex)); | |||
| if (dev->midiInput != nullptr) | |||
| if (dev->lockAgainstOtherProcesses (pair.inputName, pair.outputName)) | |||
| { | |||
| dev->midiInput->start(); | |||
| return dev.release(); | |||
| dev->midiInput.reset (juce::MidiInput::openDevice (pair.inputIndex, dev.get())); | |||
| dev->midiOutput.reset (juce::MidiOutput::openDevice (pair.outputIndex)); | |||
| if (dev->midiInput != nullptr) | |||
| { | |||
| dev->midiInput->start(); | |||
| return dev.release(); | |||
| } | |||
| } | |||
| } | |||
| @@ -486,6 +505,12 @@ struct PhysicalTopologySource::Internal | |||
| detector.handleButtonChange (deviceID, deviceTimestampToHost (timestamp), buttonID.get(), isDown); | |||
| } | |||
| void handleCustomMessage (BlocksProtocol::TopologyIndex deviceIndex, uint32 timestamp, const int32* data) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleCustomMessage (deviceID, deviceTimestampToHost (timestamp), data); | |||
| } | |||
| void handleTouchChange (BlocksProtocol::TopologyIndex deviceIndex, | |||
| uint32 timestamp, | |||
| BlocksProtocol::TouchIndex touchIndex, | |||
| @@ -532,6 +557,18 @@ struct PhysicalTopologySource::Internal | |||
| detector.handleSharedDataACK (deviceID, counter); | |||
| } | |||
| void handleFirmwareUpdateACK (BlocksProtocol::TopologyIndex deviceIndex, BlocksProtocol::FirmwareUpdateACKCode resultCode) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleFirmwareUpdateACK (deviceID, (uint8) resultCode.get()); | |||
| } | |||
| void handleLogMessage (BlocksProtocol::TopologyIndex deviceIndex, const String& message) | |||
| { | |||
| if (auto deviceID = getDeviceIDFromMessageIndex (deviceIndex)) | |||
| detector.handleLogMessage (deviceID, message); | |||
| } | |||
| //============================================================================== | |||
| template <typename PacketBuilder> | |||
| bool sendMessageToDevice (const PacketBuilder& builder) const | |||
| @@ -827,6 +864,24 @@ struct PhysicalTopologySource::Internal | |||
| bi->handleSharedDataACK (packetCounter); | |||
| } | |||
| void handleFirmwareUpdateACK (Block::UID deviceID, uint8 resultCode) | |||
| { | |||
| for (auto&& b : currentTopology.blocks) | |||
| if (b->uid == deviceID) | |||
| if (auto bi = BlockImplementation::getFrom (*b)) | |||
| bi->handleFirmwareUpdateACK (resultCode); | |||
| } | |||
| void handleLogMessage (Block::UID deviceID, const String& message) const | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| for (auto&& b : currentTopology.blocks) | |||
| if (b->uid == deviceID) | |||
| if (auto bi = BlockImplementation::getFrom (*b)) | |||
| bi->handleLogMessage (message); | |||
| } | |||
| void handleButtonChange (Block::UID deviceID, Block::Timestamp timestamp, uint32 buttonIndex, bool isDown) const | |||
| { | |||
| JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
| @@ -872,6 +927,14 @@ struct PhysicalTopologySource::Internal | |||
| surface->cancelAllActiveTouches(); | |||
| } | |||
| void handleCustomMessage (Block::UID deviceID, Block::Timestamp timestamp, const int32* data) | |||
| { | |||
| for (auto&& b : currentTopology.blocks) | |||
| if (b->uid == deviceID) | |||
| if (auto bi = BlockImplementation::getFrom (*b)) | |||
| bi->handleCustomMessage (timestamp, data); | |||
| } | |||
| //============================================================================== | |||
| int getIndexFromDeviceID (Block::UID deviceID) const noexcept | |||
| { | |||
| @@ -992,7 +1055,8 @@ struct PhysicalTopologySource::Internal | |||
| //============================================================================== | |||
| struct BlockImplementation : public Block, | |||
| private MIDIDeviceConnection::Listener | |||
| private MIDIDeviceConnection::Listener, | |||
| private Timer | |||
| { | |||
| BlockImplementation (const BlocksProtocol::BlockSerialNumber& serial, Detector& detectorToUse, bool master) | |||
| : Block (juce::String ((const char*) serial.serial, sizeof (serial.serial))), modelData (serial), | |||
| @@ -1108,6 +1172,16 @@ struct PhysicalTopologySource::Internal | |||
| return sendMessageToDevice (p); | |||
| } | |||
| void handleCustomMessage (Block::Timestamp, const int32* data) | |||
| { | |||
| ProgramEventMessage m; | |||
| for (uint32 i = 0; i < BlocksProtocol::numProgramMessageInts; ++i) | |||
| m.values[i] = data[i]; | |||
| programEventListeners.call (&Block::ProgramEventListener::handleProgramEvent, *this, m); | |||
| } | |||
| static BlockImplementation* getFrom (Block& b) noexcept | |||
| { | |||
| if (auto bi = dynamic_cast<BlockImplementation*> (&b)) | |||
| @@ -1127,42 +1201,84 @@ struct PhysicalTopologySource::Internal | |||
| } | |||
| //============================================================================== | |||
| void clearProgramAndData() | |||
| { | |||
| programSize = 0; | |||
| remoteHeap.clear(); | |||
| } | |||
| std::function<void(const String&)> logger; | |||
| void setProgram (const void* compiledCode, size_t codeSize) | |||
| void setLogger (std::function<void(const String&)> newLogger) override | |||
| { | |||
| clearProgramAndData(); | |||
| setDataBytes (0, compiledCode, codeSize); | |||
| programSize = (uint32) codeSize; | |||
| logger = newLogger; | |||
| } | |||
| void setDataByte (size_t offset, uint8 value) | |||
| void handleLogMessage (const String& message) const | |||
| { | |||
| remoteHeap.setByte (programSize + offset, value); | |||
| if (logger != nullptr) | |||
| logger (message); | |||
| } | |||
| void setDataBytes (size_t offset, const void* newData, size_t num) | |||
| //============================================================================== | |||
| juce::Result setProgram (Program* newProgram) override | |||
| { | |||
| remoteHeap.setBytes (programSize + offset, static_cast<const uint8*> (newData), num); | |||
| } | |||
| if (newProgram == nullptr || program.get() != newProgram) | |||
| { | |||
| { | |||
| std::unique_ptr<Program> p (newProgram); | |||
| void setDataBits (uint32 startBit, uint32 numBits, uint32 value) | |||
| { | |||
| remoteHeap.setBits (programSize * 8 + startBit, numBits, value); | |||
| } | |||
| if (program != nullptr | |||
| && newProgram != nullptr | |||
| && program->getLittleFootProgram() == newProgram->getLittleFootProgram()) | |||
| return juce::Result::ok(); | |||
| uint8 getDataByte (size_t offset) | |||
| { | |||
| return remoteHeap.getByte (programSize + offset); | |||
| stopTimer(); | |||
| std::swap (program, p); | |||
| } | |||
| stopTimer(); | |||
| programSize = 0; | |||
| if (program != nullptr) | |||
| { | |||
| littlefoot::Compiler compiler; | |||
| compiler.addNativeFunctions (PhysicalTopologySource::getStandardLittleFootFunctions()); | |||
| auto err = compiler.compile (program->getLittleFootProgram(), 512); | |||
| if (err.failed()) | |||
| return err; | |||
| DBG ("Compiled littlefoot program, space needed: " | |||
| << (int) compiler.getCompiledProgram().getTotalSpaceNeeded() << " bytes"); | |||
| if (compiler.getCompiledProgram().getTotalSpaceNeeded() > getMemorySize()) | |||
| return Result::fail ("Program too large!"); | |||
| size_t size = (size_t) compiler.compiledObjectCode.size(); | |||
| programSize = (uint32) size; | |||
| remoteHeap.resetDataRangeToUnknown (0, remoteHeap.blockSize); | |||
| remoteHeap.clear(); | |||
| remoteHeap.sendChanges (*this, true); | |||
| remoteHeap.resetDataRangeToUnknown (0, (uint32) size); | |||
| remoteHeap.setBytes (0, compiler.compiledObjectCode.begin(), size); | |||
| remoteHeap.sendChanges (*this, true); | |||
| } | |||
| else | |||
| { | |||
| remoteHeap.clear(); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| jassertfalse; | |||
| } | |||
| return juce::Result::ok(); | |||
| } | |||
| void sendProgramEvent (const LEDGrid::ProgramEventMessage& message) | |||
| Program* getProgram() const override { return program.get(); } | |||
| void sendProgramEvent (const ProgramEventMessage& message) override | |||
| { | |||
| static_assert (sizeof (LEDGrid::ProgramEventMessage::values) == 4 * BlocksProtocol::numProgramMessageInts, | |||
| static_assert (sizeof (ProgramEventMessage::values) == 4 * BlocksProtocol::numProgramMessageInts, | |||
| "Need to keep the internal and external messages structures the same"); | |||
| if (remoteHeap.isProgramLoaded()) | |||
| @@ -1187,9 +1303,47 @@ struct PhysicalTopologySource::Internal | |||
| } | |||
| } | |||
| void saveProgramAsDefault() | |||
| void timerCallback() override | |||
| { | |||
| if (remoteHeap.isFullySynced() && remoteHeap.isProgramLoaded()) | |||
| { | |||
| stopTimer(); | |||
| sendCommandMessage (BlocksProtocol::saveProgramAsDefault); | |||
| } | |||
| else | |||
| { | |||
| startTimer (100); | |||
| } | |||
| } | |||
| void saveProgramAsDefault() override | |||
| { | |||
| startTimer (10); | |||
| } | |||
| uint32 getMemorySize() override | |||
| { | |||
| return modelData.programAndHeapSize; | |||
| } | |||
| void setDataByte (size_t offset, uint8 value) override | |||
| { | |||
| remoteHeap.setByte (programSize + offset, value); | |||
| } | |||
| void setDataBytes (size_t offset, const void* newData, size_t num) override | |||
| { | |||
| remoteHeap.setBytes (programSize + offset, static_cast<const uint8*> (newData), num); | |||
| } | |||
| void setDataBits (uint32 startBit, uint32 numBits, uint32 value) override | |||
| { | |||
| remoteHeap.setBits (programSize * 8 + startBit, numBits, value); | |||
| } | |||
| uint8 getDataByte (size_t offset) override | |||
| { | |||
| sendCommandMessage (BlocksProtocol::saveProgramAsDefault); | |||
| return remoteHeap.getByte (programSize + offset); | |||
| } | |||
| void handleSharedDataACK (uint32 packetCounter) noexcept | |||
| @@ -1198,6 +1352,45 @@ struct PhysicalTopologySource::Internal | |||
| remoteHeap.handleACKFromDevice (*this, packetCounter); | |||
| } | |||
| bool sendFirmwareUpdatePacket (const uint8* data, uint8 size, std::function<void (uint8)> callback) override | |||
| { | |||
| firmwarePacketAckCallback = {}; | |||
| auto index = getDeviceIndex(); | |||
| if (index >= 0) | |||
| { | |||
| BlocksProtocol::HostPacketBuilder<256> p; | |||
| p.writePacketSysexHeaderBytes ((BlocksProtocol::TopologyIndex) index); | |||
| if (p.addFirmwareUpdatePacket (data, size)) | |||
| { | |||
| p.writePacketSysexFooter(); | |||
| if (sendMessageToDevice (p)) | |||
| { | |||
| firmwarePacketAckCallback = callback; | |||
| return true; | |||
| } | |||
| } | |||
| } | |||
| else | |||
| { | |||
| jassertfalse; | |||
| } | |||
| return false; | |||
| } | |||
| void handleFirmwareUpdateACK (uint8 resultCode) | |||
| { | |||
| if (firmwarePacketAckCallback != nullptr) | |||
| { | |||
| firmwarePacketAckCallback (resultCode); | |||
| firmwarePacketAckCallback = {}; | |||
| } | |||
| } | |||
| void pingFromDevice() | |||
| { | |||
| lastMessageReceiveTime = juce::Time::getCurrentTime(); | |||
| @@ -1232,7 +1425,7 @@ struct PhysicalTopologySource::Internal | |||
| if (auto renderer = ledGrid->getRenderer()) | |||
| renderer->renderLEDGrid (*ledGrid); | |||
| remoteHeap.sendChanges (*this); | |||
| remoteHeap.sendChanges (*this, false); | |||
| if (lastMessageSendTime < juce::Time::getCurrentTime() - juce::RelativeTime::milliseconds (pingIntervalMs)) | |||
| sendCommandMessage (BlocksProtocol::ping); | |||
| @@ -1260,12 +1453,15 @@ struct PhysicalTopologySource::Internal | |||
| using RemoteHeapType = littlefoot::LittleFootRemoteHeap<BlockImplementation>; | |||
| RemoteHeapType remoteHeap; | |||
| uint32 programSize = 0; | |||
| Detector& detector; | |||
| juce::Time lastMessageSendTime, lastMessageReceiveTime; | |||
| private: | |||
| std::unique_ptr<Program> program; | |||
| uint32 programSize = 0; | |||
| std::function<void (uint8)> firmwarePacketAckCallback; | |||
| uint32 resetMessagesSent = 0; | |||
| bool isStillConnected = true; | |||
| bool isMaster = false; | |||
| @@ -1316,114 +1512,137 @@ struct PhysicalTopologySource::Internal | |||
| }; | |||
| //============================================================================== | |||
| struct LEDRowImplementation : public LEDRow | |||
| struct LEDRowImplementation : public LEDRow, | |||
| private Timer | |||
| { | |||
| LEDRowImplementation (BlockImplementation& b) : LEDRow (b), blockImpl (b) | |||
| LEDRowImplementation (BlockImplementation& b) : LEDRow (b) | |||
| { | |||
| loadProgramOntoBlock(); | |||
| startTimer (300); | |||
| } | |||
| /* Data format: | |||
| 0: 10 x 5-6-5 bits for button LED RGBs | |||
| 20: 15 x 5-6-5 bits for LED row colours | |||
| 50: 1 x 5-6-5 bits for LED row overlay colour | |||
| */ | |||
| static constexpr uint32 totalDataSize = 256; | |||
| //============================================================================== | |||
| void setButtonColour (uint32 index, LEDColour colour) | |||
| { | |||
| if (index < 10) | |||
| write565Colour (16 * index, colour); | |||
| { | |||
| colours[index] = colour; | |||
| flush(); | |||
| } | |||
| } | |||
| int getNumLEDs() const override | |||
| { | |||
| return blockImpl.modelData.numLEDRowLEDs; | |||
| return static_cast<const BlockImplementation&> (block).modelData.numLEDRowLEDs; | |||
| } | |||
| void setLEDColour (int index, LEDColour colour) override | |||
| { | |||
| if ((uint32) index < 15u) | |||
| write565Colour (20 * 8 + 16 * (uint32) index, colour); | |||
| { | |||
| colours[10 + index] = colour; | |||
| flush(); | |||
| } | |||
| } | |||
| void setOverlayColour (LEDColour colour) override | |||
| { | |||
| write565Colour (50 * 8, colour); | |||
| colours[25] = colour; | |||
| flush(); | |||
| } | |||
| void resetOverlayColour() override | |||
| { | |||
| write565Colour (50 * 8, {}); | |||
| setOverlayColour ({}); | |||
| } | |||
| private: | |||
| void loadProgramOntoBlock() | |||
| { | |||
| littlefoot::Compiler compiler; | |||
| compiler.addNativeFunctions (PhysicalTopologySource::getStandardLittleFootFunctions()); | |||
| LEDColour colours[26]; | |||
| auto err = compiler.compile (getLittleFootProgram(), totalDataSize); | |||
| void timerCallback() override | |||
| { | |||
| stopTimer(); | |||
| loadProgramOntoBlock(); | |||
| flush(); | |||
| } | |||
| if (err.failed()) | |||
| void loadProgramOntoBlock() | |||
| { | |||
| if (block.getProgram() == nullptr) | |||
| { | |||
| DBG (err.getErrorMessage()); | |||
| jassertfalse; | |||
| return; | |||
| auto err = block.setProgram (new DefaultLEDGridProgram (block)); | |||
| if (err.failed()) | |||
| { | |||
| DBG (err.getErrorMessage()); | |||
| jassertfalse; | |||
| } | |||
| } | |||
| } | |||
| blockImpl.setProgram (compiler.compiledObjectCode.begin(), (size_t) compiler.compiledObjectCode.size()); | |||
| void flush() | |||
| { | |||
| if (block.getProgram() != nullptr) | |||
| for (uint32 i = 0; i < (uint32) numElementsInArray (colours); ++i) | |||
| write565Colour (16 * i, colours[i]); | |||
| } | |||
| void write565Colour (uint32 bitIndex, LEDColour colour) | |||
| { | |||
| blockImpl.setDataBits (bitIndex, 5, colour.getRed() >> 3); | |||
| blockImpl.setDataBits (bitIndex + 5, 6, colour.getGreen() >> 2); | |||
| blockImpl.setDataBits (bitIndex + 11, 5, colour.getBlue() >> 3); | |||
| block.setDataBits (bitIndex, 5, colour.getRed() >> 3); | |||
| block.setDataBits (bitIndex + 5, 6, colour.getGreen() >> 2); | |||
| block.setDataBits (bitIndex + 11, 5, colour.getBlue() >> 3); | |||
| } | |||
| static const char* getLittleFootProgram() noexcept | |||
| struct DefaultLEDGridProgram : public Block::Program | |||
| { | |||
| return R"littlefoot( | |||
| DefaultLEDGridProgram (Block& b) : Block::Program (b) {} | |||
| int getColour (int bitIndex) | |||
| juce::String getLittleFootProgram() override | |||
| { | |||
| return makeARGB (255, | |||
| getHeapBits (bitIndex, 5) << 3, | |||
| getHeapBits (bitIndex + 5, 6) << 2, | |||
| getHeapBits (bitIndex + 11, 5) << 3); | |||
| } | |||
| /* Data format: | |||
| int getButtonColour (int index) | |||
| { | |||
| return getColour (16 * index); | |||
| } | |||
| 0: 10 x 5-6-5 bits for button LED RGBs | |||
| 20: 15 x 5-6-5 bits for LED row colours | |||
| 50: 1 x 5-6-5 bits for LED row overlay colour | |||
| */ | |||
| return R"littlefoot( | |||
| int getLEDColour (int index) | |||
| { | |||
| if (getHeapInt (50)) | |||
| return getColour (50 * 8); | |||
| #heapsize: 128 | |||
| return getColour (20 * 8 + 16 * index); | |||
| } | |||
| int getColour (int bitIndex) | |||
| { | |||
| return makeARGB (255, | |||
| getHeapBits (bitIndex, 5) << 3, | |||
| getHeapBits (bitIndex + 5, 6) << 2, | |||
| getHeapBits (bitIndex + 11, 5) << 3); | |||
| } | |||
| void repaint() | |||
| { | |||
| for (int x = 0; x < 15; ++x) | |||
| setLED (x, 0, getLEDColour (x)); | |||
| int getButtonColour (int index) | |||
| { | |||
| return getColour (16 * index); | |||
| } | |||
| for (int i = 0; i < 10; ++i) | |||
| setLED (i, 1, getButtonColour (i)); | |||
| } | |||
| int getLEDColour (int index) | |||
| { | |||
| if (getHeapInt (50)) | |||
| return getColour (50 * 8); | |||
| void handleMessage (int p1, int p2) {} | |||
| return getColour (20 * 8 + 16 * index); | |||
| } | |||
| )littlefoot"; | |||
| } | |||
| void repaint() | |||
| { | |||
| for (int x = 0; x < 15; ++x) | |||
| fillPixel (getLEDColour (x), x, 0); | |||
| BlockImplementation& blockImpl; | |||
| for (int i = 0; i < 10; ++i) | |||
| fillPixel (getButtonColour (i), i, 1); | |||
| } | |||
| void handleMessage (int p1, int p2) {} | |||
| )littlefoot"; | |||
| } | |||
| }; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LEDRowImplementation) | |||
| }; | |||
| @@ -1638,50 +1857,7 @@ struct PhysicalTopologySource::Internal | |||
| int getNumColumns() const override { return blockImpl.modelData.lightGridWidth; } | |||
| int getNumRows() const override { return blockImpl.modelData.lightGridHeight; } | |||
| juce::Result setProgram (Program* newProgram) override | |||
| { | |||
| if (program.get() != newProgram) | |||
| { | |||
| program.reset (newProgram); | |||
| if (program != nullptr) | |||
| { | |||
| littlefoot::Compiler compiler; | |||
| compiler.addNativeFunctions (PhysicalTopologySource::getStandardLittleFootFunctions()); | |||
| auto err = compiler.compile (newProgram->getLittleFootProgram(), newProgram->getHeapSize()); | |||
| if (err.failed()) | |||
| return err; | |||
| DBG ("Compiled littlefoot program, size = " << (int) compiler.compiledObjectCode.size() << " bytes"); | |||
| blockImpl.setProgram (compiler.compiledObjectCode.begin(), (size_t) compiler.compiledObjectCode.size()); | |||
| } | |||
| else | |||
| { | |||
| blockImpl.clearProgramAndData(); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| jassertfalse; | |||
| } | |||
| return juce::Result::ok(); | |||
| } | |||
| Program* getProgram() const override { return program.get(); } | |||
| void sendProgramEvent (const ProgramEventMessage& m) override { blockImpl.sendProgramEvent (m); } | |||
| void saveProgramAsDefault() override { blockImpl.saveProgramAsDefault(); } | |||
| void setDataByte (size_t offset, uint8 value) override { blockImpl.setDataByte (offset, value); } | |||
| void setDataBytes (size_t offset, const void* data, size_t num) override { blockImpl.setDataBytes (offset, data, num); } | |||
| void setDataBits (uint32 startBit, uint32 numBits, uint32 value) override { blockImpl.setDataBits (startBit, numBits, value); } | |||
| uint8 getDataByte (size_t offset) override { return blockImpl.getDataByte (offset); } | |||
| BlockImplementation& blockImpl; | |||
| std::unique_ptr<Program> program; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LEDGridImplementation) | |||
| }; | |||
| @@ -29,7 +29,7 @@ | |||
| */ | |||
| BitmapLEDProgram::BitmapLEDProgram (LEDGrid& lg) : Program (lg) {} | |||
| BitmapLEDProgram::BitmapLEDProgram (Block& b) : Program (b) {} | |||
| /* | |||
| The heap format for this program is just an array of 15x15 5:6:5 colours, | |||
| @@ -38,27 +38,31 @@ BitmapLEDProgram::BitmapLEDProgram (LEDGrid& lg) : Program (lg) {} | |||
| void BitmapLEDProgram::setLED (uint32 x, uint32 y, LEDColour colour) | |||
| { | |||
| auto w = (uint32) ledGrid.getNumColumns(); | |||
| auto h = (uint32) ledGrid.getNumRows(); | |||
| if (x < w && y < h) | |||
| if (auto ledGrid = block.getLEDGrid()) | |||
| { | |||
| auto bit = (x + y * w) * 16; | |||
| auto w = (uint32) ledGrid->getNumColumns(); | |||
| auto h = (uint32) ledGrid->getNumRows(); | |||
| ledGrid.setDataBits (bit, 5, colour.getRed() >> 3); | |||
| ledGrid.setDataBits (bit + 5, 6, colour.getGreen() >> 2); | |||
| ledGrid.setDataBits (bit + 11, 5, colour.getBlue() >> 3); | |||
| } | |||
| } | |||
| if (x < w && y < h) | |||
| { | |||
| auto bit = (x + y * w) * 16; | |||
| uint32 BitmapLEDProgram::getHeapSize() | |||
| { | |||
| return 15 * 15 * 16; | |||
| block.setDataBits (bit, 5, colour.getRed() >> 3); | |||
| block.setDataBits (bit + 5, 6, colour.getGreen() >> 2); | |||
| block.setDataBits (bit + 11, 5, colour.getBlue() >> 3); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| jassertfalse; | |||
| } | |||
| } | |||
| juce::String BitmapLEDProgram::getLittleFootProgram() | |||
| { | |||
| auto program = R"littlefoot( | |||
| String program (R"littlefoot( | |||
| #heapsize: 15 * 15 * 2 | |||
| void repaint() | |||
| { | |||
| @@ -76,9 +80,12 @@ juce::String BitmapLEDProgram::getLittleFootProgram() | |||
| } | |||
| } | |||
| )littlefoot"; | |||
| )littlefoot"); | |||
| if (auto ledGrid = block.getLEDGrid()) | |||
| return program.replace ("NUM_COLUMNS", juce::String (ledGrid->getNumColumns())) | |||
| .replace ("NUM_ROWS", juce::String (ledGrid->getNumRows())); | |||
| return juce::String (program) | |||
| .replace ("NUM_COLUMNS", juce::String (ledGrid.getNumColumns())) | |||
| .replace ("NUM_ROWS", juce::String (ledGrid.getNumRows())); | |||
| jassertfalse; | |||
| return {}; | |||
| } | |||
| @@ -32,14 +32,13 @@ | |||
| /** | |||
| A simple Program to set the colours of individual LEDs. | |||
| */ | |||
| struct BitmapLEDProgram : public LEDGrid::Program | |||
| struct BitmapLEDProgram : public Block::Program | |||
| { | |||
| BitmapLEDProgram (LEDGrid&); | |||
| BitmapLEDProgram (Block&); | |||
| /** Set the colour of the LED at coordinates {x, y}. */ | |||
| void setLED (uint32 x, uint32 y, LEDColour); | |||
| private: | |||
| juce::String getLittleFootProgram() override; | |||
| uint32 getHeapSize() override; | |||
| }; | |||
| @@ -29,16 +29,16 @@ | |||
| */ | |||
| DrumPadGridProgram::DrumPadGridProgram (LEDGrid& lg) : Program (lg) {} | |||
| DrumPadGridProgram::DrumPadGridProgram (Block& b) : Program (b) {} | |||
| int DrumPadGridProgram::getPadIndex (float posX, float posY) const | |||
| { | |||
| posX = juce::jmin (0.99f, posX / ledGrid.block.getWidth()); | |||
| posY = juce::jmin (0.99f, posY / ledGrid.block.getHeight()); | |||
| posX = juce::jmin (0.99f, posX / block.getWidth()); | |||
| posY = juce::jmin (0.99f, posY / block.getHeight()); | |||
| const uint32 offset = ledGrid.getDataByte (visiblePads_byte) ? numColumns1_byte : numColumns0_byte; | |||
| const int numColumns = ledGrid.getDataByte (offset + numColumns0_byte); | |||
| const int numRows = ledGrid.getDataByte (offset + numRows0_byte); | |||
| const uint32 offset = block.getDataByte (visiblePads_byte) ? numColumns1_byte : numColumns0_byte; | |||
| const int numColumns = block.getDataByte (offset + numColumns0_byte); | |||
| const int numRows = block.getDataByte (offset + numRows0_byte); | |||
| return int (posX * numColumns) + int (posY * numRows) * numColumns; | |||
| } | |||
| @@ -49,9 +49,9 @@ void DrumPadGridProgram::startTouch (float startX, float startY) | |||
| for (size_t i = 0; i < 4; ++i) | |||
| { | |||
| if (ledGrid.getDataByte (touchedPads_byte + i) == 0) | |||
| if (block.getDataByte (touchedPads_byte + i) == 0) | |||
| { | |||
| ledGrid.setDataByte (touchedPads_byte + i, static_cast<uint8> (padIdx + 1)); | |||
| block.setDataByte (touchedPads_byte + i, static_cast<uint8> (padIdx + 1)); | |||
| break; | |||
| } | |||
| } | |||
| @@ -62,28 +62,28 @@ void DrumPadGridProgram::endTouch (float startX, float startY) | |||
| const auto padIdx = getPadIndex (startX, startY); | |||
| for (size_t i = 0; i < 4; ++i) | |||
| if (ledGrid.getDataByte (touchedPads_byte + i) == (padIdx + 1)) | |||
| ledGrid.setDataByte (touchedPads_byte + i, 0); | |||
| if (block.getDataByte (touchedPads_byte + i) == (padIdx + 1)) | |||
| block.setDataByte (touchedPads_byte + i, 0); | |||
| } | |||
| void DrumPadGridProgram::sendTouch (float x, float y, float z, LEDColour colour) | |||
| { | |||
| LEDGrid::ProgramEventMessage e; | |||
| Block::ProgramEventMessage e; | |||
| e.values[0] = 0x20000000 | |||
| + (juce::jlimit (0, 255, juce::roundToInt (x * (255.0f / ledGrid.block.getWidth()))) << 16) | |||
| + (juce::jlimit (0, 255, juce::roundToInt (y * (255.0f / ledGrid.block.getHeight()))) << 8) | |||
| + (juce::jlimit (0, 255, juce::roundToInt (x * (255.0f / block.getWidth()))) << 16) | |||
| + (juce::jlimit (0, 255, juce::roundToInt (y * (255.0f / block.getHeight()))) << 8) | |||
| + juce::jlimit (0, 255, juce::roundToInt (z * 255.0f)); | |||
| e.values[1] = (int32) colour.getARGB(); | |||
| ledGrid.sendProgramEvent (e); | |||
| block.sendProgramEvent (e); | |||
| } | |||
| //============================================================================== | |||
| void DrumPadGridProgram::setGridFills (int numColumns, int numRows, const juce::Array<GridFill>& fills) | |||
| { | |||
| uint8 visiblePads = ledGrid.getDataByte (visiblePads_byte); | |||
| uint8 visiblePads = block.getDataByte (visiblePads_byte); | |||
| setGridFills (numColumns, numRows, fills, visiblePads * numColumns1_byte); | |||
| } | |||
| @@ -92,8 +92,8 @@ void DrumPadGridProgram::setGridFills (int numColumns, int numRows, const juce:: | |||
| { | |||
| jassert (numColumns * numRows == fills.size()); | |||
| ledGrid.setDataByte (byteOffset + numColumns0_byte, (uint8) numColumns); | |||
| ledGrid.setDataByte (byteOffset + numRows0_byte, (uint8) numRows); | |||
| block.setDataByte (byteOffset + numColumns0_byte, (uint8) numColumns); | |||
| block.setDataByte (byteOffset + numRows0_byte, (uint8) numRows); | |||
| uint32 i = 0; | |||
| @@ -108,11 +108,11 @@ void DrumPadGridProgram::setGridFills (int numColumns, int numRows, const juce:: | |||
| const uint32 colourOffsetBytes = byteOffset + colours0_byte + i * colourSizeBytes; | |||
| const uint32 colourOffsetBits = colourOffsetBytes * 8; | |||
| ledGrid.setDataBits (colourOffsetBits, 5, fill.colour.getRed() >> 3); | |||
| ledGrid.setDataBits (colourOffsetBits + 5, 6, fill.colour.getGreen() >> 2); | |||
| ledGrid.setDataBits (colourOffsetBits + 11, 5, fill.colour.getBlue() >> 3); | |||
| block.setDataBits (colourOffsetBits, 5, fill.colour.getRed() >> 3); | |||
| block.setDataBits (colourOffsetBits + 5, 6, fill.colour.getGreen() >> 2); | |||
| block.setDataBits (colourOffsetBits + 11, 5, fill.colour.getBlue() >> 3); | |||
| ledGrid.setDataByte (byteOffset + fillTypes0_byte + i, static_cast<uint8> (fill.fillType)); | |||
| block.setDataByte (byteOffset + fillTypes0_byte + i, static_cast<uint8> (fill.fillType)); | |||
| ++i; | |||
| } | |||
| @@ -121,12 +121,12 @@ void DrumPadGridProgram::setGridFills (int numColumns, int numRows, const juce:: | |||
| void DrumPadGridProgram::triggerSlideTransition (int newNumColumns, int newNumRows, | |||
| const juce::Array<GridFill>& newFills, SlideDirection direction) | |||
| { | |||
| uint8 newVisible = ledGrid.getDataByte (visiblePads_byte) ? 0 : 1; | |||
| uint8 newVisible = block.getDataByte (visiblePads_byte) ? 0 : 1; | |||
| setGridFills (newNumColumns, newNumRows, newFills, newVisible * numColumns1_byte); | |||
| ledGrid.setDataByte (visiblePads_byte, newVisible); | |||
| ledGrid.setDataByte (slideDirection_byte, (uint8) direction); | |||
| block.setDataByte (visiblePads_byte, newVisible); | |||
| block.setDataByte (slideDirection_byte, (uint8) direction); | |||
| } | |||
| //============================================================================== | |||
| @@ -143,8 +143,8 @@ void DrumPadGridProgram::setPadAnimationState (uint32 padIdx, double loopTimeSec | |||
| uint32 offset = 8 * animationTimers_byte + 32 * padIdx; | |||
| ledGrid.setDataBits (offset, 16, aniValue); | |||
| ledGrid.setDataBits (offset + 16, 16, aniIncrement); | |||
| block.setDataBits (offset, 16, aniValue); | |||
| block.setDataBits (offset + 16, 16, aniIncrement); | |||
| } | |||
| void DrumPadGridProgram::suspendAnimations() | |||
| @@ -152,29 +152,26 @@ void DrumPadGridProgram::suspendAnimations() | |||
| for (uint32 i = 0; i < 16; ++i) | |||
| { | |||
| uint32 offset = 8 * animationTimers_byte + 32 * i; | |||
| ledGrid.setDataBits (offset + 16, 16, 0); | |||
| block.setDataBits (offset + 16, 16, 0); | |||
| } | |||
| // Hijack touch dimming | |||
| ledGrid.setDataByte (touchedPads_byte, 255); | |||
| block.setDataByte (touchedPads_byte, 255); | |||
| } | |||
| void DrumPadGridProgram::resumeAnimations() | |||
| { | |||
| // Unhijack touch dimming | |||
| ledGrid.setDataByte (touchedPads_byte, 0); | |||
| block.setDataByte (touchedPads_byte, 0); | |||
| } | |||
| //============================================================================== | |||
| uint32 DrumPadGridProgram::getHeapSize() | |||
| { | |||
| return totalDataSize; | |||
| } | |||
| juce::String DrumPadGridProgram::getLittleFootProgram() | |||
| { | |||
| return R"littlefoot( | |||
| #heapsize: 256 | |||
| int dimFactor; | |||
| int dimDelay; | |||
| int slideAnimationProgress; | |||
| @@ -236,8 +233,8 @@ juce::String DrumPadGridProgram::getLittleFootProgram() | |||
| { | |||
| int gradColour = blendARGB (colour, makeARGB (((xx + yy) * 250) / divisor, 0, 0, 0)); | |||
| setLED (x + xx, y + yy, gradColour); | |||
| setLED (x + yy, y + xx, gradColour); | |||
| fillPixel (gradColour, x + xx, y + yy); | |||
| fillPixel (gradColour, x + yy, y + xx); | |||
| } | |||
| } | |||
| } | |||
| @@ -255,7 +252,7 @@ juce::String DrumPadGridProgram::getLittleFootProgram() | |||
| for (int i = 1; i <= numToDo; ++i) | |||
| { | |||
| setLED (x, y, colour); | |||
| fillPixel (colour, x, y); | |||
| if (i < w) | |||
| ++x; | |||
| @@ -278,89 +275,46 @@ juce::String DrumPadGridProgram::getLittleFootProgram() | |||
| { | |||
| fillGradientRect (colour, padX, padY, padW); | |||
| } | |||
| else if (fill == 1) // Filled | |||
| { | |||
| fillRect (colour, padX, padY, padW, padW); | |||
| } | |||
| else if (fill == 2) // Hollow | |||
| { | |||
| outlineRect (colour, padX, padY, padW); | |||
| } | |||
| else if (fill == 3) // Hollow with plus | |||
| { | |||
| outlineRect (colour, padX, padY, padW); | |||
| drawPlus (0xffffffff, padX, padY, padW); | |||
| } | |||
| else if (fill == 4) // Pulsing dot | |||
| { | |||
| int pulseCol = blendARGB (colour, makeARGB (animateProgress, 0, 0, 0)); | |||
| setLED (padX + halfW, padY + halfW, pulseCol); | |||
| fillPixel (pulseCol, padX + halfW, padY + halfW); | |||
| } | |||
| else if (fill == 5) // Blinking dot | |||
| { | |||
| int blinkCol = animateProgress > 64 ? makeARGB (255, 0, 0, 0) : colour; | |||
| int blinkCol = animateProgress > 64 ? 0xff000000 : colour; | |||
| setLED (padX + halfW, padY + halfW, blinkCol); | |||
| fillPixel (blinkCol, padX + halfW, padY + halfW); | |||
| } | |||
| else if (fill == 6) // Pizza filled | |||
| { | |||
| outlineRect (blendARGB (colour, makeARGB (220, 0, 0, 0)), padX, padY, padW); // Dim outline | |||
| setLED (padX + halfW, padY + halfW, colour); // Bright centre | |||
| outlineRect (blendARGB (colour, 0xdc000000), padX, padY, padW); // Dim outline | |||
| fillPixel (colour, padX + halfW, padY + halfW); // Bright centre | |||
| drawPizzaLED (colour, padX, padY, padW, animateProgress); | |||
| } | |||
| else if (fill == 7) // Pizza hollow | |||
| else // Pizza hollow | |||
| { | |||
| outlineRect (blendARGB (colour, makeARGB (220, 0, 0, 0)), padX, padY, padW); // Dim outline | |||
| outlineRect (blendARGB (colour, 0xdc000000), padX, padY, padW); // Dim outline | |||
| drawPizzaLED (colour, padX, padY, padW, animateProgress); | |||
| return; | |||
| } | |||
| } | |||
| void fadeHeatMap() | |||
| { | |||
| for (int i = 0; i < 225; ++i) | |||
| { | |||
| int colourOffset = 226 + i * 4; | |||
| int colour = getHeapInt (colourOffset); | |||
| int alpha = (colour >> 24) & 0xff; | |||
| if (alpha > 0) | |||
| { | |||
| alpha -= getHeapByte (1126 + i); | |||
| setHeapInt (colourOffset, alpha < 0 ? 0 : ((alpha << 24) | (colour & 0xffffff))); | |||
| } | |||
| } | |||
| } | |||
| void addToHeatMap (int x, int y, int colour) | |||
| { | |||
| if (x >= 0 && y >= 0 && x < 15 && y < 15) | |||
| { | |||
| int offset = 226 + 4 * (x + y * 15); | |||
| colour = blendARGB (getHeapInt (offset), colour); | |||
| setHeapInt (offset, colour); | |||
| int decay = ((colour >> 24) & 0xff) / 14; // change divisor to change trail times | |||
| offset = 1126 + (x + y * 15); | |||
| setHeapByte (offset, decay > 0 ? decay : 1); | |||
| } | |||
| } | |||
| int getHeatmapColour (int x, int y) | |||
| { | |||
| return getHeapInt (226 + 4 * (x + y * 15)); | |||
| } | |||
| int isPadActive (int index) | |||
| { | |||
| if (getHeapInt (158) == 0) // None active | |||
| @@ -499,48 +453,25 @@ juce::String DrumPadGridProgram::getLittleFootProgram() | |||
| slideAnimatePads(); | |||
| // Overlay heatmap | |||
| for (int y = 0; y < 15; ++y) | |||
| for (int x = 0; x < 15; ++x) | |||
| blendLED (x, y, getHeatmapColour (x, y)); | |||
| fadeHeatMap(); | |||
| drawPressureMap(); | |||
| fadePressureMap(); | |||
| } | |||
| // DrumPadGridProgram::sendTouch results in this callback, giving | |||
| // us more touch updates per frame and therefore smoother trails. | |||
| void handleMessage (int pos, int colour) | |||
| void handleMessage (int pos, int colour, int dummy) | |||
| { | |||
| if ((pos >> 24) != 0x20) | |||
| return; | |||
| int tx = ((pos >> 16) & 0xff) - 13; | |||
| int ty = ((pos >> 8) & 0xff) - 13; | |||
| int tx = (pos >> 16) & 0xff; | |||
| int ty = (pos >> 8) & 0xff; | |||
| int tz = pos & 0xff; | |||
| tz = tz > 30 ? tz : 30; | |||
| int ledCentreX = tx >> 4; | |||
| int ledCentreY = ty >> 4; | |||
| int adjustX = (tx - (ledCentreX << 4)) >> 2; | |||
| int adjustY = (ty - (ledCentreY << 4)) >> 2; | |||
| for (int dy = -2; dy <= 2; ++dy) | |||
| { | |||
| for (int dx = -2; dx <= 2; ++dx) | |||
| { | |||
| int distance = dx * dx + dy * dy; | |||
| int level = distance == 0 ? 255 : (distance == 1 ? 132 : (distance < 5 ? 9 : (distance == 5 ? 2 : 0))); | |||
| level += (dx * adjustX); | |||
| level += (dy * adjustY); | |||
| level = (tz * level) >> 8; | |||
| if (level > 0) | |||
| addToHeatMap (ledCentreX + dx, ledCentreY + dy, | |||
| makeARGB (level, colour >> 16, colour >> 8, colour)); | |||
| } | |||
| } | |||
| addPressurePoint (colour, | |||
| tx * (2.0 / (256 + 20)), | |||
| ty * (2.0 / (256 + 20)), | |||
| tz * (1.0 / 3.0)); | |||
| } | |||
| )littlefoot"; | |||
| @@ -30,9 +30,9 @@ | |||
| /** | |||
| */ | |||
| struct DrumPadGridProgram : public LEDGrid::Program | |||
| struct DrumPadGridProgram : public Block::Program | |||
| { | |||
| DrumPadGridProgram (LEDGrid&); | |||
| DrumPadGridProgram (Block&); | |||
| //============================================================================== | |||
| /** These let the program dim pads which aren't having gestures performed on them. */ | |||
| @@ -114,18 +114,13 @@ private: | |||
| static constexpr uint32 slideDirection_byte = 156; // 1 byte | |||
| static constexpr uint32 touchedPads_byte = 158; // 1 byte x 4 (Zero means empty slot, so stores padIdx + 1) | |||
| static constexpr uint32 animationTimers_byte = 162; // 4 byte x 16 (16:16 bits counter:increment) | |||
| static constexpr uint32 heatMap_byte = 226; // 4 byte x 225 | |||
| static constexpr uint32 heatDecayMap_byte = 1126; // 1 byte x 225 | |||
| static constexpr uint32 totalHeapSize = 226; | |||
| static constexpr uint32 maxNumPads = 25; | |||
| static constexpr uint32 colourSizeBytes = 2; | |||
| static constexpr uint32 heatMapSize = 15 * 15 * 4; | |||
| static constexpr uint32 heatMapDecaySize = 15 * 15; | |||
| static constexpr uint32 totalDataSize = heatDecayMap_byte + heatMapDecaySize; | |||
| int getPadIndex (float posX, float posY) const; | |||
| void setGridFills (int numColumns, int numRows, const juce::Array<GridFill>& fills, uint32 byteOffset); | |||
| juce::String getLittleFootProgram() override; | |||
| uint32 getHeapSize() override; | |||
| }; | |||