marker by rocamisakitohko

《Lost In The Abyss》レーダーもつれ通信 穏やかな眠り

この記事はStormworks 第2 Advent Calendar 2024 2日目の記事です。

穏やかな夜に身を任せるな 老いても怒りを燃やせ 終わりゆく日に 怒れ 怒れ 消えゆく光に

穏やかな夜に身を任せるな (冒頭) - ディラン・トマス

往時の無線通信事情

昨年(2023年)初め頃。埼玉地上軍は、既存の手段によらない通信手法を模索していました。

StormworksにはRadio RXというブロックがあり、これをビークルに装備することで、コンポジット信号やビデオ信号などを送受信することができます。

選ぶパーツと供給する電圧次第では直線距離40kmまで情報を送受信することが可能で、32ChのOn/Off信号と32ChのNumber信号(これは32bit浮動小数点数とされています)を60fpsで送ることができ、その帯域幅は63,360bpsということになります。

しかし、周波数さえわかっていれば傍受が可能であること、信号強度の大きい信号を優先的に受信してしまう都合上妨害が容易であることなど、軍事分野における無線通信の信頼には疑問符がつきます。

今後、電子攻撃・電子防護技術がどう発展していくかは不透明であったため、Radio系ブロックを使った既存の無線通信によらない通信手段を確保することにしました。

全く新しい通信手段!

これまで埼玉地上軍で試してきたなかで最長の通信距離をもっていた通信手段が、レーダー照射通信でした。

StormworksにはRadar Detectorというブロックがあり、これはレーダーの照射を検知してBool信号を送り出します。

これを受信機、レーダーを送信機として遠隔地にシリアル情報を伝えようというのが、レーダー照射通信です。

これは数tickの遅延で理論上2000km先まで情報を伝達できるものでしたが、送信機となるレーダーの有効レンジが長くなるにつれ検出間隔が広がりその結果送信レートが低下したり、偶発的・意図的な妨害を受けやすかったりと、とても現実的とはいえない通信手段でした。

しかし、このレーダー照射通信を調べるうちに、Radar Detectorの奇妙な性質に気が付きました。

当時のStormworksでは、たとえあるPhysics Bodyが他のPhysics Bodyと完全に分離されていたとしても、同じビークルデータからスポーンされたPhysics Bodyはすべて一つのビークルとして扱われていました。

これは宇宙DLC以後、完全に分離されたPhysics Bodyがそれぞれ別のビークルとして扱われるようになったのとは対照的で、この特殊な仕様は、別の場所にあるPhysics Body同士でなんらかの方法で情報をやり取りできるのではないか、という疑問を埼玉地上軍に与えてくれました。

そしてその「なんらかの方法」こそが、まさにRadar Detectorだったのです。

Radar Detectorは、あるのビークルのどのPhysics Bodyがレーダー照射を受けていても反応するのです。たとえレーダーの照射を受けているPhysics Bodyが親となるビークルの遥か100000km先にあったとしても。

これがレーダーもつれ通信の基本原理であり、あとはここに情報を載せる仕組みを作ることで、距離無制限で傍受も不可能な通信路を形成することができました。

シリアル通信モジュール

それでは、同じビークルデータからなる2つの分離可能なPhysics Bodyを用意し、片方にはRadar Detectorとその信号を他のビークルに送るためのコネクターを準備しましょう。

これが「もつれペア」となり、どちらかのBodyにレーダーが照射されると、それをRadar Detectorで検知することができるのです。

あとは、それぞれ31ChのOn/Off信号・Number信号を1023bitのビット列に変換・送信し、また受信・復号する装置を作るだけです。

具体的にはまず、8桁(!)のプリアンブルを8tick間送信したのち、次の10tickでOn/Off信号・Number信号それぞれ何Chまで送信するのかを送信させます。

そののち、送信するビット列の内容から誤り検出に用いるハッシュ/チェックサムと呼べるものを生成し、これを8tick間送信します。

最後にビット列そのものを送信し、送信機の役目はここで終わります。

受信機では、Radar Detectorから受け取ったシリアル信号を常時リスンし、プリアンブルが一致するビット列を受信したら受信モードに入ります。

その後、データ長やチェックサムを受取り、データを完全に受信し終わった時点でチェックサムを比較、これが一致した場合にのみ受信が正常終了したとみなし、ビット列をBool/32bit浮動小数点数に再変換して出力します。

レーダーもつれ通信において考えられるのはビットが立つ方向へのバースト誤りなので、誤り検出は必須かつ容易ですが、誤り訂正は困難だと考え実装しませんでした。

以下に、当時書き殴った送受信機のコードを掲載します。

送信機:

radar_entangle_TX.lua
i,o,p,m,t=input,output,property,math,table
IB,IN,OB,ON,PB,PN,TI,TR=i.getBool,i.getNumber,o.setBool,o.setNumber,p.getBool,p.getNumber,t.insert,t.remove

infoB={}
infoN={}
checksum=0

timer=0

wakeword=165

