diff --git a/patcher/index.html b/patcher/index.html
new file mode 100644
index 0000000..eb49691
--- /dev/null
+++ b/patcher/index.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ Your patch
+
+ Apply my patch
+
+ Current config
+ Drop UF2 file above to see its config.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/patcher/patcher.js b/patcher/patcher.js
new file mode 100644
index 0000000..74f57d1
--- /dev/null
+++ b/patcher/patcher.js
@@ -0,0 +1,503 @@
+"use strict";
+
+const configKeys = {
+ PIN_ACCELEROMETER_INT: 1,
+ PIN_ACCELEROMETER_SCL: 2,
+ PIN_ACCELEROMETER_SDA: 3,
+ PIN_BTN_A: 4,
+ PIN_BTN_B: 5,
+ PIN_BTN_SLIDE: 6,
+ PIN_DOTSTAR_CLOCK: 7,
+ PIN_DOTSTAR_DATA: 8,
+ PIN_FLASH_CS: 9,
+ PIN_FLASH_MISO: 10,
+ PIN_FLASH_MOSI: 11,
+ PIN_FLASH_SCK: 12,
+ PIN_LED: 13,
+ PIN_LIGHT: 14,
+ PIN_MICROPHONE: 15,
+ PIN_MIC_CLOCK: 16,
+ PIN_MIC_DATA: 17,
+ PIN_MISO: 18,
+ PIN_MOSI: 19,
+ PIN_NEOPIXEL: 20,
+ PIN_RX: 21,
+ PIN_RXLED: 22,
+ PIN_SCK: 23,
+ PIN_SCL: 24,
+ PIN_SDA: 25,
+ PIN_SPEAKER_AMP: 26,
+ PIN_TEMPERATURE: 27,
+ PIN_TX: 28,
+ PIN_TXLED: 29,
+ PIN_IR_OUT: 30,
+ PIN_IR_IN: 31,
+ PIN_DISPLAY_SCK: 32,
+ PIN_DISPLAY_MISO: 33,
+ PIN_DISPLAY_MOSI: 34,
+ PIN_DISPLAY_CS: 35,
+ PIN_DISPLAY_DC: 36,
+ DISPLAY_WIDTH: 37,
+ DISPLAY_HEIGHT: 38,
+ DISPLAY_CFG0: 39,
+ DISPLAY_CFG1: 40,
+ DISPLAY_CFG2: 41,
+ DISPLAY_CFG3: 42,
+ PIN_DISPLAY_RST: 43,
+ PIN_DISPLAY_BL: 44,
+ PIN_SERVO_1: 45,
+ PIN_SERVO_2: 46,
+ PIN_BTN_LEFT: 47,
+ PIN_BTN_RIGHT: 48,
+ PIN_BTN_UP: 49,
+ PIN_BTN_DOWN: 50,
+ PIN_BTN_MENU: 51,
+ PIN_LED_R: 52,
+ PIN_LED_G: 53,
+ PIN_LED_B: 54,
+ PIN_LED1: 55,
+ PIN_LED2: 56,
+ PIN_LED3: 57,
+ PIN_LED4: 58,
+ SPEAKER_VOLUME: 59,
+ PIN_JACK_TX: 60,
+ PIN_JACK_SENSE: 61,
+ PIN_JACK_HPEN: 62,
+ PIN_JACK_BZEN: 63,
+ PIN_JACK_PWREN: 64,
+ PIN_JACK_SND: 65,
+ PIN_JACK_BUSLED: 66,
+ PIN_JACK_COMMLED: 67,
+ PIN_BTNMX_LATCH: 66,
+ PIN_BTNMX_CLOCK: 67,
+ PIN_BTNMX_DATA: 68,
+ PIN_BTN_SOFT_RESET: 69,
+ ACCELEROMETER_TYPE: 70,
+ PIN_A0: 100,
+ PIN_A1: 101,
+ PIN_A2: 102,
+ PIN_A3: 103,
+ PIN_A4: 104,
+ PIN_A5: 105,
+ PIN_A6: 106,
+ PIN_A7: 107,
+ PIN_A8: 108,
+ PIN_A9: 109,
+ PIN_A10: 110,
+ PIN_A11: 111,
+ PIN_A12: 112,
+ PIN_A13: 113,
+ PIN_A14: 114,
+ PIN_A15: 115,
+ PIN_D0: 150,
+ PIN_D1: 151,
+ PIN_D2: 152,
+ PIN_D3: 153,
+ PIN_D4: 154,
+ PIN_D5: 155,
+ PIN_D6: 156,
+ PIN_D7: 157,
+ PIN_D8: 158,
+ PIN_D9: 159,
+ PIN_D10: 160,
+ PIN_D11: 161,
+ PIN_D12: 162,
+ PIN_D13: 163,
+ PIN_D14: 164,
+ PIN_D15: 165,
+ NUM_NEOPIXELS: 200,
+ NUM_DOTSTARS: 201,
+ DEFAULT_BUTTON_MODE: 202,
+ SWD_ENABLED: 203,
+ FLASH_BYTES: 204,
+ RAM_BYTES: 205,
+ SYSTEM_HEAP_BYTES: 206,
+ LOW_MEM_SIMULATION_KB: 207,
+ BOOTLOADER_BOARD_ID: 208,
+ UF2_FAMILY: 209,
+ PINS_PORT_SIZE: 210,
+}
+
+const enums = {
+ // these are the same as the default I2C ID
+ ACCELEROMETER_TYPE: {
+ LIS3DH: 0x32,
+ MMA8453: 0x38,
+ FXOS8700: 0x3C,
+ MMA8653: 0x3A,
+ MSA300: 0x4C,
+ },
+ UF2_FAMILY: {
+ ATSAMD21: 0x68ed2b88,
+ ATSAMD51: 0x55114460,
+ NRF52840: 0x1b57745f,
+ STM32F103: 0x5ee21072,
+ STM32F401: 0x57755a57,
+ ATMEGA32: 0x16573617,
+ CYPRESS_FX2: 0x5a18069b,
+ },
+ PINS_PORT_SIZE: {
+ PA_16: 16, // PA00-PA15, PB00-PB15, ... - STM32
+ PA_32: 32, // PA00-PA31, ... - ATSAMD
+ P0_16: 1016, // P0_0-P0_15, P1_0-P1_15, ...
+ P0_32: 1032, // P0_0-P0_32, ... - NRF
+ },
+ DEFAULT_BUTTON_MODE: {
+ ACTIVE_HIGH_PULL_DOWN: 0x11,
+ ACTIVE_HIGH_PULL_UP: 0x21,
+ ACTIVE_HIGH_PULL_NONE: 0x31,
+ ACTIVE_LOW_PULL_DOWN: 0x10,
+ ACTIVE_LOW_PULL_UP: 0x20,
+ ACTIVE_LOW_PULL_NONE: 0x30,
+ },
+}
+
+
+let infoMsg = ""
+
+function log(msg) {
+ infoMsg += msg + "\n"
+ console.log(msg)
+}
+
+function help() {
+ console.log(`
+USAGE: node patch-cfg.js file.uf2 [patch.cf2]
+
+Without .cf2 file, it will parse config in the UF2 file and print it out
+(in .cf2 format).
+
+With .cf2 file, it will patch in-place the UF2 file with specified config.
+`)
+ process.exit(1)
+}
+
+function readBin(fn) {
+ const fs = require("fs")
+
+ if (!fn) {
+ console.log("Required argument missing.")
+ help()
+ }
+
+ try {
+ return fs.readFileSync(fn)
+ } catch (e) {
+ console.log("Cannot read file '" + fn + "': " + e.message)
+ help()
+ }
+}
+const configInvKeys = {}
+
+const UF2_MAGIC_START0 = 0x0A324655 // "UF2\n"
+const UF2_MAGIC_START1 = 0x9E5D5157 // Randomly selected
+const UF2_MAGIC_END = 0x0AB16F30 // Ditto
+
+const CFG_MAGIC0 = 0x1e9e10f1
+const CFG_MAGIC1 = 0x20227a79
+
+function err(msg) {
+ log("Fatal error: " + msg)
+ if (typeof window == "undefined") {
+ process.exit(1)
+ } else {
+ throw new Error(msg)
+ }
+}
+
+function read32(buf, off) {
+ return (buf[off + 0] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)) >>> 0
+}
+
+function write32(buf, off, v) {
+ buf[off + 0] = v & 0xff
+ buf[off + 1] = (v >> 8) & 0xff
+ buf[off + 2] = (v >> 16) & 0xff
+ buf[off + 3] = (v >> 24) & 0xff
+}
+
+
+function readWriteConfig(buf, patch) {
+ let patchPtr = null
+ let origData = []
+ let cfgLen = 0
+ if (patch)
+ patch.push(0, 0)
+ for (let off = 0; off < buf.length; off += 512) {
+ if (read32(buf, off) != UF2_MAGIC_START0 ||
+ read32(buf, off + 4) != UF2_MAGIC_START1) {
+ err("invalid data at " + off)
+ }
+
+ const payloadLen = read32(buf, off + 16)
+
+ for (let i = 32; i < 32 + payloadLen; i += 4) {
+ if (read32(buf, off + i) == CFG_MAGIC0 &&
+ read32(buf, off + i + 4) == CFG_MAGIC1) {
+ let addr = "0x" + (read32(buf, off + 12) + i - 32).toString(16)
+ if (patchPtr === null) {
+ log(`# Found CFG DATA at ${addr}`)
+ patchPtr = -4
+ } else {
+ log(`# Skipping second CFG DATA at ${addr}`)
+ }
+ }
+
+ if (patchPtr !== null) {
+ if (patchPtr == -2) {
+ cfgLen = read32(buf, off + i)
+ if (patch)
+ write32(buf, off + i, (patch.length >> 1) - 1)
+ }
+
+ if (patchPtr >= 0) {
+ if (origData.length < cfgLen * 2 + 2)
+ origData.push(read32(buf, off + i))
+ if (patch) {
+ if (patchPtr < patch.length) {
+ write32(buf, off + i, patch[patchPtr])
+ }
+ }
+ }
+ patchPtr++
+ }
+ }
+ }
+
+ if (origData.length == 0)
+ err("config data not found")
+ if (patch && patchPtr < patch.length)
+ err("no space for config data")
+ if (origData.slice(origData.length - 2).some(x => x != 0))
+ err("config data not zero terminated")
+ origData = origData.slice(0, origData.length - 2)
+ return origData
+}
+
+
+function lookupCfg(cfgdata, key) {
+ for (let i = 0; i < cfgdata.length; i += 2)
+ if (cfgdata[i] == key)
+ return cfgdata[i + 1]
+ return null
+}
+
+function pinToString(pinNo, portSize) {
+ let useLetters = true
+ if (portSize > 1000) {
+ portSize = portSize % 1000;
+ useLetters = true
+ }
+ let port = (pinNo / portSize) | 0
+ let pin = pinNo % portSize
+ if (useLetters) {
+ return "P" + String.fromCharCode(65 + port) + ("0" + pin.toString()).slice(-2)
+ } else {
+ return "P" + port + "_" + pin
+ }
+}
+
+function showKV(k, v, portSize) {
+ let vn = ""
+
+ let kn = configInvKeys[k + ""] || ""
+
+ if (enums[kn]) {
+ for (let en of Object.keys(enums[kn])) {
+ if (enums[kn][en] == v) {
+ vn = en
+ break
+ }
+ }
+ }
+
+ if (vn == "") {
+ if (/_CFG/.test(kn) || v > 10000)
+ vn = "0x" + v.toString(16)
+ else if (/^PIN_/.test(kn))
+ vn = pinToString(v, portSize)
+ else
+ vn = v + ""
+ }
+
+ if (kn == "")
+ kn = "_" + k
+
+ return `${kn} = ${vn}`
+}
+
+function readConfig(buf) {
+ init()
+ let cfgdata = readWriteConfig(buf, null)
+ let portSize = lookupCfg(cfgdata, configKeys.PINS_PORT_SIZE)
+ //if (portSize === null)
+ // cfgdata.push(configKeys.PINS_PORT_SIZE, portSize = enums.PINS_PORT_SIZE.PA_16)
+ let numentries = cfgdata.length >> 1
+ let lines = []
+ for (let i = 0; i < numentries; ++i) {
+ lines.push(showKV(cfgdata[i * 2], cfgdata[i * 2 + 1], portSize))
+ }
+ lines.sort()
+ return lines.join("\n")
+}
+
+function patchConfig(buf, cfg) {
+ init()
+ const cfgMap = {}
+ let lineNo = 0
+ for (let line of cfg.split(/\n/)) {
+ lineNo++
+ line = line.replace(/(#|\/\/).*/, "")
+ line = line.trim()
+ if (!line)
+ continue
+ let m = /(\w+)\s*=\s*(\w+)/.exec(line)
+ if (!m)
+ err("syntax error at config line " + lineNo)
+ let kn = m[1].toUpperCase()
+ let k = configKeys[kn]
+ if (!k && /^_\d+$/.test(kn))
+ k = parseInt(kn.slice(1))
+ if (!k)
+ err("Unrecognized key name: " + kn)
+ cfgMap[configKeys[kn] + ""] = m[2]
+ }
+
+ let cfgdata = readWriteConfig(buf, null)
+
+ for (let i = 0; i < cfgdata.length; i += 2) {
+ let k = cfgdata[i] + ""
+ if (!cfgMap.hasOwnProperty(k))
+ cfgMap[k] = cfgdata[i + 1] + ""
+ }
+
+ const forAll = f => {
+ for (let k of Object.keys(cfgMap)) {
+ let kn = configInvKeys[k]
+ f(kn, k, cfgMap[k])
+ }
+ }
+
+ // expand enums
+ forAll((kn, k, v) => {
+ let e = enums[kn]
+ if (e && e[v.toUpperCase()])
+ cfgMap[k] = e[v] + ""
+ })
+
+ let portSize = cfgMap[configKeys.PINS_PORT_SIZE]
+ if (portSize) portSize = parseInt(portSize)
+ let portSize0 = portSize
+ if (portSize)
+ portSize = portSize % 1000
+
+ // expand pin names
+ forAll((kn, k, v) => {
+ let thePort = -1
+ let pin = -1
+
+ let m = /^P([A-Z])_?(\d+)$/.exec(v)
+ if (m) {
+ pin = parseInt(m[2])
+ thePort = m[1].charCodeAt(0) - 65
+ }
+
+ m = /^P(\d+)_(\d+)$/.exec(v)
+ if (m) {
+ pin = parseInt(m[2])
+ thePort = parseInt(m[1])
+ }
+
+ if (thePort >= 0) {
+ if (!portSize) err("PINS_PORT_SIZE not specified, while trying to parse PIN " + v)
+ if (pin >= portSize) err("Pin name invalid: " + v)
+ cfgMap[k] = (thePort * portSize + pin) + ""
+ }
+ })
+
+ // expand existing keys
+ for (let i = 0; i < 10; ++i)
+ forAll((kn, k, v) => {
+ if (configKeys[v]) {
+ let curr = cfgMap[configKeys[v] + ""]
+ if (curr == null)
+ err("Value not specified, but referenced: " + v)
+ cfgMap[k] = curr
+ }
+ })
+
+ forAll((kn, k, v) => {
+ if (isNaN(parseInt(v)))
+ err("Value not understood: " + v)
+ })
+
+ let sorted = Object.keys(cfgMap)
+ sorted.sort((a, b) => parseInt(a) - parseInt(b))
+ let patch = []
+ for (let k of sorted) {
+ patch.push(parseInt(k))
+ patch.push(parseInt(cfgMap[k]))
+ }
+
+ let changes = ""
+ for (let i = 0; i < patch.length; i += 2) {
+ let k = patch[i]
+ let v = patch[i + 1]
+ let old = lookupCfg(cfgdata, k)
+ if (old != v) {
+ let newOne = showKV(k, v, portSize0)
+ if (old !== null) {
+ let oldOne = showKV(k, old, portSize0)
+ newOne += " (was: " + oldOne.replace(/.* = /, "") + ")"
+ }
+ changes += newOne + "\n"
+ }
+ }
+
+ readWriteConfig(buf, patch)
+
+ return changes
+}
+
+function parseHFile(hFile) {
+ if (!hFile) return
+ for (let line of hFile.split(/\n/)) {
+ line = line.trim()
+ let m = /#define\s+CFG_(\w+)\s+(\d+)/.exec(line)
+ if (m) {
+ let k = m[1]
+ let v = parseInt(m[2])
+ configKeys[k] = parseInt(v)
+ configInvKeys[v + ""] = k
+ console.log(` ${k}: ${v},`)
+ }
+ }
+}
+
+function init() {
+ for (let k of Object.keys(configKeys)) {
+ let v = configKeys[k]
+ configInvKeys[v + ""] = k
+ }
+}
+
+function main() {
+ let uf2 = readBin(process.argv[2])
+
+ if (process.argv[3]) {
+ let cfg = readBin(process.argv[3]).toString("utf8")
+ let changes = patchConfig(uf2, cfg)
+ if (!changes)
+ console.log("No changes.")
+ else
+ console.log("\nChanges:\n" + changes)
+ console.log("# Writing config...")
+ fs.writeFileSync(process.argv[2], uf2)
+ } else {
+ console.log(readConfig(uf2))
+ }
+}
+
+
+if (typeof window == "undefined")
+ main()
\ No newline at end of file
diff --git a/patcher/web.js b/patcher/web.js
new file mode 100644
index 0000000..7601069
--- /dev/null
+++ b/patcher/web.js
@@ -0,0 +1,94 @@
+"use strict";
+
+function savePatch(ev) {
+ let text = document.getElementById("patch")
+ localStorage["UF2_PATCH"] = text.value
+}
+
+function restorePatch() {
+ let text = document.getElementById("patch")
+ text.value = localStorage["UF2_PATCH"]
+ document.getElementById("apply").onclick = applyPatch
+}
+
+let currUF2 = null
+let currUF2Name = ""
+
+function showMSG() {
+ if (infoMsg)
+ document.getElementById("currconfig").textContent = infoMsg
+}
+
+function wrap(f) {
+ try {
+ infoMsg = ""
+ f()
+ showMSG()
+ } catch (e) {
+ log("Exception: " + e.message)
+ showMSG()
+ }
+
+}
+
+function applyPatch() {
+ wrap(() => {
+ let text = document.getElementById("patch")
+ let newcfg = text.value.trim()
+ if (!currUF2)
+ log("You have to drop a UF2 file with bootloader above before applying patches.")
+ else if (!newcfg)
+ log("You didn't give any patch to apply.")
+ else {
+ let buf = currUF2.slice()
+ let changes = patchConfig(buf, newcfg)
+ if (!changes) {
+ log("No changes.")
+ } else {
+ log("\nChanges:\n" + changes)
+ log("Downloading " + currUF2Name)
+
+ let blob = new Blob([buf], {
+ type: "application/x-uf2"
+ });
+ let url = URL.createObjectURL(blob);
+
+ let a = document.createElement("a");
+ document.body.appendChild(a);
+ a.style = "display: none";
+ a.href = url;
+ a.download = currUF2Name
+ a.click();
+ window.URL.revokeObjectURL(url);
+ }
+ }
+ })
+}
+
+function dropHandler(ev) {
+ ev.preventDefault();
+
+ for (let i = 0; i < ev.dataTransfer.items.length; i++) {
+ if (ev.dataTransfer.items[i].kind === 'file') {
+ let file = ev.dataTransfer.items[i].getAsFile();
+ let reader = new FileReader();
+ reader.onload = e => {
+ wrap(() => {
+ let buf = new Uint8Array(reader.result)
+ let cfg = readConfig(buf)
+ currUF2 = buf
+ infoMsg = ""
+ currUF2Name = file.name
+ document.getElementById("currconfig").textContent = cfg
+ })
+ }
+ reader.readAsArrayBuffer(file);
+ break
+ }
+ }
+}
+
+function dragOverHandler(ev) {
+ ev.preventDefault();
+ ev.dataTransfer.dropEffect = 'copy';
+}
\ No newline at end of file