#!/usr/bin/perl -w
# Perl Modbus Server
# Statements zur Fehlerbehandlung sind weitgehend entfernt. 
  
# Singlethreaded Server 
# In Kombination mit SysV-IPC (ShMem)
  
use Socket;
use IPC::SysV qw(IPC_RMID IPC_PRIVATE);         

# Process Id und Shared-Memory Id in Dateien
# ablegen, die den Servernamen tragen
$myself ( $0 =~ s/\.pl$// );
$pidfile = "$myself.pid";
$sidfile = "$myself.sid";

$shm_flags = 0666;  # Zugriffsrechte: rw-rw-rw-
$tcpmodbus = 502;   # Modbus well known port (privilegiert!)

$max_unit = 28;     # Anzahl der Steuerungsstationen (SPS)
$max_ref = 1024;    # letztes Register der SPS
$unit_size = 1024;  # Registeranzahl
$word_size = 2;     # Bytes pro Wort

# Platz fuer Shared Memory
$shm_size = 2 * $max_unit * $unit_size * $word_size; # array twice!

$MODBUS_READ  = 3;    # Modbus Funktion: Lesen
$MODBUS_WRITE = 0x10; # Modbus Funktion: Schreiben

# Unterprogramm zum Programmende (ausgeloest durch kill -TERM)
sub getout {
  shmctl ($sid, IPC_RMID, 0);     # Shared Memory freigeben
  unlink $pidfile, $sidfile;
  exit 0;
}

# Forken und im Hintergrund weiterarbeiten. 
if ($pid = fork) {
  exit 0;
}

# Signalhandler fuer kill -TERM bereitstellen und 
# an alle Kindprozesse vererben.
$SIG{TERM} = \&getout;

# Process Id vermerken.
open (PID, ">$pidfile");
print PID "$$\n";
close PID;

# Shared Memory anlegen
$sid = shmget(IPC_PRIVATE, $shm_size, $shm_flags);

# Shared Memory Id vermerken
open (SID, ">$sidfile");
print SID "$sid\n"; 
close SID; 

# Server Port und Protokoll 
my $port = $tcpmodbus; 
my $proto = getprotobyname('tcp');

# Server socket erstellen, 
# Hostadresse binden und 
# auf Requests warten
socket(Server, PF_INET, SOCK_STREAM, $proto);
setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)); 
bind (Server, sockaddr_in($port, INADDR_ANY));
listen (Server, SOMAXCONN);

my $paddr;
# Endlos Verbindungen akzeptieren, bearbeiten und 
# wieder schließen. 
for ( ; $paddr = accept(Client, Server); close Client ) {
  my ($port, $iaddr) = sockaddr_in($paddr);    
  my ($ta_id, $prot_id, $ta_len, $unit_id, $mb_fc, $bc, 
      $ref, $count, $data, @data, $got, $line, $header, 
      $req, $sent, $string);
  
  $req = 12; # Die ersten 12 bytes lesen
  recv Client, $line, $req, 0;
  # und in ihre Bestandteile zerlegen
  ($ta_id, $prot_id, $ta_len, $unit_id, $mb_fc, $ref, $count) = 
    unpack "nnnCCnn", $line; 
  if ( $mb_fc == $MODBUS_READ ) {
    # mehr lesen
    shmread $sid, $line, 2*$ref, 2*$count;
    $line = pack('n*', unpack 'S*', $line);
    $header = pack 'nnnCCC',
    $ta_id, $prot_id, 2*$count+3, $unit_id, $mb_fc, 0xff; 
    $string = $header . $line; 
    # und antworten
    send(Client, $string, 0); 
  } elsif ( $mb_fc == $MODBUS_WRITE ) {
    # oder schreiben
    $req = 2*$count+1; 
    recv Client, $line, $req, 0;
    ($bc, @data) = unpack 'Cn*', $line;
    shmwrite ($sid, pack ('S*', @data), 
	      2*(($unit_id-1)*$unit_size+$ref), 2*$count);
    $header = pack 'nnnCCCnn',
    $ta_id, $prot_id, 5, $unit_id, $mb_fc, $ref, $count; 
    send (Client, $header, 0);
  }
}	


