From 4c536b06ae869d4d9f3844557a46b7ae1549457a Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:57:39 +0900 Subject: [PATCH 01/16] =?UTF-8?q?fix:=20=E6=9C=AA=E5=AE=9A=E7=BE=A9dataTyp?= =?UTF-8?q?e=E3=81=AEData=E3=83=91=E3=82=B1=E3=83=83=E3=83=88=E5=8F=97?= =?UTF-8?q?=E4=BF=A1=E6=99=82=E3=81=AE=E3=82=AF=E3=83=A9=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #73 --- .changeset/brave-dots-play.md | 5 +++++ src/tcnet.ts | 2 +- tests/receive-unicast.test.ts | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .changeset/brave-dots-play.md diff --git a/.changeset/brave-dots-play.md b/.changeset/brave-dots-play.md new file mode 100644 index 0000000..1179351 --- /dev/null +++ b/.changeset/brave-dots-play.md @@ -0,0 +1,5 @@ +--- +"@9c5s/node-tcnet": patch +--- + +未定義dataTypeのDataパケット受信時にクラッシュする問題を修正 diff --git a/src/tcnet.ts b/src/tcnet.ts index ea1a49d..8d8a56f 100644 --- a/src/tcnet.ts +++ b/src/tcnet.ts @@ -717,7 +717,7 @@ export class TCNetClient extends EventEmitter { if (packet instanceof nw.TCNetDataPacket) { const dataPacketClass = nw.TCNetDataPackets[packet.dataType]; - if (dataPacketClass !== null) { + if (dataPacketClass != null) { const dataPacket: nw.TCNetDataPacket = new dataPacketClass(); dataPacket.buffer = msg; dataPacket.header = mgmtHeader; diff --git a/tests/receive-unicast.test.ts b/tests/receive-unicast.test.ts index 3c2e611..a01c088 100644 --- a/tests/receive-unicast.test.ts +++ b/tests/receive-unicast.test.ts @@ -42,6 +42,25 @@ describe("receiveUnicast マルチパケット対応", () => { }); }); +describe("receiveUnicast 未定義dataType", () => { + it("未定義dataTypeのDataパケットを受信してもクラッシュしない", () => { + const client = new TestTCNetClient(); + client.simulateConnected(); + const handler = vi.fn(); + client.on("data", handler); + + // dataType=255 はTCNetDataPacketsに定義されていない + const buffer = createDataBuffer(255, 1, 436); + expect(() => client.simulateUnicast(buffer)).not.toThrow(); + expect(handler).not.toHaveBeenCalled(); + }); + + it("TCNetDataPacketsに未定義キーでアクセスするとundefinedが返る", () => { + const result = nw.TCNetDataPackets[255 as nw.TCNetDataPacketType]; + expect(result).toBeUndefined(); + }); +}); + describe("TCNetClient.requestData() layer バリデーション", () => { // バリデーション失敗時は sendServer に到達しないため、接続状態不要でテスト可能 From 579cc3fce56556872a0eea33f59607cc0249e33b Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:59:49 +0900 Subject: [PATCH 02/16] =?UTF-8?q?fix:=20Mixer=E3=83=91=E3=82=B1=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=81=AEMixer=20ID=E3=81=8CLayer=20ID=E3=81=A8?= =?UTF-8?q?=E3=81=97=E3=81=A6=E8=AA=A4=E5=A4=89=E6=8F=9B=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #72 --- src/network.ts | 3 +++ tests/beatgrid-mixer.test.ts | 41 +++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/network.ts b/src/network.ts index 9ad0639..0f09a90 100644 --- a/src/network.ts +++ b/src/network.ts @@ -746,6 +746,9 @@ export class TCNetDataPacketMixer extends TCNetDataPacket { return; } + // Mixer IDはLayer IDではないため、基底クラスの-1変換を上書きする + this.layer = this.buffer.readUInt8(25); + const parseChannel = (offset: number): MixerChannel => ({ sourceSelect: this.buffer.readUInt8(offset), audioLevel: this.buffer.readUInt8(offset + 1), diff --git a/tests/beatgrid-mixer.test.ts b/tests/beatgrid-mixer.test.ts index 960044a..71ce1d3 100644 --- a/tests/beatgrid-mixer.test.ts +++ b/tests/beatgrid-mixer.test.ts @@ -8,6 +8,33 @@ function createHeader(buffer: Buffer): TCNetManagementHeader { } describe("TCNetDataPacketMixer", () => { + it("read()後のthis.layerはmixerId(buffer[25])と一致する", () => { + const buffer = Buffer.alloc(270); + buffer.writeUInt8(0, 25); + + const packet = new TCNetDataPacketMixer(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.layer = -1; // 基底クラスの-1変換を模擬する + packet.read(); + + expect(packet.layer).toBe(0); + }); + + it("mixerId=3のパケットでthis.layerが3になる", () => { + const buffer = Buffer.alloc(270); + buffer.writeUInt8(3, 25); + + const packet = new TCNetDataPacketMixer(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.layer = 2; // 基底クラスが3-1=2を設定する想定 + packet.read(); + + expect(packet.layer).toBe(3); + expect(packet.data!.mixerId).toBe(3); + }); + it("Mixerデータをパースする", () => { const buffer = Buffer.alloc(270); buffer.writeUInt8(3, 2); @@ -64,21 +91,29 @@ describe("TCNetDataPacketMixer", () => { }); it("バッファが 258 バイトの場合 data は null のまま", () => { - // Arrange: 最大オフセット 258 に届かない 258 バイトのバッファ const buffer = Buffer.alloc(258); buffer.writeUInt8(3, 2); buffer.write("TCN", 4, "ascii"); buffer.writeUInt8(200, 7); - // Act const packet = new TCNetDataPacketMixer(); packet.buffer = buffer; packet.header = createHeader(buffer); packet.read(); - // Assert expect(packet.data).toBeNull(); }); + + it("バッファが短い場合はthis.layerを上書きしない", () => { + const buffer = Buffer.alloc(258); + const packet = new TCNetDataPacketMixer(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.layer = -1; + packet.read(); + + expect(packet.layer).toBe(-1); + }); }); describe("TCNetDataPacketBeatGrid", () => { From 73dddf285a8b995db186dca78a351a8cb70f7228 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:01:21 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20MixerData=E5=9E=8B=E3=81=AB?= =?UTF-8?q?=E4=BB=95=E6=A7=98=E4=B8=8A=E3=81=AE=E5=85=A8=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #75 --- .changeset/mixer-id-and-fields.md | 5 + src/network.ts | 32 +++++- src/types.ts | 32 +++++- tests/beatgrid-mixer.test.ts | 155 +++++++++++++++++++++++------- 4 files changed, 182 insertions(+), 42 deletions(-) create mode 100644 .changeset/mixer-id-and-fields.md diff --git a/.changeset/mixer-id-and-fields.md b/.changeset/mixer-id-and-fields.md new file mode 100644 index 0000000..c203826 --- /dev/null +++ b/.changeset/mixer-id-and-fields.md @@ -0,0 +1,5 @@ +--- +"@9c5s/node-tcnet": minor +--- + +MixerパケットのMixer ID誤変換を修正し、MixerData型に仕様上の全フィールドを追加 diff --git a/src/network.ts b/src/network.ts index 0f09a90..b36070e 100644 --- a/src/network.ts +++ b/src/network.ts @@ -773,6 +773,12 @@ export class TCNetDataPacketMixer extends TCNetDataPacket { masterAudioLevel: this.buffer.readUInt8(61), masterFaderLevel: this.buffer.readUInt8(62), masterFilter: this.buffer.readUInt8(69), + micEqHi: this.buffer.readUInt8(59), + micEqLow: this.buffer.readUInt8(60), + linkCueA: this.buffer.readUInt8(67), + linkCueB: this.buffer.readUInt8(68), + masterCueA: this.buffer.readUInt8(71), + masterCueB: this.buffer.readUInt8(72), masterIsolatorOn: this.buffer.readUInt8(74) === 1, masterIsolatorHi: this.buffer.readUInt8(75), masterIsolatorMid: this.buffer.readUInt8(76), @@ -780,16 +786,36 @@ export class TCNetDataPacketMixer extends TCNetDataPacket { filterHpf: this.buffer.readUInt8(79), filterLpf: this.buffer.readUInt8(80), filterResonance: this.buffer.readUInt8(81), - crossFader: this.buffer.readUInt8(99), - crossFaderCurve: this.buffer.readUInt8(98), + sendFxEffect: this.buffer.readUInt8(84), + sendFxExt1: this.buffer.readUInt8(85), + sendFxExt2: this.buffer.readUInt8(86), + sendFxMasterMix: this.buffer.readUInt8(87), + sendFxSizeFeedback: this.buffer.readUInt8(88), + sendFxTime: this.buffer.readUInt8(89), + sendFxHpf: this.buffer.readUInt8(90), + sendFxLevel: this.buffer.readUInt8(91), + sendReturn3Source: this.buffer.readUInt8(92), + sendReturn3Type: this.buffer.readUInt8(93), + sendReturn3On: this.buffer.readUInt8(94), + sendReturn3Level: this.buffer.readUInt8(95), channelFaderCurve: this.buffer.readUInt8(97), + crossFaderCurve: this.buffer.readUInt8(98), + crossFader: this.buffer.readUInt8(99), beatFxOn: this.buffer.readUInt8(100) === 1, - beatFxSelect: this.buffer.readUInt8(103), beatFxLevelDepth: this.buffer.readUInt8(101), beatFxChannelSelect: this.buffer.readUInt8(102), + beatFxSelect: this.buffer.readUInt8(103), + beatFxFreqHi: this.buffer.readUInt8(104), + beatFxFreqMid: this.buffer.readUInt8(105), + beatFxFreqLow: this.buffer.readUInt8(106), + headphonesPreEq: this.buffer.readUInt8(107), headphonesALevel: this.buffer.readUInt8(108), + headphonesAMix: this.buffer.readUInt8(109), headphonesBLevel: this.buffer.readUInt8(110), + headphonesBMix: this.buffer.readUInt8(111), boothLevel: this.buffer.readUInt8(112), + boothEqHi: this.buffer.readUInt8(113), + boothEqLow: this.buffer.readUInt8(114), channels: [125, 149, 173, 197, 221, 245].map(parseChannel), }; } diff --git a/src/types.ts b/src/types.ts index 4530dde..46ead3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,6 +94,12 @@ export type MixerData = { masterAudioLevel: number; masterFaderLevel: number; masterFilter: number; + micEqHi: number; + micEqLow: number; + linkCueA: number; + linkCueB: number; + masterCueA: number; + masterCueB: number; masterIsolatorOn: boolean; masterIsolatorHi: number; masterIsolatorMid: number; @@ -101,15 +107,35 @@ export type MixerData = { filterHpf: number; filterLpf: number; filterResonance: number; - crossFader: number; - crossFaderCurve: number; + sendFxEffect: number; + sendFxExt1: number; + sendFxExt2: number; + sendFxMasterMix: number; + sendFxSizeFeedback: number; + sendFxTime: number; + sendFxHpf: number; + sendFxLevel: number; + sendReturn3Source: number; + sendReturn3Type: number; + sendReturn3On: number; + sendReturn3Level: number; channelFaderCurve: number; + crossFaderCurve: number; + crossFader: number; beatFxOn: boolean; - beatFxSelect: number; beatFxLevelDepth: number; beatFxChannelSelect: number; + beatFxSelect: number; + beatFxFreqHi: number; + beatFxFreqMid: number; + beatFxFreqLow: number; + headphonesPreEq: number; headphonesALevel: number; + headphonesAMix: number; headphonesBLevel: number; + headphonesBMix: number; boothLevel: number; + boothEqHi: number; + boothEqLow: number; channels: MixerChannel[]; }; diff --git a/tests/beatgrid-mixer.test.ts b/tests/beatgrid-mixer.test.ts index 71ce1d3..ba53017 100644 --- a/tests/beatgrid-mixer.test.ts +++ b/tests/beatgrid-mixer.test.ts @@ -35,35 +35,65 @@ describe("TCNetDataPacketMixer", () => { expect(packet.data!.mixerId).toBe(3); }); - it("Mixerデータをパースする", () => { + it("全フィールドが正しいオフセットから読み取られる", () => { const buffer = Buffer.alloc(270); - buffer.writeUInt8(3, 2); - buffer.write("TCN", 4, "ascii"); - buffer.writeUInt8(200, 7); - buffer.writeUInt8(150, 24); + // Mixer ID / Type buffer.writeUInt8(1, 25); buffer.writeUInt8(2, 26); - buffer.write("DJM-900NXS2\x00\x00\x00\x00\x00", 29, "ascii"); - buffer.writeUInt8(100, 61); - buffer.writeUInt8(127, 62); - buffer.writeUInt8(64, 69); - buffer.writeUInt8(1, 74); - buffer.writeUInt8(64, 75); - buffer.writeUInt8(64, 76); - buffer.writeUInt8(64, 77); - buffer.writeUInt8(0, 79); - buffer.writeUInt8(127, 80); - buffer.writeUInt8(0, 81); - buffer.writeUInt8(1, 97); - buffer.writeUInt8(2, 98); - buffer.writeUInt8(64, 99); - buffer.writeUInt8(1, 100); - buffer.writeUInt8(50, 101); - buffer.writeUInt8(3, 102); - buffer.writeUInt8(5, 103); - buffer.writeUInt8(80, 108); - buffer.writeUInt8(60, 110); - buffer.writeUInt8(90, 112); + buffer.write("DJM-900NXS2\x00\x00\x00\x00", 29, "ascii"); + // マスターセクション + buffer.writeUInt8(59, 59); // micEqHi + buffer.writeUInt8(60, 60); // micEqLow + buffer.writeUInt8(100, 61); // masterAudioLevel + buffer.writeUInt8(127, 62); // masterFaderLevel + buffer.writeUInt8(67, 67); // linkCueA + buffer.writeUInt8(68, 68); // linkCueB + buffer.writeUInt8(64, 69); // masterFilter + buffer.writeUInt8(71, 71); // masterCueA + buffer.writeUInt8(72, 72); // masterCueB + buffer.writeUInt8(1, 74); // masterIsolatorOn + buffer.writeUInt8(64, 75); // masterIsolatorHi + buffer.writeUInt8(64, 76); // masterIsolatorMid + buffer.writeUInt8(64, 77); // masterIsolatorLow + // フィルター + buffer.writeUInt8(10, 79); // filterHpf + buffer.writeUInt8(127, 80); // filterLpf + buffer.writeUInt8(50, 81); // filterResonance + // Send FX + buffer.writeUInt8(84, 84); // sendFxEffect + buffer.writeUInt8(85, 85); // sendFxExt1 + buffer.writeUInt8(86, 86); // sendFxExt2 + buffer.writeUInt8(87, 87); // sendFxMasterMix + buffer.writeUInt8(88, 88); // sendFxSizeFeedback + buffer.writeUInt8(89, 89); // sendFxTime + buffer.writeUInt8(90, 90); // sendFxHpf + buffer.writeUInt8(91, 91); // sendFxLevel + buffer.writeUInt8(92, 92); // sendReturn3Source + buffer.writeUInt8(93, 93); // sendReturn3Type + buffer.writeUInt8(94, 94); // sendReturn3On + buffer.writeUInt8(95, 95); // sendReturn3Level + // クロスフェーダー/チャンネルフェーダー + buffer.writeUInt8(1, 97); // channelFaderCurve + buffer.writeUInt8(2, 98); // crossFaderCurve + buffer.writeUInt8(64, 99); // crossFader + // Beat FX + buffer.writeUInt8(1, 100); // beatFxOn + buffer.writeUInt8(50, 101); // beatFxLevelDepth + buffer.writeUInt8(3, 102); // beatFxChannelSelect + buffer.writeUInt8(5, 103); // beatFxSelect + buffer.writeUInt8(104, 104); // beatFxFreqHi + buffer.writeUInt8(105, 105); // beatFxFreqMid + buffer.writeUInt8(106, 106); // beatFxFreqLow + // ヘッドフォン + buffer.writeUInt8(107, 107); // headphonesPreEq + buffer.writeUInt8(80, 108); // headphonesALevel + buffer.writeUInt8(109, 109); // headphonesAMix + buffer.writeUInt8(60, 110); // headphonesBLevel + buffer.writeUInt8(111, 111); // headphonesBMix + // ブース + buffer.writeUInt8(90, 112); // boothLevel + buffer.writeUInt8(113, 113); // boothEqHi + buffer.writeUInt8(114, 114); // boothEqLow // Channel 1 at offset 125 buffer.writeUInt8(1, 125); buffer.writeUInt8(100, 126); @@ -74,16 +104,69 @@ describe("TCNetDataPacketMixer", () => { packet.header = createHeader(buffer); packet.read(); - expect(packet.data).not.toBeNull(); - expect(packet.data!.mixerName).toBe("DJM-900NXS2"); - expect(packet.data!.masterAudioLevel).toBe(100); - expect(packet.data!.masterIsolatorOn).toBe(true); - expect(packet.data!.beatFxOn).toBe(true); - expect(packet.data!.crossFader).toBe(64); - expect(packet.data!.channels).toHaveLength(6); - expect(packet.data!.channels[0].sourceSelect).toBe(1); - expect(packet.data!.channels[0].audioLevel).toBe(100); - expect(packet.data!.channels[0].faderLevel).toBe(127); + const d = packet.data!; + expect(d).not.toBeNull(); + expect(d.mixerId).toBe(1); + expect(d.mixerType).toBe(2); + expect(d.mixerName).toBe("DJM-900NXS2"); + // マスターセクション + expect(d.micEqHi).toBe(59); + expect(d.micEqLow).toBe(60); + expect(d.masterAudioLevel).toBe(100); + expect(d.masterFaderLevel).toBe(127); + expect(d.linkCueA).toBe(67); + expect(d.linkCueB).toBe(68); + expect(d.masterFilter).toBe(64); + expect(d.masterCueA).toBe(71); + expect(d.masterCueB).toBe(72); + expect(d.masterIsolatorOn).toBe(true); + expect(d.masterIsolatorHi).toBe(64); + expect(d.masterIsolatorMid).toBe(64); + expect(d.masterIsolatorLow).toBe(64); + // フィルター + expect(d.filterHpf).toBe(10); + expect(d.filterLpf).toBe(127); + expect(d.filterResonance).toBe(50); + // Send FX + expect(d.sendFxEffect).toBe(84); + expect(d.sendFxExt1).toBe(85); + expect(d.sendFxExt2).toBe(86); + expect(d.sendFxMasterMix).toBe(87); + expect(d.sendFxSizeFeedback).toBe(88); + expect(d.sendFxTime).toBe(89); + expect(d.sendFxHpf).toBe(90); + expect(d.sendFxLevel).toBe(91); + expect(d.sendReturn3Source).toBe(92); + expect(d.sendReturn3Type).toBe(93); + expect(d.sendReturn3On).toBe(94); + expect(d.sendReturn3Level).toBe(95); + // クロスフェーダー/チャンネルフェーダー + expect(d.channelFaderCurve).toBe(1); + expect(d.crossFaderCurve).toBe(2); + expect(d.crossFader).toBe(64); + // Beat FX + expect(d.beatFxOn).toBe(true); + expect(d.beatFxLevelDepth).toBe(50); + expect(d.beatFxChannelSelect).toBe(3); + expect(d.beatFxSelect).toBe(5); + expect(d.beatFxFreqHi).toBe(104); + expect(d.beatFxFreqMid).toBe(105); + expect(d.beatFxFreqLow).toBe(106); + // ヘッドフォン + expect(d.headphonesPreEq).toBe(107); + expect(d.headphonesALevel).toBe(80); + expect(d.headphonesAMix).toBe(109); + expect(d.headphonesBLevel).toBe(60); + expect(d.headphonesBMix).toBe(111); + // ブース + expect(d.boothLevel).toBe(90); + expect(d.boothEqHi).toBe(113); + expect(d.boothEqLow).toBe(114); + // チャンネル + expect(d.channels).toHaveLength(6); + expect(d.channels[0].sourceSelect).toBe(1); + expect(d.channels[0].audioLevel).toBe(100); + expect(d.channels[0].faderLevel).toBe(127); }); it("length() は 270 を返す", () => { From 8f7cbcaf9dcff90325e5204eeeaa951f19e2512c Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:18:15 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor!:=20Error=E3=83=91=E3=82=B1?= =?UTF-8?q?=E3=83=83=E3=83=88=E3=81=AEerrorData=E3=82=92=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E5=8C=96=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB=E3=83=89=E3=81=AB?= =?UTF-8?q?=E7=BD=AE=E6=8F=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: errorDataプロパティを廃止し、dataType/layerId/code/messageTypeに分割 closes #77 --- docs/wiki/Implementation-Status.md | 2 +- src/network.ts | 19 +++++-- src/tcnet.ts | 10 ++-- tests/auth.test.ts | 51 ++++++++++--------- tests/packet-fields.test.ts | 82 ++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 37 deletions(-) create mode 100644 tests/packet-fields.test.ts diff --git a/docs/wiki/Implementation-Status.md b/docs/wiki/Implementation-Status.md index 1bee59e..138b50b 100644 --- a/docs/wiki/Implementation-Status.md +++ b/docs/wiki/Implementation-Status.md @@ -11,7 +11,7 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。 | 5 | Status | 300 | 受信のみ | | 20 | Request | 26 | 送信のみ | | 200 | Data | 可変 | 受信のみ(DataTypeによる) | -| 13 | Error/Notification | 30 | 受信のみ。TCNASDP認証応答の判定に使用 | +| 13 | Error/Notification | 30 | 受信のみ。dataType/layerId/code/messageType構造化済み | | 30 | Application Specific Data | 可変 | 送受信対応。TCNASDP認証のハンドシェイクに使用 | | 204 | File | 可変 | 受信のみ。Artwork(JPEG)のファイル転送。マルチパケット対応 | | 254 | Time | 154/162 | 受信のみ。OnAirセクション(V3.3.3+)の有無を自動判定する | diff --git a/src/network.ts b/src/network.ts index b36070e..c52f5ef 100644 --- a/src/network.ts +++ b/src/network.ts @@ -987,12 +987,21 @@ export class TCNetDataPacketArtwork extends TCNetDataPacket { * @category Packets */ export class TCNetErrorPacket extends TCNetPacket { - /** エラーボディの生データ (offset 24以降) */ - errorData!: Buffer; + /** データタイプ (byte 24) */ + dataType!: number; + /** レイヤーID (byte 25) */ + layerId!: number; + /** エラーコード (byte 26-27, UInt16LE) 1=Unknown, 13=Not Possible, 14=Empty, 255=OK */ + code!: number; + /** メッセージタイプ (byte 28-29, UInt16LE) */ + messageType!: number; /** バッファからパケットデータを読み取る */ read(): void { - this.errorData = Buffer.from(this.buffer.slice(24)); + this.dataType = this.buffer.readUInt8(24); + this.layerId = this.buffer.readUInt8(25); + this.code = this.buffer.readUInt16LE(26); + this.messageType = this.buffer.readUInt16LE(28); } /** パケットデータをバッファに書き込む */ @@ -1002,10 +1011,10 @@ export class TCNetErrorPacket extends TCNetPacket { /** * パケットのバイト長を返す - * @returns パケット長 (-1: 可変長) + * @returns パケット長 */ length(): number { - return -1; + return 30; } /** diff --git a/src/tcnet.ts b/src/tcnet.ts index 8d8a56f..2ed8f8a 100644 --- a/src/tcnet.ts +++ b/src/tcnet.ts @@ -1421,13 +1421,9 @@ export class TCNetClient extends EventEmitter { * @param packet - 受信した Error パケット */ private handleAuthErrorPacket(packet: nw.TCNetErrorPacket): void { - if (this._authState !== "pending" || packet.errorData.length < 3) return; + if (this._authState !== "pending") return; - const b0 = packet.errorData[0]; - const b1 = packet.errorData[1]; - const b2 = packet.errorData[2]; - - if (b0 === 0xff && b1 === 0xff && b2 === 0xff) { + if (packet.dataType === 0xff && packet.layerId === 0xff && packet.code === 255) { if (this.authTimeoutId) { clearTimeout(this.authTimeoutId); this.authTimeoutId = null; @@ -1435,7 +1431,7 @@ export class TCNetClient extends EventEmitter { this._authState = "authenticated"; this.log?.debug("TCNASDP authentication succeeded"); this.emit("authenticated"); - } else if (b0 === 0xff && b1 === 0xff && b2 === 0x0d) { + } else if (packet.dataType === 0xff && packet.layerId === 0xff && packet.code === 13) { if (this.authTimeoutId) { clearTimeout(this.authTimeoutId); this.authTimeoutId = null; diff --git a/tests/auth.test.ts b/tests/auth.test.ts index 93175f5..d279500 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -194,13 +194,14 @@ describe("TCNetApplicationDataPacket", () => { }); describe("TCNetErrorPacket", () => { - it("errorDataにoffset 24以降のバイトが格納される", () => { + it("認証成功のErrorパケットを構造化フィールドでパースする", () => { // Arrange - const buffer = Buffer.alloc(27); + const buffer = Buffer.alloc(30); writeValidHeader(buffer, TCNetMessageType.Error); - buffer.writeUInt8(0xff, 24); - buffer.writeUInt8(0xff, 25); - buffer.writeUInt8(0xff, 26); + buffer.writeUInt8(0xff, 24); // dataType + buffer.writeUInt8(0xff, 25); // layerId + buffer.writeUInt16LE(0xffff, 26); // code + buffer.writeUInt16LE(0x00, 28); // messageType const packet = new TCNetErrorPacket(); packet.buffer = buffer; @@ -210,19 +211,20 @@ describe("TCNetErrorPacket", () => { packet.read(); // Assert - expect(packet.errorData.length).toBe(3); - expect(packet.errorData[0]).toBe(0xff); - expect(packet.errorData[1]).toBe(0xff); - expect(packet.errorData[2]).toBe(0xff); + expect(packet.dataType).toBe(0xff); + expect(packet.layerId).toBe(0xff); + expect(packet.code).toBe(0xffff); + expect(packet.messageType).toBe(0x00); }); - it("認証失敗のErrorパケット (0xFFFF0D) をパースする", () => { + it("認証失敗のErrorパケット (code=0x000D) をパースする", () => { // Arrange - const buffer = Buffer.alloc(27); + const buffer = Buffer.alloc(30); writeValidHeader(buffer, TCNetMessageType.Error); - buffer.writeUInt8(0xff, 24); - buffer.writeUInt8(0xff, 25); - buffer.writeUInt8(0x0d, 26); + buffer.writeUInt8(0xff, 24); // dataType + buffer.writeUInt8(0xff, 25); // layerId + buffer.writeUInt16LE(0x000d, 26); // code = Not Possible + buffer.writeUInt16LE(0x00, 28); // messageType const packet = new TCNetErrorPacket(); packet.buffer = buffer; @@ -232,17 +234,17 @@ describe("TCNetErrorPacket", () => { packet.read(); // Assert - expect(packet.errorData[0]).toBe(0xff); - expect(packet.errorData[1]).toBe(0xff); - expect(packet.errorData[2]).toBe(0x0d); + expect(packet.dataType).toBe(0xff); + expect(packet.layerId).toBe(0xff); + expect(packet.code).toBe(0x000d); }); it("write() はエラーを投げる", () => { expect(() => new TCNetErrorPacket().write()).toThrow("not supported!"); }); - it("length() は -1 を返す (可変長)", () => { - expect(new TCNetErrorPacket().length()).toBe(-1); + it("length() は 30 を返す", () => { + expect(new TCNetErrorPacket().length()).toBe(30); }); it("type() は TCNetMessageType.Error(13) を返す", () => { @@ -274,12 +276,13 @@ function createAppDataPacket(cmd: number, token: number): TCNetApplicationDataPa return result; } -function createErrorPacket(b0: number, b1: number, b2: number): TCNetErrorPacket { - const buffer = Buffer.alloc(27); +function createErrorPacket(dataType: number, layerId: number, code: number): TCNetErrorPacket { + const buffer = Buffer.alloc(30); writeValidHeader(buffer, TCNetMessageType.Error); - buffer.writeUInt8(b0, 24); - buffer.writeUInt8(b1, 25); - buffer.writeUInt8(b2, 26); + buffer.writeUInt8(dataType, 24); + buffer.writeUInt8(layerId, 25); + buffer.writeUInt16LE(code, 26); + buffer.writeUInt16LE(0, 28); const packet = new TCNetErrorPacket(); packet.buffer = buffer; packet.header = createHeader(buffer); diff --git a/tests/packet-fields.test.ts b/tests/packet-fields.test.ts new file mode 100644 index 0000000..dc8e234 --- /dev/null +++ b/tests/packet-fields.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from "vitest"; +import { TCNetMessageType, TCNetErrorPacket, TCNetManagementHeader } from "../src/network"; + +function writeValidHeader(buffer: Buffer, messageType: number): void { + buffer.writeUInt16LE(1, 0); + buffer.writeUInt8(3, 2); + buffer.writeUInt8(5, 3); + buffer.write("TCN", 4, "ascii"); + buffer.writeUInt8(messageType, 7); + buffer.write("NODE01\x00\x00", 8, "ascii"); + buffer.writeUInt8(42, 16); + buffer.writeUInt8(2, 17); + buffer.writeUInt16LE(7, 18); + buffer.writeUInt32LE(0, 20); +} + +function createHeader(buffer: Buffer): TCNetManagementHeader { + const header = new TCNetManagementHeader(buffer); + header.minorVersion = 5; + return header; +} + +describe("TCNetErrorPacket 構造化フィールド", () => { + it("全フィールドを正しくパースする", () => { + const buffer = Buffer.alloc(30); + writeValidHeader(buffer, TCNetMessageType.Error); + buffer.writeUInt8(16, 24); // dataType = SmallWaveFormData + buffer.writeUInt8(3, 25); // layerId + buffer.writeUInt16LE(14, 26); // code = Empty + buffer.writeUInt16LE(200, 28); // messageType = Data + + const packet = new TCNetErrorPacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.dataType).toBe(16); + expect(packet.layerId).toBe(3); + expect(packet.code).toBe(14); + expect(packet.messageType).toBe(200); + }); + + it("code=255 (OK) を正しくパースする", () => { + const buffer = Buffer.alloc(30); + writeValidHeader(buffer, TCNetMessageType.Error); + buffer.writeUInt8(0xff, 24); + buffer.writeUInt8(0xff, 25); + buffer.writeUInt16LE(255, 26); + buffer.writeUInt16LE(30, 28); + + const packet = new TCNetErrorPacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.code).toBe(255); + expect(packet.messageType).toBe(30); + }); + + it("code=1 (Unknown) を正しくパースする", () => { + const buffer = Buffer.alloc(30); + writeValidHeader(buffer, TCNetMessageType.Error); + buffer.writeUInt8(2, 24); + buffer.writeUInt8(1, 25); + buffer.writeUInt16LE(1, 26); + buffer.writeUInt16LE(20, 28); + + const packet = new TCNetErrorPacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.dataType).toBe(2); + expect(packet.layerId).toBe(1); + expect(packet.code).toBe(1); + expect(packet.messageType).toBe(20); + }); + + it("length() は 30 を返す", () => { + expect(new TCNetErrorPacket().length()).toBe(30); + }); +}); From b5f3ef9534adbbd79a7f95741e84d63bc462c740 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:20:20 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat:=20=E3=83=9E=E3=83=AB=E3=83=81?= =?UTF-8?q?=E3=83=91=E3=82=B1=E3=83=83=E3=83=88=E3=83=98=E3=83=83=E3=83=80?= =?UTF-8?q?=E3=83=BC=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB=E3=83=89=E3=82=92?= =?UTF-8?q?=E5=85=AC=E9=96=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #78 --- docs/wiki/Implementation-Status.md | 8 +- src/network.ts | 41 ++++++++ tests/packet-fields.test.ts | 146 ++++++++++++++++++++++++++++- 3 files changed, 190 insertions(+), 5 deletions(-) diff --git a/docs/wiki/Implementation-Status.md b/docs/wiki/Implementation-Status.md index 138b50b..4ff2d9a 100644 --- a/docs/wiki/Implementation-Status.md +++ b/docs/wiki/Implementation-Status.md @@ -31,11 +31,11 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。 |----------|------|--------|------| | 2 | Metrics Data | 122 | BPM, Speed, Position等 | | 4 | Meta Data | 548 | Artist, Title, TrackID。V3.5.0+のUTF-16LEに対応 | -| 8 | Beat Grid Data | 2442 | マルチパケット。MultiPacketAssemblerで自動組み立て | +| 8 | Beat Grid Data | 2442 | マルチパケット。MultiPacketAssemblerで自動組み立て。ヘッダー公開 | | 12 | CUE Data | 436 | 最大18キュー、Loop In/Out Time | -| 16 | Small Wave Form | 2442 | 1200バーの波形データ | -| 32 | Big Wave Form | 可変 | マルチパケット。MultiPacketAssemblerで自動組み立て | -| 128 | Artwork Data | 可変 | マルチパケット。JPEG形式。MultiPacketAssemblerで自動組み立て | +| 16 | Small Wave Form | 2442 | 1200バーの波形データ。マルチパケットヘッダー公開 | +| 32 | Big Wave Form | 可変 | マルチパケット。MultiPacketAssemblerで自動組み立て。ヘッダー公開 | +| 128 | Artwork Data | 可変 | マルチパケット。JPEG形式。MultiPacketAssemblerで自動組み立て。ヘッダー公開 | | 150 | Mixer Data | 270 | 6チャンネル対応 | ## 未実装DataPacketタイプ diff --git a/src/network.ts b/src/network.ts index c52f5ef..98e0d76 100644 --- a/src/network.ts +++ b/src/network.ts @@ -682,6 +682,35 @@ export class TCNetDataPacketCUE extends TCNetDataPacket { } } +/** + * マルチパケットヘッダー情報 (byte 26-41) + * @category Types + */ +export type MultiPacketHeader = { + /** データ全体のサイズ (byte 26-29, UInt32LE) */ + totalDataSize: number; + /** パケット総数 (byte 30-33, UInt32LE) */ + totalPackets: number; + /** パケット番号 (byte 34-37, UInt32LE) */ + packetNo: number; + /** データクラスタサイズ (byte 38-41, UInt32LE) */ + dataClusterSize: number; +}; + +/** + * マルチパケットヘッダーをバッファから読み取るヘルパー + * @param buffer - 読み取り元バッファ + * @returns マルチパケットヘッダー情報 + */ +function readMultiPacketHeader(buffer: Buffer): MultiPacketHeader { + return { + totalDataSize: buffer.readUInt32LE(26), + totalPackets: buffer.readUInt32LE(30), + packetNo: buffer.readUInt32LE(34), + dataClusterSize: buffer.readUInt32LE(38), + }; +} + /** * 波形バーを共通パースするファイル内ヘルパー関数。 * dataStart から source の末尾 (または dataStart + maxBytes の手前) まで @@ -712,9 +741,12 @@ function parseWaveformBars(source: Buffer, dataStart: number, maxBytes?: number) */ export class TCNetDataPacketSmallWaveForm extends TCNetDataPacket { data: WaveformData | null = null; + /** マルチパケットヘッダー */ + multiPacketHeader: MultiPacketHeader | null = null; /** バッファからパケットデータを読み取る */ read(): void { + this.multiPacketHeader = readMultiPacketHeader(this.buffer); // T5: バッファが 2400 バイトに満たない場合でもクラッシュしない this.data = { bars: parseWaveformBars(this.buffer, 42, 2400) }; } @@ -839,9 +871,12 @@ export class TCNetDataPacketMixer extends TCNetDataPacket { */ export class TCNetDataPacketBeatGrid extends TCNetDataPacket { data: BeatGridData | null = null; + /** マルチパケットヘッダー */ + multiPacketHeader: MultiPacketHeader | null = null; /** バッファからパケットデータを読み取る */ read(): void { + this.multiPacketHeader = readMultiPacketHeader(this.buffer); this.readFromOffset(42); } @@ -891,9 +926,12 @@ export class TCNetDataPacketBeatGrid extends TCNetDataPacket { */ export class TCNetDataPacketBigWaveForm extends TCNetDataPacket { data: WaveformData | null = null; + /** マルチパケットヘッダー */ + multiPacketHeader: MultiPacketHeader | null = null; /** バッファからパケットデータを読み取る */ read(): void { + this.multiPacketHeader = readMultiPacketHeader(this.buffer); // T7: parseWaveformBars ヘルパーを使用して重複を排除する this.data = { bars: parseWaveformBars(this.buffer, 42) }; } @@ -943,6 +981,8 @@ export class TCNetFilePacket extends TCNetDataPacket { */ export class TCNetDataPacketArtwork extends TCNetDataPacket { data: ArtworkData | null = null; + /** マルチパケットヘッダー */ + multiPacketHeader: MultiPacketHeader | null = null; /** バッファからパケットデータを読み取る */ read(): void { @@ -950,6 +990,7 @@ export class TCNetDataPacketArtwork extends TCNetDataPacket { if (this.buffer.length < dataStart) { return; } + this.multiPacketHeader = readMultiPacketHeader(this.buffer); const clusterSize = this.buffer.readUInt32LE(38); const end = getClusterEnd(this.buffer.length, dataStart, clusterSize); this.data = { jpeg: Buffer.from(this.buffer.slice(dataStart, end)) }; diff --git a/tests/packet-fields.test.ts b/tests/packet-fields.test.ts index dc8e234..a803783 100644 --- a/tests/packet-fields.test.ts +++ b/tests/packet-fields.test.ts @@ -1,5 +1,13 @@ import { describe, it, expect } from "vitest"; -import { TCNetMessageType, TCNetErrorPacket, TCNetManagementHeader } from "../src/network"; +import { + TCNetMessageType, + TCNetErrorPacket, + TCNetDataPacketSmallWaveForm, + TCNetDataPacketBigWaveForm, + TCNetDataPacketBeatGrid, + TCNetDataPacketArtwork, + TCNetManagementHeader, +} from "../src/network"; function writeValidHeader(buffer: Buffer, messageType: number): void { buffer.writeUInt16LE(1, 0); @@ -80,3 +88,139 @@ describe("TCNetErrorPacket 構造化フィールド", () => { expect(new TCNetErrorPacket().length()).toBe(30); }); }); + +/** + * マルチパケットヘッダーをバッファに書き込む + * @param buffer - 書き込み先バッファ + * @param totalDataSize - データ全体のサイズ + * @param totalPackets - パケット総数 + * @param packetNo - パケット番号 + * @param dataClusterSize - データクラスタサイズ + */ +function writeMultiPacketHeader( + buffer: Buffer, + totalDataSize: number, + totalPackets: number, + packetNo: number, + dataClusterSize: number, +): void { + buffer.writeUInt32LE(totalDataSize, 26); + buffer.writeUInt32LE(totalPackets, 30); + buffer.writeUInt32LE(packetNo, 34); + buffer.writeUInt32LE(dataClusterSize, 38); +} + +describe("マルチパケットヘッダー公開", () => { + describe("TCNetDataPacketSmallWaveForm", () => { + it("マルチパケットヘッダーフィールドが読み取れる", () => { + const buffer = Buffer.alloc(2442); + buffer.writeUInt8(3, 2); + buffer.write("TCN", 4, "ascii"); + buffer.writeUInt8(200, 7); + buffer.writeUInt8(16, 24); // dataType + buffer.writeUInt8(1, 25); // layer + writeMultiPacketHeader(buffer, 2400, 1, 0, 2400); + + const packet = new TCNetDataPacketSmallWaveForm(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.multiPacketHeader).not.toBeNull(); + expect(packet.multiPacketHeader!.totalDataSize).toBe(2400); + expect(packet.multiPacketHeader!.totalPackets).toBe(1); + expect(packet.multiPacketHeader!.packetNo).toBe(0); + expect(packet.multiPacketHeader!.dataClusterSize).toBe(2400); + }); + }); + + describe("TCNetDataPacketBeatGrid", () => { + it("マルチパケットヘッダーフィールドが読み取れる", () => { + const buffer = Buffer.alloc(2442); + buffer.writeUInt8(3, 2); + buffer.write("TCN", 4, "ascii"); + buffer.writeUInt8(200, 7); + buffer.writeUInt8(8, 24); // dataType + buffer.writeUInt8(1, 25); + writeMultiPacketHeader(buffer, 9600, 2, 1, 4800); + + const packet = new TCNetDataPacketBeatGrid(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.multiPacketHeader).not.toBeNull(); + expect(packet.multiPacketHeader!.totalDataSize).toBe(9600); + expect(packet.multiPacketHeader!.totalPackets).toBe(2); + expect(packet.multiPacketHeader!.packetNo).toBe(1); + expect(packet.multiPacketHeader!.dataClusterSize).toBe(4800); + }); + }); + + describe("TCNetDataPacketBigWaveForm", () => { + it("マルチパケットヘッダーフィールドが読み取れる", () => { + const buffer = Buffer.alloc(100); + buffer.writeUInt8(3, 2); + buffer.write("TCN", 4, "ascii"); + buffer.writeUInt8(200, 7); + buffer.writeUInt8(32, 24); // dataType + buffer.writeUInt8(1, 25); + writeMultiPacketHeader(buffer, 48000, 10, 3, 4800); + + const packet = new TCNetDataPacketBigWaveForm(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.multiPacketHeader).not.toBeNull(); + expect(packet.multiPacketHeader!.totalDataSize).toBe(48000); + expect(packet.multiPacketHeader!.totalPackets).toBe(10); + expect(packet.multiPacketHeader!.packetNo).toBe(3); + expect(packet.multiPacketHeader!.dataClusterSize).toBe(4800); + }); + }); + + describe("TCNetDataPacketArtwork", () => { + it("マルチパケットヘッダーフィールドが読み取れる", () => { + // JPEGデータを含む最小バッファを作成 + const buffer = Buffer.alloc(50); + buffer.writeUInt8(3, 2); + buffer.write("TCN", 4, "ascii"); + buffer.writeUInt8(204, 7); // File + buffer.writeUInt8(128, 24); // dataType = Artwork + buffer.writeUInt8(1, 25); + writeMultiPacketHeader(buffer, 5000, 3, 0, 2400); + // JPEGヘッダー + buffer.writeUInt8(0xff, 42); + buffer.writeUInt8(0xd8, 43); + + const packet = new TCNetDataPacketArtwork(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.multiPacketHeader).not.toBeNull(); + expect(packet.multiPacketHeader!.totalDataSize).toBe(5000); + expect(packet.multiPacketHeader!.totalPackets).toBe(3); + expect(packet.multiPacketHeader!.packetNo).toBe(0); + expect(packet.multiPacketHeader!.dataClusterSize).toBe(2400); + }); + + it("バッファが42バイト未満の場合はmultiPacketHeaderがnullのまま", () => { + const buffer = Buffer.alloc(30); + buffer.writeUInt8(3, 2); + buffer.write("TCN", 4, "ascii"); + buffer.writeUInt8(204, 7); + buffer.writeUInt8(128, 24); + buffer.writeUInt8(1, 25); + + const packet = new TCNetDataPacketArtwork(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.multiPacketHeader).toBeNull(); + expect(packet.data).toBeNull(); + }); + }); +}); From fd23a26f419270a3b5a3bd60d2af4575ef365e07 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:21:50 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20Time=E3=83=91=E3=82=B1=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=81=AETimecode=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=82=92=E8=AA=AD=E3=81=BF=E5=8F=96=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #76 --- docs/wiki/Implementation-Status.md | 4 +- src/network.ts | 38 +++++++++++- tests/packet-fields.test.ts | 97 ++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/docs/wiki/Implementation-Status.md b/docs/wiki/Implementation-Status.md index 4ff2d9a..b6c1f8f 100644 --- a/docs/wiki/Implementation-Status.md +++ b/docs/wiki/Implementation-Status.md @@ -14,7 +14,7 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。 | 13 | Error/Notification | 30 | 受信のみ。dataType/layerId/code/messageType構造化済み | | 30 | Application Specific Data | 可変 | 送受信対応。TCNASDP認証のハンドシェイクに使用 | | 204 | File | 可変 | 受信のみ。Artwork(JPEG)のファイル転送。マルチパケット対応 | -| 254 | Time | 154/162 | 受信のみ。OnAirセクション(V3.3.3+)の有無を自動判定する | +| 254 | Time | 154/162 | 受信のみ。OnAirセクション(V3.3.3+)の有無を自動判定。Timecodeセクション対応 | ## 未実装メッセージ @@ -47,7 +47,7 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。 | 項目 | 仕様 | 実装 | 影響 | |------|------|------|------| | MetaData V3.4以前 | UTF-8(256文字) | 未対応(パケット破棄) | V3.5.0未満のノードからMetaDataを受信できない | -| Timeパケットサイズ | 162バイト(V3.3.3+) | 154または162バイト | buffer.lengthで自動判定しており、OnAir未対応ノードも正常に処理できる | +| Timeパケットサイズ | 162バイト(V3.3.3+) | 154または162バイト | buffer.lengthで自動判定しており、OnAir未対応ノードも正常に処理できる。Timecodeセクションは154バイト以上で読み取り | ## TCNASDP認証 diff --git a/src/network.ts b/src/network.ts index 98e0d76..8cbb16b 100644 --- a/src/network.ts +++ b/src/network.ts @@ -409,6 +409,25 @@ export class TCNetTimecode { } } +/** + * Timeパケットのレイヤー別タイムコードデータ + * @category Types + */ +export type TCNetTimePacketTimecode = { + /** レイヤー固有のSMPTEモード */ + smpteMode: number; + /** タイムコード状態 */ + state: TCNetTimecodeState; + /** 時 */ + hours: number; + /** 分 */ + minutes: number; + /** 秒 */ + seconds: number; + /** フレーム */ + frames: number; +}; + /** * Timeパケットの1レイヤー分のデータ * @category Types @@ -419,6 +438,8 @@ export type TCNetTimePacketLayer = { beatMarker: number; state: TCNetLayerStatus; onAir: number; + /** レイヤー別タイムコード (byte 106-153, バッファが十分な場合のみ) */ + timecode?: TCNetTimePacketTimecode; }; /** @@ -431,14 +452,29 @@ export class TCNetTimePacket extends TCNetPacket { /** バッファからパケットデータを読み取る */ read(): void { + // Timecodeセクション (byte 106-153) は8レイヤー x 6バイト = 48バイト + // 154バイトパケットには含まれるが、将来的に短いバッファが来る可能性に備えガードする + const hasTimecode = this.buffer.length >= 154; for (let n = 0; n < 8; n++) { - this._layers[n] = { + const layer: TCNetTimePacketLayer = { currentTimeMillis: this.buffer.readUInt32LE(24 + n * 4), totalTimeMillis: this.buffer.readUInt32LE(56 + n * 4), beatMarker: this.buffer.readUInt8(88 + n), state: this.buffer.readUInt8(96 + n) as TCNetLayerStatus, onAir: this.buffer.length > 154 ? this.buffer.readUInt8(154 + n) : 255, }; + if (hasTimecode) { + const tcOffset = 106 + n * 6; + layer.timecode = { + smpteMode: this.buffer.readUInt8(tcOffset), + state: this.buffer.readUInt8(tcOffset + 1) as TCNetTimecodeState, + hours: this.buffer.readUInt8(tcOffset + 2), + minutes: this.buffer.readUInt8(tcOffset + 3), + seconds: this.buffer.readUInt8(tcOffset + 4), + frames: this.buffer.readUInt8(tcOffset + 5), + }; + } + this._layers[n] = layer; } this._generalSMPTEMode = this.buffer.readUInt8(105); } diff --git a/tests/packet-fields.test.ts b/tests/packet-fields.test.ts index a803783..36191f9 100644 --- a/tests/packet-fields.test.ts +++ b/tests/packet-fields.test.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from "vitest"; import { TCNetMessageType, TCNetErrorPacket, + TCNetTimePacket, + TCNetTimecodeState, TCNetDataPacketSmallWaveForm, TCNetDataPacketBigWaveForm, TCNetDataPacketBeatGrid, @@ -224,3 +226,98 @@ describe("マルチパケットヘッダー公開", () => { }); }); }); + +describe("TCNetTimePacket Timecodeセクション", () => { + it("154バイトバッファからTimecodeセクションを読み取る", () => { + const buffer = Buffer.alloc(154); + writeValidHeader(buffer, TCNetMessageType.Time); + + // レイヤー0のTimecode (offset=106) + buffer.writeUInt8(2, 106); // smpteMode + buffer.writeUInt8(TCNetTimecodeState.Running, 107); // state + buffer.writeUInt8(1, 108); // hours + buffer.writeUInt8(30, 109); // minutes + buffer.writeUInt8(45, 110); // seconds + buffer.writeUInt8(24, 111); // frames + + // レイヤー7のTimecode (offset=148) + buffer.writeUInt8(3, 148); // smpteMode + buffer.writeUInt8(TCNetTimecodeState.Stopped, 149); // state + buffer.writeUInt8(0, 150); // hours + buffer.writeUInt8(0, 151); // minutes + buffer.writeUInt8(0, 152); // seconds + buffer.writeUInt8(0, 153); // frames + + const packet = new TCNetTimePacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.layers[0].timecode).toBeDefined(); + expect(packet.layers[0].timecode!.smpteMode).toBe(2); + expect(packet.layers[0].timecode!.state).toBe(TCNetTimecodeState.Running); + expect(packet.layers[0].timecode!.hours).toBe(1); + expect(packet.layers[0].timecode!.minutes).toBe(30); + expect(packet.layers[0].timecode!.seconds).toBe(45); + expect(packet.layers[0].timecode!.frames).toBe(24); + + expect(packet.layers[7].timecode).toBeDefined(); + expect(packet.layers[7].timecode!.smpteMode).toBe(3); + expect(packet.layers[7].timecode!.state).toBe(TCNetTimecodeState.Stopped); + }); + + it("162バイトバッファでもTimecodeセクションを読み取る", () => { + const buffer = Buffer.alloc(162); + writeValidHeader(buffer, TCNetMessageType.Time); + + // レイヤー2のTimecode (offset=118) + buffer.writeUInt8(1, 118); // smpteMode + buffer.writeUInt8(TCNetTimecodeState.ForceReSync, 119); // state + buffer.writeUInt8(23, 120); // hours + buffer.writeUInt8(59, 121); // minutes + buffer.writeUInt8(59, 122); // seconds + buffer.writeUInt8(29, 123); // frames + + const packet = new TCNetTimePacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.layers[2].timecode).toBeDefined(); + expect(packet.layers[2].timecode!.smpteMode).toBe(1); + expect(packet.layers[2].timecode!.state).toBe(TCNetTimecodeState.ForceReSync); + expect(packet.layers[2].timecode!.hours).toBe(23); + expect(packet.layers[2].timecode!.minutes).toBe(59); + expect(packet.layers[2].timecode!.seconds).toBe(59); + expect(packet.layers[2].timecode!.frames).toBe(29); + }); + + it("全8レイヤーのTimecodeが独立して読み取れる", () => { + const buffer = Buffer.alloc(154); + writeValidHeader(buffer, TCNetMessageType.Time); + + for (let n = 0; n < 8; n++) { + const offset = 106 + n * 6; + buffer.writeUInt8(n, offset); // smpteMode = レイヤー番号 + buffer.writeUInt8(1, offset + 1); // state = Running + buffer.writeUInt8(n + 1, offset + 2); // hours + buffer.writeUInt8(n * 5, offset + 3); // minutes + buffer.writeUInt8(n * 7, offset + 4); // seconds + buffer.writeUInt8(n * 3, offset + 5); // frames + } + + const packet = new TCNetTimePacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + for (let n = 0; n < 8; n++) { + expect(packet.layers[n].timecode).toBeDefined(); + expect(packet.layers[n].timecode!.smpteMode).toBe(n); + expect(packet.layers[n].timecode!.hours).toBe(n + 1); + expect(packet.layers[n].timecode!.minutes).toBe(n * 5); + expect(packet.layers[n].timecode!.seconds).toBe(n * 7); + expect(packet.layers[n].timecode!.frames).toBe(n * 3); + } + }); +}); From 4cf8721c0b9c790339f34c6b2ee6adfbc0cd5a22 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:23:04 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20Status=E3=83=91=E3=82=B1=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=81=AEAPP=20SPECIFIC=E3=82=BB=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E8=AA=AD=E3=81=BF=E5=8F=96=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #80 --- .changeset/packet-field-extensions.md | 5 +++ docs/wiki/Implementation-Status.md | 2 +- src/network.ts | 7 ++++ tests/packet-fields.test.ts | 55 +++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 .changeset/packet-field-extensions.md diff --git a/.changeset/packet-field-extensions.md b/.changeset/packet-field-extensions.md new file mode 100644 index 0000000..5307758 --- /dev/null +++ b/.changeset/packet-field-extensions.md @@ -0,0 +1,5 @@ +--- +"@9c5s/node-tcnet": minor +--- + +パケットフィールド拡充: Timecodeセクション, Error構造化, マルチパケットヘッダー公開, APP SPECIFIC diff --git a/docs/wiki/Implementation-Status.md b/docs/wiki/Implementation-Status.md index b6c1f8f..2def510 100644 --- a/docs/wiki/Implementation-Status.md +++ b/docs/wiki/Implementation-Status.md @@ -8,7 +8,7 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。 |------|------|--------|------| | 2 | OptIn | 68 | 送受信対応 | | 3 | OptOut | 28 | 送受信対応 | -| 5 | Status | 300 | 受信のみ | +| 5 | Status | 300 | 受信のみ。APP SPECIFICセクション(byte 100-171)対応 | | 20 | Request | 26 | 送信のみ | | 200 | Data | 可変 | 受信のみ(DataTypeによる) | | 13 | Error/Notification | 30 | 受信のみ。dataType/layerId/code/messageType構造化済み | diff --git a/src/network.ts b/src/network.ts index 8cbb16b..6aca805 100644 --- a/src/network.ts +++ b/src/network.ts @@ -292,6 +292,9 @@ export class TCNetStatusPacket extends TCNetPacket { name: string; }> = new Array(8); + /** APP SPECIFICセクション (byte 100-171, 72バイト) */ + appSpecific: Buffer | null = null; + /** バッファからパケットデータを読み取る */ read(): void { this.data = { @@ -301,6 +304,10 @@ export class TCNetStatusPacket extends TCNetPacket { autoMasterMode: this.buffer.readUInt8(84), }; + if (this.buffer.length >= 172) { + this.appSpecific = Buffer.from(this.buffer.slice(100, 172)); + } + for (let n = 0; n < 8; n++) { this.layers[n] = { source: this.buffer.readUInt8(34 + n), diff --git a/tests/packet-fields.test.ts b/tests/packet-fields.test.ts index 36191f9..42b66cb 100644 --- a/tests/packet-fields.test.ts +++ b/tests/packet-fields.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { TCNetMessageType, TCNetErrorPacket, + TCNetStatusPacket, TCNetTimePacket, TCNetTimecodeState, TCNetDataPacketSmallWaveForm, @@ -321,3 +322,57 @@ describe("TCNetTimePacket Timecodeセクション", () => { } }); }); + +describe("TCNetStatusPacket APP SPECIFIC", () => { + it("appSpecificフィールドに byte 100-171 が読み取れる", () => { + const buffer = Buffer.alloc(300); + writeValidHeader(buffer, TCNetMessageType.Status); + buffer.writeUInt16LE(1, 24); // nodeCount + buffer.writeUInt16LE(60000, 26); // nodeListenerPort + + // APP SPECIFICセクションにテストデータを書き込む + for (let i = 0; i < 72; i++) { + buffer.writeUInt8(i + 1, 100 + i); + } + + const packet = new TCNetStatusPacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.appSpecific).not.toBeNull(); + expect(packet.appSpecific!.length).toBe(72); + expect(packet.appSpecific![0]).toBe(1); + expect(packet.appSpecific![71]).toBe(72); + }); + + it("appSpecificは元バッファのコピーである", () => { + const buffer = Buffer.alloc(300); + writeValidHeader(buffer, TCNetMessageType.Status); + + buffer.writeUInt8(0xab, 100); + + const packet = new TCNetStatusPacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + // 元バッファを変更してもappSpecificに影響しない + buffer.writeUInt8(0x00, 100); + expect(packet.appSpecific![0]).toBe(0xab); + }); + + it("全バイトが0x00の場合も正常に読み取れる", () => { + const buffer = Buffer.alloc(300); + writeValidHeader(buffer, TCNetMessageType.Status); + + const packet = new TCNetStatusPacket(); + packet.buffer = buffer; + packet.header = createHeader(buffer); + packet.read(); + + expect(packet.appSpecific).not.toBeNull(); + expect(packet.appSpecific!.length).toBe(72); + expect(packet.appSpecific!.every((b) => b === 0)).toBe(true); + }); +}); From 41f9b3d5c560f92d1d82b0a68280c9be03e01545 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:45:41 +0900 Subject: [PATCH 08/16] =?UTF-8?q?fix:=20parsePacket=E3=81=AE=E6=9C=AA?= =?UTF-8?q?=E5=AE=9A=E7=BE=A9messageType=E3=82=AF=E3=83=A9=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A5=E4=BF=AE=E6=AD=A3=E3=81=A8=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parsePacketのnullチェックを!= nullに統一 (#73と同根のバグ) - changeset本文にBREAKING CHANGE注記を追加 - auth.test.tsのテスト名を実態に合わせて修正 --- .changeset/packet-field-extensions.md | 2 ++ src/tcnet.ts | 2 +- tests/auth.test.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.changeset/packet-field-extensions.md b/.changeset/packet-field-extensions.md index 5307758..60b2c75 100644 --- a/.changeset/packet-field-extensions.md +++ b/.changeset/packet-field-extensions.md @@ -3,3 +3,5 @@ --- パケットフィールド拡充: Timecodeセクション, Error構造化, マルチパケットヘッダー公開, APP SPECIFIC + +BREAKING CHANGE: TCNetErrorPacket.errorDataを削除し、dataType/layerId/code/messageTypeに置換 diff --git a/src/tcnet.ts b/src/tcnet.ts index 2ed8f8a..cee7108 100644 --- a/src/tcnet.ts +++ b/src/tcnet.ts @@ -534,7 +534,7 @@ export class TCNetClient extends EventEmitter { */ private parsePacket(header: nw.TCNetManagementHeader): nw.TCNetPacket | null { const packetClass = nw.TCNetPackets[header.messageType]; - if (packetClass !== null) { + if (packetClass != null) { const packet = new packetClass(); // 可変長メッセージはバッファから長さを判定する必要があるため、 // length()の前にbufferとheaderを設定する diff --git a/tests/auth.test.ts b/tests/auth.test.ts index d279500..ebceb86 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -194,7 +194,7 @@ describe("TCNetApplicationDataPacket", () => { }); describe("TCNetErrorPacket", () => { - it("認証成功のErrorパケットを構造化フィールドでパースする", () => { + it("ErrorパケットのフィールドをUInt16LEで正しくパースする", () => { // Arrange const buffer = Buffer.alloc(30); writeValidHeader(buffer, TCNetMessageType.Error); From 98bea388cd2791c38616a0cd64b9eac34fc6b569 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:48:27 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20TCNetTimePacketTimecode?= =?UTF-8?q?=E3=82=92TCNetTimecode=E3=81=AB=E7=B5=B1=E4=B8=80=E3=81=97?= =?UTF-8?q?=E3=83=87=E3=83=83=E3=83=89=E3=82=AC=E3=83=BC=E3=83=89=E3=81=AB?= =?UTF-8?q?=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TCNetTimecode.modeをsmpteModeにリネーム - TCNetTimePacketTimecode型を削除しTCNetTimecodeクラスで置換 - TimePacket.read()でTCNetTimecode.read()を活用 - StatusPacket.appSpecificの防御的ガードに意図コメント追加 --- src/network.ts | 38 +++++++----------------------------- tests/packet-parsers.test.ts | 4 ++-- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/src/network.ts b/src/network.ts index 6aca805..f78998f 100644 --- a/src/network.ts +++ b/src/network.ts @@ -304,6 +304,7 @@ export class TCNetStatusPacket extends TCNetPacket { autoMasterMode: this.buffer.readUInt8(84), }; + // parsePacketがlength()=300で事前検証するため常にtrueだが防御的に残す if (this.buffer.length >= 172) { this.appSpecific = Buffer.from(this.buffer.slice(100, 172)); } @@ -394,7 +395,7 @@ export type TCNetTimecodeState = (typeof TCNetTimecodeState)[keyof typeof TCNetT * @category Packets */ export class TCNetTimecode { - mode!: number; + smpteMode!: number; state!: TCNetTimecodeState; hours!: number; minutes!: number; @@ -407,7 +408,7 @@ export class TCNetTimecode { * @param offset - 読み取り開始位置 */ read(buffer: Buffer, offset: number): void { - this.mode = buffer.readUInt8(offset + 0); + this.smpteMode = buffer.readUInt8(offset + 0); this.state = buffer.readUInt8(offset + 1) as TCNetTimecodeState; this.hours = buffer.readUInt8(offset + 2); this.minutes = buffer.readUInt8(offset + 3); @@ -416,25 +417,6 @@ export class TCNetTimecode { } } -/** - * Timeパケットのレイヤー別タイムコードデータ - * @category Types - */ -export type TCNetTimePacketTimecode = { - /** レイヤー固有のSMPTEモード */ - smpteMode: number; - /** タイムコード状態 */ - state: TCNetTimecodeState; - /** 時 */ - hours: number; - /** 分 */ - minutes: number; - /** 秒 */ - seconds: number; - /** フレーム */ - frames: number; -}; - /** * Timeパケットの1レイヤー分のデータ * @category Types @@ -446,7 +428,7 @@ export type TCNetTimePacketLayer = { state: TCNetLayerStatus; onAir: number; /** レイヤー別タイムコード (byte 106-153, バッファが十分な場合のみ) */ - timecode?: TCNetTimePacketTimecode; + timecode?: TCNetTimecode; }; /** @@ -471,15 +453,9 @@ export class TCNetTimePacket extends TCNetPacket { onAir: this.buffer.length > 154 ? this.buffer.readUInt8(154 + n) : 255, }; if (hasTimecode) { - const tcOffset = 106 + n * 6; - layer.timecode = { - smpteMode: this.buffer.readUInt8(tcOffset), - state: this.buffer.readUInt8(tcOffset + 1) as TCNetTimecodeState, - hours: this.buffer.readUInt8(tcOffset + 2), - minutes: this.buffer.readUInt8(tcOffset + 3), - seconds: this.buffer.readUInt8(tcOffset + 4), - frames: this.buffer.readUInt8(tcOffset + 5), - }; + const tc = new TCNetTimecode(); + tc.read(this.buffer, 106 + n * 6); + layer.timecode = tc; } this._layers[n] = layer; } diff --git a/tests/packet-parsers.test.ts b/tests/packet-parsers.test.ts index 6618833..752c0ac 100644 --- a/tests/packet-parsers.test.ts +++ b/tests/packet-parsers.test.ts @@ -406,7 +406,7 @@ describe("TCNetTimecode", () => { tc.read(buffer, 10); // Assert - expect(tc.mode).toBe(1); + expect(tc.smpteMode).toBe(1); expect(tc.state).toBe(TCNetTimecodeState.Running); expect(tc.hours).toBe(2); expect(tc.minutes).toBe(30); @@ -429,7 +429,7 @@ describe("TCNetTimecode", () => { tc.read(buffer, 0); // Assert - expect(tc.mode).toBe(0); + expect(tc.smpteMode).toBe(0); expect(tc.state).toBe(TCNetTimecodeState.Stopped); expect(tc.hours).toBe(0); expect(tc.minutes).toBe(0); From a399eb0ea6e72fc62a21e1c00ac63f82a5700115 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:39:38 +0900 Subject: [PATCH 10/16] =?UTF-8?q?docs:=20Mixer=20=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E5=88=B6=E9=99=90=E3=82=92=E5=AE=9F=E6=A9=9F=E6=A4=9C?= =?UTF-8?q?=E8=A8=BC=E7=B5=90=E6=9E=9C=E3=81=AB=E5=9F=BA=E3=81=A5=E3=81=8D?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit マスター種類によるフィールド取得可否の差異を表で記載 - masterAudioLevel: 常に0 (マスター種類に依らない) - チャンネルaudioLevel: SKマスターのみ取得可能 - EQ/Filter/Trim等: Bridgeマスターのみ取得可能 - フェーダー位置: 両方で取得可能 --- docs/wiki/PRO-DJ-LINK-Bridge.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/wiki/PRO-DJ-LINK-Bridge.md b/docs/wiki/PRO-DJ-LINK-Bridge.md index e46a837..ad87971 100644 --- a/docs/wiki/PRO-DJ-LINK-Bridge.md +++ b/docs/wiki/PRO-DJ-LINK-Bridge.md @@ -48,12 +48,16 @@ byte 44-45に`0x001e` (=30) が入ることもある。用途不明 (値30はSMP ### Mixer -以下のフィールドが常に0で返される。 +`masterAudioLevel`は常に0で返される (TCNetマスターの種類に依らない)。 -- `masterAudioLevel` -- 全チャンネルの `audioLevel` +その他のフィールドの取得可否はTCNetマスターの種類に依存する。 -フェーダー位置 (`faderLevel`, `crossFader`), TRIMノブ (`trimLevel`), EQ等の物理コントロール値はリアルタイムで取得可能。Audio Levelは音量レベルメーター相当の値だが、Bridge経由では配信されない。 +| フィールド群 | Bridgeマスター | ShowKontrolマスター | +|-------------|--------------|-------------------| +| masterAudioLevel | 0 | 0 | +| チャンネル audioLevel | 0 | リアルタイム取得可能 | +| EQ, Filter, Trim等のノブ系 | リアルタイム取得可能 | 0 | +| フェーダー位置 (faderLevel, crossFader) | リアルタイム取得可能 | リアルタイム取得可能 | ### CUE From 94b2d92f178d1b1804a72d128ff264430fdf2ff2 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:59:37 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor!:=20crossfaderAssign=E3=82=92cro?= =?UTF-8?q?ssFaderAssign=E3=81=AB=E3=83=AA=E3=83=8D=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E3=81=97Wiki=E7=94=A8=E8=AA=9E=E3=82=92=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: MixerChannel.crossfaderAssignをcrossFaderAssignに変更 - MixerData側のcrossFader/crossFaderCurveと命名規則を統一 - Implementation-Status.mdのSmallWaveForm備考を他行と統一 - Bridge WikiにTCNetマスター種類の説明を追加 --- docs/wiki/Implementation-Status.md | 2 +- docs/wiki/PRO-DJ-LINK-Bridge.md | 4 ++-- src/network.ts | 4 ++-- src/types.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/wiki/Implementation-Status.md b/docs/wiki/Implementation-Status.md index 2def510..52a00a0 100644 --- a/docs/wiki/Implementation-Status.md +++ b/docs/wiki/Implementation-Status.md @@ -33,7 +33,7 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。 | 4 | Meta Data | 548 | Artist, Title, TrackID。V3.5.0+のUTF-16LEに対応 | | 8 | Beat Grid Data | 2442 | マルチパケット。MultiPacketAssemblerで自動組み立て。ヘッダー公開 | | 12 | CUE Data | 436 | 最大18キュー、Loop In/Out Time | -| 16 | Small Wave Form | 2442 | 1200バーの波形データ。マルチパケットヘッダー公開 | +| 16 | Small Wave Form | 2442 | 1200バーの波形データ。ヘッダー公開 | | 32 | Big Wave Form | 可変 | マルチパケット。MultiPacketAssemblerで自動組み立て。ヘッダー公開 | | 128 | Artwork Data | 可変 | マルチパケット。JPEG形式。MultiPacketAssemblerで自動組み立て。ヘッダー公開 | | 150 | Mixer Data | 270 | 6チャンネル対応 | diff --git a/docs/wiki/PRO-DJ-LINK-Bridge.md b/docs/wiki/PRO-DJ-LINK-Bridge.md index ad87971..38a13a2 100644 --- a/docs/wiki/PRO-DJ-LINK-Bridge.md +++ b/docs/wiki/PRO-DJ-LINK-Bridge.md @@ -48,9 +48,9 @@ byte 44-45に`0x001e` (=30) が入ることもある。用途不明 (値30はSMP ### Mixer -`masterAudioLevel`は常に0で返される (TCNetマスターの種類に依らない)。 +TCNetマスターの種類 (StatusパケットのautoMasterModeまたはノードリストで確認可能) によって取得できるフィールドが異なる。 -その他のフィールドの取得可否はTCNetマスターの種類に依存する。 +`masterAudioLevel`は常に0で返される (TCNetマスターの種類に依らない)。 | フィールド群 | Bridgeマスター | ShowKontrolマスター | |-------------|--------------|-------------------| diff --git a/src/network.ts b/src/network.ts index f78998f..acad814 100644 --- a/src/network.ts +++ b/src/network.ts @@ -792,7 +792,7 @@ export class TCNetDataPacketMixer extends TCNetDataPacket { /** バッファからパケットデータを読み取る */ read(): void { - // T6: 最大オフセット (channels[5] の crossfaderAssign = 245 + 13 = 258) を確認する + // T6: 最大オフセット (channels[5] の crossFaderAssign = 245 + 13 = 258) を確認する if (this.buffer.length < 259) { return; } @@ -814,7 +814,7 @@ export class TCNetDataPacketMixer extends TCNetDataPacket { send: this.buffer.readUInt8(offset + 10), cueA: this.buffer.readUInt8(offset + 11), cueB: this.buffer.readUInt8(offset + 12), - crossfaderAssign: this.buffer.readUInt8(offset + 13), + crossFaderAssign: this.buffer.readUInt8(offset + 13), }); this.data = { diff --git a/src/types.ts b/src/types.ts index 46ead3f..a7be888 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,7 +72,7 @@ export type MixerChannel = { send: number; cueA: number; cueB: number; - crossfaderAssign: number; + crossFaderAssign: number; }; /** From aaa20adbce9994bdf8b9fa7901a97c8a689ba32c Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:06:36 +0900 Subject: [PATCH 12/16] =?UTF-8?q?test:=20receiveUnicast=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=83=91=E3=82=B9=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=A8=E3=83=90=E3=83=83=E3=83=95=E3=82=A1=E9=95=B7=E3=82=AC?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - readMultiPacketHeaderに42バイト未満ガードを追加 (短バッファでRangeError防止) - receiveUnicastに24バイト未満の早期リターンを追加 - マルチパケットアセンブラ分岐に42バイト未満ガードを追加 - テスト: 短バッファ/未定義dataType/requestDataバリデーションを網羅 closes #85 --- src/network.ts | 3 ++- src/tcnet.ts | 3 ++- tests/receive-unicast.test.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/network.ts b/src/network.ts index acad814..318a22c 100644 --- a/src/network.ts +++ b/src/network.ts @@ -721,7 +721,8 @@ export type MultiPacketHeader = { * @param buffer - 読み取り元バッファ * @returns マルチパケットヘッダー情報 */ -function readMultiPacketHeader(buffer: Buffer): MultiPacketHeader { +function readMultiPacketHeader(buffer: Buffer): MultiPacketHeader | null { + if (buffer.length < 42) return null; return { totalDataSize: buffer.readUInt32LE(26), totalPackets: buffer.readUInt32LE(30), diff --git a/src/tcnet.ts b/src/tcnet.ts index cee7108..06e1b18 100644 --- a/src/tcnet.ts +++ b/src/tcnet.ts @@ -711,6 +711,7 @@ export class TCNetClient extends EventEmitter { * @param rinfo - 送信元情報 */ private receiveUnicast(msg: Buffer, rinfo: RemoteInfo): void { + if (msg.length < 24) return; const mgmtHeader = new nw.TCNetManagementHeader(msg); mgmtHeader.read(); const packet = this.parsePacket(mgmtHeader); @@ -729,7 +730,7 @@ export class TCNetClient extends EventEmitter { const pendingRequest = this.requests.get(key); if (pendingRequest && pendingRequest.assembler) { - // マルチパケット: アセンブラに蓄積 + if (msg.length < 42) return; const totalPackets = msg.readUInt32LE(30); const isFilePacket = packet instanceof nw.TCNetFilePacket; if (isFilePacket && (totalPackets === 0 || pendingRequest.fileChunks)) { diff --git a/tests/receive-unicast.test.ts b/tests/receive-unicast.test.ts index a01c088..337cea6 100644 --- a/tests/receive-unicast.test.ts +++ b/tests/receive-unicast.test.ts @@ -42,6 +42,38 @@ describe("receiveUnicast マルチパケット対応", () => { }); }); +describe("receiveUnicast エラーハンドリング", () => { + it("24バイト未満のバッファでクラッシュしない", () => { + const client = new TestTCNetClient(); + client.simulateConnected(); + const handler = vi.fn(); + client.on("data", handler); + + expect(() => client.simulateUnicast(Buffer.alloc(10))).not.toThrow(); + expect(handler).not.toHaveBeenCalled(); + }); + + it("42バイト未満のDataパケットでマルチパケットヘッダー読み取り時にクラッシュしない", () => { + const client = new TestTCNetClient(); + client.simulateConnected(); + + const mockSocket = { + send: vi.fn((_buf: Buffer, _port: number, _addr: string, cb: () => void) => cb()), + }; + client.configureMockBroadcast(mockSocket); + + const requestPromise = client.requestData(nw.TCNetDataPacketType.BigWaveFormData, 0); + + const shortBuffer = Buffer.alloc(30); + writeValidHeader(shortBuffer, 200); + shortBuffer.writeUInt8(nw.TCNetDataPacketType.BigWaveFormData, 24); + shortBuffer.writeUInt8(1, 25); + + expect(() => client.simulateUnicast(shortBuffer)).not.toThrow(); + requestPromise.catch(() => {}); + }); +}); + describe("receiveUnicast 未定義dataType", () => { it("未定義dataTypeのDataパケットを受信してもクラッシュしない", () => { const client = new TestTCNetClient(); From d9d6be46d9583f861883895fc56333e36dba56f8 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:03:25 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20Promise=20race=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=AE=E5=85=B1=E9=80=9A=E3=83=98=E3=83=AB?= =?UTF-8?q?=E3=83=91=E3=83=BC=E6=8A=BD=E5=87=BA=E3=81=A8=E5=8F=AF=E8=AA=AD?= =?UTF-8?q?=E6=80=A7=E5=90=91=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #84 --- tests/auth.test.ts | 125 +++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 66 deletions(-) diff --git a/tests/auth.test.ts b/tests/auth.test.ts index ebceb86..10aeeee 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -993,6 +993,41 @@ describe("sendAuthCommandOnly 連続失敗カウンタ", () => { }); describe("pending状態でのcmd=1再受信 (初回認証中の反応型プロトコル)", () => { + /** + * AuthSequenceTestClient にネットワーク設定一式を注入する + * @param client - 設定対象のクライアント + * @param socketSend - カスタム送信コールバック (省略時は即座にcb()を呼ぶno-op) + */ + function setupNetworkDefaults(client: AuthSequenceTestClient, socketSend?: (...args: unknown[]) => void): void { + client.setSelectedAdapter(createAdapter("192.168.0.10")); + client.setBroadcastAddress("255.255.255.255"); + client.setBroadcastSocket({ + send: vi.fn(socketSend ?? ((_buf: Buffer, _port: number, _addr: string, cb: () => void) => cb())), + }); + } + + /** + * detectBridgeIsWindows を手動制御可能な Promise に差し替える。 + * テスト側で resolve/reject を呼ぶまで sendAuthSequence は prepareAuthPayload 内で停止する。 + * @param client - モック対象のクライアント + */ + function mockDetectBridgeIsWindows(client: AuthSequenceTestClient): { + resolve: (value: boolean) => void; + reject: (reason: Error) => void; + } { + let resolveDetection!: (value: boolean) => void; + let rejectDetection!: (reason: Error) => void; + const detectionPromise = new Promise((resolve, reject) => { + resolveDetection = resolve; + rejectDetection = reject; + }); + vi.spyOn( + client as unknown as { detectBridgeIsWindows: () => Promise }, + "detectBridgeIsWindows", + ).mockReturnValue(detectionPromise); + return { resolve: resolveDetection, reject: rejectDetection }; + } + function makePendingClient(logger?: TCNetLogger): AuthSequenceTestClient { const client = new AuthSequenceTestClient(); client.setSessionToken(0xb3fe319e); @@ -1083,25 +1118,22 @@ describe("pending状態でのcmd=1再受信 (初回認証中の反応型プロ }); it("pending中のtoken変化時は古いauthTimeoutIdがクリアされ新タイマーが起動する", async () => { - // 古いタイマーが新世代を早期リセットする Codex#2 回帰テスト + // 回帰防止: token変化時に旧タイマーをクリアしないと、旧タイマーが + // 新世代の認証セッションを早期リセットしてしまう vi.useFakeTimers(); try { const client = new AuthSequenceTestClient(); const sendSequenceMock = vi.fn().mockResolvedValue(undefined); client.setSendAuthSequenceMock(sendSequenceMock); client.setBridgeIsWindows(false); - client.setSelectedAdapter(createAdapter("192.168.0.10")); - client.setBroadcastAddress("255.255.255.255"); - client.setBroadcastSocket({ - send: vi.fn((_buf: Buffer, _port: number, _addr: string, cb: () => void) => cb()), - }); + setupNetworkDefaults(client); // 初回 cmd=1 (token A): pending + 5秒タイマー起動 client.callHandleAuth(createAppDataPacket(1, 0xaaaaaaaa)); expect(client.authenticationState).toBe("pending"); expect(client.getSessionToken()).toBe(0xaaaaaaaa); - // 4.9 秒経過 (古いタイマー発火直前) + // 認証タイムアウト5秒のうち4.9秒経過 (古いタイマー発火直前) await vi.advanceTimersByTimeAsync(4900); expect(client.authenticationState).toBe("pending"); @@ -1112,12 +1144,12 @@ describe("pending状態でのcmd=1再受信 (初回認証中の反応型プロ expect(client.authenticationState).toBe("pending"); expect(sendSequenceMock).toHaveBeenCalledTimes(2); - // さらに 0.2 秒経過 (古いタイマーなら 5.1 秒で発火するタイミング) + // +0.2秒 (通算5.1秒): 旧タイマーが生きていればここで発火するはず await vi.advanceTimersByTimeAsync(200); expect(client.authenticationState).toBe("pending"); expect(client.getSessionToken()).toBe(0xbbbbbbbb); - // 新タイマーの 5秒後に発火すると state=none になる (残り 4.8 秒) + // +4.8秒 (token B起点で5.0秒): 新タイマーが発火し state=none await vi.advanceTimersByTimeAsync(4800); expect(client.authenticationState).toBe("none"); expect(client.getSessionToken()).toBeNull(); @@ -1144,44 +1176,27 @@ describe("pending状態でのcmd=1再受信 (初回認証中の反応型プロ }); it("authenticated遷移後にsendAuthSequenceが遅延rejectしても認証済みセッションは破壊されない", async () => { - // Codex P1#1 回帰: 新 pending fast-path で sendAuthCommandOnly が - // Error(0xffffff) 受信により state=authenticated に遷移した後、 - // 元の sendAuthSequence() promise が遅延 reject した場合、 - // handleInitialAuthRequest の catch が token 一致だけで resetAuthSession を - // 呼んでしまうと authenticated セッションを破壊してしまう。 - // 修正後は state === "pending" のチェックも追加して破壊を防ぐ。 + // 回帰防止: sendAuthSequence の catch が state==="pending" をチェックしないと、 + // authenticated 遷移後の遅延 reject で resetAuthSession が呼ばれセッションが破壊される vi.useFakeTimers(); try { const client = new AuthSequenceTestClient(); - client.setSelectedAdapter(createAdapter("192.168.0.10")); - client.setBroadcastAddress("255.255.255.255"); - client.setBroadcastSocket({ - send: vi.fn((_buf: Buffer, _port: number, _addr: string, cb: () => void) => cb()), - }); - - // sendAuthSequence が prepareAuthPayload 内の detectBridgeIsWindows await で停止するようにする - let rejectDetection!: (reason: Error) => void; - const detectionPromise = new Promise((_resolve, reject) => { - rejectDetection = reject; - }); - vi.spyOn( - client as unknown as { detectBridgeIsWindows: () => Promise }, - "detectBridgeIsWindows", - ).mockReturnValue(detectionPromise); + setupNetworkDefaults(client); + const detection = mockDetectBridgeIsWindows(client); - // 初回 cmd=1 (token A): pending + sendAuthSequence 起動 + // 初回 cmd=1 (token A): sendAuthSequence が detectBridgeIsWindows で停止 client.callHandleAuth(createAppDataPacket(1, 0xaaaaaaaa)); expect(client.authenticationState).toBe("pending"); + // sendAuthSequence 内の 50ms delay を消化し detectBridgeIsWindows await に到達させる await vi.advanceTimersByTimeAsync(50); await flushAsync(); - // sendAuthSequence は detectBridgeIsWindows の await で停止中 // Error(0xffffff) 受信で authenticated に遷移 (他経路で認証が完了した状況を再現) client.callHandleAuth(createErrorPacket(0xff, 0xff, 0xff)); expect(client.authenticationState).toBe("authenticated"); // 元の sendAuthSequence を rejection で終わらせる (送信エラー等を模擬) - rejectDetection(new Error("stale detect rejection")); + detection.reject(new Error("stale detect rejection")); await flushAsync(10); // authenticated が維持されていること (catch の pending ガードが機能) @@ -1193,59 +1208,37 @@ describe("pending状態でのcmd=1再受信 (初回認証中の反応型プロ }); it("pending中のtoken変化時は旧sendAuthSequenceが新世代のstateを破壊しない (stale promise race)", async () => { - // stale Promise race の真の回帰検知テスト。 - // detectBridgeIsWindows を controllable promise でモックし、旧 sendAuthSequence - // を prepareAuthPayload 内部の await で確実に停止させた状態で token を切り替える。 - // これにより旧 run の prepareAuthPayload が null を返す経路に到達し、 - // expectedToken ガードが無いと resetAuthSession() を呼んで新世代の state を破壊する。 + // 回帰防止: expectedToken ガードが無いと、旧 sendAuthSequence の + // prepareAuthPayload が null を返した際に resetAuthSession で新世代を破壊する vi.useFakeTimers(); try { const client = new AuthSequenceTestClient(); - client.setSelectedAdapter(createAdapter("192.168.0.10")); - client.setBroadcastAddress("255.255.255.255"); - client.setBroadcastSocket({ - send: vi.fn((_buf: Buffer, _port: number, _addr: string, cb: () => void) => cb()), - }); - - // detectBridgeIsWindows を手動制御可能な Promise に差し替える - let resolveDetection!: (value: boolean) => void; - const detectionPromise = new Promise((resolve) => { - resolveDetection = resolve; - }); - vi.spyOn( - client as unknown as { detectBridgeIsWindows: () => Promise }, - "detectBridgeIsWindows", - ).mockReturnValue(detectionPromise); + setupNetworkDefaults(client); + const detection = mockDetectBridgeIsWindows(client); - // 初回 cmd=1 (token A): handleInitialAuthRequest → sendAuthSequence 起動 + // 初回 cmd=1 (token A): sendAuthSequence が detectBridgeIsWindows で停止 client.callHandleAuth(createAppDataPacket(1, 0xaaaaaaaa)); expect(client.getSessionToken()).toBe(0xaaaaaaaa); expect(client.authenticationState).toBe("pending"); - // cmd=0 送信の microtask を消化し、50ms wait を進めて prepareAuthPayload 内の - // detectBridgeIsWindows await まで到達させる + // sendAuthSequence 内の 50ms delay を消化し detectBridgeIsWindows await に到達させる await vi.advanceTimersByTimeAsync(50); await flushAsync(); - // token B の cmd=1 受信: handlePendingReauthRequest → resetAuthSession(true) - // + handleInitialAuthRequest(B) で新世代開始。 - // 新 sendAuthSequence も detectBridgeIsWindows の同じ pending promise を待機する + // token B の cmd=1 受信: 新世代開始。新 sendAuthSequence も同じ promise を待機 client.callHandleAuth(createAppDataPacket(1, 0xbbbbbbbb)); expect(client.getSessionToken()).toBe(0xbbbbbbbb); expect(client.authenticationState).toBe("pending"); - // 新世代の 50ms wait も消化して detectBridgeIsWindows await まで到達させる + // 新世代の 50ms delay を消化し detectBridgeIsWindows await に到達させる await vi.advanceTimersByTimeAsync(50); await flushAsync(); - // detectionPromise を resolve し、旧・新の両方を再開させる - // 旧 run: tokenBeforePing=A !== sessionToken=B → prepareAuthPayload null 返却 - // → expectedToken ガードで resetAuthSession を呼ばずに return - // 新 run: tokenBeforePing=B === sessionToken=B → payload 生成 → cmd=2 送信 - resolveDetection(false); + // 旧・新両方の sendAuthSequence を再開させる + detection.resolve(false); await flushAsync(10); - // 新世代の state が維持されていることを検証 (旧 run が resetAuthSession を呼ばなかった証左) + // 新世代の state が維持されていること (旧 run が resetAuthSession を呼ばなかった証左) expect(client.getSessionToken()).toBe(0xbbbbbbbb); expect(client.authenticationState).toBe("pending"); } finally { From af849b9a3dd09f72f5df21c6a9ed83bce234c299 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:28:27 +0900 Subject: [PATCH 14/16] =?UTF-8?q?fix:=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E5=AF=BE=E5=BF=9C=20-=20multiPacket?= =?UTF-8?q?Header=E3=81=AE=E3=82=A2=E3=82=BB=E3=83=B3=E3=83=96=E3=83=AA?= =?UTF-8?q?=E3=83=91=E3=82=B9=E5=AF=BE=E5=BF=9C=E3=81=A8=E5=86=97=E9=95=B7?= =?UTF-8?q?=E8=AA=AD=E3=81=BF=E5=8F=96=E3=82=8A=E6=8E=92=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - readAssembledパスでmultiPacketHeaderを設定 (CodeRabbit P2) - Artwork.read()のreadUInt32LE(38)をmultiPacketHeader経由に変更 - readMultiPacketHeaderをexportしてtcnet.tsから利用可能に - ErrorPacketパーサーテストのデータを非認証値(0x1234)に変更 --- src/network.ts | 4 ++-- src/tcnet.ts | 3 +++ tests/auth.test.ts | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/network.ts b/src/network.ts index 318a22c..4fe5a6a 100644 --- a/src/network.ts +++ b/src/network.ts @@ -721,7 +721,7 @@ export type MultiPacketHeader = { * @param buffer - 読み取り元バッファ * @returns マルチパケットヘッダー情報 */ -function readMultiPacketHeader(buffer: Buffer): MultiPacketHeader | null { +export function readMultiPacketHeader(buffer: Buffer): MultiPacketHeader | null { if (buffer.length < 42) return null; return { totalDataSize: buffer.readUInt32LE(26), @@ -1011,7 +1011,7 @@ export class TCNetDataPacketArtwork extends TCNetDataPacket { return; } this.multiPacketHeader = readMultiPacketHeader(this.buffer); - const clusterSize = this.buffer.readUInt32LE(38); + const clusterSize = this.multiPacketHeader?.dataClusterSize ?? 0; const end = getClusterEnd(this.buffer.length, dataStart, clusterSize); this.data = { jpeg: Buffer.from(this.buffer.slice(dataStart, end)) }; } diff --git a/src/tcnet.ts b/src/tcnet.ts index 06e1b18..c79a0cf 100644 --- a/src/tcnet.ts +++ b/src/tcnet.ts @@ -746,6 +746,9 @@ export class TCNetClient extends EventEmitter { finalPacket.header = mgmtHeader; finalPacket.dataType = dataPacket.dataType; finalPacket.layer = dataPacket.layer; + if ("multiPacketHeader" in finalPacket) { + finalPacket.multiPacketHeader = nw.readMultiPacketHeader(msg); + } if ("readAssembled" in finalPacket && typeof finalPacket.readAssembled === "function") { finalPacket.readAssembled(assembled); } diff --git a/tests/auth.test.ts b/tests/auth.test.ts index 10aeeee..016cba8 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -200,8 +200,8 @@ describe("TCNetErrorPacket", () => { writeValidHeader(buffer, TCNetMessageType.Error); buffer.writeUInt8(0xff, 24); // dataType buffer.writeUInt8(0xff, 25); // layerId - buffer.writeUInt16LE(0xffff, 26); // code - buffer.writeUInt16LE(0x00, 28); // messageType + buffer.writeUInt16LE(0x1234, 26); // code + buffer.writeUInt16LE(0x5678, 28); // messageType const packet = new TCNetErrorPacket(); packet.buffer = buffer; @@ -213,8 +213,8 @@ describe("TCNetErrorPacket", () => { // Assert expect(packet.dataType).toBe(0xff); expect(packet.layerId).toBe(0xff); - expect(packet.code).toBe(0xffff); - expect(packet.messageType).toBe(0x00); + expect(packet.code).toBe(0x1234); + expect(packet.messageType).toBe(0x5678); }); it("認証失敗のErrorパケット (code=0x000D) をパースする", () => { From 3d835956c0d18751e0c9fcd63b87cf49f08d21ab Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 04:06:02 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20file-chunk=E3=83=91=E3=82=B9?= =?UTF-8?q?=E3=81=A7multiPacketHeader=E3=81=8C=E6=9C=AA=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=81=AB=E3=81=AA=E3=82=8B=E4=B8=8D=E5=85=B7=E5=90=88=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tcnet.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tcnet.ts b/src/tcnet.ts index c79a0cf..ba908a5 100644 --- a/src/tcnet.ts +++ b/src/tcnet.ts @@ -689,6 +689,9 @@ export class TCNetClient extends EventEmitter { finalPacket.header = mgmtHeader; finalPacket.dataType = dataPacket.dataType; finalPacket.layer = dataPacket.layer; + if ("multiPacketHeader" in finalPacket) { + finalPacket.multiPacketHeader = nw.readMultiPacketHeader(msg); + } if ("readAssembled" in finalPacket && typeof finalPacket.readAssembled === "function") { finalPacket.readAssembled(assembled); } From ae48cd9d355d0740a65fc9a38dd639b854974e30 Mon Sep 17 00:00:00 2001 From: xin <98406118+9c5s@users.noreply.github.com> Date: Mon, 13 Apr 2026 04:29:28 +0900 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20receiveUnicast=E3=81=AE=E6=9C=80?= =?UTF-8?q?=E5=B0=8F=E3=83=90=E3=83=83=E3=83=95=E3=82=A1=E9=95=B7=E3=82=AC?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=9226=E3=83=90=E3=82=A4=E3=83=88?= =?UTF-8?q?=E3=81=AB=E5=BC=95=E3=81=8D=E4=B8=8A=E3=81=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tcnet.ts | 2 +- tests/receive-unicast.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tcnet.ts b/src/tcnet.ts index ba908a5..f345884 100644 --- a/src/tcnet.ts +++ b/src/tcnet.ts @@ -714,7 +714,7 @@ export class TCNetClient extends EventEmitter { * @param rinfo - 送信元情報 */ private receiveUnicast(msg: Buffer, rinfo: RemoteInfo): void { - if (msg.length < 24) return; + if (msg.length < 26) return; const mgmtHeader = new nw.TCNetManagementHeader(msg); mgmtHeader.read(); const packet = this.parsePacket(mgmtHeader); diff --git a/tests/receive-unicast.test.ts b/tests/receive-unicast.test.ts index 337cea6..ad10cb3 100644 --- a/tests/receive-unicast.test.ts +++ b/tests/receive-unicast.test.ts @@ -43,7 +43,7 @@ describe("receiveUnicast マルチパケット対応", () => { }); describe("receiveUnicast エラーハンドリング", () => { - it("24バイト未満のバッファでクラッシュしない", () => { + it("26バイト未満のバッファでクラッシュしない", () => { const client = new TestTCNetClient(); client.simulateConnected(); const handler = vi.fn();