From 28d30db20644d4bc754d552a244525d7c786073f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=A7=84=ED=98=B8?= <140678689+jinbaekho@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:44:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=ED=81=90=EB=B8=8C=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block_pingpongConnect_lite.js | 1008 +++++++++++++++++ 1 file changed, 1008 insertions(+) create mode 100644 src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js diff --git a/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js b/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js new file mode 100644 index 0000000000..2a1f8339b0 --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js @@ -0,0 +1,1008 @@ +const OPCODE = { + SINGLE_STEPS: 0xc1, + SCHEDULED_STEPS: 0xca, + SCHEDULED_POINTS: 0xcb, + CONTINUOUS_STEPS: 0xcc, + AGGREGATE_STEPS: 0xcd, + LEDMATRIX: 0xa2, + SERVO: 0xe1, + MUSIC: 0xe8, +}; + +const METHOD = { + CONTINOUS: 0, + RELATIVE_SINGLE: 1, + ABSOLUTE_SINGLE: 2, + SCHED_STEPS: 3, + sCHED_POINT: 4, +}; + +const MODE = { + BASIC: 1, + MULTIROLE: 2, + CHECKCRC: 3, +}; + +const PROPERTY = { + PERI: 0x01, + MULTI: 0x02, + PORT: 0x80, + ADDRESS: 0x70, + PAUSE: 1, + RESUME: 2, + MUSIC_PLAY: 0, +}; + +class PingpongConnectLite { + constructor(connectCount, name) { + this.cubeCount = connectCount; + this.name = name; + this.url = 'http://www.roborisen.com'; + this.title = { + ko: 'G큐브', + en: 'G-cube', + }; + this.communicationType = 'manual'; + + this.isplaying = false; + this.dataReceiveQueue = []; + this.isProcessing = false; + this.groupID = 0; + this.isAllConnected = false; + this.OPCODE = OPCODE; + this.METHOD = METHOD; + this.MODE = MODE; + this.PROPERTY = PROPERTY; + + this.TILT_THRESHOLD = 20; + this.MOVE_THRESHOLD = 30; + // 엔트리Js에서 기기와 통신하는 함수 호출 Duration 간격 + this.duration = 50; + this.portData = { + baudRate: 115200, + duration: 50, + dataBits: 8, + parity: 'none', + stopBits: 1, + bufferSize: 65536, + constantServing: true, + }; + + this.customPrompt = { + title: '그룹 번호 입력', + description: '연결할 기기의 그룹 번호를 입력해 주세요.\n 별도로 지정하지 않았다면 00을 입력합니다.', + defaultValue: '00', + negativeButtonText: '취소', + positiveButtonText: '설정', + }; + + this.sensor_data=[]; + + for (let i = 0; i < this.cubeCount; i++) { + this.sensor_data.push({ + MOVE_X: 0, + MOVE_Y: 0, + MOVE_Z: 0, + TILT_X: 0, + TILT_Y: 0, + TILT_Z: 0, + BUTTON: 0, + PROXIMITY: 0, + AIN: 0, + }) + } + + this.monitorTemplate = { + width: 100, + height: 100, + imgPath: `hw_lite/PingpongG${this.cubeCount}Lite.png`, + listPorts: this.createPortView(this.cubeCount), + ports: {}, + mode: 'both', + }; + + this.tempo = 60; + this.readablePorts = []; + this.setZero(); + } + + //#region init + async initialHandshake() { + const self = this; + const originalDisconnect = Entry.hwLite.disconnect; + Entry.hwLite.disconnect = function (...params) { + console.log('PingpongConnect:disconnect 호출'); + + try { + if (Entry.hwLite && Entry.hwLite.serial) { + self.requestLocalData(self.makePackets('cubeReboot')); + } + } catch (e) { + console.warn('PingpongConnect:disconnect 전송 중 오류:', e); + } finally { + setTimeout(() => { + console.log('PingpongConnect:disconnect - 원래 disconnect 함수 호출'); + self.isAllConnected = false; + Entry.hwLite.disconnect = originalDisconnect; + return originalDisconnect.apply(this, params); + }, 1000); + } + + }; + // 그룹 번호 값 가져오기 + const payload = Entry.hwLite.getCustomPromptPayload(); + // 입력값 검증 (10진수 2자리) + if (!/^\d{2}$/.test(payload)) { + Entry.hwLite.serial.disconnect(); + window.alert('Wrong group id inputted:', payload); + return null; + } + + this.groupID = parseInt(payload, 16); + this.requestLocalData(this.makePackets('connect')); + + return true; + } + + makePackets(method) { + const isGroup = this.groupID == 0 ? 0x0a : 0x1a; + const packetMap = { + connect1: [0xff,0xff,0x00,0x07,0x00,0x00,0xce,0x00,0x0e,0x02,0x00,0x00,0x07,0x50], + connect2: [0xff,0xff,0xff,0xaa,0x20,0x00,0xad,0x00,0x0b,isGroup,0x00], + connect3: [0xff,0xff,0xff,0xaa,0x30,0x00,0xad,0x00,0x0b,isGroup,0x00], + connect4: [0xff,0xff,0xff,0xaa,0x40,0x00,0xad,0x00,0x0b,isGroup,0x00], + cubeReboot: [0xff,0xff,0xff,0xff,0x00,0x00,0xa8,0x00,0x0a,0x01], + getSensorData: [0xff,0xff,0xff,0xff,0x00,0xc8,0xb8,0x00,0x0b,0x10,0x01], + + connect: [0xdd,0xdd,this.groupID,0x00,0x00,0x00,0xda,0x00,0x0b,0x00,0x00], + checkDongle: [0xdd,0xdd,0xdd,0xdd,0x00,0x01,0xda,0x00,0x0b,0x00,0x0d], + clearDongle: [0xdd,0xdd,0xdd,0xdd,0x00,0x01,0xda,0x00,0x0b,0x00,0xcd], + + test: [0xff,0xff,0xff,0xaa,0x40,0x00,0xcd,0x00,0x59,0x02,0x01,0x00,0x00,0xff,0xff,0xff,0x00,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x00,0x37,0xff,0xff,0xff,0x01,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x01,0xef,0xff,0xff,0xff,0x02,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x01,0xef,0xff,0xff,0xff,0x03,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x01,0xef] + }; + + return packetMap[method] ? + (console.log('makePacket:', method),Buffer.from(packetMap[method])) : + (console.log(`존재하지 않는 method: ${method}\n가능한 목록:\n- ${Object.keys(packetMap).join('\n- ')}`), null); + } + + makePacket(opcode, taskid, cubeNo, opt) { + const property = Buffer.from(opt); + const header = Buffer.alloc(9, 0xff); + + if (cubeNo <= -1) { + header[3] = 0xff; + } else { + header[3] = cubeNo; + } + + header[4] = (taskid >> 8) & 0xff; + header[5] = taskid & 0xff; + header[6] = opcode; + + const size = header.length + property.length; + header[7] = (size >> 8) & 0xff; + header[8] = size & 0xff; + + return Buffer.concat([header, property]); + } + + + async postCallReturn(script, myfunc) { + if (myfunc == undefined) { + return script.callReturn(); + } + + if (script.is_start == undefined) { + script.is_start = true; + + const [packet, waitTime = this.delayTime] = myfunc(); + + if (packet && packet.length > 0) { + this.requestLocalData(packet) + this.isplaying = true; + } + + await new Promise(resolve => setTimeout(resolve, waitTime)); // waitTime 동안 대기 + + script.is_start = false; + + return script.callReturn(); + } else if (script.is_start == true) { + return script; + } else { + delete script.is_start; + + return script.callReturn(); + } + } + + // 연결시 기기 초기화 + async setZero() { + if (Entry.hwLite && Entry.hwLite.serial) { + Entry.hwLite.serial.update(); + } + + // 기기 정지 + if(this.isplaying){ + const arr = this.makeContStepPacket(-1, 0); + const packet = Buffer.from(arr); + this.requestLocalData(packet); + await new Promise(resolve => setTimeout(resolve, 50)); // 50ms 대기 + } + this.isplaying = false + } + + // 하드웨어에서 온 데이터 처리 + //#region Rx + handleLocalData(data) { + if (!data || data.length === 0) { + return; + } + + this.dataReceiveQueue.push(...data); + this.startQueueProcessing(); + } + + async startQueueProcessing() { + if (this.isProcessing) return; + this.isProcessing = true; + + try { + while (this.dataReceiveQueue.length >= 9) { + const dataPacketSize = (this.dataReceiveQueue[7] << 8) | this.dataReceiveQueue[8]; + if (dataPacketSize === 0) { + console.error('패킷 길이 0 → 20바이트 버림',Buffer.from(this.dataReceiveQueue.slice(0, 20)).toString('hex').match(/.{1,2}/g).join(' ')); + const removalLength = Math.min(20, this.dataReceiveQueue.length); + this.dataReceiveQueue.splice(0, removalLength); + continue; + } + if (this.dataReceiveQueue.length < dataPacketSize) break; + const dataPacket = this.dataReceiveQueue.splice(0, dataPacketSize); + await this.processData(dataPacket); + } + } catch (e) { + console.error('데이터 처리 오류:', e); + } + this.isProcessing = false; + } + + async processData(data) { + if (!data) return + const bufferData = Buffer.from(data); + const opcode = bufferData[6]; + console.log(`Rx[${opcode.toString(16).padStart(2, '0')}]: ${Buffer.from(bufferData).toString('hex').match(/.{1,2}/g).join(' ')} /${bufferData.length} = ${(bufferData[7] << 8) | bufferData[8]}`); + + //센서데이터 처리 + if (opcode === 0xb8 && bufferData.length == 20) { + const cubeid = this.cubeCount > 1 ? bufferData[3] : 0; + const sensor = this.sensor_data[cubeid]; + + sensor.MOVE_X = this.hexToSignedDecimal(bufferData[12]); + sensor.MOVE_Y = this.hexToSignedDecimal(bufferData[13]); + sensor.MOVE_Z = this.hexToSignedDecimal(bufferData[14]); + + const xx = this.hexToSignedDecimal(bufferData[15]); + const yy = this.hexToSignedDecimal(bufferData[16]); + const zz = this.hexToSignedDecimal(bufferData[17]); + + if(sensor.TILT_X != xx || sensor.TILT_Y != yy || sensor.TILT_Z != zz){ + Entry.engine.fireEvent('pp_when_tilted'); + } + + sensor.TILT_X = xx; + sensor.TILT_Y = yy; + sensor.TILT_Z = zz; + if(sensor.BUTTON != bufferData[11]){ + Entry.engine.fireEvent('pp_when_button_pressed'); + } + sensor.BUTTON = bufferData[11]; + sensor.PROXIMITY = bufferData[18]; + sensor.AIN = bufferData[19]; + } + + //동글 응답 + if (opcode === 0xda) { + switch (data[9]) { + case 0xc0: + console.log('C0'); + Entry.toast.alert(Lang.template.cube_connection_lost,Lang.template.cube_reconnect_notice,false); + this.requestLocalData(this.makePackets(`connect`)); + break; + case 0xc1: + Entry.toast.success(Lang.template.cube_connection_first,Lang.template.cube_first_connected,false); + setTimeout(() => { + this.requestLocalData(this.makePackets(`connect${this.cubeCount}`)); + }, 1000); + break; + } + } + + //큐브1개 연결된 경우 + if (opcode === 0xce) { + Entry.toast.success(Lang.template.cube_connection_success,Lang.template.cube_all_connected,false); + setTimeout(() => { + this.requestLocalData(this.makePackets('getSensorData')); + }, 3000); + } + + //큐브 여러개 연결된 경우, 마지막 큐브가 연결됐다는 응답이 왔을 때 센서데이터 요청 + if (opcode === 0xad) { + if (bufferData[9]==this.cubeCount-1) { + Entry.toast.success(Lang.template.cube_connection_success,Lang.template.cube_all_connected,false); + setTimeout(() => {this.requestLocalData(this.makePackets('getSensorData'));}, 3000); + this.isAllConnected = true; + } else if (this.isAllConnected) { + this.isAllConnected = false; + this.requestLocalData(this.makePackets('cubeReboot')); + } + } + } + + + // 하드웨어 기기에 전달할 데이터 + //#region Tx + requestLocalData(data) { + if (!data) return + const bufferData = Buffer.from(data); + const opcode = data[6]; + console.log(`Tx[${opcode.toString(16).padStart(2, '0')}]: ${bufferData.toString('hex').match(/.{1,2}/g).join(' ')} /${bufferData.length}`); + Entry.hwLite.serial.sendAsciiAsBuffer(bufferData); + return; + } + + test(name){ + console.log('테스트실행'); + Entry.toast.warning('1','2',false); + Entry.toast.success('1','2',false); + Entry.toast.alert(3,3,false); + this.requestLocalData(this.makePackets(name)); + } + + //#region functions + getMonitorPort() { + const result = {}; + + this.sensor_data.forEach((item, i) => { + Object.keys(item).forEach((key) => { + result[`${key}${i}`] = item[key]; + }); + }); + + return result; + } + + createPortView(count) { + const result = {}; + const inputMapping = [ + 'BUTTON', + 'MOVE_X', + 'MOVE_Y', + 'MOVE_Z', + 'TILT_X', + 'TILT_Y', + 'TILT_Z', + 'PROXIMITY', + 'AIN', + ]; + + inputMapping.forEach((k) => { + for (let i = 0; i < count; i++) { + result[`${k}${i}`] = { + name: `${i + 1}: ${k}`, + type: 'input', + pos: {x:0, y:0,}, + }; + } + }); + return result; + } + + + makeContStepPacket(cubeNo, speed) { + const packet = new Uint8Array(9 + 6); + + this._fillPacketIntoArray(packet, this.OPCODE.CONTINUOUS_STEPS, 0, cubeNo, 15); + + const sps = this._calcSpsFromSpeed(speed); + + packet[9] = this.MODE.MULTIROLE; + packet[10] = this.METHOD.CONTINOUS; + packet[11] = 0; //step_type; full=0, servo=4 + + if (sps == 0) { + packet[12] = this.PROPERTY.PAUSE; + packet[13] = 0; + packet[14] = 0; + } else { + packet[12] = this.PROPERTY.RESUME; + packet[13] = sps / 256; + packet[14] = sps % 256; + } + + return packet; + } + + _fillPacketIntoArray(data, opcode, taskid, cubeNo, size) { + data[0] = 0xff; + data[1] = 0xff; + data[2] = 0xff; + + if (cubeNo <= -1) { + data[3] = 0xff; + } else { + data[3] = cubeNo; + } + + data[4] = taskid / 256; + data[5] = taskid % 256; + + data[6] = opcode; + + data[7] = size / 256; + data[8] = size % 256; + } + + + _calcSpsFromSpeed(speed_) { + let speed = speed_; + if (speed > 100) { + speed = 100; + } + if (speed < -100) { + speed = -100; + } + + let sps = 0; + if (speed != 0) { + if (speed < 0) { + sps = 65536 + (speed * 9 - 100); + } else { + sps = speed * 9 + 100; + } + sps = Math.round(sps); + } + return sps; + } + + makeMusicNotePacket(cubeNo, note, duration) { + const packet = new Uint8Array(9 + 5); + this._fillPacketIntoArray(packet, this.OPCODE.MUSIC, 0xa1, cubeNo, 9 + 5); + + packet[9] = 0; + packet[10] = this.PROPERTY.MUSIC_PLAY; + packet[11] = note - 8; + packet[12] = duration; + packet[13] = 0; + + return packet; + } + + _clampBeats(beats) { + return Math.min(Math.max(beats, 0), 40); + } + + _clampTempo(tempo) { + return Math.min(Math.max(tempo, 20), 500); + } + + _beatsToDuration(beats) { + let duration = Math.round((60 / this.tempo) * beats * 100); + return duration; + } + + _getCubeNoFromBlock(script) { + let cubeNo = script.getNumberValue('cubeno') - 1; + if (cubeNo < 0) { + cubeNo = 0; + } + if (cubeNo >= this.cubeCount) { + cubeNo = this.cubeCount - 1; + } + return cubeNo; + } + + _getTiltValue(cubeNo=0, tiltDir) { + const pd = this.sensor_data; + let tiltValue = 0; + if (tiltDir == 'F_CIRCLE') { + tiltValue = pd[cubeNo].TILT_X * -1; + } else if (tiltDir == 'B_TRIANGLE') { + tiltValue = pd[cubeNo].TILT_X; + } else if (tiltDir == 'L_RECTANGLE') { + tiltValue = pd[cubeNo].TILT_Y; + } else if (tiltDir == 'R_STAR') { + tiltValue = pd[cubeNo].TILT_Y * -1; + } + console.log(); + return tiltValue; + } + + _isUpperDir(cubeNo, tiltDir) { + const pd = this.sensor_data; + const sensorValue = pd[cubeNo]; + const thresholdValue = 70; + + switch (tiltDir) { + case 'DF_RECTANGLE': + return -sensorValue.TILT_Y > thresholdValue; + case 'DB_STAR': + return sensorValue.TILT_Y > thresholdValue; + case 'DR_CIRCLE': + return sensorValue.TILT_X > thresholdValue; + case 'DL_TRIANGLE': + return -sensorValue.TILT_X > thresholdValue; + case 'DD_NONE': + return sensorValue.TILT_Z > thresholdValue; + case 'DU_HEART': + return -sensorValue.TILT_Z > thresholdValue; + default: + return false; + } + } + + makeSingleStepPacket(cubeNo, speed, degree) { + const packet = new Uint8Array(9 + 10); + + this._fillPacketIntoArray(packet, this.OPCODE.SINGLE_STEPS, 0 /*cubeCount*/, cubeNo, 19); + + const sps = this._calcSpsFromSpeed(speed); + let step = Math.round(Math.min(Math.max(degree, 0), 5000) * 5.5); + if (step > 32768) { + step = 32768; + } + + packet[9] = this.MODE.MULTIROLE; + packet[10] = this.METHOD.RELATIVE_SINGLE; + packet[11] = 0; //step_type; full=0, servo=4 + packet[12] = this.PROPERTY.RESUME; + + packet[13] = sps / 256; + packet[14] = sps % 256; + packet[15] = 0; + packet[16] = 0; + packet[17] = step / 256; + packet[18] = step % 256; + + const waitTime = Math.round(((1000 - Math.abs(speed) * 9) / 99) * step) + 400; + + return [packet, waitTime]; + } + + makeAggregatePacket(opcode, taskid, packets, opt = []) { + let size = 9 + opt.length; + let options = opt; + let usedCubeIds = []; + + packets.forEach((n) => { + size += n.length; + Array.prototype.push.apply(options, n); + usedCubeIds.push(n[3]); + }); + + // aggregate command must have cubeCount commands. add dummy packet + if (packets.length < this.cubeCount) { + for (let i = 0; i < this.cubeCount; i++) { + if (usedCubeIds.includes(i) == false) { + let dummyPacket = packets[0].slice(); + dummyPacket[3] = i; + dummyPacket[6] = 0; // set opcode to zero + Array.prototype.push.apply(options, dummyPacket); + } + } + } + + const cmd = this.makePacket(opcode, (packets.length << 12) | taskid, 0xaa, options); + return cmd; + } + + hexToSignedDecimal(hex) { + if (hex >= 0x80) {return hex - 256;} + return hex; + } + + //#region setLanguage + setLanguage(){ + return { + ko: { + template: { + cube_connection_first: "큐브 연결 준비됨", + cube_first_connected: "첫 번째 큐브가 연결되었습니다", + cube_connection_lost: "연결 끊어짐", + cube_reconnect_notice: "큐브의 연결이 끊어졌습니다.\n연결을 재시도합니다", + cube_connection_success: "연결 완료", + cube_all_connected: "모든 큐브가 연결되었습니다", + + pingpong_lite_g1_when_button_pressed: '%1 큐브 버튼을 눌렀을 때', + [`pingpong_lite_g${this.name}_when_button_pressed`]: '%1 %2 큐브 버튼을 눌렀을 때', + + pingpong_lite_g1_when_tilted: '%1 큐브를 %2 방향으로 기울였을 때', + [`pingpong_lite_g${this.name}_when_tilted`]: '%1 %2 큐브를 %3로 기울였을 때', + + pingpong_lite_g1_is_button_pressed: '큐브 버튼이 눌렸는가?', + [`pingpong_lite_g${this.name}_is_button_pressed`]: '%1 큐브 버튼이 눌렸는가?', + + pingpong_lite_g1_is_tilted: '큐브가 %1 방향으로 기울어졌는가?', + [`pingpong_lite_g${this.name}_is_tilted`]: '%1 큐브가 %2로 기울어졌는가?', + + pingpong_lite_g1_get_tilt_value: '%1 방향 큐브 기울기', + [`pingpong_lite_g${this.name}_get_tilt_value`]: '%1 큐브의 %2 방향 기울기', + + pingpong_lite_g1_get_sensor_value: '%1 센서값', + [`pingpong_lite_g${this.name}_get_sensor_value`]: '%1 큐브의 %2 센서값', + + pingpong_lite_g1_motor_rotate: '모터를 %1 방향으로 %2 도 회전하기 %3', + [`pingpong_lite_g${this.name}_motor_rotate`]: '%1 모터를 %2 방향으로 %3 도 회전하기 %4', + + pingpong_lite_g1_start_motor_rotate: '모터의 속도를 %1으로 계속 회전하기 %2', + [`pingpong_lite_g${this.name}_start_motor_rotate`]: '%1 모터의 속도를 %2으로 계속 회전하기 %3', + + pingpong_lite_g1_stop_motor_rotate: '모터 멈추기 %1', + [`pingpong_lite_g${this.name}_stop_motor_rotate`]: '%1 모터 멈추기 %2', + + pingpong_lite_g1_is_top_shape: '큐브 윗면에 %1 모양이 있는가?', + [`pingpong_lite_g${this.name}_is_top_shape`]: '%1 큐브의 윗면에 %2 모양이 있는가?', + + pingpong_lite_g1_rotate_servo_mortor: '서보모터를 %1도로 설정하기 %2', + [`pingpong_lite_g${this.name}_rotate_servo_mortor`]: '%1 번째 큐브의 서보모터 %2도로 설정하기 %3', + + pingpong_lite_g1_set_dot_pixel: '도트 X:%1 Y:%2 %3 %4', + [`pingpong_lite_g${this.name}_set_dot_pixel`]: '%1 번째 큐브의 도트 X:%2 Y:%3 %4 %5', + + pingpong_lite_g1_set_dot_string: '도트에 문자열 %1 %2초동안 출력 %3', + [`pingpong_lite_g${this.name}_set_dot_string`]: '%1 번째 큐브에 도트 문자열 %2 %3초동안 보여주기 %4', + + pingpong_lite_g1_set_dot_clear: '도트 화면 지우기 %1', + [`pingpong_lite_g${this.name}_set_dot_clear`]: '%1 번째 큐브의 화면 지우기 %2', + + pingpong_lite_g1_playNoteForBeats: '%1 음을 %2 박자로 연주하기 %3', + [`pingpong_lite_g${this.name}_playNoteForBeats`]: '%1 큐브의 %2 음을 %3 박자로 연주하기 %4', + + [`pingpong_lite_g${this.name}_restForBeats`]: '%1 박자 쉬기 %2', + [`pingpong_lite_g${this.name}_setTempo`]: '악보 빠르기를 %1 으로 정하기 %2', + [`pingpong_lite_g${this.name}_getTempo`]: '악보 빠르기', + + pingpong_lite_g2_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도 회전하기 %5', + pingpong_lite_g3_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도, 모터3: %5방향 %6도 회전하기 %7', + pingpong_lite_g4_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도, 모터3: %5방향 %6도, 모터4: %7 방향 %8 도 회전하기 %9', + + pingpong_lite_g2_start_multi_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2으로 계속 회전하기 %3', + pingpong_lite_g3_start_multi_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2, 모터3 속도를 %3으로 계속 회전하기 %4', + pingpong_lite_g4_start_multi_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2, 모터3 속도를 %3, 모터4 속도를 %4으로 계속 회전하기 %5', + + pingpong_lite_g2_playChordForBeats: '큐브1: %1, 큐브2: %2, %3박자로 연주하기 %4', + pingpong_lite_g3_playChordForBeats: '큐브1: %1, 큐브2: %2, 큐브3: %3, %4박자로 연주하기 %5', + pingpong_lite_g4_playChordForBeats: '큐브1: %1, 큐브2: %2, 큐브3: %3, 큐브4: %4, %5박자로 연주하기 %6', + + set_steering_direction: '조향 바퀴를 %1 방향으로 설정', + move_by_distance: '%1으로%2cm 이동하기', + }, + + Blocks: { + pingpong_right: '오른쪽', + pingpong_left: '왼쪽', + + pingpong_direction :[ + ['앞쪽','front'], + ['뒤쪽','back'] + ], + + pingpong_rotate_cw: '시계', + pingpong_rotate_ccw: '반시계', + + pingpong_sensor_proximity: '근접', + pingpong_sensor_ain: '아날로그', + + pingpong_dot_on: '켜기', + pingpong_dot_off: '끄기', + + + pingpong_opts_cube_tiltDir: [ + ['동그라미', 'F_CIRCLE'], + ['세모', 'B_TRIANGLE'], + ['네모', 'L_RECTANGLE'], + ['별', 'R_STAR'], + ], + + pingpong_opts_cube_dir6: [ + ['네모', 'DF_RECTANGLE'], + ['별', 'DB_STAR'], + ['세모', 'DL_TRIANGLE'], + ['동그라미', 'DR_CIRCLE'], + ['하트', 'DU_HEART'], + ['빈칸', 'DD_NONE'], + ], + + pingpong_opts_music_notes: [ + ['라 (A3)', 45], + ['라# (A3#)', 46], + ['시 (B3)', 47], + ['도 (C4)', 48], + ['도# (C4#)', 49], + ['레 (D4)', 50], + ['레# (D4#)', 51], + ['미 (E4)', 52], + ['파 (F4)', 53], + ['파# (F4#)', 54], + ['솔 (G4)', 55], + ['솔# (G4#)', 56], + ['라 (A4)', 57], + ['라# (A4#)', 58], + ['시 (B4)', 59], + ['도 (C5)', 60], + ['도# (C5#)', 61], + ['레 (D5)', 62], + ['레# (D5#)', 63], + ['미 (E5)', 64], + ['파 (F5)', 65], + ['파# (F5#)', 66], + ['솔 (G5)', 67], + ['솔# (G5#)', 68], + ['라 (A5)', 69], + ['라# (A5#)', 70], + ['시 (B5)', 71], + ['도 (C6)', 72], + ], + + pingpong_opts_mono :[ + ['왼쪽 45°' ,45], + ['왼쪽 40°' ,50], + ['왼쪽 35°' ,55], + ['왼쪽 30°' ,60], + ['왼쪽 25°' ,65], + ['왼쪽 20°' ,70], + ['왼쪽 15°' ,75], + ['왼쪽 10°' ,80], + ['왼쪽 5°' ,85], + ['정면' ,90], + ['오른쪽 5°' ,95], + ['오른쪽 10°' ,100], + ['오른쪽 15°' ,105], + ['오른쪽 20°' ,110], + ['오른쪽 25°' ,115], + ['오른쪽 30°' ,120], + ['오른쪽 35°' ,125], + ['오른쪽 40°' ,130], + ['오른쪽 45°' ,135], + ], + + pingpong_lite_g2_cube_id: [ + ['1번', 0], + ['2번', 1], + ], + pingpong_lite_g2_cube_all: [ + ['1번', 0], + ['2번', 1], + ['모든', -1], + ], + + pingpong_lite_g3_cube_id: [ + ['1번', 0], + ['2번', 1], + ['3번', 2], + ], + pingpong_lite_g3_cube_all: [ + ['1번', 0], + ['2번', 1], + ['3번', 2], + ['모든', -1], + ], + + pingpong_lite_g4_cube_id: [ + ['1번', 0], + ['2번', 1], + ['3번', 2], + ['4번', 3], + ], + pingpong_lite_g4_cube_all: [ + ['1번', 0], + ['2번', 1], + ['3번', 2], + ['4번', 3], + ['모든', -1], + ], + }, + }, + en: { + template: { + cube_connection_first: "Cube connection ready", + cube_first_connected: "The first cube has been connected", + cube_connection_lost: "Connection lost", + cube_reconnect_notice: "The cube connection has been lost.\nReconnecting...", + cube_connection_success: "Connection successful", + cube_all_connected: "All cubes are connected", + + pingpong_lite_g1_when_button_pressed: '%1 When the cube button is pressed', + [`pingpong_lite_g${this.name}_when_button_pressed`]: '%1 %2 When the cube button is pressed', + + pingpong_lite_g1_when_tilted: '%1 When the cube is tilted to %2 direction', + [`pingpong_lite_g${this.name}_when_tilted`]: '%1 %2 When the cube is tilted to %3', + + pingpong_lite_g1_is_button_pressed: 'Is the cube button pressed?', + [`pingpong_lite_g${this.name}_is_button_pressed`]: 'Is the %1 cube button pressed?', + + pingpong_lite_g1_is_tilted: 'Is the cube tilted to %1 direction?', + [`pingpong_lite_g${this.name}_is_tilted`]: 'Is the %1 cube tilted to %2?', + + pingpong_lite_g1_get_tilt_value: '%1 direction cube tilt', + [`pingpong_lite_g${this.name}_get_tilt_value`]: '%1 cube %2 direction tilt', + + pingpong_lite_g1_get_sensor_value: '%1 sensor value', + [`pingpong_lite_g${this.name}_get_sensor_value`]: '%1 cube %2 sensor value', + + pingpong_lite_g1_motor_rotate: 'Rotate motor %2 degrees to %1 direction %3', + [`pingpong_lite_g${this.name}_motor_rotate`]: 'Rotate %1 motor %3 degrees to %2 direction %4', + + pingpong_lite_g1_start_motor_rotate: 'Rotate motor continuously at speed %1 %2', + [`pingpong_lite_g${this.name}_start_motor_rotate`]: 'Rotate %1 motor continuously at speed %2 %3', + + pingpong_lite_g1_stop_motor_rotate: 'Stop motor %1', + [`pingpong_lite_g${this.name}_stop_motor_rotate`]: 'Stop %1 motor %2', + + pingpong_lite_g1_is_top_shape: 'Is there a %1 shape on the top of the cube?', + [`pingpong_lite_g${this.name}_is_top_shape`]: 'Is there a %2 shape on the top of %1 cube?', + + pingpong_lite_g1_rotate_servo_mortor: 'Set servo motor to %1 degrees %2', + [`pingpong_lite_g${this.name}_rotate_servo_mortor`]: 'Set servo motor of cube %1 to %2 degrees %3', + + pingpong_lite_g1_set_dot_pixel: 'Dot X:%1 Y:%2 %3 %4', + [`pingpong_lite_g${this.name}_set_dot_pixel`]: 'Cube %1 dot X:%2 Y:%3 %4 %5', + + pingpong_lite_g1_set_dot_string: 'Display string %1 on dot for %2 seconds %3', + [`pingpong_lite_g${this.name}_set_dot_string`]: 'Display string %2 on cube %1 dot for %3 seconds %4', + + pingpong_lite_g1_set_dot_clear: 'Clear dot display %1', + [`pingpong_lite_g${this.name}_set_dot_clear`]: 'Clear display of cube %1 %2', + + pingpong_lite_g1_playNoteForBeats: 'Play note %1 for %2 beats %3', + [`pingpong_lite_g${this.name}_playNoteForBeats`]: 'Play note %2 of cube %1 for %3 beats %4', + + [`pingpong_lite_g${this.name}_restForBeats`]: 'Rest for %1 beats %2', + [`pingpong_lite_g${this.name}_setTempo`]: 'Set tempo to %1 %2', + [`pingpong_lite_g${this.name}_getTempo`]: 'Tempo', + + pingpong_lite_g2_multi_motor_rotate: 'Motor1: %1 dir %2 deg, Motor2: %3 dir %4 deg rotate %5', + pingpong_lite_g3_multi_motor_rotate: 'Motor1: %1 dir %2 deg, Motor2: %3 dir %4 deg, Motor3: %5 dir %6 deg rotate %7', + pingpong_lite_g4_multi_motor_rotate: 'Motor1: %1 dir %2 deg, Motor2: %3 dir %4 deg, Motor3: %5 dir %6 deg, Motor4: %7 dir %8 deg rotate %9', + + pingpong_lite_g2_start_multi_motor_rotate: 'Motor1 speed %1, Motor2 speed %2 continuous rotate %3', + pingpong_lite_g3_start_multi_motor_rotate: 'Motor1 speed %1, Motor2 speed %2, Motor3 speed %3 continuous rotate %4', + pingpong_lite_g4_start_multi_motor_rotate: 'Motor1 speed %1, Motor2 speed %2, Motor3 speed %3, Motor4 speed %4 continuous rotate %5', + + pingpong_lite_g2_playChordForBeats: 'Cube 1: %1, Cube 2: %2, Play for %3 beats %4', + pingpong_lite_g3_playChordForBeats: 'Cube 1: %1, Cube 2: %2, Cube 3: %3, Play for %4 beats %5', + pingpong_lite_g4_playChordForBeats: 'Cube 1: %1, Cube 2: %2, Cube 3: %3, Cube 4: %4, Play for %5 beats %6', + + [`set_steering_direction${this.name}`]: 'Set the steering wheel to %1 direction', + [`move_by_distance${this.name}`]: 'Move %1 by %2 cm', + }, + + Blocks: { + pingpong_right: 'Right', + pingpong_left: 'Left', + + pingpong_direction :[ + ['Front','front'], + ['Back','back'] + ], + + pingpong_rotate_cw: 'Clockwise', + pingpong_rotate_ccw: 'Counterclockwise', + + pingpong_sensor_proximity: 'Proximity', + pingpong_sensor_ain: 'Analog', + + pingpong_dot_on: 'On', + pingpong_dot_off: 'Off', + + pingpong_opts_cube_tiltDir: [ + ['Circle', 'F_CIRCLE'], + ['Triangle', 'B_TRIANGLE'], + ['Square', 'L_RECTANGLE'], + ['Star', 'R_STAR'], + ], + + pingpong_opts_cube_dir6: [ + ['Square', 'DF_RECTANGLE'], + ['Star', 'DB_STAR'], + ['Triangle', 'DL_TRIANGLE'], + ['Circle', 'DR_CIRCLE'], + ['Heart', 'DU_HEART'], + ['None', 'DD_NONE'], + ], + + pingpong_opts_music_notes: [ + ['A3', 45], + ['A#3', 46], + ['B3', 47], + ['C4', 48], + ['C#4', 49], + ['D4', 50], + ['D#4', 51], + ['E4', 52], + ['F4', 53], + ['F#4', 54], + ['G4', 55], + ['G#4', 56], + ['A4', 57], + ['A#4', 58], + ['B4', 59], + ['C5', 60], + ['C#5', 61], + ['D5', 62], + ['D#5', 63], + ['E5', 64], + ['F5', 65], + ['F#5', 66], + ['G5', 67], + ['G#5', 68], + ['A5', 69], + ['A#5', 70], + ['B5', 71], + ['C6', 72], + ], + + pingpong_opts_mono: [ + ['Left 45°', 45], + ['Left 40°', 50], + ['Left 35°', 55], + ['Left 30°', 60], + ['Left 25°', 65], + ['Left 20°', 70], + ['Left 15°', 75], + ['Left 10°', 80], + ['Left 5°', 85], + ['Center', 90], + ['Right 5°', 95], + ['Right 10°', 100], + ['Right 15°', 105], + ['Right 20°', 110], + ['Right 25°', 115], + ['Right 30°', 120], + ['Right 35°', 125], + ['Right 40°', 130], + ['Right 45°', 135], + ], + + pingpong_lite_g2_cube_id: [ + ['1', 0], + ['2', 1], + ], + pingpong_lite_g2_cube_all: [ + ['1', 0], + ['2', 1], + ['All', -1], + ], + + pingpong_lite_g3_cube_id: [ + ['1', 0], + ['2', 1], + ['3', 2], + ], + pingpong_lite_g3_cube_all: [ + ['1', 0], + ['2', 1], + ['3', 2], + ['All', -1], + ], + + pingpong_lite_g4_cube_id: [ + ['1', 0], + ['2', 1], + ['3', 2], + ['4', 3], + ], + pingpong_lite_g4_cube_all: [ + ['1', 0], + ['2', 1], + ['3', 2], + ['4', 3], + ['All', -1], + ], + }, + } + } + } +} + +module.exports = PingpongConnectLite; \ No newline at end of file From 5de4e30f37fd95d0878eee10c859549bab790cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=A7=84=ED=98=B8?= <140678689+jinbaekho@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:44:35 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20G1~G4,=EC=8B=A4=EA=B3=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/hw_lite/PingpongG1Lite.png | Bin 0 -> 45192 bytes images/hw_lite/PingpongG2Lite.png | Bin 0 -> 45374 bytes images/hw_lite/PingpongG3Lite.png | Bin 0 -> 45485 bytes images/hw_lite/PingpongG4Lite.png | Bin 0 -> 44998 bytes images/hw_lite/PingpongPracticalartsLite.png | Bin 0 -> 100752 bytes .../hardwareLite/block_pingpongG1_lite.js | 719 +++++++++ .../hardwareLite/block_pingpongG1_liteb.js | 1373 +++++++++++++++++ .../hardwareLite/block_pingpongG2_lite.js | 916 +++++++++++ .../hardwareLite/block_pingpongG3_lite.js | 930 +++++++++++ .../hardwareLite/block_pingpongG4_lite.js | 949 ++++++++++++ .../block_pingpongPracticalarts_lite.js | 718 +++++++++ .../metadata_pingpongG1_lite.json | 9 + .../metadata_pingpongG2_lite.json | 9 + .../metadata_pingpongG3_lite.json | 9 + .../metadata_pingpongG4_lite.json | 9 + .../metadata_pingpong_practicalarts_lite.json | 10 + 16 files changed, 5651 insertions(+) create mode 100644 images/hw_lite/PingpongG1Lite.png create mode 100644 images/hw_lite/PingpongG2Lite.png create mode 100644 images/hw_lite/PingpongG3Lite.png create mode 100644 images/hw_lite/PingpongG4Lite.png create mode 100644 images/hw_lite/PingpongPracticalartsLite.png create mode 100644 src/playground/blocks/hardwareLite/block_pingpongG1_lite.js create mode 100644 src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js create mode 100644 src/playground/blocks/hardwareLite/block_pingpongG2_lite.js create mode 100644 src/playground/blocks/hardwareLite/block_pingpongG3_lite.js create mode 100644 src/playground/blocks/hardwareLite/block_pingpongG4_lite.js create mode 100644 src/playground/blocks/hardwareLite/block_pingpongPracticalarts_lite.js create mode 100644 src/playground/blocks/hardwareLite/metadata_pingpongG1_lite.json create mode 100644 src/playground/blocks/hardwareLite/metadata_pingpongG2_lite.json create mode 100644 src/playground/blocks/hardwareLite/metadata_pingpongG3_lite.json create mode 100644 src/playground/blocks/hardwareLite/metadata_pingpongG4_lite.json create mode 100644 src/playground/blocks/hardwareLite/metadata_pingpong_practicalarts_lite.json diff --git a/images/hw_lite/PingpongG1Lite.png b/images/hw_lite/PingpongG1Lite.png new file mode 100644 index 0000000000000000000000000000000000000000..00a89031969499bad12c406a5c987b941dbb5902 GIT binary patch literal 45192 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 literal 0 HcmV?d00001 diff --git a/images/hw_lite/PingpongG2Lite.png b/images/hw_lite/PingpongG2Lite.png new file mode 100644 index 0000000000000000000000000000000000000000..624b204b650e635dd9cdf1ed0b0a474a9e13b8da GIT binary patch literal 45374 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

bIKb#TKkEFLUkx9}-*OIjJ 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%0Aj7#*p;Ac( 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 literal 0 HcmV?d00001 diff --git a/images/hw_lite/PingpongG3Lite.png b/images/hw_lite/PingpongG3Lite.png new file mode 100644 index 0000000000000000000000000000000000000000..54e080cb7cd60e2ff1257f2639b0d83f24804f16 GIT binary patch literal 45485 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 Z)_k`+JUogE};j(wTDB^77@TRii(4GE>mzjTw6w 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 E046&{Pyhe` literal 0 HcmV?d00001 diff --git a/images/hw_lite/PingpongG4Lite.png b/images/hw_lite/PingpongG4Lite.png new file mode 100644 index 0000000000000000000000000000000000000000..6d7ac9d1ca37fbddbd7c94183dd1ceffca3dca3e GIT binary patch literal 44998 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≪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 KR(SJC$o~N==7&!J literal 0 HcmV?d00001 diff --git a/images/hw_lite/PingpongPracticalartsLite.png b/images/hw_lite/PingpongPracticalartsLite.png new file mode 100644 index 0000000000000000000000000000000000000000..a58e598a29cf4db892d5fd5347b1b0413185ae1f GIT binary patch literal 100752 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

Kb4K5|-7Vg}v>vWDHFqXT- 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 Mipq)9{5A;uA0d{s`~Uy| literal 0 HcmV?d00001 diff --git a/src/playground/blocks/hardwareLite/block_pingpongG1_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG1_lite.js new file mode 100644 index 0000000000..2743a5aa21 --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongG1_lite.js @@ -0,0 +1,719 @@ +'use strict'; + +import _range from 'lodash/range'; + +(function() { + const PingpongConnectLite = require('./block_pingpongConnect_lite'); + self.Lite = new (class PingpongG1Lite extends PingpongConnectLite { + constructor() { + super(1,'PingpongG1Lite'); + this.id = '350101'; + this.imageName = 'PingpongG1Lite.png'; + + this.blockMenuBlocks = [ + 'pingpong_lite_g1_when_button_pressed', + 'pingpong_lite_g1_when_tilted', + 'pingpong_lite_g1_is_button_pressed', + 'pingpong_lite_g1_is_tilted', + 'pingpong_lite_g1_get_tilt_value', + 'pingpong_lite_g1_get_sensor_value', + 'pingpong_lite_g1_is_top_shape', + 'pingpong_lite_g1_motor_rotate', + 'pingpong_lite_g1_start_motor_rotate', + 'pingpong_lite_g1_stop_motor_rotate', + 'pingpong_lite_g1_rotate_servo_mortor', + 'pingpong_lite_g1_set_dot_pixel', + 'pingpong_lite_g1_set_dot_string', + 'pingpong_lite_g1_set_dot_clear', + 'pingpong_lite_g1_playNoteForBeats', + 'pingpong_lite_g1_restForBeats', + 'pingpong_lite_g1_setTempo', + 'pingpong_lite_g1_getTempo', + ]; + } + + //#region getBlocks + getBlocks() { + const self = this; + return { + pingpong_lite_g1_when_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + ], + events: {}, + def: { + params: [], + type: 'pingpong_lite_g1_when_button_pressed', + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + event: 'pp_when_button_pressed', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const pd = self.sensor_data; + + return (pd[cubeId]?.BUTTON == 1) + ? script.callReturn() + : this.die(); + }, + }, + pingpong_lite_g1_when_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g1_when_tilted', + }, + paramsKeyMap: { + TILT_DIR: 1, + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + event: 'pp_when_tilted', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + if (tiltValue >= self.TILT_THRESHOLD) { + return script.callReturn(); + } + + return this.die(); + }, + }, + pingpong_lite_g1_is_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + def: { + type: 'pingpong_lite_g1_is_button_pressed', + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + return self.sensor_data[cubeId].BUTTON == 1; + }, + }, + pingpong_lite_g1_is_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_is_tilted' }, + paramsKeyMap: { + TILT_DIR: 0, + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + + return tiltValue >= self.TILT_THRESHOLD; + }, + }, + pingpong_lite_g1_get_tilt_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g1_get_tilt_value', + }, + paramsKeyMap: { DIR: 0 }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('DIR', script); + + return self._getTiltValue(cubeId, tiltDir); + }, + }, + pingpong_lite_g1_get_sensor_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], + [Lang.Blocks.pingpong_sensor_ain, 'AIN'], + ], + value: 'PROXIMITY', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_get_sensor_value' }, + paramsKeyMap: { SENSOR: 0 }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const sensorType = script.getStringField('SENSOR', script); + const pd = self.sensor_data; + + return pd[cubeId][sensorType]; + }, + }, + pingpong_lite_g1_is_top_shape: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_dir6, + value: 'DF_RECTANGLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g1_is_top_shape' }, + paramsKeyMap: { + TILT_DIR: 0, + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR', script); + return self._isUpperDir(cubeId, tiltDir); + }, + }, + pingpong_lite_g1_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [ + null, + { + type: 'number', + params: ['10'], + }, + ], + type: 'pingpong_lite_g1_motor_rotate', + }, + paramsKeyMap: { DIR: 0, DEGREE: 1 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const dir = script.getStringField('DIR'); + const degree = script.getNumberValue('DEGREE'); + const speed = 80 * (dir === 'LEFT' ? -1 : 1); + + const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); + const packet = Buffer.from(arr); + return [packet, waitTime]; + }); + }, + }, + + pingpong_lite_g1_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [ + { + type: 'number', + params: ['100'], + }, + ], + type: 'pingpong_lite_g1_start_motor_rotate', + }, + paramsKeyMap: { SPEED: 0 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const speed = script.getNumberValue('SPEED'); + + const arr = self.makeContStepPacket(cubeId, speed); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g1_stop_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g1_stop_motor_rotate', + }, + paramsKeyMap: {}, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + + const arr = self.makeContStepPacket(cubeId, 0); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + + pingpong_lite_g1_rotate_servo_mortor: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [{ type: 'angle' }], type: 'pingpong_lite_g1_rotate_servo_mortor' }, + paramsKeyMap: { DEGREE: 0 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const opt = [1, 0, angle, 0]; + const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); + return [packet]; + }); + }, + }, + + pingpong_lite_g1_set_dot_pixel: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_dot_on, 1], + [Lang.Blocks.pingpong_dot_off, 0], + ], + value: 1, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [null, null, null], + type: 'pingpong_lite_g1_set_dot_pixel', + }, + paramsKeyMap: { X: 0, Y: 1, onoff: 2 }, + class: 'PingpongG1_peripheral_LED', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let dotX = script.getNumberValue('X', script); + let dotY = script.getNumberValue('Y', script); + const onoff = script.getNumberField('onoff', script); + + dotX = Math.min(Math.max(dotX, 0), 7); + dotY = Math.min(Math.max(dotY, 0), 7); + + const opt = [0x70, dotY, dotX, onoff]; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe1, + cubeId, + opt + ); + return [packet]; + }); + }, + }, + pingpong_lite_g1_set_dot_string: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', value: 'Hello!' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [null, null], type: 'pingpong_lite_g1_set_dot_string' }, + paramsKeyMap: { STR: 0, DURATION: 1 }, + class: 'PingpongG1_peripheral_LED', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const str = script.getStringValue('STR', script); + const duration = script.getNumberValue('DURATION', script); + + let period = Math.round((duration * 100) / (str.length * 8)); + period = Math.min(Math.max(period, 1), 200); + + const opt = Buffer.concat([ + Buffer.from([0x70, period, 0]), + Buffer.from(str.substring(0, 20)), + ]); + + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g1_set_dot_clear: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_set_dot_clear' }, + paramsKeyMap: {}, + class: 'PingpongG1_peripheral_LED', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const opt = [0x70, 1, 0, ' ']; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + return [packet, 400]; + }); + }, + }, + pingpong_lite_g1_playNoteForBeats: { + //'%1 번 음을 %2 박자로 연주하기 %3', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g1_playNoteForBeats' }, + paramsKeyMap: { NOTE: 0, BEATS: 1 }, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const NOTE = script.getNumberField('NOTE', script); + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); + const packet = Buffer.from(arr); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g1_restForBeats: { + //'%1 박자 쉬기 %2', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_restForBeats' }, + paramsKeyMap: { BEATS: 0 }, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + return [null, waitTime]; + }); + }, + }, + pingpong_lite_g1_setTempo: { + //'악보 빠르기를 %1 으로 정하기 %2', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_setTempo' }, + paramsKeyMap: { TEMPO: 0 }, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + let tempo = script.getNumberValue('TEMPO', script); + self.tempo = self._clampTempo(tempo); + return script.callReturn(); + }, + }, + pingpong_lite_g1_getTempo: { + //'악보 빠르기', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [], + def: { params: [], type: 'pingpong_lite_g1_getTempo' }, + paramsKeyMap: {}, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.tempo; + }, + }, + + set_steering_direction: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_mono, + value: 90, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, + ], + events: {}, + def: { params: [], type: 'set_steering_direction' }, + paramsKeyMap: { DEGREE: 0,}, + class: 'PingpongG1_motor', + isNotFor: ['PingpongPracticalArtsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const packet = self.makePacket(0xe1, 0x00, [2, 0, angle, 1]); + return [packet, 400]; + }); + }, + }, + + move_by_distance: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_direction, + value: Lang.Blocks.pingpong_direction[0][1], + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, + ], + events: {}, + def: { params: [], type: 'move_by_distance' }, + paramsKeyMap: { DIR: 0, CM: 1 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongPracticalArtsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const direction = script.getStringField('DIR'); + let degree = script.getNumberValue('CM'); + degree = degree*9; // convert cm to degree + + let speed = 800; + if (direction == 'back') { + speed *= -1; + } + + degree = Math.min(Math.max(degree, 0), 5000); + + let step = Math.round(degree * 5.5); + if (step > 32768) { + step = 32768; + } + + const opt = [2, 1, 0, 2, 0, 0, 0, 0, 0, 0]; + const packet = self.makePacket(0xc1, 0x0004, opt); // SETP_MOTOR + + packet.writeInt16BE(speed, 13); + packet.writeUInt16BE(step, 17); + + const waitTime = Math.round(((1100 - Math.abs(speed)) / 99) * step) + 400; + return [packet, waitTime]; + }); + }, + }, + }; + } + + })(); +})(); + +module.exports = self.Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js b/src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js new file mode 100644 index 0000000000..b79a180588 --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js @@ -0,0 +1,1373 @@ +// 'use strict'; + +// import _range from 'lodash/range'; + +// const OPCODE = { +// SINGLE_STEPS: 0xc1, +// SCHEDULED_STEPS: 0xca, +// SCHEDULED_POINTS: 0xcb, +// CONTINUOUS_STEPS: 0xcc, +// AGGREGATE_STEPS: 0xcd, +// LEDMATRIX: 0xa2, +// SERVO: 0xe1, +// MUSIC: 0xe8, +// }; + +// const METHOD = { +// CONTINOUS: 0, +// RELATIVE_SINGLE: 1, +// ABSOLUTE_SINGLE: 2, +// SCHED_STEPS: 3, +// sCHED_POINT: 4, +// }; + +// const MODE = { +// BASIC: 1, +// MULTIROLE: 2, +// CHECKCRC: 3, +// }; + +// const PROPERTY = { +// PERI: 0x01, +// MULTI: 0x02, +// PORT: 0x80, +// ADDRESS: 0x70, +// PAUSE: 1, +// RESUME: 2, +// MUSIC_PLAY: 0, +// }; + +// const DEFAULT_TEMPO = 60; +// (function() { +// Entry.PingpongG1Lite = new (class PingpongG1Lite { +// constructor() { +// this.id = '060101'; +// this.webapiType = 'ble'; +// this.bluetoothInfo = { +// filters: [ +// { +// namePrefix: 'PINGPONG', +// }, +// ], +// }; +// this.services=null +// this.name = 'PingpongG1Lite'; +// this.url = 'http://www.roborisen.com'; +// this.imageName = 'PingpongG1Lite.png'; +// this.title = { +// ko: 'G큐브(Web)', +// en: 'G-cube(Web)', +// }; + +// this.customPrompt = { +// title: '그룹 번호 입력', +// description: '연결할 기기의 그룹 번호를 입력해 주세요.\n 별도로 지정하지 않았다면 00을 입력합니다.', +// defaultValue: '00', +// negativeButtonText: '취소', +// positiveButtonText: '설정', +// }; + +// this.communicationType = 'manual'; +// this.cubeCount = 1; +// this.checkMultiroleAction = false; +// this.isplaying = false; +// this.sendBuffer = []; +// this.TILT_THRESHOLD = 20; +// this.MOVE_THRESHOLD = 30; +// this.send_cmd_id = 0; +// // 엔트리Js에서 기기와 통신하는 함수 호출 Duration 간격 +// this.duration = 50; +// this.portData = { +// baudRate: 115200, +// duration: 50, +// dataBits: 8, +// parity: 'none', +// stopBits: 1, +// bufferSize: 65536, +// constantServing: true, +// }; + +// this.sensor_data = { +// MOVE_X: 0, +// MOVE_Y: 0, +// MOVE_Z: 0, +// TILT_X: 0, +// TILT_Y: 0, +// TILT_Z: 0, +// BUTTON: 0, +// PROXIMITY: 0, +// AIN: 0, +// }; + +// this.blockMenuBlocks = [ +// 'pingpong_lite_g1_when_button_pressed', +// 'pingpong_lite_g1_is_button_pressed', +// 'pingpong_lite_g1_when_tilted', +// 'pingpong_lite_g1_is_tilted', +// 'pingpong_lite_g1_get_tilt_value', +// 'pingpong_lite_g1_is_top_shape', +// 'pingpong_lite_g1_get_sensor_value', +// 'pingpong_lite_g1_motor_rotate', +// 'pingpong_lite_g1_start_motor_rotate', +// 'pingpong_lite_g1_stop_motor_rotate', +// 'pingpong_lite_g1_rotate_servo_mortor', +// 'pingpong_lite_g1_set_dot_pixel', +// 'pingpong_lite_g1_set_dot_string', +// 'pingpong_lite_g1_set_dot_clear', +// 'pingpong_lite_g1_playNoteForBeats', +// 'pingpong_lite_g1_restForBeats', +// 'pingpong_lite_g1_setTempo', +// 'pingpong_lite_g1_getTempo', +// ]; +// this.tempo = 60; +// this.readablePorts = []; +// this.setZero(); +// } + +// // 그룹번호 넣기 +// requestInitialData(sp, payload) { +// console.log('g1함수이름:requestInitialData'); +// const grpid = payload.match(/[0-9]{1,2}$/g); +// if (grpid == null) { +// console.warn('Wrong group id inputted', payload); +// return null; +// } +// const grpno = parseInt(grpid[0], 16); + +// if (this.checkMultiroleAction == false) { +// this.checkMultiroleAction = true; +// return this.makePackets('connect', grpno); +// } else { +// return null; +// } +// } + +// async initialHandshake() { +// console.log('g1함수이름:initialHandshake'); +// const 원본disconnect = Entry.hwLite.disconnect; + +// Entry.hwLite.disconnect = async function(...인수) { +// console.log('disconnect 호출됨'); + +// await 캐릭터리스틱쓰기.writeValue(new Uint8Array([0xff,0xff,0xff,0xff,0x00,0x00,0xa8,0x00,0x0a,0x01])); +// return await 원본disconnect.apply(this, 인수); +// }; + + +// const 디바이스 = Entry.hwLite.webConnector.device +// console.log('디바이스',디바이스); +// const 서비스 = await 디바이스.gatt.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e'); +// console.log('서비스',서비스); +// const 캐릭터리스틱쓰기 = await 서비스.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e'); +// const 캐릭터리스틱읽기 = await 서비스.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e'); +// console.log('캐릭터리스틱쓰기',캐릭터리스틱쓰기); +// console.log('캐릭터리스틱읽기',캐릭터리스틱읽기); +// await 캐릭터리스틱읽기.startNotifications(); +// 캐릭터리스틱읽기.addEventListener('characteristicvaluechanged', (이벤트) => { +// const 대상 = 이벤트.target; +// const 값 = 대상.value; + +// if (!값 || 값.byteLength == 0) {return;} + +// const 바이트배열 = new Uint8Array(값.buffer); +// console.log('구독 데이터', 바이트배열); +// }); +// await 캐릭터리스틱쓰기.writeValue(new Uint8Array([0xff,0xff,0x00,0x07,0x00,0x00,0xce,0x00,0x0e,0x02,0x00,0x00,0x07,0x50])); +// setTimeout(async () => { +// await 캐릭터리스틱쓰기.writeValue(new Uint8Array([0xff,0xff,0x00,0xff,0x00,0xc8,0xb8,0x00,0x0b,0x0a,0x01])); +// }, 5000); + + +// } +// // async initialHandshake() { +// // Entry.hwLite.serial.sendAsciiAsBuffer(this.requestInitialData(0,'0')); + +// // return true; +// // } + +// makePackets(method, grpid = 0) { +// console.log('g1함수이름:makePackets'); +// let result = null; +// if (method === 'connect') { +// result = Buffer.from([ +// 0xdd, +// 0xdd, +// grpid, +// 0x00, +// 0x00, +// 0x00, +// 0xda, +// 0x00, +// 0x0b, +// 0x00, +// 0x00, +// ]); +// //result[2] = this.groupId; +// } else if (method === 'getSensorData') { +// result = Buffer.from([ +// 0xff, +// 0xff, +// 0xff, +// 0xff, // position +// 0x00, +// 0xc8, // continuous sampling +// 0xb8, +// 0x00, +// 0x0b, +// 10, // interval //YIM's 30->10 +// 0x01, +// ]); +// } else if (method === 'setColorLed') { +// result = Buffer.from('ffff00070000ce000e0200000750', 'hex'); +// } +// return result; +// } + +// makePacket(opcode, taskid, opt) { +// console.log('g1함수이름:makePacket'); + +// const header = Buffer.from([0xff, 0xff, 0xff, 0xff, 0, 0, opcode, 0, 0]); +// const property = Buffer.from(opt); + +// //header.writeUInt16BE(0xFFFF, 0); +// //header.writeUInt16BE(0xFFFF, 2); // cubdid + +// header.writeUInt16BE(taskid, 4); +// header.writeUInt16BE(header.length + property.length, 7); + +// return Buffer.concat([header, property]); +// } + + +// async postCallReturn(script, myfunc) { +// console.log('g1함수이름:postCallReturn'); +// if (myfunc == undefined) { +// return script.callReturn(); +// } + +// if (script.is_start == undefined) { +// script.is_start = true; + +// const [packet, waitTime = this.delayTime] = myfunc(); + +// if (packet && packet.length > 0) { +// Entry.hwLite.serial.sendAsciiAsBuffer(packet) +// this.isplaying = true; +// } + +// await new Promise(resolve => setTimeout(resolve, waitTime)); // waitTime 동안 대기 + +// script.is_start = false; + +// return script.callReturn(); +// } else if (script.is_start == true) { +// return script; +// } else { +// delete script.is_start; + +// return script.callReturn(); +// } +// } + +// // 연결시 기기 초기화 +// async setZero() { +// console.log('g1함수이름:setZero'); +// console.log('pingpong g1 lite loaded'); +// Entry.hwLite.testCustomPromptPayload() +// } +// // async setZero() { +// // if (Entry.hwLite && Entry.hwLite.serial) { +// // Entry.hwLite.serial.update(); +// // Entry.hwLite.serial.sendAsciiAsBuffer(this.makePackets('setColorLed', 0)); +// // } + +// // // 기기 정지 +// // if(this.isplaying){ +// // for(let j = 0; j < 5; j++){ +// // const packet = Entry.PingpongG1Lite.makeContStepPacket(0, 0); +// // Entry.hwLite.serial.sendAsciiAsBuffer(packet); + +// // await new Promise(resolve => setTimeout(resolve, 50)); // 50ms 대기 +// // } +// // } +// // this.isplaying = false +// // } + +// // 하드웨어에서 온 데이터 처리 +// handleLocalData(data) { +// console.log('g1함수이름:handleLocalData'); +// console.log('그룹',Entry.hwLite.getCustomPromptPayload()); + +// if (data.length >= 9) { +// const dataView = new DataView(data.buffer); +// const packetSize = dataView.getUint16(7, false); +// const opcode = data[6]; + +// if (data.length === packetSize && opcode === 0xb8) { +// const sensor = Entry.PingpongG1Lite.sensor_data; + +// sensor.MOVE_X = dataView.getInt8(12); +// sensor.MOVE_Y = dataView.getInt8(13); +// sensor.MOVE_Z = dataView.getInt8(14); + +// const xx = Math.max(Math.min(dataView.getInt8(15), 90), -90); +// let yy = Math.max(Math.min(dataView.getInt8(16), 90), -90); +// yy *= -1; +// const zz = Math.max(Math.min(dataView.getInt8(17), 90), -90); + +// if(sensor.TILT_X != xx || sensor.TILT_Y != yy || sensor.TILT_Z != zz){ +// Entry.engine.fireEvent('pp_when_tilted'); +// } + +// sensor.TILT_X = xx; +// sensor.TILT_Y = yy; +// sensor.TILT_Z = zz; + +// if(sensor.BUTTON != dataView.getUint8(11)){ +// Entry.engine.fireEvent('pp_when_button_pressed'); +// } + +// console.log(zz); + +// sensor.BUTTON = dataView.getUint8(11); + +// sensor.PROXIMITY = dataView.getUint8(18); + +// // 기존 FW 70 버전 = data length 19 bytes (ANALOG IN 미지원) +// if (packetSize > 19) { +// sensor.AIN = dataView.getUint8(19) * 4; +// } else { +// sensor.AIN = 0; +// } +// } +// } +// } + + +// // 하드웨어 기기에 전달할 데이터 +// requestLocalData() { +// console.log('g1함수이름:requestLocalData'); +// this.sendBuffer = []; +// this.sendBuffer.push(this.makePackets('getSensorData', 1)) + +// return this.sendBuffer[0]; +// } + +// _clampBeats(beats) { +// console.log('g1함수이름:_clampBeats'); +// return Math.min(Math.max(beats, 0), 40); +// } + +// _clampTempo(tempo) { +// console.log('g1함수이름:_clampTempo'); +// return Math.min(Math.max(tempo, 20), 500); +// } + +// _beatsToDuration(beats) { +// console.log('g1함수이름:_beatsToDuration'); +// let duration = Math.round((60 / this.tempo) * beats * 100); +// return duration; +// } + +// _fillPacketIntoArray(data, opcode, taskid, cubeNo, size) { +// console.log('g1함수이름:_fillPacketIntoArray'); +// data[0] = 0xff; +// data[1] = 0xff; +// data[2] = 0xff; + +// if (cubeNo <= -1) { +// data[3] = 0xff; +// } else { +// data[3] = cubeNo; +// } + +// data[4] = taskid / 256; +// data[5] = taskid % 256; + +// data[6] = opcode; + +// data[7] = size / 256; +// data[8] = size % 256; +// } + +// makeMusicNotePacket(cubeNo, note, duration) { +// console.log('g1함수이름:makeMusicNotePacket'); +// const packet = new Uint8Array(9 + 5); +// this._fillPacketIntoArray(packet, OPCODE.MUSIC, 0xa1, cubeNo, 9 + 5); + +// packet[9] = 0; +// packet[10] = PROPERTY.MUSIC_PLAY; +// packet[11] = note - 8; +// packet[12] = duration; +// packet[13] = 0; + +// return packet; +// } + +// makeContStepPacket(cubeNo, speed) { +// console.log('g1함수이름:makeContStepPacket'); +// const packet = new Uint8Array(9 + 6); + +// this._fillPacketIntoArray(packet, OPCODE.CONTINUOUS_STEPS, 0, cubeNo, 15); + +// const sps = this._calcSpsFromSpeed(speed); + +// packet[9] = MODE.MULTIROLE; +// packet[10] = METHOD.CONTINOUS; +// packet[11] = 0; //step_type; full=0, servo=4 + +// if (sps == 0) { +// packet[12] = PROPERTY.PAUSE; +// packet[13] = 0; +// packet[14] = 0; +// } else { +// packet[12] = PROPERTY.RESUME; +// packet[13] = sps / 256; +// packet[14] = sps % 256; +// } + +// return packet; +// } + +// _calcSpsFromSpeed(speed_) { +// console.log('g1함수이름:_calcSpsFromSpeed'); +// let speed = speed_; +// if (speed > 100) { +// speed = 100; +// } +// if (speed < -100) { +// speed = -100; +// } + +// let sps = 0; +// if (speed != 0) { +// if (speed < 0) { +// sps = 65536 + (speed * 9 - 100); +// } else { +// sps = speed * 9 + 100; +// } +// sps = Math.round(sps); +// } +// return sps; +// } + +// monitorTemplate = { +// imgPath: 'hw_lite/PingpongG1Lite.png', +// width: 400, +// height: 400, +// listPorts: { +// BUTTON: { +// name: 'button', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// MOVE_X: { +// name: 'move_x', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// MOVE_Y: { +// name: 'move_y', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// MOVE_Z: { +// name: 'move_z', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// TILT_X: { +// name: 'tilt_x', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// TILT_Y: { +// name: 'tilt_y', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// TILT_Z: { +// name: 'tilt_z', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// PROXIMITY: { +// name: 'proximity', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// AIN: { +// name: 'ain', +// type: 'input', +// pos: { x: 0, y: 0 }, +// }, +// }, +// ports: {}, +// mode: 'both', +// }; + +// setLanguage() { +// console.log('g1함수이름:setLanguage'); +// return { +// ko: { +// template: { +// pingpong_lite_g1_when_button_pressed: '%1 큐브 버튼을 눌렀을 때', +// pingpong_lite_g1_when_tilted: '%1 큐브를 %2 방향으로 기울였을 때', +// pingpong_lite_g1_is_button_pressed: '큐브 버튼이 눌렸는가?', +// pingpong_lite_g1_is_tilted: '큐브가 %1 방향으로 기울어졌는가?', +// pingpong_lite_g1_get_tilt_value: '%1 방향 큐브 기울기', +// pingpong_lite_g1_get_sensor_value: '%1 센서값', +// pingpong_lite_g1_motor_rotate: '모터를 %1 방향으로 %2 도 회전하기 %3', +// pingpong_lite_g1_start_motor_rotate: '모터의 속도를 %1으로 계속 회전하기 %2', +// pingpong_lite_g1_stop_motor_rotate: '모터 멈추기 %1', +// pingpong_lite_g1_rotate_servo_mortor: '서보모터를 %1도로 설정하기 %2', +// pingpong_lite_g1_is_top_shape: '큐브 윗면에 %1 모양이 있는가?', +// pingpong_lite_g1_set_dot_pixel: '도트 X:%1 Y:%2 %3 %4', +// pingpong_lite_g1_set_dot_string: '도트에 문자열 %1 %2초동안 출력 %3', +// pingpong_lite_g1_set_dot_clear: '도트 화면 지우기 %1', +// pingpong_lite_g1_playNoteForBeats: '%1 음을 %2 박자로 연주하기 %3', +// pingpong_lite_g1_restForBeats: '%1 박자 쉬기 %2', +// pingpong_lite_g1_setTempo: '악보 빠르기를 %1 으로 정하기 %2', +// pingpong_lite_g1_getTempo: '악보 빠르기', +// }, +// Blocks: { +// pingpong_right: '오른쪽', +// pingpong_left: '왼쪽', + +// pingpong_rotate_cw: '시계', +// pingpong_rotate_ccw: '반시계', + +// pingpong_sensor_proximity: '근접', +// pingpong_sensor_ain: '아날로그', +// pingpong_dot_on: '켜기', +// pingpong_dot_off: '끄기', + +// pingpong_opts_cube_tiltDir: [ +// ['동그라미', 'F_CIRCLE'], +// ['세모', 'B_TRIANGLE'], +// ['네모', 'L_RECTANGLE'], +// ['별', 'R_STAR'], +// ], + +// pingpong_opts_cube_dir6: [ +// ['네모', 'DF_RECTANGLE'], +// ['별', 'DB_STAR'], +// ['세모', 'DL_TRIANGLE'], +// ['동그라미', 'DR_CIRCLE'], +// ['하트', 'DU_HEART'], +// ['빈칸', 'DD_NONE'], +// ], + +// pingpong_opts_music_notes: [ +// ['라 (A3)', 45], +// ['라# (A3#)', 46], +// ['시 (B3)', 47], +// ['도 (C4)', 48], +// ['도# (C4#)', 49], +// ['레 (D4)', 50], +// ['레# (D4#)', 51], +// ['미 (E4)', 52], +// ['파 (F4)', 53], +// ['파# (F4#)', 54], +// ['솔 (G4)', 55], +// ['솔# (G4#)', 56], +// ['라 (A4)', 57], +// ['라# (A4#)', 58], +// ['시 (B4)', 59], +// ['도 (C5)', 60], +// ['도# (C5#)', 61], +// ['레 (D5)', 62], +// ['레# (D5#)', 63], +// ['미 (E5)', 64], +// ['파 (F5)', 65], +// ['파# (F5#)', 66], +// ['솔 (G5)', 67], +// ['솔# (G5#)', 68], +// ['라 (A5)', 69], +// ['라# (A5#)', 70], +// ['시 (B5)', 71], +// ['도 (C6)', 72], +// ], +// }, +// }, +// en: { +// template: { +// pingpong_lite_g1_when_button_pressed: '%1 Button pressed', +// pingpong_lite_g1_when_tilted: '%1 Tilted to %2', +// pingpong_lite_g1_is_button_pressed: 'button pressed?', +// pingpong_lite_g1_is_tilted: 'cube tilted to %1', +// pingpong_lite_g1_get_tilt_value: 'tilt angle to %1', +// pingpong_lite_g1_get_sensor_value: 'read sensor %1', +// pingpong_lite_g1_motor_rotate: 'rotate %2 degrees %1 %3', +// pingpong_lite_g1_start_motor_rotate: 'set motor speed to %1 %2', +// pingpong_lite_g1_stop_motor_rotate: 'stop motor rotate %1', +// pingpong_lite_g1_rotate_servo_mortor: 'set servo mortor to %1 degrees %2', +// pingpong_lite_g1_is_top_shape: '%1 shown in top view?', +// pingpong_lite_g1_set_dot_pixel: 'set %3 DOT X:%1 Y:%2 %4', +// pingpong_lite_g1_set_dot_string: 'print string %1 during %2 seconds to DOT %3', +// pingpong_lite_g1_set_dot_clear: 'clear DOT %1', +// pingpong_lite_g1_playNoteForBeats: 'play note %1 for %2 beats %3', +// pingpong_lite_g1_restForBeats: 'rest for %1 beats %2', +// pingpong_lite_g1_setTempo: 'set tempo to %1 %2', +// pingpong_lite_g1_getTempo: 'tempo', +// }, +// Blocks: { +// pingpong_right: 'right', +// pingpong_left: 'left', + +// pingpong_rotate_cw: 'clockwise', +// pingpong_rotate_ccw: 'counter clockwise', + +// pingpong_sensor_proximity: 'proximity', +// pingpong_sensor_ain: 'ain', +// pingpong_dot_on: 'ON', +// pingpong_dot_off: 'OFF', + +// pingpong_opts_cube_tiltDir: [ +// ['circle', 'F_CIRCLE'], +// ['triangle', 'B_TRIANGLE'], +// ['rectangle', 'L_RECTANGLE'], +// ['star', 'R_STAR'], +// ], + +// pingpong_opts_cube_dir6: [ +// ['rectangle', 'DF_RECTANGLE'], +// ['star', 'DB_STAR'], +// ['triangle', 'DL_TRIANGLE'], +// ['circle', 'DR_CIRCLE'], +// ['heart', 'DU_HEART'], +// ['none', 'DD_NONE'], +// ], +// pingpong_opts_music_notes: [ +// ['La (A3)', 45], +// ['La# (A3#)', 46], +// ['Ti (B3)', 47], +// ['Do (C4)', 48], +// ['Do# (C4#)', 49], +// ['Re (D4)', 50], +// ['Re# (D4#)', 51], +// ['Mi (E4)', 52], +// ['Fa (F4)', 53], +// ['Fa# (F4#)', 54], +// ['Sol (G4)', 55], +// ['Sol#(G4#)', 56], +// ['La (A4)', 57], +// ['La# (A4#)', 58], +// ['Ti (B4)', 59], +// ['Do (C5)', 60], +// ['Do# (C5#)', 61], +// ['Re (D5)', 62], +// ['Re# (D5#)', 63], +// ['Mi (E5)', 64], +// ['Fa (F5)', 65], +// ['Fa# (F5#)', 66], +// ['Sol (G5)', 67], +// ['Sol#(G5#)', 68], +// ['La (A5)', 69], +// ['La# (A5#)', 70], +// ['Ti (B5)', 71], +// ['Do (C6)', 72], +// ], +// }, +// }, +// }; +// } + +// getBlocks() { +// console.log('g1함수이름:getBlocks'); +// return { +// pingpong_lite_g1_when_button_pressed: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_event', +// statements: [], +// params: [ +// { +// type: 'Indicator', +// img: 'block_icon/start_icon_hardware.svg', +// size: 14, +// position: { x: 0, y: -2 }, +// }, +// ], +// events: {}, +// def: { +// params: [], +// type: 'pingpong_lite_g1_when_button_pressed', +// }, +// class: 'PingpongG1', +// isNotFor: ['PingpongG1Lite'], +// event: 'pp_when_button_pressed', +// func(sprite, script) { +// const pd = Entry.PingpongG1Lite.sensor_data; + +// if (pd.BUTTON == 1) { +// return script.callReturn(); +// } + +// return this.die(); +// }, +// }, +// pingpong_lite_g1_when_tilted: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_event', +// statements: [], +// params: [ +// { +// type: 'Indicator', +// img: 'block_icon/start_icon_hardware.svg', +// size: 14, +// position: { x: 0, y: -2 }, +// }, +// { +// type: 'Dropdown', +// options: Lang.Blocks.pingpong_opts_cube_tiltDir, +// value: 'F_CIRCLE', +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// ], +// events: {}, +// def: { +// params: [null], +// type: 'pingpong_lite_g1_when_tilted', +// }, +// paramsKeyMap: { +// TILT_DIR: 1, +// }, +// class: 'PingpongG1', +// isNotFor: ['PingpongG1Lite'], +// event: 'pp_when_tilted', +// func(sprite, script) { +// const tiltDir = script.getStringField('TILT_DIR'); +// const pd = Entry.PingpongG1Lite.sensor_data; + +// let tiltValue = 0; +// switch (tiltDir) { +// case 'F_CIRCLE': +// tiltValue = pd.TILT_X * -1; +// break; +// case 'B_TRIANGLE': +// tiltValue = pd.TILT_X; +// break; +// case 'L_RECTANGLE': +// tiltValue = pd.TILT_Y * -1; +// break; +// case 'R_STAR': +// tiltValue = pd.TILT_Y; +// break; +// default: +// break; +// } + +// if (tiltValue >= Entry.PingpongG1Lite.TILT_THRESHOLD) { +// return script.callReturn(); +// } else { +// return this.die(); +// } +// }, +// }, +// pingpong_lite_g1_is_button_pressed: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_boolean_field', +// def: { +// type: 'pingpong_lite_g1_is_button_pressed', +// }, +// class: 'PingpongG1', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// const pd = Entry.PingpongG1Lite.sensor_data; +// return pd.BUTTON == 1; +// }, +// }, +// pingpong_lite_g1_is_tilted: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_boolean_field', +// params: [ +// { +// type: 'Dropdown', +// options: Lang.Blocks.pingpong_opts_cube_tiltDir, +// value: 'F_CIRCLE', +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// ], +// def: { params: [], type: 'pingpong_lite_g1_is_tilted' }, +// paramsKeyMap: { +// TILT_DIR: 0, +// }, +// class: 'PingpongG1', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// const tiltDir = script.getStringField('TILT_DIR', script); +// const pd = Entry.PingpongG1Lite.sensor_data; +// let tiltValue = 0; +// switch (tiltDir) { +// case 'F_CIRCLE': +// tiltValue = pd.TILT_X * -1; +// break; +// case 'B_TRIANGLE': +// tiltValue = pd.TILT_X; +// break; +// case 'L_RECTANGLE': +// tiltValue = pd.TILT_Y * -1; +// break; +// case 'R_STAR': +// tiltValue = pd.TILT_Y; +// break; +// default: +// break; +// } +// return tiltValue >= Entry.PingpongG1Lite.TILT_THRESHOLD; +// }, +// }, +// pingpong_lite_g1_get_tilt_value: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_string_field', +// params: [ +// { +// type: 'Dropdown', +// options: Lang.Blocks.pingpong_opts_cube_tiltDir, +// value: 'F_CIRCLE', +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// ], +// events: { +// viewAdd: [ +// function() { +// //console.log('... viewAdd called!'); +// }, +// ], +// viewDestroy: [ +// function() { +// //console.log('... viewDestroy called!'); +// }, +// ], +// dataAdd: [ +// function(block) { +// //console.log(' ...... dataAdd called'); +// }, +// ], +// dataDestroy: [ +// function(block) { +// //console.log(' ...... dataDestroy called'); +// }, +// ], +// }, +// def: { +// params: [null], +// type: 'pingpong_lite_g1_get_tilt_value', +// }, +// paramsKeyMap: { DIR: 0 }, +// class: 'PingpongG1', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// const dir = script.getStringField('DIR', script); +// const pd = Entry.PingpongG1Lite.sensor_data; +// let value = 0; +// switch (dir) { +// case 'F_CIRCLE': +// value = pd.TILT_X * -1; +// break; +// case 'B_TRIANGLE': +// value = pd.TILT_X; +// break; +// case 'L_RECTANGLE': +// value = pd.TILT_Y * -1; +// break; +// case 'R_STAR': +// value = pd.TILT_Y; +// break; +// default: +// break; +// } +// return value; +// }, +// }, +// pingpong_lite_g1_get_sensor_value: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_string_field', +// params: [ +// { +// type: 'Dropdown', +// options: [ +// [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], +// [Lang.Blocks.pingpong_sensor_ain, 'AIN'], +// ], +// value: 'PROXIMITY', +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// ], +// def: { params: [], type: 'pingpong_lite_g1_get_sensor_value' }, +// paramsKeyMap: { SENSOR: 0 }, +// class: 'PingpongG1', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// const sensorType = script.getStringField('SENSOR', script); +// const pd = Entry.PingpongG1Lite.sensor_data; +// let value = 0; +// switch (sensorType) { +// case 'PROXIMITY': +// value = pd.PROXIMITY; +// break; +// case 'AIN': +// value = pd.AIN; +// break; +// default: +// break; +// } +// return value; +// }, +// }, +// pingpong_lite_g1_is_top_shape: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_boolean_field', +// statements: [], +// params: [ +// { +// type: 'Dropdown', +// options: Lang.Blocks.pingpong_opts_cube_dir6, +// value: 'DF_RECTANGLE', +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// ], +// events: {}, +// def: { params: [], type: 'pingpong_lite_g1_is_top_shape' }, +// paramsKeyMap: { +// TILT_DIR: 0, +// }, +// class: 'PingpongG1', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// const tiltDir = script.getStringField('TILT_DIR', script); +// const pd = Entry.PingpongG1Lite.sensor_data; + +// if (tiltDir == 'DF_RECTANGLE' && pd.TILT_Y > 70) return true; +// if (tiltDir == 'DB_STAR' && pd.TILT_Y < -70) return true; +// if (tiltDir == 'DR_CIRCLE' && pd.TILT_X > 70) return true; +// if (tiltDir == 'DL_TRIANGLE' && pd.TILT_X < -70) return true; +// if (tiltDir == 'DD_NONE' && pd.TILT_Z > 70) return true; +// if (tiltDir == 'DU_HEART' && pd.TILT_Z < -70) return true; +// return false; +// }, +// }, +// pingpong_lite_g1_motor_rotate: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { +// type: 'Dropdown', +// options: [ +// [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], +// [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], +// ], +// value: 'RIGHT', +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// { +// type: 'Block', +// accept: 'string', +// defaultType: 'number', +// }, +// { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, +// ], +// def: { +// params: [ +// null, +// { +// type: 'number', +// params: ['10'], +// }, +// ], +// type: 'pingpong_lite_g1_motor_rotate', +// }, +// paramsKeyMap: { DIR: 0, DEGREE: 1 }, +// class: 'PingpongG1_motor', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// const dir = script.getStringField('DIR'); +// let degree = script.getNumberValue('DEGREE'); + +// let speed = 800; +// if (dir == 'LEFT') { +// speed *= -1; +// } + +// degree = Math.min(Math.max(degree, 0), 5000); + +// let step = Math.round(degree * 5.5); +// if (step > 32768) { +// step = 32768; +// } + +// const opt = [2, 1, 0, 2, 0, 0, 0, 0, 0, 0]; +// const packet = Entry.PingpongG1Lite.makePacket(0xc1, 0x0004, opt); // SETP_MOTOR + +// packet.writeInt16BE(speed, 13); +// packet.writeUInt16BE(step, 17); + +// const waitTime = Math.round(((1100 - Math.abs(speed)) / 99) * step) + 400; + +// return [packet, waitTime]; +// }); +// }, +// }, + +// pingpong_lite_g1_start_motor_rotate: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { +// type: 'Block', +// accept: 'string', +// defaultType: 'number', +// }, +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { +// params: [ +// { +// type: 'number', +// params: ['100'], +// }, +// ], +// type: 'pingpong_lite_g1_start_motor_rotate', +// }, +// paramsKeyMap: { SPEED: 0 }, +// class: 'PingpongG1_motor', +// isNotFor: ['PingpongG1Lite'], +// func (sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// let speed = script.getNumberValue('SPEED'); +// if (speed > 100) { +// speed = 100; +// } +// if (speed < -100) { +// speed = -100; +// } + +// let sps = 0; +// if (speed != 0) { +// if (speed < 0) { +// sps = 65536 + (speed * 9 - 100); +// } else { +// sps = speed * 9 + 100; +// } +// sps = Math.round(sps); +// } + +// const opt = [2, 0, 0, 2, sps / 256, sps % 256]; +// const packet = Entry.PingpongG1Lite.makePacket(0xcc, 0x0004, opt); + +// const waitTime = Math.round(((1100 - Math.abs(sps)) / 99) * 10) + 400; +// return [packet, waitTime]; +// }); +// }, +// }, +// pingpong_lite_g1_stop_motor_rotate: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { +// params: [], +// type: 'pingpong_lite_g1_stop_motor_rotate', +// }, +// paramsKeyMap: {}, +// class: 'PingpongG1_motor', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// const opt = [2, 0, 0, 1, 0, 0]; +// const packet = Entry.PingpongG1Lite.makePacket(0xcc, 0x0004, opt); +// return [packet]; +// }); +// }, +// }, + +// pingpong_lite_g1_rotate_servo_mortor: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { +// type: 'Block', +// accept: 'string', +// defaultType: 'number', +// }, +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { params: [{ type: 'angle' }], type: 'pingpong_lite_g1_rotate_servo_mortor' }, +// paramsKeyMap: { DEGREE: 0 }, +// class: 'PingpongG1_motor', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// let angle = script.getNumberValue('DEGREE', script); + +// angle = Math.min(Math.max(angle, 0), 180); + +// const packet = Entry.PingpongG1Lite.makePacket(0xe1, 0x00, [2, 0, angle, 1]); +// return [packet, 400]; +// }); +// }, +// }, + +// pingpong_lite_g1_set_dot_pixel: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, +// { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, +// { +// type: 'Dropdown', +// options: [ +// [Lang.Blocks.pingpong_dot_on, 1], +// [Lang.Blocks.pingpong_dot_off, 0], +// ], +// value: 1, +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { +// params: [null, null, null], +// type: 'pingpong_lite_g1_set_dot_pixel', +// }, +// paramsKeyMap: { X: 0, Y: 1, onoff: 2 }, +// class: 'PingpongG1_peripheral_LED', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// let dotX = script.getNumberValue('X', script); +// let dotY = script.getNumberValue('Y', script); +// const onoff = script.getNumberField('onoff', script); + +// dotX = Math.min(Math.max(dotX, 0), 7); +// dotY = Math.min(Math.max(dotY, 0), 7); + +// const packet = Entry.PingpongG1Lite.makePacket(0xa2, 0xe1, [ +// 0x70, +// dotY, +// dotX, +// onoff, +// ]); // turn on +// return [packet,500]; +// }); +// }, +// }, +// pingpong_lite_g1_set_dot_string: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { type: 'Block', accept: 'string', value: 'Hello!' }, +// { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { params: [null, null], type: 'pingpong_lite_g1_set_dot_string' }, +// paramsKeyMap: { STR: 0, DURATION: 1 }, +// class: 'PingpongG1_peripheral_LED', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// const str = script.getStringValue('STR', script); +// const duration = script.getNumberValue('DURATION', script); + +// let period = Math.round((duration * 100) / (str.length * 8)); +// period = Math.min(Math.max(period, 1), 200); + +// const opt = Buffer.concat([ +// Buffer.from([0x70, period, 0]), +// Buffer.from(str.substring(0, 20)), +// ]); + +// const packet = Entry.PingpongG1Lite.makePacket(0xa2, 0xe3, opt); +// const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms +// return [packet, waitTime]; +// }); +// }, +// }, +// pingpong_lite_g1_set_dot_clear: { +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { params: [], type: 'pingpong_lite_g1_set_dot_clear' }, +// paramsKeyMap: {}, +// class: 'PingpongG1_peripheral_LED', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// const packet = Entry.PingpongG1Lite.makePacket(0xa2, 0xe3, [0x70, 1, 0, ' ']); +// return [packet, 400]; +// }); +// }, +// }, +// pingpong_lite_g1_playNoteForBeats: { +// //'%1 번 음을 %2 박자로 연주하기 %3', +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// statements: [], +// params: [ +// { +// type: 'Dropdown', +// options: Lang.Blocks.pingpong_opts_music_notes, +// value: 48, +// fontSize: 11, +// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, +// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, +// }, +// { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// events: {}, +// def: { params: [], type: 'pingpong_lite_g1_playNoteForBeats' }, +// paramsKeyMap: { NOTE: 0, BEATS: 1 }, +// class: 'PingpongG1_Music', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// const NOTE = script.getNumberField('NOTE', script); +// const BEATS = script.getNumberValue('BEATS', script); + +// const cBeats = Entry.PingpongG1Lite._clampBeats(BEATS); +// const durationSec = Entry.PingpongG1Lite._beatsToDuration(cBeats); + +// const waitTime = durationSec * 10 + 60; + +// const arr = Entry.PingpongG1Lite.makeMusicNotePacket(0, NOTE, durationSec); +// const packet = Buffer.from(arr); + +// return [packet, waitTime]; +// }); +// }, +// }, +// pingpong_lite_g1_restForBeats: { +// //'%1 박자 쉬기 %2', +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { params: [], type: 'pingpong_lite_g1_restForBeats' }, +// paramsKeyMap: { BEATS: 0 }, +// class: 'PingpongG1_Music', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.postCallReturn(script, () => { +// const BEATS = script.getNumberValue('BEATS', script); + +// const cBeats = Entry.PingpongG1Lite._clampBeats(BEATS); +// const durationSec = Entry.PingpongG1Lite._beatsToDuration(cBeats); + +// const waitTime = durationSec * 10 + 60; + +// return [null, waitTime]; +// }); +// }, +// }, +// pingpong_lite_g1_setTempo: { +// //'악보 빠르기를 %1 으로 정하기 %2', +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic', +// params: [ +// { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, +// { +// type: 'Indicator', +// img: 'block_icon/hardware_icon.svg', +// size: 12, +// }, +// ], +// def: { params: [], type: 'pingpong_lite_g1_setTempo' }, +// paramsKeyMap: { TEMPO: 0 }, +// class: 'PingpongG1_Music', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// let tempo = script.getNumberValue('TEMPO', script); +// Entry.PingpongG1Lite.tempo = Entry.PingpongG1Lite._clampTempo(tempo); +// return script.callReturn(); +// }, +// }, +// pingpong_lite_g1_getTempo: { +// //'악보 빠르기', +// color: EntryStatic.colorSet.block.default.HARDWARE, +// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, +// skeleton: 'basic_string_field', +// params: [], +// def: { params: [], type: 'pingpong_lite_g1_getTempo' }, +// paramsKeyMap: {}, +// class: 'PingpongG1_Music', +// isNotFor: ['PingpongG1Lite'], +// func(sprite, script) { +// return Entry.PingpongG1Lite.tempo; +// }, +// }, +// }; +// } + +// })(); +// })(); + +// module.exports = Entry.PingpongG1Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG2_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG2_lite.js new file mode 100644 index 0000000000..64dcdcd2f8 --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongG2_lite.js @@ -0,0 +1,916 @@ +'use strict'; + +import _range from 'lodash/range'; + +(function() { + const PingpongConnectLite = require('./block_pingpongConnect_lite'); + Entry.PingpongG2Lite = new (class PingpongG2Lite extends PingpongConnectLite { + constructor() { + super(2, 'PingpongG2Lite'); + this.id = '350201'; + this.imageName = 'PingpongG2Lite.png'; + + this.blockMenuBlocks = [ + 'pingpong_lite_g2_when_button_pressed', + 'pingpong_lite_g2_when_tilted', + 'pingpong_lite_g2_is_button_pressed', + 'pingpong_lite_g2_is_tilted', + 'pingpong_lite_g2_get_tilt_value', + 'pingpong_lite_g2_is_top_shape', + 'pingpong_lite_g2_get_sensor_value', + 'pingpong_lite_g2_multi_motor_rotate', + 'pingpong_lite_g2_motor_rotate', + 'pingpong_lite_g2_start_multi_motor_rotate', + 'pingpong_lite_g2_start_motor_rotate', + 'pingpong_lite_g2_stop_motor_rotate', + 'pingpong_lite_g2_rotate_servo_mortor', + 'pingpong_lite_g2_set_dot_pixel', + 'pingpong_lite_g2_set_dot_string', + 'pingpong_lite_g2_set_dot_clear', + 'pingpong_lite_g2_playNoteForBeats', + 'pingpong_lite_g2_playChordForBeats', + 'pingpong_lite_g2_restForBeats', + 'pingpong_lite_g2_setTempo', + 'pingpong_lite_g2_getTempo', + ]; + } + + getBlocks() { + const self = this; + return { + pingpong_lite_g2_when_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g2_when_button_pressed', + }, + paramsKeyMap: { + CUBEID: 1, + }, + class: 'PingpongG2', + isNotFor: ['PingpongG2Lite'], + event: 'pp_when_button_pressed', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const pd = self.sensor_data; + + return (pd[cubeId]?.BUTTON == 1) + ? script.callReturn() + : this.die(); + }, + }, + pingpong_lite_g2_when_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null, null], + type: 'pingpong_lite_g2_when_tilted', + }, + paramsKeyMap: { + CUBEID: 1, + TILT_DIR: 2, + }, + class: 'PingpongG2', + isNotFor: ['PingpongG2Lite'], + event: 'pp_when_tilted', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + if (tiltValue >= self.TILT_THRESHOLD) { + return script.callReturn(); + } + + return this.die(); + }, + }, + pingpong_lite_g2_is_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { + params: [null], + type: 'pingpong_lite_g2_is_button_pressed', + }, + paramsKeyMap: { + CUBEID: 0, + }, + class: 'PingpongG2', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + return self.sensor_data[cubeId].BUTTON == 1; + }, + }, + pingpong_lite_g2_is_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g2_is_tilted' }, + paramsKeyMap: { + CUBEID: 0, + TILT_DIR: 1, + }, + class: 'PingpongG2', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + + return tiltValue >= self.TILT_THRESHOLD; + }, + }, + pingpong_lite_g2_get_tilt_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g2_get_tilt_value', + }, + paramsKeyMap: { CUBEID: 0, DIR: 1 }, + class: 'PingpongG2', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('DIR', script); + + return self._getTiltValue(cubeId, tiltDir); + }, + }, + pingpong_lite_g2_get_sensor_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], + [Lang.Blocks.pingpong_sensor_ain, 'AIN'], + ], + value: 'PROXIMITY', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: 'pingpong_lite_g2_get_sensor_value' }, + paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, + class: 'PingpongG2', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const sensorType = script.getStringField('SENSOR', script); + const pd = self.sensor_data; + + return pd[cubeId][sensorType]; + }, + }, + pingpong_lite_g2_is_top_shape: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_dir6, + value: 'DF_RECTANGLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g2_is_top_shape' }, + paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, + class: 'PingpongG2', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR', script); + return self._isUpperDir(cubeId, tiltDir); + }, + }, + pingpong_lite_g2_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [null], + type: 'pingpong_lite_g2_multi_motor_rotate', + }, + paramsKeyMap: { DIR_1: 0, DEGREE_1: 1, DIR_2: 2, DEGREE_2: 3 }, + class: 'PingpongG2_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g2_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + events: {}, + def: { + params: [null, null, null], + type: 'pingpong_lite_g2_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, + class: 'PingpongG2_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const dir = script.getStringField('DIR'); + const degree = script.getNumberValue('DEGREE'); + const speed = 80 * (dir === 'LEFT' ? -1 : 1); + + const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); + const packet = Buffer.from(arr); + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g2_start_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g2_start_multi_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1 }, + class: 'PingpongG2_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g2_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 100, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [null, null], + type: 'pingpong_lite_g2_start_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0, SPEED: 1 }, + class: 'PingpongG2_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const speed = script.getNumberValue('SPEED'); + + const arr = self.makeContStepPacket(cubeId, speed); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g2_stop_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g2_stop_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0 }, + class: 'PingpongG2_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + + const arr = self.makeContStepPacket(cubeId, 0); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g2_rotate_servo_mortor: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, { type: 'angle' }], type: 'pingpong_lite_g2_rotate_servo_mortor' }, + paramsKeyMap: { cubeno: 0, DEGREE: 1 }, + class: 'PingpongG2_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const opt = [2, 0, angle, 1]; + const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); + return [packet]; + }); + }, + }, + pingpong_lite_g2_set_dot_pixel: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_dot_on, 1], + [Lang.Blocks.pingpong_dot_off, 0], + ], + value: 1, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { + params: [null, null, null], + type: 'pingpong_lite_g2_set_dot_pixel', + }, + paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, + class: 'PingpongG2_peripheral_LED', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let dotX = script.getNumberValue('X', script); + let dotY = script.getNumberValue('Y', script); + const onoff = script.getNumberField('onoff', script); + + dotX = Math.min(Math.max(dotX, 0), 7); + dotY = Math.min(Math.max(dotY, 0), 7); + + const opt = [0x70, dotY, dotX, onoff]; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe1, + cubeId, + opt + ); + return [packet]; + }); + }, + }, + pingpong_lite_g2_set_dot_string: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', value: 'Hello!' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, null], type: 'pingpong_lite_g2_set_dot_string' }, + paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, + class: 'PingpongG2_peripheral_LED', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const str = script.getStringValue('STR', script); + const duration = script.getNumberValue('DURATION', script); + + let period = Math.round((duration * 100) / (str.length * 8)); + period = Math.min(Math.max(period, 1), 200); + + const opt = Buffer.concat([ + Buffer.from([0x70, period, 0]), + Buffer.from(str.substring(0, 20)), + ]); + + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g2_set_dot_clear: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g2_set_dot_clear' }, + paramsKeyMap: { cubeno: 0 }, + class: 'PingpongG2_peripheral_LED', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const opt = [0x70, 1, 0, ' ']; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + return [packet, 400]; + }); + }, + }, + pingpong_lite_g2_playNoteForBeats: { + //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g2_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g2_playNoteForBeats' }, + paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, + class: 'PingpongG2_Music', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const NOTE = script.getNumberField('NOTE', script); + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); + const packet = Buffer.from(arr); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g2_playChordForBeats: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g2_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + BEATS: 2, + }, + class: 'PingpongG2_Music', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + console.log('NOTE_1',script.getNumberField('NOTE_1', script)); + console.log('dfljksadflkdsja',script.getNumberField('dfljksadflkdsja', script)); + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g2_restForBeats: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g2_restForBeats' }, + paramsKeyMap: { BEATS: 0 }, + class: 'PingpongG2_Music', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + return [null, waitTime]; + }); + }, + }, + pingpong_lite_g2_setTempo: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g2_setTempo' }, + paramsKeyMap: { TEMPO: 0 }, + class: 'PingpongG2_Music', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + let tempo = script.getNumberValue('TEMPO', script); + self.tempo = self._clampTempo(tempo); + return script.callReturn(); + }, + }, + pingpong_lite_g2_getTempo: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [], + def: { params: [], type: 'pingpong_lite_g2_getTempo' }, + paramsKeyMap: {}, + class: 'PingpongG2_Music', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.tempo; + }, + }, + }; + } + })(); +})(); + +module.exports = Entry.PingpongG2Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG3_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG3_lite.js new file mode 100644 index 0000000000..ba7b2f9d2e --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongG3_lite.js @@ -0,0 +1,930 @@ +'use strict'; + +import _range from 'lodash/range'; + +(function() { + const PingpongConnectLite = require('./block_pingpongConnect_lite'); + Entry.PingpongG3Lite = new (class PingpongG3Lite extends PingpongConnectLite { + constructor() { + super(3, 'PingpongG3Lite'); + this.id = '350301'; + this.imageName = 'PingpongG3Lite.png'; + + this.blockMenuBlocks = [ + 'pingpong_lite_g3_when_button_pressed', + 'pingpong_lite_g3_when_tilted', + 'pingpong_lite_g3_is_button_pressed', + 'pingpong_lite_g3_is_tilted', + 'pingpong_lite_g3_get_tilt_value', + 'pingpong_lite_g3_is_top_shape', + 'pingpong_lite_g3_get_sensor_value', + 'pingpong_lite_g3_multi_motor_rotate', + 'pingpong_lite_g3_motor_rotate', + 'pingpong_lite_g3_start_multi_motor_rotate', + 'pingpong_lite_g3_start_motor_rotate', + 'pingpong_lite_g3_stop_motor_rotate', + 'pingpong_lite_g3_rotate_servo_mortor', + 'pingpong_lite_g3_set_dot_pixel', + 'pingpong_lite_g3_set_dot_string', + 'pingpong_lite_g3_set_dot_clear', + 'pingpong_lite_g3_playNoteForBeats', + 'pingpong_lite_g3_playChordForBeats', + 'pingpong_lite_g3_restForBeats', + 'pingpong_lite_g3_setTempo', + 'pingpong_lite_g3_getTempo', + ]; + } + + getBlocks() { + const self = this; + return { + pingpong_lite_g3_when_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g3_when_button_pressed', + }, + paramsKeyMap: { + CUBEID: 1, + }, + class: 'PingpongG3', + isNotFor: ['PingpongG3Lite'], + event: 'pp_when_button_pressed', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const pd = self.sensor_data; + + return (pd[cubeId]?.BUTTON == 1) + ? script.callReturn() + : this.die(); + }, + }, + pingpong_lite_g3_when_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null, null], + type: 'pingpong_lite_g3_when_tilted', + }, + paramsKeyMap: { + CUBEID: 1, + TILT_DIR: 2, + }, + class: 'PingpongG3', + isNotFor: ['PingpongG3Lite'], + event: 'pp_when_tilted', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + if (tiltValue >= self.TILT_THRESHOLD) { + return script.callReturn(); + } + + return this.die(); + }, + }, + pingpong_lite_g3_is_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { + params: [null], + type: 'pingpong_lite_g3_is_button_pressed', + }, + paramsKeyMap: { + CUBEID: 0, + }, + class: 'PingpongG3', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + return self.sensor_data[cubeId].BUTTON == 1; + }, + }, + pingpong_lite_g3_is_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g3_is_tilted' }, + paramsKeyMap: { + CUBEID: 0, + TILT_DIR: 1, + }, + class: 'PingpongG3', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + + return tiltValue >= self.TILT_THRESHOLD; + }, + }, + pingpong_lite_g3_get_tilt_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g3_get_tilt_value', + }, + paramsKeyMap: { CUBEID: 0, DIR: 1 }, + class: 'PingpongG3', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('DIR', script); + + return self._getTiltValue(cubeId, tiltDir); + }, + }, + pingpong_lite_g3_get_sensor_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], + [Lang.Blocks.pingpong_sensor_ain, 'AIN'], + ], + value: 'PROXIMITY', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: 'pingpong_lite_g3_get_sensor_value' }, + paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, + class: 'PingpongG3', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const sensorType = script.getStringField('SENSOR', script); + const pd = self.sensor_data; + + return pd[cubeId][sensorType]; + }, + }, + pingpong_lite_g3_is_top_shape: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_dir6, + value: 'DF_RECTANGLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g3_is_top_shape' }, + paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, + class: 'PingpongG3', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR', script); + return self._isUpperDir(cubeId, tiltDir); + }, + }, + pingpong_lite_g3_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [], + type: 'pingpong_lite_g3_multi_motor_rotate', + }, + paramsKeyMap: { + DIR_1: 0, + DEGREE_1: 1, + DIR_2: 2, + DEGREE_2: 3, + DIR_3: 4, + DEGREE_3: 5, + }, + class: 'PingpongG3_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + events: {}, + def: { + params: [], + type: 'pingpong_lite_g3_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, + class: 'PingpongG3_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const dir = script.getStringField('DIR'); + const degree = script.getNumberValue('DEGREE'); + const speed = 80 * (dir === 'LEFT' ? -1 : 1); + + const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); + const packet = Buffer.from(arr); + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_start_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g3_start_multi_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2 }, + class: 'PingpongG3_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g3_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 100, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [null, null], + type: 'pingpong_lite_g3_start_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0, SPEED: 1 }, + class: 'PingpongG3_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const speed = script.getNumberValue('SPEED'); + + const arr = self.makeContStepPacket(cubeId, speed); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g3_stop_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g3_stop_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0 }, + class: 'PingpongG3_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + + const arr = self.makeContStepPacket(cubeId, 0); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g3_rotate_servo_mortor: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', defaultType: 'number' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, { type: 'angle' }], type: 'pingpong_lite_g3_rotate_servo_mortor' }, + paramsKeyMap: { cubeno: 0, DEGREE: 1 }, + class: 'PingpongG3_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const opt = [2, 0, angle, 1]; + const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); + return [packet]; + }); + }, + }, + pingpong_lite_g3_set_dot_pixel: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_dot_on, 1], + [Lang.Blocks.pingpong_dot_off, 0], + ], + value: 1, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { + params: [null, null, null], + type: 'pingpong_lite_g3_set_dot_pixel', + }, + paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, + class: 'PingpongG3_peripheral_LED', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let dotX = script.getNumberValue('X', script); + let dotY = script.getNumberValue('Y', script); + const onoff = script.getNumberField('onoff', script); + + dotX = Math.min(Math.max(dotX, 0), 7); + dotY = Math.min(Math.max(dotY, 0), 7); + + const opt = [0x70, dotY, dotX, onoff]; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe1, + cubeId, + opt + ); + return [packet]; + }); + }, + }, + pingpong_lite_g3_set_dot_string: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', value: 'Hello!' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, null], type: 'pingpong_lite_g3_set_dot_string' }, + paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, + class: 'PingpongG3_peripheral_LED', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const str = script.getStringValue('STR', script); + const duration = script.getNumberValue('DURATION', script); + + let period = Math.round((duration * 100) / (str.length * 8)); + period = Math.min(Math.max(period, 1), 200); + + const opt = Buffer.concat([ + Buffer.from([0x70, period, 0]), + Buffer.from(str.substring(0, 20)), + ]); + + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_set_dot_clear: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g3_set_dot_clear' }, + paramsKeyMap: { cubeno: 0 }, + class: 'PingpongG3_peripheral_LED', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const opt = [0x70, 1, 0, ' ']; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + return [packet, 400]; + }); + }, + }, + pingpong_lite_g3_playNoteForBeats: { + //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g3_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g3_playNoteForBeats' }, + paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, + class: 'PingpongG3_Music', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const NOTE = script.getNumberField('NOTE', script); + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); + const packet = Buffer.from(arr); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_playChordForBeats: { + //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g3_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + NOTE_3: 2, + BEATS: 3, + }, + class: 'PingpongG3_Music', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_restForBeats: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g3_restForBeats' }, + paramsKeyMap: { BEATS: 0 }, + class: 'PingpongG3_Music', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + return [null, waitTime]; + }); + }, + }, + pingpong_lite_g3_setTempo: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g3_setTempo' }, + paramsKeyMap: { TEMPO: 0 }, + class: 'PingpongG3_Music', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + let tempo = script.getNumberValue('TEMPO', script); + self.tempo = self._clampTempo(tempo); + return script.callReturn(); + }, + }, + pingpong_lite_g3_getTempo: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [], + def: { params: [], type: 'pingpong_lite_g3_getTempo' }, + paramsKeyMap: {}, + class: 'PingpongG3_Music', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.tempo; + }, + }, + }; + } + })(); +})(); + +module.exports = Entry.PingpongG3Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG4_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG4_lite.js new file mode 100644 index 0000000000..63242f06a6 --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongG4_lite.js @@ -0,0 +1,949 @@ +'use strict'; + +import _range from 'lodash/range'; + +(function() { + const PingpongConnectLite = require('./block_pingpongConnect_lite'); + Entry.PingpongG4Lite = new (class PingpongG4Lite extends PingpongConnectLite { + constructor() { + super(4, 'PingpongG4Lite'); + this.id = '350401'; + this.imageName = 'PingpongG4Lite.png'; + + this.blockMenuBlocks = [ + 'pingpong_lite_g4_when_button_pressed', + 'pingpong_lite_g4_when_tilted', + 'pingpong_lite_g4_is_button_pressed', + 'pingpong_lite_g4_is_tilted', + 'pingpong_lite_g4_get_tilt_value', + 'pingpong_lite_g4_is_top_shape', + 'pingpong_lite_g4_get_sensor_value', + 'pingpong_lite_g4_multi_motor_rotate', + 'pingpong_lite_g4_motor_rotate', + 'pingpong_lite_g4_start_multi_motor_rotate', + 'pingpong_lite_g4_start_motor_rotate', + 'pingpong_lite_g4_stop_motor_rotate', + 'pingpong_lite_g4_rotate_servo_mortor', + 'pingpong_lite_g4_set_dot_pixel', + 'pingpong_lite_g4_set_dot_string', + 'pingpong_lite_g4_set_dot_clear', + 'pingpong_lite_g4_playNoteForBeats', + 'pingpong_lite_g4_playChordForBeats', + 'pingpong_lite_g4_restForBeats', + 'pingpong_lite_g4_setTempo', + 'pingpong_lite_g4_getTempo', + ]; + } + + getBlocks() { + const self = this; + return { + pingpong_lite_g4_when_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g4_when_button_pressed', + }, + paramsKeyMap: { + CUBEID: 1, + }, + class: 'PingpongG4', + isNotFor: ['PingpongG4Lite'], + event: 'pp_when_button_pressed', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const pd = self.sensor_data; + + return (pd[cubeId]?.BUTTON == 1) + ? script.callReturn() + : this.die(); + }, + }, + pingpong_lite_g4_when_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null, null], + type: 'pingpong_lite_g4_when_tilted', + }, + paramsKeyMap: { + CUBEID: 1, + TILT_DIR: 2, + }, + class: 'PingpongG4', + isNotFor: ['PingpongG4Lite'], + event: 'pp_when_tilted', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + if (tiltValue >= self.TILT_THRESHOLD) { + return script.callReturn(); + } + + return this.die(); + }, + }, + pingpong_lite_g4_is_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { + params: [null], + type: 'pingpong_lite_g4_is_button_pressed', + }, + paramsKeyMap: { + CUBEID: 0, + }, + class: 'PingpongG4', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + return self.sensor_data[cubeId].BUTTON == 1; + }, + }, + pingpong_lite_g4_is_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g4_is_tilted' }, + paramsKeyMap: { + CUBEID: 0, + TILT_DIR: 1, + }, + class: 'PingpongG4', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + + return tiltValue >= self.TILT_THRESHOLD; + }, + }, + pingpong_lite_g4_get_tilt_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g4_get_tilt_value', + }, + paramsKeyMap: { CUBEID: 0, DIR: 1 }, + class: 'PingpongG4', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('DIR', script); + + return self._getTiltValue(cubeId, tiltDir); + }, + }, + pingpong_lite_g4_get_sensor_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], + [Lang.Blocks.pingpong_sensor_ain, 'AIN'], + ], + value: 'PROXIMITY', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: 'pingpong_lite_g4_get_sensor_value' }, + paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, + class: 'PingpongG4', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const sensorType = script.getStringField('SENSOR', script); + const pd = self.sensor_data; + + return pd[cubeId][sensorType]; + }, + }, + pingpong_lite_g4_is_top_shape: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_dir6, + value: 'DF_RECTANGLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g4_is_top_shape' }, + paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, + class: 'PingpongG4', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR', script); + return self._isUpperDir(cubeId, tiltDir); + }, + }, + pingpong_lite_g4_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [], + type: 'pingpong_lite_g4_multi_motor_rotate', + }, + paramsKeyMap: { + DIR_1: 0, + DEGREE_1: 1, + DIR_2: 2, + DEGREE_2: 3, + DIR_3: 4, + DEGREE_3: 5, + DIR_4: 6, + DEGREE_4: 7, + }, + class: 'PingpongG4_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + events: {}, + def: { + params: [], + type: 'pingpong_lite_g4_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, + class: 'PingpongG4_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const dir = script.getStringField('DIR'); + const degree = script.getNumberValue('DEGREE'); + const speed = 80 * (dir === 'LEFT' ? -1 : 1); + + const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); + const packet = Buffer.from(arr); + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_start_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g4_start_multi_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2, SPEED_4: 3 }, + class: 'PingpongG4_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g4_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 100, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [null, null], + type: 'pingpong_lite_g4_start_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0, SPEED: 1 }, + class: 'PingpongG4_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const speed = script.getNumberValue('SPEED'); + + const arr = self.makeContStepPacket(cubeId, speed); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g4_stop_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_all, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g4_stop_motor_rotate', + }, + paramsKeyMap: { CUBEID: 0 }, + class: 'PingpongG4_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + + const arr = self.makeContStepPacket(cubeId, 0); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g4_rotate_servo_mortor: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', defaultType: 'number' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, { type: 'angle' }], type: 'pingpong_lite_g4_rotate_servo_mortor' }, + paramsKeyMap: { cubeno: 0, DEGREE: 1 }, + class: 'PingpongG4_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const opt = [2, 0, angle, 1]; + const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); + return [packet]; + }); + }, + }, + pingpong_lite_g4_set_dot_pixel: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_dot_on, 1], + [Lang.Blocks.pingpong_dot_off, 0], + ], + value: 1, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { + params: [null, null, null], + type: 'pingpong_lite_g4_set_dot_pixel', + }, + paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, + class: 'PingpongG4_peripheral_LED', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let dotX = script.getNumberValue('X', script); + let dotY = script.getNumberValue('Y', script); + const onoff = script.getNumberField('onoff', script); + + dotX = Math.min(Math.max(dotX, 0), 7); + dotY = Math.min(Math.max(dotY, 0), 7); + + const opt = [0x70, dotY, dotX, onoff]; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe1, + cubeId, + opt + ); + return [packet]; + }); + }, + }, + pingpong_lite_g4_set_dot_string: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', value: 'Hello!' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, null], type: 'pingpong_lite_g4_set_dot_string' }, + paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, + class: 'PingpongG4_peripheral_LED', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const str = script.getStringValue('STR', script); + const duration = script.getNumberValue('DURATION', script); + + let period = Math.round((duration * 100) / (str.length * 8)); + period = Math.min(Math.max(period, 1), 200); + + const opt = Buffer.concat([ + Buffer.from([0x70, period, 0]), + Buffer.from(str.substring(0, 20)), + ]); + + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_set_dot_clear: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g4_set_dot_clear' }, + paramsKeyMap: { cubeno: 0 }, + class: 'PingpongG4_peripheral_LED', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const opt = [0x70, 1, 0, ' ']; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + return [packet, 400]; + }); + }, + }, + pingpong_lite_g4_playNoteForBeats: { + //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_lite_g4_cube_id, + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g4_playNoteForBeats' }, + paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, + class: 'PingpongG4_Music', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const NOTE = script.getNumberField('NOTE', script); + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); + const packet = Buffer.from(arr); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_playChordForBeats: { + //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g4_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + NOTE_3: 2, + NOTE_4: 3, + BEATS: 4, + }, + class: 'PingpongG4_Music', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_restForBeats: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g4_restForBeats' }, + paramsKeyMap: { BEATS: 0 }, + class: 'PingpongG4_Music', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + return [null, waitTime]; + }); + }, + }, + pingpong_lite_g4_setTempo: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g4_setTempo' }, + paramsKeyMap: { TEMPO: 0 }, + class: 'PingpongG4_Music', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + let tempo = script.getNumberValue('TEMPO', script); + self.tempo = self._clampTempo(tempo); + return script.callReturn(); + }, + }, + pingpong_lite_g4_getTempo: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [], + def: { params: [], type: 'pingpong_lite_g4_getTempo' }, + paramsKeyMap: {}, + class: 'PingpongG4_Music', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.tempo; + }, + }, + }; + } + })(); +})(); + +module.exports = Entry.PingpongG4Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongPracticalarts_lite.js b/src/playground/blocks/hardwareLite/block_pingpongPracticalarts_lite.js new file mode 100644 index 0000000000..8b3601c5d8 --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongPracticalarts_lite.js @@ -0,0 +1,718 @@ +'use strict'; + +import _range from 'lodash/range'; + +(function() { + const PingpongConnectLite = require('./block_pingpongConnect_lite'); + Entry.PingpongPracticalartsLite = new (class PingpongPracticalartsLite extends PingpongConnectLite { + constructor() { + super(1,'PingpongPracticalartsLite'); + this.id = '350501'; + this.imageName = 'PingpongPracticalartsLite.png'; + + this.blockMenuBlocks = [ + 'pingpong_g1_when_button_pressed', + 'pingpong_g1_is_button_pressed', + 'pingpong_g1_when_tilted', + 'pingpong_g1_is_tilted', + 'pingpong_g1_get_tilt_value', + 'pingpong_g1_is_top_shape', + 'pingpong_g1_get_sensor_value', + 'pingpong_g1_motor_rotate', + 'pingpong_g1_start_motor_rotate', + 'pingpong_g1_stop_motor_rotate', + 'pingpong_g1_rotate_servo_mortor', + 'set_steering_direction', + 'move_by_distance', + 'pingpong_g1_set_dot_pixel', + 'pingpong_g1_set_dot_string', + 'pingpong_g1_set_dot_clear', + 'pingpong_g1_playNoteForBeats', + 'pingpong_g1_restForBeats', + 'pingpong_g1_setTempo', + 'pingpong_g1_getTempo', + ]; + } + getBlocks() { + const self = this; + return { + pingpong_lite_g1_when_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + ], + events: {}, + def: { + params: [], + type: 'pingpong_lite_g1_when_button_pressed', + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + event: 'pp_when_button_pressed', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const pd = self.sensor_data; + + return (pd[cubeId]?.BUTTON == 1) + ? script.callReturn() + : this.die(); + }, + }, + pingpong_lite_g1_when_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g1_when_tilted', + }, + paramsKeyMap: { + TILT_DIR: 1, + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + event: 'pp_when_tilted', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + if (tiltValue >= self.TILT_THRESHOLD) { + return script.callReturn(); + } + + return this.die(); + }, + }, + pingpong_lite_g1_is_button_pressed: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + def: { + type: 'pingpong_lite_g1_is_button_pressed', + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + return self.sensor_data[cubeId].BUTTON == 1; + }, + }, + pingpong_lite_g1_is_tilted: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_is_tilted' }, + paramsKeyMap: { + TILT_DIR: 0, + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + + return tiltValue >= self.TILT_THRESHOLD; + }, + }, + pingpong_lite_g1_get_tilt_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: 'pingpong_lite_g1_get_tilt_value', + }, + paramsKeyMap: { DIR: 0 }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('DIR', script); + + return self._getTiltValue(cubeId, tiltDir); + }, + }, + pingpong_lite_g1_get_sensor_value: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], + [Lang.Blocks.pingpong_sensor_ain, 'AIN'], + ], + value: 'PROXIMITY', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_get_sensor_value' }, + paramsKeyMap: { SENSOR: 0 }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const sensorType = script.getStringField('SENSOR', script); + const pd = self.sensor_data; + + return pd[cubeId][sensorType]; + }, + }, + pingpong_lite_g1_is_top_shape: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_dir6, + value: 'DF_RECTANGLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g1_is_top_shape' }, + paramsKeyMap: { + TILT_DIR: 0, + }, + class: 'PingpongG1', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR', script); + return self._isUpperDir(cubeId, tiltDir); + }, + }, + pingpong_lite_g1_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [ + null, + { + type: 'number', + params: ['10'], + }, + ], + type: 'pingpong_lite_g1_motor_rotate', + }, + paramsKeyMap: { DIR: 0, DEGREE: 1 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const dir = script.getStringField('DIR'); + const degree = script.getNumberValue('DEGREE'); + const speed = 80 * (dir === 'LEFT' ? -1 : 1); + + const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); + const packet = Buffer.from(arr); + return [packet, waitTime]; + }); + }, + }, + + pingpong_lite_g1_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [ + { + type: 'number', + params: ['100'], + }, + ], + type: 'pingpong_lite_g1_start_motor_rotate', + }, + paramsKeyMap: { SPEED: 0 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const speed = script.getNumberValue('SPEED'); + + const arr = self.makeContStepPacket(cubeId, speed); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + pingpong_lite_g1_stop_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g1_stop_motor_rotate', + }, + paramsKeyMap: {}, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + + const arr = self.makeContStepPacket(cubeId, 0); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + + pingpong_lite_g1_rotate_servo_mortor: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [{ type: 'angle' }], type: 'pingpong_lite_g1_rotate_servo_mortor' }, + paramsKeyMap: { DEGREE: 0 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const opt = [1, 0, angle, 0]; + const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); + return [packet]; + }); + }, + }, + + pingpong_lite_g1_set_dot_pixel: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_dot_on, 1], + [Lang.Blocks.pingpong_dot_off, 0], + ], + value: 1, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [null, null, null], + type: 'pingpong_lite_g1_set_dot_pixel', + }, + paramsKeyMap: { X: 0, Y: 1, onoff: 2 }, + class: 'PingpongG1_peripheral_LED', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let dotX = script.getNumberValue('X', script); + let dotY = script.getNumberValue('Y', script); + const onoff = script.getNumberField('onoff', script); + + dotX = Math.min(Math.max(dotX, 0), 7); + dotY = Math.min(Math.max(dotY, 0), 7); + + const opt = [0x70, dotY, dotX, onoff]; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe1, + cubeId, + opt + ); + return [packet]; + }); + }, + }, + pingpong_lite_g1_set_dot_string: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', value: 'Hello!' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [null, null], type: 'pingpong_lite_g1_set_dot_string' }, + paramsKeyMap: { STR: 0, DURATION: 1 }, + class: 'PingpongG1_peripheral_LED', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const str = script.getStringValue('STR', script); + const duration = script.getNumberValue('DURATION', script); + + let period = Math.round((duration * 100) / (str.length * 8)); + period = Math.min(Math.max(period, 1), 200); + + const opt = Buffer.concat([ + Buffer.from([0x70, period, 0]), + Buffer.from(str.substring(0, 20)), + ]); + + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g1_set_dot_clear: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_set_dot_clear' }, + paramsKeyMap: {}, + class: 'PingpongG1_peripheral_LED', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const opt = [0x70, 1, 0, ' ']; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + return [packet, 400]; + }); + }, + }, + pingpong_lite_g1_playNoteForBeats: { + //'%1 번 음을 %2 박자로 연주하기 %3', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g1_playNoteForBeats' }, + paramsKeyMap: { NOTE: 0, BEATS: 1 }, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const NOTE = script.getNumberField('NOTE', script); + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); + const packet = Buffer.from(arr); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g1_restForBeats: { + //'%1 박자 쉬기 %2', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_restForBeats' }, + paramsKeyMap: { BEATS: 0 }, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + return [null, waitTime]; + }); + }, + }, + pingpong_lite_g1_setTempo: { + //'악보 빠르기를 %1 으로 정하기 %2', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: 'pingpong_lite_g1_setTempo' }, + paramsKeyMap: { TEMPO: 0 }, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + let tempo = script.getNumberValue('TEMPO', script); + self.tempo = self._clampTempo(tempo); + return script.callReturn(); + }, + }, + pingpong_lite_g1_getTempo: { + //'악보 빠르기', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [], + def: { params: [], type: 'pingpong_lite_g1_getTempo' }, + paramsKeyMap: {}, + class: 'PingpongG1_Music', + isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], + func(sprite, script) { + return self.tempo; + }, + }, + + set_steering_direction: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_mono, + value: 90, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, + ], + events: {}, + def: { params: [], type: 'set_steering_direction' }, + paramsKeyMap: { DEGREE: 0,}, + class: 'PingpongG1_motor', + isNotFor: ['PingpongPracticalArtsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const packet = self.makePacket(0xe1, 0x00, [2, 0, angle, 1]); + return [packet, 400]; + }); + }, + }, + + move_by_distance: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_direction, + value: Lang.Blocks.pingpong_direction[0][1], + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, + ], + events: {}, + def: { params: [], type: 'move_by_distance' }, + paramsKeyMap: { DIR: 0, CM: 1 }, + class: 'PingpongG1_motor', + isNotFor: ['PingpongPracticalArtsLite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const direction = script.getStringField('DIR'); + let degree = script.getNumberValue('CM'); + degree = degree*9; // convert cm to degree + + let speed = 800; + if (direction == 'back') { + speed *= -1; + } + + degree = Math.min(Math.max(degree, 0), 5000); + + let step = Math.round(degree * 5.5); + if (step > 32768) { + step = 32768; + } + + const opt = [2, 1, 0, 2, 0, 0, 0, 0, 0, 0]; + const packet = self.makePacket(0xc1, 0x0004, opt); // SETP_MOTOR + + packet.writeInt16BE(speed, 13); + packet.writeUInt16BE(step, 17); + + const waitTime = Math.round(((1100 - Math.abs(speed)) / 99) * step) + 400; + return [packet, waitTime]; + }); + }, + }, + }; + } + })(); +})(); + +module.exports = Entry.PingpongPracticalartsLite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/metadata_pingpongG1_lite.json b/src/playground/blocks/hardwareLite/metadata_pingpongG1_lite.json new file mode 100644 index 0000000000..b42be7a280 --- /dev/null +++ b/src/playground/blocks/hardwareLite/metadata_pingpongG1_lite.json @@ -0,0 +1,9 @@ +{ + "name": "PingpongG1Lite", + "version": "1.0.0", + "type": "hardware", + "title": "핑퐁 G1", + "description": "로보라이즌", + "imageName": "PingpongG1Lite", + "moduleId": "350101" +} diff --git a/src/playground/blocks/hardwareLite/metadata_pingpongG2_lite.json b/src/playground/blocks/hardwareLite/metadata_pingpongG2_lite.json new file mode 100644 index 0000000000..5f36afbc02 --- /dev/null +++ b/src/playground/blocks/hardwareLite/metadata_pingpongG2_lite.json @@ -0,0 +1,9 @@ +{ + "name": "PingpongG2Lite", + "version": "1.0.0", + "type": "hardware", + "title": "핑퐁 G2", + "description": "로보라이즌", + "imageName": "PingpongG2Lite", + "moduleId": "350201" +} diff --git a/src/playground/blocks/hardwareLite/metadata_pingpongG3_lite.json b/src/playground/blocks/hardwareLite/metadata_pingpongG3_lite.json new file mode 100644 index 0000000000..7d729f5c2f --- /dev/null +++ b/src/playground/blocks/hardwareLite/metadata_pingpongG3_lite.json @@ -0,0 +1,9 @@ +{ + "name": "PingpongG3Lite", + "version": "1.0.0", + "type": "hardware", + "title": "핑퐁 G3", + "description": "로보라이즌", + "imageName": "PingpongG3Lite", + "moduleId": "350301" +} diff --git a/src/playground/blocks/hardwareLite/metadata_pingpongG4_lite.json b/src/playground/blocks/hardwareLite/metadata_pingpongG4_lite.json new file mode 100644 index 0000000000..bb97e21f64 --- /dev/null +++ b/src/playground/blocks/hardwareLite/metadata_pingpongG4_lite.json @@ -0,0 +1,9 @@ +{ + "name": "PingpongG4Lite", + "version": "1.0.0", + "type": "hardware", + "title": "핑퐁 G4", + "description": "로보라이즌", + "imageName": "PingpongG4Lite", + "moduleId": "350401" +} diff --git a/src/playground/blocks/hardwareLite/metadata_pingpong_practicalarts_lite.json b/src/playground/blocks/hardwareLite/metadata_pingpong_practicalarts_lite.json new file mode 100644 index 0000000000..83c7ce4e93 --- /dev/null +++ b/src/playground/blocks/hardwareLite/metadata_pingpong_practicalarts_lite.json @@ -0,0 +1,10 @@ +{ + "name": "PingpongPracticalartsLite", + "version": "1.0.0", + "type": "hardware", + "title": "핑퐁(실과수업용)", + "description": "로보라이즌", + "imageName": "PingpongPracticalartsLite", + "moduleId": "350501" +} + From 3a54036605741768c740db84cca86ec17d5516b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=A7=84=ED=98=B8?= <140678689+jinbaekho@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:41:25 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=ED=81=90=EB=B8=8C=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=EB=B3=84=20=EB=AA=A8=EB=8D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block_pingpongConnect_lite.js | 1705 ++++++++++++++--- .../hardwareLite/block_pingpongG1_lite.js | 719 ------- .../hardwareLite/block_pingpongG1_liteb.js | 1373 ------------- .../hardwareLite/block_pingpongG2_lite.js | 916 --------- .../hardwareLite/block_pingpongG3_lite.js | 930 --------- .../hardwareLite/block_pingpongG4_lite.js | 949 --------- .../hardwareLite/block_pingpongMulti_lite.js | 50 + ...s_lite.js => block_pingpongSingle_lite.js} | 146 +- 8 files changed, 1619 insertions(+), 5169 deletions(-) delete mode 100644 src/playground/blocks/hardwareLite/block_pingpongG1_lite.js delete mode 100644 src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js delete mode 100644 src/playground/blocks/hardwareLite/block_pingpongG2_lite.js delete mode 100644 src/playground/blocks/hardwareLite/block_pingpongG3_lite.js delete mode 100644 src/playground/blocks/hardwareLite/block_pingpongG4_lite.js create mode 100644 src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js rename src/playground/blocks/hardwareLite/{block_pingpongPracticalarts_lite.js => block_pingpongSingle_lite.js} (90%) diff --git a/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js b/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js index 2a1f8339b0..a3d3299962 100644 --- a/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js +++ b/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js @@ -111,7 +111,6 @@ class PingpongConnectLite { const self = this; const originalDisconnect = Entry.hwLite.disconnect; Entry.hwLite.disconnect = function (...params) { - console.log('PingpongConnect:disconnect 호출'); try { if (Entry.hwLite && Entry.hwLite.serial) { @@ -121,7 +120,7 @@ class PingpongConnectLite { console.warn('PingpongConnect:disconnect 전송 중 오류:', e); } finally { setTimeout(() => { - console.log('PingpongConnect:disconnect - 원래 disconnect 함수 호출'); + console.log('PingpongConnect:disconnect 실행, 연결 상태 초기화'); self.isAllConnected = false; Entry.hwLite.disconnect = originalDisconnect; return originalDisconnect.apply(this, params); @@ -157,13 +156,9 @@ class PingpongConnectLite { connect: [0xdd,0xdd,this.groupID,0x00,0x00,0x00,0xda,0x00,0x0b,0x00,0x00], checkDongle: [0xdd,0xdd,0xdd,0xdd,0x00,0x01,0xda,0x00,0x0b,0x00,0x0d], clearDongle: [0xdd,0xdd,0xdd,0xdd,0x00,0x01,0xda,0x00,0x0b,0x00,0xcd], - - test: [0xff,0xff,0xff,0xaa,0x40,0x00,0xcd,0x00,0x59,0x02,0x01,0x00,0x00,0xff,0xff,0xff,0x00,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x00,0x37,0xff,0xff,0xff,0x01,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x01,0xef,0xff,0xff,0xff,0x02,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x01,0xef,0xff,0xff,0xff,0x03,0x00,0x00,0xc1,0x00,0x13,0x02,0x01,0x00,0x02,0x03,0x20,0x00,0x00,0x01,0xef] }; - return packetMap[method] ? - (console.log('makePacket:', method),Buffer.from(packetMap[method])) : - (console.log(`존재하지 않는 method: ${method}\n가능한 목록:\n- ${Object.keys(packetMap).join('\n- ')}`), null); + return packetMap[method] } makePacket(opcode, taskid, cubeNo, opt) { @@ -305,12 +300,11 @@ class PingpongConnectLite { if (opcode === 0xda) { switch (data[9]) { case 0xc0: - console.log('C0'); - Entry.toast.alert(Lang.template.cube_connection_lost,Lang.template.cube_reconnect_notice,false); + Entry.toast.alert(Lang.template.pingpong_lite_cube_connection_lost,Lang.template.pingpong_lite_cube_reconnect_notice,false); this.requestLocalData(this.makePackets(`connect`)); break; case 0xc1: - Entry.toast.success(Lang.template.cube_connection_first,Lang.template.cube_first_connected,false); + Entry.toast.success(Lang.template.pingpong_lite_cube_connection_first,Lang.template.pingpong_lite_cube_first_connected,false); setTimeout(() => { this.requestLocalData(this.makePackets(`connect${this.cubeCount}`)); }, 1000); @@ -320,7 +314,7 @@ class PingpongConnectLite { //큐브1개 연결된 경우 if (opcode === 0xce) { - Entry.toast.success(Lang.template.cube_connection_success,Lang.template.cube_all_connected,false); + Entry.toast.success(Lang.template.pingpong_lite_cube_connection_success,Lang.template.pingpong_lite_cube_all_connected,false); setTimeout(() => { this.requestLocalData(this.makePackets('getSensorData')); }, 3000); @@ -329,7 +323,7 @@ class PingpongConnectLite { //큐브 여러개 연결된 경우, 마지막 큐브가 연결됐다는 응답이 왔을 때 센서데이터 요청 if (opcode === 0xad) { if (bufferData[9]==this.cubeCount-1) { - Entry.toast.success(Lang.template.cube_connection_success,Lang.template.cube_all_connected,false); + Entry.toast.success(Lang.template.pingpong_lite_cube_connection_success,Lang.template.pingpong_lite_cube_all_connected,false); setTimeout(() => {this.requestLocalData(this.makePackets('getSensorData'));}, 3000); this.isAllConnected = true; } else if (this.isAllConnected) { @@ -351,14 +345,6 @@ class PingpongConnectLite { return; } - test(name){ - console.log('테스트실행'); - Entry.toast.warning('1','2',false); - Entry.toast.success('1','2',false); - Entry.toast.alert(3,3,false); - this.requestLocalData(this.makePackets(name)); - } - //#region functions getMonitorPort() { const result = {}; @@ -514,7 +500,6 @@ class PingpongConnectLite { } else if (tiltDir == 'R_STAR') { tiltValue = pd[cubeNo].TILT_Y * -1; } - console.log(); return tiltValue; } @@ -603,79 +588,76 @@ class PingpongConnectLite { //#region setLanguage setLanguage(){ - return { + const lang = { ko: { template: { - cube_connection_first: "큐브 연결 준비됨", - cube_first_connected: "첫 번째 큐브가 연결되었습니다", - cube_connection_lost: "연결 끊어짐", - cube_reconnect_notice: "큐브의 연결이 끊어졌습니다.\n연결을 재시도합니다", - cube_connection_success: "연결 완료", - cube_all_connected: "모든 큐브가 연결되었습니다", - - pingpong_lite_g1_when_button_pressed: '%1 큐브 버튼을 눌렀을 때', - [`pingpong_lite_g${this.name}_when_button_pressed`]: '%1 %2 큐브 버튼을 눌렀을 때', - - pingpong_lite_g1_when_tilted: '%1 큐브를 %2 방향으로 기울였을 때', - [`pingpong_lite_g${this.name}_when_tilted`]: '%1 %2 큐브를 %3로 기울였을 때', - - pingpong_lite_g1_is_button_pressed: '큐브 버튼이 눌렸는가?', - [`pingpong_lite_g${this.name}_is_button_pressed`]: '%1 큐브 버튼이 눌렸는가?', - - pingpong_lite_g1_is_tilted: '큐브가 %1 방향으로 기울어졌는가?', - [`pingpong_lite_g${this.name}_is_tilted`]: '%1 큐브가 %2로 기울어졌는가?', - - pingpong_lite_g1_get_tilt_value: '%1 방향 큐브 기울기', - [`pingpong_lite_g${this.name}_get_tilt_value`]: '%1 큐브의 %2 방향 기울기', - - pingpong_lite_g1_get_sensor_value: '%1 센서값', - [`pingpong_lite_g${this.name}_get_sensor_value`]: '%1 큐브의 %2 센서값', - - pingpong_lite_g1_motor_rotate: '모터를 %1 방향으로 %2 도 회전하기 %3', - [`pingpong_lite_g${this.name}_motor_rotate`]: '%1 모터를 %2 방향으로 %3 도 회전하기 %4', - - pingpong_lite_g1_start_motor_rotate: '모터의 속도를 %1으로 계속 회전하기 %2', - [`pingpong_lite_g${this.name}_start_motor_rotate`]: '%1 모터의 속도를 %2으로 계속 회전하기 %3', - - pingpong_lite_g1_stop_motor_rotate: '모터 멈추기 %1', - [`pingpong_lite_g${this.name}_stop_motor_rotate`]: '%1 모터 멈추기 %2', - - pingpong_lite_g1_is_top_shape: '큐브 윗면에 %1 모양이 있는가?', - [`pingpong_lite_g${this.name}_is_top_shape`]: '%1 큐브의 윗면에 %2 모양이 있는가?', - - pingpong_lite_g1_rotate_servo_mortor: '서보모터를 %1도로 설정하기 %2', - [`pingpong_lite_g${this.name}_rotate_servo_mortor`]: '%1 번째 큐브의 서보모터 %2도로 설정하기 %3', - - pingpong_lite_g1_set_dot_pixel: '도트 X:%1 Y:%2 %3 %4', - [`pingpong_lite_g${this.name}_set_dot_pixel`]: '%1 번째 큐브의 도트 X:%2 Y:%3 %4 %5', - - pingpong_lite_g1_set_dot_string: '도트에 문자열 %1 %2초동안 출력 %3', - [`pingpong_lite_g${this.name}_set_dot_string`]: '%1 번째 큐브에 도트 문자열 %2 %3초동안 보여주기 %4', - - pingpong_lite_g1_set_dot_clear: '도트 화면 지우기 %1', - [`pingpong_lite_g${this.name}_set_dot_clear`]: '%1 번째 큐브의 화면 지우기 %2', - - pingpong_lite_g1_playNoteForBeats: '%1 음을 %2 박자로 연주하기 %3', - [`pingpong_lite_g${this.name}_playNoteForBeats`]: '%1 큐브의 %2 음을 %3 박자로 연주하기 %4', - - [`pingpong_lite_g${this.name}_restForBeats`]: '%1 박자 쉬기 %2', - [`pingpong_lite_g${this.name}_setTempo`]: '악보 빠르기를 %1 으로 정하기 %2', - [`pingpong_lite_g${this.name}_getTempo`]: '악보 빠르기', - - pingpong_lite_g2_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도 회전하기 %5', - pingpong_lite_g3_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도, 모터3: %5방향 %6도 회전하기 %7', - pingpong_lite_g4_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도, 모터3: %5방향 %6도, 모터4: %7 방향 %8 도 회전하기 %9', - - pingpong_lite_g2_start_multi_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2으로 계속 회전하기 %3', - pingpong_lite_g3_start_multi_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2, 모터3 속도를 %3으로 계속 회전하기 %4', - pingpong_lite_g4_start_multi_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2, 모터3 속도를 %3, 모터4 속도를 %4으로 계속 회전하기 %5', - - pingpong_lite_g2_playChordForBeats: '큐브1: %1, 큐브2: %2, %3박자로 연주하기 %4', - pingpong_lite_g3_playChordForBeats: '큐브1: %1, 큐브2: %2, 큐브3: %3, %4박자로 연주하기 %5', - pingpong_lite_g4_playChordForBeats: '큐브1: %1, 큐브2: %2, 큐브3: %3, 큐브4: %4, %5박자로 연주하기 %6', - - set_steering_direction: '조향 바퀴를 %1 방향으로 설정', - move_by_distance: '%1으로%2cm 이동하기', + pingpong_lite_cube_connection_first: "큐브 연결 준비됨", + pingpong_lite_cube_first_connected: "첫 번째 큐브가 연결되었습니다", + pingpong_lite_cube_connection_lost: "연결 끊어짐", + pingpong_lite_cube_reconnect_notice: "큐브의 연결이 끊어졌습니다.\n연결을 재시도합니다", + pingpong_lite_cube_connection_success: "연결 완료", + pingpong_lite_cube_all_connected: "모든 큐브가 연결되었습니다", + ...(this.cubeCount === 1 ?{ + pingpong_lite_g1_when_button_pressed: '%1 큐브 버튼을 눌렀을 때', + pingpong_lite_g1_when_tilted: '%1 큐브를 %2 방향으로 기울였을 때', + pingpong_lite_g1_is_button_pressed: '큐브 버튼이 눌렸는가?', + pingpong_lite_g1_is_tilted: '큐브가 %1 방향으로 기울어졌는가?', + pingpong_lite_g1_get_tilt_value: '%1 방향 큐브 기울기', + pingpong_lite_g1_get_sensor_value: '%1 센서값', + pingpong_lite_g1_motor_rotate: '모터를 %1 방향으로 %2 도 회전하기 %3', + pingpong_lite_g1_start_motor_rotate: '모터의 속도를 %1으로 계속 회전하기 %2', + pingpong_lite_g1_stop_motor_rotate: '모터 멈추기 %1', + pingpong_lite_g1_is_top_shape: '큐브 윗면에 %1 모양이 있는가?', + pingpong_lite_g1_rotate_servo_mortor: '서보모터를 %1도로 설정하기 %2', + pingpong_lite_g1_set_dot_pixel: '도트 X:%1 Y:%2 %3 %4', + pingpong_lite_g1_set_dot_string: '도트에 문자열 %1 %2초동안 출력 %3', + pingpong_lite_g1_set_dot_clear: '도트 화면 지우기 %1', + pingpong_lite_g1_playNoteForBeats: '%1 음을 %2 박자로 연주하기 %3', + pingpong_lite_art_set_steering_direction: '조향 바퀴를 %1 방향으로 설정', + pingpong_lite_art_move_by_distance: '%1으로%2cm 이동하기', + pingpong_lite_g1_restForBeats: '%1 박자 쉬기 %2', + pingpong_lite_g1_setTempo: '악보 빠르기를 %1 으로 정하기 %2', + pingpong_lite_g1_getTempo: '악보 빠르기', + }:{ + [`pingpong_lite_g${this.cubeCount}_when_button_pressed`]: '%1 %2 큐브 버튼을 눌렀을 때', + [`pingpong_lite_g${this.cubeCount}_when_tilted`]: '%1 %2 큐브를 %3로 기울였을 때', + [`pingpong_lite_g${this.cubeCount}_is_button_pressed`]: '%1 큐브 버튼이 눌렸는가?', + [`pingpong_lite_g${this.cubeCount}_is_tilted`]: '%1 큐브가 %2로 기울어졌는가?', + [`pingpong_lite_g${this.cubeCount}_get_tilt_value`]: '%1 큐브의 %2 방향 기울기', + [`pingpong_lite_g${this.cubeCount}_get_sensor_value`]: '%1 큐브의 %2 센서값', + [`pingpong_lite_g${this.cubeCount}_motor_rotate`]: '%1 모터를 %2 방향으로 %3 도 회전하기 %4', + [`pingpong_lite_g${this.cubeCount}_start_motor_rotate`]: '%1 모터의 속도를 %2으로 계속 회전하기 %3', + [`pingpong_lite_g${this.cubeCount}_stop_motor_rotate`]: '%1 모터 멈추기 %2', + [`pingpong_lite_g${this.cubeCount}_is_top_shape`]: '%1 큐브의 윗면에 %2 모양이 있는가?', + [`pingpong_lite_g${this.cubeCount}_rotate_servo_mortor`]: '%1 번째 큐브의 서보모터 %2도로 설정하기 %3', + [`pingpong_lite_g${this.cubeCount}_set_dot_pixel`]: '%1 번째 큐브의 도트 X:%2 Y:%3 %4 %5', + [`pingpong_lite_g${this.cubeCount}_set_dot_string`]: '%1 번째 큐브에 도트 문자열 %2 %3초동안 보여주기 %4', + [`pingpong_lite_g${this.cubeCount}_set_dot_clear`]: '%1 번째 큐브의 화면 지우기 %2', + [`pingpong_lite_g${this.cubeCount}_playNoteForBeats`]: '%1 큐브의 %2 음을 %3 박자로 연주하기 %4', + [`pingpong_lite_g${this.cubeCount}_restForBeats`]: '%1 박자 쉬기 %2', + [`pingpong_lite_g${this.cubeCount}_setTempo`]: '악보 빠르기를 %1 으로 정하기 %2', + [`pingpong_lite_g${this.cubeCount}_getTempo`]: '악보 빠르기', + ...(() => { + switch (this.cubeCount) { + case 2: return { + pingpong_lite_g2_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도 회전하기 %5', + pingpong_lite_g2_multi_start_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2으로 계속 회전하기 %3', + pingpong_lite_g2_multi_playChordForBeats: '큐브1: %1, 큐브2: %2, %3박자로 연주하기 %4', + }; + case 3: return { + pingpong_lite_g3_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도, 모터3: %5방향 %6도 회전하기 %7', + pingpong_lite_g3_multi_start_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2, 모터3 속도를 %3으로 계속 회전하기 %4', + pingpong_lite_g3_multi_playChordForBeats: '큐브1: %1, 큐브2: %2, 큐브3: %3, %4박자로 연주하기 %5', + }; + case 4: return { + pingpong_lite_g4_multi_motor_rotate: '모터1: %1 방향 %2 도, 모터2: %3 방향 %4 도, 모터3: %5방향 %6도, 모터4: %7 방향 %8 도 회전하기 %9', + pingpong_lite_g4_multi_start_motor_rotate: '모터1 속도를 %1, 모터2 속도를 %2, 모터3 속도를 %3, 모터4 속도를 %4으로 계속 회전하기 %5', + pingpong_lite_g4_multi_playChordForBeats: '큐브1: %1, 큐브2: %2, 큐브3: %3, 큐브4: %4, %5박자로 연주하기 %6', + }; + default: return {}; + } + })(), + }), }, Blocks: { @@ -805,85 +787,84 @@ class PingpongConnectLite { }, en: { template: { - cube_connection_first: "Cube connection ready", - cube_first_connected: "The first cube has been connected", - cube_connection_lost: "Connection lost", - cube_reconnect_notice: "The cube connection has been lost.\nReconnecting...", - cube_connection_success: "Connection successful", - cube_all_connected: "All cubes are connected", - - pingpong_lite_g1_when_button_pressed: '%1 When the cube button is pressed', - [`pingpong_lite_g${this.name}_when_button_pressed`]: '%1 %2 When the cube button is pressed', - - pingpong_lite_g1_when_tilted: '%1 When the cube is tilted to %2 direction', - [`pingpong_lite_g${this.name}_when_tilted`]: '%1 %2 When the cube is tilted to %3', - - pingpong_lite_g1_is_button_pressed: 'Is the cube button pressed?', - [`pingpong_lite_g${this.name}_is_button_pressed`]: 'Is the %1 cube button pressed?', - - pingpong_lite_g1_is_tilted: 'Is the cube tilted to %1 direction?', - [`pingpong_lite_g${this.name}_is_tilted`]: 'Is the %1 cube tilted to %2?', - - pingpong_lite_g1_get_tilt_value: '%1 direction cube tilt', - [`pingpong_lite_g${this.name}_get_tilt_value`]: '%1 cube %2 direction tilt', - - pingpong_lite_g1_get_sensor_value: '%1 sensor value', - [`pingpong_lite_g${this.name}_get_sensor_value`]: '%1 cube %2 sensor value', - - pingpong_lite_g1_motor_rotate: 'Rotate motor %2 degrees to %1 direction %3', - [`pingpong_lite_g${this.name}_motor_rotate`]: 'Rotate %1 motor %3 degrees to %2 direction %4', - - pingpong_lite_g1_start_motor_rotate: 'Rotate motor continuously at speed %1 %2', - [`pingpong_lite_g${this.name}_start_motor_rotate`]: 'Rotate %1 motor continuously at speed %2 %3', - - pingpong_lite_g1_stop_motor_rotate: 'Stop motor %1', - [`pingpong_lite_g${this.name}_stop_motor_rotate`]: 'Stop %1 motor %2', - - pingpong_lite_g1_is_top_shape: 'Is there a %1 shape on the top of the cube?', - [`pingpong_lite_g${this.name}_is_top_shape`]: 'Is there a %2 shape on the top of %1 cube?', - - pingpong_lite_g1_rotate_servo_mortor: 'Set servo motor to %1 degrees %2', - [`pingpong_lite_g${this.name}_rotate_servo_mortor`]: 'Set servo motor of cube %1 to %2 degrees %3', - - pingpong_lite_g1_set_dot_pixel: 'Dot X:%1 Y:%2 %3 %4', - [`pingpong_lite_g${this.name}_set_dot_pixel`]: 'Cube %1 dot X:%2 Y:%3 %4 %5', - - pingpong_lite_g1_set_dot_string: 'Display string %1 on dot for %2 seconds %3', - [`pingpong_lite_g${this.name}_set_dot_string`]: 'Display string %2 on cube %1 dot for %3 seconds %4', - - pingpong_lite_g1_set_dot_clear: 'Clear dot display %1', - [`pingpong_lite_g${this.name}_set_dot_clear`]: 'Clear display of cube %1 %2', - - pingpong_lite_g1_playNoteForBeats: 'Play note %1 for %2 beats %3', - [`pingpong_lite_g${this.name}_playNoteForBeats`]: 'Play note %2 of cube %1 for %3 beats %4', - - [`pingpong_lite_g${this.name}_restForBeats`]: 'Rest for %1 beats %2', - [`pingpong_lite_g${this.name}_setTempo`]: 'Set tempo to %1 %2', - [`pingpong_lite_g${this.name}_getTempo`]: 'Tempo', - - pingpong_lite_g2_multi_motor_rotate: 'Motor1: %1 dir %2 deg, Motor2: %3 dir %4 deg rotate %5', - pingpong_lite_g3_multi_motor_rotate: 'Motor1: %1 dir %2 deg, Motor2: %3 dir %4 deg, Motor3: %5 dir %6 deg rotate %7', - pingpong_lite_g4_multi_motor_rotate: 'Motor1: %1 dir %2 deg, Motor2: %3 dir %4 deg, Motor3: %5 dir %6 deg, Motor4: %7 dir %8 deg rotate %9', - - pingpong_lite_g2_start_multi_motor_rotate: 'Motor1 speed %1, Motor2 speed %2 continuous rotate %3', - pingpong_lite_g3_start_multi_motor_rotate: 'Motor1 speed %1, Motor2 speed %2, Motor3 speed %3 continuous rotate %4', - pingpong_lite_g4_start_multi_motor_rotate: 'Motor1 speed %1, Motor2 speed %2, Motor3 speed %3, Motor4 speed %4 continuous rotate %5', - - pingpong_lite_g2_playChordForBeats: 'Cube 1: %1, Cube 2: %2, Play for %3 beats %4', - pingpong_lite_g3_playChordForBeats: 'Cube 1: %1, Cube 2: %2, Cube 3: %3, Play for %4 beats %5', - pingpong_lite_g4_playChordForBeats: 'Cube 1: %1, Cube 2: %2, Cube 3: %3, Cube 4: %4, Play for %5 beats %6', - - [`set_steering_direction${this.name}`]: 'Set the steering wheel to %1 direction', - [`move_by_distance${this.name}`]: 'Move %1 by %2 cm', + pingpong_lite_cube_connection_first: "Cube connection ready", + pingpong_lite_cube_first_connected: "First cube connected", + pingpong_lite_cube_connection_lost: "Connection lost", + pingpong_lite_cube_reconnect_notice: "Cube connection lost.\nRetrying connection", + pingpong_lite_cube_connection_success: "Connection successful", + pingpong_lite_cube_all_connected: "All cubes connected", + + ...(this.cubeCount === 1 ? { + pingpong_lite_g1_when_button_pressed: 'When %1 cube button is pressed', + pingpong_lite_g1_when_tilted: 'When %1 cube is tilted to %2', + pingpong_lite_g1_is_button_pressed: 'Is the cube button pressed?', + pingpong_lite_g1_is_tilted: 'Is the cube tilted to %1?', + pingpong_lite_g1_get_tilt_value: 'Cube tilt value (%1)', + pingpong_lite_g1_get_sensor_value: '%1 sensor value', + pingpong_lite_g1_motor_rotate: 'Rotate motor %2 degrees to %1 direction %3', + pingpong_lite_g1_start_motor_rotate: 'Rotate motor continuously at speed %1 %2', + pingpong_lite_g1_stop_motor_rotate: 'Stop motor %1', + pingpong_lite_g1_is_top_shape: 'Is there a %1 shape on top of the cube?', + pingpong_lite_g1_rotate_servo_mortor: 'Set servo motor to %1 degrees %2', + pingpong_lite_g1_set_dot_pixel: 'Dot X:%1 Y:%2 %3 %4', + pingpong_lite_g1_set_dot_string: 'Display "%1" on dot for %2 seconds %3', + pingpong_lite_g1_set_dot_clear: 'Clear dot display %1', + pingpong_lite_g1_playNoteForBeats: 'Play note %1 for %2 beats %3', + pingpong_lite_art_set_steering_direction: 'Set steering wheel to %1 direction', + pingpong_lite_art_move_by_distance: 'Move %2 cm to %1', + pingpong_lite_g1_restForBeats: 'Rest for %1 beats %2', + pingpong_lite_g1_setTempo: 'Set tempo to %1 %2', + pingpong_lite_g1_getTempo: 'Tempo', + } : { + [`pingpong_lite_g${this.cubeCount}_when_button_pressed`]: 'When %1 %2 cube button is pressed', + [`pingpong_lite_g${this.cubeCount}_when_tilted`]: 'When %1 %2 cube is tilted to %3', + [`pingpong_lite_g${this.cubeCount}_is_button_pressed`]: 'Is %1 cube button pressed?', + [`pingpong_lite_g${this.cubeCount}_is_tilted`]: 'Is %1 cube tilted to %2?', + [`pingpong_lite_g${this.cubeCount}_get_tilt_value`]: '%1 cube tilt value (%2)', + [`pingpong_lite_g${this.cubeCount}_get_sensor_value`]: '%1 cube %2 sensor value', + [`pingpong_lite_g${this.cubeCount}_motor_rotate`]: 'Rotate %1 motor %3 degrees to %2 direction %4', + [`pingpong_lite_g${this.cubeCount}_start_motor_rotate`]: 'Rotate %1 motor continuously at speed %2 %3', + [`pingpong_lite_g${this.cubeCount}_stop_motor_rotate`]: 'Stop %1 motor %2', + [`pingpong_lite_g${this.cubeCount}_is_top_shape`]: 'Is there a %2 shape on top of %1 cube?', + [`pingpong_lite_g${this.cubeCount}_rotate_servo_mortor`]: 'Set servo motor of cube %1 to %2 degrees %3', + [`pingpong_lite_g${this.cubeCount}_set_dot_pixel`]: 'Cube %1 dot X:%2 Y:%3 %4 %5', + [`pingpong_lite_g${this.cubeCount}_set_dot_string`]: 'Display "%2" on cube %1 for %3 seconds %4', + [`pingpong_lite_g${this.cubeCount}_set_dot_clear`]: 'Clear display of cube %1 %2', + [`pingpong_lite_g${this.cubeCount}_playNoteForBeats`]: 'Play note %2 on cube %1 for %3 beats %4', + [`pingpong_lite_g${this.cubeCount}_restForBeats`]: 'Rest for %1 beats %2', + [`pingpong_lite_g${this.cubeCount}_setTempo`]: 'Set tempo to %1 %2', + [`pingpong_lite_g${this.cubeCount}_getTempo`]: 'Tempo', + + ...(() => { + switch (this.cubeCount) { + case 2: return { + pingpong_lite_g2_multi_motor_rotate: 'Motor1: rotate %2 degrees %1, Motor2: rotate %4 degrees %3 %5', + pingpong_lite_g2_multi_start_motor_rotate: 'Motor1 speed %1, Motor2 speed %2 continuous rotation %3', + pingpong_lite_g2_multi_playChordForBeats: 'Cube1: %1, Cube2: %2, play for %3 beats %4', + }; + case 3: return { + pingpong_lite_g3_multi_motor_rotate: 'Motor1: rotate %2 degrees %1, Motor2: rotate %4 degrees %3, Motor3: rotate %6 degrees %5 %7', + pingpong_lite_g3_multi_start_motor_rotate: 'Motor1 speed %1, Motor2 speed %2, Motor3 speed %3 continuous rotation %4', + pingpong_lite_g3_multi_playChordForBeats: 'Cube1: %1, Cube2: %2, Cube3: %3, play for %4 beats %5', + }; + case 4: return { + pingpong_lite_g4_multi_motor_rotate: 'Motor1: rotate %2 degrees %1, Motor2: rotate %4 degrees %3, Motor3: rotate %6 degrees %5, Motor4: rotate %8 degrees %7 %9', + pingpong_lite_g4_multi_start_motor_rotate: 'Motor1 speed %1, Motor2 speed %2, Motor3 speed %3, Motor4 speed %4 continuous rotation %5', + pingpong_lite_g4_multi_playChordForBeats: 'Cube1: %1, Cube2: %2, Cube3: %3, Cube4: %4, play for %5 beats %6', + }; + default: return {}; + } + })(), + }), }, Blocks: { pingpong_right: 'Right', pingpong_left: 'Left', - pingpong_direction :[ - ['Front','front'], - ['Back','back'] + pingpong_direction: [ + ['Forward', 'front'], + ['Backward', 'back'] ], pingpong_rotate_cw: 'Clockwise', @@ -912,34 +893,34 @@ class PingpongConnectLite { ], pingpong_opts_music_notes: [ - ['A3', 45], - ['A#3', 46], - ['B3', 47], - ['C4', 48], - ['C#4', 49], - ['D4', 50], - ['D#4', 51], - ['E4', 52], - ['F4', 53], - ['F#4', 54], - ['G4', 55], - ['G#4', 56], - ['A4', 57], - ['A#4', 58], - ['B4', 59], - ['C5', 60], - ['C#5', 61], - ['D5', 62], - ['D#5', 63], - ['E5', 64], - ['F5', 65], - ['F#5', 66], - ['G5', 67], - ['G#5', 68], - ['A5', 69], - ['A#5', 70], - ['B5', 71], - ['C6', 72], + ['A (A3)', 45], + ['A# (A3#)', 46], + ['B (B3)', 47], + ['C (C4)', 48], + ['C# (C4#)', 49], + ['D (D4)', 50], + ['D# (D4#)', 51], + ['E (E4)', 52], + ['F (F4)', 53], + ['F# (F4#)', 54], + ['G (G4)', 55], + ['G# (G4#)', 56], + ['A (A4)', 57], + ['A# (A4#)', 58], + ['B (B4)', 59], + ['C (C5)', 60], + ['C# (C5#)', 61], + ['D (D5)', 62], + ['D# (D5#)', 63], + ['E (E5)', 64], + ['F (F5)', 65], + ['F# (F5#)', 66], + ['G (G5)', 67], + ['G# (G5#)', 68], + ['A (A5)', 69], + ['A# (A5#)', 70], + ['B (B5)', 71], + ['C (C6)', 72], ], pingpong_opts_mono: [ @@ -963,45 +944,1313 @@ class PingpongConnectLite { ['Right 40°', 130], ['Right 45°', 135], ], + }, + } + }; + console.log(this.name,'lang',lang); + return lang; + } - pingpong_lite_g2_cube_id: [ - ['1', 0], - ['2', 1], - ], - pingpong_lite_g2_cube_all: [ - ['1', 0], - ['2', 1], - ['All', -1], - ], + //#region getBlocks + getBlocks() { + const self = this; + const blocks = { + [`pingpong_lite_g${self.cubeCount}_when_button_pressed`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: `pingpong_lite_g${self.cubeCount}_when_button_pressed`, + }, + paramsKeyMap: { + CUBEID: 1, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + event: 'pp_when_button_pressed', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const pd = self.sensor_data; + + return (pd[cubeId]?.BUTTON == 1) + ? script.callReturn() + : this.die(); + }, + }, + [`pingpong_lite_g${self.cubeCount}_when_tilted`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null, null], + type: `pingpong_lite_g${self.cubeCount}_when_tilted`, + }, + paramsKeyMap: { + CUBEID: 1, + TILT_DIR: 2, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + event: 'pp_when_tilted', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + if (tiltValue >= self.TILT_THRESHOLD) { + return script.callReturn(); + } + + return this.die(); + }, + }, + [`pingpong_lite_g${self.cubeCount}_is_button_pressed`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { + params: [null], + type: `pingpong_lite_g${self.cubeCount}_is_button_pressed`, + }, + paramsKeyMap: { + CUBEID: 0, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + return self.sensor_data[cubeId].BUTTON == 1; + }, + }, + [`pingpong_lite_g${self.cubeCount}_is_tilted`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_is_tilted` }, + paramsKeyMap: { + CUBEID: 0, + TILT_DIR: 1, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); - pingpong_lite_g3_cube_id: [ - ['1', 0], - ['2', 1], - ['3', 2], - ], - pingpong_lite_g3_cube_all: [ - ['1', 0], - ['2', 1], - ['3', 2], - ['All', -1], - ], + const tiltValue = self._getTiltValue(cubeId, tiltDir); - pingpong_lite_g4_cube_id: [ - ['1', 0], - ['2', 1], - ['3', 2], - ['4', 3], - ], - pingpong_lite_g4_cube_all: [ - ['1', 0], - ['2', 1], - ['3', 2], - ['4', 3], - ['All', -1], - ], + return tiltValue >= self.TILT_THRESHOLD; }, - } - } + }, + [`pingpong_lite_g${self.cubeCount}_get_tilt_value`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: `pingpong_lite_g${self.cubeCount}_get_tilt_value`, + }, + paramsKeyMap: { CUBEID: 0, DIR: 1 }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('DIR', script); + + return self._getTiltValue(cubeId, tiltDir); + }, + }, + [`pingpong_lite_g${self.cubeCount}_get_sensor_value`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], + [Lang.Blocks.pingpong_sensor_ain, 'AIN'], + ], + value: 'PROXIMITY', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_get_sensor_value` }, + paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const sensorType = script.getStringField('SENSOR', script); + const pd = self.sensor_data; + + return pd[cubeId][sensorType]; + }, + }, + [`pingpong_lite_g${self.cubeCount}_is_top_shape`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_dir6, + value: 'DF_RECTANGLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_is_top_shape` }, + paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR', script); + return self._isUpperDir(cubeId, tiltDir); + }, + }, + [`pingpong_lite_g${self.cubeCount}_motor_rotate`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + events: {}, + def: { + params: [null, null, null], + type: `pingpong_lite_g${self.cubeCount}_motor_rotate`, + }, + paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const dir = script.getStringField('DIR'); + const degree = script.getNumberValue('DEGREE'); + const speed = 80 * (dir === 'LEFT' ? -1 : 1); + + const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); + const packet = Buffer.from(arr); + return [packet, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_start_motor_rotate`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 100, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [null, null], + type: `pingpong_lite_g${self.cubeCount}_start_motor_rotate`, + }, + paramsKeyMap: { CUBEID: 0, SPEED: 1 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const speed = script.getNumberValue('SPEED'); + + const arr = self.makeContStepPacket(cubeId, speed); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_stop_motor_rotate`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: `pingpong_lite_g${self.cubeCount}_stop_motor_rotate`, + }, + paramsKeyMap: { CUBEID: 0 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + + const arr = self.makeContStepPacket(cubeId, 0); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_rotate_servo_mortor`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, { type: 'angle' }], type: `pingpong_lite_g${self.cubeCount}_rotate_servo_mortor` }, + paramsKeyMap: { cubeno: 0, DEGREE: 1 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const opt = [2, 0, angle, 1]; + const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_set_dot_pixel`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_dot_on, 1], + [Lang.Blocks.pingpong_dot_off, 0], + ], + value: 1, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { + params: [null, null, null], + type: `pingpong_lite_g${self.cubeCount}_set_dot_pixel`, + }, + paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, + class: 'PingpongGN_peripheral_LED', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let dotX = script.getNumberValue('X', script); + let dotY = script.getNumberValue('Y', script); + const onoff = script.getNumberField('onoff', script); + + dotX = Math.min(Math.max(dotX, 0), 7); + dotY = Math.min(Math.max(dotY, 0), 7); + + const opt = [0x70, dotY, dotX, onoff]; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe1, + cubeId, + opt + ); + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_set_dot_string`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', value: 'Hello!' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, null], type: `pingpong_lite_g${self.cubeCount}_set_dot_string` }, + paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, + class: 'PingpongGN_peripheral_LED', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const str = script.getStringValue('STR', script); + const duration = script.getNumberValue('DURATION', script); + + let period = Math.round((duration * 100) / (str.length * 8)); + period = Math.min(Math.max(period, 1), 200); + + const opt = Buffer.concat([ + Buffer.from([0x70, period, 0]), + Buffer.from(str.substring(0, 20)), + ]); + + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms + return [packet, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_set_dot_clear`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_set_dot_clear` }, + paramsKeyMap: { cubeno: 0 }, + class: 'PingpongGN_peripheral_LED', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const opt = [0x70, 1, 0, ' ']; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + return [packet, 400]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_playNoteForBeats`]: { + //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_playNoteForBeats` }, + paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const NOTE = script.getNumberField('NOTE', script); + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); + const packet = Buffer.from(arr); + + return [packet, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_restForBeats`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_restForBeats` }, + paramsKeyMap: { BEATS: 0 }, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + return [null, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_setTempo`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_setTempo` }, + paramsKeyMap: { TEMPO: 0 }, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + let tempo = script.getNumberValue('TEMPO', script); + self.tempo = self._clampTempo(tempo); + return script.callReturn(); + }, + }, + [`pingpong_lite_g${self.cubeCount}_getTempo`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_getTempo` }, + paramsKeyMap: {}, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.tempo; + }, + }, + pingpong_lite_g2_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [null], + type: 'pingpong_lite_g2_multi_motor_rotate', + }, + paramsKeyMap: { DIR_1: 0, DEGREE_1: 1, DIR_2: 2, DEGREE_2: 3 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g2_multi_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g2_multi_start_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g2_multi_playChordForBeats: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g2_multi_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + BEATS: 2, + }, + class: 'PingpongGN_Music', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [], + type: 'pingpong_lite_g3_multi_motor_rotate', + }, + paramsKeyMap: { + DIR_1: 0, + DEGREE_1: 1, + DIR_2: 2, + DEGREE_2: 3, + DIR_3: 4, + DEGREE_3: 5, + }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_multi_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g3_multi_start_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g3_multi_playChordForBeats: { + //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g3_multi_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + NOTE_3: 2, + BEATS: 3, + }, + class: 'PingpongGN_Music', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [], + type: 'pingpong_lite_g4_multi_motor_rotate', + }, + paramsKeyMap: { + DIR_1: 0, + DEGREE_1: 1, + DIR_2: 2, + DEGREE_2: 3, + DIR_3: 4, + DEGREE_3: 5, + DIR_4: 6, + DEGREE_4: 7, + }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_multi_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g4_multi_start_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2, SPEED_4: 3 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g4_multi_playChordForBeats: { + //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g4_multi_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + NOTE_3: 2, + NOTE_4: 3, + BEATS: 4, + }, + class: 'PingpongGN_Music', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + }; + console.log(this.name,'blocks',blocks); + return blocks; } } diff --git a/src/playground/blocks/hardwareLite/block_pingpongG1_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG1_lite.js deleted file mode 100644 index 2743a5aa21..0000000000 --- a/src/playground/blocks/hardwareLite/block_pingpongG1_lite.js +++ /dev/null @@ -1,719 +0,0 @@ -'use strict'; - -import _range from 'lodash/range'; - -(function() { - const PingpongConnectLite = require('./block_pingpongConnect_lite'); - self.Lite = new (class PingpongG1Lite extends PingpongConnectLite { - constructor() { - super(1,'PingpongG1Lite'); - this.id = '350101'; - this.imageName = 'PingpongG1Lite.png'; - - this.blockMenuBlocks = [ - 'pingpong_lite_g1_when_button_pressed', - 'pingpong_lite_g1_when_tilted', - 'pingpong_lite_g1_is_button_pressed', - 'pingpong_lite_g1_is_tilted', - 'pingpong_lite_g1_get_tilt_value', - 'pingpong_lite_g1_get_sensor_value', - 'pingpong_lite_g1_is_top_shape', - 'pingpong_lite_g1_motor_rotate', - 'pingpong_lite_g1_start_motor_rotate', - 'pingpong_lite_g1_stop_motor_rotate', - 'pingpong_lite_g1_rotate_servo_mortor', - 'pingpong_lite_g1_set_dot_pixel', - 'pingpong_lite_g1_set_dot_string', - 'pingpong_lite_g1_set_dot_clear', - 'pingpong_lite_g1_playNoteForBeats', - 'pingpong_lite_g1_restForBeats', - 'pingpong_lite_g1_setTempo', - 'pingpong_lite_g1_getTempo', - ]; - } - - //#region getBlocks - getBlocks() { - const self = this; - return { - pingpong_lite_g1_when_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - ], - events: {}, - def: { - params: [], - type: 'pingpong_lite_g1_when_button_pressed', - }, - class: 'PingpongG1', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - event: 'pp_when_button_pressed', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const pd = self.sensor_data; - - return (pd[cubeId]?.BUTTON == 1) - ? script.callReturn() - : this.die(); - }, - }, - pingpong_lite_g1_when_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g1_when_tilted', - }, - paramsKeyMap: { - TILT_DIR: 1, - }, - class: 'PingpongG1', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - event: 'pp_when_tilted', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - if (tiltValue >= self.TILT_THRESHOLD) { - return script.callReturn(); - } - - return this.die(); - }, - }, - pingpong_lite_g1_is_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - def: { - type: 'pingpong_lite_g1_is_button_pressed', - }, - class: 'PingpongG1', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - return self.sensor_data[cubeId].BUTTON == 1; - }, - }, - pingpong_lite_g1_is_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { params: [], type: 'pingpong_lite_g1_is_tilted' }, - paramsKeyMap: { - TILT_DIR: 0, - }, - class: 'PingpongG1', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - - return tiltValue >= self.TILT_THRESHOLD; - }, - }, - pingpong_lite_g1_get_tilt_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g1_get_tilt_value', - }, - paramsKeyMap: { DIR: 0 }, - class: 'PingpongG1', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('DIR', script); - - return self._getTiltValue(cubeId, tiltDir); - }, - }, - pingpong_lite_g1_get_sensor_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], - [Lang.Blocks.pingpong_sensor_ain, 'AIN'], - ], - value: 'PROXIMITY', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { params: [], type: 'pingpong_lite_g1_get_sensor_value' }, - paramsKeyMap: { SENSOR: 0 }, - class: 'PingpongG1', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const sensorType = script.getStringField('SENSOR', script); - const pd = self.sensor_data; - - return pd[cubeId][sensorType]; - }, - }, - pingpong_lite_g1_is_top_shape: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_dir6, - value: 'DF_RECTANGLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g1_is_top_shape' }, - paramsKeyMap: { - TILT_DIR: 0, - }, - class: 'PingpongG1', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR', script); - return self._isUpperDir(cubeId, tiltDir); - }, - }, - pingpong_lite_g1_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - def: { - params: [ - null, - { - type: 'number', - params: ['10'], - }, - ], - type: 'pingpong_lite_g1_motor_rotate', - }, - paramsKeyMap: { DIR: 0, DEGREE: 1 }, - class: 'PingpongG1_motor', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const dir = script.getStringField('DIR'); - const degree = script.getNumberValue('DEGREE'); - const speed = 80 * (dir === 'LEFT' ? -1 : 1); - - const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); - const packet = Buffer.from(arr); - return [packet, waitTime]; - }); - }, - }, - - pingpong_lite_g1_start_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Block', - accept: 'string', - defaultType: 'number', - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [ - { - type: 'number', - params: ['100'], - }, - ], - type: 'pingpong_lite_g1_start_motor_rotate', - }, - paramsKeyMap: { SPEED: 0 }, - class: 'PingpongG1_motor', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const speed = script.getNumberValue('SPEED'); - - const arr = self.makeContStepPacket(cubeId, speed); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - pingpong_lite_g1_stop_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g1_stop_motor_rotate', - }, - paramsKeyMap: {}, - class: 'PingpongG1_motor', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - - const arr = self.makeContStepPacket(cubeId, 0); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - - pingpong_lite_g1_rotate_servo_mortor: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Block', - accept: 'string', - defaultType: 'number', - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [{ type: 'angle' }], type: 'pingpong_lite_g1_rotate_servo_mortor' }, - paramsKeyMap: { DEGREE: 0 }, - class: 'PingpongG1_motor', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let angle = script.getNumberValue('DEGREE', script); - - angle = Math.min(Math.max(angle, 0), 180); - - const opt = [1, 0, angle, 0]; - const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); - return [packet]; - }); - }, - }, - - pingpong_lite_g1_set_dot_pixel: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_dot_on, 1], - [Lang.Blocks.pingpong_dot_off, 0], - ], - value: 1, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [null, null, null], - type: 'pingpong_lite_g1_set_dot_pixel', - }, - paramsKeyMap: { X: 0, Y: 1, onoff: 2 }, - class: 'PingpongG1_peripheral_LED', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let dotX = script.getNumberValue('X', script); - let dotY = script.getNumberValue('Y', script); - const onoff = script.getNumberField('onoff', script); - - dotX = Math.min(Math.max(dotX, 0), 7); - dotY = Math.min(Math.max(dotY, 0), 7); - - const opt = [0x70, dotY, dotX, onoff]; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe1, - cubeId, - opt - ); - return [packet]; - }); - }, - }, - pingpong_lite_g1_set_dot_string: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', value: 'Hello!' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [null, null], type: 'pingpong_lite_g1_set_dot_string' }, - paramsKeyMap: { STR: 0, DURATION: 1 }, - class: 'PingpongG1_peripheral_LED', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const str = script.getStringValue('STR', script); - const duration = script.getNumberValue('DURATION', script); - - let period = Math.round((duration * 100) / (str.length * 8)); - period = Math.min(Math.max(period, 1), 200); - - const opt = Buffer.concat([ - Buffer.from([0x70, period, 0]), - Buffer.from(str.substring(0, 20)), - ]); - - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g1_set_dot_clear: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g1_set_dot_clear' }, - paramsKeyMap: {}, - class: 'PingpongG1_peripheral_LED', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const opt = [0x70, 1, 0, ' ']; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - return [packet, 400]; - }); - }, - }, - pingpong_lite_g1_playNoteForBeats: { - //'%1 번 음을 %2 박자로 연주하기 %3', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g1_playNoteForBeats' }, - paramsKeyMap: { NOTE: 0, BEATS: 1 }, - class: 'PingpongG1_Music', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const NOTE = script.getNumberField('NOTE', script); - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); - const packet = Buffer.from(arr); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g1_restForBeats: { - //'%1 박자 쉬기 %2', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g1_restForBeats' }, - paramsKeyMap: { BEATS: 0 }, - class: 'PingpongG1_Music', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - return [null, waitTime]; - }); - }, - }, - pingpong_lite_g1_setTempo: { - //'악보 빠르기를 %1 으로 정하기 %2', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g1_setTempo' }, - paramsKeyMap: { TEMPO: 0 }, - class: 'PingpongG1_Music', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - let tempo = script.getNumberValue('TEMPO', script); - self.tempo = self._clampTempo(tempo); - return script.callReturn(); - }, - }, - pingpong_lite_g1_getTempo: { - //'악보 빠르기', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [], - def: { params: [], type: 'pingpong_lite_g1_getTempo' }, - paramsKeyMap: {}, - class: 'PingpongG1_Music', - isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], - func(sprite, script) { - return self.tempo; - }, - }, - - set_steering_direction: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_mono, - value: 90, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, - ], - events: {}, - def: { params: [], type: 'set_steering_direction' }, - paramsKeyMap: { DEGREE: 0,}, - class: 'PingpongG1_motor', - isNotFor: ['PingpongPracticalArtsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - let angle = script.getNumberValue('DEGREE', script); - - angle = Math.min(Math.max(angle, 0), 180); - - const packet = self.makePacket(0xe1, 0x00, [2, 0, angle, 1]); - return [packet, 400]; - }); - }, - }, - - move_by_distance: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_direction, - value: Lang.Blocks.pingpong_direction[0][1], - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, - ], - events: {}, - def: { params: [], type: 'move_by_distance' }, - paramsKeyMap: { DIR: 0, CM: 1 }, - class: 'PingpongG1_motor', - isNotFor: ['PingpongPracticalArtsLite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const direction = script.getStringField('DIR'); - let degree = script.getNumberValue('CM'); - degree = degree*9; // convert cm to degree - - let speed = 800; - if (direction == 'back') { - speed *= -1; - } - - degree = Math.min(Math.max(degree, 0), 5000); - - let step = Math.round(degree * 5.5); - if (step > 32768) { - step = 32768; - } - - const opt = [2, 1, 0, 2, 0, 0, 0, 0, 0, 0]; - const packet = self.makePacket(0xc1, 0x0004, opt); // SETP_MOTOR - - packet.writeInt16BE(speed, 13); - packet.writeUInt16BE(step, 17); - - const waitTime = Math.round(((1100 - Math.abs(speed)) / 99) * step) + 400; - return [packet, waitTime]; - }); - }, - }, - }; - } - - })(); -})(); - -module.exports = self.Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js b/src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js deleted file mode 100644 index b79a180588..0000000000 --- a/src/playground/blocks/hardwareLite/block_pingpongG1_liteb.js +++ /dev/null @@ -1,1373 +0,0 @@ -// 'use strict'; - -// import _range from 'lodash/range'; - -// const OPCODE = { -// SINGLE_STEPS: 0xc1, -// SCHEDULED_STEPS: 0xca, -// SCHEDULED_POINTS: 0xcb, -// CONTINUOUS_STEPS: 0xcc, -// AGGREGATE_STEPS: 0xcd, -// LEDMATRIX: 0xa2, -// SERVO: 0xe1, -// MUSIC: 0xe8, -// }; - -// const METHOD = { -// CONTINOUS: 0, -// RELATIVE_SINGLE: 1, -// ABSOLUTE_SINGLE: 2, -// SCHED_STEPS: 3, -// sCHED_POINT: 4, -// }; - -// const MODE = { -// BASIC: 1, -// MULTIROLE: 2, -// CHECKCRC: 3, -// }; - -// const PROPERTY = { -// PERI: 0x01, -// MULTI: 0x02, -// PORT: 0x80, -// ADDRESS: 0x70, -// PAUSE: 1, -// RESUME: 2, -// MUSIC_PLAY: 0, -// }; - -// const DEFAULT_TEMPO = 60; -// (function() { -// Entry.PingpongG1Lite = new (class PingpongG1Lite { -// constructor() { -// this.id = '060101'; -// this.webapiType = 'ble'; -// this.bluetoothInfo = { -// filters: [ -// { -// namePrefix: 'PINGPONG', -// }, -// ], -// }; -// this.services=null -// this.name = 'PingpongG1Lite'; -// this.url = 'http://www.roborisen.com'; -// this.imageName = 'PingpongG1Lite.png'; -// this.title = { -// ko: 'G큐브(Web)', -// en: 'G-cube(Web)', -// }; - -// this.customPrompt = { -// title: '그룹 번호 입력', -// description: '연결할 기기의 그룹 번호를 입력해 주세요.\n 별도로 지정하지 않았다면 00을 입력합니다.', -// defaultValue: '00', -// negativeButtonText: '취소', -// positiveButtonText: '설정', -// }; - -// this.communicationType = 'manual'; -// this.cubeCount = 1; -// this.checkMultiroleAction = false; -// this.isplaying = false; -// this.sendBuffer = []; -// this.TILT_THRESHOLD = 20; -// this.MOVE_THRESHOLD = 30; -// this.send_cmd_id = 0; -// // 엔트리Js에서 기기와 통신하는 함수 호출 Duration 간격 -// this.duration = 50; -// this.portData = { -// baudRate: 115200, -// duration: 50, -// dataBits: 8, -// parity: 'none', -// stopBits: 1, -// bufferSize: 65536, -// constantServing: true, -// }; - -// this.sensor_data = { -// MOVE_X: 0, -// MOVE_Y: 0, -// MOVE_Z: 0, -// TILT_X: 0, -// TILT_Y: 0, -// TILT_Z: 0, -// BUTTON: 0, -// PROXIMITY: 0, -// AIN: 0, -// }; - -// this.blockMenuBlocks = [ -// 'pingpong_lite_g1_when_button_pressed', -// 'pingpong_lite_g1_is_button_pressed', -// 'pingpong_lite_g1_when_tilted', -// 'pingpong_lite_g1_is_tilted', -// 'pingpong_lite_g1_get_tilt_value', -// 'pingpong_lite_g1_is_top_shape', -// 'pingpong_lite_g1_get_sensor_value', -// 'pingpong_lite_g1_motor_rotate', -// 'pingpong_lite_g1_start_motor_rotate', -// 'pingpong_lite_g1_stop_motor_rotate', -// 'pingpong_lite_g1_rotate_servo_mortor', -// 'pingpong_lite_g1_set_dot_pixel', -// 'pingpong_lite_g1_set_dot_string', -// 'pingpong_lite_g1_set_dot_clear', -// 'pingpong_lite_g1_playNoteForBeats', -// 'pingpong_lite_g1_restForBeats', -// 'pingpong_lite_g1_setTempo', -// 'pingpong_lite_g1_getTempo', -// ]; -// this.tempo = 60; -// this.readablePorts = []; -// this.setZero(); -// } - -// // 그룹번호 넣기 -// requestInitialData(sp, payload) { -// console.log('g1함수이름:requestInitialData'); -// const grpid = payload.match(/[0-9]{1,2}$/g); -// if (grpid == null) { -// console.warn('Wrong group id inputted', payload); -// return null; -// } -// const grpno = parseInt(grpid[0], 16); - -// if (this.checkMultiroleAction == false) { -// this.checkMultiroleAction = true; -// return this.makePackets('connect', grpno); -// } else { -// return null; -// } -// } - -// async initialHandshake() { -// console.log('g1함수이름:initialHandshake'); -// const 원본disconnect = Entry.hwLite.disconnect; - -// Entry.hwLite.disconnect = async function(...인수) { -// console.log('disconnect 호출됨'); - -// await 캐릭터리스틱쓰기.writeValue(new Uint8Array([0xff,0xff,0xff,0xff,0x00,0x00,0xa8,0x00,0x0a,0x01])); -// return await 원본disconnect.apply(this, 인수); -// }; - - -// const 디바이스 = Entry.hwLite.webConnector.device -// console.log('디바이스',디바이스); -// const 서비스 = await 디바이스.gatt.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e'); -// console.log('서비스',서비스); -// const 캐릭터리스틱쓰기 = await 서비스.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e'); -// const 캐릭터리스틱읽기 = await 서비스.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e'); -// console.log('캐릭터리스틱쓰기',캐릭터리스틱쓰기); -// console.log('캐릭터리스틱읽기',캐릭터리스틱읽기); -// await 캐릭터리스틱읽기.startNotifications(); -// 캐릭터리스틱읽기.addEventListener('characteristicvaluechanged', (이벤트) => { -// const 대상 = 이벤트.target; -// const 값 = 대상.value; - -// if (!값 || 값.byteLength == 0) {return;} - -// const 바이트배열 = new Uint8Array(값.buffer); -// console.log('구독 데이터', 바이트배열); -// }); -// await 캐릭터리스틱쓰기.writeValue(new Uint8Array([0xff,0xff,0x00,0x07,0x00,0x00,0xce,0x00,0x0e,0x02,0x00,0x00,0x07,0x50])); -// setTimeout(async () => { -// await 캐릭터리스틱쓰기.writeValue(new Uint8Array([0xff,0xff,0x00,0xff,0x00,0xc8,0xb8,0x00,0x0b,0x0a,0x01])); -// }, 5000); - - -// } -// // async initialHandshake() { -// // Entry.hwLite.serial.sendAsciiAsBuffer(this.requestInitialData(0,'0')); - -// // return true; -// // } - -// makePackets(method, grpid = 0) { -// console.log('g1함수이름:makePackets'); -// let result = null; -// if (method === 'connect') { -// result = Buffer.from([ -// 0xdd, -// 0xdd, -// grpid, -// 0x00, -// 0x00, -// 0x00, -// 0xda, -// 0x00, -// 0x0b, -// 0x00, -// 0x00, -// ]); -// //result[2] = this.groupId; -// } else if (method === 'getSensorData') { -// result = Buffer.from([ -// 0xff, -// 0xff, -// 0xff, -// 0xff, // position -// 0x00, -// 0xc8, // continuous sampling -// 0xb8, -// 0x00, -// 0x0b, -// 10, // interval //YIM's 30->10 -// 0x01, -// ]); -// } else if (method === 'setColorLed') { -// result = Buffer.from('ffff00070000ce000e0200000750', 'hex'); -// } -// return result; -// } - -// makePacket(opcode, taskid, opt) { -// console.log('g1함수이름:makePacket'); - -// const header = Buffer.from([0xff, 0xff, 0xff, 0xff, 0, 0, opcode, 0, 0]); -// const property = Buffer.from(opt); - -// //header.writeUInt16BE(0xFFFF, 0); -// //header.writeUInt16BE(0xFFFF, 2); // cubdid - -// header.writeUInt16BE(taskid, 4); -// header.writeUInt16BE(header.length + property.length, 7); - -// return Buffer.concat([header, property]); -// } - - -// async postCallReturn(script, myfunc) { -// console.log('g1함수이름:postCallReturn'); -// if (myfunc == undefined) { -// return script.callReturn(); -// } - -// if (script.is_start == undefined) { -// script.is_start = true; - -// const [packet, waitTime = this.delayTime] = myfunc(); - -// if (packet && packet.length > 0) { -// Entry.hwLite.serial.sendAsciiAsBuffer(packet) -// this.isplaying = true; -// } - -// await new Promise(resolve => setTimeout(resolve, waitTime)); // waitTime 동안 대기 - -// script.is_start = false; - -// return script.callReturn(); -// } else if (script.is_start == true) { -// return script; -// } else { -// delete script.is_start; - -// return script.callReturn(); -// } -// } - -// // 연결시 기기 초기화 -// async setZero() { -// console.log('g1함수이름:setZero'); -// console.log('pingpong g1 lite loaded'); -// Entry.hwLite.testCustomPromptPayload() -// } -// // async setZero() { -// // if (Entry.hwLite && Entry.hwLite.serial) { -// // Entry.hwLite.serial.update(); -// // Entry.hwLite.serial.sendAsciiAsBuffer(this.makePackets('setColorLed', 0)); -// // } - -// // // 기기 정지 -// // if(this.isplaying){ -// // for(let j = 0; j < 5; j++){ -// // const packet = Entry.PingpongG1Lite.makeContStepPacket(0, 0); -// // Entry.hwLite.serial.sendAsciiAsBuffer(packet); - -// // await new Promise(resolve => setTimeout(resolve, 50)); // 50ms 대기 -// // } -// // } -// // this.isplaying = false -// // } - -// // 하드웨어에서 온 데이터 처리 -// handleLocalData(data) { -// console.log('g1함수이름:handleLocalData'); -// console.log('그룹',Entry.hwLite.getCustomPromptPayload()); - -// if (data.length >= 9) { -// const dataView = new DataView(data.buffer); -// const packetSize = dataView.getUint16(7, false); -// const opcode = data[6]; - -// if (data.length === packetSize && opcode === 0xb8) { -// const sensor = Entry.PingpongG1Lite.sensor_data; - -// sensor.MOVE_X = dataView.getInt8(12); -// sensor.MOVE_Y = dataView.getInt8(13); -// sensor.MOVE_Z = dataView.getInt8(14); - -// const xx = Math.max(Math.min(dataView.getInt8(15), 90), -90); -// let yy = Math.max(Math.min(dataView.getInt8(16), 90), -90); -// yy *= -1; -// const zz = Math.max(Math.min(dataView.getInt8(17), 90), -90); - -// if(sensor.TILT_X != xx || sensor.TILT_Y != yy || sensor.TILT_Z != zz){ -// Entry.engine.fireEvent('pp_when_tilted'); -// } - -// sensor.TILT_X = xx; -// sensor.TILT_Y = yy; -// sensor.TILT_Z = zz; - -// if(sensor.BUTTON != dataView.getUint8(11)){ -// Entry.engine.fireEvent('pp_when_button_pressed'); -// } - -// console.log(zz); - -// sensor.BUTTON = dataView.getUint8(11); - -// sensor.PROXIMITY = dataView.getUint8(18); - -// // 기존 FW 70 버전 = data length 19 bytes (ANALOG IN 미지원) -// if (packetSize > 19) { -// sensor.AIN = dataView.getUint8(19) * 4; -// } else { -// sensor.AIN = 0; -// } -// } -// } -// } - - -// // 하드웨어 기기에 전달할 데이터 -// requestLocalData() { -// console.log('g1함수이름:requestLocalData'); -// this.sendBuffer = []; -// this.sendBuffer.push(this.makePackets('getSensorData', 1)) - -// return this.sendBuffer[0]; -// } - -// _clampBeats(beats) { -// console.log('g1함수이름:_clampBeats'); -// return Math.min(Math.max(beats, 0), 40); -// } - -// _clampTempo(tempo) { -// console.log('g1함수이름:_clampTempo'); -// return Math.min(Math.max(tempo, 20), 500); -// } - -// _beatsToDuration(beats) { -// console.log('g1함수이름:_beatsToDuration'); -// let duration = Math.round((60 / this.tempo) * beats * 100); -// return duration; -// } - -// _fillPacketIntoArray(data, opcode, taskid, cubeNo, size) { -// console.log('g1함수이름:_fillPacketIntoArray'); -// data[0] = 0xff; -// data[1] = 0xff; -// data[2] = 0xff; - -// if (cubeNo <= -1) { -// data[3] = 0xff; -// } else { -// data[3] = cubeNo; -// } - -// data[4] = taskid / 256; -// data[5] = taskid % 256; - -// data[6] = opcode; - -// data[7] = size / 256; -// data[8] = size % 256; -// } - -// makeMusicNotePacket(cubeNo, note, duration) { -// console.log('g1함수이름:makeMusicNotePacket'); -// const packet = new Uint8Array(9 + 5); -// this._fillPacketIntoArray(packet, OPCODE.MUSIC, 0xa1, cubeNo, 9 + 5); - -// packet[9] = 0; -// packet[10] = PROPERTY.MUSIC_PLAY; -// packet[11] = note - 8; -// packet[12] = duration; -// packet[13] = 0; - -// return packet; -// } - -// makeContStepPacket(cubeNo, speed) { -// console.log('g1함수이름:makeContStepPacket'); -// const packet = new Uint8Array(9 + 6); - -// this._fillPacketIntoArray(packet, OPCODE.CONTINUOUS_STEPS, 0, cubeNo, 15); - -// const sps = this._calcSpsFromSpeed(speed); - -// packet[9] = MODE.MULTIROLE; -// packet[10] = METHOD.CONTINOUS; -// packet[11] = 0; //step_type; full=0, servo=4 - -// if (sps == 0) { -// packet[12] = PROPERTY.PAUSE; -// packet[13] = 0; -// packet[14] = 0; -// } else { -// packet[12] = PROPERTY.RESUME; -// packet[13] = sps / 256; -// packet[14] = sps % 256; -// } - -// return packet; -// } - -// _calcSpsFromSpeed(speed_) { -// console.log('g1함수이름:_calcSpsFromSpeed'); -// let speed = speed_; -// if (speed > 100) { -// speed = 100; -// } -// if (speed < -100) { -// speed = -100; -// } - -// let sps = 0; -// if (speed != 0) { -// if (speed < 0) { -// sps = 65536 + (speed * 9 - 100); -// } else { -// sps = speed * 9 + 100; -// } -// sps = Math.round(sps); -// } -// return sps; -// } - -// monitorTemplate = { -// imgPath: 'hw_lite/PingpongG1Lite.png', -// width: 400, -// height: 400, -// listPorts: { -// BUTTON: { -// name: 'button', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// MOVE_X: { -// name: 'move_x', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// MOVE_Y: { -// name: 'move_y', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// MOVE_Z: { -// name: 'move_z', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// TILT_X: { -// name: 'tilt_x', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// TILT_Y: { -// name: 'tilt_y', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// TILT_Z: { -// name: 'tilt_z', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// PROXIMITY: { -// name: 'proximity', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// AIN: { -// name: 'ain', -// type: 'input', -// pos: { x: 0, y: 0 }, -// }, -// }, -// ports: {}, -// mode: 'both', -// }; - -// setLanguage() { -// console.log('g1함수이름:setLanguage'); -// return { -// ko: { -// template: { -// pingpong_lite_g1_when_button_pressed: '%1 큐브 버튼을 눌렀을 때', -// pingpong_lite_g1_when_tilted: '%1 큐브를 %2 방향으로 기울였을 때', -// pingpong_lite_g1_is_button_pressed: '큐브 버튼이 눌렸는가?', -// pingpong_lite_g1_is_tilted: '큐브가 %1 방향으로 기울어졌는가?', -// pingpong_lite_g1_get_tilt_value: '%1 방향 큐브 기울기', -// pingpong_lite_g1_get_sensor_value: '%1 센서값', -// pingpong_lite_g1_motor_rotate: '모터를 %1 방향으로 %2 도 회전하기 %3', -// pingpong_lite_g1_start_motor_rotate: '모터의 속도를 %1으로 계속 회전하기 %2', -// pingpong_lite_g1_stop_motor_rotate: '모터 멈추기 %1', -// pingpong_lite_g1_rotate_servo_mortor: '서보모터를 %1도로 설정하기 %2', -// pingpong_lite_g1_is_top_shape: '큐브 윗면에 %1 모양이 있는가?', -// pingpong_lite_g1_set_dot_pixel: '도트 X:%1 Y:%2 %3 %4', -// pingpong_lite_g1_set_dot_string: '도트에 문자열 %1 %2초동안 출력 %3', -// pingpong_lite_g1_set_dot_clear: '도트 화면 지우기 %1', -// pingpong_lite_g1_playNoteForBeats: '%1 음을 %2 박자로 연주하기 %3', -// pingpong_lite_g1_restForBeats: '%1 박자 쉬기 %2', -// pingpong_lite_g1_setTempo: '악보 빠르기를 %1 으로 정하기 %2', -// pingpong_lite_g1_getTempo: '악보 빠르기', -// }, -// Blocks: { -// pingpong_right: '오른쪽', -// pingpong_left: '왼쪽', - -// pingpong_rotate_cw: '시계', -// pingpong_rotate_ccw: '반시계', - -// pingpong_sensor_proximity: '근접', -// pingpong_sensor_ain: '아날로그', -// pingpong_dot_on: '켜기', -// pingpong_dot_off: '끄기', - -// pingpong_opts_cube_tiltDir: [ -// ['동그라미', 'F_CIRCLE'], -// ['세모', 'B_TRIANGLE'], -// ['네모', 'L_RECTANGLE'], -// ['별', 'R_STAR'], -// ], - -// pingpong_opts_cube_dir6: [ -// ['네모', 'DF_RECTANGLE'], -// ['별', 'DB_STAR'], -// ['세모', 'DL_TRIANGLE'], -// ['동그라미', 'DR_CIRCLE'], -// ['하트', 'DU_HEART'], -// ['빈칸', 'DD_NONE'], -// ], - -// pingpong_opts_music_notes: [ -// ['라 (A3)', 45], -// ['라# (A3#)', 46], -// ['시 (B3)', 47], -// ['도 (C4)', 48], -// ['도# (C4#)', 49], -// ['레 (D4)', 50], -// ['레# (D4#)', 51], -// ['미 (E4)', 52], -// ['파 (F4)', 53], -// ['파# (F4#)', 54], -// ['솔 (G4)', 55], -// ['솔# (G4#)', 56], -// ['라 (A4)', 57], -// ['라# (A4#)', 58], -// ['시 (B4)', 59], -// ['도 (C5)', 60], -// ['도# (C5#)', 61], -// ['레 (D5)', 62], -// ['레# (D5#)', 63], -// ['미 (E5)', 64], -// ['파 (F5)', 65], -// ['파# (F5#)', 66], -// ['솔 (G5)', 67], -// ['솔# (G5#)', 68], -// ['라 (A5)', 69], -// ['라# (A5#)', 70], -// ['시 (B5)', 71], -// ['도 (C6)', 72], -// ], -// }, -// }, -// en: { -// template: { -// pingpong_lite_g1_when_button_pressed: '%1 Button pressed', -// pingpong_lite_g1_when_tilted: '%1 Tilted to %2', -// pingpong_lite_g1_is_button_pressed: 'button pressed?', -// pingpong_lite_g1_is_tilted: 'cube tilted to %1', -// pingpong_lite_g1_get_tilt_value: 'tilt angle to %1', -// pingpong_lite_g1_get_sensor_value: 'read sensor %1', -// pingpong_lite_g1_motor_rotate: 'rotate %2 degrees %1 %3', -// pingpong_lite_g1_start_motor_rotate: 'set motor speed to %1 %2', -// pingpong_lite_g1_stop_motor_rotate: 'stop motor rotate %1', -// pingpong_lite_g1_rotate_servo_mortor: 'set servo mortor to %1 degrees %2', -// pingpong_lite_g1_is_top_shape: '%1 shown in top view?', -// pingpong_lite_g1_set_dot_pixel: 'set %3 DOT X:%1 Y:%2 %4', -// pingpong_lite_g1_set_dot_string: 'print string %1 during %2 seconds to DOT %3', -// pingpong_lite_g1_set_dot_clear: 'clear DOT %1', -// pingpong_lite_g1_playNoteForBeats: 'play note %1 for %2 beats %3', -// pingpong_lite_g1_restForBeats: 'rest for %1 beats %2', -// pingpong_lite_g1_setTempo: 'set tempo to %1 %2', -// pingpong_lite_g1_getTempo: 'tempo', -// }, -// Blocks: { -// pingpong_right: 'right', -// pingpong_left: 'left', - -// pingpong_rotate_cw: 'clockwise', -// pingpong_rotate_ccw: 'counter clockwise', - -// pingpong_sensor_proximity: 'proximity', -// pingpong_sensor_ain: 'ain', -// pingpong_dot_on: 'ON', -// pingpong_dot_off: 'OFF', - -// pingpong_opts_cube_tiltDir: [ -// ['circle', 'F_CIRCLE'], -// ['triangle', 'B_TRIANGLE'], -// ['rectangle', 'L_RECTANGLE'], -// ['star', 'R_STAR'], -// ], - -// pingpong_opts_cube_dir6: [ -// ['rectangle', 'DF_RECTANGLE'], -// ['star', 'DB_STAR'], -// ['triangle', 'DL_TRIANGLE'], -// ['circle', 'DR_CIRCLE'], -// ['heart', 'DU_HEART'], -// ['none', 'DD_NONE'], -// ], -// pingpong_opts_music_notes: [ -// ['La (A3)', 45], -// ['La# (A3#)', 46], -// ['Ti (B3)', 47], -// ['Do (C4)', 48], -// ['Do# (C4#)', 49], -// ['Re (D4)', 50], -// ['Re# (D4#)', 51], -// ['Mi (E4)', 52], -// ['Fa (F4)', 53], -// ['Fa# (F4#)', 54], -// ['Sol (G4)', 55], -// ['Sol#(G4#)', 56], -// ['La (A4)', 57], -// ['La# (A4#)', 58], -// ['Ti (B4)', 59], -// ['Do (C5)', 60], -// ['Do# (C5#)', 61], -// ['Re (D5)', 62], -// ['Re# (D5#)', 63], -// ['Mi (E5)', 64], -// ['Fa (F5)', 65], -// ['Fa# (F5#)', 66], -// ['Sol (G5)', 67], -// ['Sol#(G5#)', 68], -// ['La (A5)', 69], -// ['La# (A5#)', 70], -// ['Ti (B5)', 71], -// ['Do (C6)', 72], -// ], -// }, -// }, -// }; -// } - -// getBlocks() { -// console.log('g1함수이름:getBlocks'); -// return { -// pingpong_lite_g1_when_button_pressed: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_event', -// statements: [], -// params: [ -// { -// type: 'Indicator', -// img: 'block_icon/start_icon_hardware.svg', -// size: 14, -// position: { x: 0, y: -2 }, -// }, -// ], -// events: {}, -// def: { -// params: [], -// type: 'pingpong_lite_g1_when_button_pressed', -// }, -// class: 'PingpongG1', -// isNotFor: ['PingpongG1Lite'], -// event: 'pp_when_button_pressed', -// func(sprite, script) { -// const pd = Entry.PingpongG1Lite.sensor_data; - -// if (pd.BUTTON == 1) { -// return script.callReturn(); -// } - -// return this.die(); -// }, -// }, -// pingpong_lite_g1_when_tilted: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_event', -// statements: [], -// params: [ -// { -// type: 'Indicator', -// img: 'block_icon/start_icon_hardware.svg', -// size: 14, -// position: { x: 0, y: -2 }, -// }, -// { -// type: 'Dropdown', -// options: Lang.Blocks.pingpong_opts_cube_tiltDir, -// value: 'F_CIRCLE', -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// ], -// events: {}, -// def: { -// params: [null], -// type: 'pingpong_lite_g1_when_tilted', -// }, -// paramsKeyMap: { -// TILT_DIR: 1, -// }, -// class: 'PingpongG1', -// isNotFor: ['PingpongG1Lite'], -// event: 'pp_when_tilted', -// func(sprite, script) { -// const tiltDir = script.getStringField('TILT_DIR'); -// const pd = Entry.PingpongG1Lite.sensor_data; - -// let tiltValue = 0; -// switch (tiltDir) { -// case 'F_CIRCLE': -// tiltValue = pd.TILT_X * -1; -// break; -// case 'B_TRIANGLE': -// tiltValue = pd.TILT_X; -// break; -// case 'L_RECTANGLE': -// tiltValue = pd.TILT_Y * -1; -// break; -// case 'R_STAR': -// tiltValue = pd.TILT_Y; -// break; -// default: -// break; -// } - -// if (tiltValue >= Entry.PingpongG1Lite.TILT_THRESHOLD) { -// return script.callReturn(); -// } else { -// return this.die(); -// } -// }, -// }, -// pingpong_lite_g1_is_button_pressed: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_boolean_field', -// def: { -// type: 'pingpong_lite_g1_is_button_pressed', -// }, -// class: 'PingpongG1', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// const pd = Entry.PingpongG1Lite.sensor_data; -// return pd.BUTTON == 1; -// }, -// }, -// pingpong_lite_g1_is_tilted: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_boolean_field', -// params: [ -// { -// type: 'Dropdown', -// options: Lang.Blocks.pingpong_opts_cube_tiltDir, -// value: 'F_CIRCLE', -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// ], -// def: { params: [], type: 'pingpong_lite_g1_is_tilted' }, -// paramsKeyMap: { -// TILT_DIR: 0, -// }, -// class: 'PingpongG1', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// const tiltDir = script.getStringField('TILT_DIR', script); -// const pd = Entry.PingpongG1Lite.sensor_data; -// let tiltValue = 0; -// switch (tiltDir) { -// case 'F_CIRCLE': -// tiltValue = pd.TILT_X * -1; -// break; -// case 'B_TRIANGLE': -// tiltValue = pd.TILT_X; -// break; -// case 'L_RECTANGLE': -// tiltValue = pd.TILT_Y * -1; -// break; -// case 'R_STAR': -// tiltValue = pd.TILT_Y; -// break; -// default: -// break; -// } -// return tiltValue >= Entry.PingpongG1Lite.TILT_THRESHOLD; -// }, -// }, -// pingpong_lite_g1_get_tilt_value: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_string_field', -// params: [ -// { -// type: 'Dropdown', -// options: Lang.Blocks.pingpong_opts_cube_tiltDir, -// value: 'F_CIRCLE', -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// ], -// events: { -// viewAdd: [ -// function() { -// //console.log('... viewAdd called!'); -// }, -// ], -// viewDestroy: [ -// function() { -// //console.log('... viewDestroy called!'); -// }, -// ], -// dataAdd: [ -// function(block) { -// //console.log(' ...... dataAdd called'); -// }, -// ], -// dataDestroy: [ -// function(block) { -// //console.log(' ...... dataDestroy called'); -// }, -// ], -// }, -// def: { -// params: [null], -// type: 'pingpong_lite_g1_get_tilt_value', -// }, -// paramsKeyMap: { DIR: 0 }, -// class: 'PingpongG1', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// const dir = script.getStringField('DIR', script); -// const pd = Entry.PingpongG1Lite.sensor_data; -// let value = 0; -// switch (dir) { -// case 'F_CIRCLE': -// value = pd.TILT_X * -1; -// break; -// case 'B_TRIANGLE': -// value = pd.TILT_X; -// break; -// case 'L_RECTANGLE': -// value = pd.TILT_Y * -1; -// break; -// case 'R_STAR': -// value = pd.TILT_Y; -// break; -// default: -// break; -// } -// return value; -// }, -// }, -// pingpong_lite_g1_get_sensor_value: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_string_field', -// params: [ -// { -// type: 'Dropdown', -// options: [ -// [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], -// [Lang.Blocks.pingpong_sensor_ain, 'AIN'], -// ], -// value: 'PROXIMITY', -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// ], -// def: { params: [], type: 'pingpong_lite_g1_get_sensor_value' }, -// paramsKeyMap: { SENSOR: 0 }, -// class: 'PingpongG1', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// const sensorType = script.getStringField('SENSOR', script); -// const pd = Entry.PingpongG1Lite.sensor_data; -// let value = 0; -// switch (sensorType) { -// case 'PROXIMITY': -// value = pd.PROXIMITY; -// break; -// case 'AIN': -// value = pd.AIN; -// break; -// default: -// break; -// } -// return value; -// }, -// }, -// pingpong_lite_g1_is_top_shape: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_boolean_field', -// statements: [], -// params: [ -// { -// type: 'Dropdown', -// options: Lang.Blocks.pingpong_opts_cube_dir6, -// value: 'DF_RECTANGLE', -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// ], -// events: {}, -// def: { params: [], type: 'pingpong_lite_g1_is_top_shape' }, -// paramsKeyMap: { -// TILT_DIR: 0, -// }, -// class: 'PingpongG1', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// const tiltDir = script.getStringField('TILT_DIR', script); -// const pd = Entry.PingpongG1Lite.sensor_data; - -// if (tiltDir == 'DF_RECTANGLE' && pd.TILT_Y > 70) return true; -// if (tiltDir == 'DB_STAR' && pd.TILT_Y < -70) return true; -// if (tiltDir == 'DR_CIRCLE' && pd.TILT_X > 70) return true; -// if (tiltDir == 'DL_TRIANGLE' && pd.TILT_X < -70) return true; -// if (tiltDir == 'DD_NONE' && pd.TILT_Z > 70) return true; -// if (tiltDir == 'DU_HEART' && pd.TILT_Z < -70) return true; -// return false; -// }, -// }, -// pingpong_lite_g1_motor_rotate: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { -// type: 'Dropdown', -// options: [ -// [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], -// [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], -// ], -// value: 'RIGHT', -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// { -// type: 'Block', -// accept: 'string', -// defaultType: 'number', -// }, -// { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, -// ], -// def: { -// params: [ -// null, -// { -// type: 'number', -// params: ['10'], -// }, -// ], -// type: 'pingpong_lite_g1_motor_rotate', -// }, -// paramsKeyMap: { DIR: 0, DEGREE: 1 }, -// class: 'PingpongG1_motor', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// const dir = script.getStringField('DIR'); -// let degree = script.getNumberValue('DEGREE'); - -// let speed = 800; -// if (dir == 'LEFT') { -// speed *= -1; -// } - -// degree = Math.min(Math.max(degree, 0), 5000); - -// let step = Math.round(degree * 5.5); -// if (step > 32768) { -// step = 32768; -// } - -// const opt = [2, 1, 0, 2, 0, 0, 0, 0, 0, 0]; -// const packet = Entry.PingpongG1Lite.makePacket(0xc1, 0x0004, opt); // SETP_MOTOR - -// packet.writeInt16BE(speed, 13); -// packet.writeUInt16BE(step, 17); - -// const waitTime = Math.round(((1100 - Math.abs(speed)) / 99) * step) + 400; - -// return [packet, waitTime]; -// }); -// }, -// }, - -// pingpong_lite_g1_start_motor_rotate: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { -// type: 'Block', -// accept: 'string', -// defaultType: 'number', -// }, -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { -// params: [ -// { -// type: 'number', -// params: ['100'], -// }, -// ], -// type: 'pingpong_lite_g1_start_motor_rotate', -// }, -// paramsKeyMap: { SPEED: 0 }, -// class: 'PingpongG1_motor', -// isNotFor: ['PingpongG1Lite'], -// func (sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// let speed = script.getNumberValue('SPEED'); -// if (speed > 100) { -// speed = 100; -// } -// if (speed < -100) { -// speed = -100; -// } - -// let sps = 0; -// if (speed != 0) { -// if (speed < 0) { -// sps = 65536 + (speed * 9 - 100); -// } else { -// sps = speed * 9 + 100; -// } -// sps = Math.round(sps); -// } - -// const opt = [2, 0, 0, 2, sps / 256, sps % 256]; -// const packet = Entry.PingpongG1Lite.makePacket(0xcc, 0x0004, opt); - -// const waitTime = Math.round(((1100 - Math.abs(sps)) / 99) * 10) + 400; -// return [packet, waitTime]; -// }); -// }, -// }, -// pingpong_lite_g1_stop_motor_rotate: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { -// params: [], -// type: 'pingpong_lite_g1_stop_motor_rotate', -// }, -// paramsKeyMap: {}, -// class: 'PingpongG1_motor', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// const opt = [2, 0, 0, 1, 0, 0]; -// const packet = Entry.PingpongG1Lite.makePacket(0xcc, 0x0004, opt); -// return [packet]; -// }); -// }, -// }, - -// pingpong_lite_g1_rotate_servo_mortor: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { -// type: 'Block', -// accept: 'string', -// defaultType: 'number', -// }, -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { params: [{ type: 'angle' }], type: 'pingpong_lite_g1_rotate_servo_mortor' }, -// paramsKeyMap: { DEGREE: 0 }, -// class: 'PingpongG1_motor', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// let angle = script.getNumberValue('DEGREE', script); - -// angle = Math.min(Math.max(angle, 0), 180); - -// const packet = Entry.PingpongG1Lite.makePacket(0xe1, 0x00, [2, 0, angle, 1]); -// return [packet, 400]; -// }); -// }, -// }, - -// pingpong_lite_g1_set_dot_pixel: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, -// { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, -// { -// type: 'Dropdown', -// options: [ -// [Lang.Blocks.pingpong_dot_on, 1], -// [Lang.Blocks.pingpong_dot_off, 0], -// ], -// value: 1, -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { -// params: [null, null, null], -// type: 'pingpong_lite_g1_set_dot_pixel', -// }, -// paramsKeyMap: { X: 0, Y: 1, onoff: 2 }, -// class: 'PingpongG1_peripheral_LED', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// let dotX = script.getNumberValue('X', script); -// let dotY = script.getNumberValue('Y', script); -// const onoff = script.getNumberField('onoff', script); - -// dotX = Math.min(Math.max(dotX, 0), 7); -// dotY = Math.min(Math.max(dotY, 0), 7); - -// const packet = Entry.PingpongG1Lite.makePacket(0xa2, 0xe1, [ -// 0x70, -// dotY, -// dotX, -// onoff, -// ]); // turn on -// return [packet,500]; -// }); -// }, -// }, -// pingpong_lite_g1_set_dot_string: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { type: 'Block', accept: 'string', value: 'Hello!' }, -// { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { params: [null, null], type: 'pingpong_lite_g1_set_dot_string' }, -// paramsKeyMap: { STR: 0, DURATION: 1 }, -// class: 'PingpongG1_peripheral_LED', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// const str = script.getStringValue('STR', script); -// const duration = script.getNumberValue('DURATION', script); - -// let period = Math.round((duration * 100) / (str.length * 8)); -// period = Math.min(Math.max(period, 1), 200); - -// const opt = Buffer.concat([ -// Buffer.from([0x70, period, 0]), -// Buffer.from(str.substring(0, 20)), -// ]); - -// const packet = Entry.PingpongG1Lite.makePacket(0xa2, 0xe3, opt); -// const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms -// return [packet, waitTime]; -// }); -// }, -// }, -// pingpong_lite_g1_set_dot_clear: { -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { params: [], type: 'pingpong_lite_g1_set_dot_clear' }, -// paramsKeyMap: {}, -// class: 'PingpongG1_peripheral_LED', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// const packet = Entry.PingpongG1Lite.makePacket(0xa2, 0xe3, [0x70, 1, 0, ' ']); -// return [packet, 400]; -// }); -// }, -// }, -// pingpong_lite_g1_playNoteForBeats: { -// //'%1 번 음을 %2 박자로 연주하기 %3', -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// statements: [], -// params: [ -// { -// type: 'Dropdown', -// options: Lang.Blocks.pingpong_opts_music_notes, -// value: 48, -// fontSize: 11, -// bgColor: EntryStatic.colorSet.block.darken.HARDWARE, -// arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, -// }, -// { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// events: {}, -// def: { params: [], type: 'pingpong_lite_g1_playNoteForBeats' }, -// paramsKeyMap: { NOTE: 0, BEATS: 1 }, -// class: 'PingpongG1_Music', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// const NOTE = script.getNumberField('NOTE', script); -// const BEATS = script.getNumberValue('BEATS', script); - -// const cBeats = Entry.PingpongG1Lite._clampBeats(BEATS); -// const durationSec = Entry.PingpongG1Lite._beatsToDuration(cBeats); - -// const waitTime = durationSec * 10 + 60; - -// const arr = Entry.PingpongG1Lite.makeMusicNotePacket(0, NOTE, durationSec); -// const packet = Buffer.from(arr); - -// return [packet, waitTime]; -// }); -// }, -// }, -// pingpong_lite_g1_restForBeats: { -// //'%1 박자 쉬기 %2', -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { params: [], type: 'pingpong_lite_g1_restForBeats' }, -// paramsKeyMap: { BEATS: 0 }, -// class: 'PingpongG1_Music', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.postCallReturn(script, () => { -// const BEATS = script.getNumberValue('BEATS', script); - -// const cBeats = Entry.PingpongG1Lite._clampBeats(BEATS); -// const durationSec = Entry.PingpongG1Lite._beatsToDuration(cBeats); - -// const waitTime = durationSec * 10 + 60; - -// return [null, waitTime]; -// }); -// }, -// }, -// pingpong_lite_g1_setTempo: { -// //'악보 빠르기를 %1 으로 정하기 %2', -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic', -// params: [ -// { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, -// { -// type: 'Indicator', -// img: 'block_icon/hardware_icon.svg', -// size: 12, -// }, -// ], -// def: { params: [], type: 'pingpong_lite_g1_setTempo' }, -// paramsKeyMap: { TEMPO: 0 }, -// class: 'PingpongG1_Music', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// let tempo = script.getNumberValue('TEMPO', script); -// Entry.PingpongG1Lite.tempo = Entry.PingpongG1Lite._clampTempo(tempo); -// return script.callReturn(); -// }, -// }, -// pingpong_lite_g1_getTempo: { -// //'악보 빠르기', -// color: EntryStatic.colorSet.block.default.HARDWARE, -// outerLine: EntryStatic.colorSet.block.darken.HARDWARE, -// skeleton: 'basic_string_field', -// params: [], -// def: { params: [], type: 'pingpong_lite_g1_getTempo' }, -// paramsKeyMap: {}, -// class: 'PingpongG1_Music', -// isNotFor: ['PingpongG1Lite'], -// func(sprite, script) { -// return Entry.PingpongG1Lite.tempo; -// }, -// }, -// }; -// } - -// })(); -// })(); - -// module.exports = Entry.PingpongG1Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG2_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG2_lite.js deleted file mode 100644 index 64dcdcd2f8..0000000000 --- a/src/playground/blocks/hardwareLite/block_pingpongG2_lite.js +++ /dev/null @@ -1,916 +0,0 @@ -'use strict'; - -import _range from 'lodash/range'; - -(function() { - const PingpongConnectLite = require('./block_pingpongConnect_lite'); - Entry.PingpongG2Lite = new (class PingpongG2Lite extends PingpongConnectLite { - constructor() { - super(2, 'PingpongG2Lite'); - this.id = '350201'; - this.imageName = 'PingpongG2Lite.png'; - - this.blockMenuBlocks = [ - 'pingpong_lite_g2_when_button_pressed', - 'pingpong_lite_g2_when_tilted', - 'pingpong_lite_g2_is_button_pressed', - 'pingpong_lite_g2_is_tilted', - 'pingpong_lite_g2_get_tilt_value', - 'pingpong_lite_g2_is_top_shape', - 'pingpong_lite_g2_get_sensor_value', - 'pingpong_lite_g2_multi_motor_rotate', - 'pingpong_lite_g2_motor_rotate', - 'pingpong_lite_g2_start_multi_motor_rotate', - 'pingpong_lite_g2_start_motor_rotate', - 'pingpong_lite_g2_stop_motor_rotate', - 'pingpong_lite_g2_rotate_servo_mortor', - 'pingpong_lite_g2_set_dot_pixel', - 'pingpong_lite_g2_set_dot_string', - 'pingpong_lite_g2_set_dot_clear', - 'pingpong_lite_g2_playNoteForBeats', - 'pingpong_lite_g2_playChordForBeats', - 'pingpong_lite_g2_restForBeats', - 'pingpong_lite_g2_setTempo', - 'pingpong_lite_g2_getTempo', - ]; - } - - getBlocks() { - const self = this; - return { - pingpong_lite_g2_when_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g2_when_button_pressed', - }, - paramsKeyMap: { - CUBEID: 1, - }, - class: 'PingpongG2', - isNotFor: ['PingpongG2Lite'], - event: 'pp_when_button_pressed', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const pd = self.sensor_data; - - return (pd[cubeId]?.BUTTON == 1) - ? script.callReturn() - : this.die(); - }, - }, - pingpong_lite_g2_when_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null, null], - type: 'pingpong_lite_g2_when_tilted', - }, - paramsKeyMap: { - CUBEID: 1, - TILT_DIR: 2, - }, - class: 'PingpongG2', - isNotFor: ['PingpongG2Lite'], - event: 'pp_when_tilted', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - if (tiltValue >= self.TILT_THRESHOLD) { - return script.callReturn(); - } - - return this.die(); - }, - }, - pingpong_lite_g2_is_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { - params: [null], - type: 'pingpong_lite_g2_is_button_pressed', - }, - paramsKeyMap: { - CUBEID: 0, - }, - class: 'PingpongG2', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - return self.sensor_data[cubeId].BUTTON == 1; - }, - }, - pingpong_lite_g2_is_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g2_is_tilted' }, - paramsKeyMap: { - CUBEID: 0, - TILT_DIR: 1, - }, - class: 'PingpongG2', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - - return tiltValue >= self.TILT_THRESHOLD; - }, - }, - pingpong_lite_g2_get_tilt_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g2_get_tilt_value', - }, - paramsKeyMap: { CUBEID: 0, DIR: 1 }, - class: 'PingpongG2', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('DIR', script); - - return self._getTiltValue(cubeId, tiltDir); - }, - }, - pingpong_lite_g2_get_sensor_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], - [Lang.Blocks.pingpong_sensor_ain, 'AIN'], - ], - value: 'PROXIMITY', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { params: [], type: 'pingpong_lite_g2_get_sensor_value' }, - paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, - class: 'PingpongG2', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const sensorType = script.getStringField('SENSOR', script); - const pd = self.sensor_data; - - return pd[cubeId][sensorType]; - }, - }, - pingpong_lite_g2_is_top_shape: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_dir6, - value: 'DF_RECTANGLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g2_is_top_shape' }, - paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, - class: 'PingpongG2', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR', script); - return self._isUpperDir(cubeId, tiltDir); - }, - }, - pingpong_lite_g2_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - def: { - params: [null], - type: 'pingpong_lite_g2_multi_motor_rotate', - }, - paramsKeyMap: { DIR_1: 0, DEGREE_1: 1, DIR_2: 2, DEGREE_2: 3 }, - class: 'PingpongG2_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const dir1 = script.getStringField('DIR_1'); - const dir2 = script.getStringField('DIR_2'); - const dir3 = script.getStringField('DIR_3'); - const dir4 = script.getStringField('DIR_4'); - const degree1 = script.getNumberValue('DEGREE_1'); - const degree2 = script.getNumberValue('DEGREE_2'); - const degree3 = script.getNumberValue('DEGREE_3'); - const degree4 = script.getNumberValue('DEGREE_4'); - - const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); - const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); - const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); - const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); - - const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); - const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); - const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); - const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); - - const opt = [self.MODE.MULTIROLE, 1, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - const waitTime = Math.max(delay1, delay2, delay3, delay4); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g2_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - events: {}, - def: { - params: [null, null, null], - type: 'pingpong_lite_g2_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, - class: 'PingpongG2_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const dir = script.getStringField('DIR'); - const degree = script.getNumberValue('DEGREE'); - const speed = 80 * (dir === 'LEFT' ? -1 : 1); - - const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); - const packet = Buffer.from(arr); - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g2_start_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g2_start_multi_motor_rotate', - }, - paramsKeyMap: { SPEED_1: 0, SPEED_2: 1 }, - class: 'PingpongG2_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const speed1 = script.getNumberValue('SPEED_1'); - const speed2 = script.getNumberValue('SPEED_2'); - const speed3 = script.getNumberValue('SPEED_3'); - const speed4 = script.getNumberValue('SPEED_4'); - - const arr1 = self.makeContStepPacket(0, speed1); - const arr2 = self.makeContStepPacket(1, speed2); - const arr3 = self.makeContStepPacket(2, speed3); - const arr4 = self.makeContStepPacket(3, speed4); - - const opt = [self.MODE.MULTIROLE, 0, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - - return [packet]; - }); - }, - }, - pingpong_lite_g2_start_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 100, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [null, null], - type: 'pingpong_lite_g2_start_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0, SPEED: 1 }, - class: 'PingpongG2_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const speed = script.getNumberValue('SPEED'); - - const arr = self.makeContStepPacket(cubeId, speed); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - pingpong_lite_g2_stop_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g2_stop_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0 }, - class: 'PingpongG2_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - - const arr = self.makeContStepPacket(cubeId, 0); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - pingpong_lite_g2_rotate_servo_mortor: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, { type: 'angle' }], type: 'pingpong_lite_g2_rotate_servo_mortor' }, - paramsKeyMap: { cubeno: 0, DEGREE: 1 }, - class: 'PingpongG2_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let angle = script.getNumberValue('DEGREE', script); - - angle = Math.min(Math.max(angle, 0), 180); - - const opt = [2, 0, angle, 1]; - const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); - return [packet]; - }); - }, - }, - pingpong_lite_g2_set_dot_pixel: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_dot_on, 1], - [Lang.Blocks.pingpong_dot_off, 0], - ], - value: 1, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { - params: [null, null, null], - type: 'pingpong_lite_g2_set_dot_pixel', - }, - paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, - class: 'PingpongG2_peripheral_LED', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let dotX = script.getNumberValue('X', script); - let dotY = script.getNumberValue('Y', script); - const onoff = script.getNumberField('onoff', script); - - dotX = Math.min(Math.max(dotX, 0), 7); - dotY = Math.min(Math.max(dotY, 0), 7); - - const opt = [0x70, dotY, dotX, onoff]; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe1, - cubeId, - opt - ); - return [packet]; - }); - }, - }, - pingpong_lite_g2_set_dot_string: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', value: 'Hello!' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, null], type: 'pingpong_lite_g2_set_dot_string' }, - paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, - class: 'PingpongG2_peripheral_LED', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const str = script.getStringValue('STR', script); - const duration = script.getNumberValue('DURATION', script); - - let period = Math.round((duration * 100) / (str.length * 8)); - period = Math.min(Math.max(period, 1), 200); - - const opt = Buffer.concat([ - Buffer.from([0x70, period, 0]), - Buffer.from(str.substring(0, 20)), - ]); - - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g2_set_dot_clear: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g2_set_dot_clear' }, - paramsKeyMap: { cubeno: 0 }, - class: 'PingpongG2_peripheral_LED', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const opt = [0x70, 1, 0, ' ']; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - return [packet, 400]; - }); - }, - }, - pingpong_lite_g2_playNoteForBeats: { - //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g2_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g2_playNoteForBeats' }, - paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, - class: 'PingpongG2_Music', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const NOTE = script.getNumberField('NOTE', script); - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); - const packet = Buffer.from(arr); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g2_playChordForBeats: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g2_playChordForBeats' }, - paramsKeyMap: { - NOTE_1: 0, - NOTE_2: 1, - BEATS: 2, - }, - class: 'PingpongG2_Music', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - console.log('NOTE_1',script.getNumberField('NOTE_1', script)); - console.log('dfljksadflkdsja',script.getNumberField('dfljksadflkdsja', script)); - const NOTE1 = script.getNumberField('NOTE_1', script); - const NOTE2 = script.getNumberField('NOTE_2', script); - const NOTE3 = script.getNumberField('NOTE_3', script); - const NOTE4 = script.getNumberField('NOTE_4', script); - - const BEATS = script.getNumberValue('BEATS', script); - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); - const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); - const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); - const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); - - const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g2_restForBeats: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g2_restForBeats' }, - paramsKeyMap: { BEATS: 0 }, - class: 'PingpongG2_Music', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - return [null, waitTime]; - }); - }, - }, - pingpong_lite_g2_setTempo: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g2_setTempo' }, - paramsKeyMap: { TEMPO: 0 }, - class: 'PingpongG2_Music', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - let tempo = script.getNumberValue('TEMPO', script); - self.tempo = self._clampTempo(tempo); - return script.callReturn(); - }, - }, - pingpong_lite_g2_getTempo: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [], - def: { params: [], type: 'pingpong_lite_g2_getTempo' }, - paramsKeyMap: {}, - class: 'PingpongG2_Music', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.tempo; - }, - }, - }; - } - })(); -})(); - -module.exports = Entry.PingpongG2Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG3_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG3_lite.js deleted file mode 100644 index ba7b2f9d2e..0000000000 --- a/src/playground/blocks/hardwareLite/block_pingpongG3_lite.js +++ /dev/null @@ -1,930 +0,0 @@ -'use strict'; - -import _range from 'lodash/range'; - -(function() { - const PingpongConnectLite = require('./block_pingpongConnect_lite'); - Entry.PingpongG3Lite = new (class PingpongG3Lite extends PingpongConnectLite { - constructor() { - super(3, 'PingpongG3Lite'); - this.id = '350301'; - this.imageName = 'PingpongG3Lite.png'; - - this.blockMenuBlocks = [ - 'pingpong_lite_g3_when_button_pressed', - 'pingpong_lite_g3_when_tilted', - 'pingpong_lite_g3_is_button_pressed', - 'pingpong_lite_g3_is_tilted', - 'pingpong_lite_g3_get_tilt_value', - 'pingpong_lite_g3_is_top_shape', - 'pingpong_lite_g3_get_sensor_value', - 'pingpong_lite_g3_multi_motor_rotate', - 'pingpong_lite_g3_motor_rotate', - 'pingpong_lite_g3_start_multi_motor_rotate', - 'pingpong_lite_g3_start_motor_rotate', - 'pingpong_lite_g3_stop_motor_rotate', - 'pingpong_lite_g3_rotate_servo_mortor', - 'pingpong_lite_g3_set_dot_pixel', - 'pingpong_lite_g3_set_dot_string', - 'pingpong_lite_g3_set_dot_clear', - 'pingpong_lite_g3_playNoteForBeats', - 'pingpong_lite_g3_playChordForBeats', - 'pingpong_lite_g3_restForBeats', - 'pingpong_lite_g3_setTempo', - 'pingpong_lite_g3_getTempo', - ]; - } - - getBlocks() { - const self = this; - return { - pingpong_lite_g3_when_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g3_when_button_pressed', - }, - paramsKeyMap: { - CUBEID: 1, - }, - class: 'PingpongG3', - isNotFor: ['PingpongG3Lite'], - event: 'pp_when_button_pressed', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const pd = self.sensor_data; - - return (pd[cubeId]?.BUTTON == 1) - ? script.callReturn() - : this.die(); - }, - }, - pingpong_lite_g3_when_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null, null], - type: 'pingpong_lite_g3_when_tilted', - }, - paramsKeyMap: { - CUBEID: 1, - TILT_DIR: 2, - }, - class: 'PingpongG3', - isNotFor: ['PingpongG3Lite'], - event: 'pp_when_tilted', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - if (tiltValue >= self.TILT_THRESHOLD) { - return script.callReturn(); - } - - return this.die(); - }, - }, - pingpong_lite_g3_is_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { - params: [null], - type: 'pingpong_lite_g3_is_button_pressed', - }, - paramsKeyMap: { - CUBEID: 0, - }, - class: 'PingpongG3', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - return self.sensor_data[cubeId].BUTTON == 1; - }, - }, - pingpong_lite_g3_is_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g3_is_tilted' }, - paramsKeyMap: { - CUBEID: 0, - TILT_DIR: 1, - }, - class: 'PingpongG3', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - - return tiltValue >= self.TILT_THRESHOLD; - }, - }, - pingpong_lite_g3_get_tilt_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g3_get_tilt_value', - }, - paramsKeyMap: { CUBEID: 0, DIR: 1 }, - class: 'PingpongG3', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('DIR', script); - - return self._getTiltValue(cubeId, tiltDir); - }, - }, - pingpong_lite_g3_get_sensor_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], - [Lang.Blocks.pingpong_sensor_ain, 'AIN'], - ], - value: 'PROXIMITY', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { params: [], type: 'pingpong_lite_g3_get_sensor_value' }, - paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, - class: 'PingpongG3', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const sensorType = script.getStringField('SENSOR', script); - const pd = self.sensor_data; - - return pd[cubeId][sensorType]; - }, - }, - pingpong_lite_g3_is_top_shape: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_dir6, - value: 'DF_RECTANGLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g3_is_top_shape' }, - paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, - class: 'PingpongG3', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR', script); - return self._isUpperDir(cubeId, tiltDir); - }, - }, - pingpong_lite_g3_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - def: { - params: [], - type: 'pingpong_lite_g3_multi_motor_rotate', - }, - paramsKeyMap: { - DIR_1: 0, - DEGREE_1: 1, - DIR_2: 2, - DEGREE_2: 3, - DIR_3: 4, - DEGREE_3: 5, - }, - class: 'PingpongG3_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const dir1 = script.getStringField('DIR_1'); - const dir2 = script.getStringField('DIR_2'); - const dir3 = script.getStringField('DIR_3'); - const dir4 = script.getStringField('DIR_4'); - const degree1 = script.getNumberValue('DEGREE_1'); - const degree2 = script.getNumberValue('DEGREE_2'); - const degree3 = script.getNumberValue('DEGREE_3'); - const degree4 = script.getNumberValue('DEGREE_4'); - - const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); - const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); - const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); - const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); - - const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); - const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); - const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); - const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); - - const opt = [self.MODE.MULTIROLE, 1, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - const waitTime = Math.max(delay1, delay2, delay3, delay4); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g3_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - events: {}, - def: { - params: [], - type: 'pingpong_lite_g3_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, - class: 'PingpongG3_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const dir = script.getStringField('DIR'); - const degree = script.getNumberValue('DEGREE'); - const speed = 80 * (dir === 'LEFT' ? -1 : 1); - - const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); - const packet = Buffer.from(arr); - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g3_start_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g3_start_multi_motor_rotate', - }, - paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2 }, - class: 'PingpongG3_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const speed1 = script.getNumberValue('SPEED_1'); - const speed2 = script.getNumberValue('SPEED_2'); - const speed3 = script.getNumberValue('SPEED_3'); - const speed4 = script.getNumberValue('SPEED_4'); - - const arr1 = self.makeContStepPacket(0, speed1); - const arr2 = self.makeContStepPacket(1, speed2); - const arr3 = self.makeContStepPacket(2, speed3); - const arr4 = self.makeContStepPacket(3, speed4); - - const opt = [self.MODE.MULTIROLE, 0, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - - return [packet]; - }); - }, - }, - pingpong_lite_g3_start_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 100, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [null, null], - type: 'pingpong_lite_g3_start_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0, SPEED: 1 }, - class: 'PingpongG3_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const speed = script.getNumberValue('SPEED'); - - const arr = self.makeContStepPacket(cubeId, speed); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - pingpong_lite_g3_stop_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g3_stop_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0 }, - class: 'PingpongG3_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - - const arr = self.makeContStepPacket(cubeId, 0); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - pingpong_lite_g3_rotate_servo_mortor: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', defaultType: 'number' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, { type: 'angle' }], type: 'pingpong_lite_g3_rotate_servo_mortor' }, - paramsKeyMap: { cubeno: 0, DEGREE: 1 }, - class: 'PingpongG3_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let angle = script.getNumberValue('DEGREE', script); - - angle = Math.min(Math.max(angle, 0), 180); - - const opt = [2, 0, angle, 1]; - const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); - return [packet]; - }); - }, - }, - pingpong_lite_g3_set_dot_pixel: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_dot_on, 1], - [Lang.Blocks.pingpong_dot_off, 0], - ], - value: 1, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { - params: [null, null, null], - type: 'pingpong_lite_g3_set_dot_pixel', - }, - paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, - class: 'PingpongG3_peripheral_LED', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let dotX = script.getNumberValue('X', script); - let dotY = script.getNumberValue('Y', script); - const onoff = script.getNumberField('onoff', script); - - dotX = Math.min(Math.max(dotX, 0), 7); - dotY = Math.min(Math.max(dotY, 0), 7); - - const opt = [0x70, dotY, dotX, onoff]; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe1, - cubeId, - opt - ); - return [packet]; - }); - }, - }, - pingpong_lite_g3_set_dot_string: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', value: 'Hello!' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, null], type: 'pingpong_lite_g3_set_dot_string' }, - paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, - class: 'PingpongG3_peripheral_LED', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const str = script.getStringValue('STR', script); - const duration = script.getNumberValue('DURATION', script); - - let period = Math.round((duration * 100) / (str.length * 8)); - period = Math.min(Math.max(period, 1), 200); - - const opt = Buffer.concat([ - Buffer.from([0x70, period, 0]), - Buffer.from(str.substring(0, 20)), - ]); - - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g3_set_dot_clear: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g3_set_dot_clear' }, - paramsKeyMap: { cubeno: 0 }, - class: 'PingpongG3_peripheral_LED', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const opt = [0x70, 1, 0, ' ']; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - return [packet, 400]; - }); - }, - }, - pingpong_lite_g3_playNoteForBeats: { - //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g3_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g3_playNoteForBeats' }, - paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, - class: 'PingpongG3_Music', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const NOTE = script.getNumberField('NOTE', script); - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); - const packet = Buffer.from(arr); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g3_playChordForBeats: { - //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g3_playChordForBeats' }, - paramsKeyMap: { - NOTE_1: 0, - NOTE_2: 1, - NOTE_3: 2, - BEATS: 3, - }, - class: 'PingpongG3_Music', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const NOTE1 = script.getNumberField('NOTE_1', script); - const NOTE2 = script.getNumberField('NOTE_2', script); - const NOTE3 = script.getNumberField('NOTE_3', script); - const NOTE4 = script.getNumberField('NOTE_4', script); - - const BEATS = script.getNumberValue('BEATS', script); - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); - const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); - const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); - const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); - - const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g3_restForBeats: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g3_restForBeats' }, - paramsKeyMap: { BEATS: 0 }, - class: 'PingpongG3_Music', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - return [null, waitTime]; - }); - }, - }, - pingpong_lite_g3_setTempo: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g3_setTempo' }, - paramsKeyMap: { TEMPO: 0 }, - class: 'PingpongG3_Music', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - let tempo = script.getNumberValue('TEMPO', script); - self.tempo = self._clampTempo(tempo); - return script.callReturn(); - }, - }, - pingpong_lite_g3_getTempo: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [], - def: { params: [], type: 'pingpong_lite_g3_getTempo' }, - paramsKeyMap: {}, - class: 'PingpongG3_Music', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.tempo; - }, - }, - }; - } - })(); -})(); - -module.exports = Entry.PingpongG3Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongG4_lite.js b/src/playground/blocks/hardwareLite/block_pingpongG4_lite.js deleted file mode 100644 index 63242f06a6..0000000000 --- a/src/playground/blocks/hardwareLite/block_pingpongG4_lite.js +++ /dev/null @@ -1,949 +0,0 @@ -'use strict'; - -import _range from 'lodash/range'; - -(function() { - const PingpongConnectLite = require('./block_pingpongConnect_lite'); - Entry.PingpongG4Lite = new (class PingpongG4Lite extends PingpongConnectLite { - constructor() { - super(4, 'PingpongG4Lite'); - this.id = '350401'; - this.imageName = 'PingpongG4Lite.png'; - - this.blockMenuBlocks = [ - 'pingpong_lite_g4_when_button_pressed', - 'pingpong_lite_g4_when_tilted', - 'pingpong_lite_g4_is_button_pressed', - 'pingpong_lite_g4_is_tilted', - 'pingpong_lite_g4_get_tilt_value', - 'pingpong_lite_g4_is_top_shape', - 'pingpong_lite_g4_get_sensor_value', - 'pingpong_lite_g4_multi_motor_rotate', - 'pingpong_lite_g4_motor_rotate', - 'pingpong_lite_g4_start_multi_motor_rotate', - 'pingpong_lite_g4_start_motor_rotate', - 'pingpong_lite_g4_stop_motor_rotate', - 'pingpong_lite_g4_rotate_servo_mortor', - 'pingpong_lite_g4_set_dot_pixel', - 'pingpong_lite_g4_set_dot_string', - 'pingpong_lite_g4_set_dot_clear', - 'pingpong_lite_g4_playNoteForBeats', - 'pingpong_lite_g4_playChordForBeats', - 'pingpong_lite_g4_restForBeats', - 'pingpong_lite_g4_setTempo', - 'pingpong_lite_g4_getTempo', - ]; - } - - getBlocks() { - const self = this; - return { - pingpong_lite_g4_when_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g4_when_button_pressed', - }, - paramsKeyMap: { - CUBEID: 1, - }, - class: 'PingpongG4', - isNotFor: ['PingpongG4Lite'], - event: 'pp_when_button_pressed', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const pd = self.sensor_data; - - return (pd[cubeId]?.BUTTON == 1) - ? script.callReturn() - : this.die(); - }, - }, - pingpong_lite_g4_when_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null, null], - type: 'pingpong_lite_g4_when_tilted', - }, - paramsKeyMap: { - CUBEID: 1, - TILT_DIR: 2, - }, - class: 'PingpongG4', - isNotFor: ['PingpongG4Lite'], - event: 'pp_when_tilted', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - if (tiltValue >= self.TILT_THRESHOLD) { - return script.callReturn(); - } - - return this.die(); - }, - }, - pingpong_lite_g4_is_button_pressed: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { - params: [null], - type: 'pingpong_lite_g4_is_button_pressed', - }, - paramsKeyMap: { - CUBEID: 0, - }, - class: 'PingpongG4', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - return self.sensor_data[cubeId].BUTTON == 1; - }, - }, - pingpong_lite_g4_is_tilted: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g4_is_tilted' }, - paramsKeyMap: { - CUBEID: 0, - TILT_DIR: 1, - }, - class: 'PingpongG4', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - - return tiltValue >= self.TILT_THRESHOLD; - }, - }, - pingpong_lite_g4_get_tilt_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: 'pingpong_lite_g4_get_tilt_value', - }, - paramsKeyMap: { CUBEID: 0, DIR: 1 }, - class: 'PingpongG4', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('DIR', script); - - return self._getTiltValue(cubeId, tiltDir); - }, - }, - pingpong_lite_g4_get_sensor_value: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], - [Lang.Blocks.pingpong_sensor_ain, 'AIN'], - ], - value: 'PROXIMITY', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { params: [], type: 'pingpong_lite_g4_get_sensor_value' }, - paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, - class: 'PingpongG4', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const sensorType = script.getStringField('SENSOR', script); - const pd = self.sensor_data; - - return pd[cubeId][sensorType]; - }, - }, - pingpong_lite_g4_is_top_shape: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_dir6, - value: 'DF_RECTANGLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g4_is_top_shape' }, - paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, - class: 'PingpongG4', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR', script); - return self._isUpperDir(cubeId, tiltDir); - }, - }, - pingpong_lite_g4_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - def: { - params: [], - type: 'pingpong_lite_g4_multi_motor_rotate', - }, - paramsKeyMap: { - DIR_1: 0, - DEGREE_1: 1, - DIR_2: 2, - DEGREE_2: 3, - DIR_3: 4, - DEGREE_3: 5, - DIR_4: 6, - DEGREE_4: 7, - }, - class: 'PingpongG4_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const dir1 = script.getStringField('DIR_1'); - const dir2 = script.getStringField('DIR_2'); - const dir3 = script.getStringField('DIR_3'); - const dir4 = script.getStringField('DIR_4'); - const degree1 = script.getNumberValue('DEGREE_1'); - const degree2 = script.getNumberValue('DEGREE_2'); - const degree3 = script.getNumberValue('DEGREE_3'); - const degree4 = script.getNumberValue('DEGREE_4'); - - const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); - const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); - const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); - const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); - - const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); - const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); - const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); - const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); - - const opt = [self.MODE.MULTIROLE, 1, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - const waitTime = Math.max(delay1, delay2, delay3, delay4); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g4_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - events: {}, - def: { - params: [], - type: 'pingpong_lite_g4_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, - class: 'PingpongG4_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const dir = script.getStringField('DIR'); - const degree = script.getNumberValue('DEGREE'); - const speed = 80 * (dir === 'LEFT' ? -1 : 1); - - const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); - const packet = Buffer.from(arr); - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g4_start_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g4_start_multi_motor_rotate', - }, - paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2, SPEED_4: 3 }, - class: 'PingpongG4_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const speed1 = script.getNumberValue('SPEED_1'); - const speed2 = script.getNumberValue('SPEED_2'); - const speed3 = script.getNumberValue('SPEED_3'); - const speed4 = script.getNumberValue('SPEED_4'); - - const arr1 = self.makeContStepPacket(0, speed1); - const arr2 = self.makeContStepPacket(1, speed2); - const arr3 = self.makeContStepPacket(2, speed3); - const arr4 = self.makeContStepPacket(3, speed4); - - const opt = [self.MODE.MULTIROLE, 0, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - - return [packet]; - }); - }, - }, - pingpong_lite_g4_start_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 100, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [null, null], - type: 'pingpong_lite_g4_start_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0, SPEED: 1 }, - class: 'PingpongG4_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const speed = script.getNumberValue('SPEED'); - - const arr = self.makeContStepPacket(cubeId, speed); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - pingpong_lite_g4_stop_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_all, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g4_stop_motor_rotate', - }, - paramsKeyMap: { CUBEID: 0 }, - class: 'PingpongG4_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - - const arr = self.makeContStepPacket(cubeId, 0); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - pingpong_lite_g4_rotate_servo_mortor: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', defaultType: 'number' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, { type: 'angle' }], type: 'pingpong_lite_g4_rotate_servo_mortor' }, - paramsKeyMap: { cubeno: 0, DEGREE: 1 }, - class: 'PingpongG4_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let angle = script.getNumberValue('DEGREE', script); - - angle = Math.min(Math.max(angle, 0), 180); - - const opt = [2, 0, angle, 1]; - const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); - return [packet]; - }); - }, - }, - pingpong_lite_g4_set_dot_pixel: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_dot_on, 1], - [Lang.Blocks.pingpong_dot_off, 0], - ], - value: 1, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { - params: [null, null, null], - type: 'pingpong_lite_g4_set_dot_pixel', - }, - paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, - class: 'PingpongG4_peripheral_LED', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let dotX = script.getNumberValue('X', script); - let dotY = script.getNumberValue('Y', script); - const onoff = script.getNumberField('onoff', script); - - dotX = Math.min(Math.max(dotX, 0), 7); - dotY = Math.min(Math.max(dotY, 0), 7); - - const opt = [0x70, dotY, dotX, onoff]; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe1, - cubeId, - opt - ); - return [packet]; - }); - }, - }, - pingpong_lite_g4_set_dot_string: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', value: 'Hello!' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, null], type: 'pingpong_lite_g4_set_dot_string' }, - paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, - class: 'PingpongG4_peripheral_LED', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const str = script.getStringValue('STR', script); - const duration = script.getNumberValue('DURATION', script); - - let period = Math.round((duration * 100) / (str.length * 8)); - period = Math.min(Math.max(period, 1), 200); - - const opt = Buffer.concat([ - Buffer.from([0x70, period, 0]), - Buffer.from(str.substring(0, 20)), - ]); - - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g4_set_dot_clear: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g4_set_dot_clear' }, - paramsKeyMap: { cubeno: 0 }, - class: 'PingpongG4_peripheral_LED', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const opt = [0x70, 1, 0, ' ']; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - return [packet, 400]; - }); - }, - }, - pingpong_lite_g4_playNoteForBeats: { - //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_lite_g4_cube_id, - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g4_playNoteForBeats' }, - paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, - class: 'PingpongG4_Music', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const NOTE = script.getNumberField('NOTE', script); - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); - const packet = Buffer.from(arr); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g4_playChordForBeats: { - //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g4_playChordForBeats' }, - paramsKeyMap: { - NOTE_1: 0, - NOTE_2: 1, - NOTE_3: 2, - NOTE_4: 3, - BEATS: 4, - }, - class: 'PingpongG4_Music', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const NOTE1 = script.getNumberField('NOTE_1', script); - const NOTE2 = script.getNumberField('NOTE_2', script); - const NOTE3 = script.getNumberField('NOTE_3', script); - const NOTE4 = script.getNumberField('NOTE_4', script); - - const BEATS = script.getNumberValue('BEATS', script); - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); - const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); - const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); - const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); - - const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g4_restForBeats: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g4_restForBeats' }, - paramsKeyMap: { BEATS: 0 }, - class: 'PingpongG4_Music', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - return [null, waitTime]; - }); - }, - }, - pingpong_lite_g4_setTempo: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: 'pingpong_lite_g4_setTempo' }, - paramsKeyMap: { TEMPO: 0 }, - class: 'PingpongG4_Music', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - let tempo = script.getNumberValue('TEMPO', script); - self.tempo = self._clampTempo(tempo); - return script.callReturn(); - }, - }, - pingpong_lite_g4_getTempo: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [], - def: { params: [], type: 'pingpong_lite_g4_getTempo' }, - paramsKeyMap: {}, - class: 'PingpongG4_Music', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.tempo; - }, - }, - }; - } - })(); -})(); - -module.exports = Entry.PingpongG4Lite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js b/src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js new file mode 100644 index 0000000000..a07756e87b --- /dev/null +++ b/src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js @@ -0,0 +1,50 @@ +'use strict'; + +import _range from 'lodash/range'; + +(function() { + const PingpongConnectLite = require('./block_pingpongConnect_lite'); + + function createBlockMenuList(cubeCount) { + return [ + `pingpong_lite_g${cubeCount}_when_button_pressed`, + `pingpong_lite_g${cubeCount}_when_tilted`, + `pingpong_lite_g${cubeCount}_is_button_pressed`, + `pingpong_lite_g${cubeCount}_is_tilted`, + `pingpong_lite_g${cubeCount}_get_tilt_value`, + `pingpong_lite_g${cubeCount}_is_top_shape`, + `pingpong_lite_g${cubeCount}_get_sensor_value`, + `pingpong_lite_g${cubeCount}_motor_rotate`, + `pingpong_lite_g${cubeCount}_start_motor_rotate`, + `pingpong_lite_g${cubeCount}_stop_motor_rotate`, + `pingpong_lite_g${cubeCount}_multi_motor_rotate`, + `pingpong_lite_g${cubeCount}_multi_start_motor_rotate`, + `pingpong_lite_g${cubeCount}_rotate_servo_mortor`, + `pingpong_lite_g${cubeCount}_set_dot_pixel`, + `pingpong_lite_g${cubeCount}_set_dot_string`, + `pingpong_lite_g${cubeCount}_set_dot_clear`, + `pingpong_lite_g${cubeCount}_playNoteForBeats`, + `pingpong_lite_g${cubeCount}_multi_playChordForBeats`, + `pingpong_lite_g${cubeCount}_restForBeats`, + `pingpong_lite_g${cubeCount}_setTempo`, + `pingpong_lite_g${cubeCount}_getTempo`, + ]; + } + + function createPingpongLite(cubeCount, className, hardwareId, imageName) { + return new (class extends PingpongConnectLite { + constructor() { + super(cubeCount, className); + this.id = hardwareId; + this.imageName = imageName; + this.blockMenuBlocks = createBlockMenuList(this.cubeCount); + } + })(); + } + + Entry.PingpongG2Lite = createPingpongLite(2, 'PingpongG2Lite', '350201', 'PingpongG2Lite.png'); + Entry.PingpongG3Lite = createPingpongLite(3, 'PingpongG3Lite', '350301', 'PingpongG3Lite.png'); + Entry.PingpongG4Lite = createPingpongLite(4, 'PingpongG4Lite', '350401', 'PingpongG4Lite.png'); +})(); + +module.exports = [Entry.PingpongG2Lite, Entry.PingpongG3Lite, Entry.PingpongG4Lite]; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongPracticalarts_lite.js b/src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js similarity index 90% rename from src/playground/blocks/hardwareLite/block_pingpongPracticalarts_lite.js rename to src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js index 8b3601c5d8..9a4da8a6d9 100644 --- a/src/playground/blocks/hardwareLite/block_pingpongPracticalarts_lite.js +++ b/src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js @@ -4,38 +4,38 @@ import _range from 'lodash/range'; (function() { const PingpongConnectLite = require('./block_pingpongConnect_lite'); - Entry.PingpongPracticalartsLite = new (class PingpongPracticalartsLite extends PingpongConnectLite { + Entry.PingpongG1Lite = new (class PingpongG1Lite extends PingpongConnectLite { constructor() { - super(1,'PingpongPracticalartsLite'); - this.id = '350501'; - this.imageName = 'PingpongPracticalartsLite.png'; + super(1,'PingpongG1Lite'); + this.id = '350101'; + this.imageName = 'PingpongG1Lite.png'; this.blockMenuBlocks = [ - 'pingpong_g1_when_button_pressed', - 'pingpong_g1_is_button_pressed', - 'pingpong_g1_when_tilted', - 'pingpong_g1_is_tilted', - 'pingpong_g1_get_tilt_value', - 'pingpong_g1_is_top_shape', - 'pingpong_g1_get_sensor_value', - 'pingpong_g1_motor_rotate', - 'pingpong_g1_start_motor_rotate', - 'pingpong_g1_stop_motor_rotate', - 'pingpong_g1_rotate_servo_mortor', - 'set_steering_direction', - 'move_by_distance', - 'pingpong_g1_set_dot_pixel', - 'pingpong_g1_set_dot_string', - 'pingpong_g1_set_dot_clear', - 'pingpong_g1_playNoteForBeats', - 'pingpong_g1_restForBeats', - 'pingpong_g1_setTempo', - 'pingpong_g1_getTempo', + 'pingpong_lite_g1_when_button_pressed', + 'pingpong_lite_g1_when_tilted', + 'pingpong_lite_g1_is_button_pressed', + 'pingpong_lite_g1_is_tilted', + 'pingpong_lite_g1_get_tilt_value', + 'pingpong_lite_g1_get_sensor_value', + 'pingpong_lite_g1_is_top_shape', + 'pingpong_lite_g1_motor_rotate', + 'pingpong_lite_g1_start_motor_rotate', + 'pingpong_lite_g1_stop_motor_rotate', + 'pingpong_lite_g1_rotate_servo_mortor', + 'pingpong_lite_g1_set_dot_pixel', + 'pingpong_lite_g1_set_dot_string', + 'pingpong_lite_g1_set_dot_clear', + 'pingpong_lite_g1_playNoteForBeats', + 'pingpong_lite_g1_restForBeats', + 'pingpong_lite_g1_setTempo', + 'pingpong_lite_g1_getTempo', ]; } + + //#region getBlocks getBlocks() { const self = this; - return { + const blocks = { pingpong_lite_g1_when_button_pressed: { color: EntryStatic.colorSet.block.default.HARDWARE, outerLine: EntryStatic.colorSet.block.darken.HARDWARE, @@ -618,17 +618,14 @@ import _range from 'lodash/range'; color: EntryStatic.colorSet.block.default.HARDWARE, outerLine: EntryStatic.colorSet.block.darken.HARDWARE, skeleton: 'basic_string_field', - params: [], def: { params: [], type: 'pingpong_lite_g1_getTempo' }, - paramsKeyMap: {}, class: 'PingpongG1_Music', isNotFor: ['PingpongG1Lite', 'PingpongPracticalartsLite'], func(sprite, script) { return self.tempo; }, }, - - set_steering_direction: { + pingpong_lite_art_set_steering_direction: { color: EntryStatic.colorSet.block.default.HARDWARE, outerLine: EntryStatic.colorSet.block.darken.HARDWARE, skeleton: 'basic', @@ -644,23 +641,22 @@ import _range from 'lodash/range'; { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, ], events: {}, - def: { params: [], type: 'set_steering_direction' }, + def: { params: [], type: 'pingpong_lite_art_set_steering_direction' }, paramsKeyMap: { DEGREE: 0,}, class: 'PingpongG1_motor', - isNotFor: ['PingpongPracticalArtsLite'], + isNotFor: ['PingpongPracticalartsLite'], func(sprite, script) { return self.postCallReturn(script, () => { let angle = script.getNumberValue('DEGREE', script); angle = Math.min(Math.max(angle, 0), 180); - const packet = self.makePacket(0xe1, 0x00, [2, 0, angle, 1]); + const packet = self.makePacket(0xe1, 0x00, 0, [2, 0, angle, 1]); return [packet, 400]; }); }, }, - - move_by_distance: { + pingpong_lite_art_move_by_distance: { color: EntryStatic.colorSet.block.default.HARDWARE, outerLine: EntryStatic.colorSet.block.darken.HARDWARE, skeleton: 'basic', @@ -677,42 +673,84 @@ import _range from 'lodash/range'; { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12, }, ], events: {}, - def: { params: [], type: 'move_by_distance' }, + def: { params: [], type: 'pingpong_lite_art_move_by_distance' }, paramsKeyMap: { DIR: 0, CM: 1 }, class: 'PingpongG1_motor', - isNotFor: ['PingpongPracticalArtsLite'], + isNotFor: ['PingpongPracticalartsLite'], func(sprite, script) { return self.postCallReturn(script, () => { const direction = script.getStringField('DIR'); - let degree = script.getNumberValue('CM'); - degree = degree*9; // convert cm to degree - - let speed = 800; - if (direction == 'back') { - speed *= -1; - } + const distanceCm = script.getNumberValue('CM'); + + const degreePerCm = 9; + const baseSpeed = 800; + const maxDegree = 5000; + const stepRatio = 5.5; + const maxStep = 32768; + const waitTimeOffset = 400; + + const degree = distanceCm * degreePerCm; + const clampedDegree = Math.min(Math.max(degree, 0), maxDegree); + const speed = direction === 'back' ? -baseSpeed : baseSpeed; + + const step = Math.min( + Math.round(clampedDegree * stepRatio), + maxStep + ); - degree = Math.min(Math.max(degree, 0), 5000); + const options = [2, 1, 0, 2, 0, 0, 0, 0, 0, 0]; + const packet = self.makePacket(0xc1, 0x0004, 0, options); // STEP_MOTOR - let step = Math.round(degree * 5.5); - if (step > 32768) { - step = 32768; - } + packet[13] = (speed >> 8) & 0xff; + packet[14] = speed & 0xff; - const opt = [2, 1, 0, 2, 0, 0, 0, 0, 0, 0]; - const packet = self.makePacket(0xc1, 0x0004, opt); // SETP_MOTOR + packet[17] = (step >> 8) & 0xff; + packet[18] = step & 0xff; - packet.writeInt16BE(speed, 13); - packet.writeUInt16BE(step, 17); + const waitTime = + Math.round(((1100 - Math.abs(speed)) / 99) * step) + waitTimeOffset; - const waitTime = Math.round(((1100 - Math.abs(speed)) / 99) * step) + 400; return [packet, waitTime]; }); }, }, }; + console.log(this.name,'blocks',blocks); + return blocks; + } + + })(); + Entry.PingpongPracticalartsLite = new (class PingpongPracticalartsLite extends PingpongConnectLite { + constructor() { + super(1,'PingpongPracticalartsLite'); + this.id = '350501'; + this.imageName = 'PingpongPracticalartsLite.png'; + + this.blockMenuBlocks = [ + 'pingpong_g1_when_button_pressed', + 'pingpong_g1_is_button_pressed', + 'pingpong_g1_when_tilted', + 'pingpong_g1_is_tilted', + 'pingpong_g1_get_tilt_value', + 'pingpong_g1_is_top_shape', + 'pingpong_g1_get_sensor_value', + 'pingpong_g1_motor_rotate', + 'pingpong_g1_start_motor_rotate', + 'pingpong_g1_stop_motor_rotate', + 'pingpong_g1_rotate_servo_mortor', + 'pingpong_lite_art_set_steering_direction', + 'pingpong_lite_art_move_by_distance', + 'pingpong_g1_set_dot_pixel', + 'pingpong_g1_set_dot_string', + 'pingpong_g1_set_dot_clear', + 'pingpong_g1_playNoteForBeats', + 'pingpong_g1_restForBeats', + 'pingpong_g1_setTempo', + 'pingpong_g1_getTempo', + ]; } + getBlocks() {return Entry.PingpongG1Lite.getBlocks();} })(); })(); -module.exports = Entry.PingpongPracticalartsLite; \ No newline at end of file +module.exports = [Entry.PingpongG1Lite, Entry.PingpongPracticalartsLite]; \ No newline at end of file From 759e2b07a0e8cc901fde818ffb79c1a8f61273de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=A7=84=ED=98=B8?= <140678689+jinbaekho@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:21:39 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20getBlocks=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block_pingpongConnect_lite.js | 1303 ----------------- .../hardwareLite/block_pingpongMulti_lite.js | 1301 ++++++++++++++++ 2 files changed, 1301 insertions(+), 1303 deletions(-) diff --git a/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js b/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js index a3d3299962..0694d07d30 100644 --- a/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js +++ b/src/playground/blocks/hardwareLite/block_pingpongConnect_lite.js @@ -947,1311 +947,8 @@ class PingpongConnectLite { }, } }; - console.log(this.name,'lang',lang); return lang; } - - //#region getBlocks - getBlocks() { - const self = this; - const blocks = { - [`pingpong_lite_g${self.cubeCount}_when_button_pressed`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: `pingpong_lite_g${self.cubeCount}_when_button_pressed`, - }, - paramsKeyMap: { - CUBEID: 1, - }, - class: 'PingpongGN', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - event: 'pp_when_button_pressed', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const pd = self.sensor_data; - - return (pd[cubeId]?.BUTTON == 1) - ? script.callReturn() - : this.die(); - }, - }, - [`pingpong_lite_g${self.cubeCount}_when_tilted`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_event', - statements: [], - params: [ - { - type: 'Indicator', - img: 'block_icon/start_icon_hardware.svg', - size: 14, - position: { x: 0, y: -2 }, - }, - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null, null], - type: `pingpong_lite_g${self.cubeCount}_when_tilted`, - }, - paramsKeyMap: { - CUBEID: 1, - TILT_DIR: 2, - }, - class: 'PingpongGN', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - event: 'pp_when_tilted', - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - if (tiltValue >= self.TILT_THRESHOLD) { - return script.callReturn(); - } - - return this.die(); - }, - }, - [`pingpong_lite_g${self.cubeCount}_is_button_pressed`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { - params: [null], - type: `pingpong_lite_g${self.cubeCount}_is_button_pressed`, - }, - paramsKeyMap: { - CUBEID: 0, - }, - class: 'PingpongGN', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - return self.sensor_data[cubeId].BUTTON == 1; - }, - }, - [`pingpong_lite_g${self.cubeCount}_is_tilted`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_is_tilted` }, - paramsKeyMap: { - CUBEID: 0, - TILT_DIR: 1, - }, - class: 'PingpongGN', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR'); - - const tiltValue = self._getTiltValue(cubeId, tiltDir); - - return tiltValue >= self.TILT_THRESHOLD; - }, - }, - [`pingpong_lite_g${self.cubeCount}_get_tilt_value`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_tiltDir, - value: 'F_CIRCLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { - params: [null], - type: `pingpong_lite_g${self.cubeCount}_get_tilt_value`, - }, - paramsKeyMap: { CUBEID: 0, DIR: 1 }, - class: 'PingpongGN', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('DIR', script); - - return self._getTiltValue(cubeId, tiltDir); - }, - }, - [`pingpong_lite_g${self.cubeCount}_get_sensor_value`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], - [Lang.Blocks.pingpong_sensor_ain, 'AIN'], - ], - value: 'PROXIMITY', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_get_sensor_value` }, - paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, - class: 'PingpongGN', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const sensorType = script.getStringField('SENSOR', script); - const pd = self.sensor_data; - - return pd[cubeId][sensorType]; - }, - }, - [`pingpong_lite_g${self.cubeCount}_is_top_shape`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_boolean_field', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_cube_dir6, - value: 'DF_RECTANGLE', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - ], - events: {}, - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_is_top_shape` }, - paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, - class: 'PingpongGN', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - const cubeId = script.getNumberField('CUBEID'); - const tiltDir = script.getStringField('TILT_DIR', script); - return self._isUpperDir(cubeId, tiltDir); - }, - }, - [`pingpong_lite_g${self.cubeCount}_motor_rotate`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - events: {}, - def: { - params: [null, null, null], - type: `pingpong_lite_g${self.cubeCount}_motor_rotate`, - }, - paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, - class: 'PingpongGN_motor', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const dir = script.getStringField('DIR'); - const degree = script.getNumberValue('DEGREE'); - const speed = 80 * (dir === 'LEFT' ? -1 : 1); - - const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); - const packet = Buffer.from(arr); - return [packet, waitTime]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_start_motor_rotate`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 100, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [null, null], - type: `pingpong_lite_g${self.cubeCount}_start_motor_rotate`, - }, - paramsKeyMap: { CUBEID: 0, SPEED: 1 }, - class: 'PingpongGN_motor', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const speed = script.getNumberValue('SPEED'); - - const arr = self.makeContStepPacket(cubeId, speed); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_stop_motor_rotate`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: `pingpong_lite_g${self.cubeCount}_stop_motor_rotate`, - }, - paramsKeyMap: { CUBEID: 0 }, - class: 'PingpongGN_motor', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - - const arr = self.makeContStepPacket(cubeId, 0); - const packet = Buffer.from(arr); - - return [packet]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_rotate_servo_mortor`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, { type: 'angle' }], type: `pingpong_lite_g${self.cubeCount}_rotate_servo_mortor` }, - paramsKeyMap: { cubeno: 0, DEGREE: 1 }, - class: 'PingpongGN_motor', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let angle = script.getNumberValue('DEGREE', script); - - angle = Math.min(Math.max(angle, 0), 180); - - const opt = [2, 0, angle, 1]; - const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); - return [packet]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_set_dot_pixel`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_dot_on, 1], - [Lang.Blocks.pingpong_dot_off, 0], - ], - value: 1, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { - params: [null, null, null], - type: `pingpong_lite_g${self.cubeCount}_set_dot_pixel`, - }, - paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, - class: 'PingpongGN_peripheral_LED', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - let dotX = script.getNumberValue('X', script); - let dotY = script.getNumberValue('Y', script); - const onoff = script.getNumberField('onoff', script); - - dotX = Math.min(Math.max(dotX, 0), 7); - dotY = Math.min(Math.max(dotY, 0), 7); - - const opt = [0x70, dotY, dotX, onoff]; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe1, - cubeId, - opt - ); - return [packet]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_set_dot_string`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { type: 'Block', accept: 'string', value: 'Hello!' }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [null, null], type: `pingpong_lite_g${self.cubeCount}_set_dot_string` }, - paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, - class: 'PingpongGN_peripheral_LED', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const str = script.getStringValue('STR', script); - const duration = script.getNumberValue('DURATION', script); - - let period = Math.round((duration * 100) / (str.length * 8)); - period = Math.min(Math.max(period, 1), 200); - - const opt = Buffer.concat([ - Buffer.from([0x70, period, 0]), - Buffer.from(str.substring(0, 20)), - ]); - - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms - return [packet, waitTime]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_set_dot_clear`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_set_dot_clear` }, - paramsKeyMap: { cubeno: 0 }, - class: 'PingpongGN_peripheral_LED', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = self._getCubeNoFromBlock(script); - const opt = [0x70, 1, 0, ' ']; - const packet = self.makePacket( - self.OPCODE.LEDMATRIX, - 0xe3, - cubeId, - opt - ); - return [packet, 400]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_playNoteForBeats`]: { - //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], - value: 0, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_playNoteForBeats` }, - paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, - class: 'PingpongGN_Music', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const cubeId = script.getNumberField('CUBEID'); - const NOTE = script.getNumberField('NOTE', script); - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); - const packet = Buffer.from(arr); - - return [packet, waitTime]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_restForBeats`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_restForBeats` }, - paramsKeyMap: { BEATS: 0 }, - class: 'PingpongGN_Music', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.postCallReturn(script, () => { - const BEATS = script.getNumberValue('BEATS', script); - - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - return [null, waitTime]; - }); - }, - }, - [`pingpong_lite_g${self.cubeCount}_setTempo`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_setTempo` }, - paramsKeyMap: { TEMPO: 0 }, - class: 'PingpongGN_Music', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - let tempo = script.getNumberValue('TEMPO', script); - self.tempo = self._clampTempo(tempo); - return script.callReturn(); - }, - }, - [`pingpong_lite_g${self.cubeCount}_getTempo`]: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic_string_field', - params: [], - def: { params: [], type: `pingpong_lite_g${self.cubeCount}_getTempo` }, - paramsKeyMap: {}, - class: 'PingpongGN_Music', - isNotFor: [`PingpongG${self.cubeCount}Lite`], - func(sprite, script) { - return self.tempo; - }, - }, - pingpong_lite_g2_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - def: { - params: [null], - type: 'pingpong_lite_g2_multi_motor_rotate', - }, - paramsKeyMap: { DIR_1: 0, DEGREE_1: 1, DIR_2: 2, DEGREE_2: 3 }, - class: 'PingpongGN_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const dir1 = script.getStringField('DIR_1'); - const dir2 = script.getStringField('DIR_2'); - const dir3 = script.getStringField('DIR_3'); - const dir4 = script.getStringField('DIR_4'); - const degree1 = script.getNumberValue('DEGREE_1'); - const degree2 = script.getNumberValue('DEGREE_2'); - const degree3 = script.getNumberValue('DEGREE_3'); - const degree4 = script.getNumberValue('DEGREE_4'); - - const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); - const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); - const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); - const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); - - const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); - const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); - const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); - const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); - - const opt = [self.MODE.MULTIROLE, 1, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - const waitTime = Math.max(delay1, delay2, delay3, delay4); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g2_multi_start_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g2_multi_start_motor_rotate', - }, - paramsKeyMap: { SPEED_1: 0, SPEED_2: 1 }, - class: 'PingpongGN_motor', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const speed1 = script.getNumberValue('SPEED_1'); - const speed2 = script.getNumberValue('SPEED_2'); - const speed3 = script.getNumberValue('SPEED_3'); - const speed4 = script.getNumberValue('SPEED_4'); - - const arr1 = self.makeContStepPacket(0, speed1); - const arr2 = self.makeContStepPacket(1, speed2); - const arr3 = self.makeContStepPacket(2, speed3); - const arr4 = self.makeContStepPacket(3, speed4); - - const opt = [self.MODE.MULTIROLE, 0, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - - return [packet]; - }); - }, - }, - pingpong_lite_g2_multi_playChordForBeats: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g2_multi_playChordForBeats' }, - paramsKeyMap: { - NOTE_1: 0, - NOTE_2: 1, - BEATS: 2, - }, - class: 'PingpongGN_Music', - isNotFor: ['PingpongG2Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const NOTE1 = script.getNumberField('NOTE_1', script); - const NOTE2 = script.getNumberField('NOTE_2', script); - const NOTE3 = script.getNumberField('NOTE_3', script); - const NOTE4 = script.getNumberField('NOTE_4', script); - - const BEATS = script.getNumberValue('BEATS', script); - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); - const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); - const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); - const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); - - const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g3_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Block', - accept: 'string', - defaultType: 'number', - value: 10, - }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - def: { - params: [], - type: 'pingpong_lite_g3_multi_motor_rotate', - }, - paramsKeyMap: { - DIR_1: 0, - DEGREE_1: 1, - DIR_2: 2, - DEGREE_2: 3, - DIR_3: 4, - DEGREE_3: 5, - }, - class: 'PingpongGN_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const dir1 = script.getStringField('DIR_1'); - const dir2 = script.getStringField('DIR_2'); - const dir3 = script.getStringField('DIR_3'); - const dir4 = script.getStringField('DIR_4'); - const degree1 = script.getNumberValue('DEGREE_1'); - const degree2 = script.getNumberValue('DEGREE_2'); - const degree3 = script.getNumberValue('DEGREE_3'); - const degree4 = script.getNumberValue('DEGREE_4'); - - const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); - const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); - const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); - const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); - - const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); - const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); - const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); - const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); - - const opt = [self.MODE.MULTIROLE, 1, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - const waitTime = Math.max(delay1, delay2, delay3, delay4); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g3_multi_start_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g3_multi_start_motor_rotate', - }, - paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2 }, - class: 'PingpongGN_motor', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const speed1 = script.getNumberValue('SPEED_1'); - const speed2 = script.getNumberValue('SPEED_2'); - const speed3 = script.getNumberValue('SPEED_3'); - const speed4 = script.getNumberValue('SPEED_4'); - - const arr1 = self.makeContStepPacket(0, speed1); - const arr2 = self.makeContStepPacket(1, speed2); - const arr3 = self.makeContStepPacket(2, speed3); - const arr4 = self.makeContStepPacket(3, speed4); - - const opt = [self.MODE.MULTIROLE, 0, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - - return [packet]; - }); - }, - }, - pingpong_lite_g3_multi_playChordForBeats: { - //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g3_multi_playChordForBeats' }, - paramsKeyMap: { - NOTE_1: 0, - NOTE_2: 1, - NOTE_3: 2, - BEATS: 3, - }, - class: 'PingpongGN_Music', - isNotFor: ['PingpongG3Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const NOTE1 = script.getNumberField('NOTE_1', script); - const NOTE2 = script.getNumberField('NOTE_2', script); - const NOTE3 = script.getNumberField('NOTE_3', script); - const NOTE4 = script.getNumberField('NOTE_4', script); - - const BEATS = script.getNumberValue('BEATS', script); - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); - const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); - const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); - const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); - - const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g4_multi_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { - type: 'Dropdown', - options: [ - [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], - [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], - ], - value: 'RIGHT', - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, - { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, - ], - def: { - params: [], - type: 'pingpong_lite_g4_multi_motor_rotate', - }, - paramsKeyMap: { - DIR_1: 0, - DEGREE_1: 1, - DIR_2: 2, - DEGREE_2: 3, - DIR_3: 4, - DEGREE_3: 5, - DIR_4: 6, - DEGREE_4: 7, - }, - class: 'PingpongGN_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const dir1 = script.getStringField('DIR_1'); - const dir2 = script.getStringField('DIR_2'); - const dir3 = script.getStringField('DIR_3'); - const dir4 = script.getStringField('DIR_4'); - const degree1 = script.getNumberValue('DEGREE_1'); - const degree2 = script.getNumberValue('DEGREE_2'); - const degree3 = script.getNumberValue('DEGREE_3'); - const degree4 = script.getNumberValue('DEGREE_4'); - - const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); - const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); - const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); - const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); - - const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); - const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); - const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); - const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); - - const opt = [self.MODE.MULTIROLE, 1, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - const waitTime = Math.max(delay1, delay2, delay3, delay4); - - return [packet, waitTime]; - }); - }, - }, - pingpong_lite_g4_multi_start_motor_rotate: { - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - params: [ - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - def: { - params: [], - type: 'pingpong_lite_g4_multi_start_motor_rotate', - }, - paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2, SPEED_4: 3 }, - class: 'PingpongGN_motor', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const speed1 = script.getNumberValue('SPEED_1'); - const speed2 = script.getNumberValue('SPEED_2'); - const speed3 = script.getNumberValue('SPEED_3'); - const speed4 = script.getNumberValue('SPEED_4'); - - const arr1 = self.makeContStepPacket(0, speed1); - const arr2 = self.makeContStepPacket(1, speed2); - const arr3 = self.makeContStepPacket(2, speed3); - const arr4 = self.makeContStepPacket(3, speed4); - - const opt = [self.MODE.MULTIROLE, 0, 0, 2]; - const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); - - return [packet]; - }); - }, - }, - pingpong_lite_g4_multi_playChordForBeats: { - //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', - color: EntryStatic.colorSet.block.default.HARDWARE, - outerLine: EntryStatic.colorSet.block.darken.HARDWARE, - skeleton: 'basic', - statements: [], - params: [ - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { - type: 'Dropdown', - options: Lang.Blocks.pingpong_opts_music_notes, - value: 48, - fontSize: 11, - bgColor: EntryStatic.colorSet.block.darken.HARDWARE, - arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, - }, - { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, - { - type: 'Indicator', - img: 'block_icon/hardware_icon.svg', - size: 12, - }, - ], - events: {}, - def: { params: [], type: 'pingpong_lite_g4_multi_playChordForBeats' }, - paramsKeyMap: { - NOTE_1: 0, - NOTE_2: 1, - NOTE_3: 2, - NOTE_4: 3, - BEATS: 4, - }, - class: 'PingpongGN_Music', - isNotFor: ['PingpongG4Lite'], - func(sprite, script) { - return self.postCallReturn(script, () => { - const NOTE1 = script.getNumberField('NOTE_1', script); - const NOTE2 = script.getNumberField('NOTE_2', script); - const NOTE3 = script.getNumberField('NOTE_3', script); - const NOTE4 = script.getNumberField('NOTE_4', script); - - const BEATS = script.getNumberValue('BEATS', script); - const cBeats = self._clampBeats(BEATS); - const durationSec = self._beatsToDuration(cBeats); - - const waitTime = durationSec * 10 + 60; - - const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); - const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); - const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); - const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); - - const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); - - return [packet, waitTime]; - }); - }, - }, - }; - console.log(this.name,'blocks',blocks); - return blocks; - } } module.exports = PingpongConnectLite; \ No newline at end of file diff --git a/src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js b/src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js index a07756e87b..ae6479e141 100644 --- a/src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js +++ b/src/playground/blocks/hardwareLite/block_pingpongMulti_lite.js @@ -31,6 +31,1306 @@ import _range from 'lodash/range'; ]; } + //#region getBlocks + function getBlocks(self) { + const blocks = { + [`pingpong_lite_g${self.cubeCount}_when_button_pressed`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: `pingpong_lite_g${self.cubeCount}_when_button_pressed`, + }, + paramsKeyMap: { + CUBEID: 1, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + event: 'pp_when_button_pressed', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const pd = self.sensor_data; + + return (pd[cubeId]?.BUTTON == 1) + ? script.callReturn() + : this.die(); + }, + }, + [`pingpong_lite_g${self.cubeCount}_when_tilted`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_event', + statements: [], + params: [ + { + type: 'Indicator', + img: 'block_icon/start_icon_hardware.svg', + size: 14, + position: { x: 0, y: -2 }, + }, + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null, null], + type: `pingpong_lite_g${self.cubeCount}_when_tilted`, + }, + paramsKeyMap: { + CUBEID: 1, + TILT_DIR: 2, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + event: 'pp_when_tilted', + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + if (tiltValue >= self.TILT_THRESHOLD) { + return script.callReturn(); + } + + return this.die(); + }, + }, + [`pingpong_lite_g${self.cubeCount}_is_button_pressed`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { + params: [null], + type: `pingpong_lite_g${self.cubeCount}_is_button_pressed`, + }, + paramsKeyMap: { + CUBEID: 0, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + return self.sensor_data[cubeId].BUTTON == 1; + }, + }, + [`pingpong_lite_g${self.cubeCount}_is_tilted`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_is_tilted` }, + paramsKeyMap: { + CUBEID: 0, + TILT_DIR: 1, + }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR'); + + const tiltValue = self._getTiltValue(cubeId, tiltDir); + + return tiltValue >= self.TILT_THRESHOLD; + }, + }, + [`pingpong_lite_g${self.cubeCount}_get_tilt_value`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_tiltDir, + value: 'F_CIRCLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { + params: [null], + type: `pingpong_lite_g${self.cubeCount}_get_tilt_value`, + }, + paramsKeyMap: { CUBEID: 0, DIR: 1 }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('DIR', script); + + return self._getTiltValue(cubeId, tiltDir); + }, + }, + [`pingpong_lite_g${self.cubeCount}_get_sensor_value`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_sensor_proximity, 'PROXIMITY'], + [Lang.Blocks.pingpong_sensor_ain, 'AIN'], + ], + value: 'PROXIMITY', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_get_sensor_value` }, + paramsKeyMap: { CUBEID: 0, SENSOR: 1 }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const sensorType = script.getStringField('SENSOR', script); + const pd = self.sensor_data; + + return pd[cubeId][sensorType]; + }, + }, + [`pingpong_lite_g${self.cubeCount}_is_top_shape`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_boolean_field', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_cube_dir6, + value: 'DF_RECTANGLE', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_is_top_shape` }, + paramsKeyMap: { CUBEID: 0, TILT_DIR: 1 }, + class: 'PingpongGN', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + const cubeId = script.getNumberField('CUBEID'); + const tiltDir = script.getStringField('TILT_DIR', script); + return self._isUpperDir(cubeId, tiltDir); + }, + }, + [`pingpong_lite_g${self.cubeCount}_motor_rotate`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + events: {}, + def: { + params: [null, null, null], + type: `pingpong_lite_g${self.cubeCount}_motor_rotate`, + }, + paramsKeyMap: { CUBEID: 0, DIR: 1, DEGREE: 2 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const dir = script.getStringField('DIR'); + const degree = script.getNumberValue('DEGREE'); + const speed = 80 * (dir === 'LEFT' ? -1 : 1); + + const [arr, waitTime] = self.makeSingleStepPacket(cubeId,speed,degree); + const packet = Buffer.from(arr); + return [packet, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_start_motor_rotate`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 100, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [null, null], + type: `pingpong_lite_g${self.cubeCount}_start_motor_rotate`, + }, + paramsKeyMap: { CUBEID: 0, SPEED: 1 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const speed = script.getNumberValue('SPEED'); + + const arr = self.makeContStepPacket(cubeId, speed); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_stop_motor_rotate`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_all`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: `pingpong_lite_g${self.cubeCount}_stop_motor_rotate`, + }, + paramsKeyMap: { CUBEID: 0 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + + const arr = self.makeContStepPacket(cubeId, 0); + const packet = Buffer.from(arr); + + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_rotate_servo_mortor`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, { type: 'angle' }], type: `pingpong_lite_g${self.cubeCount}_rotate_servo_mortor` }, + paramsKeyMap: { cubeno: 0, DEGREE: 1 }, + class: 'PingpongGN_motor', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let angle = script.getNumberValue('DEGREE', script); + + angle = Math.min(Math.max(angle, 0), 180); + + const opt = [2, 0, angle, 1]; + const packet = self.makePacket(self.OPCODE.SERVO, 0x00, cubeId, opt); + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_set_dot_pixel`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '0' }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_dot_on, 1], + [Lang.Blocks.pingpong_dot_off, 0], + ], + value: 1, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { + params: [null, null, null], + type: `pingpong_lite_g${self.cubeCount}_set_dot_pixel`, + }, + paramsKeyMap: { cubeno: 0, X: 1, Y: 2, onoff: 3 }, + class: 'PingpongGN_peripheral_LED', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + let dotX = script.getNumberValue('X', script); + let dotY = script.getNumberValue('Y', script); + const onoff = script.getNumberField('onoff', script); + + dotX = Math.min(Math.max(dotX, 0), 7); + dotY = Math.min(Math.max(dotY, 0), 7); + + const opt = [0x70, dotY, dotX, onoff]; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe1, + cubeId, + opt + ); + return [packet]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_set_dot_string`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { type: 'Block', accept: 'string', value: 'Hello!' }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '2' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [null, null], type: `pingpong_lite_g${self.cubeCount}_set_dot_string` }, + paramsKeyMap: { cubeno: 0, STR: 1, DURATION: 2 }, + class: 'PingpongGN_peripheral_LED', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const str = script.getStringValue('STR', script); + const duration = script.getNumberValue('DURATION', script); + + let period = Math.round((duration * 100) / (str.length * 8)); + period = Math.min(Math.max(period, 1), 200); + + const opt = Buffer.concat([ + Buffer.from([0x70, period, 0]), + Buffer.from(str.substring(0, 20)), + ]); + + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + const waitTime = period * str.length * 8 * 10 + 400; // add wait for 400ms + return [packet, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_set_dot_clear`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_set_dot_clear` }, + paramsKeyMap: { cubeno: 0 }, + class: 'PingpongGN_peripheral_LED', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = self._getCubeNoFromBlock(script); + const opt = [0x70, 1, 0, ' ']; + const packet = self.makePacket( + self.OPCODE.LEDMATRIX, + 0xe3, + cubeId, + opt + ); + return [packet, 400]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_playNoteForBeats`]: { + //'%1 큐브의 %2 번 음을 %3 박자로 연주하기 %4', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks[`pingpong_lite_g${self.cubeCount}_cube_id`], + value: 0, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_playNoteForBeats` }, + paramsKeyMap: { CUBEID: 0, NOTE: 1, BEATS: 2 }, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const cubeId = script.getNumberField('CUBEID'); + const NOTE = script.getNumberField('NOTE', script); + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + const arr = self.makeMusicNotePacket(cubeId, NOTE, durationSec); + const packet = Buffer.from(arr); + + return [packet, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_restForBeats`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_restForBeats` }, + paramsKeyMap: { BEATS: 0 }, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.postCallReturn(script, () => { + const BEATS = script.getNumberValue('BEATS', script); + + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + return [null, waitTime]; + }); + }, + }, + [`pingpong_lite_g${self.cubeCount}_setTempo`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: '60' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_setTempo` }, + paramsKeyMap: { TEMPO: 0 }, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + let tempo = script.getNumberValue('TEMPO', script); + self.tempo = self._clampTempo(tempo); + return script.callReturn(); + }, + }, + [`pingpong_lite_g${self.cubeCount}_getTempo`]: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic_string_field', + params: [], + def: { params: [], type: `pingpong_lite_g${self.cubeCount}_getTempo` }, + paramsKeyMap: {}, + class: 'PingpongGN_Music', + isNotFor: [`PingpongG${self.cubeCount}Lite`], + func(sprite, script) { + return self.tempo; + }, + }, + pingpong_lite_g2_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [null], + type: 'pingpong_lite_g2_multi_motor_rotate', + }, + paramsKeyMap: { DIR_1: 0, DEGREE_1: 1, DIR_2: 2, DEGREE_2: 3 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g2_multi_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g2_multi_start_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g2_multi_playChordForBeats: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g2_multi_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + BEATS: 2, + }, + class: 'PingpongGN_Music', + isNotFor: ['PingpongG2Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Block', + accept: 'string', + defaultType: 'number', + value: 10, + }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [], + type: 'pingpong_lite_g3_multi_motor_rotate', + }, + paramsKeyMap: { + DIR_1: 0, + DEGREE_1: 1, + DIR_2: 2, + DEGREE_2: 3, + DIR_3: 4, + DEGREE_3: 5, + }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g3_multi_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g3_multi_start_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g3_multi_playChordForBeats: { + //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g3_multi_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + NOTE_3: 2, + BEATS: 3, + }, + class: 'PingpongGN_Music', + isNotFor: ['PingpongG3Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_multi_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { + type: 'Dropdown', + options: [ + [Lang.Blocks.pingpong_rotate_cw, 'RIGHT'], + [Lang.Blocks.pingpong_rotate_ccw, 'LEFT'], + ], + value: 'RIGHT', + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 10 }, + { type: 'Indicator', img: 'block_icon/hardware_icon.svg', size: 12 }, + ], + def: { + params: [], + type: 'pingpong_lite_g4_multi_motor_rotate', + }, + paramsKeyMap: { + DIR_1: 0, + DEGREE_1: 1, + DIR_2: 2, + DEGREE_2: 3, + DIR_3: 4, + DEGREE_3: 5, + DIR_4: 6, + DEGREE_4: 7, + }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const dir1 = script.getStringField('DIR_1'); + const dir2 = script.getStringField('DIR_2'); + const dir3 = script.getStringField('DIR_3'); + const dir4 = script.getStringField('DIR_4'); + const degree1 = script.getNumberValue('DEGREE_1'); + const degree2 = script.getNumberValue('DEGREE_2'); + const degree3 = script.getNumberValue('DEGREE_3'); + const degree4 = script.getNumberValue('DEGREE_4'); + + const speed1 = 80 * (dir1 === 'LEFT' ? -1 : 1); + const speed2 = 80 * (dir2 === 'LEFT' ? -1 : 1); + const speed3 = 80 * (dir3 === 'LEFT' ? -1 : 1); + const speed4 = 80 * (dir4 === 'LEFT' ? -1 : 1); + + const [arr1, delay1] = self.makeSingleStepPacket(0,speed1,degree1); + const [arr2, delay2] = self.makeSingleStepPacket(1,speed2,degree2); + const [arr3, delay3] = self.makeSingleStepPacket(2,speed3,degree3); + const [arr4, delay4] = self.makeSingleStepPacket(3,speed4,degree4); + + const opt = [self.MODE.MULTIROLE, 1, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + const waitTime = Math.max(delay1, delay2, delay3, delay4); + + return [packet, waitTime]; + }); + }, + }, + pingpong_lite_g4_multi_start_motor_rotate: { + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + params: [ + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { type: 'Block', accept: 'string', defaultType: 'number', value: 100 }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + def: { + params: [], + type: 'pingpong_lite_g4_multi_start_motor_rotate', + }, + paramsKeyMap: { SPEED_1: 0, SPEED_2: 1, SPEED_3: 2, SPEED_4: 3 }, + class: 'PingpongGN_motor', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const speed1 = script.getNumberValue('SPEED_1'); + const speed2 = script.getNumberValue('SPEED_2'); + const speed3 = script.getNumberValue('SPEED_3'); + const speed4 = script.getNumberValue('SPEED_4'); + + const arr1 = self.makeContStepPacket(0, speed1); + const arr2 = self.makeContStepPacket(1, speed2); + const arr3 = self.makeContStepPacket(2, speed3); + const arr4 = self.makeContStepPacket(3, speed4); + + const opt = [self.MODE.MULTIROLE, 0, 0, 2]; + const packet = self.makeAggregatePacket(self.OPCODE.AGGREGATE_STEPS,0,[arr1, arr2, arr3, arr4],opt); + + return [packet]; + }); + }, + }, + pingpong_lite_g4_multi_playChordForBeats: { + //'%1 큐브 %2, %3 큐브 %4, %5 큐브 %6 %7 박자로 연주하기 %8', + color: EntryStatic.colorSet.block.default.HARDWARE, + outerLine: EntryStatic.colorSet.block.darken.HARDWARE, + skeleton: 'basic', + statements: [], + params: [ + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { + type: 'Dropdown', + options: Lang.Blocks.pingpong_opts_music_notes, + value: 48, + fontSize: 11, + bgColor: EntryStatic.colorSet.block.darken.HARDWARE, + arrowColor: EntryStatic.colorSet.arrow.default.HARDWARE, + }, + { type: 'Block', accept: 'string', defaultType: 'number', value: '1' }, + { + type: 'Indicator', + img: 'block_icon/hardware_icon.svg', + size: 12, + }, + ], + events: {}, + def: { params: [], type: 'pingpong_lite_g4_multi_playChordForBeats' }, + paramsKeyMap: { + NOTE_1: 0, + NOTE_2: 1, + NOTE_3: 2, + NOTE_4: 3, + BEATS: 4, + }, + class: 'PingpongGN_Music', + isNotFor: ['PingpongG4Lite'], + func(sprite, script) { + return self.postCallReturn(script, () => { + const NOTE1 = script.getNumberField('NOTE_1', script); + const NOTE2 = script.getNumberField('NOTE_2', script); + const NOTE3 = script.getNumberField('NOTE_3', script); + const NOTE4 = script.getNumberField('NOTE_4', script); + + const BEATS = script.getNumberValue('BEATS', script); + const cBeats = self._clampBeats(BEATS); + const durationSec = self._beatsToDuration(cBeats); + + const waitTime = durationSec * 10 + 60; + + const arr1 = self.makeMusicNotePacket(0,NOTE1,durationSec); + const arr2 = self.makeMusicNotePacket(1,NOTE2,durationSec); + const arr3 = self.makeMusicNotePacket(2,NOTE3,durationSec); + const arr4 = self.makeMusicNotePacket(3,NOTE4,durationSec); + + const packet = self.makeAggregatePacket(self.OPCODE.MUSIC,0xa2,[arr1, arr2, arr3, arr4],[0, 0]); + + return [packet, waitTime]; + }); + }, + }, + }; + return blocks; + } + function createPingpongLite(cubeCount, className, hardwareId, imageName) { return new (class extends PingpongConnectLite { constructor() { @@ -39,6 +1339,7 @@ import _range from 'lodash/range'; this.imageName = imageName; this.blockMenuBlocks = createBlockMenuList(this.cubeCount); } + getBlocks() {return getBlocks(this);} })(); } From ae1f44da05ec31299d873385b3d19d62e8a9ac1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=A7=84=ED=98=B8?= <140678689+jinbaekho@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:28:56 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js b/src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js index 9a4da8a6d9..abea0819ee 100644 --- a/src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js +++ b/src/playground/blocks/hardwareLite/block_pingpongSingle_lite.js @@ -715,7 +715,6 @@ import _range from 'lodash/range'; }, }, }; - console.log(this.name,'blocks',blocks); return blocks; }