├── image1.jpg ├── Spark Protocol Description v3.2.pdf ├── Spark ESP32 Library Description v0.2.pdf ├── SparkClass ├── SparkAmpParser.py ├── SparkAppParser.py ├── TestCode │ ├── SparkClassTester.py │ ├── SparkPresetConverter.py │ ├── SparkReaderTesterPH.py │ ├── SparkRunSomeCommandTests.py │ └── SparkReaderTester.py ├── SparkCommsClass.py ├── SparkClass.py └── SparkReaderClass.py ├── SparkESP32 ├── RingBuffer.h ├── SparkStructures.h ├── SparkESP32.ino ├── Spark.h ├── SparkComms.h ├── RingBuffer.ino ├── SparkIO.h ├── Spark.ino ├── SparkComms.ino └── SparkIO.ino ├── README.md └── LICENSE /image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulhamsh/Spark/HEAD/image1.jpg -------------------------------------------------------------------------------- /Spark Protocol Description v3.2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulhamsh/Spark/HEAD/Spark Protocol Description v3.2.pdf -------------------------------------------------------------------------------- /Spark ESP32 Library Description v0.2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulhamsh/Spark/HEAD/Spark ESP32 Library Description v0.2.pdf -------------------------------------------------------------------------------- /SparkClass/SparkAmpParser.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import struct 3 | import socket 4 | 5 | from SparkReaderClass import * 6 | from SparkCommsClass import * 7 | 8 | 9 | 10 | reader = SparkReadMessage() 11 | comms = SparkComms("bluetooth") 12 | comms.connect() 13 | 14 | while True: 15 | dat = comms.get_data() 16 | reader.set_message(dat) 17 | s = reader.read_message() 18 | print ("================") 19 | #print (reader.python) 20 | print (reader.text) 21 | #print (reader.raw) 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SparkESP32/RingBuffer.h: -------------------------------------------------------------------------------- 1 | #ifndef RingBuffer_h 2 | #define RingBuffer_h 3 | 4 | class RingBuffer 5 | { 6 | public: 7 | RingBuffer(); 8 | bool add(uint8_t b); 9 | bool get(uint8_t *b); 10 | bool set_at_index(int ind, uint8_t b); 11 | bool get_at_index(int ind, uint8_t *b); 12 | bool set_bit_at_index(int ind, uint8_t b); 13 | int get_len(); 14 | int get_pos(); 15 | bool is_empty(); 16 | void commit(); 17 | void drop(); 18 | void clear(); 19 | void dump(); 20 | void dump2(); 21 | void dump3(); 22 | private: 23 | static const int RB_BUFF_MAX = 5000; 24 | uint8_t rb[RB_BUFF_MAX]; 25 | int st, en, len, t_len; 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /SparkClass/SparkAppParser.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import struct 3 | 4 | from SparkReaderClass import * 5 | from SparkCommsClass import * 6 | 7 | 8 | # This captures the bluetooth traffic sent on a serial port 9 | # I am using a ESP32 board to mimick the Spark amp and capture bluetooth traffic and relay it to the serial port 10 | # Can be altered to parse a bytearray or similar 11 | 12 | #ser=serial.Serial("COM7", BAUD, timeout=0) 13 | 14 | reader = SparkReadMessage() 15 | comms = SparkComms("serial") 16 | comms.connect() 17 | 18 | 19 | while True: 20 | dat = comms.get_data() 21 | reader.set_message(dat) 22 | reader.read_message() 23 | print ("================") 24 | print (reader.python) 25 | print (reader.text) 26 | print (reader.raw) 27 | 28 | 29 | -------------------------------------------------------------------------------- /SparkESP32/SparkStructures.h: -------------------------------------------------------------------------------- 1 | #ifndef SparkStructures_h 2 | #define SparkStructures_h 3 | 4 | #define DEBUG(x) Serial.println(x) 5 | #define STR_LEN 40 6 | 7 | typedef struct { 8 | uint8_t curr_preset; 9 | uint8_t preset_num; 10 | char UUID[STR_LEN]; 11 | char Name[STR_LEN]; 12 | char Version[STR_LEN]; 13 | char Description[STR_LEN]; 14 | char Icon[STR_LEN]; 15 | float BPM; 16 | struct SparkEffects { 17 | char EffectName[STR_LEN]; 18 | bool OnOff; 19 | uint8_t NumParameters; 20 | float Parameters[10]; 21 | } effects[7]; 22 | uint8_t chksum; 23 | } SparkPreset; 24 | 25 | typedef struct { 26 | uint8_t param1; 27 | uint8_t param2; 28 | uint8_t param3; 29 | uint8_t param4; 30 | uint32_t param5; 31 | float val; 32 | char str1[STR_LEN]; 33 | char str2[STR_LEN]; 34 | bool onoff; 35 | } SparkMessage; 36 | 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /SparkESP32/SparkESP32.ino: -------------------------------------------------------------------------------- 1 | // if CLASSIC is defined, will compile with BLE stack which allows serial bluetooth - if not defined will use NimBLE which has no serial bluetooth 2 | // it matters because Android Spark apps use serial bluetooth 3 | // but BLE doesn't handle Spark disconnection well, whereas NimBLE does 4 | #define CLASSIC 5 | 6 | // define this if using a bluetooth controller like Akai LPD8 Wireless or IK Multimedia iRig Blueboard 7 | #define BT_CONTROLLER 8 | 9 | #include "Spark.h" 10 | 11 | void setup() { 12 | spark_state_tracker_start(); // set up data to track Spark and app state 13 | } 14 | 15 | 16 | void loop() { 17 | // create commands to send to the Spark amp 18 | /* 19 | change_amp_param(AMP_GAIN, mi3/127.0); 20 | change_amp_param(AMP_MASTER, mi3/127.0); 21 | change_drive_toggle(); 22 | change_delay_toggle(); 23 | change_reverb_toggle(); 24 | change_hardware_preset(0); 25 | change_hardware_preset(1); 26 | change_hardware_preset(2); 27 | change_hardware_preset(3); 28 | */ 29 | 30 | 31 | if (update_spark_state()) { // this sends those commands and picks up incoming messages 32 | // do your own checks and processing here to respond to the incoming messages - only one message per call of this function 33 | /* 34 | if (cmdsub == 0x0301) { 35 | // an example - do something on message 0x0301 36 | } 37 | */ 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /SparkClass/TestCode/SparkClassTester.py: -------------------------------------------------------------------------------- 1 | ######## 2 | # 3 | # Spark Commander 4 | # 5 | # Program to send commands to Positive Grid Spark 6 | # 7 | # See https://github.com/paulhamsh/Spark-Parser 8 | 9 | #### PRESETS #### 10 | 11 | from AllPresets import * 12 | from SparkClass import * 13 | 14 | 15 | 16 | msg = SparkMessage() 17 | 18 | for i in range (len(preset_list)): 19 | b = msg.create_preset(preset_list[i]) 20 | 21 | for x in range(len(b)): 22 | if b[x].hex() != preset_listh[i][x]: 23 | print("Preset %s failed test in block %d" % (preset_list[i]["Name"], x)) 24 | print (b[x].hex()) 25 | print (preset_listh[i][x]) 26 | print ("---------") 27 | 28 | b = msg.change_effect("Twin","SLO100") 29 | if b[0].hex() != "01fe000053fe27000000000000000000f0013a1501060204245477696e060126534c4f313030f7": 30 | print ("Pedal change failed test") 31 | b = msg.change_hardware_preset(3) 32 | if b[0].hex() != "01fe000053fe1a000000000000000000f0013a150138000003f7": 33 | print ("Hardware preset change failed test") 34 | b = msg.turn_effect_onoff("Booster", "On") 35 | if b[0].hex() != "01fe000053fe23000000000000000000f0013a150115020727426f6f737404657243f7": 36 | print ("Pedal on/off failed test") 37 | b = msg.change_effect_parameter("Twin", 0, 0.344) 38 | if b[0].hex() != "01fe000053fe25000000000000000000f0013a1501040204245477696e00154a3e302045f7": 39 | print ("Pedal parameter change failed test") 40 | print (b[0].hex()) 41 | print ("01fe000053fe25000000000000000000f0013a1501044204245477696e00154a3e302045f7") 42 | 43 | 44 | print ("Tests complete") 45 | 46 | 47 | -------------------------------------------------------------------------------- /SparkClass/TestCode/SparkPresetConverter.py: -------------------------------------------------------------------------------- 1 | from AllPresets import * 2 | from SparkClass import * 3 | 4 | preset = { "Preset Number": [0x00, 0x7f], 5 | "UUID": "07079063-94A9-41B1-AB1D-02CBC5D00790", 6 | "Name": "Silver Ship", 7 | "Version": "0.7", 8 | "Description": "1-Clean", 9 | "Icon": "icon.png", 10 | "BPM": 120.0, 11 | "Pedals": [ 12 | { "Name": "bias.noisegate", 13 | "OnOff": "Off", 14 | "Parameters": [0.138313, 0.224643, 0.000000] }, 15 | { "Name": "LA2AComp", 16 | "OnOff": "On", 17 | "Parameters": [0.000000, 0.852394, 0.373072] }, 18 | { "Name": "Booster", 19 | "OnOff": "Off", 20 | "Parameters": [0.722592] }, 21 | { "Name": "RolandJC120", 22 | "OnOff": "On", 23 | "Parameters": [0.632231, 0.281820, 0.158359, 0.671320, 0.805785] }, 24 | { "Name": "Cloner", 25 | "OnOff": "On", 26 | "Parameters": [0.199593, 0.000000] }, 27 | { "Name": "VintageDelay", 28 | "OnOff": "Off", 29 | "Parameters": [0.378739, 0.425745, 0.419816, 1.000000] }, 30 | { "Name": "bias.reverb", 31 | "OnOff": "On", 32 | "Parameters": [0.285714, 0.408354, 0.289489, 0.388317, 0.582143, 0.650000, 0.200000] }], 33 | "End Filler": 0xb4} 34 | 35 | 36 | msg = SparkMessage() 37 | 38 | b = msg.create_preset(preset) 39 | print ("preset_hex=[") 40 | for x in b: 41 | print ("\"%s\"," % x.hex()) 42 | print("]") 43 | -------------------------------------------------------------------------------- /SparkESP32/Spark.h: -------------------------------------------------------------------------------- 1 | #ifndef Spark_h 2 | #define Spark_h 3 | 4 | #include "SparkIO.h" 5 | 6 | // variables required to track spark state and also for communications generally 7 | unsigned int cmdsub; 8 | SparkMessage msg; 9 | SparkPreset preset; 10 | SparkPreset presets[6]; 11 | 12 | void spark_state_tracker_start(); 13 | bool update_spark_state(); 14 | void update_ui(); 15 | 16 | void change_comp_model(char *new_eff); 17 | void change_drive_model(char *new_eff); 18 | void change_amp_model(char *new_eff); 19 | void change_mod_model(char *new_eff); 20 | void change_delay_model(char *new_eff); 21 | 22 | void change_noisegate_onoff(bool onoff); 23 | void change_comp_onoff(bool onoff); 24 | void change_drive_onoff(bool onoff); 25 | void change_amp_onoff(bool onoff); 26 | void change_mod_onoff(bool onoff); 27 | void change_delay_onoff(bool onoff); 28 | void change_reverb_onoff(bool onoff); 29 | 30 | void change_noisegate_toggle(); 31 | void change_comp_toggle(); 32 | void change_drive_toggle(); 33 | void change_amp_toggle(); 34 | void change_mod_toggle(); 35 | void change_delay_toggle(); 36 | void change_reverb_toggle(); 37 | 38 | void change_noisegate_param(int param, float val); 39 | void change_comp_param(int param, float val); 40 | void change_drive_param(int param, float val); 41 | void change_amp_param(int param, float val); 42 | void change_mod_param(int param, float val); 43 | void change_delay_param(int param, float val); 44 | void change_reverb_param(int param, float val); 45 | 46 | void change_hardware_preset(int pres_num); 47 | void change_custom_preset(SparkPreset *preset, int pres_num); 48 | 49 | #define AMP_GAIN 0 50 | #define AMP_TREBLE 1 51 | #define AMP_MID 2 52 | #define AMP_BASS 3 53 | #define AMP_MASTER 4 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /SparkClass/TestCode/SparkReaderTesterPH.py: -------------------------------------------------------------------------------- 1 | from SparkReaderClass import * 2 | 3 | reader = SparkReadMessage() 4 | 5 | 6 | msg_rx = ["01fe000041ff6a000000000000000000f001046b0301200f001900005924003734323532313100372d433241412d00343133352d3846f7f00104190301000f011939322d370043464441303146103531363727312d20436c65616e2330f7f00104420301200f02192e37", 7 | "01fe000041ff6a0000000000000000002731402d436c65616e280069636f6e2e706e4a674a4270000017f7f00104090301080f03192e62696100732e6e6f69736530676174654313003b114a3d756e4301f7f001046d0301580f0419114a3e29192f1202114a00000400", 8 | "01fe000041ff6a000000000000000000002a436f6d7040726573736f7243f7f00104260301680f05191200114a6e3e2a3b1001114a143f7f474b27426f606f737465724311f7f001041d0301300f061900114a3f080f1f7824547769366e431500114a3f341d09790111", 9 | "01fe000041ff6a0000000000000000004a3ef7f00104460301280f071961740a021b114a3e417054032b114a3e7b13490453114a3f2079532cf7f00104370301000f081943686f72007573416e616c6f3667421400114a3e3541153201114a3ff7f001042c0301000f09", 10 | "01fe000041ff6a00000000000000000019115b1e0223114a3e5d4944034b114a3e000000290044656c61794d6ff7f00104760301600f0a196e6f43156600114a3e1f31206601114a3e6e24611602114a3e7b2457f7f00104590301300f0b1903114a3f341b556a04114a", 11 | "01fe000041ff6a0000000000000000003f090000002b62696100732e7265766572f7f00104070301300f0c19624317000b114a3e2d3027013b114a3e28193b023b114a3e60173203f7f00104240301180f0d19114a3f315b162004114a3e795b797a05114a3e6e594a38", 12 | "01fe000041ff2400000000000000000006114a3e19f7f00104640301180f0e03191a7df7"] 13 | 14 | preset_rx=[] 15 | for h in msg_rx: 16 | preset_rx.append(bytes.fromhex(h)) 17 | 18 | 19 | reader.set_message(preset_rx) 20 | b = reader.read_message() 21 | c = reader.message 22 | 23 | print (reader.text) 24 | print (reader.raw) 25 | print (reader.python) 26 | print (c) 27 | print() 28 | 29 | 30 | s= c[0][2].hex() 31 | 32 | 33 | -------------------------------------------------------------------------------- /SparkESP32/SparkComms.h: -------------------------------------------------------------------------------- 1 | #ifndef SparkComms_h 2 | #define SparkComms_h 3 | 4 | #ifdef CLASSIC 5 | #include "BluetoothSerial.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #else 11 | #include "NimBLEDevice.h" 12 | #endif 13 | 14 | #include "RingBuffer.h" 15 | 16 | 17 | // Functions for logging changes for any UI updates 18 | #define SPK 0 19 | #define APP 1 20 | #define BLE_MIDI 2 21 | #define USB_MIDI 3 22 | #define SER_MIDI 4 23 | 24 | #define NUM_CONNS 5 25 | 26 | #define TO 0 27 | #define FROM 1 28 | #define STATUS 2 29 | 30 | bool conn_status[NUM_CONNS]; 31 | unsigned long conn_last_changed[3][NUM_CONNS]; 32 | 33 | void set_conn_status_connected(int connection); 34 | void set_conn_status_disconnected(int connection); 35 | void set_conn_received(int connection); 36 | void set_conn_sent(int connection); 37 | 38 | 39 | #define BLE_BUFSIZE 5000 40 | 41 | #define C_SERVICE "ffc0" 42 | #define C_CHAR1 "ffc1" 43 | #define C_CHAR2 "ffc2" 44 | 45 | #define S_SERVICE "ffc0" 46 | #define S_CHAR1 "ffc1" 47 | #define S_CHAR2 "ffc2" 48 | 49 | #define PEDAL_SERVICE "03b80e5a-ede8-4b33-a751-6ce34ec4c700" 50 | #define PEDAL_CHAR "7772e5db-3868-4112-a1a9-f2669d106bf3" 51 | #define SPARK_BT_NAME "Spark 40 Audio" 52 | 53 | 54 | void connect_to_all(); 55 | void connect_spark(); 56 | #ifdef BT_CONTROLLER 57 | void connect_pedal(); 58 | #endif 59 | 60 | bool sp_available(); 61 | bool app_available(); 62 | uint8_t sp_read(); 63 | uint8_t app_read(); 64 | void sp_write(byte *buf, int len); 65 | void app_write(byte *buf, int len); 66 | int ble_getRSSI(); 67 | 68 | #ifdef CLASSIC 69 | BluetoothSerial *bt; 70 | #endif 71 | 72 | bool is_ble; 73 | 74 | bool ble_app_connected; 75 | bool classic_app_connected; 76 | 77 | #ifdef BT_CONTROLLER 78 | bool connected_pedal; 79 | bool found_pedal; 80 | #endif 81 | 82 | bool connected_sp; 83 | bool found_sp; 84 | 85 | 86 | BLEServer *pServer; 87 | BLEService *pService; 88 | BLECharacteristic *pCharacteristic_receive; 89 | BLECharacteristic *pCharacteristic_send; 90 | BLEAdvertising *pAdvertising; 91 | 92 | BLEScan *pScan; 93 | BLEScanResults pResults; 94 | BLEAdvertisedDevice device; 95 | 96 | BLEClient *pClient_sp; 97 | BLERemoteService *pService_sp; 98 | BLERemoteCharacteristic *pReceiver_sp; 99 | BLERemoteCharacteristic *pSender_sp; 100 | BLERemoteDescriptor* p2902_sp; 101 | BLEAdvertisedDevice *sp_device; 102 | 103 | #ifdef BT_CONTROLLER 104 | BLEClient *pClient_pedal; 105 | BLERemoteService *pService_pedal; 106 | BLERemoteCharacteristic *pReceiver_pedal; 107 | BLERemoteCharacteristic *pSender_pedal; 108 | BLERemoteDescriptor* p2902_pedal; 109 | BLEAdvertisedDevice *pedal_device; 110 | #endif 111 | 112 | RingBuffer ble_in; 113 | RingBuffer ble_app_in; 114 | RingBuffer midi_in; 115 | 116 | #endif 117 | -------------------------------------------------------------------------------- /SparkClass/TestCode/SparkRunSomeCommandTests.py: -------------------------------------------------------------------------------- 1 | ######## 2 | # 3 | # Spark Run Some Command Tests 4 | # 5 | # Program to send commands to Positive Grid Spark 6 | # 7 | # See https://github.com/paulhamsh/Spark-Parser 8 | 9 | 10 | from AllPresets import * 11 | from SparkClass import * 12 | 13 | import socket 14 | import time 15 | import struct 16 | 17 | 18 | SERVER_PORT = 2 19 | MY_SPARK = "08:EB:ED:4E:47:07" # Change to address of YOUR Spark 20 | 21 | 22 | def send_receive(b): 23 | cs.send(b) 24 | a=cs.recv(100) 25 | 26 | 27 | def send_preset(pres): 28 | for i in pres: 29 | cs.send(i) 30 | a=cs.recv(100) 31 | cs.send(change_user_preset[0]) 32 | a=cs.recv(100) 33 | 34 | def just_send(b): 35 | cs.send(b) 36 | 37 | 38 | 39 | try: 40 | cs = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) 41 | cs.connect((MY_SPARK, SERVER_PORT)) 42 | print ("Connected successfully") 43 | 44 | msg = SparkMessage() 45 | 46 | 47 | # Run some basic tests 48 | 49 | change_user_preset = msg.change_hardware_preset(0x7f) 50 | 51 | print("Change to hardware preset 0") 52 | b = msg.change_hardware_preset(0) 53 | send_receive(b[0]) 54 | time.sleep(3) 55 | 56 | 57 | 58 | print ("Sweep up gain") 59 | for v in range (0, 100): 60 | val = v*0.01 61 | b = msg.change_effect_parameter ("Twin", 0, val) 62 | just_send(b[0]) 63 | time.sleep(0.02) 64 | 65 | print ("Change amp from Twin to SLO 100") 66 | b = msg.change_effect ("Twin", "SLO100") 67 | send_receive(b[0]) 68 | time.sleep(3) 69 | 70 | print ("Change amp from SLO 100 to Twin") 71 | b = msg.change_effect ( "SLO100", "Twin") 72 | send_receive(b[0]) 73 | time.sleep(3) 74 | 75 | print ("Turn on the Booster pedal") 76 | b = msg.turn_effect_onoff ( "Booster", "On") 77 | send_receive(b[0]) 78 | time.sleep(3) 79 | 80 | print ("Booster gain to 9") 81 | b = msg.change_effect_parameter ("Booster", 0, 0.9) 82 | just_send(b[0]) 83 | time.sleep(3) 84 | 85 | print ("Booster gain to 1") 86 | b = msg.change_effect_parameter ("Booster", 0, 0.1) 87 | just_send(b[0]) 88 | time.sleep(3) 89 | 90 | print ("Turn off Booster") 91 | b = msg.turn_effect_onoff ( "Booster", "Off") 92 | send_receive(b[0]) 93 | time.sleep(3) 94 | 95 | print ("Turn on the Booster pedal") 96 | b = msg.turn_effect_onoff ( "Booster", "On") 97 | send_receive(b[0]) 98 | time.sleep(3) 99 | 100 | for i in range (len(preset_list)): 101 | print ("\t", preset_list[i]["Name"]) 102 | b = msg.create_preset(preset_list[i]) 103 | send_preset(b) 104 | time.sleep(5) 105 | 106 | print("Preset ", preset_list[1]["Name"]) 107 | b = msg.create_preset(preset_list[8]) 108 | send_preset(b) 109 | time.sleep(5) 110 | 111 | 112 | print ("Finished") 113 | 114 | 115 | except OSError as e: 116 | print(e) 117 | finally: 118 | if cs is not None: 119 | cs.close() 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spark 2 | All latest code for controlling Spark 40 amp via bluetooth. 3 | 4 | Contains latest set of documentation on the bluetooth message format, ESP32 code to control the amp and also the app (works with the Hendrix models), python code to create a preset. 5 | 6 | # SparkESP32 7 | 8 | All the libraries to control the Spark amp and app with an ESP 32 device. 9 | 10 | The required libraries are: 11 | SparkComms.ino 12 | SparkComms.h 13 | SparkIO.ino 14 | SparkIO.h 15 | SparkStructures.h 16 | Spark.ino 17 | Spark.h 18 | RingBuffer.ino 19 | RingBuffer.h 20 | 21 | Needs an ESP32 device, preferrably with USB host capability. 22 | 23 | This allows either the Android or IOS apps to connect to the ESP32 and the ESP32 (or things connected to it) and app can control the Spark 40 amp - and the app will update to show the changes. 24 | 25 | The last bit requires a workaround because the app doesn't want to receive changes from the ESP32 - it doesn't expect much from the Spark 40 amp. So this uses the fourth preset to hold the current values and updates the app by fooling it into thinking the preset was saved on the amp (as if the preset button was held for a period). 26 | 27 | 28 | The API to control the Spark is new for this version - as in this table (Spark.h). 29 | 30 | 31 | ``` 32 | void change_comp_model(char *new_eff); 33 | void change_drive_model(char *new_eff); 34 | void change_amp_model(char *new_eff); 35 | void change_mod_model(char *new_eff); 36 | void change_delay_model(char *new_eff); 37 | 38 | void change_noisegate_onoff(bool onoff); 39 | void change_comp_onoff(bool onoff); 40 | void change_drive_onoff(bool onoff); 41 | void change_amp_onoff(bool onoff); 42 | void change_mod_onoff(bool onoff); 43 | void change_delay_onoff(bool onoff); 44 | void change_reverb_onoff(bool onoff); 45 | 46 | void change_noisegate_toggle(); 47 | void change_comp_toggle(); 48 | void change_drive_toggle(); 49 | void change_amp_toggle(); 50 | void change_mod_toggle(); 51 | void change_delay_toggle(); 52 | void change_reverb_toggle(); 53 | 54 | void change_noisegate_param(int param, float val); 55 | void change_comp_param(int param, float val); 56 | void change_drive_param(int param, float val); 57 | void change_amp_param(int param, float val); 58 | void change_mod_param(int param, float val); 59 | void change_delay_param(int param, float val); 60 | void change_reverb_param(int param, float val); 61 | 62 | void change_hardware_preset(int pres_num); 63 | void change_custom_preset(SparkPreset *preset, int pres_num); 64 | void update_ui(); 65 | ``` 66 | 67 | And the core program to do this looks like: 68 | 69 | ``` 70 | #define CLASSIC 71 | 72 | void setup() { 73 | spark_state_tracker_start(); 74 | } 75 | 76 | void loop() { 77 | if (update_spark_state()) { 78 | // do your own checks and processing here based on changes to Spark state 79 | } 80 | 81 | // and put other commands here, such as 82 | 83 | // change_hardware_preset(3); 84 | // change_reverb_toggle(); 85 | // change_amp_param(AMP_GAIN, 0.99); 86 | 87 | // change_amp_model("94MatchDCV2"); 88 | // change_drive_model("Booster"); 89 | // change_mod_model("GuitarEQ6"); 90 | // change_delay_model("DelayMono"); 91 | // update_ui(); 92 | 93 | } 94 | ``` 95 | 96 | ```update_ui()``` will force the app ui to refresh to reflect the actual amp. 97 | 98 | The ```#define CLASSIC``` specifies which bluetooth library to use. 99 | ```CLASSIC``` uses the BLE bluedroid library which will support a classic bluetooth connection (Android app) and BLE (IOS app) - but it doesn't handle loss of connection to Spark whilst using an IOS app well (it is fine with classic ie Android). 100 | If ```CLASSIC``` is not defined it will use the NimBLE library which will only work with BLE, but handles a Spark connection loss properly for BLE. 101 | 102 | 103 | # Spark Class 104 | 105 | Python class to interpret Spark messages. 106 | 107 | 108 | -------------------------------------------------------------------------------- /SparkClass/TestCode/SparkReaderTester.py: -------------------------------------------------------------------------------- 1 | preset_h=[ 2 | "01fe000053fead000000000000000000f0013a15010124030000007f5924003037303739303600332d393441392d00343142312d41420031442d30324342004335443030373902302b53696c7665407220536869702308302e3727312d43106c65616e286963406f6e2e706e674a3242700000172e62006961732e6e6f6940736567617465424d1300114a3e0d210d6c01114a3e66080d5102114a0000000200284c41324143186f6d704313f7", 3 | "01fe000053fead000000000000000000f0013a1501013403010000114a003000000001114a3f305a367e02114a3e093f034b27426f6f307374657242110073114a3f387b4a2b00526f6c616e644a304331323043150033114a3f215964012b114a3e104a360223114a3e22285d0333114a3f2b5b210463114a3f4e476d2640436c6f6e6572430d1200114a3e4c620c1b01114a00000002002c56696e746100676544656c61791b421400114af7", 4 | "01fe000053fe81000000000000000000f0013a150101300302593e416a143601114a3e597b403602114a3e56721f1603114a3f000000012b626961732e7260657665726243171600114a3e12491b5601114a3e5113475602114a3e1437673603114a3e46517c0604114a3f1507530605114a3f2666666606114a3e4c4c4d0134f7", 5 | ] 6 | 7 | preset_snd=[] 8 | for h in preset_h: 9 | preset_snd.append (bytes.fromhex(h)) 10 | 11 | from SparkReaderClass import * 12 | 13 | 14 | reader = SparkReadMessage() 15 | reader.set_message(preset_snd) 16 | b = reader.read_message() 17 | 18 | print (b) 19 | print (reader.text) 20 | print (reader.raw) 21 | print (reader.python) 22 | print() 23 | 24 | msg_rx = ["01fe000041ff6a000000000000000000f001046b0301200f001900005924003734323532313100372d433241412d00343133352d3846f7f00104190301000f011939322d370043464441303146103531363727312d20436c65616e2330f7f00104420301200f02192e37", 25 | "01fe000041ff6a0000000000000000002731402d436c65616e280069636f6e2e706e4a674a4270000017f7f00104090301080f03192e62696100732e6e6f69736530676174654313003b114a3d756e4301f7f001046d0301580f0419114a3e29192f1202114a00000400", 26 | "01fe000041ff6a000000000000000000002a436f6d7040726573736f7243f7f00104260301680f05191200114a6e3e2a3b1001114a143f7f474b27426f606f737465724311f7f001041d0301300f061900114a3f080f1f7824547769366e431500114a3f341d09790111", 27 | "01fe000041ff6a0000000000000000004a3ef7f00104460301280f071961740a021b114a3e417054032b114a3e7b13490453114a3f2079532cf7f00104370301000f081943686f72007573416e616c6f3667421400114a3e3541153201114a3ff7f001042c0301000f09", 28 | "01fe000041ff6a00000000000000000019115b1e0223114a3e5d4944034b114a3e000000290044656c61794d6ff7f00104760301600f0a196e6f43156600114a3e1f31206601114a3e6e24611602114a3e7b2457f7f00104590301300f0b1903114a3f341b556a04114a", 29 | "01fe000041ff6a0000000000000000003f090000002b62696100732e7265766572f7f00104070301300f0c19624317000b114a3e2d3027013b114a3e28193b023b114a3e60173203f7f00104240301180f0d19114a3f315b162004114a3e795b797a05114a3e6e594a38", 30 | "01fe000041ff2400000000000000000006114a3e19f7f00104640301180f0e03191a7df7"] 31 | 32 | preset_rx=[] 33 | for h in msg_rx: 34 | preset_rx.append(bytes.fromhex(h)) 35 | 36 | 37 | reader.set_message(preset_rx) 38 | b = reader.read_message() 39 | 40 | print (reader.text) 41 | print (reader.raw) 42 | print (reader.python) 43 | print() 44 | 45 | preset_m = [ 46 | "01fe000053fead000000000000000000f0011077010124030000007f5924006666633862626200312d623037372d00343563662d61300032392d65343135003765363964663002362b4246582d4c4065467265616b2318302e3720286963406f6e2e706e674a3242700000172e62006961732e6e6f6940736567617465430d1200114a3f110b4d0501114a3f0b28020d2a436f6d7072606573736f7243123600114a3e3b4d361601114a3e39f", 47 | "01fe000053fead000000000000000000f0011017010124030100652c2742406f6f73746572434d1100114a3f3015433c245477696e430d1500114a3f32320c2c01114a3f0e694d2d02114a3f165c4c5d03114a3e57414d3e04114a3f4d1a031d2650686173653672421400114a3f36005e5301114a3f3100000002114a003000000003114a00080000002a44656c00617952653230315b421500114a3d075b673101114a3e1a19016b02114af", 48 | "01fe000053fe79000000000000000000f00110460101000302523f2a79577603114a3d4a11271604114a3f000000012b626961732e7260657665726243170600114a3d4e075f5601114a3e5113385602114a3e1437637603114a3e4c69660604114a3f1507500605114a3f2666676606114a3e4c4c4d003af", 49 | "01fe000053fe1a000000000000000000f001117f013800007ff", 50 | ] 51 | 52 | preset_rx=[] 53 | for h in msg_rx: 54 | preset_rx.append(bytes.fromhex(h)) 55 | 56 | 57 | reader.set_message(preset_rx) 58 | b = reader.read_message() 59 | 60 | print (reader.text) 61 | print (reader.raw) 62 | print (reader.python) 63 | print() 64 | -------------------------------------------------------------------------------- /SparkESP32/RingBuffer.ino: -------------------------------------------------------------------------------- 1 | #include "RingBuffer.h" 2 | 3 | // 4 | // RingBuffer class 5 | // 6 | 7 | /* Implementation of a ring buffer - with a difference 8 | * New data is written to a temp area with add_to_buffer() and committed into the buffer once done - commit() 9 | * Commited data is read from the head of the buffer with get_from_buffer() 10 | * Data in this area can be read and updated by index - get_at_index() and set_at_index() 11 | * 12 | * If the temp data is not required it can be ignored using drop() rather than commit() 13 | * 14 | * +----------------------------------++----------------------------------------------------------------------+ 15 | * | 0 | 1 | 2 | 3 | 4 | 5 | 6 || 7 | 8 | 9 | 10 | 11 | 12 | 13 || 14 | 15 | 16 | 17 | 18 | 19 | 20 | | 16 | * +----------------------------------++----------------------------------------------------------------------+ 17 | * ^ ^ ^ ^ 18 | * st ---------- len ------------+ +----------- t_len --------- en 19 | * 20 | * committed data temporary data empty 21 | */ 22 | 23 | RingBuffer::RingBuffer() { 24 | st = 0; 25 | en = 0; 26 | len = 0; 27 | t_len = 0; 28 | } 29 | 30 | bool RingBuffer::add(uint8_t b) { 31 | if (len + t_len < RB_BUFF_MAX) { 32 | rb[en] = b; 33 | t_len++; 34 | en++; 35 | if (en >= RB_BUFF_MAX) en = 0; 36 | return true; 37 | } 38 | else 39 | return false; 40 | } 41 | 42 | bool RingBuffer::get(uint8_t *b) { 43 | if (len > 0) { 44 | *b = rb[st]; 45 | len--; 46 | st++; 47 | if (st >= RB_BUFF_MAX) st = 0; 48 | return true; 49 | } 50 | else 51 | return false; 52 | } 53 | 54 | // set a value at a location in the temp area 55 | bool RingBuffer::set_at_index(int ind, uint8_t b) { 56 | if (ind >= 0 && ind < t_len) { 57 | rb[(st+len+ind) % RB_BUFF_MAX] = b; 58 | return true; 59 | } 60 | else 61 | return false; 62 | } 63 | 64 | // get a value from a location in the temp area 65 | bool RingBuffer::get_at_index(int ind, uint8_t *b) { 66 | if (ind >= 0 && ind < t_len) { 67 | *b = rb[(st+len+ind) % RB_BUFF_MAX]; 68 | return true; 69 | } 70 | else 71 | return false; 72 | } 73 | 74 | bool RingBuffer::set_bit_at_index(int ind, uint8_t b) { 75 | if (ind >= 0 && ind < t_len) { 76 | rb[(st+len+ind) % RB_BUFF_MAX] |= b; 77 | return true; 78 | } 79 | else 80 | return false; 81 | } 82 | 83 | int RingBuffer::get_len() { // total temp len 84 | return t_len; 85 | } 86 | 87 | int RingBuffer::get_pos() { // current position 88 | return t_len; 89 | } 90 | 91 | void RingBuffer::commit() { 92 | len += t_len; 93 | t_len = 0; 94 | } 95 | 96 | void RingBuffer::drop() { 97 | en = st + len; 98 | t_len = 0; 99 | } 100 | 101 | void RingBuffer::clear() { 102 | en = st; 103 | len = 0; 104 | } 105 | 106 | bool RingBuffer::is_empty() { 107 | return (len == 0); 108 | } 109 | 110 | void RingBuffer::dump() { 111 | int i; 112 | 113 | for (i=0; i= 0: 91 | # found the start, trim of anything before that 92 | rd_data = rd_data [a:] 93 | while len(rd_data) < 7: 94 | rd_data += self.read_it(read_len) 95 | blk_len = rd_data[6] 96 | while len(rd_data) < blk_len: 97 | rd_data += self.read_it(read_len) 98 | 99 | res = rd_data[:blk_len] 100 | rd_data = rd_data[blk_len:] 101 | return res 102 | 103 | # get_data will read a number of blocks from get_block, 104 | # dependent on whether this is a multi-chunk message or not 105 | 106 | def get_data(self): 107 | resp=[] 108 | last_block = False 109 | 110 | while not last_block: 111 | blk = self.get_block() 112 | resp.append(blk) 113 | 114 | blk_len = blk[6] 115 | direction = blk[4:6] 116 | seq = blk[18] 117 | cmd = blk[20] 118 | sub_cmd = blk[21] 119 | 120 | 121 | if direction == b'\x53\xfe' and cmd == 0x01 and sub_cmd != 0x04: 122 | # the app sent a message that needs a response 123 | self.send_ack(seq, cmd) 124 | 125 | # now we need to see if this is the last block 126 | 127 | # if the block length is less than the max size then 128 | # definitely last block 129 | # could be a full block and still last one 130 | # but to be full surely means it is a multi-block as 131 | # other messages are always small 132 | # so need to check the chunk counts - in different places 133 | # depending on whether 134 | 135 | if direction == b'\x53\xfe': 136 | if blk_len < 0xad: 137 | last_block = True 138 | else: 139 | # this is sent to Spark so will have a chunk header at top 140 | num_chunks = blk[23] 141 | this_chunk = blk[24] 142 | if this_chunk + 1 == num_chunks: 143 | last_block = True 144 | 145 | if direction == b'\x41\xff': 146 | if blk_len < 0x6a: 147 | last_block = True 148 | else: 149 | # this is from Spark so chunk header could be anywhere 150 | # so search from the end 151 | pos = blk.rfind(b'\xf0\x01') 152 | num_chunks = blk[pos+7] 153 | this_chunk = blk[pos+8] 154 | if this_chunk + 1 == num_chunks: 155 | last_block = True 156 | return resp 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /SparkClass/SparkClass.py: -------------------------------------------------------------------------------- 1 | ######## 2 | # 3 | # Spark Class 4 | # 5 | # Class to package commands to send to Positive Grid Spark 6 | # 7 | # See https://github.com/paulhamsh/Spark-Parser 8 | 9 | 10 | import struct 11 | 12 | 13 | class SparkMessage: 14 | def __init__(self): 15 | 16 | # declarations 17 | self.data = b'' 18 | self.split_data8=[] 19 | self.split_data7=[] 20 | self.final_data=[] 21 | self.cmd=0 22 | self.sub_cmd=0 23 | 24 | ######## Helper functions to package a command for the Spark (handles the 'format bytes' 25 | 26 | 27 | # Start the process - clear the data and create the headers 28 | def start_message (self, cmd, sub_cmd, multi = False): 29 | self.cmd = cmd 30 | self.sub_cmd = sub_cmd 31 | self.multi = multi 32 | self.data=b'' 33 | self.split_data8=[] 34 | self.split_data7=[] 35 | self.final_message=[] 36 | 37 | def end_message(self): 38 | 39 | # determine how many chunks there are 40 | 41 | data_len = len (self.data) 42 | 43 | num_chunks = int ((data_len + 0x7f) / 0x80 ) 44 | 45 | # split the data into chunks of maximum 0x80 bytes (still 8 bit bytes) 46 | # and add a chunk sub-header if a multi-chunk message 47 | 48 | 49 | for this_chunk in range (0, num_chunks): 50 | chunk_len = min (0x80, data_len - (this_chunk * 0x80)) 51 | if (num_chunks > 1): 52 | # we need the chunk sub-header 53 | data8 = bytes([num_chunks]) + bytes([this_chunk]) + bytes([chunk_len]) 54 | else: 55 | data8 = b'' 56 | data8 += self.data[this_chunk * 0x80 : this_chunk * 0x80 + chunk_len] 57 | 58 | self.split_data8.append (data8) 59 | 60 | # now we can convert this to 7-bit data format with the 8-bits byte at the front 61 | # so loop over each chunk 62 | # and in each chunk loop over every sequence of (max) 7 bytes 63 | # and extract the 8th bit and put in 'bit8' 64 | # and then add bit8 and the 7-bit sequence to data7 65 | 66 | for chunk in self.split_data8: 67 | 68 | chunk_len = len (chunk) 69 | num_seq = int ((chunk_len + 6) / 7) 70 | bytes7 = b'' 71 | 72 | for this_seq in range (0, num_seq): 73 | seq_len = min (7, chunk_len - (this_seq * 7)) 74 | bit8 = 0 75 | seq = b'' 76 | for ind in range (0,seq_len): 77 | # can change this so not [dat] and not [ x: x+1] 78 | dat = chunk[this_seq * 7 + ind] 79 | if dat & 0x80 == 0x80: 80 | bit8 |= (1<f", flt) 122 | self.add_bytes(b'\xca') 123 | self.add_bytes(bytes_val) 124 | 125 | 126 | def add_onoff (self, onoff): 127 | if onoff == "On": 128 | b = b'\xc3' 129 | else: 130 | b = b'\xc2' 131 | self.add_bytes(b) 132 | 133 | ######## Functions to package a command for the Spark 134 | 135 | 136 | def change_effect_parameter (self, pedal, param, val): 137 | cmd = 0x01 138 | sub_cmd = 0x04 139 | 140 | self.start_message (cmd, sub_cmd) 141 | self.add_prefixed_string (pedal) 142 | self.add_bytes (bytes([param])) 143 | self.add_float(val) 144 | return self.end_message () 145 | 146 | 147 | def change_effect (self, pedal1, pedal2): 148 | cmd = 0x01 149 | sub_cmd = 0x06 150 | 151 | self.start_message (cmd, sub_cmd) 152 | self.add_prefixed_string (pedal1) 153 | self.add_prefixed_string (pedal2) 154 | return self.end_message () 155 | 156 | def change_hardware_preset (self, preset_num): 157 | # preset_num is 0 to 3 158 | cmd = 0x01 159 | sub_cmd = 0x38 160 | 161 | self.start_message (cmd, sub_cmd) 162 | self.add_bytes (bytes([0])) 163 | self.add_bytes (bytes([preset_num])) 164 | return self.end_message () 165 | 166 | def turn_effect_onoff (self, pedal, onoff): 167 | cmd = 0x01 168 | sub_cmd = 0x15 169 | 170 | self.start_message (cmd, sub_cmd) 171 | self.add_prefixed_string (pedal) 172 | self.add_onoff (onoff) 173 | return self.end_message () 174 | 175 | 176 | def create_preset (self, preset): 177 | cmd = 0x01 178 | sub_cmd = 0x01 179 | this_chunk = 0 180 | 181 | self.start_message (cmd, sub_cmd, True) 182 | self.add_bytes (b'\x00\x7f') 183 | self.add_long_string (preset["UUID"]) 184 | self.add_string (preset["Name"]) 185 | self.add_string (preset["Version"]) 186 | descr = preset["Description"] 187 | if len (descr) > 31: 188 | self.add_long_string (descr) 189 | else: 190 | self.add_string (descr) 191 | self.add_string (preset["Icon"]) 192 | self.add_float (120.0) #preset["BPM"]) 193 | self.add_bytes (bytes([0x90 + 7])) # always 7 pedals 194 | for i in range (0, 7): 195 | self.add_string (preset["Pedals"][i]["Name"]) 196 | self.add_onoff (preset["Pedals"][i]["OnOff"]) 197 | num_p = len(preset["Pedals"][i]["Parameters"]) 198 | self.add_bytes (bytes([num_p + 0x90])) 199 | for p in range (0, num_p): 200 | self.add_bytes (bytes([p])) 201 | self.add_bytes (b'\x91') ### 202 | self.add_float (preset["Pedals"][i]["Parameters"][p]) 203 | self.add_bytes (bytes([preset["End Filler"]])) 204 | return self.end_message () 205 | 206 | -------------------------------------------------------------------------------- /SparkESP32/SparkIO.h: -------------------------------------------------------------------------------- 1 | #ifndef SparkIO_h 2 | #define SparkIO_h 3 | 4 | #include "RingBuffer.h" 5 | #include "SparkStructures.h" 6 | #include "SparkComms.h" 7 | 8 | #define MAX_IO_BUFFER 5000 9 | 10 | uint8_t license_key[64]; 11 | 12 | // BLOCK INPUT CLASS 13 | class BlockIn 14 | { 15 | public: 16 | BlockIn() {}; 17 | void process(); 18 | virtual bool data_available(); 19 | virtual uint8_t data_read(); 20 | virtual void data_write(uint8_t *buf, int len); 21 | 22 | // processing received block 23 | uint8_t *blk_hdr; 24 | RingBuffer *rb; 25 | int rb_state = 0; 26 | int rb_len = 0; 27 | // passthru 28 | uint8_t io_buf[MAX_IO_BUFFER]; 29 | int io_pos = 0; 30 | int io_len = -1; 31 | int io_state = 0; 32 | bool pass_through = true; 33 | }; 34 | 35 | class SparkBlockIn: public BlockIn 36 | { 37 | public: 38 | SparkBlockIn() {}; 39 | bool data_available(); 40 | uint8_t data_read(); 41 | void data_write(uint8_t *buf, int len); 42 | void set(bool pass, RingBuffer *ring_buffer, uint8_t *hdr); 43 | }; 44 | 45 | class AppBlockIn: public BlockIn 46 | { 47 | public: 48 | AppBlockIn() {}; 49 | bool data_available(); 50 | uint8_t data_read(); 51 | void data_write(uint8_t *buf, int len); 52 | void set(bool pass, RingBuffer *ring_buffer, uint8_t *hdr); 53 | }; 54 | 55 | // CHUNK INPUT CLASS 56 | class ChunkIn 57 | { 58 | public: 59 | ChunkIn() {}; 60 | void process(); 61 | // processing received block 62 | RingBuffer *in_chunk; 63 | RingBuffer *in_message; 64 | bool *ok_to_send; 65 | 66 | int rc_state; 67 | bool in_message_bad; 68 | 69 | int rc_seq; 70 | int rc_cmd; 71 | int rc_sub; 72 | int rc_checksum; 73 | 74 | int rc_calc_checksum; 75 | 76 | bool rc_multi_chunk; 77 | int rc_data_pos; 78 | uint8_t rc_bitmask; 79 | int rc_bits; 80 | 81 | int rc_total_chunks; 82 | int rc_this_chunk; 83 | int rc_chunk_len; 84 | int rc_last_chunk; 85 | 86 | uint8_t *rec_seq; // last received sequence from app or amp 87 | }; 88 | 89 | class SparkChunkIn: public ChunkIn 90 | { 91 | public: 92 | SparkChunkIn() {}; 93 | void set(RingBuffer *chunks, RingBuffer *messages, bool *ok, uint8_t *seq); 94 | }; 95 | 96 | class AppChunkIn: public ChunkIn 97 | { 98 | public: 99 | AppChunkIn() {}; 100 | void set(RingBuffer *chunks, RingBuffer *messages, bool *ok, uint8_t *seq); 101 | }; 102 | 103 | // MESSAGE INPUT CLASS 104 | class MessageIn 105 | { 106 | public: 107 | MessageIn() {}; 108 | bool get_message(unsigned int *cmdsub, SparkMessage *msg, SparkPreset *preset); 109 | RingBuffer *in_message; 110 | 111 | void read_string(char *str); 112 | void read_prefixed_string(char *str); 113 | void read_onoff(bool *b); 114 | void read_float(float *f); 115 | void read_uint(uint8_t *b); 116 | void read_byte(uint8_t *b); 117 | }; 118 | 119 | class SparkMessageIn: public MessageIn 120 | { 121 | public: 122 | SparkMessageIn() {}; 123 | void set(RingBuffer *messages); 124 | }; 125 | 126 | class AppMessageIn: public MessageIn 127 | { 128 | public: 129 | AppMessageIn() {}; 130 | void set(RingBuffer *messages); 131 | }; 132 | 133 | // MESSAGE OUTPUT CLASS 134 | class MessageOut 135 | { 136 | public: 137 | MessageOut() {}; 138 | 139 | // creating messages to send 140 | void start_message(int cmdsub); 141 | void end_message(); 142 | void write_byte(byte b); 143 | void write_byte_no_chksum(byte b); 144 | 145 | void write_uint(byte b); 146 | void write_prefixed_string(const char *str); 147 | void write_long_string(const char *str); 148 | void write_string(const char *str); 149 | void write_float(float flt); 150 | void write_onoff(bool onoff); 151 | void write_uint32(uint32_t w); 152 | 153 | void create_preset(SparkPreset *preset); 154 | void turn_effect_onoff(char *pedal, bool onoff); 155 | void change_hardware_preset(uint8_t curr_preset, uint8_t preset_num); 156 | void change_effect(char *pedal1, char *pedal2); 157 | void change_effect_parameter(char *pedal, int param, float val); 158 | void get_serial(); 159 | void get_name(); 160 | void get_hardware_preset_number(); 161 | void get_preset_details(unsigned int preset); 162 | void save_hardware_preset(uint8_t curr_preset, uint8_t preset_num); 163 | void send_firmware_version(uint32_t firmware); 164 | void send_0x022a_info(byte v1, byte v2, byte v3, byte v4); 165 | void send_preset_number(uint8_t preset_h, uint8_t preset_l); 166 | void send_key_ack(); 167 | void send_serial_number(char *serial); 168 | void send_ack(unsigned int cmdsub); 169 | 170 | RingBuffer *out_message; 171 | int cmd_base; 172 | int out_msg_chksum; 173 | }; 174 | 175 | class SparkMessageOut: public MessageOut 176 | { 177 | public: 178 | SparkMessageOut() {}; 179 | void set(RingBuffer *messages); 180 | }; 181 | 182 | class AppMessageOut: public MessageOut 183 | { 184 | public: 185 | AppMessageOut() {}; 186 | void set(RingBuffer *messages); 187 | }; 188 | 189 | // CHUNK INPUT CLASS 190 | class ChunkOut 191 | { 192 | public: 193 | ChunkOut() {}; 194 | void process(); 195 | 196 | void out_store(uint8_t b); 197 | // processing received block 198 | RingBuffer *out_chunk; 199 | RingBuffer *out_message; 200 | int chunk_size; 201 | 202 | uint8_t *rec_seq; 203 | 204 | uint8_t oc_cmd; 205 | uint8_t oc_sub; 206 | unsigned int oc_len; 207 | uint8_t oc_seq; 208 | 209 | uint8_t oc_bit_mask; 210 | int oc_bit_pos; 211 | uint8_t oc_checksum; 212 | }; 213 | 214 | class SparkChunkOut: public ChunkOut 215 | { 216 | public: 217 | SparkChunkOut() {}; 218 | void set(RingBuffer *chunks, RingBuffer *messages, uint8_t *seq); 219 | }; 220 | 221 | class AppChunkOut: public ChunkOut 222 | { 223 | public: 224 | AppChunkOut() {}; 225 | void set(RingBuffer *chunks, RingBuffer *messages, uint8_t *seq); 226 | }; 227 | 228 | // CHUNK INPUT CLASS 229 | class BlockOut 230 | { 231 | public: 232 | BlockOut() {}; 233 | void process(); 234 | virtual void data_write(uint8_t *buf, int len); 235 | 236 | RingBuffer *out_chunk; 237 | bool *ok_to_send; 238 | unsigned int last_sent_time; 239 | bool to_spark; 240 | 241 | int block_size; 242 | uint8_t *blk_hdr; 243 | 244 | uint8_t out_block[0xad]; 245 | int ob_pos; 246 | }; 247 | 248 | class SparkBlockOut: public BlockOut 249 | { 250 | public: 251 | SparkBlockOut() {}; 252 | void set(RingBuffer *chunks, uint8_t *hdr, bool *ok); 253 | void data_write(uint8_t *buf, int len); 254 | }; 255 | 256 | class AppBlockOut: public BlockOut 257 | { 258 | public: 259 | AppBlockOut() {}; 260 | void set(RingBuffer *chunks, uint8_t *hdr, bool *ok); 261 | void data_write(uint8_t *buf, int len); 262 | }; 263 | 264 | ///////////// 265 | 266 | 267 | void spark_start(bool passthru); 268 | 269 | void spark_process(); 270 | 271 | // processing received messages 272 | // bool spark_get_message(unsigned int *cmdsub, SparkMessage *msg, SparkPreset *preset); 273 | 274 | // sending data 275 | 276 | SparkBlockIn sp_bin; 277 | RingBuffer sp_in_chunk; 278 | SparkChunkIn sp_cin; 279 | RingBuffer sp_in_message; 280 | SparkMessageIn spark_msg_in; 281 | 282 | SparkMessageOut spark_msg_out; 283 | RingBuffer sp_out_message; 284 | SparkChunkOut sp_cout; 285 | RingBuffer sp_out_chunk; 286 | SparkBlockOut sp_bout; 287 | 288 | bool sp_ok_to_send; 289 | bool app_ok_to_send; 290 | 291 | uint8_t sp_rec_seq; 292 | uint8_t app_rec_seq; 293 | 294 | void app_process(); 295 | 296 | AppBlockIn app_bin; 297 | RingBuffer app_in_chunk; 298 | AppChunkIn app_cin; 299 | RingBuffer app_in_message; 300 | AppMessageIn app_msg_in; 301 | 302 | AppMessageOut app_msg_out; 303 | RingBuffer app_out_message; 304 | AppChunkOut app_cout; 305 | RingBuffer app_out_chunk; 306 | AppBlockOut app_bout; 307 | 308 | 309 | #endif 310 | 311 | -------------------------------------------------------------------------------- /SparkESP32/Spark.ino: -------------------------------------------------------------------------------- 1 | ///// ROUTINES TO SYNC TO AMP SETTINGS 2 | 3 | int selected_preset; 4 | bool ui_update_in_progress; 5 | 6 | 7 | 8 | int get_effect_index(char *str) { 9 | int ind, i; 10 | 11 | ind = -1; 12 | for (i = 0; ind == -1 && i <= 6; i++) { 13 | if (strcmp(presets[5].effects[i].EffectName, str) == 0) { 14 | ind = i; 15 | } 16 | } 17 | return ind; 18 | } 19 | 20 | 21 | void spark_state_tracker_start() { 22 | connect_to_all(); // sort out bluetooth connections 23 | spark_start(true); // set up the classes to communicate with Spark and app 24 | 25 | selected_preset = 0; 26 | ui_update_in_progress = false; 27 | 28 | // send commands to get preset details for all presets and current state (0x0100) 29 | spark_msg_out.get_preset_details(0x0000); 30 | spark_msg_out.get_preset_details(0x0001); 31 | spark_msg_out.get_preset_details(0x0002); 32 | spark_msg_out.get_preset_details(0x0003); 33 | spark_msg_out.get_preset_details(0x0100); 34 | } 35 | 36 | // get changes from app or Spark and update internal state to reflect this 37 | // this function has the side-effect of loading cmdsub, msg and preset which can be used later 38 | 39 | bool update_spark_state() { 40 | int pres, ind; 41 | 42 | connect_spark(); // reconnects if any disconnects happen 43 | spark_process(); 44 | app_process(); 45 | 46 | // K&R: Expressions connected by && or || are evaluated left to right, 47 | // and it is guaranteed that evaluation will stop as soon as the truth or falsehood is known. 48 | 49 | if (spark_msg_in.get_message(&cmdsub, &msg, &preset) || app_msg_in.get_message(&cmdsub, &msg, & preset)) { 50 | Serial.print("Message: "); 51 | Serial.println(cmdsub, HEX); 52 | 53 | // all the processing for sync 54 | switch (cmdsub) { 55 | // full preset details 56 | case 0x0301: 57 | case 0x0101: 58 | pres = (preset.preset_num == 0x7f) ? 4 : preset.preset_num; 59 | if (preset.curr_preset == 0x01) 60 | pres = 5; 61 | presets[pres] = preset; 62 | break; 63 | // change of amp model 64 | case 0x0306: 65 | strcpy(presets[5].effects[3].EffectName, msg.str2); 66 | break; 67 | // change of effect 68 | case 0x0106: 69 | ind = get_effect_index(msg.str1); 70 | if (ind >= 0) 71 | strcpy(presets[5].effects[ind].EffectName, msg.str2); 72 | break; 73 | // effect on/off 74 | case 0x0315: 75 | case 0x0115: 76 | ind = get_effect_index(msg.str1); 77 | if (ind >= 0) 78 | presets[5].effects[ind].OnOff = msg.onoff; 79 | break; 80 | // change parameter value 81 | case 0x0337: 82 | case 0x0104: 83 | ind = get_effect_index(msg.str1); 84 | if (ind >= 0) 85 | presets[5].effects[ind].Parameters[msg.param1] = msg.val; 86 | break; 87 | // change to preset 88 | case 0x0338: 89 | case 0x0138: 90 | selected_preset = (msg.param2 == 0x7f) ? 4 : msg.param2; 91 | presets[5] = presets[selected_preset]; 92 | break; 93 | // store to preset 94 | case 0x0327: 95 | selected_preset = (msg.param2 == 0x7f) ? 4 : msg.param2; 96 | presets[selected_preset] = presets[5]; 97 | break; 98 | // current selected preset 99 | case 0x0310: 100 | selected_preset = (msg.param2 == 0x7f) ? 4 : msg.param2; 101 | if (msg.param1 == 0x01) 102 | selected_preset = 5; 103 | presets[5] = presets[selected_preset]; 104 | break; 105 | default: 106 | break; 107 | } 108 | 109 | // all the processing for UI update 110 | switch (cmdsub) { 111 | case 0x0201: 112 | if (ui_update_in_progress) { 113 | Serial.println("Updating UI"); 114 | 115 | strcpy(presets[5].Name, "SyncPreset"); 116 | strcpy(presets[5].UUID, "F00DF00D-FEED-0123-4567-987654321000"); 117 | presets[5].curr_preset = 0x00; 118 | presets[5].preset_num = 0x03; 119 | app_msg_out.create_preset(&presets[5]); 120 | app_process(); 121 | delay(100); 122 | 123 | app_msg_out.change_hardware_preset(0x00, 0x00); 124 | app_process(); 125 | app_msg_out.change_hardware_preset(0x00, 0x03); 126 | app_process(); 127 | 128 | // spark_msg_out.change_hardware_preset(0x00, 0x7f); 129 | // spark_process(); 130 | 131 | sp_bin.pass_through = true; 132 | app_bin.pass_through = true; 133 | ui_update_in_progress = false; 134 | } 135 | break; 136 | } 137 | 138 | return true; 139 | } 140 | else 141 | return false; 142 | } 143 | 144 | void update_ui() { 145 | sp_bin.pass_through = false; 146 | app_bin.pass_through = false; 147 | 148 | app_msg_out.save_hardware_preset(0x00, 0x03); 149 | app_process(); 150 | ui_update_in_progress = true; 151 | } 152 | 153 | ///// ROUTINES TO CHANGE AMP SETTINGS 154 | 155 | void change_generic_model(char *new_eff, int slot) { 156 | if (strcmp(presets[5].effects[slot].EffectName, new_eff) != 0) { 157 | spark_msg_out.change_effect(presets[5].effects[slot].EffectName, new_eff); 158 | strcpy(presets[5].effects[slot].EffectName, new_eff); 159 | spark_process(); 160 | delay(100); 161 | } 162 | } 163 | 164 | void change_comp_model(char *new_eff) { 165 | change_generic_model(new_eff, 1); 166 | } 167 | 168 | void change_drive_model(char *new_eff) { 169 | change_generic_model(new_eff, 2); 170 | } 171 | 172 | void change_amp_model(char *new_eff) { 173 | if (strcmp(presets[5].effects[3].EffectName, new_eff) != 0) { 174 | spark_msg_out.change_effect(presets[5].effects[3].EffectName, new_eff); 175 | app_msg_out.change_effect(presets[5].effects[3].EffectName, new_eff); 176 | strcpy(presets[5].effects[3].EffectName, new_eff); 177 | spark_process(); 178 | app_process(); 179 | delay(100); 180 | } 181 | } 182 | 183 | void change_mod_model(char *new_eff) { 184 | change_generic_model(new_eff, 4); 185 | } 186 | 187 | void change_delay_model(char *new_eff) { 188 | change_generic_model(new_eff, 5); 189 | } 190 | 191 | 192 | 193 | void change_generic_onoff(int slot,bool onoff) { 194 | spark_msg_out.turn_effect_onoff(presets[5].effects[slot].EffectName, onoff); 195 | app_msg_out.turn_effect_onoff(presets[5].effects[slot].EffectName, onoff); 196 | presets[5].effects[slot].OnOff = onoff; 197 | spark_process(); 198 | app_process(); 199 | } 200 | 201 | void change_noisegate_onoff(bool onoff) { 202 | change_generic_onoff(0, onoff); 203 | } 204 | 205 | void change_comp_onoff(bool onoff) { 206 | change_generic_onoff(1, onoff); 207 | } 208 | 209 | void change_drive_onoff(bool onoff) { 210 | change_generic_onoff(2, onoff); 211 | } 212 | 213 | void change_amp_onoff(bool onoff) { 214 | change_generic_onoff(3, onoff); 215 | } 216 | 217 | void change_mod_onoff(bool onoff) { 218 | change_generic_onoff(4, onoff); 219 | } 220 | 221 | void change_delay_onoff(bool onoff) { 222 | change_generic_onoff(5, onoff); 223 | } 224 | 225 | void change_reverb_onoff(bool onoff) { 226 | change_generic_onoff(6, onoff); 227 | } 228 | 229 | 230 | void change_generic_toggle(int slot) { 231 | bool new_onoff; 232 | 233 | new_onoff = !presets[5].effects[slot].OnOff; 234 | 235 | spark_msg_out.turn_effect_onoff(presets[5].effects[slot].EffectName, new_onoff); 236 | app_msg_out.turn_effect_onoff(presets[5].effects[slot].EffectName, new_onoff); 237 | presets[5].effects[slot].OnOff = new_onoff; 238 | spark_process(); 239 | app_process(); 240 | } 241 | 242 | void change_noisegate_toggle() { 243 | change_generic_toggle(0); 244 | } 245 | 246 | void change_comp_toggle() { 247 | change_generic_toggle(1); 248 | } 249 | 250 | void change_drive_toggle() { 251 | change_generic_toggle(2); 252 | } 253 | 254 | void change_amp_toggle() { 255 | change_generic_toggle(3); 256 | } 257 | 258 | void change_mod_toggle() { 259 | change_generic_toggle(4); 260 | } 261 | 262 | void change_delay_toggle() { 263 | change_generic_toggle(5); 264 | } 265 | 266 | void change_reverb_toggle() { 267 | change_generic_toggle(6); 268 | } 269 | 270 | 271 | void change_generic_param(int slot, int param, float val) { 272 | float diff; 273 | 274 | // some code to reduce the number of changes 275 | diff = presets[5].effects[slot].Parameters[param] - val; 276 | if (diff < 0) diff = -diff; 277 | if (diff > 0.04) { 278 | spark_msg_out.change_effect_parameter(presets[5].effects[slot].EffectName, param, val); 279 | app_msg_out.change_effect_parameter(presets[5].effects[slot].EffectName, param, val); 280 | presets[5].effects[slot].Parameters[param] = val; 281 | spark_process(); 282 | app_process(); 283 | } 284 | } 285 | 286 | void change_noisegate_param(int param, float val) { 287 | change_generic_param(0, param, val); 288 | } 289 | 290 | void change_comp_param(int param, float val) { 291 | change_generic_param(1, param, val); 292 | } 293 | 294 | void change_drive_param(int param, float val) { 295 | change_generic_param(2, param, val); 296 | } 297 | 298 | void change_amp_param(int param, float val) { 299 | change_generic_param(3, param, val); 300 | } 301 | 302 | void change_mod_param(int param, float val) { 303 | change_generic_param(4, param, val); 304 | } 305 | 306 | void change_delay_param(int param, float val) { 307 | change_generic_param(5, param, val); 308 | } 309 | 310 | void change_reverb_param(int param, float val){ 311 | change_generic_param(6, param, val); 312 | } 313 | 314 | 315 | void change_hardware_preset(int pres_num) { 316 | if (pres_num >= 0 && pres_num <= 3) { 317 | presets[5] = presets[pres_num]; 318 | 319 | spark_msg_out.change_hardware_preset(0, pres_num); 320 | app_msg_out.change_hardware_preset(0, pres_num); 321 | spark_process(); 322 | app_process(); 323 | } 324 | } 325 | 326 | void change_custom_preset(SparkPreset *preset, int pres_num) { 327 | if (pres_num >= 0 && pres_num <= 4) { 328 | preset->preset_num = (pres_num < 4) ? pres_num : 0x7f; 329 | presets[5] = *preset; 330 | presets[pres_num] = *preset; 331 | 332 | spark_msg_out.create_preset(preset); 333 | spark_msg_out.change_hardware_preset(0, preset->preset_num); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /SparkClass/SparkReaderClass.py: -------------------------------------------------------------------------------- 1 | ######## 2 | # 3 | # Spark Class 4 | # 5 | # Class to read commands sent to Positive Grid Spark 6 | # 7 | # See https://github.com/paulhamsh/Spark-Parser 8 | 9 | # Use: reader = SparkReadMessage() 10 | # reader.set_message(preset) 11 | # reader.read_message() 12 | # 13 | # reader.text is a text representation 14 | # reader.raw is a raw unformatted text representation 15 | # reader.pyhon is a python dict representation 16 | # 17 | # reader.data is the input bytes 18 | # reader.message is the 8 bit data in the message 19 | 20 | import struct 21 | 22 | 23 | class SparkReadMessage: 24 | def __init__(self): 25 | self.data = b'' 26 | self.message = [] 27 | 28 | def set_message(self, msg): 29 | self.data = msg 30 | self.message = [] 31 | 32 | def structure_data(self): 33 | self.cmd=0 34 | self.sub_cmd=0 35 | block_content = b'' 36 | 37 | # remove all the block headers and concatenate the contents 38 | # no point in retaining the block structure as chunks can span blocks 39 | # in messages received from Spark 40 | 41 | for block in self.data: 42 | block_length = block[6] 43 | if len (block) != block_length: 44 | print ("Block is length %d and reports %d" % (len(block), block_length)) 45 | 46 | chunk = block[16:] 47 | block_content += chunk 48 | 49 | # and split them into chunks now, splitting on each f7 50 | 51 | chunk_temp = b'' 52 | chunks=[] 53 | for by in block_content: 54 | chunk_temp += bytes([by]) 55 | if by == 0xf7: 56 | chunks.append(chunk_temp) 57 | chunk_temp = b'' 58 | 59 | 60 | # remove the chunk headers, saving the command and sub_command 61 | # and convert the 7 bit data to 8 bits 62 | 63 | chunk_8bit=[] 64 | for chunk in chunks: 65 | this_cmd = chunk[4] 66 | this_sub_cmd = chunk[5] 67 | data7bit = chunk[6:-1] 68 | 69 | chunk_len = len (data7bit) 70 | num_seq = int ((chunk_len + 7) / 8) 71 | data8bit = b'' 72 | 73 | for this_seq in range (0, num_seq): 74 | seq_len = min (8, chunk_len - (this_seq * 8)) 75 | seq = b'' 76 | bit8 = data7bit[this_seq * 8] 77 | for ind in range (0,seq_len-1): 78 | dat = data7bit[this_seq * 8 + ind + 1] 79 | if bit8 & (1<= 0xa0: 131 | str_len = a_byte - 0xa0 132 | else: 133 | a_byte = self.read_byte() 134 | str_len = a_byte - 0xa0 135 | 136 | a_str = "" 137 | for i in range (0, str_len): 138 | a_str += chr(self.read_byte()) 139 | return a_str 140 | 141 | # floats are special - bit 7 is actually stored in the format byte and not in the data 142 | def read_float (self): 143 | prefix = self.read_byte() # should be ca 144 | flt_bytes = b'' 145 | for i in range (0,4): 146 | flt_bytes += bytes([self.read_byte()]) 147 | [val] = struct.unpack(">f", flt_bytes) 148 | return val 149 | 150 | 151 | def read_onoff (self): 152 | a_byte = self.read_byte() 153 | if a_byte == 0xc3: 154 | return "On" 155 | elif a_byte == 0xc2: 156 | return "Off" 157 | else: 158 | return "?" 159 | 160 | ############################# 161 | 162 | def start_str(self): 163 | self.text = "" 164 | self.python = "{" 165 | self.raw = "" 166 | self.dict={} 167 | self.indent = "" 168 | 169 | def end_str (self): 170 | self.python += "}" 171 | 172 | def add_indent(self): 173 | self.indent += "\t" 174 | 175 | def del_indent(self): 176 | self.indent = self.indent[:-1] 177 | 178 | def add_python(self, python_str): 179 | self.python += self.indent + python_str + "\n" 180 | 181 | def add_str(self, a_title, a_str, nature = "alL"): 182 | self.raw += a_str+ " " 183 | self.text += self.indent+ "%-20s" % a_title+":"+ a_str + "\n" 184 | if nature != "python": 185 | self.python += self.indent + "\""+a_title+"\":\""+ a_str + "\",\n" 186 | 187 | def add_int(self, a_title, an_int, nature = "all"): 188 | self.raw += "%d" % an_int + " " 189 | self.text += self.indent + "%-20s" % a_title+ ":" + "%d" % an_int + "\n" 190 | if nature != "python": 191 | self.python += self.indent+ "\"" +a_title+ "\":" + "%d" % an_int + ",\n" 192 | 193 | def add_float(self, a_title, a_float, nature = "all"): 194 | self.raw += "%2.6f" % a_float + " " 195 | self.text += self.indent + "%-20s" % a_title+ ":" + "%2.6f" % a_float + "\n" 196 | if nature == "python": 197 | self.python += self.indent + "%2.6f" % a_float +",\n" 198 | else: 199 | self.python += self.indent + "\"" +a_title+ "\": " + "%2.6f" % a_float +",\n" 200 | 201 | ######## Functions to package a command for the Spark 202 | 203 | 204 | def read_effect_parameter (self): 205 | self.start_str() 206 | effect = self.read_prefixed_string () 207 | param = self.read_byte () 208 | val = self.read_float() 209 | self.add_str ("Effect", effect) 210 | self.add_int ("Parameter", param) 211 | self.add_float ("Value", val) 212 | self.end_str() 213 | 214 | def read_effect (self): 215 | self.start_str() 216 | effect1 = self.read_prefixed_string () 217 | effect2 = self.read_prefixed_string () 218 | self.add_str ("OldEffect", effect1) 219 | self.add_str ("NewEffect", effect2) 220 | self.end_str() 221 | 222 | def read_hardware_preset (self): 223 | self.start_str() 224 | self.read_byte () 225 | preset_num = self.read_byte () 226 | self.add_int ("NewPreset", preset_num) 227 | self.end_str() 228 | 229 | def read_store_hardware_preset (self): 230 | self.start_str() 231 | self.read_byte () 232 | preset_num = self.read_byte () 233 | self.add_int ("NewStoredPreset", preset_num) 234 | self.end_str() 235 | 236 | def read_effect_onoff (self): 237 | self.start_str() 238 | effect = self.read_prefixed_string () 239 | onoff = self.read_onoff () 240 | self.add_str ("Effect", effect) 241 | self.add_str ("OnOff", onoff) 242 | self.end_str() 243 | 244 | def read_preset (self): 245 | self.start_str() 246 | self.read_byte () 247 | preset = self.read_byte() 248 | self.add_int ("PresetNumber", preset) 249 | uuid = self.read_string () 250 | self.add_str ("UUID", uuid) 251 | name = self.read_string () 252 | self.add_str ("Name", name) 253 | version = self.read_string () 254 | self.add_str ("Version", version) 255 | descr = self.read_string () 256 | self.add_str ("Description", descr) 257 | 258 | icon = self.read_string () 259 | self.add_str("Icon", icon) 260 | bpm = self.read_float () 261 | self.add_float ("BPM", bpm) 262 | 263 | num_effects = self.read_byte() - 0x90 264 | self.add_python("\"Pedals\": [") 265 | self.add_indent() 266 | 267 | for i in range (0, 7): 268 | e_str = self.read_string () 269 | e_onoff = self.read_onoff () 270 | self.add_python ("{") 271 | self.add_str ("Name", e_str) 272 | self.add_str ("OnOff", e_onoff) 273 | num_p = self.read_byte() - 0x90 274 | self.add_python("\"Parameters\":[") 275 | self.add_indent() 276 | for p in range (0, num_p): 277 | num = self.read_byte() 278 | spec = self.read_byte () 279 | val = self.read_float() 280 | self.add_int ("Parameter", num, "python") 281 | self.add_str ("Special", hex(spec), "python") 282 | self.add_float ("Value", val, "python") 283 | self.add_python("],") 284 | self.del_indent() 285 | self.add_python("},") 286 | self.add_python("],") 287 | self.del_indent() 288 | unk = self.read_byte() 289 | self.add_str("Unknown", hex(unk)) 290 | self.end_str() 291 | 292 | 293 | ######################## 294 | 295 | def set_interpreter (self, msg): 296 | self.msg = msg 297 | self.msg_pos = 0 298 | 299 | def run_interpreter (self, cmd, sub_cmd): 300 | if cmd == 0x01: 301 | if sub_cmd == 0x01: 302 | self.read_preset() 303 | elif sub_cmd == 0x04: 304 | self.read_effect_parameter() 305 | elif sub_cmd == 0x06: 306 | self.read_effect() 307 | elif sub_cmd == 0x15: 308 | self.read_effect_onoff() 309 | elif sub_cmd == 0x38: 310 | self.read_hardware_preset() 311 | else: 312 | print(hex(cmd), hex(sub_cmd), "not handled") 313 | elif cmd == 0x03: 314 | if sub_cmd == 0x01: 315 | self.read_preset() 316 | elif sub_cmd == 0x06: 317 | self.read_effect() 318 | elif sub_cmd == 0x27: 319 | self.read_store_hardware_preset() 320 | elif sub_cmd == 0x37: 321 | self.read_effect_parameter() 322 | elif sub_cmd == 0x38: 323 | self.read_hardware_preset() 324 | else: 325 | print(hex(cmd), hex(sub_cmd), "not handled") 326 | elif cmd == 0x04: 327 | print ("Acknowledgement") 328 | else: 329 | print ("Unprocessed") 330 | return 1 331 | 332 | def interpret_data(self): 333 | for msg in self.message: 334 | this_cmd = msg[0] 335 | this_sub_cmd = msg[1] 336 | this_data = msg[2] 337 | 338 | self.set_interpreter(this_data) 339 | self.run_interpreter(this_cmd, this_sub_cmd) 340 | 341 | 342 | def read_message(self): 343 | self.structure_data() 344 | self.interpret_data() 345 | return self.message 346 | 347 | 348 | 349 | 350 | -------------------------------------------------------------------------------- /SparkESP32/SparkComms.ino: -------------------------------------------------------------------------------- 1 | #include "SparkComms.h" 2 | 3 | const uint8_t notifyOn[] = {0x1, 0x0}; 4 | 5 | // client callback for connection to Spark 6 | 7 | class MyClientCallback : public BLEClientCallbacks 8 | { 9 | void onConnect(BLEClient *pclient) 10 | { 11 | DEBUG("callback: Spark connected"); 12 | set_conn_status_connected(SPK); 13 | } 14 | 15 | void onDisconnect(BLEClient *pclient) 16 | { 17 | // if (pclient->isConnected()) { 18 | 19 | connected_sp = false; 20 | DEBUG("callback: Spark disconnected"); 21 | set_conn_status_disconnected(SPK); 22 | } 23 | }; 24 | 25 | // server callback for connection to BLE app 26 | 27 | class MyServerCallback : public BLEServerCallbacks 28 | { 29 | void onConnect(BLEServer *pserver) 30 | { 31 | set_conn_status_connected(APP); 32 | DEBUG("callback: BLE app connected"); 33 | } 34 | 35 | void onDisconnect(BLEServer *pserver) 36 | { 37 | 38 | // if (pserver->getConnectedCount() == 1) { 39 | DEBUG("callback: BLE app disconnected"); 40 | set_conn_status_disconnected(APP); 41 | } 42 | }; 43 | 44 | 45 | #ifdef CLASSIC 46 | // server callback for connection to BT classic app 47 | 48 | void bt_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param){ 49 | if(event == ESP_SPP_SRV_OPEN_EVT){ 50 | DEBUG("callback: Classic BT app connected"); 51 | //set_conn_status_connected(APP); 52 | } 53 | 54 | if(event == ESP_SPP_CLOSE_EVT ){ 55 | DEBUG("callback: Classic BT app disconnected"); 56 | set_conn_status_disconnected(APP); 57 | } 58 | } 59 | #endif 60 | 61 | void notifyCB_sp(BLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { 62 | 63 | int i; 64 | byte b; 65 | 66 | for (i = 0; i < length; i++) { 67 | b = pData[i]; 68 | ble_in.add(b); 69 | } 70 | ble_in.commit(); 71 | } 72 | 73 | #ifdef BT_CONTROLLER 74 | // This works with IK Multimedia iRig Blueboard and the Akai LPD8 wireless - interestingly they have the same UUIDs 75 | void notifyCB_pedal(BLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){ 76 | 77 | int i; 78 | byte b; 79 | 80 | for (i = 0; i < length; i++) { 81 | b = pData[i]; 82 | midi_in.add(b); 83 | } 84 | midi_in.commit(); 85 | 86 | set_conn_received(BLE_MIDI); 87 | } 88 | #endif 89 | 90 | class CharacteristicCallbacks: public BLECharacteristicCallbacks { 91 | void onWrite(BLECharacteristic* pCharacteristic) { 92 | int j, l; 93 | const char *p; 94 | byte b; 95 | l = pCharacteristic->getValue().length(); 96 | p = pCharacteristic->getValue().c_str(); 97 | for (j=0; j < l; j++) { 98 | b = p[j]; 99 | ble_app_in.add(b); 100 | } 101 | ble_app_in.commit(); 102 | }; 103 | }; 104 | 105 | static CharacteristicCallbacks chrCallbacks_s, chrCallbacks_r; 106 | 107 | BLEUUID SpServiceUuid(C_SERVICE); 108 | #ifdef BT_CONTROLLER 109 | BLEUUID PedalServiceUuid(PEDAL_SERVICE); 110 | #endif 111 | 112 | 113 | void connect_spark() { 114 | if (found_sp && !connected_sp) { 115 | if (pClient_sp != nullptr && pClient_sp->isConnected()) { 116 | DEBUG("HMMMM - connect_spark() SAYS I WAS CONNECTED ANYWAY"); 117 | } 118 | 119 | if (pClient_sp->connect(sp_device)) { 120 | #ifdef CLASSIC 121 | pClient_sp->setMTU(517); 122 | #endif 123 | connected_sp = true; 124 | pService_sp = pClient_sp->getService(SpServiceUuid); 125 | if (pService_sp != nullptr) { 126 | pSender_sp = pService_sp->getCharacteristic(C_CHAR1); 127 | pReceiver_sp = pService_sp->getCharacteristic(C_CHAR2); 128 | if (pReceiver_sp && pReceiver_sp->canNotify()) { 129 | #ifdef CLASSIC 130 | pReceiver_sp->registerForNotify(notifyCB_sp); 131 | p2902_sp = pReceiver_sp->getDescriptor(BLEUUID((uint16_t)0x2902)); 132 | if (p2902_sp != nullptr) 133 | p2902_sp->writeValue((uint8_t*)notifyOn, 2, true); 134 | #else 135 | if (!pReceiver_sp->subscribe(true, notifyCB_sp, true)) { 136 | connected_sp = false; 137 | DEBUG("Spark disconnected"); 138 | NimBLEDevice::deleteClient(pClient_sp); 139 | } 140 | #endif 141 | } 142 | } 143 | DEBUG("connect_spark(): Spark connected"); 144 | } 145 | } 146 | } 147 | 148 | 149 | #ifdef BT_CONTROLLER 150 | void connect_pedal() { 151 | if (found_pedal && !connected_pedal) { 152 | 153 | if (pClient_pedal->connect(pedal_device)) { 154 | #ifdef CLASSIC 155 | pClient_sp->setMTU(517); 156 | #endif 157 | connected_pedal = true; 158 | pService_pedal = pClient_pedal->getService(PedalServiceUuid); 159 | if (pService_pedal != nullptr) { 160 | pReceiver_pedal = pService_pedal->getCharacteristic(PEDAL_CHAR); 161 | 162 | if (pReceiver_pedal && pReceiver_pedal->canNotify()) { 163 | #ifdef CLASSIC 164 | pReceiver_pedal->registerForNotify(notifyCB_pedal); 165 | p2902_pedal = pReceiver_pedal->getDescriptor(BLEUUID((uint16_t)0x2902)); 166 | if(p2902_pedal != nullptr) 167 | p2902_pedal->writeValue((uint8_t*)notifyOn, 2, true); 168 | #else 169 | if (!pReceiver_pedal->subscribe(true, notifyCB_pedal, true)) { 170 | connected_pedal = false; 171 | DEBUG("Pedal disconnected"); 172 | NimBLEDevice::deleteClient(pClient_pedal); 173 | } 174 | #endif 175 | } 176 | } 177 | DEBUG("connect_pedal(): pedal connected"); 178 | set_conn_status_connected(BLE_MIDI); 179 | } 180 | } 181 | } 182 | #endif 183 | 184 | void connect_to_all() { 185 | int i, j; 186 | uint8_t b; 187 | unsigned long t; 188 | 189 | // set up connection status tracking array 190 | t = millis(); 191 | for (i = 0; i < NUM_CONNS; i++) { 192 | conn_status[i] = false; 193 | for (j = 0; j < 3; j++) 194 | conn_last_changed[j][i] = t; 195 | } 196 | 197 | is_ble = true; 198 | 199 | BLEDevice::init("Spark 40 BLE"); 200 | pClient_sp = BLEDevice::createClient(); 201 | pClient_sp->setClientCallbacks(new MyClientCallback()); 202 | 203 | #ifdef BT_CONTROLLER 204 | pClient_pedal = BLEDevice::createClient(); 205 | #endif 206 | pScan = BLEDevice::getScan(); 207 | 208 | pServer = BLEDevice::createServer(); 209 | pServer->setCallbacks(new MyServerCallback()); 210 | pService = pServer->createService(S_SERVICE); 211 | 212 | #ifdef CLASSIC 213 | pCharacteristic_receive = pService->createCharacteristic(S_CHAR1, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); 214 | pCharacteristic_send = pService->createCharacteristic(S_CHAR2, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); 215 | #else 216 | pCharacteristic_receive = pService->createCharacteristic(S_CHAR1, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); 217 | pCharacteristic_send = pService->createCharacteristic(S_CHAR2, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); 218 | #endif 219 | 220 | pCharacteristic_receive->setCallbacks(&chrCallbacks_r); 221 | pCharacteristic_send->setCallbacks(&chrCallbacks_s); 222 | #ifdef CLASSIC 223 | pCharacteristic_send->addDescriptor(new BLE2902()); 224 | #endif 225 | 226 | pService->start(); 227 | 228 | #ifndef CLASSIC 229 | pServer->start(); 230 | #endif 231 | 232 | pAdvertising = BLEDevice::getAdvertising(); // create advertising instance 233 | pAdvertising->addServiceUUID(pService->getUUID()); // tell advertising the UUID of our service 234 | pAdvertising->setScanResponse(true); 235 | 236 | // Connect to Spark 237 | connected_sp = false; 238 | found_sp = false; 239 | 240 | #ifdef BT_CONTROLLER 241 | connected_pedal = false; 242 | found_pedal = false; 243 | #endif 244 | 245 | while (!found_sp) { // assume we only use a pedal if on already and hopefully found at same time as Spark, don't wait for it 246 | pResults = pScan->start(4); 247 | 248 | for(i = 0; i < pResults.getCount() && !found_sp; i++) { 249 | device = pResults.getDevice(i); 250 | Serial.println(device.toString().c_str()); 251 | 252 | if (device.isAdvertisingService(SpServiceUuid)) { 253 | DEBUG("Found Spark"); 254 | found_sp = true; 255 | connected_sp = false; 256 | sp_device = new BLEAdvertisedDevice(device); 257 | } 258 | 259 | #ifdef BT_CONTROLLER 260 | if (device.isAdvertisingService(PedalServiceUuid) || strcmp(device.getName().c_str(),"iRig BlueBoard") == 0) { 261 | DEBUG("Found pedal"); 262 | found_pedal = true; 263 | connected_pedal = false; 264 | pedal_device = new BLEAdvertisedDevice(device); 265 | } 266 | 267 | 268 | #endif 269 | } 270 | } 271 | 272 | // Set up client 273 | connect_spark(); 274 | #ifdef BT_CONTROLLER 275 | connect_pedal(); 276 | #endif 277 | 278 | #ifdef CLASSIC 279 | DEBUG("Starting classic bluetooth"); 280 | // now advertise Serial Bluetooth 281 | bt = new BluetoothSerial(); 282 | bt->register_callback(bt_callback); 283 | if (!bt->begin (SPARK_BT_NAME)) { 284 | DEBUG("Classic bluetooth init fail"); 285 | while (true); 286 | } 287 | 288 | // flush anything read from App - just in case 289 | while (bt->available()) 290 | b = bt->read(); 291 | DEBUG("Spark 40 Audio set up"); 292 | #endif 293 | 294 | DEBUG("Available for app to connect..."); 295 | pAdvertising->start(); 296 | } 297 | 298 | 299 | // app_available both returns whether any data is available but also selects which type of bluetooth to use based 300 | // on whether there is any input there 301 | bool app_available() { 302 | if (!ble_app_in.is_empty()) { 303 | is_ble = true; 304 | return true; 305 | } 306 | #ifdef CLASSIC 307 | if (bt->available()) { 308 | is_ble = false; 309 | return true; 310 | } 311 | #endif 312 | // if neither have input, then there definitely is no input 313 | return false; 314 | } 315 | 316 | uint8_t app_read() { 317 | set_conn_received(APP); 318 | if (is_ble) { 319 | uint8_t b; 320 | ble_app_in.get(&b); 321 | return b; 322 | } 323 | #ifdef CLASSIC 324 | else 325 | return bt->read(); 326 | #endif 327 | return 0; // just here to avoid compiler warning - shouldn't get to this 328 | } 329 | 330 | void app_write(byte *buf, int len) { 331 | set_conn_sent(APP); 332 | if (is_ble) { 333 | pCharacteristic_send->setValue(buf, len); 334 | pCharacteristic_send->notify(true); 335 | } 336 | #ifdef CLASSIC 337 | else { 338 | bt->write(buf, len); 339 | } 340 | #endif 341 | } 342 | 343 | 344 | void app_write_timed(byte *buf, int len) { // same as app_write but with a slight delay for classic bluetooth - it seems to need it 345 | set_conn_sent(APP); 346 | if (is_ble) { 347 | pCharacteristic_send->setValue(buf, len); 348 | pCharacteristic_send->notify(true); 349 | } 350 | #ifdef CLASSIC 351 | else { 352 | bt->write(buf, len); 353 | delay(50); // this helps the timing of a 'fake' store hardware preset 354 | } 355 | #endif 356 | } 357 | 358 | bool sp_available() { 359 | return !ble_in.is_empty(); 360 | } 361 | 362 | uint8_t sp_read() { 363 | uint8_t b; 364 | 365 | set_conn_received(SPK); 366 | ble_in.get(&b); 367 | return b; 368 | } 369 | 370 | void sp_write(byte *buf, int len) { 371 | set_conn_sent(SPK); 372 | pSender_sp->writeValue(buf, len, false); 373 | } 374 | 375 | // for some reason getRssi() crashes with two clients! 376 | int ble_getRSSI() { 377 | #ifdef BT_CONTROLLER 378 | return 0; 379 | #else 380 | return pClient_sp->getRssi(); 381 | #endif 382 | } 383 | 384 | 385 | // Code to enable UI changes 386 | 387 | 388 | void set_conn_received(int connection) { 389 | conn_last_changed[FROM][connection] = millis(); 390 | } 391 | 392 | void set_conn_sent(int connection) { 393 | conn_last_changed[TO][connection] = millis(); 394 | } 395 | 396 | void set_conn_status_connected(int connection) { 397 | if (conn_status[connection] == false) { 398 | conn_status[connection] = true; 399 | conn_last_changed[STATUS][connection] = millis(); 400 | } 401 | } 402 | 403 | void set_conn_status_disconnected(int connection) { 404 | if (conn_status[connection] == true) { 405 | conn_status[connection] = false; 406 | conn_last_changed[STATUS][connection] = millis(); 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /SparkESP32/SparkIO.ino: -------------------------------------------------------------------------------- 1 | #include "SparkIO.h" 2 | 3 | /* SparkIO 4 | * 5 | * SparkIO handles communication to and from the Positive Grid Spark amp over bluetooth for ESP32 boards 6 | * 7 | * From the programmers perspective, you create and read two formats - a Spark Message or a Spark Preset. 8 | * The Preset has all the data for a full preset (amps, effects, values) and can be sent or received from the amp. 9 | * The Message handles all other changes - change amp, change effect, change value of an effect parameter, change hardware preset and so on 10 | * 11 | * The class is initialized by creating an instance such as: 12 | * 13 | * SparkClass sp; 14 | * 15 | * Conection is handled with the two commands: 16 | * 17 | * sp.start_bt(); 18 | * sp.connect_to_spark(); 19 | * 20 | * The first starts up bluetooth, the second connects to the amp 21 | * 22 | * 23 | * Messages and presets to and from the amp are then queued and processed. 24 | * The essential thing is the have the process() function somewhere in loop() - this handles all the processing of the input and output queues 25 | * 26 | * loop() { 27 | * ... 28 | * sp.process() 29 | * ... 30 | * do something 31 | * ... 32 | * } 33 | * 34 | * Sending functions: 35 | * void create_preset(SparkPreset *preset); 36 | * void get_serial(); 37 | * void turn_effect_onoff(char *pedal, bool onoff); 38 | * void change_hardware_preset(uint8_t preset_num); 39 | * void change_effect(char *pedal1, char *pedal2); 40 | * void change_effect_parameter(char *pedal, int param, float val); 41 | * 42 | * These all create a message or preset to be sent to the amp when they reach the front of the 'send' queue 43 | * 44 | * Receiving functions: 45 | * bool get_message(unsigned int *cmdsub, SparkMessage *msg, SparkPreset *preset); 46 | * 47 | * This receives the front of the 'received' queue - if there is nothing it returns false 48 | * 49 | * Based on whatever was in the queue, it will populate fields of the msg parameter or the preset parameter. 50 | * Eveything apart from a full preset sent from the amp will be a message. 51 | * 52 | * You can determine which by inspecting cmdsub - this will be 0x0301 for a preset. 53 | * 54 | * Other values are: 55 | * 56 | * cmdsub str1 str2 val param1 param2 onoff 57 | * 0323 amp serial # 58 | * 0337 effect name effect val effect number 59 | * 0306 old effect new effect 60 | * 0338 0 new hw preset (0-3) 61 | * 62 | * 63 | * 64 | */ 65 | 66 | 67 | void dump_buf(char *hdr, uint8_t *buf, int len) { 68 | 69 | Serial.print(hdr); 70 | Serial.print(" <"); 71 | Serial.print(buf[18], HEX); 72 | Serial.print("> "); 73 | Serial.print(buf[len-2], HEX); 74 | Serial.print(" "); 75 | Serial.print(buf[len-1], HEX); 76 | 77 | int i; 78 | for (i=0; i<0 ; i++) { 79 | if (buf[i]<16) Serial.print("0"); 80 | Serial.print(buf[i], HEX); 81 | Serial.print(" "); 82 | if (i % 16 == 15) Serial.println(); 83 | }; 84 | Serial.println(); 85 | } 86 | 87 | 88 | // TO FIX 89 | // *ok_to_send shared across classes, and also checked whilst sending to the app 90 | // app_rc_seq / sp_rc_seq - anaylse and work out how it should work!!! 91 | 92 | // UTILITY FUNCTIONS 93 | 94 | void uint_to_bytes(unsigned int i, uint8_t *h, uint8_t *l) { 95 | *h = (i & 0xff00) / 256; 96 | *l = i & 0xff; 97 | } 98 | 99 | void bytes_to_uint(uint8_t h, uint8_t l,unsigned int *i) { 100 | *i = (h & 0xff) * 256 + (l & 0xff); 101 | } 102 | 103 | 104 | // MAIN SparkIO CLASS 105 | 106 | uint8_t chunk_header_from_spark[16]{0x01, 0xfe, 0x00, 0x00, 0x41, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 107 | uint8_t chunk_header_to_spark[16] {0x01, 0xfe, 0x00, 0x00, 0x53, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 108 | 109 | void spark_start(bool passthru) { 110 | sp_bin.set(passthru, &sp_in_chunk, chunk_header_from_spark); 111 | app_bin.set(passthru, &app_in_chunk, chunk_header_to_spark); 112 | 113 | sp_cin.set(&sp_in_chunk, &sp_in_message, &sp_ok_to_send, &sp_rec_seq); 114 | app_cin.set(&app_in_chunk, &app_in_message, &app_ok_to_send, &app_rec_seq); 115 | 116 | spark_msg_in.set(&sp_in_message); 117 | app_msg_in.set(&app_in_message); 118 | 119 | spark_msg_out.set(&sp_out_message); 120 | app_msg_out.set(&app_out_message); 121 | 122 | sp_cout.set(&sp_out_chunk, &sp_out_message, &sp_rec_seq); 123 | app_cout.set(&app_out_chunk, &app_out_message, &app_rec_seq); 124 | 125 | sp_bout.set(&sp_out_chunk, chunk_header_to_spark, &sp_ok_to_send); 126 | app_bout.set(&app_out_chunk, chunk_header_from_spark, &app_ok_to_send); 127 | } 128 | 129 | // 130 | // Main processing routine 131 | // 132 | 133 | void spark_process() 134 | { 135 | // process inputs 136 | sp_bin.process(); 137 | sp_cin.process(); 138 | 139 | if (!sp_ok_to_send && (millis() - sp_bout.last_sent_time > 500)) { 140 | DEBUG("Timeout on send"); 141 | sp_ok_to_send = true; 142 | } 143 | 144 | // process outputs 145 | sp_cout.process(); 146 | sp_bout.process(); 147 | 148 | } 149 | 150 | 151 | void app_process() 152 | { 153 | // process inputs 154 | app_bin.process(); 155 | app_cin.process(); 156 | 157 | // process outputs 158 | app_cout.process(); 159 | app_bout.process(); 160 | } 161 | 162 | // BLOCK INPUT ROUTINES 163 | // Routine to read the block and put into the in_chunk ring buffer 164 | 165 | //////////////////// TEMPORARY TO DEBUG BAD BLOCK PROCESSING 166 | bool in_bad_block = false; 167 | 168 | void BlockIn::process() { 169 | uint8_t b; 170 | bool boo; 171 | 172 | while (data_available()) { 173 | b = data_read(); 174 | 175 | // **** PASSTHROUGH APP AND AMP **** 176 | 177 | if (pass_through) { 178 | if (io_state == 0 && b == 0x01) 179 | io_state = 1; 180 | else if (io_state == 1) { 181 | if (b == 0xfe) { 182 | io_state = 2; 183 | io_buf[0] = 0x01; 184 | io_buf[1] = 0xfe; 185 | io_pos = 2; 186 | io_len = MAX_IO_BUFFER; 187 | } 188 | else if (b == 0x01) { 189 | io_state = 1; 190 | } 191 | else 192 | io_state = 0; 193 | } 194 | else if (io_state == 2) { 195 | if (io_pos == 6) { 196 | io_len = b; 197 | } 198 | io_buf[io_pos++] = b; 199 | if (io_pos >= io_len) { // could only be > if the earlier matching was fooled and len is <= 6 200 | data_write(io_buf, io_pos); 201 | 202 | io_pos = 0; 203 | io_len = MAX_IO_BUFFER; 204 | io_state = 0; 205 | } 206 | } 207 | 208 | if (io_pos > MAX_IO_BUFFER) { 209 | DEBUG("SPARKIO IO_PROCESS_IN_BLOCKS OVERRUN"); 210 | while (true); 211 | } 212 | } 213 | // **** END PASSTHROUGH **** 214 | 215 | if (in_bad_block) { 216 | Serial.print(" * "); 217 | Serial.print(b, HEX); 218 | Serial.print(" * "); 219 | } 220 | 221 | // check the 7th byte which holds the block length 222 | if (rb_state == 6) { 223 | rb_len = b - 16; 224 | rb_state++; 225 | } 226 | // check every other byte in the block header for a match to the header standard 227 | else if (rb_state >= 0 && rb_state < 16) { 228 | if (b == blk_hdr[rb_state]) { 229 | rb_state++; 230 | } 231 | else { 232 | Serial.print(rb_state); 233 | Serial.print(" "); 234 | Serial.print(b, HEX); 235 | Serial.print(" "); 236 | Serial.print(blk_hdr[rb_state], HEX); 237 | Serial.println(); 238 | rb_state = -1; 239 | in_bad_block = true; 240 | DEBUG("SparkIO bad block header"); 241 | } 242 | } 243 | // and once past the header just read the next bytes as defined by rb_len 244 | // store these to the chunk buffer 245 | else if (rb_state == 16) { 246 | in_bad_block = false; 247 | 248 | rb->add(b); 249 | rb_len--; 250 | if (rb_len == 0) { 251 | rb_state = 0; 252 | rb->commit(); 253 | } 254 | } 255 | 256 | // checking for rb_state 0 is done separately so that if a prior step finds a mismatch 257 | // and resets the state to 0 it can be processed here for that byte - saves missing the 258 | // first byte of the header if that was misplaced 259 | 260 | if (rb_state == -1) 261 | if (b == blk_hdr[0]) 262 | rb_state = 1; 263 | } 264 | } 265 | 266 | void SparkBlockIn::set(bool pass, RingBuffer *ring_buffer, uint8_t *hdr) { 267 | rb = ring_buffer; 268 | blk_hdr = hdr; 269 | pass_through = pass; 270 | } 271 | 272 | bool SparkBlockIn::data_available() { 273 | return sp_available(); 274 | } 275 | 276 | uint8_t SparkBlockIn::data_read(){ 277 | return sp_read(); 278 | } 279 | void SparkBlockIn::data_write(uint8_t *buf, int len){ 280 | app_write(buf, len); 281 | } 282 | 283 | void AppBlockIn::set(bool pass, RingBuffer *ring_buffer, uint8_t *hdr) { 284 | rb = ring_buffer; 285 | blk_hdr = hdr; 286 | pass_through = pass; 287 | } 288 | 289 | bool AppBlockIn::data_available() { 290 | return app_available(); 291 | } 292 | 293 | uint8_t AppBlockIn::data_read(){ 294 | return app_read(); 295 | } 296 | 297 | void AppBlockIn::data_write(uint8_t *buf, int len){ 298 | sp_write(buf, len); 299 | } 300 | 301 | // CHUNK INPUT ROUTINES 302 | // Routine to read chunks from the in_chunk ring buffer and copy to a in_message msgpack buffer 303 | 304 | 305 | void SparkChunkIn::set(RingBuffer *chunks, RingBuffer *messages, bool *ok, uint8_t *seq) { 306 | in_chunk = chunks; 307 | in_message = messages; 308 | ok_to_send = ok; 309 | *ok_to_send = true; 310 | rec_seq = seq; 311 | } 312 | 313 | void AppChunkIn::set(RingBuffer *chunks, RingBuffer *messages, bool *ok, uint8_t *seq) { 314 | in_chunk = chunks; 315 | in_message = messages; 316 | ok_to_send = ok; 317 | *ok_to_send = true; 318 | rec_seq = seq; 319 | } 320 | 321 | void ChunkIn::process() { 322 | uint8_t b; 323 | bool boo; 324 | unsigned int len; 325 | uint8_t len_h, len_l; 326 | 327 | while (!in_chunk->is_empty()) { 328 | boo = in_chunk->get(&b); 329 | if (!boo) DEBUG("Chunk is_empty was false but the buffer was empty!"); 330 | 331 | switch (rc_state) { 332 | case 1: 333 | if (b == 0x01) 334 | rc_state++; 335 | else 336 | rc_state = 0; 337 | break; 338 | case 2: 339 | rc_seq = b; 340 | *rec_seq = b; 341 | rc_state++; 342 | break; 343 | case 3: 344 | rc_checksum = b; 345 | rc_state++; 346 | break; 347 | case 4: 348 | rc_cmd = b; 349 | rc_state++; 350 | break; 351 | case 5: 352 | rc_sub = b; 353 | rc_state = 10; 354 | 355 | // flow control for blocking sends - put here in case we want to check rc_sub too 356 | // in here for amp responses - only triggered by the amp 357 | if ((rc_cmd == 0x04 || rc_cmd == 0x05) && rc_sub == 0x01) { 358 | ////////////// THIS FEELS CLUNKY - HOW CAN THIS BE DONE BETTER? <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 359 | if (*ok_to_send == false) { 360 | *ok_to_send = true; 361 | DEBUG("Unblocked"); 362 | } 363 | } 364 | 365 | // set up for the main data loop - rc_state 10 366 | rc_bitmask = 0x80; 367 | rc_calc_checksum = 0; 368 | rc_data_pos = 0; 369 | 370 | // check for multi-chunk 371 | if (rc_sub == 1 && (rc_cmd == 3 || rc_cmd == 1)) 372 | rc_multi_chunk = true; 373 | else { 374 | rc_multi_chunk = false; 375 | in_message_bad = false; 376 | in_message->add(rc_cmd); 377 | in_message->add(rc_sub); 378 | in_message->add(0); 379 | in_message->add(0); 380 | } 381 | break; 382 | case 10: // the main loop which ends on an 0xf7 383 | if (b == 0xf7) { 384 | if (rc_calc_checksum != rc_checksum) 385 | in_message_bad = true; 386 | rc_state = 0; 387 | if (!rc_multi_chunk || (rc_this_chunk == rc_total_chunks-1)) { //last chunk in message 388 | if (in_message_bad) { 389 | DEBUG("Bad message, dropped"); 390 | in_message->drop(); 391 | } 392 | else { 393 | len = in_message->get_len(); 394 | uint_to_bytes(len, &len_h, &len_l); 395 | 396 | in_message->set_at_index(2, len_h); 397 | in_message->set_at_index(3, len_l); 398 | in_message->commit(); 399 | } 400 | } 401 | } 402 | else if (rc_bitmask == 0x80) { // if the bitmask got to this value it is now a new bits 403 | rc_calc_checksum ^= b; 404 | rc_bits = b; 405 | rc_bitmask = 1; 406 | } 407 | else { 408 | rc_data_pos++; 409 | rc_calc_checksum ^= b; 410 | if (rc_bits & rc_bitmask) 411 | b |= 0x80; 412 | rc_bitmask *= 2; 413 | 414 | if (rc_multi_chunk && rc_data_pos == 1) 415 | rc_total_chunks = b; 416 | else if (rc_multi_chunk && rc_data_pos == 2) { 417 | rc_last_chunk = rc_this_chunk; 418 | rc_this_chunk = b; 419 | if (rc_this_chunk == 0) { 420 | in_message_bad = false; 421 | in_message->add(rc_cmd); 422 | in_message->add(rc_sub); 423 | in_message->add(0); 424 | in_message->add(0); 425 | } 426 | else if (rc_this_chunk != rc_last_chunk+1) 427 | in_message_bad = true; 428 | } 429 | else if (rc_multi_chunk && rc_data_pos == 3) 430 | rc_chunk_len = b; 431 | else { 432 | in_message->add(b); 433 | } 434 | 435 | }; 436 | break; 437 | } 438 | 439 | // checking for rc_state 0 is done separately so that if a prior step finds a mismatch 440 | // and resets the state to 0 it can be processed here for that byte - saves missing the 441 | // first byte of the header if that was misplaced 442 | 443 | if (rc_state == 0) { 444 | if (b == 0xf0) 445 | rc_state++; 446 | } 447 | } 448 | } 449 | 450 | 451 | 452 | // MESSAGE INPUT ROUTINES 453 | // Routine to read messages from the in_message ring buffer and copy to a message C structurer 454 | 455 | 456 | void SparkMessageIn::set(RingBuffer *messages) { 457 | in_message = messages; 458 | } 459 | 460 | void AppMessageIn::set(RingBuffer *messages) { 461 | in_message = messages; 462 | } 463 | 464 | void MessageIn::read_byte(uint8_t *b) 465 | { 466 | uint8_t a; 467 | in_message->get(&a); 468 | *b = a; 469 | } 470 | 471 | void MessageIn::read_uint(uint8_t *b) 472 | { 473 | uint8_t a; 474 | in_message->get(&a); 475 | if (a == 0xCC) 476 | in_message->get(&a); 477 | *b = a; 478 | } 479 | 480 | void MessageIn::read_string(char *str) 481 | { 482 | uint8_t a, len; 483 | int i; 484 | 485 | read_byte(&a); 486 | if (a == 0xd9) { 487 | read_byte(&len); 488 | } 489 | else if (a >= 0xa0) { 490 | len = a - 0xa0; 491 | } 492 | else { 493 | read_byte(&a); 494 | if (a < 0xa0 || a >= 0xc0) DEBUG("Bad string"); 495 | len = a - 0xa0; 496 | } 497 | 498 | if (len > 0) { 499 | // process whole string but cap it at STR_LEN-1 500 | for (i = 0; i < len; i++) { 501 | read_byte(&a); 502 | if (a<0x20 || a>0x7e) a=0x20; // make sure it is in ASCII range - to cope with get_serial 503 | if (i < STR_LEN -1) str[i]=a; 504 | } 505 | str[i > STR_LEN-1 ? STR_LEN-1 : i]='\0'; 506 | } 507 | else { 508 | str[0]='\0'; 509 | } 510 | } 511 | 512 | void MessageIn::read_prefixed_string(char *str) 513 | { 514 | uint8_t a, len; 515 | int i; 516 | 517 | read_byte(&a); 518 | read_byte(&a); 519 | 520 | if (a < 0xa0 || a >= 0xc0) DEBUG("Bad string"); 521 | len = a-0xa0; 522 | 523 | if (len > 0) { 524 | for (i = 0; i < len; i++) { 525 | read_byte(&a); 526 | if (a<0x20 || a>0x7e) a=0x20; // make sure it is in ASCII range - to cope with get_serial 527 | if (i < STR_LEN -1) str[i]=a; 528 | } 529 | str[i > STR_LEN-1 ? STR_LEN-1 : i]='\0'; 530 | } 531 | else { 532 | str[0]='\0'; 533 | } 534 | } 535 | 536 | void MessageIn::read_float(float *f) 537 | { 538 | union { 539 | float val; 540 | byte b[4]; 541 | } conv; 542 | uint8_t a; 543 | int i; 544 | 545 | read_byte(&a); // should be 0xca 546 | if (a != 0xca) return; 547 | 548 | // Seems this creates the most significant byte in the last position, so for example 549 | // 120.0 = 0x42F00000 is stored as 0000F042 550 | 551 | for (i=3; i>=0; i--) { 552 | read_byte(&a); 553 | conv.b[i] = a; 554 | } 555 | *f = conv.val; 556 | } 557 | 558 | void MessageIn::read_onoff(bool *b) 559 | { 560 | uint8_t a; 561 | 562 | read_byte(&a); 563 | if (a == 0xc3) 564 | *b = true; 565 | else // 0xc2 566 | *b = false; 567 | } 568 | 569 | // The functions to get the message 570 | 571 | bool MessageIn::get_message(unsigned int *cmdsub, SparkMessage *msg, SparkPreset *preset) 572 | { 573 | uint8_t cmd, sub, len_h, len_l; 574 | unsigned int len; 575 | unsigned int cs; 576 | 577 | uint8_t junk; 578 | int i, j; 579 | uint8_t num; 580 | 581 | if (in_message->is_empty()) return false; 582 | 583 | read_byte(&cmd); 584 | read_byte(&sub); 585 | read_byte(&len_h); 586 | read_byte(&len_l); 587 | 588 | bytes_to_uint(len_h, len_l, &len); 589 | bytes_to_uint(cmd, sub, &cs); 590 | 591 | *cmdsub = cs; 592 | switch (cs) { 593 | 594 | // 0x02 series - requests 595 | // get preset information 596 | case 0x0201: 597 | read_byte(&msg->param1); 598 | read_byte(&msg->param2); 599 | for (i=0; i < 30; i++) read_byte(&junk); // 30 bytes of 0x00 600 | break; 601 | // get current hardware preset number - this is a request with no payload 602 | case 0x0210: 603 | break; 604 | // get amp name - no payload 605 | case 0x0211: 606 | break; 607 | // get name - this is a request with no payload 608 | case 0x0221: 609 | break; 610 | // get serial number - this is a request with no payload 611 | case 0x0223: 612 | break; 613 | // the UNKNOWN command - 0x0224 00 01 02 03 614 | case 0x0224: 615 | case 0x022a: 616 | case 0x032a: 617 | // the data is a fixed array of four bytes (0x94 00 01 02 03) 618 | read_byte(&junk); 619 | read_uint(&msg->param1); 620 | read_uint(&msg->param2); 621 | read_uint(&msg->param3); 622 | read_uint(&msg->param4); 623 | // get firmware version - this is a request with no payload 624 | case 0x022f: 625 | break; 626 | // change effect parameter 627 | case 0x0104: 628 | read_string(msg->str1); 629 | read_byte(&msg->param1); 630 | read_float(&msg->val); 631 | break; 632 | // change of effect model 633 | case 0x0306: 634 | case 0x0106: 635 | read_string(msg->str1); 636 | read_string(msg->str2); 637 | break; 638 | // get current hardware preset number 639 | case 0x0310: 640 | read_byte(&msg->param1); 641 | read_byte(&msg->param2); 642 | break; 643 | // get name 644 | case 0x0311: 645 | read_string(msg->str1); 646 | break; 647 | // enable / disable an effect 648 | // and 0x0128 amp info command 649 | case 0x0315: 650 | case 0x0115: 651 | case 0x0128: 652 | read_string(msg->str1); 653 | read_onoff(&msg->onoff); 654 | break; 655 | // get serial number 656 | case 0x0323: 657 | read_string(msg->str1); 658 | break; 659 | // store into hardware preset 660 | case 0x0327: 661 | read_byte(&msg->param1); 662 | read_byte(&msg->param2); 663 | break; 664 | // amp info 665 | case 0x0328: 666 | read_float(&msg->val); 667 | break; 668 | // firmware version number 669 | case 0x032f: 670 | // really this is a uint32 but it is just as easy to read into 4 uint8 - a bit of a cheat 671 | read_byte(&junk); // this will be 0xce for a uint32 672 | read_byte(&msg->param1); 673 | read_byte(&msg->param2); 674 | read_byte(&msg->param3); 675 | read_byte(&msg->param4); 676 | break; 677 | // change of effect parameter 678 | case 0x0337: 679 | read_string(msg->str1); 680 | read_byte(&msg->param1); 681 | read_float(&msg->val); 682 | break; 683 | // change of preset number selected on the amp via the buttons 684 | case 0x0338: 685 | case 0x0138: 686 | read_byte(&msg->param1); 687 | read_byte(&msg->param2); 688 | break; 689 | // license key 690 | case 0x0170: 691 | for (i = 0; i < 64; i++) 692 | read_uint(&license_key[i]); 693 | break; 694 | // response to a request for a full preset 695 | case 0x0301: 696 | case 0x0101: 697 | read_byte(&preset->curr_preset); 698 | read_byte(&preset->preset_num); 699 | read_string(preset->UUID); 700 | read_string(preset->Name); 701 | read_string(preset->Version); 702 | read_string(preset->Description); 703 | read_string(preset->Icon); 704 | read_float(&preset->BPM); 705 | 706 | for (j=0; j<7; j++) { 707 | read_string(preset->effects[j].EffectName); 708 | read_onoff(&preset->effects[j].OnOff); 709 | read_byte(&num); 710 | preset->effects[j].NumParameters = num - 0x90; 711 | for (i = 0; i < preset->effects[j].NumParameters; i++) { 712 | read_byte(&junk); 713 | read_byte(&junk); 714 | read_float(&preset->effects[j].Parameters[i]); 715 | } 716 | } 717 | read_byte(&preset->chksum); 718 | 719 | break; 720 | // tap tempo! 721 | case 0x0363: 722 | read_float(&msg->val); 723 | break; 724 | case 0x0470: 725 | case 0x0428: 726 | read_byte(&junk); 727 | break; 728 | // acks - no payload to read - no ack sent for an 0x0104 729 | case 0x0401: 730 | case 0x0501: 731 | case 0x0406: 732 | case 0x0415: 733 | case 0x0438: 734 | // Serial.print("Got an ack "); 735 | // Serial.println(cs, HEX); 736 | break; 737 | default: 738 | Serial.print("Unprocessed message SparkIO "); 739 | Serial.print (cs, HEX); 740 | Serial.print(":"); 741 | for (i = 0; i < len - 4; i++) { 742 | read_byte(&junk); 743 | Serial.print(junk, HEX); 744 | Serial.print(" "); 745 | } 746 | Serial.println(); 747 | } 748 | return true; 749 | } 750 | 751 | // 752 | // Output routines 753 | // 754 | 755 | void MessageOut::start_message(int cmdsub) 756 | { 757 | int om_cmd, om_sub; 758 | 759 | om_cmd = (cmdsub & 0xff00) >> 8; 760 | om_sub = cmdsub & 0xff; 761 | 762 | out_message->add(om_cmd); 763 | out_message->add(om_sub); 764 | out_message->add(0); // placeholder for length 765 | out_message->add(0); // placeholder for length 766 | 767 | out_msg_chksum = 0; 768 | } 769 | 770 | void MessageOut::end_message() 771 | { 772 | unsigned int len; 773 | uint8_t len_h, len_l; 774 | 775 | len = out_message->get_len(); 776 | uint_to_bytes(len, &len_h, &len_l); 777 | 778 | out_message->set_at_index(2, len_h); 779 | out_message->set_at_index(3, len_l); 780 | out_message->commit(); 781 | } 782 | 783 | void MessageOut::write_byte_no_chksum(byte b) 784 | { 785 | out_message->add(b); 786 | } 787 | 788 | void MessageOut::write_byte(byte b) 789 | { 790 | out_message->add(b); 791 | out_msg_chksum += int(b); 792 | } 793 | 794 | void MessageOut::write_uint(byte b) 795 | { 796 | if (b > 127) { 797 | out_message->add(0xCC); 798 | out_msg_chksum += int(0xCC); 799 | } 800 | out_message->add(b); 801 | out_msg_chksum += int(b); 802 | } 803 | 804 | void MessageOut::write_uint32(uint32_t w) 805 | { 806 | int i; 807 | uint8_t b; 808 | uint32_t mask; 809 | 810 | mask = 0xFF000000; 811 | 812 | out_message->add(0xCE); 813 | out_msg_chksum += int(0xCE); 814 | 815 | for (i = 3; i >= 0; i--) { 816 | b = (w & mask) >> (8*i); 817 | mask >>= 8; 818 | write_uint(b); 819 | // out_message->add(b); 820 | // out_msg_chksum += int(b); 821 | } 822 | } 823 | 824 | 825 | void MessageOut::write_prefixed_string(const char *str) 826 | { 827 | int len, i; 828 | 829 | len = strnlen(str, STR_LEN); 830 | write_byte(byte(len)); 831 | write_byte(byte(len + 0xa0)); 832 | for (i=0; i=0; i--) { 871 | write_byte(byte(conv.b[i])); 872 | } 873 | } 874 | 875 | void MessageOut::write_onoff (bool onoff) 876 | { 877 | byte b; 878 | 879 | if (onoff) 880 | // true is 'on' 881 | b = 0xc3; 882 | else 883 | b = 0xc2; 884 | write_byte(b); 885 | } 886 | 887 | 888 | void MessageOut::change_effect_parameter (char *pedal, int param, float val) 889 | { 890 | if (cmd_base == 0x0100) 891 | start_message (cmd_base + 0x04); 892 | else 893 | start_message (cmd_base + 0x37); 894 | write_prefixed_string (pedal); 895 | write_byte (byte(param)); 896 | write_float(val); 897 | end_message(); 898 | } 899 | 900 | void MessageOut::change_effect (char *pedal1, char *pedal2) 901 | { 902 | start_message (cmd_base + 0x06); 903 | write_prefixed_string (pedal1); 904 | write_prefixed_string (pedal2); 905 | end_message(); 906 | } 907 | 908 | 909 | 910 | void MessageOut::change_hardware_preset (uint8_t curr_preset, uint8_t preset_num) 911 | { 912 | // preset_num is 0 to 3 913 | 914 | start_message (cmd_base + 0x38); 915 | write_byte (curr_preset); 916 | write_byte (preset_num) ; 917 | end_message(); 918 | } 919 | 920 | 921 | void MessageOut::turn_effect_onoff (char *pedal, bool onoff) 922 | { 923 | start_message (cmd_base + 0x15); 924 | write_prefixed_string (pedal); 925 | write_onoff (onoff); 926 | end_message(); 927 | } 928 | 929 | void MessageOut::get_serial() 930 | { 931 | start_message (0x0223); 932 | end_message(); 933 | } 934 | 935 | void MessageOut::get_name() 936 | { 937 | start_message (0x0211); 938 | end_message(); 939 | } 940 | 941 | void MessageOut::get_hardware_preset_number() 942 | { 943 | start_message (0x0210); 944 | end_message(); 945 | } 946 | 947 | void MessageOut::save_hardware_preset(uint8_t curr_preset, uint8_t preset_num) 948 | { 949 | start_message (cmd_base + 0x27); 950 | // start_message (0x0327); 951 | write_byte (curr_preset); 952 | write_byte (preset_num); 953 | end_message(); 954 | } 955 | 956 | void MessageOut::send_firmware_version(uint32_t firmware) 957 | { 958 | start_message (0x032f); 959 | write_uint32(firmware); 960 | end_message(); 961 | } 962 | 963 | void MessageOut::send_serial_number(char *serial) 964 | { 965 | start_message (0x0323); 966 | write_prefixed_string(serial); 967 | end_message(); 968 | } 969 | 970 | void MessageOut::send_ack(unsigned int cmdsub) { 971 | start_message (cmdsub); 972 | end_message(); 973 | } 974 | 975 | void MessageOut::send_0x022a_info(byte v1, byte v2, byte v3, byte v4) 976 | { 977 | start_message (0x032a); 978 | write_byte(0x94); 979 | write_uint(v1); 980 | write_uint(v2); 981 | write_uint(v3); 982 | write_uint(v4); 983 | end_message(); 984 | } 985 | 986 | void MessageOut::send_key_ack() 987 | { 988 | start_message (0x0470); 989 | write_byte(0x00); 990 | end_message(); 991 | } 992 | 993 | void MessageOut::send_preset_number(uint8_t preset_h, uint8_t preset_l) 994 | { 995 | start_message (0x0310); 996 | write_byte(preset_h); 997 | write_byte(preset_l); 998 | end_message(); 999 | } 1000 | 1001 | void MessageOut::get_preset_details(unsigned int preset) 1002 | { 1003 | int i; 1004 | uint8_t h, l; 1005 | 1006 | uint_to_bytes(preset, &h, &l); 1007 | 1008 | start_message (0x0201); 1009 | write_byte(h); 1010 | write_byte(l); 1011 | 1012 | for (i=0; i<30; i++) { 1013 | write_byte(0); 1014 | } 1015 | 1016 | end_message(); 1017 | } 1018 | 1019 | void MessageOut::create_preset(SparkPreset *preset) 1020 | { 1021 | int i, j, siz; 1022 | 1023 | start_message (cmd_base + 0x01); 1024 | 1025 | write_byte_no_chksum (0x00); 1026 | write_byte_no_chksum (preset->preset_num); 1027 | write_long_string (preset->UUID); 1028 | write_string (preset->Name); 1029 | write_string (preset->Version); 1030 | if (strnlen (preset->Description, STR_LEN) > 31) 1031 | write_long_string (preset->Description); 1032 | else 1033 | write_string (preset->Description); 1034 | write_string(preset->Icon); 1035 | write_float (preset->BPM); 1036 | 1037 | write_byte (byte(0x90 + 7)); // always 7 pedals 1038 | for (i=0; i<7; i++) { 1039 | write_string (preset->effects[i].EffectName); 1040 | write_onoff(preset->effects[i].OnOff); 1041 | 1042 | siz = preset->effects[i].NumParameters; 1043 | write_byte ( 0x90 + siz); 1044 | 1045 | for (j=0; jeffects[i].Parameters[j]); 1049 | } 1050 | } 1051 | write_byte_no_chksum (uint8_t(out_msg_chksum % 256)); 1052 | end_message(); 1053 | } 1054 | 1055 | void SparkMessageOut::set(RingBuffer *messages) { 1056 | out_message = messages; 1057 | cmd_base = 0x0100; 1058 | } 1059 | 1060 | void AppMessageOut::set(RingBuffer *messages) { 1061 | out_message = messages; 1062 | cmd_base = 0x0300; 1063 | } 1064 | 1065 | void ChunkOut::out_store(uint8_t b) 1066 | { 1067 | uint8_t bits; 1068 | 1069 | if (oc_bit_mask == 0x80) { 1070 | oc_bit_mask = 1; 1071 | oc_bit_pos = out_chunk->get_pos(); 1072 | out_chunk->add(0); 1073 | } 1074 | 1075 | if (b & 0x80) { 1076 | out_chunk->set_bit_at_index(oc_bit_pos, oc_bit_mask); 1077 | oc_checksum ^= oc_bit_mask; 1078 | } 1079 | out_chunk->add(b & 0x7f); 1080 | oc_checksum ^= (b & 0x7f); 1081 | 1082 | oc_len++; 1083 | oc_bit_mask *= 2; 1084 | } 1085 | 1086 | void ChunkOut::process() { 1087 | int i, j, len; 1088 | int checksum_pos; 1089 | uint8_t b; 1090 | uint8_t len_h, len_l; 1091 | 1092 | uint8_t num_chunks, this_chunk, this_len; 1093 | 1094 | while (!out_message->is_empty()) { 1095 | out_message->get(&oc_cmd); 1096 | out_message->get(&oc_sub); 1097 | out_message->get(&len_h); 1098 | out_message->get(&len_l); 1099 | bytes_to_uint(len_h, len_l, &oc_len); 1100 | len = oc_len -4; 1101 | 1102 | if (len > chunk_size) { //this is a multi-chunk message 1103 | num_chunks = int(len / chunk_size) + 1; 1104 | for (this_chunk = 0; this_chunk < num_chunks; this_chunk++) { 1105 | 1106 | // create chunk header 1107 | out_chunk->add(0xf0); 1108 | out_chunk->add(0x01); 1109 | 1110 | // WATCH OUT THIS CODE IS IN TWO PLACES!!! NEED TO CHANGE BOTH IF CHANGING EITHER 1111 | // Feels clunky to use a 'global' variable but how else to get the sequence number from the input to the output? 1112 | if (oc_cmd == 0x04 || oc_cmd == 0x05 || oc_cmd == 0x03) { // response, so use other sequence counter 1113 | out_chunk->add(*rec_seq); 1114 | } 1115 | else { 1116 | out_chunk->add(oc_seq); 1117 | oc_seq++; 1118 | if (oc_seq == 0x7f) oc_seq = 0x40; // for sending from amp to app 1119 | if (oc_seq == 0x3f) oc_seq = 0x01; // for sending from app to amp 1120 | }; 1121 | 1122 | 1123 | checksum_pos = out_chunk->get_pos(); 1124 | out_chunk->add(0); // checksum 1125 | 1126 | out_chunk->add(oc_cmd); 1127 | out_chunk->add(oc_sub); 1128 | 1129 | if (num_chunks == this_chunk+1) 1130 | this_len = len % chunk_size; 1131 | else 1132 | this_len = chunk_size; 1133 | 1134 | oc_bit_mask = 0x80; 1135 | oc_checksum = 0; 1136 | 1137 | // create chunk sub-header 1138 | out_store(num_chunks); 1139 | out_store(this_chunk); 1140 | out_store(this_len); 1141 | 1142 | for (i = 0; i < this_len; i++) { 1143 | out_message->get(&b); 1144 | out_store(b); 1145 | } 1146 | out_chunk->set_at_index(checksum_pos, oc_checksum); 1147 | out_chunk->add(0xf7); 1148 | } 1149 | } 1150 | else { 1151 | // create chunk header 1152 | out_chunk->add(0xf0); 1153 | out_chunk->add(0x01); 1154 | 1155 | // Feels clunky to use a 'global' variable but how else to get the sequence number from the input to the output? 1156 | if (oc_cmd == 0x04 || oc_cmd == 0x05 || (oc_cmd == 0x03 && (oc_sub != 0x27 && oc_sub != 0x37 && oc_sub != 0x38 && oc_sub != 0x15 && oc_sub != 0x06))) { // response, so use other sequence counter 1157 | out_chunk->add(*rec_seq); 1158 | } 1159 | else { 1160 | out_chunk->add(oc_seq); 1161 | oc_seq++; 1162 | if (oc_seq == 0x7f) oc_seq = 0x40; // for sending from amp to app 1163 | if (oc_seq == 0x3f) oc_seq = 0x01; // for sending from app to amp 1164 | }; 1165 | 1166 | checksum_pos = out_chunk->get_pos(); 1167 | out_chunk->add(0); // checksum 1168 | 1169 | out_chunk->add(oc_cmd); 1170 | out_chunk->add(oc_sub); 1171 | 1172 | oc_bit_mask = 0x80; 1173 | oc_checksum = 0; 1174 | for (i = 0; i < len; i++) { 1175 | out_message->get(&b); 1176 | out_store(b); 1177 | } 1178 | out_chunk->set_at_index(checksum_pos, oc_checksum); 1179 | out_chunk->add(0xf7); 1180 | } 1181 | out_chunk->commit(); 1182 | } 1183 | } 1184 | 1185 | void SparkChunkOut::set(RingBuffer *chunks, RingBuffer *messages, uint8_t *seq) { 1186 | out_chunk = chunks; 1187 | out_message = messages; 1188 | chunk_size = 0x80; 1189 | oc_seq = 0x01; 1190 | rec_seq = seq; 1191 | } 1192 | 1193 | void AppChunkOut::set(RingBuffer *chunks, RingBuffer *messages, uint8_t *seq) { 1194 | out_chunk = chunks; 1195 | out_message = messages; 1196 | chunk_size = 0x19; 1197 | oc_seq = 0x40; 1198 | rec_seq = seq; 1199 | } 1200 | 1201 | 1202 | void BlockOut::process() { 1203 | int i; 1204 | int len; 1205 | uint8_t b; 1206 | uint8_t cmd, sub; 1207 | 1208 | while (!out_chunk->is_empty() && *ok_to_send) { 1209 | ob_pos = 16; 1210 | 1211 | for (i=0; i < 16; i++) 1212 | out_block[i]= blk_hdr[i]; 1213 | 1214 | b = 0; 1215 | 1216 | // This condition is complex because sending to the Spark is always a block ending in 0xf7 and chunk aligned 1217 | // but the Spark sends a slew of data until it runs out, and the block contains multiple and partial 1218 | // chunks, and with a different block size - inconsistent 1219 | 1220 | while ( (!to_spark && (ob_pos < block_size && !out_chunk->is_empty())) 1221 | || ( to_spark && (b != 0xf7)) ) { 1222 | out_chunk->get(&b); 1223 | 1224 | // look for cmd and sub in the stream and set blocking to true if 0x0101 found - multi chunk 1225 | // not sure if that should be here because it means the block send needs to understand the chunks content 1226 | // perhaps it should be between converting msgpack to chunks and put flow control in there 1227 | if (ob_pos == 20) 1228 | cmd = b; 1229 | if (ob_pos == 21) 1230 | sub = b; 1231 | 1232 | out_block[ob_pos++] = b; 1233 | } 1234 | 1235 | out_block[6] = ob_pos; 1236 | data_write(out_block, ob_pos); 1237 | 1238 | 1239 | 1240 | /* 1241 | for (i=0; i