diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 56e516acad..3953826004 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -631,6 +631,12 @@ set( COMMON_IO_SRCS
     io/cadstar/cadstar_archive_parser.cpp
     io/cadstar/cadstar_parts_lib_parser.cpp
 
+    # CAST
+    #io/cast/cast_cst_file.cpp
+    #io/cast/cast_io.cpp
+    #io/cast/cast_lst_file.cpp
+    #io/cast/cast_utils.cpp
+
     # Eagle
     io/eagle/eagle_parser.cpp
 
diff --git a/common/io/cast/cast_cst_file.cpp b/common/io/cast/cast_cst_file.cpp
new file mode 100644
index 0000000000..1ef02b0c43
--- /dev/null
+++ b/common/io/cast/cast_cst_file.cpp
@@ -0,0 +1,248 @@
+#include "cast_cst_file.h"
+#include <padstack.h>
+#include <pcb_shape.h>
+
+template <typename T>
+void CAST_CST_FILE::SEPARATED_DATA<T>::Read()
+
+{
+    wxString name;
+    uint16_t count;
+    int16_t sigs[2];
+
+    ReadVals( count, sigs );
+    if ( sigs[0] != -1 || sigs[1] != 0 )
+        THROW_BIN( "Unexpected binary content." );
+    ReadStr<int16_t>( name );
+    ReadItems( count );
+}
+
+template <typename T>
+void CAST_CST_FILE::SEPARATED_DATA<T>::ReadSeparator()
+{
+    uint8_t separator[sizeof( T::SEPARATOR )];
+    ReadVals( separator );
+    if ( memcmp( separator, T::SEPARATOR, sizeof( T::SEPARATOR )) )
+        THROW_BIN( "Unexpected binary content." );
+}
+
+template <typename T>
+void CAST_CST_FILE::SEPARATED_DATA<T>::ReadItems( uint16_t count )
+{
+    m_items.resize( count );
+
+    if ( count > 0 ) {
+        m_items[0].Read( *this );
+        for ( size_t i = 1; i < count; ++ i ) {
+            ReadSeparator();
+            m_items[i].Read( *this );
+        }
+    }
+}
+
+CAST_CST_FILE::CAST_CST_FILE( wxInputStream& aStream, const wxString& aSource )
+: BINARY_READER( aStream, aSource ),
+  m_devs( aStream, aSource ),
+  m_tps( aStream, aSource ),
+  m_vias( aStream, aSource ),
+  m_pads( aStream, aSource ),
+  m_lines_copper( aStream, aSource ),
+  m_lines_board( aStream, aSource ),
+  m_shapes( aStream, aSource )
+{ }
+
+std::vector<VECTOR2I> CAST_CST_FILE::CSHAPE::PolyPoints() const
+{
+    std::vector<VECTOR2I> points;
+    if ( type != CSHAPE_T::POLY )
+            throw KI_PARAM_ERROR( "Shape is not a poly." );
+    points.push_back( params[0].coord );
+    for ( size_t i = 2; i < params.size(); ++ i )
+        points.push_back( params[i].coord );
+    return points;
+}
+
+void CAST_CST_FILE::CSHAPE::Read( BINARY_READER & aReader )
+{
+    uint16_t count;
+
+    aReader.ReadVals( id, type, count );
+    params.resize( count );
+    for ( auto & param : params )
+        aReader.ReadVals( param );
+    switch( type ) {
+        case CSHAPE_T::ELLIPSE:
+            if ( params.size() != 1 )
+                THROW_BIN_FOR( "Unhandled ellipse data.", aReader );
+            if ( !IsCircle() )
+                THROW_BIN_FOR( "Unhandled ellipse data.", aReader );
+            break;
+        case CSHAPE_T::RECT:
+            if ( params.size() != 1 )
+                THROW_BIN_FOR( "Unhandled rect data.", aReader );
+            break;
+        case CSHAPE_T::POLY:
+            if ( params.size() < 2 )
+                THROW_BIN_FOR( "Unhandled poly data.", aReader );
+            if ( params[1].coord.x != 1 || params[1].coord.y < 0 )
+                THROW_BIN_FOR( "Unhandled poly data.", aReader );
+            break;
+        case CSHAPE_T::PADSTACK:
+            for ( auto & param : params )
+                if ( param.coord.x )
+                    THROW_BIN_FOR( "Unhandled layer shape data.", aReader );
+            break;
+        case CSHAPE_T::QUAD:
+            if ( params.size() != 2 )
+                THROW_BIN_FOR( "Unhandled rect data.", aReader );
+            break;
+        default:
+            THROW_BIN_FOR( "Unhandled shape type.", aReader );
+    }
+}
+
+CAST_CST_FILE::CSHAPE::operator PCB_SHAPE() const
+{
+    PCB_SHAPE shape( NULL );
+    VECTOR2I size;
+    switch ( type ) {
+        case CSHAPE_T::ELLIPSE:
+            if ( IsCircle() ) {
+                shape.SetShape( SHAPE_T::CIRCLE );
+                shape.SetStart( { 0, 0 } );
+                shape.SetEnd( VECTOR2I( CircleRadius(), 0 ) );
+            } else {
+                // from pad.cpp : an oval is a wide segment
+		// width = minor dim
+		// length = major dim - minor dim
+		size = EllipseSize();
+                shape.SetShape( SHAPE_T::SEGMENT );
+                if ( size.x > size.y ) {
+                    shape.SetStart( { ( size.x - size.y ) / 2, 0 } );
+                    shape.SetEnd ( { ( size.x + size.y ) / 2, 0 } );
+                    shape.SetWidth( size.y );
+                } else {
+                    shape.SetStart( { 0, ( size.y - size.x ) / 2 } );
+                    shape.SetEnd ( { 0, ( size.y + size.x ) / 2 } );
+                    shape.SetWidth( size.x );
+                }
+            }
+            break;
+        case CSHAPE_T::RECT:
+	    size = RectSize();
+            shape.SetShape( SHAPE_T::RECTANGLE );
+            shape.SetStart( size / -2 );
+            shape.SetEnd( size / 2 );
+            break;
+        case CSHAPE_T::QUAD:
+            shape.SetShape( SHAPE_T::RECTANGLE );
+            shape.SetStart( params[0].coord );
+            shape.SetEnd( params[1].coord );
+            break;
+        case CSHAPE_T::POLY: {
+            std::vector<VECTOR2I> polyPoints;
+            shape.SetShape( SHAPE_T::POLY );
+            polyPoints.reserve( params.size() - 1 );
+            polyPoints.push_back( params[0].coord );
+            for ( size_t i = 2; i < params.size(); ++ i )
+                polyPoints.push_back( params[i].coord );
+            shape.SetPolyPoints( polyPoints );
+            break;
+        }
+        case CSHAPE_T::PADSTACK:
+        default:
+            throw KI_PARAM_ERROR( "Unhandled shape conversion." );
+    }
+
+    return shape;
+};
+
+PADSTACK CAST_CST_FILE::CSHAPE::Padstack( CAST_CST_FILE const& cst, BOARD_ITEM* parent ) const
+{
+    if ( type != CSHAPE_T::PADSTACK )
+        throw KI_PARAM_ERROR( "Shape is not a padstack." );
+    ::PADSTACK padstack( parent );
+    //PCB_LAYER_ID minLayer = UNDEFINED_LAYER, maxLayer = UNDEFINED_LAYER;
+    size_t numLayers = 0;
+    LSET layerSet;
+    for ( size_t layer = 0; layer < params.size(); ++ layer ) {
+        size_t shape_lookup = params[layer].id, idx;
+        PCB_LAYER_ID pcbLayer = (PCB_LAYER_ID)layer;
+        if ( shape_lookup ) {
+            layerSet.set( pcbLayer );
+            ++ numLayers;
+            //if ( minLayer < 0 )
+            //    minLayer = pcbLayer;
+            //maxLayer = pcbLayer;
+            try {
+                idx = cst.m_shape_lookup.at( shape_lookup );
+            } catch ( ... ) {
+                continue;
+            }
+            const CAST_CST_FILE::CSHAPE& shape = cst.m_shapes.Items()[idx];
+            switch( shape.type ) {
+            case CAST_CST_FILE::CSHAPE_T::ELLIPSE:
+                if ( shape.IsCircle() ) {
+                    padstack.SetShape( PAD_SHAPE::CIRCLE, pcbLayer );
+                } else {
+                    padstack.SetShape( PAD_SHAPE::OVAL, pcbLayer );
+                }
+                padstack.SetSize( shape.EllipseSize(), pcbLayer );
+                break;
+            case CAST_CST_FILE::CSHAPE_T::RECT:
+                padstack.SetShape( PAD_SHAPE::RECTANGLE, pcbLayer );
+                padstack.SetSize( shape.RectSize(), pcbLayer );
+                break;
+            default:
+                padstack.SetShape( PAD_SHAPE::CUSTOM, pcbLayer );
+                padstack.AddPrimitive( new PCB_SHAPE( shape ), pcbLayer );
+                break;
+            }
+        }
+    }
+    padstack.SetLayerSet( layerSet );
+    return padstack;
+}
+
+void CAST_CST_FILE::FOOTER::Read( BINARY_READER & aReader )
+{
+    // these pads could also be interpreted as an empty
+    // separated data header with its -1 value as 0.
+    int16_t pad[4];
+    aReader.ReadVals( pad );
+    if ( pad[0] | pad[1] | pad[2] | pad[3] )
+        THROW_BIN_FOR( "Unexpected binary content.", aReader );
+    aReader.ReadVals( size, layers, position );
+    if ( size.x <= 0 || size.y <= 0 || layers <= 0 )
+        THROW_BIN_FOR( "Unexpected binary content.", aReader );
+}
+
+void CAST_CST_FILE::Read()
+{
+    
+    m_devs.Read();
+    {
+        uint16_t netct;
+        ReadVals( netct );
+        m_nets.resize( netct );
+    }
+    for ( wxString & net : m_nets )
+        ReadStr<int8_t>( net );
+    m_tps.Read();
+    m_vias.Read();
+    m_pads.Read();
+    m_lines_copper.Read();
+    {
+        uint16_t count;
+        ReadVals( count );
+        m_lines_board.ReadSeparator();
+        m_lines_board.ReadItems( count );
+    }
+    m_shapes.Read();
+    m_metadata.Read( *this );
+
+    m_shape_lookup.clear();
+    for ( size_t i = 0; i < m_shapes.Items().size(); ++ i ) {
+        m_shape_lookup[m_shapes.Items()[i].id] = i;
+    }
+}
diff --git a/common/io/cast/cast_cst_file.h b/common/io/cast/cast_cst_file.h
new file mode 100644
index 0000000000..6ef7935ae4
--- /dev/null
+++ b/common/io/cast/cast_cst_file.h
@@ -0,0 +1,204 @@
+#ifndef CAST_CST_FILE_H_
+#define CAST_CST_FILE_H_
+
+#include <vector>
+#include <math/vector2d.h>
+#include "cast_utils.h"
+
+class BOARD_ITEM;
+class PADSTACK;
+class PCB_SHAPE;
+
+class CAST_CST_FILE : protected BINARY_READER
+{
+public:
+    CAST_CST_FILE( wxInputStream& aStream, const wxString& aSource );
+
+    typedef VECTOR2<int16_t> COORD32;
+    typedef VECTOR2<int32_t> COORD64;
+
+    template <typename T>
+    class SEPARATED_DATA : protected BINARY_READER
+    {
+    public:
+        using BINARY_READER::BINARY_READER;
+
+        void Read();
+        void ReadSeparator();
+        void ReadItems( uint16_t count );
+        std::vector<T> const & Items() const { return m_items; }
+    
+    private:
+        std::vector<T> m_items;
+    };
+
+    struct CDEV
+    {
+        wxString name;
+        COORD32 pos;
+        uint8_t layer;
+        int32_t shape;
+
+        constexpr static const char* NAME = "CDev";
+        constexpr static int16_t SEPARATOR[] = { -0x7fff };
+
+        void Read( BINARY_READER & aReader ) {
+            aReader.ReadStr<int8_t>( name );
+            name = name.Trim();
+            aReader.ReadVals( pos.x, pos.y, layer, shape );
+        }
+    };
+
+    struct CTP
+    {
+        wxString name;
+        uint8_t layer;
+        COORD32 pos;
+
+        constexpr static const char* NAME = "CTp";
+        constexpr static int16_t SEPARATOR[] = { -0x76b6 };
+
+        void Read( BINARY_READER & aReader )
+        {
+            aReader.ReadStr<int8_t>( name );
+            name = name.Trim();
+            aReader.ReadVals( layer, pos.x, pos.y );
+        }
+    };
+
+    struct CVIA
+    {
+        int32_t net, width;
+        COORD32 pos;
+        int32_t shape;
+
+        constexpr static const char* NAME = "CVia";
+        constexpr static int16_t SEPARATOR[] = { -0x6e60 };
+
+        void Read( BINARY_READER & aReader )
+        {
+            aReader.ReadVals( net, width, pos.x, pos.y, shape );
+        }
+    };
+
+    struct CPAD
+    {
+        int16_t dev, pin, net;
+        COORD32 pos;
+        int32_t shape;
+
+        constexpr static const char* NAME = "CPad";
+        constexpr static int16_t SEPARATOR[] = { -0x50d9 };
+
+        void Read( BINARY_READER & aReader )
+        {
+            aReader.ReadVals( dev, pin, net, pos.x, pos.y, shape );
+        }
+    };
+
+    struct CLINE
+    {
+        int16_t net;
+        uint8_t layer;
+        int8_t width;
+        COORD32 ends[2];
+
+        constexpr static const char* NAME = "CLine";
+        constexpr static int16_t SEPARATOR[] = { -0x50d9 };
+
+        void Read( BINARY_READER & aReader )
+        {
+            aReader.ReadVals( net, layer, width,
+                ends[0].x, ends[0].y, ends[1].x, ends[1].y );
+        }
+    };
+
+    enum CSHAPE_T : int8_t {
+        ELLIPSE = 2,
+        RECT = 1,
+        POLY = -1,
+        PADSTACK = -2,
+        QUAD = -4,
+    };
+
+    struct CSHAPE
+    {
+        union PARAM
+        {
+            PARAM() {}
+            PARAM(const PARAM& p) : id(p.id) {}
+            void operator=(const PARAM& p) { id = p.id; }
+            COORD64 coord;
+            int64_t id;
+        };
+
+        int32_t id;
+        CSHAPE_T type;
+        std::vector<PARAM> params;
+
+        bool IsCircle() const
+        {
+            if ( type == CSHAPE_T::ELLIPSE )
+                return params[0].coord.x == params[0].coord.y;
+            else
+                return false;
+        }
+        int32_t CircleRadius() const
+        {
+            if ( !IsCircle() )
+                    throw KI_PARAM_ERROR( "Shape is not a circle." );
+            return params[0].coord.x / 2;
+        }
+        COORD64 EllipseSize() const
+        {
+            if ( type != CSHAPE_T::ELLIPSE )
+                    throw KI_PARAM_ERROR( "Shape is not an ellipse." );
+            return params[0].coord;
+        }
+        COORD64 RectSize() const
+        {
+            if ( type != CSHAPE_T::RECT )
+                    throw KI_PARAM_ERROR( "Shape is not a rectangle." );
+            return params[0].coord;
+        }
+        const COORD64* QuadCorners() const
+        {
+            if ( type != CSHAPE_T::QUAD )
+                    throw KI_PARAM_ERROR( "Shape is not a quad." );
+            return &params[0].coord;
+        }
+        std::vector<VECTOR2I> PolyPoints() const;
+        ::PADSTACK Padstack( const CAST_CST_FILE& shapes, BOARD_ITEM* aParent ) const;
+
+        constexpr static const char* NAME = "CShape";
+        constexpr static int16_t SEPARATOR[] = { 0x7fff, 0x570b, -0x7fff };
+
+        void Read( BINARY_READER & aReader );
+
+        operator PCB_SHAPE() const;
+    };
+
+    struct FOOTER
+    {
+        COORD64 size;
+        int32_t layers;
+        COORD32 position;
+
+        void Read( BINARY_READER & aReader );
+    };
+
+    void Read();
+
+    SEPARATED_DATA<CDEV> m_devs;
+    std::vector<wxString> m_nets;
+    SEPARATED_DATA<CTP> m_tps;
+    SEPARATED_DATA<CVIA> m_vias;
+    SEPARATED_DATA<CPAD> m_pads;
+    SEPARATED_DATA<CLINE> m_lines_copper;
+    SEPARATED_DATA<CLINE> m_lines_board;
+    SEPARATED_DATA<CSHAPE> m_shapes;
+    std::unordered_map<uint32_t, size_t> m_shape_lookup;
+    FOOTER m_metadata;
+};
+
+#endif // CAST_CST_FILE_H_
diff --git a/common/io/cast/cast_io.cpp b/common/io/cast/cast_io.cpp
new file mode 100644
index 0000000000..eba9af816c
--- /dev/null
+++ b/common/io/cast/cast_io.cpp
@@ -0,0 +1,23 @@
+#include "cast_io.h"
+#include "cast_cst_file.h"
+#include "cast_lst_file.h"
+#include <wx/filename.h>
+#include <wx/wfstream.h>
+
+void CAST_IO::Load( const wxString& aFileName )
+{
+    wxFileName cstFN = aFileName, lstFN = aFileName, pdfFN = aFileName;
+    cstFN.SetExt( wxT( "CST" ) );
+    lstFN.SetExt( wxT( "LST" ) );
+    pdfFN.SetExt( wxT( "PDF" ) );
+    m_pdfFN = pdfFN.GetFullName();
+
+    m_cstStream.reset( new wxFileInputStream( cstFN.GetFullPath() ) );
+    m_lstStream.reset( new wxFileInputStream( lstFN.GetFullPath() ) );
+
+    m_cst.reset( new CAST_CST_FILE( *m_cstStream, cstFN.GetFullPath() ) );
+    m_lst.reset( new CAST_LST_FILE( m_lstStream.get(), lstFN.GetFullPath() ) );
+
+    m_cst->Read();
+    m_lst->Read();
+}
diff --git a/common/io/cast/cast_io.h b/common/io/cast/cast_io.h
new file mode 100644
index 0000000000..c9481c50ae
--- /dev/null
+++ b/common/io/cast/cast_io.h
@@ -0,0 +1,44 @@
+#ifndef CAST_IO_H_
+#define CAST_IO_H_
+
+#include <memory>
+#include <wx/string.h>
+#include <io/io_base.h>
+#include <template_fieldnames.h>
+
+class CAST_CST_FILE;
+class CAST_LST_FILE;
+class wxFileInputStream;
+
+//enum  CAST_FIELD_T {
+//    CAST_DEV_ID_FIELD = MANDATORY_FIELDS,
+//    CAST_FIELDS
+//};
+
+
+class CAST_IO
+{
+protected:
+    const IO_BASE::IO_FILE_DESC GetFileDescr() const
+    {
+        return IO_BASE::IO_FILE_DESC(
+                wxT( "Card Analysis Support Tool board view" ), // description
+                { "CST" }, // single file extensions
+                { "CST", "LST" }, // file extensions in folder
+                false // is single file?
+            //    true, // can read?
+            //    false // can write?
+            );
+    }
+    void Load( const wxString& aFileName );
+
+protected:
+    // stream object ownsership might go inside CST/LST
+    std::unique_ptr<wxFileInputStream> m_cstStream;
+    std::unique_ptr<wxFileInputStream> m_lstStream;
+    std::unique_ptr<CAST_CST_FILE> m_cst;
+    std::unique_ptr<CAST_LST_FILE> m_lst;
+    wxString m_pdfFN;
+};
+
+#endif
diff --git a/common/io/cast/cast_lst_file.cpp b/common/io/cast/cast_lst_file.cpp
new file mode 100644
index 0000000000..ba215df4c2
--- /dev/null
+++ b/common/io/cast/cast_lst_file.cpp
@@ -0,0 +1,413 @@
+#include <math/vector2d.h>
+#include "cast_utils.h"
+#include "cast_lst_file.h"
+
+CAST_LST_FILE::CAST_LST_FILE( wxInputStream* aStream, const wxString& aSource )
+: INPUTSTREAM_LINE_READER( aStream, aSource )
+{ }
+
+bool CAST_LST_FILE::VALUE::Empty() const
+{
+	return str.IsEmpty();
+}
+
+bool CAST_LST_FILE::VALUE::CoordZero() const
+{
+    return str == "0 0";
+}
+
+bool CAST_LST_FILE::VALUE::CoordUnknown() const
+{
+    // matches "????? ?????"
+    size_t first_pad = str.find_first_not_of('?');
+    size_t last_pad = str.find_last_not_of('?');
+    return first_pad != wxString::npos && last_pad != wxString::npos && str.find_first_not_of(' ', first_pad) == last_pad + 1;
+}
+
+wxString const & CAST_LST_FILE::VALUE::String() const
+{
+    return str;
+}
+
+long CAST_LST_FILE::VALUE::Integer() const
+{
+    long integer;
+    if ( str.ToLong(&integer) )
+        return integer;
+    else
+        throw KI_PARAM_ERROR( "Not integer data." );
+}
+
+CAST_LST_FILE::ORIENTATION CAST_LST_FILE::VALUE::Orientation() const
+{
+    if ( str.size() == 1 ) {
+        switch ( (char)str[0] ) {
+        case TOP:
+        case BOTTOM:
+        case RIGHT:
+        case LEFT:
+            return (ORIENTATION)(char)str[0];
+        default:
+            break;
+        }
+    }
+    throw KI_PARAM_ERROR( "Not orientation data." );
+}
+
+VECTOR2I CAST_LST_FILE::VALUE::Coord() const
+{
+    long x, y;
+    wxString left, right;
+    left = str.BeforeFirst( wxT( ' ' ), &right );
+    if ( left.ToLong( &x ) && right.ToLong( &y ) )
+        return { x, y };
+    else
+        throw KI_PARAM_ERROR( "Not integer data." );
+}
+
+CAST_LST_FILE::STRUCTURED_COMMENT CAST_LST_FILE::VALUE::StructuredComment() const
+{
+    STRUCTURED_COMMENT comment;
+    size_t offset, mid_offset, next_offset;
+    next_offset = str.find(' ', 0);
+    comment.footprint.assign( str, next_offset );
+
+    while ( wxString::npos != next_offset ) {
+        offset = next_offset + 1;
+        mid_offset = str.find( '=', offset );
+        next_offset = str.find( ' ', mid_offset );
+        wxString key( str, offset, mid_offset - offset );
+        wxString value( str, mid_offset + 1, next_offset - mid_offset - 1 );
+        if ( comment.fields.count( key ) )
+            throw KI_PARAM_ERROR( "Duplicate comment field." );
+        comment.fields[key] = value;
+    }
+    return comment;
+}
+
+CAST_LST_FILE::DEV::DEV( size_t aIdx, size_t aGroup, std::vector<FIELD>& aFields, char const* line_, size_t length )
+: idx( aIdx ),
+  group( aGroup ),
+  fields( aFields )
+{
+    wxString line(line_, length);
+    size_t next_start = line.find_first_not_of( ' ' );
+    for (auto & field : fields) {
+        wxString & value = values.emplace_back( VALUE{ .field = field, .str = {}, .idx = values.size() - 1 } ).str;
+        size_t start = next_start;
+        if ( start < field.stop && start != wxString::npos ) {
+            next_start = line.find_first_not_of( ' ', start );
+            if ( field.stop + 1 >= line.size() // if the line ends before this field does
+                    || next_start >= field.margin_stop // or the right margin is respected
+                    || next_start == wxString::npos)
+            {
+                // then extract it as a field value
+                size_t stop = line.find_last_not_of( ' ', field.stop ) + 1;
+                value.assign( line, start, stop - start );
+            } else {
+                // otherwise this is the last field value, and may overflow
+                value.assign( line, start, length - start );
+            }
+        }
+        field.lookup.insert( std::pair<wxString, size_t>( value, idx ) );
+    }
+    if ( Positioned() ) {
+        Lead1();
+    }
+    if ( MultiplePins() ) {
+        Lead2(); LeadSpace();
+        Orientation();
+    }
+}
+
+CAST_LST_FILE::VALUE& CAST_LST_FILE::DEV::Lookup(wxString const & field)
+{
+    for ( size_t i = 0 ; i < fields.size(); ++ i ) {
+        if ( fields[i].name == field ) {
+            return values[i];
+        }
+    }
+    throw KI_PARAM_ERROR( "Field not found." );
+}
+
+const CAST_LST_FILE::VALUE& CAST_LST_FILE::DEV::Lookup(wxString const & field) const
+{
+    for ( size_t i = 0 ; i < fields.size(); ++ i ) {
+        if ( fields[i].name == field ) {
+            return values[i];
+        }
+    }
+    throw KI_PARAM_ERROR( "Field not found." );
+}
+
+wxString const& CAST_LST_FILE::DEV::RefDes() const
+{
+	return values[0].String();
+}
+
+size_t CAST_LST_FILE::DEV::Layer() const
+{
+    return Lookup("PLN NBR").Integer();
+}
+
+wxString const& CAST_LST_FILE::DEV::Datasheet() const
+{
+    return Lookup("P/N").String();
+}
+
+wxString const& CAST_LST_FILE::DEV::Description() const
+{
+    return Lookup("DESCRIPTION").String();
+}
+
+wxString const& CAST_LST_FILE::DEV::Value() const
+{
+    return Lookup("CDB NAME").String();
+}
+
+size_t CAST_LST_FILE::DEV::SchPage() const
+{
+    return Lookup("LOGC PAGE").Integer();
+}
+
+wxString CAST_LST_FILE::DEV::Footprint() const
+{
+    return Lookup("COMMENT").StructuredComment().footprint;
+}
+
+bool CAST_LST_FILE::DEV::Positioned() const
+{
+    const VALUE& lstPos = Lookup("CENTER POS");
+    return !lstPos.Empty() && !lstPos.CoordZero() && !lstPos.CoordUnknown();
+}
+
+VECTOR2I CAST_LST_FILE::DEV::Position() const
+{
+    return Lookup( "CENTER POS" ).Coord();
+}
+
+VECTOR2I CAST_LST_FILE::DEV::Lead1() const
+{
+    return Lookup("LEAD-L1").Coord();
+}
+
+size_t CAST_LST_FILE::DEV::LeadSpace() const
+{
+    return Lookup("LEAD SPACE").Integer();
+}
+
+bool CAST_LST_FILE::DEV::MultiplePins() const
+{
+	return !Lookup("LEAD-L2").Empty();
+}
+
+VECTOR2I CAST_LST_FILE::DEV::Lead2() const
+{
+    return Lookup("LEAD-L2").Coord();
+}
+
+CAST_LST_FILE::ORIENTATION CAST_LST_FILE::DEV::Orientation() const
+{
+    return Lookup("L2 ORI").Orientation();
+}
+
+/*
+bool CAST_LST_FILE::DEV::Position( VECTOR2I& position ) const
+{
+    auto& lstPos = Lookup( "CENTER POS" );
+    if ( lstPos.Empty() ) {
+        return false;
+    } else {
+        position = lstPos.Coord();
+        return true;
+    }
+}*/
+
+CAST_LST_FILE::DEV& CAST_LST_FILE::Lookup( const VECTOR2I& position )
+{
+    return Lookup( "CENTER POS", wxString() << position.x << ' ' << position.y );
+}
+
+bool CAST_LST_FILE::DEV::Present() const
+{
+    // returns false only if x == 0 and y == 0
+    if ( Positioned() ) {
+        VECTOR2I pos = Position();
+        return pos.x != 0 || pos.y != 0;
+    } else {
+        return true;
+    }
+}
+
+bool CAST_LST_FILE::DEV::Populated() const
+{
+	return Lookup("P/N").String() != "NO_ASM";
+}
+
+void CAST_LST_FILE::Read()
+{
+    std::vector<wxString> headerLines;
+    m_props.clear();
+    m_title.clear();
+    m_fields.clear();
+    m_devs.clear();
+    m_comments.clear();
+    size_t group = 0;
+    size_t section = 0;
+    while ( char* line = ReadLine() ) {
+        size_t length = Length();
+        if ( length == 0 ) {
+            continue;
+        } else if ( line[length - 1] == '\n' ) {
+            -- length;
+        }
+        switch ( line[0] ) {
+        case '$':
+            // control
+            switch ( line[1] ) {
+            case ' ':
+                if ( !m_fields.empty() || section )
+                    THROW_LINE( "Field names after positions?" );
+                headerLines.emplace_back( line, length );
+                break;
+            case '=': {
+                if ( section ) {
+                    THROW_LINE( "Multiple field position lines." );
+                }
+                wxString fieldPositionsLine( line, length );
+                for ( size_t offset = 1; offset != wxString::npos; ) {
+                    FIELD & prev_field = m_fields.back();
+                    FIELD & field = m_fields.emplace_back();
+                    field.start = prev_field.margin_stop = fieldPositionsLine.find( '=', offset );
+                    if ( field.start == wxString::npos )
+                        break;
+                    offset = field.stop = fieldPositionsLine.find( ' ', field.start );
+                    if ( field.stop == wxString::npos ) {
+                        field.stop = fieldPositionsLine.size();
+                    }
+                    for ( wxString & headerLine : headerLines ) {
+                        size_t front = headerLine.find_first_not_of( ' ', field.start );
+                        size_t back = headerLine.find_last_not_of( ' ', field.stop );
+                        if ( back > front ) {
+                            if ( !field.name.empty() )
+                                -- front; // include 1 space as a wordbreak
+                            field.name.Append( headerLine.substr( front, back + 1 - front ) );
+                        }
+                    }
+                    if ( field.name.EndsWith( " XXXXX-YYYYY", &field.name ) ) // remove this suffix
+                        field.name = field.name.Trim();
+                }
+                m_fields.back().margin_stop = Length();
+                ++ section;
+                break;
+            }
+            default:
+                if ( length < 6 || line[5] != '=' ) {
+                    if ( length < 3 ) {
+                        THROW_LINE( "Unhandled control characters." );
+                    } else if ( line[1] == 'V' && line[2] == 'S' ) {
+                        // this looks likely to always be the first 4 characters, giving the file a signature
+                        m_version = line[3] - '0';
+                        if ( m_version != 2 )
+                            THROW_LINE( "Unhandled LST file version." );
+                    } else if ( line[1] == 'T' && line[2] == 'T' && line[3] == ' ' ) {
+                        m_title.assign( &line[4], Length() - 1 - 4 );
+                    } else if ( line[1] == 'S' && line[2] == 'P' ) {
+                        ++ group;
+                        if ( length > 3 ) {
+                            if ( length != 5 || line[3] != ' ' || line[4] != '2' ) {
+                                THROW_LINE( "Unhandled control characters." );
+                            }
+                        }
+                        // a single SP (trailed by \n) appears to be between component types (groups)
+                        // the file ends with an empty SP, followed by an "SP 2", and then continues with
+                        // a footer format starting with a comment and then listing what looks like substitution variants
+                    } else {
+                        THROW_LINE( "Unhandled control characters." );
+                    }
+                } else {
+                    if ( length < 7 || line[4] != ' ' || line[6] != ' ' ) {
+                        THROW_LINE( "Unhandled control characters." );
+                    }
+                    wxString key( &line[1], 3 ), value( &line[7], length - 7 );
+                    key = key.Trim();
+                    value.EndsWith( " <-- TO BE CHANGE", &value ); // remove suffix
+                    m_props[key] = value.Trim();
+                }
+            }
+            break;
+        case ' ':
+            switch ( line[1] ) {
+            case ' ':
+                // trailing commentary
+                m_comments.Append( line, Length() );
+                break;
+            default:
+                // data
+                m_devs.emplace_back( m_devs.size(), group, m_fields, line, length );
+                break;
+            }
+            break;
+        case '-': {
+            // my file had substitution information
+            // all columns were empty except for p/n and comment
+            // it would be appropriate to find the matching p/ns and add a line to their comments
+            // my file also had a spurious ^B character here in a description field
+            DEV additional_data( -1, group, m_fields, line, length );
+            bool processed = false;
+            for ( auto & field : m_fields ) {
+                auto & value = additional_data.values[field.idx];
+                if ( !value.Empty() ) {
+                    DEV * match;
+                    try {
+                        match = &Lookup( field.idx, value.str );
+                    } catch ( ... ) {
+                        continue;
+                    }
+                    for ( auto & field2 : m_fields ) {
+                        auto & value2 = additional_data.values[field2.idx];
+                        if ( field2.idx != field.idx && !value2.Empty() ) {
+                            match->values[value2.idx].str.Append( ' ' );
+                            match->values[value2.idx].str.Append( value2.String() );
+                        }
+                    }
+                    processed = true;
+                    break;
+                }
+            }
+            if ( !processed )
+                THROW_LINE( "No match found for '-' line." );
+            break;
+        }
+        default:
+            THROW_LINE( "Unhandled line start character." );
+        }
+    }
+}
+
+CAST_LST_FILE::DEV& CAST_LST_FILE::Lookup( size_t fieldidx, const wxString& value )
+{
+    FIELD const& field = m_fields.at( fieldidx );
+    auto bounds = field.lookup.equal_range( value );
+    if ( bounds.first != bounds.second ) {
+        auto it = bounds.first;
+        size_t dev_idx = (it++)->second;
+        if ( it == bounds.second ) {
+            return m_devs.at(dev_idx);
+        } else {
+            throw KI_PARAM_ERROR( "More than one value matches." );
+        }
+    } else {
+        throw KI_PARAM_ERROR( "Value not found." );
+    }
+}
+
+CAST_LST_FILE::DEV& CAST_LST_FILE::Lookup( const wxString& fieldname, const wxString& value )
+{
+    for ( size_t fieldIdx = 0; fieldIdx < m_fields.size(); ++ fieldIdx ) {
+        if ( m_fields[fieldIdx].name == fieldname ) {
+            return Lookup( fieldIdx, value );
+        }
+    }
+    throw KI_PARAM_ERROR( "Field not found." );
+}
diff --git a/common/io/cast/cast_lst_file.h b/common/io/cast/cast_lst_file.h
new file mode 100644
index 0000000000..7dcca29757
--- /dev/null
+++ b/common/io/cast/cast_lst_file.h
@@ -0,0 +1,99 @@
+#ifndef CAST_LST_FilE_H_
+#define CAST_LST_FilE_H_
+
+#include <richio.h>
+#include <wx/string.h>
+#include <unordered_map>
+#include <vector>
+
+template <class> class VECTOR2;
+typedef VECTOR2<int32_t> VECTOR2I;
+
+class CAST_LST_FILE : public INPUTSTREAM_LINE_READER
+{
+public:
+    CAST_LST_FILE( wxInputStream* aStream, const wxString& aSource );
+
+    struct FIELD
+    {
+        wxString name;
+
+        std::unordered_multimap<wxString, size_t> lookup;
+        size_t idx, start, stop, margin_stop;
+    };
+
+    enum ORIENTATION : char
+    {
+        TOP = 'T',
+        BOTTOM = 'B',
+        RIGHT = 'R',
+        LEFT = 'L'
+    };
+
+    struct STRUCTURED_COMMENT
+    {
+        wxString footprint;
+        std::unordered_map<wxString, wxString> fields;
+    };
+
+    struct VALUE
+    {
+        FIELD & field;
+        wxString str;
+        size_t idx;
+
+        bool Empty() const;
+        bool CoordZero() const;
+        bool CoordUnknown() const;
+        wxString const & String() const;
+        long Integer() const;
+        ORIENTATION Orientation() const;
+        VECTOR2I Coord() const;
+        STRUCTURED_COMMENT StructuredComment() const;
+    };
+
+    struct DEV
+    {
+        size_t idx;
+        size_t group;
+        std::vector<FIELD>& fields;
+        std::vector<VALUE> values;
+
+        DEV( size_t idx, size_t group, std::vector<FIELD>& fields, char const* line_, size_t length );
+        VALUE& Lookup(wxString const & field);
+        const VALUE& Lookup(wxString const & field) const;
+
+        wxString const& RefDes() const;
+        size_t Layer() const;
+        wxString const& Datasheet() const;
+        wxString const& Description() const;
+        wxString const& Value() const;
+        size_t SchPage() const;
+        wxString Footprint() const;
+        bool Positioned() const;
+        VECTOR2I Position() const;
+        VECTOR2I Lead1() const;
+        size_t LeadSpace() const;
+        bool MultiplePins() const;
+        VECTOR2I Lead2() const;
+        ORIENTATION Orientation() const;
+        //bool Position( VECTOR2I& position ) const;
+        bool Present() const;
+        bool Populated() const;
+    };
+
+    void Read();
+
+    DEV& Lookup( size_t fieldidx, const wxString& value );
+    DEV& Lookup( const wxString& fieldname, const wxString& value );
+    DEV& Lookup( const VECTOR2I& position );
+
+    uint8_t m_version;
+    std::unordered_map<wxString, wxString> m_props;
+    wxString m_title;
+    wxString m_comments;
+    std::vector<FIELD> m_fields;
+    std::vector<DEV> m_devs;
+};
+
+#endif // CAST_LST_FilE_H_
diff --git a/common/io/cast/cast_utils.cpp b/common/io/cast/cast_utils.cpp
new file mode 100644
index 0000000000..a35a15ffc3
--- /dev/null
+++ b/common/io/cast/cast_utils.cpp
@@ -0,0 +1,30 @@
+#include "cast_utils.h"
+
+BINARY_READER::BINARY_READER( wxInputStream& aStream, const wxString& aSource )
+: m_stream( aStream ),
+  m_source( aSource )
+{ }
+
+BINARY_READER::~BINARY_READER()
+{ }
+
+void BINARY_READER::ReadStr( wxString & str, size_t size )
+{
+    if ( sizeof(wxStringCharType) == sizeof(char) ) {
+        wxStringBuffer buf( str, size);
+        m_stream.ReadAll( (wxStringCharType*)buf, size );
+    } else {
+        char* charbuf;
+        {
+            // allocate
+            wxStringBuffer buf( str, size);
+            // fetch raw pointer halfway in
+            charbuf = (char*)(wxStringCharType*)buf + size;
+            // scope lets ~wxStringBuffer finalize state
+        }
+        // write into raw pointer aliased halfway in
+        m_stream.ReadAll( charbuf, size );
+        // perform conversion in-place, expanding down
+        str.assign( charbuf, size );
+    }
+}
diff --git a/common/io/cast/cast_utils.h b/common/io/cast/cast_utils.h
new file mode 100644
index 0000000000..f8892d1ebf
--- /dev/null
+++ b/common/io/cast/cast_utils.h
@@ -0,0 +1,46 @@
+#ifndef CAST_UTILS_H_
+#define CAST_UTILS_H_
+
+#include <ki_exception.h>
+#include <wx/stream.h>
+#include <wx/string.h>
+
+#define THROW_BIN_FOR( aProblem, aReader ) THROW_PARSE_ERROR( aProblem, aReader.Source(), nullptr, -1, aReader.Tell() );
+#define THROW_BIN( aProblem ) THROW_BIN_FOR( aProblem, ( *this ))
+#define THROW_LINE( aProblem ) THROW_PARSE_ERROR( aProblem, GetSource(), Line(), LineNumber(), 0 );
+
+class BINARY_READER
+{
+public:
+    BINARY_READER( wxInputStream& aStream, const wxString& aSource );
+    ~BINARY_READER();
+
+    void ReadStr( wxString & str, size_t size );
+
+    template <typename T, typename... Ts>
+    void ReadVals( T& value, Ts&... rest )
+    {
+        if ( !m_stream.ReadAll( &value, sizeof( value )) )
+            THROW_BIN( "Short file." );
+        if constexpr ( sizeof...( rest ) ) ReadVals( rest... );
+    }
+
+    template <typename T>
+    void ReadStr( wxString & str )
+    {
+        T size;
+        ReadVals( size );
+        if ( size < 0 )
+            THROW_BIN( "Unexpected binary content." );
+        return ReadStr( str, size );
+    }
+
+    wxString Source() { return m_source; }
+    wxFileOffset Tell() { return m_stream.TellI(); }
+
+private:
+    wxInputStream& m_stream;
+    const wxString& m_source;
+};
+
+#endif	// CAST_UTILS_H_
diff --git a/eeschema/sch_io/cast/sch_io_cast.cpp b/eeschema/sch_io/cast/sch_io_cast.cpp
new file mode 100644
index 0000000000..2394d5637e
--- /dev/null
+++ b/eeschema/sch_io/cast/sch_io_cast.cpp
@@ -0,0 +1,31 @@
+#include <common/io/cast/cast_cst_file.h>
+#include <common/io/cast/cast_cst_file.h>
+#include <sch_io/cast/sch_io_cast.h>
+
+int mils( int mils )
+{
+    return schIUScale.MilsToIU( mils );
+}
+
+VECTOR2I mils( VECTOR2I mils )
+{
+    return { ::mils( mils.x ), ::mils( mils.y ) };
+}
+
+SCH_IO_CAST::SCH_IO_CAST()
+{ }
+
+SCH_IO_CAST::~SCH_IO_CAST()
+{ }
+
+BOARD* SCH_IO_CAST::LoadSchematicFile( const wxString& aFileName, SCH_SHEET* aAppendtoMe,
+                                       const STRING_UTF8_MAP* aProperties )
+{
+    if ( aAppendToMe != nullptr ) {
+        m_sheet = aAppendToMe;
+    } else if ( m_board == nullptr ) {
+        m_sheet = new SCH_SHEET();
+        m_sheet->SetFileName( m_cst.Source() );
+    }
+    return m_sheet;
+}
diff --git a/eeschema/sch_io/cast/sch_io_cast.h b/eeschema/sch_io/cast/sch_io_cast.h
new file mode 100644
index 0000000000..6acb44ab3d
--- /dev/null
+++ b/eeschema/sch_io/cast/sch_io_cast.h
@@ -0,0 +1,26 @@
+#ifndef SCH_IO_CAST_H_
+#define SCH_IO_CAST_H_
+
+#include <sch_io/sch_io.h>
+#include <io/cast/cast_io.h>
+
+class SCH_IO_CST_PLUGIN : public SCH_IO, public CAST_IO
+{
+public:
+    SCH_IO_CST_PLUGIN();
+    virtual ~SCH_IO_CST_PLUGIN();
+
+    const IO_BASE::IO_FILE_DESC GetSchematicFileDesc() const override
+    {
+        return GetFileDescr();
+    }
+
+    SCH_SHEET* LoadSchematicFile( const wxString& aFileName, SCHEMATIC* aSchematic,
+                                  SCH_SHEET* aAppendToMe = nullptr,
+                                  const std::map<std::string, UTF8>* aProperties = nullptr ) override;
+
+private:
+    SCH_SHEET* m_sheet;
+};
+
+#endif // SCH_IO_CAST_H_
diff --git a/pcbnew/CMakeLists.txt b/pcbnew/CMakeLists.txt
index d8c5af6abc..2bce32dbe8 100644
--- a/pcbnew/CMakeLists.txt
+++ b/pcbnew/CMakeLists.txt
@@ -707,13 +707,14 @@ endif()
 add_subdirectory( pcb_io/pcad )
 add_subdirectory( pcb_io/altium )
 add_subdirectory( pcb_io/cadstar )
+add_subdirectory( pcb_io/cast )
 add_subdirectory( pcb_io/easyeda )
 add_subdirectory( pcb_io/easyedapro )
 add_subdirectory( pcb_io/fabmaster )
 add_subdirectory( pcb_io/ipc2581 )
 add_subdirectory( pcb_io/odbpp )
 
-set( PCBNEW_IO_LIBRARIES pcad2kicadpcb altium2pcbnew cadstar2pcbnew easyeda easyedapro fabmaster ipc2581 odbpp CACHE INTERNAL "")
+set( PCBNEW_IO_LIBRARIES pcad2kicadpcb altium2pcbnew cadstar2pcbnew cast easyeda easyedapro fabmaster ipc2581 odbpp CACHE INTERNAL "")
 
 # a very small program launcher for pcbnew_kiface
 add_executable( pcbnew WIN32 MACOSX_BUNDLE
diff --git a/pcbnew/pcb_io/cast/CMakeLists.txt b/pcbnew/pcb_io/cast/CMakeLists.txt
new file mode 100644
index 0000000000..b58025babe
--- /dev/null
+++ b/pcbnew/pcb_io/cast/CMakeLists.txt
@@ -0,0 +1,12 @@
+
+# Sources for the pcbnew pcb_io called CAST
+
+include_directories( ${CMAKE_CURRENT_SOURCE_DIR} )
+
+set( CAST_SRCS
+    pcb_io_cast.cpp
+    )
+
+add_library( cast STATIC ${CAST_SRCS} )
+
+target_link_libraries( cast pcbcommon )
diff --git a/pcbnew/pcb_io/cast/pcb_io_cast.cpp b/pcbnew/pcb_io/cast/pcb_io_cast.cpp
new file mode 100644
index 0000000000..6f072dfa96
--- /dev/null
+++ b/pcbnew/pcb_io/cast/pcb_io_cast.cpp
@@ -0,0 +1,367 @@
+
+#include <board.h>
+#include <io/cast/cast_cst_file.h>
+#include <io/cast/cast_lst_file.h>
+#include <footprint.h>
+#include <pad.h>
+#include <pcb_shape.h>
+#include <pcb_target.h>
+#include <pcb_track.h>
+#include <pcb_io/cast/pcb_io_cast.h>
+#include <wx/wfstream.h>
+
+using namespace std;
+
+int mils( int mils )
+{
+    return pcbIUScale.MilsToIU( mils );
+}
+
+VECTOR2I mils( VECTOR2I mils )
+{
+    return { ::mils( mils.x ), ::mils( mils.y ) };
+}
+
+PCB_IO_CAST::PCB_IO_CAST()
+: PCB_IO( wxS( "CAST" ) )
+{ }
+
+PCB_IO_CAST::~PCB_IO_CAST()
+{ }
+
+BOARD* PCB_IO_CAST::LoadBoard( const wxString& aFileName, BOARD* aAppendToMe,
+                               const std::map<std::string, UTF8>* aProperties,
+                               PROJECT* aProject )
+{
+    if ( aAppendToMe != nullptr ) {
+        m_board = aAppendToMe;
+    } else if ( m_board == nullptr ) {
+        m_board = new BOARD();
+        m_board->SetFileName( m_lst->GetSource() );
+    }
+    m_props = aProperties;
+
+    Load( aFileName );
+
+    LSET filledLayers;
+    std::unordered_map<wxString,uint16_t> devIdxByRefDes;
+    std::vector<BOARD_ITEM*> bulkAddedItems;
+    devIdxByRefDes.reserve( std::max( m_cst->m_devs.Items().size(), m_lst->m_devs.size() ) );
+    bulkAddedItems.reserve(
+            devIdxByRefDes.size() +
+            m_cst->m_nets.size() +
+            m_cst->m_tps.Items().size() +
+            m_cst->m_vias.Items().size() +
+            m_cst->m_pads.Items().size() +
+            m_cst->m_lines_copper.Items().size() +
+            m_cst->m_lines_board.Items().size() +
+            m_cst->m_shapes.Items().size() );
+
+    // add title
+    TITLE_BLOCK pcbTitleBlock;
+    pcbTitleBlock.SetTitle( m_lst->m_title );
+    m_board->SetTitleBlock( pcbTitleBlock );
+
+    // add devs
+    for ( uint16_t cstDevId = 0; cstDevId < m_cst->m_devs.Items().size(); ++ cstDevId ) {
+        auto & cstDev = m_cst->m_devs.Items()[cstDevId];
+        filledLayers.set( cstDev.layer - 1 );
+        FOOTPRINT* fp = new FOOTPRINT( m_board );
+        // FPID? an importer i looked at set it
+        //fp->SetFPID( { cstfn.GetName(), dev.name } );
+        fp->SetPosition( mils( cstDev.pos - m_cst->m_metadata.position ) );
+        // orientation?
+        fp->SetReference( cstDev.name );
+        fp->SetLayer( (PCB_LAYER_ID)(cstDev.layer - 1) );
+        // fabmaster added a text item
+        PCB_SHAPE shape = m_cst->m_shapes.Items()[cstDev.shape];
+        shape.SetLayer( (PCB_LAYER_ID)(cstDev.layer - 1) );
+        fp->Add( new PCB_SHAPE( shape ), ADD_MODE::APPEND );
+        m_board->Add( fp, ADD_MODE::BULK_APPEND );
+        if ( bulkAddedItems.size() != cstDevId ) {
+            throw KI_PARAM_ERROR( "logic assertion failed" );
+        }
+        devIdxByRefDes[cstDev.name] = bulkAddedItems.size();
+        bulkAddedItems.push_back( fp );
+    }
+
+    // calculate lst position
+    size_t nSharedPositions;
+    m_lstPosition = VECTOR2D(0,0);
+    for ( auto & lstDev : m_lst->m_devs ) {
+        auto devIdxIt = devIdxByRefDes.find( lstDev.RefDes() );
+        if ( devIdxIt != devIdxByRefDes.end() && lstDev.Positioned() ) {
+            VECTOR2I cstPosition = m_cst->m_devs.Items()[devIdxIt->second].pos - m_cst->m_metadata.position;
+            VECTOR2I lstPosition = lstDev.Position() - cstPosition;
+            if ( 0 == nSharedPositions ) {
+                m_lstPositionMin = m_lstPositionMax = lstPosition;
+            } else {
+                m_lstPositionMin = std::min( lstPosition, m_lstPositionMin );
+                m_lstPositionMax = std::max( lstPosition, m_lstPositionMax );
+            }
+            m_lstPosition += lstPosition;
+            ++ nSharedPositions;
+        }
+    }
+    if ( 0 == nSharedPositions )
+        throw KI_PARAM_ERROR( "No shared positions between CST and LST" );
+    if ( m_lstPositionMax.x > m_lstPositionMin.x + 393 ||
+         m_lstPositionMax.y > m_lstPositionMin.y + 393 )
+        throw KI_PARAM_ERROR( "Positions between LST and CST severely mismatch" );
+    m_lstPosition = m_lstPosition / nSharedPositions;
+
+    // add lst items
+    for ( auto & lstDev : m_lst->m_devs ) {
+        FOOTPRINT* fp;
+        auto devIdxIt = devIdxByRefDes.find( lstDev.RefDes() );
+        // orientation information?
+        if ( devIdxIt == devIdxByRefDes.end() ) {
+            fp = new FOOTPRINT( m_board );
+            fp->SetReference( lstDev.RefDes() );
+            fp->SetLayer( PCB_LAYER_ID( lstDev.Layer() - 1 ) );
+            if ( lstDev.Positioned() ) {
+                fp->SetPosition( mils( lstDev.Position() - m_lstPosition ) );
+            }
+            m_board->Add( fp, ADD_MODE::BULK_APPEND );
+            devIdxByRefDes[ fp->GetReference() ] = bulkAddedItems.size();
+            bulkAddedItems.push_back( fp );
+        } else {
+            const CAST_CST_FILE::CDEV& cstDev = m_cst->m_devs.Items()[devIdxIt->second];
+            fp = (FOOTPRINT*) bulkAddedItems[devIdxIt->second];
+            if ( cstDev.layer != lstDev.Layer() ) {
+                throw KI_PARAM_ERROR( "Layer number mismatches between CST and LST." );
+            }
+        }
+        fp->SetIsPlaced( lstDev.Populated() );
+        if ( lstDev.Positioned() ) {
+            switch ( lstDev.Orientation() ) {
+            case CAST_LST_FILE::ORIENTATION::TOP:
+                fp->SetOrientation( EDA_ANGLE( 0, DEGREES_T ) );
+                break;
+            case CAST_LST_FILE::ORIENTATION::BOTTOM:
+                fp->SetOrientation( EDA_ANGLE( 180, DEGREES_T ) );
+                break;
+            case CAST_LST_FILE::ORIENTATION::RIGHT:
+                fp->SetOrientation( EDA_ANGLE( 90, DEGREES_T ) );
+                break;
+            case CAST_LST_FILE::ORIENTATION::LEFT:
+                fp->SetOrientation( EDA_ANGLE( -90, DEGREES_T ) );
+                break;
+            }
+        }
+        // COMMENT appears to contain the footprint id (FOOTPRINT_FIELD) and the height (clearance?)
+        for ( auto & lstField : m_lst->m_fields ) {
+            auto & lstValue = lstDev.values[lstField.idx];
+            if ( !lstValue.Empty() ) {
+                PCB_FIELD * pcbField;
+                if ( lstField.name == "P/N" ) {
+                    pcbField = fp->GetField( FIELD_T::DATASHEET );
+                } else if ( lstField.name == "DESCRIPTION" ) {
+                    pcbField = fp->GetField( FIELD_T::DESCRIPTION );
+                    fp->SetLibDescription( lstValue.String() );
+                } else if ( lstField.name == "CDB NAME" ) {
+                    pcbField = fp->GetField( FIELD_T::VALUE );
+                } else {
+                    if ( lstField.name == "COMMENT" ) {
+                        auto comment = lstValue.StructuredComment();
+                        auto footprint = comment.footprint;
+                        //fp->SetFPID( footprint );
+                        fp->GetField( FIELD_T::FOOTPRINT )->SetText( footprint );
+                        // each dev has H field like H=0.1
+                        // but i have not checked the units of this field
+                        double H;
+                        if ( comment.fields["H"].ToDouble( &H ) )
+                            fp->SetLocalClearance( mils( H * 1000 ) );
+                    } else if ( lstField.name == "LOGC PAGE" ) {
+                        fp->SetSheetname( lstValue.String() );
+                        fp->SetSheetfile( m_pdfFN );
+                    }
+                    pcbField = new PCB_FIELD( fp, FIELD_T::USER, lstField.name );
+                    pcbField->SetLayer( fp->GetLayer() );
+                    pcbField->StyleFromSettings( m_board->GetDesignSettings(), true );
+                    fp->Add( pcbField );
+                }
+                pcbField->SetName( lstField.name );
+                pcbField->SetText( lstValue.String() );
+                pcbField->SetVisible( true );
+            }
+        }
+    }
+
+    // add nets
+    for ( size_t netCode = 0; netCode < m_cst->m_nets.size(); ++netCode )
+    {
+        if ( netCode > NETINFO_LIST::UNCONNECTED || !m_board->FindNet( NETINFO_LIST::UNCONNECTED ) )
+        {
+            auto & netName = m_cst->m_nets[netCode];
+            NETINFO_ITEM* net = new NETINFO_ITEM( m_board, netName, netCode);
+            m_board->Add( net, ADD_MODE::BULK_APPEND, true );
+            bulkAddedItems.push_back( net );
+        }
+    }
+
+    // add tps?
+    // not yet sure what tps are
+    // the first two align with vias
+    // i flipped the board over for the second and the other end was under a chip, i think it might have been a bga chip
+    for ( auto & cstTp : m_cst->m_tps.Items() ) {
+        filledLayers.set( cstTp.layer - 1 );
+        PCB_TARGET* pcbT = new PCB_TARGET( m_board );
+        pcbT->SetPosition( mils( cstTp.pos - m_cst->m_metadata.position ) );
+        //pcbT->SetReference( cstTp.name );
+        pcbT->SetLayer( PCB_LAYER_ID( cstTp.layer - 1 ) );
+        m_board->Add( pcbT, ADD_MODE::BULK_APPEND );
+        bulkAddedItems.push_back( pcbT );
+    }
+
+    // add vias
+    // considered vias 0-19 
+    //      width is likely drill width
+    //      width=0 has only pads on 1 layer
+    //      pads are a stack and show the pad size
+    for ( auto & cstVia : m_cst->m_vias.Items() ) {
+        PCB_VIA* pcbVia = new PCB_VIA( m_board );
+        const CAST_CST_FILE::CSHAPE & cstPadstack = m_cst->m_shapes.Items()[m_cst->m_shape_lookup[cstVia.shape]];
+        PADSTACK pcbPadstack = cstPadstack.Padstack( *m_cst, m_board) ; //m_cst->m_shapes.Items() );
+        LSET layerSet = pcbPadstack.LayerSet();
+        pcbVia->SetPadstack( pcbPadstack );
+        pcbVia->SetNetCode( cstVia.net );
+        pcbVia->SetDrill( mils( cstVia.width ) );
+        pcbVia->SetStart( mils( cstVia.pos - m_cst->m_metadata.position ) );
+        pcbVia->SetEnd( mils( cstVia.pos - m_cst->m_metadata.position ) );
+        if ( layerSet.size() >= m_cst->m_metadata.layers )
+            pcbVia->SetViaType( VIATYPE::THROUGH );
+        else if ( layerSet.size() <= 2 && ( layerSet.Contains( PCB_LAYER_ID( 0 ) ) || layerSet.Contains( PCB_LAYER_ID( m_cst->m_metadata.layers - 1 ) ) ) )
+            pcbVia->SetViaType( VIATYPE::MICROVIA );
+        else
+            pcbVia->SetViaType( VIATYPE::BURIED );
+        m_board->Add( pcbVia, ADD_MODE::BULK_APPEND, true );
+        bulkAddedItems.push_back( pcbVia );
+    }
+
+    // add pads
+    size_t last_lst_tp = -1;
+    for ( size_t cstPadIdx = 0; cstPadIdx < m_cst->m_pads.Items().size(); ++ cstPadIdx ) {
+        const CAST_CST_FILE::CPAD& cstPad = m_cst->m_pads.Items()[cstPadIdx];
+        FOOTPRINT* fp = nullptr;
+        PAD* pcbPad;
+        const CAST_CST_FILE::CSHAPE& cstPadstack = m_cst->m_shapes.Items()[m_cst->m_shape_lookup[cstPad.shape]];
+        if ( cstPad.dev >= 0 ) {
+            fp = (FOOTPRINT*) bulkAddedItems[ cstPad.dev ];
+        } else if ( cstPad.dev == -1 ) {
+            if ( cstPad.pin == 1 ) {
+                // testpad
+                for ( CAST_LST_FILE::DEV& lstDev : m_lst->m_devs ) {
+                    if ( lstDev.Positioned() ) {
+                        auto lstPosition = lstDev.Position() - cstPad.pos;
+                        if ( lstPosition.x >= m_lstPositionMin.x && lstPosition.y >= m_lstPositionMin.y && lstPosition.x <= m_lstPositionMax.x && lstPosition.y <= m_lstPositionMax.y ) {
+                            if ( fp != nullptr )
+                                throw KI_PARAM_ERROR( "Multiple LST items at position of unassociated pad." );
+                            // found lst item at this pos
+                            uint16_t devIdx = devIdxByRefDes.at( lstDev.RefDes() );
+                            fp = (FOOTPRINT*) bulkAddedItems.at( devIdx );
+                        }
+                    }
+                }
+            } else if ( cstPad.pin == 0 ) {
+                // i am not experienced in pcb design to know
+                // what these pads are. they are disconnected
+                // and appear with shaded boxes around them.
+                // maybe EMI management?
+                auto& cstShape = m_cst->m_shapes.Items()[m_cst->m_shape_lookup[cstPad.shape]];
+                size_t layer;
+                fp = new FOOTPRINT( m_board );
+                for ( layer = 0; !cstShape.params[layer].id; ++ layer );
+                fp->SetReference( wxString( "PAD" ) << cstPadIdx );
+                fp->SetLayer( (PCB_LAYER_ID)layer );
+                fp->SetPosition( mils( cstPad.pos - m_cst->m_metadata.position ) );
+                m_board->Add( fp, ADD_MODE::BULK_APPEND );
+                bulkAddedItems.push_back( fp );
+            } else {
+                throw KI_PARAM_ERROR( "Unhandled CPad with pins but no matching CDev." );
+            }
+        } else {
+            throw KI_PARAM_ERROR( "Unhandled CPad with negative CDev id.");
+        }
+        pcbPad = new PAD( fp );
+        PADSTACK pcbPadstack = cstPadstack.Padstack( *m_cst, pcbPad );
+        LSET layers = pcbPadstack.LayerSet();
+        pcbPad->SetPadstack( pcbPadstack );
+        pcbPad->SetNetCode( cstPad.net );
+        pcbPad->SetPosition( mils( cstPad.pos - m_cst->m_metadata.position ) );
+        pcbPad->SetNumber( wxString() << cstPad.pin );
+    }
+
+    // add lines
+    for ( CAST_CST_FILE::CLINE const & cstLine : m_cst->m_lines_copper.Items() ) {
+        PCB_TRACK* pcbTrack = new PCB_TRACK( m_board );
+        pcbTrack->SetNetCode( cstLine.net );
+        pcbTrack->SetWidth( mils( cstLine.width ) );
+        pcbTrack->SetLayer( PCB_LAYER_ID( cstLine.layer - 1 ) );
+        filledLayers.set( PCB_LAYER_ID( cstLine.layer - 1 ) );
+        pcbTrack->SetStart( mils( cstLine.ends[0] - m_cst->m_metadata.position ) );
+        pcbTrack->SetEnd( mils( cstLine.ends[1] - m_cst->m_metadata.position ) );
+        m_board->Add( pcbTrack, ADD_MODE::BULK_APPEND, true );
+        bulkAddedItems.push_back( pcbTrack );
+    }
+    {
+        //std::vector<VECTOR2I> polyPoints;
+        int8_t width = m_cst->m_lines_board.Items()[0].width;
+        for ( size_t i = 1; i < m_cst->m_lines_board.Items().size(); ++ i ) {
+            CAST_CST_FILE::CLINE line0 = m_cst->m_lines_board.Items()[i - 1];
+            CAST_CST_FILE::CLINE line1 = m_cst->m_lines_board.Items()[i];
+
+            if ( line0.width != width || line1.width != width || line0.net != 0 || line1.net != 0 || line0.ends[0] != line1.ends[0] || line0.ends[1] != line1.ends[1] ) {
+                throw KI_PARAM_ERROR( "Board outline not in identical pairs." );
+            }
+            if ( line0.layer != 1 || line1.layer != m_cst->m_metadata.layers ) {
+                throw KI_PARAM_ERROR( "Board outline pairs not full layer." );
+            }
+            //if ( line0.ends[1] == m_cst->m_lines_board[( i+2 )%m_cst->m_lines_board.size()].ends[0] ) {
+            //    polyPoints.push_back( line0.ends[0] );
+            //    this doesn't appear to be how they are
+            //    throw KI_PARAM_ERROR( "Board outline pairs not tip-to-tail." );
+            //}
+            //boardOutline.push_back( line0.ends[0] );
+            PCB_SHAPE* pcbLine = new PCB_SHAPE( m_board, SHAPE_T::SEGMENT );
+            pcbLine->SetWidth( mils( width ) );
+            pcbLine->SetStart( mils( line0.ends[0] - m_cst->m_metadata.position ) );
+            pcbLine->SetEnd( mils( line0.ends[1] - m_cst->m_metadata.position ) );
+            pcbLine->SetLayer( Edge_Cuts );
+            m_board->Add( pcbLine, ADD_MODE::BULK_APPEND, true );
+            bulkAddedItems.push_back( pcbLine );
+        }
+        //PCB_SHAPE* poly = new PCB_SHAPE( m_board, SHAPE_T::POLY );
+        //poly->SetWidth( width );
+        //poly->SetPolyPoints( boardOutline );
+        //poly->SetLayer( Edge_Cuts );
+        //m_board->Add( poly, ADD_MODE::BULK_APPEND, true );
+        //bulkAddedItems.push_back( poly );
+    }
+
+    // metadata
+    m_board->SetPosition( mils( m_cst->m_metadata.position ) );
+    m_board->SetCopperLayerCount( m_cst->m_metadata.layers );
+    m_board->SetEnabledLayers( filledLayers );
+
+    // done
+    m_board->FinalizeBulkAdd( bulkAddedItems );
+    //if ( aProperties != nullptr )
+    //    m_board->SetProperties( *aProperties );
+    // kicad/pcb_parser.cpp has a nice section here where it alerts the user about undefined layers
+    return m_board;
+}
+
+/*
+FOOTPRINT* PCB_IO_CAST::FootprintFromLST( size_t lstDevIdx, size_t cstDevIdx )
+{
+    auto & lstDev = m_lst->m_devs.at( lstDevIdx );
+    FOOTPRINT* fp = new FOOTPRINT( m_board );
+    fp->SetReference( lstDev.RefDes() );
+    fp->SetLayer( lstDev.Layer() - 1 );
+
+    VECTOR2I lstPosition;
+
+
+    devIdxIt = devIdxByRefDes.find( lstDev.RefDes() );
+}*/
diff --git a/pcbnew/pcb_io/cast/pcb_io_cast.h b/pcbnew/pcb_io/cast/pcb_io_cast.h
new file mode 100644
index 0000000000..d5b03e3822
--- /dev/null
+++ b/pcbnew/pcb_io/cast/pcb_io_cast.h
@@ -0,0 +1,44 @@
+#ifndef PCB_IO_CAST_H_
+#define PCB_IO_CAST_H_
+
+#include <pcb_io/pcb_io.h>
+#include <io/cast/cast_io.h>
+#include <math/vector2d.h>
+
+class CAST_LST_FILE;
+class CAST_CST_FILE;
+
+class PCB_IO_CAST : public PCB_IO, public CAST_IO
+{
+public:
+    PCB_IO_CAST();
+    virtual ~PCB_IO_CAST();
+
+    const IO_BASE::IO_FILE_DESC GetLibraryDesc() const override
+    {
+	    return GetFileDescr();
+    }
+
+    const IO_BASE::IO_FILE_DESC GetBoardFileDesc() const override
+    {
+	    return GetFileDescr();
+    }
+
+    BOARD* LoadBoard( const wxString& aFileName, BOARD* aAppendToMe,
+                      const std::map<std::string, UTF8>* aProperties = nullptr,
+                      PROJECT* aProject = nullptr ) override;
+
+    long long GetLibraryTimestamp( const wxString& aLibraryPath ) const override
+    {
+	    // there was a date in the lst header (title)
+	    return 0;
+    }
+
+private:
+
+    VECTOR2I m_lstPositionMin;
+    VECTOR2I m_lstPositionMax;
+    VECTOR2D m_lstPosition;
+};
+
+#endif // PCB_IO_CAST_H_
diff --git a/pcbnew/pcb_io/pcb_io_mgr.cpp b/pcbnew/pcb_io/pcb_io_mgr.cpp
index 8c4f4baa6c..9e3e42dcc6 100644
--- a/pcbnew/pcb_io/pcb_io_mgr.cpp
+++ b/pcbnew/pcb_io/pcb_io_mgr.cpp
@@ -41,6 +41,7 @@
 #include <pcb_io/altium/pcb_io_altium_designer.h>
 #include <pcb_io/altium/pcb_io_solidworks.h>
 #include <pcb_io/cadstar/pcb_io_cadstar_archive.h>
+#include <pcb_io/cast/pcb_io_cast.h>
 #include <pcb_io/fabmaster/pcb_io_fabmaster.h>
 #include <pcb_io/easyeda/pcb_io_easyeda_plugin.h>
 #include <pcb_io/easyedapro/pcb_io_easyedapro.h>
@@ -300,6 +301,11 @@ static PCB_IO_MGR::REGISTER_PLUGIN registerCadstarArchivePlugin(
         wxT( "CADSTAR PCB Archive" ),
         []() -> PCB_IO* { return new PCB_IO_CADSTAR_ARCHIVE; } );
 
+static PCB_IO_MGR::REGISTER_PLUGIN registerCastPlugin(
+        PCB_IO_MGR::CAST,
+        wxT( "Card Analysis Support Tool" ),
+        []() -> PCB_IO* { return new PCB_IO_CAST; } );
+
 static PCB_IO_MGR::REGISTER_PLUGIN registerEaglePlugin(
         PCB_IO_MGR::EAGLE,
         wxT( "Eagle" ),
diff --git a/pcbnew/pcb_io/pcb_io_mgr.h b/pcbnew/pcb_io/pcb_io_mgr.h
index 260fe901e8..14cb766334 100644
--- a/pcbnew/pcb_io/pcb_io_mgr.h
+++ b/pcbnew/pcb_io/pcb_io_mgr.h
@@ -61,6 +61,7 @@ public:
         ALTIUM_CIRCUIT_STUDIO,
         ALTIUM_DESIGNER,
         CADSTAR_PCB_ARCHIVE,
+	CAST,
         EAGLE,
         EASYEDA,
         EASYEDAPRO,