function onTick()
    OB(1,false)

	if IB(32) and timer==0 then
        infoB={}
        infoN={}
        checksum=0
        timer=1
        length=IN(32)

        for i=1,length%32 do    -- 0 to 31
            TI(infoB,IB(i))
            if IB(i) then checksum=checksum+1 end
        end
        for i=1,m.floor(length/32) do  -- 0 to 31
            obj=("L"):unpack(("f"):pack(IN(i)))
            TI(infoN,obj)
            for j=0,31 do
                checksum=(checksum+((obj>>j)&1))%256
            end
        end
		debug.log(checksum)
    end

    if timer>0 then
        OB(2,true)
        if timer<9 then -- wake word(8)
            bit=(wakeword>>(timer-1))&1
            OB(1,bit>0)
        elseif timer<14 then    -- binary data length(5)
            bit=(#infoB>>(timer-9))&1
            OB(1,bit>0)
        elseif timer<19 then    -- number data length(5)
            bit=(#infoN>>(timer-14))&1
            OB(1,bit>0)
        elseif timer<27 then    -- checksum(8)
            bit=(checksum>>(timer-19))&1    -- tbw
            OB(1,bit>0)
        elseif timer<(27+#infoB) then   -- binary(0to31)
            offset=timer-26
            OB(1,infoB[offset])
        elseif timer<(27+#infoB+#infoN*32) then
            offset=timer-(26+#infoB)
            channel=m.ceil(offset/32)
            bitshift=((offset-1)%32)
            bit=(infoN[channel]>>bitshift)&1 
            
            OB(1,bit>0)
        else
            timer=-1
            OB(2,false)
        end
        timer=timer+1
    end
    ON(1,timer)
end

受信機:

radar_entangle_RX.lua
i,o,p,m,t=input,output,property,math,table
IB,IN,OB,ON,PB,PN,TI,TR=i.getBool,i.getNumber,o.setBool,o.setNumber,p.getBool,p.getNumber,t.insert,t.remove

listen={}
infoB={}
infoN={}
buffer={}
countB=0
countN=0
checksum=0
checksumr=0
state=0

timer=0

wakeword=165

function onTick()

    if timer==0 then  -- tbrw -> wakeword
        TI(listen,IB(1))
		if #listen>8 then TR(listen,1) end
        w=0
        for i=0,7 do
            w=w|(listen[i+1] and 1 or 0)<<i
        end

        if w==wakeword then
            timer=8
            infoB={}
            infoN={}
            buffer={}
            countB=0
            countN=0
            checksum=0
            checksumr=0
            state=0
        end
    end

    if timer>0 then
        OB(32,true)
        if timer>8 and timer<14 then    -- binary data size
            offset=timer-9
            countB=countB|(IB(1) and 1 or 0)<<offset
        elseif timer<19 then    -- number data size
            offset=timer-14
            countN=countN|(IB(1) and 1 or 0)<<offset
        elseif timer<27 then    -- checksum
            offset=timer-19
            checksum=checksum|(IB(1) and 1 or 0)<<offset
        elseif timer<(27+countB) then
            TI(infoB,IB(1))
            if IB(1) then checksumr=(checksumr+1)%256 end
        elseif timer<(27+countB+32*countN) then
            TI(buffer,IB(1))
            if IB(1) then checksumr=(checksumr+1)%256 end
        else
            timer=-1
			OB(32,false)
			debug.log(checksum)
			debug.log(checksumr)
            if checksum==checksumr then state=1 end
        end
        offset=timer-countB-26

        if offset>0 and offset%32==0 then
            channel=m.ceil(offset/32)-1
			debug.log(channel)

            val=("L"):unpack(("L"):pack(0))
            for i=0,31 do
                bit=channel*32+i+1
                val=val|(buffer[bit] and 1 or 0)<<i
            end

            float=("f"):unpack(("L"):pack(val))
			TI(infoN,float)			
        end

		timer=timer+1
    end

	for i=1,31 do
        if i>#infoB then OB(i,false) else OB(i,infoB[i]) end
	end

	for i=1,31 do
        if i>#infoN then ON(i,0) else ON(i,infoN[i]) end
	end
    ON(32,state)
end

Shairoさんが一晩でやってくれました

ところで、このレーダーもつれ通信についての動画をアップロードして一日も経たないうちに、Shairoさんという方が興味深い投稿をしました。

https://x.com/shairo_jp/status/1657007472153071617

これは、XML編集して潰したHardpoint Connectorが互いに接続されると、それらが属していたPhysics Bodyが姿を消すことを利用しています。

そうして"虚無送り"されるPhysics Bodyを経由するようにElectric Cableを接続することで、有線通信路にもかかわらず、送信機から経由Bodyまで、経由Bodyから受信機までの距離が制限されなくなります。

これで、普段有線通信や無線通信においてComposite信号を扱うのと同じように、距離無制限・傍受不可能な通信路を形成できるのです。

レーダーもつ・れ通信 穏やかな眠り

しかし、2023年10月、GeometaがStormworksの新DLC、Stormworks: Spaceを発表し、状況は一変します。

先述の通りビークルの分離可能Bodyに関する仕様が変更され、分離可能なPhysics Bodyはサブビークルとして扱われるようになりました。

これによりRadar Detectorをもってしても分離可能なBodyを越えて通信することは不可能になり、レーダーもつれ通信は早すぎる死を迎えることとなります。

また、Shairoさんによる虚無送り有線通信も(恐らく)同じタイミングで使用不能になりました。 Bodyを「虚無送り」すると、それに接続されたElectric Cable、またそのCableが接続されたBodyまで「虚無送り」が波及するようになり、通信路の形成が事実上不可能になったのです。

そして、既存の無線通信を取り巻く環境もまた変化しました。

RX Directionalなる、パーツ単位で指向性をもち、海面高度においての通信可能距離10000kmを誇る強力なアンテナが追加されたのです。

手元に残ったのは……?

そうしてレーダーもつれ通信が使用不可能となったところで、手元にはシリアル通信モジュールだけが残りました。

これはレーザー通信(4km/5km)に活かすことこそ可能なものの、レーザー通信は無線通信に対する明確な優位性がなく、埼玉地上軍・海軍も、海中での「リアルな」通信手段としてのみ用いています。

stormworkslua