diff --git a/CMakeLists.txt b/CMakeLists.txt index c1ef571..6bd5827 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ pico_sdk_init() add_executable(gb_link_printer) -pico_generate_pio_header(gb_link_printer ${CMAKE_CURRENT_LIST_DIR}/pio/spi.pio) +#pico_generate_pio_header(gb_link_printer ${CMAKE_CURRENT_LIST_DIR}/pio/spi.pio) # target_include_directories(gbusb PRIVATE ${CMAKE_CURRENT_LIST_DIR}) @@ -16,7 +16,7 @@ target_sources(gb_link_printer PRIVATE main.c ) -target_link_libraries(gb_link_printer PRIVATE pico_stdlib pico_stdio pico_stdlib pico_multicore pico_time pico_sync hardware_pio) # tinyusb_device tinyusb_board) +target_link_libraries(gb_link_printer PRIVATE pico_stdlib pico_stdio pico_stdlib pico_multicore pico_time pico_sync) # hardware_pio) # tinyusb_device tinyusb_board) pico_add_extra_outputs(gb_link_printer) pico_enable_stdio_usb(gb_link_printer 1) diff --git a/main.c b/main.c index c2b8edc..c804d7d 100644 --- a/main.c +++ b/main.c @@ -33,9 +33,10 @@ #include #include // #include "tusb.h" -#include "hardware/pio.h" -#include "pio/pio_spi.h" -const uint SI_PIN = 3; +//#include "hardware/pio.h" +//#include "pio/pio_spi.h" +#include "hardware/gpio.h" +//const uint SI_PIN = 3; //--------------------------------------------------------------------+ // MACRO CONSTANT TYPEDEF PROTYPES //--------------------------------------------------------------------+ @@ -75,10 +76,10 @@ void webserial_task(void); /*------------- MAIN -------------*/ - pio_spi_inst_t spi = { - .pio = pio1, - .sm = 0 - }; +// pio_spi_inst_t spi = { +// .pio = pio1, +// .sm = 0 +// }; #define PIN_SCK 0 @@ -101,22 +102,23 @@ enum printer_state { GB_STATUS }; -void send_byte(uint8_t b) { - gpio_put(PIN_SCK, 1); - gpio_set_dir(PIN_SCK, GPIO_OUT); - - for(int i=0; i < 8; i++) { - gpio_put(PIN_SOUT, b & 0x1); - gpio_put(PIN_SCK, 0); - sleep_us(64); - gpio_put(PIN_SCK, 1); - sleep_us(64); - b = b >> 1; - } - gpio_set_dir(PIN_SCK, GPIO_IN); -} +//void send_byte(uint8_t b) { +// gpio_put(PIN_SCK, 1); +// gpio_set_dir(PIN_SCK, GPIO_OUT); +// +// for(int i=0; i < 8; i++) { +// gpio_put(PIN_SOUT, b & 0x1); +// gpio_put(PIN_SCK, 0); +// sleep_us(64); +// gpio_put(PIN_SCK, 1); +// sleep_us(64); +// b = b >> 1; +// } +// gpio_set_dir(PIN_SCK, GPIO_IN); +//} char data[30*1024]; +char params[4]; int main(void) { @@ -151,6 +153,8 @@ int main(void) uint16_t received_bytes_counter = 0; while(1) { + gpio_put(25, synced); + // wait for clk to go low while(gpio_get(PIN_SCK)) {}; gpio_put(PIN_SOUT, send_data & 0x1); @@ -160,7 +164,6 @@ int main(void) received_data = (received_data << 1) | (gpio_get(PIN_SIN) & 0x1); - gpio_put(25, synced); if(synced == false) { if(received_data != 0x88) { continue; @@ -202,14 +205,6 @@ int main(void) state = GB_COMPRESSION_INDICATOR; if(command == 1) { received_bytes = 0; - } else if(command == 2) { - printf("PRINT "); - for(int i=0; i < received_bytes; i++) { - printf("%02X ", data[i]); - } - printf("\nFIN\n"); - state = GB_WAIT_FOR_SYNC_1; - synced = false; } break; case GB_COMPRESSION_INDICATOR: @@ -233,9 +228,13 @@ int main(void) break; case GB_DATA: // printf("LEN %d\n", length); + if (command == 2) { + params[received_bytes_counter] = received_data; + } else { + data[received_bytes] = received_data; + received_bytes++; + } received_bytes_counter++; - data[received_bytes] = received_data; - received_bytes++; if(length == received_bytes_counter) { state = GB_CHECKSUM_1; } @@ -257,18 +256,33 @@ int main(void) case GB_DEVICE_ID: // send_byte(0x81); // send_byte(0x0); - send_data = 0x0; + send_data = (command == 1) ? 0x0 : 0x10; // Bits must be reversed because data is expected most-significant-bit first (0x10 outputs as 0x08) state = GB_STATUS; // synced = false; break; case GB_STATUS: state = GB_WAIT_FOR_SYNC_1; synced = false; + send_data = 0x0; printf("DONE %d\n", command); printf("Command: %d\n", command); printf("Compression: %d\n", compression_indicator); printf("Length: %d\n", length); + + if(command == 2) { + printf("PARAMS "); + for(int i=0; i < length; i++) { + printf("%02X ", params[i]); + } + printf("\nFIN\n"); + printf("PRINT "); + for(int i=0; i < received_bytes; i++) { + printf("%02X ", data[i]); + } + printf("\nFIN\n"); + } + break; default: printf("INVALID STATE HELP\n"); diff --git a/web/src/index.css b/web/src/index.css index 5f96e47..ffb1be9 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -201,3 +201,26 @@ body { .score { font-size: 200px; } + + +.palette-preview { + display: flex; + width: 480px; + margin: auto; +} +.palette-preview .palette-color { + width: 120px; + height: 120px; + font-size: 10pt; +} +.palette-selector { + border: 1px solid white; + border-radius: 5px; + margin: auto; + margin-top: 10px; + margin-bottom: 10px; + width: 480px; +} +.palette-credits { + font-style: italic; +} \ No newline at end of file diff --git a/web/src/index.js b/web/src/index.js index 02b9319..0d0ea82 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -15,6 +15,145 @@ require('bootstrap'); const fromHexString = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); +class PaletteSelector extends React.Component { + + arraySat = [3,10,17,22,29,37,43,47,54,61,67,75,82,83,88,100]; + arrayBr = [98,94,87,81,76,70,64,57,51,44,38,32,25,19,13,6]; + + constructor(props) { + super(props); + var initHue = 180; + const storedHue = localStorage.getItem('paletteSelector.hue'); + if (storedHue !== null) { + console.log('Initializing hue with stored value: %d', parseInt(storedHue)); + initHue = parseInt(storedHue); + } + var initShift = 0; + const storedShift = localStorage.getItem('paletteSelector.shift'); + if (storedShift !== null) { + console.log('Initializing shift with stored value: %d', parseInt(storedShift)); + initShift = parseInt(storedShift); + } + var initExpression = 0; + const storedExpression = localStorage.getItem('paletteSelector.expression'); + if (storedExpression !== null) { + console.log('Initializing expression with stored value: %d', parseInt(storedExpression)); + initExpression = parseInt(storedExpression); + } + this.state = { + hue: initHue, + shift: initShift, + expression: initExpression, + arrayColor4: [] + }; + } + + componentDidMount() { + this.recalculate(); + } + + handleHueChange(e) { + const newHue = parseInt(e.target.value); + localStorage.setItem('paletteSelector.hue', newHue); + this.setState({hue: newHue}, this.recalculate); + } + + handleShiftChange(e) { + const newShift = parseInt(e.target.value); + localStorage.setItem('paletteSelector.shift', newShift); + this.setState({shift: newShift}, this.recalculate); + } + + handleExpressionChange(e) { + const newExpression = parseInt(e.target.value); + localStorage.setItem('paletteSelector.expression', newExpression); + this.setState({expression: newExpression}, this.recalculate); + } + + getHue(i) { + return Math.abs(i%360); + } + + hsv2rgb(hue, sat, val) { + var red, grn, blu, i, f, p, q, t; + hue %= 360; + if (val === 0) { + return ({ r: 0, g: 0, b: 0 }); + } + if (sat > 100) sat = 100; + if (sat < 0) sat = 0; + if (val > 100) val = 100; + if (val < 0) val = 0; + sat /= 100; + val /= 100; + hue /= 60; + i = Math.floor(hue); + f = hue - i; + p = val * (1 - sat); + q = val * (1 - (sat * f)); + t = val * (1 - (sat * (1 - f))); + if (i === 0) { + red = val; grn = t; blu = p; + } + else if (i === 1) { + red = q; grn = val; blu = p; + } + else if (i === 2) { + red = p; grn = val; blu = t; + } + else if (i === 3) { + red = p; grn = q; blu = val; + } + else if (i === 4) { + red = t; grn = p; blu = val; + } + else if (i === 5) { + red = val; grn = p; blu = q; + } + red = Math.floor(red * 255); + grn = Math.floor(grn * 255); + blu = Math.floor(blu * 255); + return { + r: red, g: grn, b: blu + }; + } + + recalculate() { + var arrayColor16 = []; + for ( var i=0; i<16; i++ ) { + var h = this.getHue( this.state.hue - this.state.shift*(i-8) ); + var shift = (8-Math.abs(i-7))*this.state.expression/5; + arrayColor16[i] = this.hsv2rgb( h, this.arraySat[i] + shift, this.arrayBr[i] + shift ); + } + + // and 4 shaded ramp color array + var arrayColor4 = [ arrayColor16[1], arrayColor16[5], arrayColor16[10], arrayColor16[14] ]; + this.setState({ arrayColor4: arrayColor4 }, () => this.props.onChange(this.state.arrayColor4)) + } + + render() { + return (
+

