I am running Node.js server with express. I'd also like the server to accept IceCast audio stream.
I could use another port, sure, but not all hostings (like Heroku) allow that. Ice cast's stream request looks like this:
SOURCE /mountpoint ICE/1.0\n
content-type: audio/mpeg\n
Authorization: Basic USER+PASS base64encoded\n
ice-name: This is my server name\n
ice-url: http://www.oddsock.org\n
ice-genre: Rock\n
ice-bitrate: 128\n
ice-private: 0\n
ice-public: 1\n
ice-description: This is my server description\n
ice-audio-info: ice-samplerate=44100;ice-bitrate=128;ice-channels=2\n
\n
After that, audio stream follows. I wrote a separate server that handles this on another port and it works fine.
var headers = "";
var headersEnd = false;
var mp3;
const audioServer = net.createServer(function (socket) {
if (mp3) {
socket.write("HTTP/1.0 403 Client already connected\r\n\r\n");
socket.end();
socket.on("error", (e) => {});
return;
}
mp3 = fs.createWriteStream("test.mp3", { encoding: null, flags: "a" });
socket.on("data", (data) => {
if (!headersEnd) {
var tmp = "";
for (let i = 0, l = data.byteLength; i < l; ++i) {
const item = data[i];
if (item == CR_NUMBER)
continue;
const character = String.fromCharCode(item);
tmp += character;
headers += character;
if (headers.endsWith("\n\n")) {
headersEnd = true;
console.log("ICE CAST HEADERS: \n", headers.replace(/\n/g, "\\n\n").replace(/\r/g, "\\r"));
break;
}
}
}
else {
mp3.write(data);
}
});
socket.on("close", () => {
console.log("ICE CAST: END");
if (mp3) {
mp3.close();
mp3 = null;
}
});
socket.on("error", (e) => {
console.log("ICE CAST: ERROR" + e.message);
socket.end();
});
});
audioServer.listen(11666);
What I'd like is to somehow bootstrap node's HTTP server so that I can stream over the same port.
I tried to access the req connection info, that doesn't really work, because the server does not even let the SOURCE /mountpoint ICE/1.0 through.
const server = http.createServer(function (req, res) {
/// does not happen, server closes the connection from icecast
if (handleAudioStream(req, res)) {
return;
}
else {
return expressApp(req, res);
}
});
So I'd need to go deeper. I tried to inspect the net and http code, but didn't fund anything useful.
How can I do this? I really need to use same port, and since icecast DOES send the HTTP-like headers, it should be possible.
This isn't trivial, but possible. You can do some duck punching/monkey patching. See this answer: https://stackoverflow.com/a/24298059/362536
Also, it may be possible to get official support some day, but we're a ways off from that. The first blocker was the non-standard SOURCE method. I sponsored a bounty on that and Ben Noordhuis was kind enough to implement last week: https://github.com/nodejs/http-parser/issues/405 It should land in Node.js eventually.
The next issue is the ICE/1.0. I've opened an issue for that here: https://github.com/nodejs/http-parser/issues/410 There hasn't been any objection to adding it to the parser yet, but if you want to add a pull request, that might help a chance of approval.
You'll find other compatibility issues as well as you continue down this road, but all I've hit I've been able to overcome with various solutions. The trick is, maintaining strict compatibility with the Node.js core as it is updated.
Related
I installed the rets-client package from npm.
I ran other query and get meta which works fine but when I am trying to do the photo streaming example I kept on getting errors
Error: RetsReplyError: RETS Server reply while attempting getObject - ReplyCode 20403 (NO_OBJECT_FOUND); ReplyText: No Object Found [260978536:1].
I followed the the code in the example
https://github.com/sbruno81/rets-client#photo-streaming-example
try {
rets.getAutoLogoutClient(clientSettings, async (client) => {
const photoIds = {
'260978536': '*', // get all photos for listingId 260978536
};
const photoStream = await client.objects.stream.getObjects('Property', 'Photo', photoIds, {
alwaysGroupObjects: true,
ObjectData: '*'
});
console.log("========================================");
console.log("======== Photo Stream Results ========");
console.log("========================================");
return new Promise(function (resolve, reject) {
let i = 0;
photoStream.objectStream.on('data', function (event) {
try {
if (event.type === 'headerInfo') {
console.log(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(event.headerInfo);
return
}
console.log(" -------- Photo " + (i + 1) + " --------");
if (event.type === 'error') {
console.log(" Error: " + event.error);
} else if (event.type === 'dataStream') {
outputFields(event.headerInfo);
fileStream = fs.createWriteStream(
"/tmp/photo_" + event.headerInfo.contentId + "_" + event.headerInfo.objectId + "." + event.headerInfo.contentType.match(/\w+\/(\w+)/i)[1]);
event.dataStream.pipe(fileStream);
}
i++;
} catch (err) {
reject(err);
}
});
photoStream.objectStream.on('error', function (errorInfo) {
reject(errorInfo);
});
photoStream.objectStream.on('end', function () {
resolve();
});
})
})
} catch (errorInfo) {
const error = errorInfo.error || errorInfo;
console.log(" ERROR: issue encountered:");
outputFields(error);
console.log(' ' + (error.stack || error).replace(/\n/g, '\n '));
}
reason I used that photo id is because when I do query I can see that this listing id has PictureCount of 20 but somehow it's giving me no object found.
sample listing query return for the same id
{ L_Area: 'Islands-Van. & Gulf',
L_ListingID: '260978536',
L_Status: 'Expired',
L_PictureCount: '20',
L_Last_Photo_updt: '2015-07-15T04:27:00',
L_DisplayId: 'V1064230' }
Can someone please give me a hand on where I am doing wrong here?
Thanks in advance for any help and suggestions.
P.S. I also tried using one L_ListingID with L_Status as Active instead of Expired but the result is the same
The RETS server you're connecting to does not allow image downloads because it's a staging server and they want to keep the bandwidth low. You'll have to test your code against their production server, or ask the MLS to allow downloads from their staging environment.
Points to note while downloading images from RETS server:
Ensure you have permission to access listing images.
Secondly check you have image download access or public image url access only(CDN link)? Depends on RETS server either one or both permission will be given.
To download images/imageURLs you need photoIds. Here either "listingId" or "listingKey" will work, again depends on RETS server. So try with both.
You may have access to multiple image types like Thumbnail, normal size and high resolution. That also you can mention in the "getObject" method.
Once an image/imageURL downloaded, frequently cross check Photo Modification Timestamp field to identify any modification to the image/imageURL.
Some of the RETS servers will provide image URLs as data via resources like Media, Tour etc.
I would like to get a multi-process node. Workers are listening clients connections. I need pass sockets to master process because master process emit message to clients. Workers also need socket to emit message to clients.
Socket is a circular object and can't pass to a master process.
My code:
const cluster = require('cluster');
const http = require('http');
var io = require('socket.io');
var users;
var clients = {};
if (cluster.isMaster) {
function messageHandler(msg) {
if (msg.usersarray) {
usersarray = msg.usersarray;
console.log(usersarray);
}else if(msg.socket){
clients[usersarray["manu"][0]] = msg.socket;
clients[usersarray["manu"][0]].emit("hola","hola");
}
}
// Start workers and listen for messages containing notifyRequest
const numCPUs = require('os').cpus().length;
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
Object.keys(cluster.workers).forEach((id) => {
cluster.workers[id].on('message', messageHandler);
});
}else {
// Create server & socket
var server = http.createServer(function(req, res){
// Send HTML headers and message
res.writeHead(404, {'Content-Type': 'text/html'});
res.end('<h1>Aw, snap! 404</h1>');
});
server.listen(3000);
io = io.listen(server);
// Add a connect listener
io.sockets.on('connection', function(socket) {
var hs = socket.handshake;
console.log("socket connected");
if(users == undefined){
users = {};
}
if(hs.query.usuario != undefined){
if(users[hs.query.usuario] == undefined){
users[hs.query.usuario] = new Array();
}
users[hs.query.usuario].push(socket.id); // connected user with its socket.id
clients[socket.id] = socket; // add the client data to the hash
process.send({ usersarray: users});
process.send({ socket: socket});
}
// Disconnect listener
socket.on('disconnect', function() {
console.log('Client disconnected.');
});
});
}
in line process.send({ socket: socket}); Node js get error "TypeError: Converting circular structure to JSON"
-I used some module to transform circular object but don't working.
-I tried to pass socket id and then in master process, created new socket with this id but I didn't know to use it.
There is any posibility to pass socket from worker to master process?
Node js version: v5.5.0
Hm, I don't think it is possible what you are trying to do. When you create a cluster it means that you create separate processes (master + workers) which can only talk over the pipe.
Talking over the pipe means they can only send strings to each other. process.send tries to serialize a Javascript object as JSON (--> making a string out of it) using JSON.stringify. JSON for example cannot have functions, circles, etc. I just checked the socket object, it is very complex and contains functions (such as socket.emit()), so you cannot just serialize it and send it over the pipe.
Maybe you can check this or this on how to use clustered WebSockets.
It doesn't seem very trivial.. Maybe you can just pass CPU intensive tasks to some worker processes (via cluster or just spawning them yourself), send the results back to the master and let him do all the communication with the client?
I understand your purpose of broadcasting to all the node worker processes in a cluster, although you can not send socket component as such but there is a workaround for the purpose to be served. I will try an explain with an example :
Step 1: When a client action requires a broadcast :
Child.js (Process that has been forked) :
socket.on("BROADCAST_TO_ALL_WORKERS", function (data)
{
process.send({cmd : 'BROADCAST_TO_ALL_WORKERS', message :data.message});
})
Step 2: On the cluster creation side
Server.js (Place where cluster forking happens):
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
var worker = cluster.fork();
worker.on('message', function (data) {
if (data.cmd === "BROADCAST_TO_ALL_WORKERS") {
console.log(server_debug_prefix() + "Server Broadcast To All, Message : " + data.message + " , Reload : " + data.reload + " Player Id : " + data.player_id);
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({cmd : "BROADCAST_TO_WORKER", message : data.message});
});
}
});
}
cluster.on('exit', function (worker, code, signal) {
var newWorker = cluster.fork();
newWorker.on('message', function (data) {
console.log(data);
if (data.cmd === "BROADCAST_TO_ALL_WORKERS") {
console.log(data.cmd,data);
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({cmd : "BROADCAST_TO_WORKER", message : data.message});
});
}
});
});
}
else {
//Node Js App Entry
require("./Child.js");
}
Step 3: To Broadcast in the child process
-> Put this before io.on("connection") in Child.js
process.on("message", function(data){
if(data.cmd === "BROADCAST_TO_WORKER"){
io.sockets.emit("SERVER_MESSAGE", { message: data.message, reload: data.reload, player_id : data.player_id });
}
});
I hope its clear. Please comment if its confusing ... I will try and make it clear.
My server is running NodeJS and uses the amqplib api to request data from another application. The NodeJS server is receiving the information successfully but there's a noticable delay and I'm trying to determine whether I am doing this in the most efficient manner. Specifically I'm concerned with the way that I open and close connections.
Project Layout
I have two controller files that handle receiving and requesting the data, request.img.server.controller.js and receive.img.server.controller.js. Finally the routes handle the controller methods when a button on the front end is pushed, oct.server.routes.js.
request.img.server.controller.js
'use strict';
var amqp = require('amqplib/callback_api');
var connReady = false;
var conn, ch;
amqp.connect('amqp://localhost:5672', function(err, connection) {
conn = connection;
connReady = true;
conn.createChannel(function(err, channel) {
ch = channel;
});
});
exports.sendRequest = function(message) {
console.log('sending request');
if(connReady) {
var ex = '';
var key = 'utils';
ch.publish(ex, key, new Buffer(message));
console.log(" [x] Sent %s: '%s'", key, message);
}
};
receive.img.server.controller.js
var amqp = require('amqplib/callback_api');
var fs = require('fs');
var wstream = fs.createWriteStream('C:\\Users\\yako\\desktop\\binarytest.txt');
var image, rows, cols;
exports.getResponse = function(resCallback) {
amqp.connect('amqp://localhost:5672', function(err, conn) {
conn.createChannel(function(err, ch) {
var ex = '';
ch.assertQueue('server', {}, function(err, q) {
console.log('waiting for images');
var d = new Date();
var n = d.getTime();
ch.consume(q.queue, function(msg) {
console.log(" [x] %s: '%s'", msg.fields.routingKey, msg.content.toJSON());
rows = msg.content.readInt16LE(0);
cols = msg.content.readInt16LE(2);
console.log("rows = %s", msg.content.readInt16LE(0));
console.log("cols = %s", msg.content.readInt16LE(2));
image = msg.content;
var currMax = 0;
for (var i = 4; i < image.length; i+=2) {
if (image.readInt16LE(i) > currMax) {
currMax = image.readInt16LE(i);
}
wstream.write(image.readInt16LE(i) + ',');
}
console.log('done writing max is', currMax);
//console.log(image);
resCallback(rows, cols, image);
}, {
noAck: true
});
});
});
});
};
oct.server.routes.js
'use strict';
module.exports = function(app) {
var request_img = require('../../app/controllers/image-tools/request.img.server.controller.js');
var receive_img = require('../../app/controllers/image-tools/receive.img.server.controller.js');
// oct routes
app.get('/load_slice', function(req, res) {
console.log('load slice hit');
receive_img.getResponse(function (rows, cols, image) {
res.end(image);
});
request_img.sendRequest('123:C:\\Users\\yako\\Documents\\Developer\\medicaldiag\\test_files\\RUS-01-035-09M-21.oct');
});
};
The way you're opening connections is bad, and is at least part of the performance problem.
Connections are expensive to open. They open a new TCP/IP connection on a TCP/IP port between the client and rabbitmq server. This takes time, and uses up a limited resource on both the client and server.
Because of this, a single connection to RabbitMQ should be created and used within each of your node.js processes. This one connection should be shared by all of the code in that process.
Whenever you need to do something with RabbitMQ, open a new channel on the shared connection and do your work. Channels are cheap and are meant to be opened and closed as needed, within a connection.
More specifically in your code, the receive.img.server.controller.js file is the major problem. This opens a new connection to RabbitMQ every time you call the getResponse method.
If you have 10 users hitting the site, you'll have 10 open RabbitMQ connections when 1 would be sufficient. If you have thousands of users hitting the site, you'll have thousands of open RabbitMQ connections when 1 would be sufficient. You also run the risk of exhausting your available TCP/IP connections on the RabbitMQ server or client.
Your receive.img.server.controller.js should look more like your request.img.server.controller.js - one connection open, and re-used all the time.
Also, FWIW - I recommend using the wascally library for RabbitMQ w/ node.js. This library sits on top of amqplib, but makes things significantly easier. It will manage your one connection for you, and make it easier for you to send and receive messages.
I also have some training material available for RabbitMQ and node.js that covers the basics of amqplib and then moves in to using wascally for real application development.
I'm trying to list the all serial ports and select the port name that begins with /dev/cu.usbmodem. For context; it's an arduino hooked up to a RaspberryPi running node. The Raspberry Pi has a habit of renaming the ports every time it is rebooted.
So far I have this:
com.list(function (err, ports) {
ports.forEach(function(port) {
var arduinoPort = port.comName;
if (arduinoPort.substring(0, 16) == "/dev/cu.usbmodem") {
var SERIALPORT_ID = arduinoPort;
}
});
});
This takes long enough that this next statement fails as the SERIALPORT_ID variable has yet to be declared;
var serialPort = new com.SerialPort(SERIALPORT_ID, {
baudrate: 57600,
parser: com.parsers.readline('\r\n')
});
What callback or structuring technique will make the second statement wait for the first one to declare the variable before executing?
The function below assumes that in your result ports, there is only one serial port. I changed your ports.forEach to a standard for loop. I believe the work going on in this loop was synchronous. I think forEach is synchronous, but I know for(var i = 0; ....) is sync, and if we only have one proper 'port' then we want to be able to skip looping over the other results. This logic can easily be changed if my assumption on 'only one good port' is incorrect.
function getSerialPort(callback) {
'use strict';
com.list(function (err, ports) {
for (var i = 0; i < ports.length; i++) {//ports.forEach works too, but I know this is sync, and that's what we want in this case so we can break out of the loop when we find the right port
var port = ports[i];
var arduinoPort = port.comName;
if (arduinoPort.substring(0, 16) === "/dev/cu.usbmodem") {
var serialPort = new com.SerialPort(arduinoPort, {
baudrate: 57600,
parser: com.parsers.readline('\r\n')
});
callback(serialPort);
return;//I'm not sure what return does in a ports.forEach situation, so I changed it to a standard for loop, so that we know that this is breaking us out of it.
}
}
});
}
getSerialPort(function (serialPort) {
'use strict';
console.log('Serial Port: ' + serialPort);
});
The Raspberry Pi has a habit of renaming the ports every time it is
rebooted.
Well, you could also create some udev rules for the USB hardware you are using, so that the arduino will always be mapped to the same serial port. Assuming you are running debian...
vim /etc/udev/rules.d/98-usb-serial.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="0044", ATTRS{serial}=="64935343733351F072D0", SYMLINK+="arduinoUno"
SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="0043", ATTRS{serial}=="7523230313535121B0E1", SYMLINK+="arduinoMega"
To find out the vendor id, product id and serial number of a usb device use:
dmesg
lsusb
Unplug the device in question, plug it back in and it should be mapped to:
/dev/arduinoUno
/dev/arduinoMega
Or you can do that to look for the right port and connect automagically!
It works great on OS X and Ubuntu, I haven't tested it yet on Raspi, but you get the idea.
Thanks to ChrisCM for the "for" :)
var myPort;
function getSerialPort(callback) {
com.list(function (err, ports) {
for (var i = 0; i < ports.length; i++) {//ports.forEach works too, but I know this is sync, and that's what we want in this case so we can break out of the loop when we find the right port
var port = ports[i];
if(port.pnpId.indexOf("duino") != -1 || port.manufacturer.indexOf("duino") != -1 || port.comName.indexOf('moti') != -1){ // it look for "duino" somewhere
myPort = new SerialPort(port.comName,{
baudrate: 115200,
parser: serialport.parsers.readline("\r\n"),
});
callback(serialPort);
return;//I'm not sure what return does in a ports.forEach situation, so I changed it to a standard for loop, so that we know that this is breaking us out of it.
}
}
});
}
getSerialPort(function (myPort) {
console.log('Serial Port: ' + myPort);
});
You can also output all the port specs using:
console.log("pnpId: " + port.pnpId);
console.log("manufacturer: " + port.manufacturer);
console.log("comName: " + port.comName);
console.log("serialNumber: " + port.serialNumber);
console.log("vendorId: " + port.vendorId);
console.log("productId: " + port.productId);
to find a pattern you could use for automatic connection.
Hope it helps!
I made a program that sends out data to my arduino which detects what was sent and then turns on the correct pin according to what key is pressed.
When using the arduino software from my windows computer the arduino sketch works fine, I can make each pin turn on and off by sending either W A S Or D.
When sending via node the RX light on the arduino flashes but nothing else happens.
Can anyone help?
Node.js program:
var httpServer = require('http').createServer(function(req, response){ /* Serve your static files */ })
httpServer.listen(8080);
var nowjs = require("now");
var everyone = nowjs.initialize(httpServer);
everyone.now.logStuff = function(msg){
console.log(msg);
}
var SerialPort = require('serialport2').SerialPort;
var assert = require('assert');
var portName;
if (process.platform == 'win32') {
portName = 'COM4';
} else if (process.platform == 'darwin') {
portName = '/dev/cu.usbserial-A800eFN5';
} else {
portName = '/dev/ttyUSB0';
}
var readData = '';
var sp = new SerialPort();
sp.on('close', function (err) {
console.log('port closed');
});
sp.on('error', function (err) {
console.error("error", err);
});
sp.on('open', function () {
console.log('port opened... Press reset on the Arduino.');
});
sp.open(portName, {
baudRate: 9600,
dataBits: 8,
parity: 'none',
stopBits: 1,
flowControl: false
});
everyone.now.forward = function() {
sp.write("w");
}
everyone.now.back = function() {
sp.write("s");
}
everyone.now.left = function() {
sp.write("a");
}
everyone.now.right = function() {
sp.write("d");
}
sp.on('data', function(data) {
console.log(data.toString());
});
Arduino Program:
void setup(){
Serial.begin(9600);
Serial.write("READY");
//Set all the pins we need to output pins
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
pinMode(11, OUTPUT);
}
void loop (){
if (Serial.available() > 0) {
//read serial as a character
char ser = Serial.read();
Serial.write(ser);
//NOTE because the serial is read as "char" and not "int", the read value must be compared to character numbers
//hence the quotes around the numbers in the case statement
switch (ser) {
case 'w':
move(8);
break;
case 's':
move(9);
break;
case 'a':
move(10);
break;
case 'q':
move(10);
move(8);
break;
case 'd':
move(11);
break;
case 'e':
move(11);
move(8);
break;
}
}
}
void move(int pin){
Serial.print(pin);
digitalWrite(pin, HIGH);
delay(1);
digitalWrite(pin, LOW);
}
I recently dabbled into this. The Arduino automatically resets when it receives serial communication from most things other than the Arduino IDE. This is why you can send from the IDE but not node.js.
I have an Uno and put a capacitor between Reset and Ground.Here's a page with some good info on the subject.
Good luck. http://arduino.cc/playground/Main/DisablingAutoResetOnSerialConnection
I use node on a daily basis to send actions to my Arduino via usb or via bt and it works great in both cases.
I think your problem comes from sending letters. You should send a buffer instead, with the ascii value of the letter, just like that:
myPort.write(Buffer([myValueToBeSent]));
also, for this, I think you would be better with some "logic" interface, with data headers, number of actions, stuff like that. It is no required for you but it will make your code more robust and easier to modify in the future.
Here is an example of how I do it. First, Node:
var dataHeader = 0x0f, //beginning of the data stream, very useful if you intend to send a batch of actions
myFirstAction = 0x01,
mySecondAction = 0x02,
myThirdAction = 0x03;
You then call them like you did:
everyone.now.MyBatchOfActions = function() {
sp.write(Buffer([dataHeader]));
sp.write(Buffer([0x03])); // this is the number of actions for the Arduino code
sp.write(Buffer([myFirstAction]));
sp.write(Buffer([mySecondAction]));
sp.write(Buffer([myThirdAction]));
}
This way it is easy on the Arduino to Serial.read() the data: (Note that you need to define data header and data footer somewhere)
void readCommands(){
while(Serial.available() > 0){
// Read first byte of stream.
uint8_t numberOfActions;
uint8_t recievedByte = Serial.read();
// If first byte is equal to dataHeader, lets do
if(recievedByte == DATA_HEADER){
delay(10);
// Get the number of actions to execute
numberOfActions = Serial.read();
delay(10);
// Execute each actions
for (uint8_t i = 0 ; i < numberOfActions ; i++){
// Get action type
actionType = Serial.read();
if(actionType == 0x01){
// do you first action
}
else if(actionType == 0x02{
// do your second action
}
else if(actionType == 0x03){
// do your third action
}
}
}
}
}
I hope I'm clear and I hope it helps!
Cheers!
On the capacitor and reset issue...
There is a small capacitor between one of the serial control lines and reset on the Arduino in the later models. This capacitor causes the Arduino to reset when the port is opened but otherwise does not interfere with normal serial operation.
This reset trick allows the code upload to reset the Arduino as part of the upload process. When the Arduino starts up the code boot loader runs first for a short time before the loaded code runs.
The upload process is: Reset the Arduino which starts the boot loader, start the upload process in the Arduino IDE, establish communications, upload, then run the loaded code. When the Arduino starts up it waits for uploads for a short period of time, if none are received, it moves on to running the code.
I find this very useful as it allows us to effectively reset the Arduino just by closing and opening the port. In the old Arduino's, without this capacitor, you had to press the reset button at the right time to get the code to upload. And the timing was such that the Arduino spent much more time waiting before it started with the uploaded code.
In the problem described here, I do not believe he was having any troubles due to the reset trick used. It should have had only the effect of resetting the Arduino when he opened the serial port, and from the looks of his information, this is a desired side-effect.
In my case the issue was the reset, but that the serial port was opened - but not available for write until the reset has finished. Putting a 3s delay before writing to the port fixed the issue. Writing ASCII was not an issue.