2017-08-19 16:28:11 +00:00
|
|
|
/*
|
|
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
|
|
*
|
2018-03-16 18:32:49 +00:00
|
|
|
* Copyright (C) 2018 Jean_Pierre Charras <jp.charras at wanadoo.fr>
|
2019-05-17 00:13:21 +00:00
|
|
|
* Copyright (C) 1992-2020 KiCad Developers, see AUTHORS.txt for contributors.
|
2017-08-19 16:28:11 +00:00
|
|
|
*
|
|
|
|
* 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
|
2018-03-16 18:32:49 +00:00
|
|
|
* @brief Functions to create the Gerber job file in JSON format.
|
2017-08-19 16:28:11 +00:00
|
|
|
*/
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
#include <fstream>
|
|
|
|
#include <iomanip>
|
2017-08-19 16:28:11 +00:00
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
#include <build_version.h>
|
2020-10-24 01:38:50 +00:00
|
|
|
#include <locale_io.h>
|
2019-12-25 16:30:46 +00:00
|
|
|
#include <pcb_edit_frame.h>
|
|
|
|
#include <plotter.h>
|
2017-08-19 16:28:11 +00:00
|
|
|
|
2020-11-12 20:19:22 +00:00
|
|
|
#include <board.h>
|
|
|
|
#include <footprint.h>
|
|
|
|
#include <track.h>
|
2020-11-11 23:05:59 +00:00
|
|
|
#include <zone.h>
|
2017-08-19 16:28:11 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
#include <board_stackup_manager/stackup_predefined_prms.h>
|
|
|
|
#include <gbr_metadata.h>
|
2017-08-19 16:28:11 +00:00
|
|
|
#include <gerber_jobfile_writer.h>
|
2019-12-25 16:30:46 +00:00
|
|
|
#include <pcbplot.h>
|
2017-08-19 16:28:11 +00:00
|
|
|
#include <reporter.h>
|
2019-12-25 16:30:46 +00:00
|
|
|
#include <wildcards_and_files_ext.h>
|
2017-08-19 16:28:11 +00:00
|
|
|
|
|
|
|
GERBER_JOBFILE_WRITER::GERBER_JOBFILE_WRITER( BOARD* aPcb, REPORTER* aReporter )
|
|
|
|
{
|
|
|
|
m_pcb = aPcb;
|
|
|
|
m_reporter = aReporter;
|
2019-12-25 16:30:46 +00:00
|
|
|
m_conversionUnits = 1.0 / IU_PER_MM; // Gerber units = mm
|
2017-08-19 16:28:11 +00:00
|
|
|
}
|
|
|
|
|
2019-07-14 08:19:53 +00:00
|
|
|
std::string GERBER_JOBFILE_WRITER::formatStringFromUTF32( const wxString& aText )
|
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
std::string fmt_text; // the text after UTF32 to UTF8 conversion
|
2019-07-14 08:19:53 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
for( unsigned long letter : aText )
|
2019-07-14 08:19:53 +00:00
|
|
|
{
|
|
|
|
if( letter >= ' ' && letter <= 0x7F )
|
|
|
|
fmt_text += char( letter );
|
|
|
|
else
|
|
|
|
{
|
2019-07-18 01:24:25 +00:00
|
|
|
char buff[16];
|
2019-07-14 08:19:53 +00:00
|
|
|
sprintf( buff, "\\u%4.4lX", letter );
|
|
|
|
fmt_text += buff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fmt_text;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-08-19 16:28:11 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
return (enum ONSIDE) flag;
|
2017-08-19 16:28:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
return (enum ONSIDE) flag;
|
2017-08-19 16:28:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 )
|
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
case SIDE_NONE:
|
|
|
|
value = "No";
|
|
|
|
break;
|
2017-08-19 16:28:11 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
case SIDE_TOP:
|
|
|
|
value = "TopOnly";
|
|
|
|
break;
|
2017-08-19 16:28:11 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
case SIDE_BOTTOM:
|
|
|
|
value = "BotOnly";
|
|
|
|
break;
|
2017-08-19 16:28:11 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
case SIDE_BOTH:
|
|
|
|
value = "Both";
|
|
|
|
break;
|
2017-08-19 16:28:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GERBER_JOBFILE_WRITER::CreateJobFile( const wxString& aFullFilename )
|
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
bool success;
|
2017-08-19 16:28:11 +00:00
|
|
|
wxString msg;
|
|
|
|
|
2018-04-08 17:49:02 +00:00
|
|
|
success = WriteJSONJobFile( aFullFilename );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
if( !success )
|
2017-08-19 16:28:11 +00:00
|
|
|
{
|
|
|
|
if( m_reporter )
|
|
|
|
{
|
2017-12-15 11:37:46 +00:00
|
|
|
msg.Printf( _( "Unable to create job file \"%s\"" ), aFullFilename );
|
2020-03-04 09:48:18 +00:00
|
|
|
m_reporter->Report( msg, RPT_SEVERITY_ERROR );
|
2017-08-19 16:28:11 +00:00
|
|
|
}
|
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
else if( m_reporter )
|
|
|
|
{
|
|
|
|
msg.Printf( _( "Create Gerber job file \"%s\"" ), aFullFilename );
|
2020-03-04 09:48:18 +00:00
|
|
|
m_reporter->Report( msg, RPT_SEVERITY_ACTION );
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GERBER_JOBFILE_WRITER::addJSONHeader()
|
|
|
|
{
|
|
|
|
wxString text;
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
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 )
|
|
|
|
}
|
|
|
|
};
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
|
2018-04-08 17:49:02 +00:00
|
|
|
bool GERBER_JOBFILE_WRITER::WriteJSONJobFile( const wxString& aFullFilename )
|
2018-03-16 18:32:49 +00:00
|
|
|
{
|
|
|
|
// Note: in Gerber job file, dimensions are in mm, and are floating numbers
|
2019-12-25 16:30:46 +00:00
|
|
|
std::ofstream file( aFullFilename.ToUTF8() );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
LOCALE_IO dummy;
|
|
|
|
|
2020-08-01 17:13:36 +00:00
|
|
|
m_json = nlohmann::ordered_json( {} );
|
2019-12-25 16:30:46 +00:00
|
|
|
|
2018-03-16 18:32:49 +00:00
|
|
|
// 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();
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
file << std::setw( 2 ) << m_json << std::endl;
|
2017-08-19 16:28:11 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
|
2020-03-29 13:35:47 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-03-16 18:32:49 +00:00
|
|
|
void GERBER_JOBFILE_WRITER::addJSONGeneralSpecs()
|
|
|
|
{
|
2020-08-01 17:13:36 +00:00
|
|
|
m_json["GeneralSpecs"] = nlohmann::ordered_json( {} );
|
|
|
|
m_json["GeneralSpecs"]["ProjectId"] = nlohmann::ordered_json( {} );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// Creates the ProjectId. Format is (from Gerber file format doc):
|
|
|
|
// ProjectId,<project id>,<project GUID>,<revision id>*%
|
|
|
|
// <project id> 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: <project id> accepts only ASCII 7 code (only basic ASCII codes are allowed in gerber files).
|
2018-11-06 07:16:07 +00:00
|
|
|
//
|
|
|
|
// <project GUID> 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
|
2018-03-16 18:32:49 +00:00
|
|
|
wxFileName fn = m_pcb->GetFileName();
|
2019-12-25 16:30:46 +00:00
|
|
|
wxString msg = fn.GetFullName();
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2018-11-06 07:16:07 +00:00
|
|
|
// Build a <project GUID>, from the board name
|
|
|
|
wxString guid = GbrMakeProjectGUIDfromString( msg );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// build the <project id> string: this is the board short filename (without ext)
|
2019-07-14 08:19:53 +00:00
|
|
|
// and all non ASCII chars are replaced by '_', to be compatible with .gbr files.
|
2018-03-16 18:32:49 +00:00
|
|
|
msg = fn.GetName();
|
|
|
|
|
|
|
|
// build the <rec> string. All non ASCII chars and comma are replaced by '_'
|
|
|
|
wxString rev = m_pcb->GetTitleBlock().GetRevision();
|
|
|
|
|
|
|
|
if( rev.IsEmpty() )
|
|
|
|
rev = wxT( "rev?" );
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["ProjectId"]["Name"] = msg.ToAscii();
|
|
|
|
m_json["GeneralSpecs"]["ProjectId"]["GUID"] = guid;
|
|
|
|
m_json["GeneralSpecs"]["ProjectId"]["Revision"] = rev.ToAscii();
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// output the bord size in mm:
|
|
|
|
EDA_RECT brect = m_pcb->GetBoardEdgesBoundingBox();
|
|
|
|
|
2020-03-29 13:35:47 +00:00
|
|
|
m_json["GeneralSpecs"]["Size"]["X"] = mapValue( brect.GetWidth() );
|
|
|
|
m_json["GeneralSpecs"]["Size"]["Y"] = mapValue( brect.GetHeight() );
|
2019-12-25 16:30:46 +00:00
|
|
|
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// Add some data to the JSON header, GeneralSpecs:
|
|
|
|
// number of copper layers
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["LayerNumber"] = m_pcb->GetCopperLayerCount();
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// Board thickness
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["BoardThickness"] =
|
2020-03-29 13:35:47 +00:00
|
|
|
mapValue( m_pcb->GetDesignSettings().GetBoardThickness() );
|
2019-06-23 11:20:22 +00:00
|
|
|
|
|
|
|
// Copper finish
|
|
|
|
BOARD_STACKUP brd_stackup = m_pcb->GetDesignSettings().GetStackupDescriptor();
|
|
|
|
|
|
|
|
if( !brd_stackup.m_FinishType.IsEmpty() )
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["Finish"] = brd_stackup.m_FinishType;
|
2019-06-23 11:20:22 +00:00
|
|
|
|
|
|
|
if( brd_stackup.m_CastellatedPads )
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["Castellated"] = true;
|
2019-06-23 11:20:22 +00:00
|
|
|
|
|
|
|
if( brd_stackup.m_EdgePlating )
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["EdgePlating"] = true;
|
2019-06-23 11:20:22 +00:00
|
|
|
|
|
|
|
if( brd_stackup.m_EdgeConnectorConstraints )
|
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["EdgeConnector"] = true;
|
2019-06-23 11:20:22 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["EdgeConnectorBevelled"] =
|
|
|
|
( brd_stackup.m_EdgeConnectorConstraints == BS_EDGE_CONNECTOR_BEVELLED );
|
2019-06-23 11:20:22 +00:00
|
|
|
}
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
#if 0 // Not yet in use
|
2018-03-16 18:32:49 +00:00
|
|
|
/* The board type according to IPC-2221. There are six primary board types:
|
|
|
|
- Type 1 - Single-sided
|
|
|
|
- Type 2 - Double-sided
|
2018-10-07 06:19:41 +00:00
|
|
|
- Type 3 - Multilayer, TH components only
|
|
|
|
- Type 4 - Multilayer, with TH, blind and/or buried vias.
|
2018-03-16 18:32:49 +00:00
|
|
|
- Type 5 - Multilayer metal-core board, TH components only
|
|
|
|
- Type 6 - Multilayer metal-core
|
|
|
|
*/
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["IPC-2221-Type"] = 4;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
/* Via protection: key words:
|
|
|
|
Ia Tented - Single-sided
|
|
|
|
Ib Tented - Double-sided
|
2018-10-07 06:19:41 +00:00
|
|
|
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
|
2018-03-16 18:32:49 +00:00
|
|
|
V Filled (fully plugged)
|
|
|
|
VI Filled and Covered
|
|
|
|
VIII Filled and Capped
|
2018-10-07 06:19:41 +00:00
|
|
|
None...No protection
|
2018-03-16 18:32:49 +00:00
|
|
|
*/
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["GeneralSpecs"]["ViaProtection"] = "Ib";
|
2018-03-16 18:32:49 +00:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GERBER_JOBFILE_WRITER::addJSONFilesAttributes()
|
|
|
|
{
|
|
|
|
// Add the Files Attributes section in JSON format to m_JSONbuffer
|
2020-08-01 17:13:36 +00:00
|
|
|
m_json["FilesAttributes"] = nlohmann::ordered_json::array();
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
for( unsigned ii = 0; ii < m_params.m_GerberFileList.GetCount(); ii++ )
|
2018-03-16 18:32:49 +00:00
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
wxString& name = m_params.m_GerberFileList[ii];
|
2018-03-16 18:32:49 +00:00
|
|
|
PCB_LAYER_ID layer = m_params.m_LayerId[ii];
|
2019-12-25 16:30:46 +00:00
|
|
|
wxString gbr_layer_id;
|
|
|
|
bool skip_file = false; // true to skip files which should not be in job file
|
|
|
|
const char* polarity = "Positive";
|
2020-08-01 17:13:36 +00:00
|
|
|
|
|
|
|
nlohmann::ordered_json file_json;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
if( layer <= B_Cu )
|
|
|
|
{
|
|
|
|
gbr_layer_id = "Copper,L";
|
|
|
|
|
|
|
|
if( layer == B_Cu )
|
|
|
|
gbr_layer_id << m_pcb->GetCopperLayerCount();
|
|
|
|
else
|
2019-12-25 16:30:46 +00:00
|
|
|
gbr_layer_id << layer + 1;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
gbr_layer_id << ",";
|
|
|
|
|
|
|
|
if( layer == B_Cu )
|
|
|
|
gbr_layer_id << "Bot";
|
|
|
|
else if( layer == F_Cu )
|
|
|
|
gbr_layer_id << "Top";
|
|
|
|
else
|
|
|
|
gbr_layer_id << "Inr";
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
{
|
|
|
|
switch( layer )
|
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
case B_Adhes:
|
|
|
|
gbr_layer_id = "Glue,Bot";
|
|
|
|
break;
|
|
|
|
case F_Adhes:
|
|
|
|
gbr_layer_id = "Glue,Top";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case B_Paste:
|
|
|
|
gbr_layer_id = "SolderPaste,Bot";
|
|
|
|
break;
|
|
|
|
case F_Paste:
|
|
|
|
gbr_layer_id = "SolderPaste,Top";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case B_SilkS:
|
|
|
|
gbr_layer_id = "Legend,Bot";
|
|
|
|
break;
|
|
|
|
case F_SilkS:
|
|
|
|
gbr_layer_id = "Legend,Top";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case B_Mask:
|
|
|
|
gbr_layer_id = "SolderMask,Bot";
|
|
|
|
polarity = "Negative";
|
|
|
|
break;
|
|
|
|
case F_Mask:
|
|
|
|
gbr_layer_id = "SolderMask,Top";
|
|
|
|
polarity = "Negative";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Edge_Cuts:
|
|
|
|
gbr_layer_id = "Profile";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case B_Fab:
|
|
|
|
gbr_layer_id = "AssemblyDrawing,Bot";
|
|
|
|
break;
|
|
|
|
case F_Fab:
|
|
|
|
gbr_layer_id = "AssemblyDrawing,Top";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Dwgs_User:
|
|
|
|
case Cmts_User:
|
|
|
|
case Eco1_User:
|
|
|
|
case Eco2_User:
|
|
|
|
case Margin:
|
|
|
|
case B_CrtYd:
|
|
|
|
case F_CrtYd:
|
|
|
|
skip_file = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
skip_file = true;
|
2020-03-04 09:48:18 +00:00
|
|
|
m_reporter->Report( "Unexpected layer id in job file", RPT_SEVERITY_ERROR );
|
2019-12-25 16:30:46 +00:00
|
|
|
break;
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !skip_file )
|
|
|
|
{
|
|
|
|
// name can contain non ASCII7 chars.
|
2019-07-14 08:19:53 +00:00
|
|
|
// Ensure the name is JSON compatible.
|
|
|
|
std::string strname = formatStringFromUTF32( name );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
file_json["Path"] = strname.c_str();
|
|
|
|
file_json["FileFunction"] = gbr_layer_id;
|
|
|
|
file_json["FilePolarity"] = polarity;
|
|
|
|
|
|
|
|
m_json["FilesAttributes"] += file_json;
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GERBER_JOBFILE_WRITER::addJSONDesignRules()
|
|
|
|
{
|
|
|
|
// Add the Design Rules section in JSON format to m_JSONbuffer
|
|
|
|
// Job file support a few design rules:
|
|
|
|
const BOARD_DESIGN_SETTINGS& dsnSettings = m_pcb->GetDesignSettings();
|
2019-12-25 16:30:46 +00:00
|
|
|
NETCLASS defaultNC = *dsnSettings.GetDefault();
|
|
|
|
int minclearanceOuter = defaultNC.GetClearance();
|
|
|
|
bool hasInnerLayers = m_pcb->GetCopperLayerCount() > 2;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// Search a smaller clearance in other net classes, if any.
|
2020-05-31 21:42:04 +00:00
|
|
|
for( const std::pair<const wxString, NETCLASSPTR>& entry : dsnSettings.GetNetClasses() )
|
2020-05-29 12:36:45 +00:00
|
|
|
minclearanceOuter = std::min( minclearanceOuter, entry.second->GetClearance() );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// 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 = defaultNC.GetClearance();
|
|
|
|
int minPadClearanceInner = defaultNC.GetClearance();
|
|
|
|
|
2020-11-13 15:15:52 +00:00
|
|
|
for( FOOTPRINT* footprint : m_pcb->Footprints() )
|
2018-03-16 18:32:49 +00:00
|
|
|
{
|
2020-11-12 23:50:33 +00:00
|
|
|
for( PAD* pad : footprint->Pads() )
|
2018-03-16 18:32:49 +00:00
|
|
|
{
|
2020-08-07 20:18:33 +00:00
|
|
|
for( PCB_LAYER_ID layer : pad->GetLayerSet().Seq() )
|
|
|
|
{
|
2020-10-12 17:40:03 +00:00
|
|
|
int padClearance = pad->GetOwnClearance( layer );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2020-08-07 20:18:33 +00:00
|
|
|
if( layer == B_Cu || layer == F_Cu )
|
|
|
|
minPadClearanceOuter = std::min( minPadClearanceOuter, padClearance );
|
|
|
|
else
|
|
|
|
minPadClearanceInner = std::min( minPadClearanceInner, padClearance );
|
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["DesignRules"] = { {
|
|
|
|
{ "Layers", "Outer" },
|
2020-03-29 13:35:47 +00:00
|
|
|
{ "PadToPad", mapValue( minPadClearanceOuter ) },
|
|
|
|
{ "PadToTrack", mapValue( minPadClearanceOuter ) },
|
|
|
|
{ "TrackToTrack", mapValue( minclearance_track2track ) }
|
2019-12-25 16:30:46 +00:00
|
|
|
} };
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// 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( 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 )
|
2020-03-29 13:35:47 +00:00
|
|
|
m_json["DesignRules"][0]["MinLineWidth"] = mapValue( mintrackWidthOuter );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
2020-11-11 23:05:59 +00:00
|
|
|
for( ZONE* zone : m_pcb->Zones() )
|
2018-03-16 18:32:49 +00:00
|
|
|
{
|
2020-09-21 23:32:07 +00:00
|
|
|
if( zone->GetIsRuleArea() || !zone->IsOnCopperLayer() )
|
2018-03-16 18:32:49 +00:00
|
|
|
continue;
|
|
|
|
|
2020-08-07 20:18:33 +00:00
|
|
|
for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
|
|
|
|
{
|
2020-10-12 17:40:03 +00:00
|
|
|
int zclerance = zone->GetOwnClearance( layer );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2020-08-07 20:18:33 +00:00
|
|
|
if( layer == B_Cu || layer == F_Cu )
|
|
|
|
minclearanceOuter = std::min( minclearanceOuter, zclerance );
|
|
|
|
else
|
|
|
|
minclearanceInner = std::min( minclearanceInner, zclerance );
|
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if( minclearanceOuter != INT_MAX )
|
2020-03-29 13:35:47 +00:00
|
|
|
m_json["DesignRules"][0]["TrackToRegion"] = mapValue( minclearanceOuter );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
if( minclearanceOuter != INT_MAX )
|
2020-03-29 13:35:47 +00:00
|
|
|
m_json["DesignRules"][0]["RegionToRegion"] = mapValue( minclearanceOuter );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
if( hasInnerLayers )
|
|
|
|
{
|
2020-08-01 17:13:36 +00:00
|
|
|
m_json["DesignRules"] += nlohmann::ordered_json( {
|
2019-12-25 16:30:46 +00:00
|
|
|
{ "Layers", "Inner" },
|
2020-03-29 13:35:47 +00:00
|
|
|
{ "PadToPad", mapValue( minPadClearanceInner ) },
|
|
|
|
{ "PadToTrack", mapValue( minPadClearanceInner ) },
|
|
|
|
{ "TrackToTrack", mapValue( minclearance_track2track ) }
|
2019-12-25 16:30:46 +00:00
|
|
|
} );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
if( mintrackWidthInner != INT_MAX )
|
2020-03-29 13:35:47 +00:00
|
|
|
m_json["DesignRules"][1]["MinLineWidth"] = mapValue( mintrackWidthInner );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
if( minclearanceInner != INT_MAX )
|
2020-03-29 13:35:47 +00:00
|
|
|
m_json["DesignRules"][1]["TrackToRegion"] = mapValue( minclearanceInner );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
if( minclearanceInner != INT_MAX )
|
2020-03-29 13:35:47 +00:00
|
|
|
m_json["DesignRules"][1]["RegionToRegion"] = mapValue( minclearanceInner );
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GERBER_JOBFILE_WRITER::addJSONMaterialStackup()
|
|
|
|
{
|
|
|
|
// Add the Material Stackup section in JSON format to m_JSONbuffer
|
2020-08-01 17:13:36 +00:00
|
|
|
m_json["MaterialStackup"] = nlohmann::ordered_json::array();
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-06-23 11:20:22 +00:00
|
|
|
// Build the candidates list:
|
2019-12-25 16:30:46 +00:00
|
|
|
LSET maskLayer;
|
2019-06-23 11:20:22 +00:00
|
|
|
BOARD_STACKUP brd_stackup = m_pcb->GetDesignSettings().GetStackupDescriptor();
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-06-23 11:20:22 +00:00
|
|
|
// Ensure brd_stackup is up to date (i.e. no change made by SynchronizeWithBoard() )
|
|
|
|
bool uptodate = not brd_stackup.SynchronizeWithBoard( &m_pcb->GetDesignSettings() );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-06-23 11:20:22 +00:00
|
|
|
if( !uptodate && m_pcb->GetDesignSettings().m_HasStackup )
|
|
|
|
m_reporter->Report( _( "Board stackup settings not up to date\n"
|
2019-12-25 16:30:46 +00:00
|
|
|
"Please fix the stackup" ),
|
2020-03-04 09:48:18 +00:00
|
|
|
RPT_SEVERITY_ERROR );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-06-23 11:20:22 +00:00
|
|
|
PCB_LAYER_ID last_copper_layer = F_Cu;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
|
|
|
// Generate the list (top to bottom):
|
2019-06-23 11:20:22 +00:00
|
|
|
for( int ii = 0; ii < brd_stackup.GetCount(); ++ii )
|
2018-03-16 18:32:49 +00:00
|
|
|
{
|
2019-06-23 11:20:22 +00:00
|
|
|
BOARD_STACKUP_ITEM* item = brd_stackup.GetStackupLayer( ii );
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
int sub_layer_count =
|
|
|
|
item->GetType() == BS_ITEM_TYPE_DIELECTRIC ? item->GetSublayersCount() : 1;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
for( int sub_idx = 0; sub_idx < sub_layer_count; sub_idx++ )
|
2019-06-23 11:20:22 +00:00
|
|
|
{
|
2019-11-14 15:26:05 +00:00
|
|
|
// layer thickness is always in mm
|
2020-03-29 13:35:47 +00:00
|
|
|
double thickness = mapValue( item->GetThickness( sub_idx ) );
|
2019-12-25 16:30:46 +00:00
|
|
|
wxString layer_type;
|
|
|
|
std::string layer_name; // for comment
|
2020-08-01 17:13:36 +00:00
|
|
|
|
|
|
|
nlohmann::ordered_json layer_json;
|
2019-09-20 11:13:19 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
switch( item->GetType() )
|
|
|
|
{
|
|
|
|
case BS_ITEM_TYPE_COPPER:
|
|
|
|
layer_type = "Copper";
|
|
|
|
layer_name = formatStringFromUTF32( m_pcb->GetLayerName( item->GetBrdLayerId() ) );
|
|
|
|
last_copper_layer = item->GetBrdLayerId();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case BS_ITEM_TYPE_SILKSCREEN:
|
|
|
|
layer_type = "Legend";
|
|
|
|
layer_name = formatStringFromUTF32( item->GetTypeName() );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case BS_ITEM_TYPE_SOLDERMASK:
|
|
|
|
layer_type = "SolderMask";
|
|
|
|
layer_name = formatStringFromUTF32( item->GetTypeName() );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case BS_ITEM_TYPE_SOLDERPASTE:
|
|
|
|
layer_type = "SolderPaste";
|
|
|
|
layer_name = formatStringFromUTF32( item->GetTypeName() );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case BS_ITEM_TYPE_DIELECTRIC:
|
|
|
|
layer_type = "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 )
|
2019-09-20 11:13:19 +00:00
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_name =
|
|
|
|
formatStringFromUTF32( wxString::Format( "dielectric layer %d - %d/%d",
|
|
|
|
item->GetDielectricLayerId(), sub_idx + 1, sub_layer_count ) );
|
2019-09-20 11:13:19 +00:00
|
|
|
}
|
2019-11-14 15:26:05 +00:00
|
|
|
else
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_name = formatStringFromUTF32( wxString::Format(
|
|
|
|
"dielectric layer %d", item->GetDielectricLayerId() ) );
|
2019-11-14 15:26:05 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
2019-09-20 11:13:19 +00:00
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Type"] = layer_type;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
if( item->IsColorEditable() && uptodate )
|
2019-06-23 11:20:22 +00:00
|
|
|
{
|
2019-11-14 15:26:05 +00:00
|
|
|
if( IsPrmSpecified( item->GetColor() ) )
|
2019-09-24 12:33:28 +00:00
|
|
|
{
|
2019-11-14 15:26:05 +00:00
|
|
|
wxString colorName = item->GetColor();
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
if( colorName.StartsWith( "#" ) ) // This is a user defined color.
|
2019-11-14 15:26:05 +00:00
|
|
|
{
|
|
|
|
// In job file a color can be given by its RGB values (0...255)
|
|
|
|
wxColor color( colorName );
|
|
|
|
colorName.Printf( "R%dG%dB%d", color.Red(), color.Green(), color.Blue() );
|
|
|
|
}
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Color"] = colorName;
|
2019-09-24 12:33:28 +00:00
|
|
|
}
|
2019-06-23 11:20:22 +00:00
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
if( item->IsThicknessEditable() && uptodate )
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Thickness"] = thickness;
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
|
|
|
|
{
|
|
|
|
if( item->HasMaterialValue() )
|
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Material"] = item->GetMaterial( sub_idx );
|
2019-11-14 15:26:05 +00:00
|
|
|
|
|
|
|
// 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 )
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["DielectricConstant"] = item->FormatEpsilonR( sub_idx );
|
2019-11-14 15:26:05 +00:00
|
|
|
|
|
|
|
// Generate LossTangent > 0.0 (value <= 0.0 means not specified: it is not
|
|
|
|
// a possible value
|
|
|
|
if( item->GetLossTangent() > 0.0 )
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["LossTangent"] = item->FormatLossTangent( sub_idx );
|
2019-11-14 15:26:05 +00:00
|
|
|
}
|
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
PCB_LAYER_ID next_copper_layer = ( PCB_LAYER_ID )( last_copper_layer + 1 );
|
2019-09-14 07:36:23 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
// If the next_copper_layer is the last copper layer, the next layer id is B_Cu
|
2019-12-25 16:30:46 +00:00
|
|
|
if( next_copper_layer >= m_pcb->GetCopperLayerCount() - 1 )
|
2019-11-14 15:26:05 +00:00
|
|
|
next_copper_layer = B_Cu;
|
2019-09-14 07:36:23 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
wxString subLayerName;
|
2019-06-23 11:20:22 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
if( sub_layer_count > 1 )
|
2019-12-25 16:30:46 +00:00
|
|
|
subLayerName.Printf( " (%d/%d)", sub_idx + 1, sub_layer_count );
|
2019-06-23 11:20:22 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
wxString name = wxString::Format( "%s/%s%s",
|
|
|
|
formatStringFromUTF32( m_pcb->GetLayerName( last_copper_layer ) ),
|
|
|
|
formatStringFromUTF32( m_pcb->GetLayerName( next_copper_layer ) ),
|
|
|
|
subLayerName );
|
|
|
|
|
|
|
|
layer_json["Name"] = name;
|
2019-06-23 11:20:22 +00:00
|
|
|
|
2019-11-14 15:26:05 +00:00
|
|
|
// Add a comment ("Notes"):
|
2019-12-25 16:30:46 +00:00
|
|
|
wxString note;
|
2019-11-14 15:26:05 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
note << wxString::Format( "Type: %s", layer_name.c_str() );
|
2019-11-14 15:26:05 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
note << wxString::Format( " (from %s to %s)",
|
|
|
|
formatStringFromUTF32( m_pcb->GetLayerName( last_copper_layer ) ),
|
|
|
|
formatStringFromUTF32( m_pcb->GetLayerName( next_copper_layer ) ) );
|
2019-09-24 12:33:28 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Notes"] = note;
|
2019-11-14 15:26:05 +00:00
|
|
|
}
|
2019-12-25 16:30:46 +00:00
|
|
|
else if( item->GetType() == BS_ITEM_TYPE_SOLDERMASK
|
|
|
|
|| item->GetType() == BS_ITEM_TYPE_SILKSCREEN )
|
2019-11-14 15:26:05 +00:00
|
|
|
{
|
|
|
|
if( item->HasMaterialValue() )
|
2019-09-24 12:33:28 +00:00
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Material"] = item->GetMaterial();
|
2019-11-14 15:26:05 +00:00
|
|
|
|
|
|
|
// 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 )
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["DielectricConstant"] = item->FormatEpsilonR();
|
2019-11-14 15:26:05 +00:00
|
|
|
|
|
|
|
// Generate LossTangent > 0.0 (value <= 0.0 means not specified: it is not
|
|
|
|
// a possible value
|
|
|
|
if( item->GetLossTangent() > 0.0 )
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["LossTangent"] = item->FormatLossTangent();
|
2019-11-14 15:26:05 +00:00
|
|
|
}
|
2019-09-24 12:33:28 +00:00
|
|
|
}
|
2019-11-14 15:26:05 +00:00
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Name"] = layer_name.c_str();
|
2019-11-14 15:26:05 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2019-12-25 16:30:46 +00:00
|
|
|
layer_json["Name"] = layer_name.c_str();
|
2019-09-24 12:33:28 +00:00
|
|
|
}
|
|
|
|
|
2019-12-25 16:30:46 +00:00
|
|
|
m_json["MaterialStackup"].insert( m_json["MaterialStackup"].end(), layer_json );
|
2019-07-14 08:19:53 +00:00
|
|
|
}
|
2018-03-16 18:32:49 +00:00
|
|
|
}
|
|
|
|
}
|