public LayoutGenerator(Eagle.schematic sch_obj, TestBench tb_obj, GMELogger inLogger) { this.Logger = inLogger; this.pkgPartMap = new Dictionary<Package, Eagle.part>(); this.partPkgMap = new Dictionary<Eagle.part, Package>(); this.preroutedPkgMap = new Dictionary<ComponentAssembly, Package>(); boardLayout = new LayoutJson.Layout(); var bw = tb_obj.Parameters.Where(p => p.Name.Equals("boardWidth")).FirstOrDefault(); var bh = tb_obj.Parameters.Where(p => p.Name.Equals("boardHeight")).FirstOrDefault(); try { boardLayout.boardWidth = (bw != null) ? Convert.ToDouble(bw.Value) : 40.0; boardLayout.boardHeight = (bh != null) ? Convert.ToDouble(bh.Value) : 40.0; } catch (FormatException ex) { Logger.WriteWarning("Exception while reading board dimensions: {0}", ex.Message); } boardLayout.numLayers = 2; { // Look to see if there is a PCB component in the top level assembly var compImpl = tb_obj.ComponentAssemblies.SelectMany(c => c.ComponentInstances).Select(i => i.Impl) .Where(j => (j as Tonka.Component).Attributes.Classifications .Contains("pcb_board")).FirstOrDefault(); // Look to see if it has a resource labeled 'BoardTemplate' var comp = (compImpl != null) ? compImpl as Tonka.Component : null; var btRes = (comp != null) ? comp.Children.ResourceCollection.Where(r => r.Name.ToUpper().Contains("BOARDTEMPLATE")).FirstOrDefault() : null; if (btRes != null) { var btPath = Path.Combine(comp.Attributes.Path, btRes.Attributes.Path); boardLayout.boardTemplate = Path.GetFileName(btPath); } } // the prior code looks up for a boardTemplate file associated with a PCB component // the users can override it with a boardTemplate file specified as a testbench parameter { var bt = tb_obj.Parameters.Where(p => p.Name.Equals("boardTemplate")).FirstOrDefault(); // if its a file path - we only want to pass the file name to board synthesis if (bt != null) try { boardLayout.boardTemplate = Path.GetFileName(bt.Value); } catch (System.ArgumentException ex) { CodeGenerator.Logger.WriteError("Error extracting boardTemplate filename: {0}", ex.Message); } } // the users also can specify a designRule file as a testbench parameter { var dr = tb_obj.Parameters.Where(p => p.Name.Equals("designRules")).FirstOrDefault(); if (dr != null) try { boardLayout.designRules = Path.GetFileName(dr.Value); } catch (System.ArgumentException ex) { CodeGenerator.Logger.WriteError("Error extracting designRules filename: {0}", ex.Message); } } boardLayout.packages = new List<Package>(); int pkg_idx = 0; // we want to handle prerouted assemblies as follows // 1) all parts that are part of the pre-routed assembly are tagged with a 'virtual' spaceClaim part // 2) we add their absolute location (in the subckt frame of refernece) in the output json // 3) we create virtual spaceClaim parts for pre-routed subcircuits // 4) all nets belonging to the pre-routed subcircuit are tagged with the spaceClaim part // 5) these nets are added to json with absolute location (same as parts above) foreach (var ca in tb_obj.ComponentAssemblies) { pkg_idx = HandlePreRoutedAsm(ca, pkg_idx); } // compute part dimensions from foreach (var part in sch_obj.parts.part) { var dev = sch_obj.libraries.library.Where(l => l.name.Equals(part.library)). SelectMany(l => l.devicesets.deviceset).Where(ds => ds.name.Equals(part.deviceset)). SelectMany(ds => ds.devices.device).Where(d => d.name.Equals(part.device)).FirstOrDefault(); var spkg = (dev != null) ? sch_obj.libraries.library.Where(l => l.name.Equals(part.library)) .SelectMany(l => l.packages.package).Where(p => p.name.Equals(dev.package)) .FirstOrDefault() : null; Package pkg = new Package(); pkg.name = part.name; pkg.pkg_idx = pkg_idx++; if (dev == null || spkg == null) { // emit warning Logger.WriteWarning("Unable to get package size for part - layout/chipfit results may be inaccurate: {0}", part.name); } else { pkg.package = spkg.name; // note that the eagle package information may be incomplete - should really have curated version from CyPhy #region ComputePackageSize var minX = Double.MaxValue; var minY = Double.MaxValue; var maxX = Double.MinValue; var maxY = Double.MinValue; foreach (Eagle.wire wire in spkg.Items.Where(s => s is Eagle.wire)) { if (wire == null) continue; if (!LayersForBoundingBoxComputation.Any(wire.layer.Contains)) continue; var x1 = Convert.ToDouble(wire.x1); var x2 = Convert.ToDouble(wire.x2); var y1 = Convert.ToDouble(wire.y1); var y2 = Convert.ToDouble(wire.y2); if (x1 < minX) minX = x1; if (x2 < minX) minX = x2; if (x1 > maxX) maxX = x1; if (x2 > maxX) maxX = x2; if (y1 < minY) minY = y1; if (y2 < minY) minY = y2; if (y1 > maxY) maxY = y1; if (y2 > maxY) maxY = y2; } foreach (Eagle.pad pad in spkg.Items.Where(s => s is Eagle.pad)) { if (pad == null) continue; var pad_num = pad.name; var x = Convert.ToDouble(pad.x); var y = Convert.ToDouble(pad.y); var drill = Convert.ToDouble(pad.drill); var dia = Convert.ToDouble(pad.diameter); var shape = pad.shape; // enum padShape {round, octagon, @long, offset} var rot = pad.rot; // { R90, R180, R270, ...} var r = 0.0; if (dia == 0.0) // JS: Workaround for no diameter present, estimate dia to be 2x drill size { dia = drill * 2.0; } if (shape == Eagle.padShape.@long) { // TODO: consider PAD rotation; for now, consider long pads are 2x diameter. dia *= 2.0; // diameter value from package desc is for the "short" side, for max calc, consider long side is 2x (by inspection) } r = dia / 2.0; if ((x - r) < minX) minX = x - r; if ((x + r) > maxX) maxX = x + r; if ((y - r) < minY) minY = y - r; if ((y + r) > maxY) maxY = y + r; } foreach (Eagle.circle circle in spkg.Items.Where(s => s is Eagle.circle)) { if (circle == null) continue; var x = Convert.ToDouble(circle.x); var y = Convert.ToDouble(circle.y); var r = Convert.ToDouble(circle.radius); if ((x - r) < minX) minX = x - r; if ((x + r) > maxX) maxX = x + r; if ((y - r) < minY) minY = y - r; if ((y + r) > maxY) maxY = y + r; } foreach (Eagle.smd smd in spkg.Items.Where(s => s is Eagle.smd)) { if (smd == null) continue; // SKN: after intense research on eagle, it seems that the way SMD pads are placed is as follows // dx - dy tells the length of the pad, while the x is the center point of the SMD pad var x = Convert.ToDouble(smd.x); var y = Convert.ToDouble(smd.y); var ddx = Convert.ToDouble(smd.dx); var ddy = Convert.ToDouble(smd.dy); var dx = ddx; // should be /2?? var dy = ddy; if (smd.rot.Equals("R90") || smd.rot.Equals("R270")) { // flip dx and dy if there is rotation dx = ddy; dy = ddx; } var x1 = x - dx / 2.0; var x2 = x + dx / 2.0; var y1 = y - dy / 2.0; var y2 = y + dy / 2.0; if (x1 < minX) minX = x1; if (x2 > maxX) maxX = x2; if (y1 < minY) minY = y1; if (y2 > maxY) maxY = y2; } pkg.width = maxX - minX; pkg.height = maxY - minY; pkg.originX = Math.Floor(10.0 * (maxX + minX) / 2.0) / 10.0; pkg.originY = Math.Floor(10.0 * (maxY + minY) / 2.0) / 10.0; #endregion // emit component ID for locating components in CAD assembly if (CodeGenerator.partComponentMap.ContainsKey(part)) { var comp_obj = CodeGenerator.partComponentMap[part]; var comp = comp_obj.Impl as Tonka.Component; // emit component ID for locating components in CAD assembly pkg.ComponentID = comp.Attributes.InstanceGUID; Boolean isMultiLayer = comp.Children.EDAModelCollection. Any(e => e.Attributes.HasMultiLayerFootprint); if (isMultiLayer) pkg.multiLayer = true; #region ExactConstraints // exact constraints related to this part var exactCons = from conn in comp.SrcConnections.ApplyExactLayoutConstraintCollection select conn.SrcEnds.ExactLayoutConstraint; foreach (var c in exactCons) { var pcons = new ExactConstraint(); pcons.type = "exact"; if (!c.Attributes.X.Equals("")) pcons.x = Convert.ToDouble(c.Attributes.X); if (!c.Attributes.Y.Equals("")) pcons.y = Convert.ToDouble(c.Attributes.Y); if (!c.Attributes.Layer.Equals("")) pcons.layer = Convert.ToInt32(c.Attributes.Layer); if (!c.Attributes.Rotation.Equals("")) pcons.rotation = Convert.ToInt32(c.Attributes.Rotation); if (pkg.constraints == null) pkg.constraints = new List<Constraint>(); pkg.constraints.Add(pcons); } #endregion #region RangeConstraints // range constraints related to this part var rangeCons = from conn in comp.SrcConnections.ApplyRangeLayoutConstraintCollection select conn.SrcEnds.RangeLayoutConstraint; foreach (var c in rangeCons) { var pcons = new RangeConstraint(); pcons.type = "range"; if (!c.Attributes.XRange.Equals("")) pcons.x = c.Attributes.XRange; if (!c.Attributes.YRange.Equals("")) pcons.y = c.Attributes.YRange; if (!c.Attributes.LayerRange.Equals("")) pcons.layer = c.Attributes.LayerRange; if (pkg.constraints == null) pkg.constraints = new List<Constraint>(); pkg.constraints.Add(pcons); } #endregion #region PreRoutedAssemblyPart // now handle if this component is part of a pre-routed sub-ckt if (CodeGenerator.preRouted.ContainsKey(comp_obj.Parent)) { var layoutParser = CodeGenerator.preRouted[comp_obj.Parent]; var prePkg = layoutParser.componentPackageMap[comp_obj]; pkg.x = prePkg.x; pkg.y = prePkg.y; pkg.rotation = prePkg.rotation; pkg.RelComponentID = (comp_obj.Parent.Impl.Impl as GME.MGA.MgaFCO) .RegistryValue["Elaborator/InstanceGUID_Chain"]; pkg.doNotPlace = true; } #endregion } } pkgPartMap.Add(pkg, part); // add to map partPkgMap.Add(part, pkg); boardLayout.packages.Add(pkg); } #region AddSignalsToBoardLayout boardLayout.signals = new List<Signal>(); foreach (var net in sch_obj.sheets.sheet.FirstOrDefault().nets.net) { // dump pre-routed signals to board file var sig = new Signal(); sig.name = net.name; sig.pins = new List<Pin>(); Signal preRouted = null; string preRoutedAsmID = null; // ID of the parent pre-routed assembly string preRoutedAsm = null; // name the parent pre-routed assembly bool onlyOnePreroute = true; foreach (var seg in net.segment.SelectMany(s => s.Items).Where(s => s is Eagle.pinref)) { bool isPrerouted = false; var pr = seg as Eagle.pinref; var pin = new Pin(); pin.name = pr.pin; pin.gate = pr.gate; pin.package = pr.part.ToUpper(); // find package and pad var part = sch_obj.parts.part.Where(p => p.name.Equals(pr.part)).FirstOrDefault(); var dev = (part != null) ? sch_obj.libraries.library.Where(l => l.name.Equals(part.library)). SelectMany(l => l.devicesets.deviceset).Where(ds => ds.name.Equals(part.deviceset)). SelectMany(ds => ds.devices.device).Where(d => d.name.Equals(part.device)).FirstOrDefault() : null; var pad = (dev != null) ? dev.connects.connect.Where(c => c.gate.Equals(pr.gate) && c.pin.Equals(pr.pin)).Select(c => c.pad).FirstOrDefault() : null; if (pad != null) pin.pad = pad; // check for preroutes - all pins in this signal should be in the prerouted net, otherwise reject it if (part != null && CodeGenerator.partComponentMap.ContainsKey(part)) { var comp_obj = CodeGenerator.partComponentMap[part]; if (CodeGenerator.preRouted.ContainsKey(comp_obj.Parent)) // is the parent assembly prerouted { Logger.WriteDebug("Net {0} in Prerouted Asm {1}", net.name, comp_obj.Parent.Name); var layoutParser = CodeGenerator.preRouted[comp_obj.Parent]; // find the name/gate matching port in schematic domain model var sch = comp_obj.Impl.Children.SchematicModelCollection.FirstOrDefault(); var port = (sch != null) ? sch.Children.SchematicModelPortCollection. Where(p => p.Attributes.EDAGate == pin.gate && p.Name == pin.name). FirstOrDefault() : null; // find the buildPort var buildPort = port != null && CyPhyBuildVisitor.Ports.ContainsKey(port.ID) ? CyPhyBuildVisitor.Ports[port.ID] : null; if (buildPort != null && layoutParser.portTraceMap.ContainsKey(buildPort)) { isPrerouted = true; Logger.WriteDebug("Found build Port and Associated Trace"); if (preRouted == null) { preRouted = layoutParser.portTraceMap[buildPort]; preRoutedAsmID = (comp_obj.Parent.Impl.Impl as GME.MGA.MgaFCO) .RegistryValue["Elaborator/InstanceGUID_Chain"]; preRoutedAsm = comp_obj.Parent.Name; } else if (preRouted != layoutParser.portTraceMap[buildPort]) isPrerouted = false; } } } onlyOnePreroute = onlyOnePreroute && isPrerouted; // add pin to list of pins for this signal sig.pins.Add(pin); } // if pre-routed then copy wires if (onlyOnePreroute && preRouted != null) { Logger.WriteInfo("Prerouted net {0}, from assembly {1}, originally as {2}", net.name, preRoutedAsm, preRouted.name); sig.wires = new List<Wire>(); sig.vias = new List<Via>(); sig.RelComponentID = preRoutedAsmID; foreach (var w in preRouted.wires) sig.wires.Add(w); foreach (var v in preRouted.vias) sig.vias.Add(v); } boardLayout.signals.Add(sig); } #endregion #region AddRelativeConstraintsToBoardLayout // now process relative constraints - they require that all parts be mapped to packages already for (int i = 0; i < boardLayout.packages.Count; i++) { var pkg = boardLayout.packages[i]; if (!pkgPartMap.ContainsKey(pkg)) continue; var part = pkgPartMap[pkg]; var comp = CodeGenerator.partComponentMap.ContainsKey(part) ? CodeGenerator.partComponentMap[part] : null; var impl = comp != null ? comp.Impl as Tonka.Component : null; var relCons = from conn in impl.SrcConnections.ApplyRelativeLayoutConstraintCollection select conn.SrcEnds.RelativeLayoutConstraint; foreach (var c in relCons) { var pcons = new RelativeConstraint(); pcons.type = "relative-pkg"; if (!c.Attributes.XOffset.Equals("")) pcons.x = Convert.ToDouble(c.Attributes.XOffset); if (!c.Attributes.YOffset.Equals("")) pcons.y = Convert.ToDouble(c.Attributes.YOffset); // find origin comp var origCompImpl = (from conn in c.SrcConnections.RelativeLayoutConstraintOriginCollection select conn.SrcEnds.Component).FirstOrDefault(); var origComp = ((origCompImpl != null) && CyPhyBuildVisitor.Components.ContainsKey(origCompImpl.ID)) ? CyPhyBuildVisitor.Components[origCompImpl.ID] : null; var origPart = ((origComp != null) && CodeGenerator.componentPartMap.ContainsKey(origComp)) ? CodeGenerator.componentPartMap[origComp] : null; var origPkg = ((origPart != null) && partPkgMap.ContainsKey(origPart)) ? partPkgMap[origPart] : null; pcons.pkg_idx = origPkg.pkg_idx.Value; if (pkg.constraints == null) pkg.constraints = new List<Constraint>(); pkg.constraints.Add(pcons); boardLayout.packages[i] = pkg; } } #endregion }
static void Main(string[] args) { #region ProcessArguments // need schematic input file / layout json input file name / output board file name as command line args if (args.Length < 2) { Console.WriteLine("usage: BoardSynthesis <schema.sch> <layout.json> [-r]"); Console.WriteLine(" Input: <schema.sch>, <layout.json>; Output: <schema.brd>"); Console.WriteLine("[-r] Input: <schema.sch> (implicit schema.brd), <layout.json>; Output: <layout.json>"); return; } var schematicFile = args[0]; var layoutFile = args[1]; var boardFile = schematicFile.Replace(".sch", ".brd"); var programMode = 0; // forward if (args.Length == 3 && args[2].CompareTo("-r") == 0) { programMode = 1; } #endregion #region LoadSchematic // load schematic file Eagle.eagle schematic = null; try { schematic = Eagle.eagle.LoadFromFile(schematicFile); Console.WriteLine("Parsed Schematic File : " + schematicFile); } catch (Exception e) { Console.WriteLine("ERROR Reading Schematic XML: " + e.Message); System.Environment.Exit(-1); } #endregion #region LoadLayout // load layout file Layout boardLayout = null; Dictionary<string, Package> packageMap = new Dictionary<string, Package> (StringComparer.InvariantCultureIgnoreCase); Dictionary<string, Signal> signalMap = new Dictionary<string, Signal> (StringComparer.InvariantCultureIgnoreCase); try { string sjson = "{}"; using (StreamReader reader = new StreamReader(layoutFile)) { sjson = reader.ReadToEnd(); boardLayout = JsonConvert.DeserializeObject<Layout>(sjson); reader.Close(); } foreach (var pkg in boardLayout.packages) { packageMap[pkg.name] = pkg; packageMap[pkg.ComponentID] = pkg; // for ID search } if (boardLayout.signals == null) boardLayout.signals = new List<Signal>(); foreach (var sig in boardLayout.signals) { signalMap[sig.name] = sig; } } catch (Exception e) { Console.WriteLine("ERROR Reading Layout File: " + e.Message); System.Environment.Exit(-1); } #endregion Eagle.eagle eagle = null; #region LoadBoardTemplate // load board template file // if there is a user-defined load that one, // otherwise, load the one bundled as a project resource if (boardLayout.boardTemplate != null && boardLayout.boardTemplate != "") { try { eagle = Eagle.eagle.LoadFromFile(boardLayout.boardTemplate); Console.WriteLine("Parsed Eagle Board Template File {0}: " + eagle.version, boardLayout.boardTemplate); } catch (Exception e) { Console.WriteLine("ERROR: Unable to load Board Template XML: " + e.Message); } } if (eagle == null) // unable { try { var brdTempl = Properties.Resources.boardTemplate; eagle = Eagle.eagle.Deserialize(brdTempl); Console.WriteLine("Parsed Eagle Library schema version: " + eagle.version); } catch (Exception e) { Console.WriteLine("ERROR: Failed parsing default board Template XML: " + e.Message); System.Environment.Exit(-1); } } #endregion #region Layout2Board if (programMode == 0) // forward mode: layout --> board { // Create Board File { // Mark board boundaries var plain = (eagle.drawing.Item as Eagle.board).plain; AddBoundary(boardLayout, plain); // Design Rules if (boardLayout.designRules != null || boardLayout.designRules != "") CopyDesignRules(boardLayout.designRules, (eagle.drawing.Item as Eagle.board).designrules); // Copy Libraries var srcLib = (schematic.drawing.Item as Eagle.schematic).libraries; var dstLib = (eagle.drawing.Item as Eagle.board).libraries; foreach (var lib in srcLib.library) { dstLib.library.Add(lib); } // Add Elements with Position var parts = (schematic.drawing.Item as Eagle.schematic).parts; var elements = (eagle.drawing.Item as Eagle.board).elements; AddElements(parts, packageMap, dstLib, elements); // Add Signals var nets = (schematic.drawing.Item as Eagle.schematic).sheets.sheet.Select(s => s.nets).FirstOrDefault(); var signals = (eagle.drawing.Item as Eagle.board).signals; AddSignals(nets, parts, signalMap, packageMap, dstLib, signals); } eagle.SaveToFile(boardFile); } #endregion #region Board2Layout else if (programMode == 1) // reverse mode: board --> layout { try { eagle = Eagle.eagle.LoadFromFile(boardFile); Console.WriteLine("Parsed Board File : " + boardFile); } catch (Exception e) { Console.WriteLine("ERROR Reading Board XML: " + e.Message); System.Environment.Exit(-1); } Dictionary<string, Eagle.element> elementMap = new Dictionary<string, Eagle.element>(); var elements = (eagle.drawing.Item as Eagle.board).elements; foreach (var element in elements.element) { if (packageMap.ContainsKey(element.name)) { var pkg = packageMap[element.name]; if (element.rot.Contains("R90")) pkg.rotation = 1; else if (element.rot.Contains("R180")) pkg.rotation = 2; else if (element.rot.Contains("R270")) pkg.rotation = 3; if (element.rot.Contains("M")) pkg.layer = 1; double x = Convert.ToDouble(element.x) - ((pkg.rotation == 1 || pkg.rotation == 3) ? pkg.height/2.0 : pkg.width/2.0); double y = Convert.ToDouble(element.y) - ((pkg.rotation == 1 || pkg.rotation == 3) ? pkg.width/2.0 : pkg.height/2.0); pkg.x = Math.Ceiling(x * 10.0) / 10.0; // ceil to compensate for floor that we do in forward operation pkg.y = Math.Ceiling(y * 10.0) / 10.0; // ceil to compensate for floor that we do in forward operation } else { Console.WriteLine("WARNING: part {0} not found in layout file", element.name); } elementMap.Add(element.name, element); } var signals = (eagle.drawing.Item as Eagle.board).signals; foreach (var signal in signals.signal) { Signal jsig = null; bool update = true; if (signalMap.ContainsKey(signal.name)) jsig = signalMap[signal.name]; else { jsig = new Signal(); jsig.name = signal.name; update = false; } jsig.pins = new List<Pin>(); jsig.wires = new List<Wire>(); jsig.vias = new List<Via>(); double traceLength = 0.0; Dictionary<string, Pin> pinMap = new Dictionary<string,Pin>(); foreach (var item in signal.Items) { if (item is Eagle.contactref) { var cref = item as Eagle.contactref; var pin = new Pin(); var element = elementMap.ContainsKey(cref.element) ? elementMap[cref.element] : null; if (element != null) { var lib = (schematic.drawing.Item as Eagle.schematic).libraries.library.Where(l => l.name.Equals(element.library)).FirstOrDefault(); var dev = (lib != null) ? lib.devicesets.deviceset.SelectMany(ds => ds.devices.device).Where(d => d.package.Equals(element.package)).FirstOrDefault() : null; var connect = (dev != null) ? dev.connects.connect.Where(c => c.pad.Equals(cref.pad)).FirstOrDefault() : null; pin.name = (connect != null) ? connect.pin : cref.pad; pin.pad = (connect != null) ? connect.pad : cref.pad; pin.gate = (connect != null) ? connect.gate : null; } pin.package = cref.element; var pinName = string.Format("{0}.{1}", pin.package, pin.name); // prevent creation of duplicate pins from the same package if (!pinMap.ContainsKey(pinName)) { jsig.pins.Add(pin); pinMap[pinName] = pin; } } else if (item is Eagle.wire) { var wire = item as Eagle.wire; var jwire = new Wire(); jwire.x1 = Convert.ToDouble(wire.x1); jwire.x2 = Convert.ToDouble(wire.x2); jwire.y1 = Convert.ToDouble(wire.y1); jwire.y2 = Convert.ToDouble(wire.y2); jwire.width = Convert.ToDouble(wire.width); jwire.layer = Convert.ToInt32(wire.layer); jsig.wires.Add(jwire); double w = jwire.x2 - jwire.x1; double h = jwire.y2 - jwire.y1; double l = Math.Sqrt(w * w + h * h); traceLength += l; } else if (item is Eagle.via) { var via = item as Eagle.via; var jvia = new Via(); jvia.x = Convert.ToDouble(via.x); jvia.y = Convert.ToDouble(via.y); jvia.drill = Convert.ToDouble(via.drill); string[] layerRange = via.extent.Split('-'); if (layerRange.Count() >= 2) { jvia.layerBegin = Convert.ToInt32(layerRange[0]); jvia.layerEnd = Convert.ToInt32(layerRange[1]); } jsig.vias.Add(jvia); } jsig.length = traceLength; jsig.bends = jsig.wires.Count + jsig.vias.Count - 1; } if (!update) boardLayout.signals.Add(jsig); } Console.WriteLine("Writing Output to {0}", layoutFile); var sjson = JsonConvert.SerializeObject(boardLayout, Formatting.Indented); StreamWriter writer = new StreamWriter(layoutFile); writer.Write(sjson); writer.Close(); } #endregion }