#!/usr/bin/perl my @edid_info = group_by2( a8 => '_header', a2 => 'manufacturer_name', v => 'product_code', V => 'serial_number', C => 'week', C => 'year', C => 'edid_version', C => 'edid_revision', a => 'video_input_definition', C => 'max_size_horizontal', # in cm C => 'max_size_vertical', # in cm C => 'gamma', a => 'feature_support', a10 => '_color_characteristics', a3 => '_established_timings', a16 => '_standard_timings', a72 => 'monitor_details', C => 'extension_flag', C => 'checksum', ); my %subfields = ( manufacturer_name => [ group_by2( 1 => '', 5 => '1', 5 => '2', 5 => '3', ) ], video_input_definition => [ group_by2( 1 => 'digital', 1 => 'separate_sync', 1 => 'composite_sync', 1 => 'sync_on_green', 2 => '', 2 => 'voltage_level', ) ], feature_support => [ group_by2( 1 => 'DPMS_standby', 1 => 'DPMS_suspend', 1 => 'DPMS_active_off', 1 => '', 1 => 'rgb', ) ], detailed_timing => [ group_by2( 8 => 'horizontal_active', 8 => 'horizontal_blanking', 4 => 'horizontal_active_hi', 4 => 'horizontal_blanking_hi', 8 => 'vertical_active', 8 => 'vertical_blanking', 4 => 'vertical_active_hi', 4 => 'vertical_blanking_hi', 8 => 'horizontal_sync_offset', 8 => 'horizontal_sync_pulse_width', 4 => 'vertical_sync_offset', 4 => 'vertical_sync_pulse_width', 2 => 'horizontal_sync_offset_hi', 2 => 'horizontal_sync_pulse_width_hi', 2 => 'vertical_sync_offset_hi', 2 => 'vertical_sync_pulse_width_hi', 8 => 'horizontal_image_size', # in mm 8 => 'vertical_image_size', # in mm 4 => 'horizontal_image_size_hi', 4 => 'vertical_image_size_hi', 8 => 'horizontal_border', 8 => 'vertical_border', 1 => 'interlaced', 2 => 'stereo', 2 => 'digital_composite', 1 => 'horizontal_sync_positive', 1 => 'vertical_sync_positive', 1 => '', ) ], monitor_range => [ group_by2( 8 => 'vertical_min', 8 => 'vertical_max', 8 => 'horizontal_min', 8 => 'horizontal_max', 8 => 'pixel_clock_max', ) ], ); sub get_many_bits { my ($s, $field_name) = @_; my @bits = split('', unpack('B*', $s)); my %h; foreach (@{$subfields{$field_name}}) { my ($size, $field) = @$_; my @l = ('0' x (8 - $size), splice(@bits, 0, $size)); $h{$field} = unpack("C", pack('B*', join('', @l))) if $field && $field !~ /^_/; } \%h; } sub check_parsed_edid { my ($edid) = @_; $edid->{manufacturer_name} ne '@@@' or return 'bad manufacturer_name'; $edid->{product_code} != 0 or return 'bad product_code'; $edid->{edid_version} != 0xff && $edid->{edid_revision} != 0xff or return 'bad edid_version'; if ($edid->{monitor_range}) { $edid->{monitor_range}{horizontal_min} && $edid->{monitor_range}{horizontal_min} <= $edid->{monitor_range}{horizontal_max} or return 'bad HorizSync'; $edid->{monitor_range}{vertical_min} && $edid->{monitor_range}{vertical_min} <= $edid->{monitor_range}{vertical_max} or return 'bad VertRefresh'; } ''; } sub parse_edid { my ($raw_edid) = @_; my %edid; my @vals = unpack(join('', map { $_->[0] } @edid_info), $raw_edid); my $i; foreach (@edid_info) { my ($field, $v) = ($_->[1], $vals[$i++]); if ($field eq 'year') { $v += 1990; } elsif ($field eq 'manufacturer_name') { my $h = get_many_bits($v, 'manufacturer_name'); $v = join('', map { chr(ord('A') + $h->{$_} - 1) } 1 .. 3); } elsif ($field eq 'video_input_definition') { $v = get_many_bits($v, 'video_input_definition'); } elsif ($field eq 'feature_support') { $v = get_many_bits($v, 'feature_support'); } elsif ($field eq 'monitor_details') { while ($v) { (my $pixel_clock, my $vv, $v) = unpack("v a16 a*", $v); if ($pixel_clock) { # detailed timing my $h = get_many_bits($vv, 'detailed_timing'); $h->{pixel_clock} = $pixel_clock / 100; # to have it in MHz my %detailed_timing_field_size = map { $_->[1], $_->[0] } @{$subfields{detailed_timing}}; foreach my $field (keys %detailed_timing_field_size) { $field =~ s/_hi$// or next; my $hi = delete($h->{$field . '_hi'}); $h->{$field} += $hi << $detailed_timing_field_size{$field}; } push @{$edid{detailed_timings}}, $h; } else { (my $flag, $vv) = unpack("n x a*", $vv); if ($flag == 0xfd) { # range $edid{monitor_range} = get_many_bits($vv, 'monitor_range'); if ($edid{monitor_range}{pixel_clock_max} == 0xff) { delete $edid{monitor_range}{pixel_clock_max}; } else { $edid{monitor_range}{pixel_clock_max} *= 10; #- to have it in MHz } } elsif ($flag == 0xfc) { my $prev = $edid{monitor_name}; $edid{monitor_name} = ($prev ? "$prev " : '') . unpack('A13', $vv); } elsif ($flag == 0xfe) { push @{$edid{monitor_text}}, unpack('A13', $vv); } elsif ($flag == 0xff) { push @{$edid{serial_number2}}, unpack('A13', $vv); } else { #warn "parse_edid: unknown flag $flag\n"; } } } } $edid{$field} = $v if $field && $field !~ /^_/; } $edid{EISA_ID} = $edid{manufacturer_name} . sprintf('%x', $edid{product_code}); if ($edid{monitor_range}) { $edid{HorizSync} = $edid{monitor_range}{horizontal_min} . '-' . $edid{monitor_range}{horizontal_max}; $edid{VertRefresh} = $edid{monitor_range}{vertical_min} . '-' . $edid{monitor_range}{vertical_max}; } foreach my $h (@{$edid{detailed_timings}}) { my $horizontal_total = $h->{horizontal_active} + $h->{horizontal_blanking}; my $vertical_total = $h->{vertical_active} + $h->{vertical_blanking}; $h->{ModeLine_comment} = sprintf qq(# %dx%d @ %.1f Hz, %.1f kHz hsync), $h->{horizontal_active}, $h->{vertical_active}, $h->{pixel_clock} / $horizontal_total / $vertical_total * 1000 * 1000, $h->{pixel_clock} / $horizontal_total * 1000; $h->{ModeLine} = sprintf qq(ModeLine "%dx%d" $h->{pixel_clock} %d %d %d %d %d %d %d %d), $h->{horizontal_active}, $h->{vertical_active}, $h->{horizontal_active}, $h->{horizontal_active} + $h->{horizontal_sync_offset}, $h->{horizontal_active} + $h->{horizontal_sync_offset} + $h->{horizontal_sync_pulse_width}, $horizontal_total, $h->{vertical_active}, $h->{vertical_active} + $h->{vertical_sync_offset}, $h->{vertical_active} + $h->{vertical_sync_offset} + $h->{vertical_sync_pulse_width}, $vertical_total; } \%edid; } sub print_edid { my ($edid) = @_; my $diagonal_inches = sqrt(sqr($edid->{max_size_horizontal}) + sqr($edid->{max_size_vertical})) / 2.54; print "Name: $edid->{monitor_name}\n"; print "EISA ID: $edid->{EISA_ID}\n"; printf "Screen size: %d cm x %d cm (%3.2f inches)\n", $edid->{max_size_horizontal}, $edid->{max_size_vertical}, $diagonal_inches * 1.08; print "Gamma: ", $edid->{gamma} / 100 + 1, "\n"; printf "%s signal\n", $edid->{video_input_definition}{digital} ? 'Digital' : 'Analog'; if ($edid->{monitor_range}) { printf "Max video bandwidth: %u MHz\n", $edid->{monitor_range}{pixel_clock_max}; print "\n"; printf "\tHorizSync %u-%u\n", $edid->{monitor_range}{horizontal_min}, $edid->{monitor_range}{horizontal_max}; printf "\tVertRefresh %u-%u\n", $edid->{monitor_range}{vertical_min}, $edid->{monitor_range}{vertical_max}; } foreach my $h (@{$edid->{detailed_timings}}) { print "\n"; print "\t", $h->{ModeLine_comment}, "\n"; print "\t", $h->{ModeLine}, "\n"; } } sub usage() { die "usage: monitor-parse-edid [--perl] []\n"; } use Getopt::Long; GetOptions( 'perl' => \ (my $raw_perl), ) or usage(); my $F; if (@ARGV == 0) { $F = *STDIN; } elsif (@ARGV == 1) { open($F, $ARGV[0]) or usage(); } else { usage(); } my $raw_edid = join('', <$F>); length($raw_edid) == 128 || length($raw_edid) == 256 or die "monitor-parse-edid: bad edid\n"; my $edid = parse_edid($raw_edid); if (my $err = check_parsed_edid($edid)) { die "$err\n"; } if ($raw_perl) { use Data::Dumper; $Data::Dumper::Sortkeys = 1; print Dumper($edid); } else { print_edid($edid); } sub sqr { $_[0] * $_[0] } sub group_by2 { my @l; for (my $i = 0; $i < @_; $i += 2) { push @l, [ $_[$i], $_[$i+1] ]; } @l; }