Custom palette settings:

+ +
+ this.handleHueChange(e)} /> + +
+
+ this.handleShiftChange(e)} /> + +
+
+ this.handleExpressionChange(e)} /> + +
+ +

Custom palette powered by Color Ramp Creator / @kaytrance

+
) + } + +} + class Arcade extends React.Component { ArcStateConnect = "connect"; @@ -23,13 +162,34 @@ class Arcade extends React.Component { ArcStateGame = "game"; ArcStateOver = "over"; + palette = [ + [255, 255, 255], + [170, 170, 170], + [85, 85, 85], + [0, 0, 0], + ]; + margins = { + top: 0, + bottom: 0 + }; + constructor(props) { super(props); + var initUseCustomPalette = false; + const storedUseCustomPalette = localStorage.getItem('arcade.useCustomPalette'); + if (storedUseCustomPalette !== null) { + console.log('Initializing useCustomPalette with stored value: %o', JSON.parse(storedUseCustomPalette)); + initUseCustomPalette = JSON.parse(storedUseCustomPalette); + } this.state = { player1: 0, player2: 0, time: 0, arcade: this.ArcStateConnect, + printdata: null, + linesCount: 0, + customPalette: null, + useCustomPalette: initUseCustomPalette } } @@ -52,69 +212,71 @@ class Arcade extends React.Component { }); } + handlePaletteChange(palette) { + this.setState({customPalette: palette.map(color => [color.r, color.g, color.b])}); + } + + handleCustomPaletteChange(e) { + const newUseCustomPalette = e.target.checked; + localStorage.setItem('arcade.useCustomPalette', newUseCustomPalette); + this.setState({useCustomPalette: newUseCustomPalette}); + } + + handleCanvasRef(ref) { + this.canvasRef = ref; + } + readSerial() { - var palette = [ - [255, 255, 255], - [170, 170, 170], - [85, 85, 85], - [0, 0, 0], - ] this.serial.read().then(result => { console.log("PARSING DATA"); console.log(result); - if(!result.startsWith("PRINT ")) { + if(!result.startsWith("PRINT ") && !result.startsWith("PARAMS ")) { setTimeout(() => { this.readSerial(); - }, 80); + }, 10); return; } - console.log("IT's a rpint!"); - var hexprint = result.substring(6).replaceAll(" ", ""); - console.log(hexprint); - var printdata = fromHexString(hexprint); - console.log(printdata); - var canvas = document.getElementById('printercanvas'); - var ctx = canvas.getContext('2d'); - - var canvasWidth = canvas.width; - var canvasHeight = canvas.height; - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - var id = ctx.getImageData(0, 0, canvasWidth, canvasHeight); - var pixels = id.data; - - for(let y = 0; y < 144/8; y++) { - for (let x = 0; x < 160/8; x++) { - const img_x = x * 8; - const img_y = y * 8; - - let idx = ((y * 320) + x * 16); - for (let y2=0; y2 < 8; y2++) { - let b = y2 * 2; - let local_idx = idx + b; - for(let i=0; i < 8; i++) { - var local_x = img_x + 7 - i; - var local_y = img_y + y2; - var off = (local_y * id.width + local_x) * 4; - var pal_idx = (printdata[local_idx]>>i & 1) | ((printdata[local_idx+1]>>i & 1) << 1); - var pal = palette[pal_idx]; - console.log(pal_idx); - pixels[off] = pal[0]; - pixels[off + 1] = pal[1]; - pixels[off + 2] = pal[2]; - pixels[off + 3] = 255; - } - } - } + if (result.startsWith("PRINT ")) { + console.log("IT's a rpint!"); + var hexprint = result.substring(6).replaceAll(" ", ""); + console.log(hexprint); + var printdata = fromHexString(hexprint); + console.log(printdata); + // Trigger render + this.setState({ + printdata: printdata, + linesCount: ((printdata.length/16)/20) + }); + } else if (result.startsWith("PARAMS ")) { + console.log("Got print parameters"); + var hexparams = result.substring(7).replaceAll(" ", ""); + console.log(hexparams); + var paramsdata = fromHexString(hexparams); + console.log(paramsdata); + console.log("Margin top: %d", (paramsdata[1]>>4) & 0x0f); + console.log("Margin bottom: %d", paramsdata[1] & 0x0f); + console.log("Palette brightest: %d", (paramsdata[2]>>6) & 0x03); + console.log("Palette bright: %d", (paramsdata[2]>>4) & 0x03); + console.log("Palette dark: %d", (paramsdata[2]>>2) & 0x03); + console.log("Palette darkest: %d", paramsdata[2] & 0x03); + // Update palette and margins + this.margins.top = (paramsdata[1]>>4) & 0x0f; + this.margins.bottom = paramsdata[1] & 0x0f; + var paletteIndexes = [ + (paramsdata[2]>>6) & 0x03, + (paramsdata[2]>>4) & 0x03, + (paramsdata[2]>>2) & 0x03, + paramsdata[2] & 0x03 + ]; + this.palette = paletteIndexes.map(index => [ index*0x55, index*0x55, index*0x55 ]); } - ctx.putImageData(id, 0, 0); - setTimeout(() => { this.readSerial(); - }, 80); + }, 10); }); } @@ -136,13 +298,84 @@ class Arcade extends React.Component { ) } else if (this.state.arcade === this.ArcStateIdle) { + var actualPalette = this.state.useCustomPalette ? this.state.customPalette : this.palette; + var canvas = this.canvasRef; + if (canvas && this.state.printdata && this.state.linesCount > 0) { + var ctx = canvas.getContext('2d'); + + // Handle print parameters: palette + margins + console.log("Palette: %o", actualPalette); + console.log("Margins: %o", this.margins); + + // Resize canvas + console.log("%d bytes to print...", this.state.printdata.length); + console.log("%d tiles to print...", (this.state.printdata.length/16)); // 16 bytes per tile + //linesCount = ((this.printdata.length/16)/20); + console.log("%d lines to print...", this.state.linesCount); // 20 tiles per line + canvas.height = 8 * ( this.state.linesCount + this.margins.top + this.margins.bottom); + + var canvasWidth = canvas.width; + var canvasHeight = canvas.height; + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + var id = ctx.getImageData(0, 0, canvasWidth, canvasHeight); + var pixels = id.data; + + var topOffset = this.margins.top * 8 * 160 * 4; // Each margin line is 160 pixels wide and 8 pixels high. Each pixel is 4 bytes. + console.log("pixel data offset = %d", topOffset); + + // Re-render with current palette from raw link data + for(let y = 0; y < this.state.linesCount; y++) { + for (let x = 0; x < 160/8; x++) { + const img_x = x * 8; + const img_y = y * 8; + + let idx = ((y * 320) + x * 16); + for (let y2=0; y2 < 8; y2++) { + let b = y2 * 2; + let local_idx = idx + b; + for(let i=0; i < 8; i++) { + var local_x = img_x + 7 - i; + var local_y = img_y + y2; + var off = topOffset + (local_y * id.width + local_x) * 4; + var pal_idx = (this.state.printdata[local_idx]>>i & 1) | ((this.state.printdata[local_idx+1]>>i & 1) << 1); + var pal = actualPalette[pal_idx]; + //console.log(pal_idx); + pixels[off] = pal[0]; + pixels[off + 1] = pal[1]; + pixels[off + 2] = pal[2]; + pixels[off + 3] = 255; + } + } + } + } + + ctx.putImageData(id, 0, 0); + } + return (

Press print on your Game Boy!

- + this.handleCanvasRef(ref)} id="printercanvas" width="160px" height="144px"> + + {/* Switch between default and custom palette */} +
+ this.handleCustomPaletteChange(e)} /> + +
+ + {/* Display palette being used for rendering (either default or custom) */} + {actualPalette && +
+ {actualPalette.map((color,idx) =>
({color[0]}, {color[1]}, {color[2]})
)} +
+ } + + {/* Custom palette selector */} + this.handlePaletteChange(p)}>
) }