/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2018 Jean_Pierre Charras * Copyright (C) 1992-2022 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ /** * @file gendrill_gerber_writer.cpp * @brief Functions to create the Gerber job file in JSON format. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include GERBER_JOBFILE_WRITER::GERBER_JOBFILE_WRITER( BOARD* aPcb, REPORTER* aReporter ) { m_pcb = aPcb; m_reporter = aReporter; m_conversionUnits = 1.0 / pcbIUScale.IU_PER_MM; // Gerber units = mm } std::string GERBER_JOBFILE_WRITER::formatStringFromUTF32( const wxString& aText ) { std::string fmt_text; // the text after UTF32 to UTF8 conversion for( unsigned long letter : aText ) { if( letter >= ' ' && letter <= 0x7F ) fmt_text += char( letter ); else { char buff[16]; sprintf( buff, "\\u%4.4lX", letter ); fmt_text += buff; } } return fmt_text; } enum ONSIDE GERBER_JOBFILE_WRITER::hasSilkLayers() { int flag = SIDE_NONE; for( unsigned ii = 0; ii < m_params.m_LayerId.size(); ii++ ) { if( m_params.m_LayerId[ii] == B_SilkS ) flag |= SIDE_BOTTOM; if( m_params.m_LayerId[ii] == F_SilkS ) flag |= SIDE_TOP; } return (enum ONSIDE) flag; } enum ONSIDE GERBER_JOBFILE_WRITER::hasSolderMasks() { int flag = SIDE_NONE; for( unsigned ii = 0; ii < m_params.m_LayerId.size(); ii++ ) { if( m_params.m_LayerId[ii] == B_Mask ) flag |= SIDE_BOTTOM; if( m_params.m_LayerId[ii] == F_Mask ) flag |= SIDE_TOP; } return (enum ONSIDE) flag; } const char* GERBER_JOBFILE_WRITER::sideKeyValue( enum ONSIDE aValue ) { // return the key associated to sides used for some layers // "No, TopOnly, BotOnly or Both" const char* value = nullptr; switch( aValue ) { case SIDE_NONE: value = "No"; break; case SIDE_TOP: value = "TopOnly"; break; case SIDE_BOTTOM: value = "BotOnly"; break; case SIDE_BOTH: value = "Both"; break; } return value; } bool GERBER_JOBFILE_WRITER::CreateJobFile( const wxString& aFullFilename ) { bool success; wxString msg; success = WriteJSONJobFile( aFullFilename ); if( !success ) { if( m_reporter ) { msg.Printf( _( "Failed to create file '%s'." ), aFullFilename ); m_reporter->Report( msg, RPT_SEVERITY_ERROR ); } } else if( m_reporter ) { msg.Printf( _( "Created Gerber job file '%s'." ), aFullFilename ); m_reporter->Report( msg, RPT_SEVERITY_ACTION ); } return success; } void GERBER_JOBFILE_WRITER::addJSONHeader() { m_json["Header"] = { { "GenerationSoftware", { { "Vendor", "KiCad" }, { "Application", "Pcbnew" }, { "Version", GetBuildVersion() } } }, { // The attribute value must conform to the full version of the ISO 8601 // date and time format, including time and time zone. "CreationDate", GbrMakeCreationDateAttributeString( GBR_NC_STRING_FORMAT_GBRJOB ) } }; } bool GERBER_JOBFILE_WRITER::WriteJSONJobFile( const wxString& aFullFilename ) { // Note: in Gerber job file, dimensions are in mm, and are floating numbers std::ofstream file( aFullFilename.ToUTF8() ); LOCALE_IO dummy; m_json = nlohmann::ordered_json( {} ); // output the job file header addJSONHeader(); // Add the General Specs addJSONGeneralSpecs(); // Job file support a few design rules: addJSONDesignRules(); // output the gerber file list: addJSONFilesAttributes(); // output the board stackup: addJSONMaterialStackup(); file << std::setw( 2 ) << m_json << std::endl; return true; } double GERBER_JOBFILE_WRITER::mapValue( double aUiValue ) { // A helper function to convert aUiValue in Json units (mm) and to have // 4 digits in Json in mantissa when using %g to print it // i.e. displays values truncated in 0.1 microns. // This is enough for a Json file char buffer[128]; sprintf( buffer, "%.4f", aUiValue * m_conversionUnits ); long double output; sscanf( buffer, "%Lg", &output ); return output; } void GERBER_JOBFILE_WRITER::addJSONGeneralSpecs() { m_json["GeneralSpecs"] = nlohmann::ordered_json( {} ); m_json["GeneralSpecs"]["ProjectId"] = nlohmann::ordered_json( {} ); // Creates the ProjectId. Format is (from Gerber file format doc): // ProjectId,,,*% // is the name of the project, restricted to basic ASCII symbols only, // and comma not accepted // All illegal chars will be replaced by underscore // Rem: accepts only ASCII 7 code (only basic ASCII codes are allowed in gerber files). // // is a string which is an unique id of a project. // However Kicad does not handle such a project GUID, so it is built from the board name wxFileName fn = m_pcb->GetFileName(); wxString msg = fn.GetFullName(); // Build a , from the board name wxString guid = GbrMakeProjectGUIDfromString( msg ); // build the string: this is the board short filename (without ext) // and all non ASCII chars are replaced by '_', to be compatible with .gbr files. msg = fn.GetName(); // build the string. All non ASCII chars and comma are replaced by '_' wxString rev = ExpandTextVars( m_pcb->GetTitleBlock().GetRevision(), m_pcb->GetProject() ); if( rev.IsEmpty() ) rev = wxT( "rev?" ); m_json["GeneralSpecs"]["ProjectId"]["Name"] = msg.ToAscii(); m_json["GeneralSpecs"]["ProjectId"]["GUID"] = guid; m_json["GeneralSpecs"]["ProjectId"]["Revision"] = rev.ToAscii(); // output the board size in mm: BOX2I brect = m_pcb->GetBoardEdgesBoundingBox(); m_json["GeneralSpecs"]["Size"]["X"] = mapValue( brect.GetWidth() ); m_json["GeneralSpecs"]["Size"]["Y"] = mapValue( brect.GetHeight() ); // Add some data to the JSON header, GeneralSpecs: // number of copper layers m_json["GeneralSpecs"]["LayerNumber"] = m_pcb->GetCopperLayerCount(); // Board thickness m_json["GeneralSpecs"]["BoardThickness"] = mapValue( m_pcb->GetDesignSettings().GetBoardThickness() ); // Copper finish const BOARD_STACKUP brd_stackup = m_pcb->GetDesignSettings().GetStackupDescriptor(); if( !brd_stackup.m_FinishType.IsEmpty() ) m_json["GeneralSpecs"]["Finish"] = brd_stackup.m_FinishType; if( brd_stackup.m_HasDielectricConstrains ) m_json["GeneralSpecs"]["ImpedanceControlled"] = true; if( brd_stackup.m_CastellatedPads ) m_json["GeneralSpecs"]["Castellated"] = true; if( brd_stackup.m_EdgePlating ) m_json["GeneralSpecs"]["EdgePlating"] = true; if( brd_stackup.m_EdgeConnectorConstraints ) { m_json["GeneralSpecs"]["EdgeConnector"] = true; m_json["GeneralSpecs"]["EdgeConnectorBevelled"] = ( brd_stackup.m_EdgeConnectorConstraints == BS_EDGE_CONNECTOR_BEVELLED ); } #if 0 // Not yet in use /* The board type according to IPC-2221. There are six primary board types: - Type 1 - Single-sided - Type 2 - Double-sided - Type 3 - Multilayer, TH components only - Type 4 - Multilayer, with TH, blind and/or buried vias. - Type 5 - Multilayer metal-core board, TH components only - Type 6 - Multilayer metal-core */ m_json["GeneralSpecs"]["IPC-2221-Type"] = 4; /* Via protection: key words: Ia Tented - Single-sided Ib Tented - Double-sided IIa Tented and Covered - Single-sided IIb Tented and Covered - Double-sided IIIa Plugged - Single-sided IIIb Plugged - Double-sided IVa Plugged and Covered - Single-sided IVb Plugged and Covered - Double-sided V Filled (fully plugged) VI Filled and Covered VIII Filled and Capped None...No protection */ m_json["GeneralSpecs"]["ViaProtection"] = "Ib"; #endif } void GERBER_JOBFILE_WRITER::addJSONFilesAttributes() { // Add the Files Attributes section in JSON format to m_JSONbuffer m_json["FilesAttributes"] = nlohmann::ordered_json::array(); for( unsigned ii = 0; ii < m_params.m_GerberFileList.GetCount(); ii++ ) { wxString& name = m_params.m_GerberFileList[ii]; PCB_LAYER_ID layer = m_params.m_LayerId[ii]; wxString gbr_layer_id; bool skip_file = false; // true to skip files which should not be in job file const char* polarity = "Positive"; nlohmann::ordered_json file_json; if( layer <= B_Cu ) { gbr_layer_id = wxT( "Copper,L" ); if( layer == B_Cu ) gbr_layer_id << m_pcb->GetCopperLayerCount(); else gbr_layer_id << layer + 1; gbr_layer_id << wxT( "," ); if( layer == B_Cu ) gbr_layer_id << wxT( "Bot" ); else if( layer == F_Cu ) gbr_layer_id << wxT( "Top" ); else gbr_layer_id << wxT( "Inr" ); } else { switch( layer ) { case B_Adhes: gbr_layer_id = wxT( "Glue,Bot" ); break; case F_Adhes: gbr_layer_id = wxT( "Glue,Top" ); break; case B_Paste: gbr_layer_id = wxT( "SolderPaste,Bot" ); break; case F_Paste: gbr_layer_id = wxT( "SolderPaste,Top" ); break; case B_SilkS: gbr_layer_id = wxT( "Legend,Bot" ); break; case F_SilkS: gbr_layer_id = wxT( "Legend,Top" ); break; case B_Mask: gbr_layer_id = wxT( "SolderMask,Bot" ); polarity = "Negative"; break; case F_Mask: gbr_layer_id = wxT( "SolderMask,Top" ); polarity = "Negative"; break; case Edge_Cuts: gbr_layer_id = wxT( "Profile" ); break; case B_Fab: gbr_layer_id = wxT( "AssemblyDrawing,Bot" ); break; case F_Fab: gbr_layer_id = wxT( "AssemblyDrawing,Top" ); break; case Margin: case B_CrtYd: case F_CrtYd: skip_file = true; break; case Dwgs_User: case Cmts_User: case Eco1_User: case Eco2_User: case User_1: case User_2: case User_3: case User_4: case User_5: case User_6: case User_7: case User_8: case User_9: gbr_layer_id = wxT( "Other,User" ); break; default: skip_file = true; if( m_reporter ) m_reporter->Report( wxT( "Unexpected layer id in job file" ), RPT_SEVERITY_ERROR ); break; } } if( !skip_file ) { // name can contain non ASCII7 chars. // Ensure the name is JSON compatible. std::string strname = formatStringFromUTF32( name ); file_json["Path"] = strname.c_str(); file_json["FileFunction"] = gbr_layer_id; file_json["FilePolarity"] = polarity; m_json["FilesAttributes"] += file_json; } } } void GERBER_JOBFILE_WRITER::addJSONDesignRules() { // Add the Design Rules section in JSON format to m_JSONbuffer // Job file support a few design rules: std::shared_ptr& netSettings = m_pcb->GetDesignSettings().m_NetSettings; int minclearanceOuter = netSettings->m_DefaultNetClass->GetClearance(); bool hasInnerLayers = m_pcb->GetCopperLayerCount() > 2; // Search a smaller clearance in other net classes, if any. for( const auto& [ name, netclass ] : netSettings->m_NetClasses ) minclearanceOuter = std::min( minclearanceOuter, netclass->GetClearance() ); // job file knows different clearance types. // Kicad knows only one clearance for pads and tracks int minclearance_track2track = minclearanceOuter; // However, pads can have a specific clearance defined for a pad or a footprint, // and min clearance can be dependent on layers. // Search for a minimal pad clearance: int minPadClearanceOuter = netSettings->m_DefaultNetClass->GetClearance(); int minPadClearanceInner = netSettings->m_DefaultNetClass->GetClearance(); for( FOOTPRINT* footprint : m_pcb->Footprints() ) { for( PAD* pad : footprint->Pads() ) { for( PCB_LAYER_ID layer : pad->GetLayerSet().Seq() ) { int padClearance = pad->GetOwnClearance( layer ); if( layer == B_Cu || layer == F_Cu ) minPadClearanceOuter = std::min( minPadClearanceOuter, padClearance ); else minPadClearanceInner = std::min( minPadClearanceInner, padClearance ); } } } m_json["DesignRules"] = { { { "Layers", "Outer" }, { "PadToPad", mapValue( minPadClearanceOuter ) }, { "PadToTrack", mapValue( minPadClearanceOuter ) }, { "TrackToTrack", mapValue( minclearance_track2track ) } } }; // Until this is changed in Kicad, use the same value for internal tracks int minclearanceInner = minclearanceOuter; // Output the minimal track width int mintrackWidthOuter = INT_MAX; int mintrackWidthInner = INT_MAX; for( PCB_TRACK* track : m_pcb->Tracks() ) { if( track->Type() == PCB_VIA_T ) continue; if( track->GetLayer() == B_Cu || track->GetLayer() == F_Cu ) mintrackWidthOuter = std::min( mintrackWidthOuter, track->GetWidth() ); else mintrackWidthInner = std::min( mintrackWidthInner, track->GetWidth() ); } if( mintrackWidthOuter != INT_MAX ) m_json["DesignRules"][0]["MinLineWidth"] = mapValue( mintrackWidthOuter ); // Output the minimal zone to xx clearance // Note: zones can have a zone clearance set to 0 // if happens, the actual zone clearance is the clearance of its class minclearanceOuter = INT_MAX; minclearanceInner = INT_MAX; for( ZONE* zone : m_pcb->Zones() ) { if( zone->GetIsRuleArea() || !zone->IsOnCopperLayer() ) continue; for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() ) { int zclerance = zone->GetOwnClearance( layer ); if( layer == B_Cu || layer == F_Cu ) minclearanceOuter = std::min( minclearanceOuter, zclerance ); else minclearanceInner = std::min( minclearanceInner, zclerance ); } } if( minclearanceOuter != INT_MAX ) m_json["DesignRules"][0]["TrackToRegion"] = mapValue( minclearanceOuter ); if( minclearanceOuter != INT_MAX ) m_json["DesignRules"][0]["RegionToRegion"] = mapValue( minclearanceOuter ); if( hasInnerLayers ) { m_json["DesignRules"] += nlohmann::ordered_json( { { "Layers", "Inner" }, { "PadToPad", mapValue( minPadClearanceInner ) }, { "PadToTrack", mapValue( minPadClearanceInner ) }, { "TrackToTrack", mapValue( minclearance_track2track ) } } ); if( mintrackWidthInner != INT_MAX ) m_json["DesignRules"][1]["MinLineWidth"] = mapValue( mintrackWidthInner ); if( minclearanceInner != INT_MAX ) m_json["DesignRules"][1]["TrackToRegion"] = mapValue( minclearanceInner ); if( minclearanceInner != INT_MAX ) m_json["DesignRules"][1]["RegionToRegion"] = mapValue( minclearanceInner ); } } void GERBER_JOBFILE_WRITER::addJSONMaterialStackup() { // Add the Material Stackup section in JSON format to m_JSONbuffer m_json["MaterialStackup"] = nlohmann::ordered_json::array(); // Build the candidates list: LSET maskLayer; BOARD_STACKUP brd_stackup = m_pcb->GetDesignSettings().GetStackupDescriptor(); // Ensure brd_stackup is up to date (i.e. no change made by SynchronizeWithBoard() ) bool uptodate = not brd_stackup.SynchronizeWithBoard( &m_pcb->GetDesignSettings() ); if( m_reporter && !uptodate && m_pcb->GetDesignSettings().m_HasStackup ) m_reporter->Report( _( "Board stackup settings not up to date." ), RPT_SEVERITY_ERROR ); PCB_LAYER_ID last_copper_layer = F_Cu; // Generate the list (top to bottom): for( int ii = 0; ii < brd_stackup.GetCount(); ++ii ) { BOARD_STACKUP_ITEM* item = brd_stackup.GetStackupLayer( ii ); int sub_layer_count = item->GetType() == BS_ITEM_TYPE_DIELECTRIC ? item->GetSublayersCount() : 1; for( int sub_idx = 0; sub_idx < sub_layer_count; sub_idx++ ) { // layer thickness is always in mm double thickness = mapValue( item->GetThickness( sub_idx ) ); wxString layer_type; std::string layer_name; // for comment nlohmann::ordered_json layer_json; switch( item->GetType() ) { case BS_ITEM_TYPE_COPPER: layer_type = wxT( "Copper" ); layer_name = formatStringFromUTF32( m_pcb->GetLayerName( item->GetBrdLayerId() ) ); last_copper_layer = item->GetBrdLayerId(); break; case BS_ITEM_TYPE_SILKSCREEN: layer_type = wxT( "Legend" ); layer_name = formatStringFromUTF32( item->GetTypeName() ); break; case BS_ITEM_TYPE_SOLDERMASK: layer_type = wxT( "SolderMask" ); layer_name = formatStringFromUTF32( item->GetTypeName() ); break; case BS_ITEM_TYPE_SOLDERPASTE: layer_type = wxT( "SolderPaste" ); layer_name = formatStringFromUTF32( item->GetTypeName() ); break; case BS_ITEM_TYPE_DIELECTRIC: layer_type = wxT( "Dielectric" ); // The option core or prepreg is not added here, as it creates constraints // in build process, not necessary wanted. if( sub_layer_count > 1 ) { layer_name = formatStringFromUTF32( wxString::Format( wxT( "dielectric layer %d - %d/%d" ), item->GetDielectricLayerId(), sub_idx + 1, sub_layer_count ) ); } else layer_name = formatStringFromUTF32( wxString::Format( wxT( "dielectric layer %d" ), item->GetDielectricLayerId() ) ); break; default: break; } layer_json["Type"] = layer_type; if( item->IsColorEditable() && uptodate ) { if( IsPrmSpecified( item->GetColor( sub_idx ) ) ) { wxString colorName = item->GetColor( sub_idx ); if( colorName.StartsWith( wxT( "#" ) ) ) // This is a user defined color. { // In job file a color can be given by its RGB values (0...255) wxColor color( COLOR4D( colorName ).ToColour() ); colorName.Printf( wxT( "R%dG%dB%d" ), color.Red(), color.Green(), color.Blue() ); } layer_json["Color"] = colorName; } } if( item->IsThicknessEditable() && uptodate ) layer_json["Thickness"] = thickness; if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC ) { if( item->HasMaterialValue() ) { layer_json["Material"] = item->GetMaterial( sub_idx ); // These constrains are only written if the board has impedance controlled tracks. // If the board is not impedance controlled, they are useless. // Do not add constrains that create more expensive boards. if( brd_stackup.m_HasDielectricConstrains ) { // Generate Epsilon R if > 1.0 (value <= 1.0 means not specified: it is not // a possible value if( item->GetEpsilonR() > 1.0 ) layer_json["DielectricConstant"] = item->FormatEpsilonR( sub_idx ); // Generate LossTangent > 0.0 (value <= 0.0 means not specified: it is not // a possible value if( item->GetLossTangent() > 0.0 ) layer_json["LossTangent"] = item->FormatLossTangent( sub_idx ); } } PCB_LAYER_ID next_copper_layer = ( PCB_LAYER_ID )( last_copper_layer + 1 ); // If the next_copper_layer is the last copper layer, the next layer id is B_Cu if( next_copper_layer >= m_pcb->GetCopperLayerCount() - 1 ) next_copper_layer = B_Cu; wxString subLayerName; if( sub_layer_count > 1 ) subLayerName.Printf( wxT( " (%d/%d)" ), sub_idx + 1, sub_layer_count ); wxString name = wxString::Format( wxT( "%s/%s%s" ), formatStringFromUTF32( m_pcb->GetLayerName( last_copper_layer ) ), formatStringFromUTF32( m_pcb->GetLayerName( next_copper_layer ) ), subLayerName ); layer_json["Name"] = name; // Add a comment ("Notes"): wxString note; note << wxString::Format( wxT( "Type: %s" ), layer_name.c_str() ); note << wxString::Format( wxT( " (from %s to %s)" ), formatStringFromUTF32( m_pcb->GetLayerName( last_copper_layer ) ), formatStringFromUTF32( m_pcb->GetLayerName( next_copper_layer ) ) ); layer_json["Notes"] = note; } else if( item->GetType() == BS_ITEM_TYPE_SOLDERMASK || item->GetType() == BS_ITEM_TYPE_SILKSCREEN ) { if( item->HasMaterialValue() ) { layer_json["Material"] = item->GetMaterial(); // These constrains are only written if the board has impedance controlled tracks. // If the board is not impedance controlled, they are useless. // Do not add constrains that create more expensive boards. if( brd_stackup.m_HasDielectricConstrains ) { // Generate Epsilon R if > 1.0 (value <= 1.0 means not specified: it is not // a possible value if( item->GetEpsilonR() > 1.0 ) layer_json["DielectricConstant"] = item->FormatEpsilonR(); // Generate LossTangent > 0.0 (value <= 0.0 means not specified: it is not // a possible value if( item->GetLossTangent() > 0.0 ) layer_json["LossTangent"] = item->FormatLossTangent(); } } layer_json["Name"] = layer_name.c_str(); } else { layer_json["Name"] = layer_name.c_str(); } m_json["MaterialStackup"].insert( m_json["MaterialStackup"].end(), layer_json ); } } }