Jump to content

Xmodem in PHP? Is it even possible?


Recommended Posts

PHP support for a 45-year-old protocol? Odds aren't good.

If you can find some C code for it then you could translate that into PHP code. Or if you were really serious then you could instead write a PHP extension around it.

Link to comment
Share on other sites

Posted (edited)

I was interested since I've actually used Xmodem a couple times in the last year.  Implementing the protocol was easy enough, figuring how terminal settings to make it actually work was more of a pain.  It's not pretty, but here's a receive implementation (with the help of this crc library)

<?php

class XModem {

    const SOH = "\x01";
    const EOT = "\x04";
    const ACK = "\x06";
    const NAK = "\x15";
    const CAN = "\x18";
    const C = "C";

    private $channel;
    private $channelBuffer;
    private $outFile;
    private $lastPacketData;

    /**
     * @param resource $channel
     * @param resource $file
     *
     * @return void
     */
    public function receive($channel, $file){
        $this->channel = $channel;
        $this->outFile = $file;
        stream_set_blocking($channel, false);

        $lastPacketNumber = 0;
        do {
            $packet = $this->readPacket();
            if (!$packet){
                if ($lastPacketNumber === 0){
                    $this->writePacket(self::C);
                } else {
                    $this->writePacket(self::ACK);
                }
                continue;
            }

            if ($packet[0] === self::SOH){
                $packetNumber = $this->packetNumber(substr($packet, 1, 2));
                $data = substr($packet, 3, 128);
                $receivedCRC = substr($packet, -2);
                $calculatedCRC = $this->calcCRC($data);
                if (($packetNumber === $lastPacketNumber + 1 || $packetNumber === 0 && $lastPacketNumber === 255) && $calculatedCRC === $receivedCRC){
                    $lastPacketNumber = $packetNumber;
                    $this->updateReceiveFile($data);
                    $this->writePacket(self::ACK);
                } else {
                    $this->channelBuffer = '';
                    $this->writePacket(self::NAK);
                }
            } else if ($packet[0] === self::CAN){
                $lastPacketNumber = 0;
                fseek($file, 0);
                ftruncate($file, 0);
                $this->writePacket(self::C);
            } else if ($packet[0] === self::EOT){
                $this->finishReceiveFile();
                $this->writePacket(self::ACK);
            }
        } while (!feof($channel) && !$packet || $packet[0] !== self::EOT);
    }

    private function updateReceiveFile($data){
        if ($this->lastPacketData){
            fwrite($this->outFile, $this->lastPacketData);
        }

        $this->lastPacketData = $data;
    }

    private function finishReceiveFile(){
        fwrite($this->outFile, rtrim($this->lastPacketData, "\x1a"));
    }

    private function readPacket(){
        $packetType = $this->readChannelBuffer(1);
        if ($packetType === self::SOH){
            $pn = $this->readChannelBuffer(2);
            $data = $this->readChannelBuffer(128);
            $crc = $this->readChannelBuffer(2);
            $packet = $packetType . $pn . $data . $crc;
        } else {
            $packet = $packetType;
        }

        return $packet;
    }

    private function readChannelBuffer($length){
        $startTime = time();
        while (strlen($this->channelBuffer) < $length && time() - $startTime < 10){
            if (!$this->bufferInput()){
                usleep(100);
            }
        }

        $data = substr($this->channelBuffer, 0, $length);
        $this->channelBuffer = substr($this->channelBuffer, $length);

        return $data;
    }

    private function bufferInput(){
        $byteCounter = 0;
        do {
            $r = [$this->channel];
            $w = $e = [];
            $readable = stream_select($r, $w, $e, 0, 0);
            if ($readable){
                $dataRead = stream_get_contents($this->channel);
                $byteCounter += strlen($dataRead);
                if ($dataRead){
                    $this->channelBuffer .= $dataRead;
                }
            }
        } while ($readable);

        return $byteCounter > 0;
    }

    private function writePacket($data){
        $total = strlen($data);
        $written = 0;
        do {
            $dataToWrite = substr($data, $written);
            $written += fwrite($this->channel, $dataToWrite);
            fflush($this->channel);
        } while ($written < $total);
    }

    private function packetNumber($pnBytes){
        $n1 = ord($pnBytes[0]);
        $n2 = ord($pnBytes[1]);
        if ($n1 + $n2 !== 255){
            return null;
        }

        return $n1;
    }

    private function calcCRC($data){
        $o = new mermshaus\CRC\CRC16XModem();
        $o->update($data);

        return $o->finish();
    }
}

Requires some terminal setup to work properly.  I eventually included this code in my test script to fix terminal problems, but it creates it's own problems (can't ctrl+c the script).

system('/usr/bin/stty -cooked -echo');
register_shutdown_function(function(){
    system('/usr/bin/stty cooked echo');
});

I'll leave implementing send and other improvements for the reader.

 

Edited by kicken
Link to comment
Share on other sites

14 hours ago, KenHorse said:

Did you ever do a SEND version?

No, as noted in my reply:

14 hours ago, kicken said:

I'll leave implementing send and other improvements for the reader.

The XModem protocol for sending wouldn't be too hard to implement, just as receive wasn't. 

What would be more difficult is dealing with the serial connection to your device.  You'd likely have to create a script that can connect to your device and issue whatever commands are necessary to get it ready to receive.  That would be a more complex task and likely have to be tailor made to your device.  I don't think it's impossible to do, PHP is capable of dealing with serial connections. 

I simply don't currently have any xmodem capable serial devices to mess with (did the receive bit over ssh), nor do I really have much free time to continue messing with it.

 

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.