Skip to content

Commit 3cdb86f

Browse files
committed
Add support for CTT/Cornell tags
1 parent 18b56c0 commit 3cdb86f

File tree

7 files changed

+194
-50
lines changed

7 files changed

+194
-50
lines changed

src/cornelltagxcvr.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
3+
operate a Cornell (Gabrielson & Winkler) tag transceiver, in
4+
receive-only mode; also works for a CTT LifeTag Motus Adapter
5+
6+
This object represents a plugged-in CornellTagXCVR. As soon as it
7+
is created, it begins recording tag detections. The device reports detections
8+
via an FTDI FT232 USB serial adapter running at 115200 bps raw. Tag
9+
detections are printed as XXXXXXXX\r\n where X are hex digits.
10+
The CTT LifeTag Motus Adapter reports detections as XXXXXXXX,RSSI\r\n.
11+
12+
This module watches for such strings, and emits gotTag events of the form:
13+
14+
T[0-9]{1,2},<TS>,<ID>,<RSSI>\n
15+
16+
where the number after 'T' is the USB port #, <TS> is the ISO timestamp,
17+
<ID> is the bare 8-hex digit tag ID, and <RSSI> is the raw RSSI value.
18+
19+
*/
20+
21+
const readline = require('readline');
22+
23+
CornellTagXCVR = function(matron, dev) {
24+
25+
this.matron = matron;
26+
this.dev = dev;
27+
this.rs = null; // readable stream to serial device
28+
this.rl = null; // readline interface for readstream
29+
this.rate = 115200; // assumed fixed serial rate (bits per second)
30+
31+
// callback closures
32+
this.this_devRemoved = this.devRemoved.bind(this);
33+
this.this_gotTag = this.gotTag.bind(this);
34+
35+
this.matron.on("devRemoved", this.this_devRemoved);
36+
37+
this.init();
38+
};
39+
40+
41+
CornellTagXCVR.prototype.devRemoved = function(dev) {
42+
if (dev.path != this.dev.path)
43+
return;
44+
if (this.rs) {
45+
this.rs.close();
46+
this.rs = null;
47+
}
48+
};
49+
50+
CornellTagXCVR.prototype.init = function() {
51+
// open the device fd
52+
53+
try {
54+
this.rs = Fs.createReadStream(this.dev.path);
55+
this.rl = readline.createInterface({
56+
input: this.rs,
57+
terminal: false
58+
});
59+
this.rl.on("line", this.this_gotTag);
60+
//console.log('Starting read stream at', this.dev.path);
61+
} catch (e) {
62+
// not sure what to do here
63+
console.log("Failed to open CornellTagXCVR at " + this.dev.path + "\n");
64+
console.log(e);
65+
}
66+
};
67+
68+
CornellTagXCVR.prototype.gotTag = function(record) {
69+
//console.log("Got CTT line: " + record)
70+
var vals = record.split(',')
71+
var tag = vals.shift(); // Tag ID should be the first record
72+
// emit all detections at once
73+
if (tag) {
74+
var now = new Date();
75+
var now_secs = now.getTime() / 1000;
76+
77+
var rssi = vals.shift(); // RSSI should be the second record
78+
var lifetag_record = ['T'+this.dev.attr.port, now.toISOString(), tag, rssi].join(',') // build the values to generate a CSV row
79+
this.matron.emit('gotTag', lifetag_record+'\n');
80+
// an event can be emitted here for vahdata if interested in streaming to 'all' files
81+
// this.matron.emit('vahData', lifetag_record+'\n');
82+
LifetagOut.write(lifetag_record+'\n');
83+
}
84+
};
85+
86+
exports.CornellTagXCVR = CornellTagXCVR;

src/main.js

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,6 @@ Machine = require('./machine.js');
4343
Matron = require('./matron.js');
4444
TheMatron = new Matron.Matron();
4545

46-
// Figure out the tag database file location, iterating through a number of locations
47-
tdb_locs = [
48-
"/data/config/SG_tag_database.sqlite", // new preferred location
49-
"/data/config/SG_tag_database.csv",
50-
]
51-
TheMatron.tagDBFile = tdb_locs[tdb_locs.length-1] // default/fall-back location
52-
for (const f of tdb_locs) {
53-
if (Fs.existsSync(f)) {
54-
TheMatron.tagDBFile = f
55-
break
56-
}
57-
}
58-
5946
// Load singleton objects
6047
GPS = new (require('./gps.js').GPS) (TheMatron);
6148
HubMan = new (require('./hubman.js').HubMan) (TheMatron, "/dev/sensorgnome");
@@ -66,44 +53,52 @@ Schedule = require('./schedule.js');
6653
Sensor = require('./sensor.js');
6754
USBAudio = require("./usbaudio.js");
6855
RTLSDR = require("./rtlsdr.js");
56+
CornellTagXCVR= require("./cornelltagxcvr.js");
6957

7058
//WavMaker = require('./wavmaker.js');
7159

7260
// Figure out the location of the deployment.txt file
73-
Deployment = new (require("./deployment.js").Deployment) (
74-
[
75-
"/data/config/deployment.txt", // new preferred location
76-
]);
61+
Deployment = new (require("./deployment.js").Deployment)(
62+
[
63+
"/data/config/deployment.txt", // new preferred location
64+
]);
7765

7866
// replace "-" with "_" in deployment short label, so filenames
7967
// use "-" only for delimiting fields
8068

8169
Deployment.shortLabel = Deployment.shortLabel.replace(/-/g,"_");
8270

83-
TagFinder = new (require('./tagfinder.js').TagFinder) (TheMatron,
84-
"/usr/bin/find_tags_unifile",
85-
TheMatron.tagDBFile,
86-
Deployment.module_options.find_tags.params
87-
);
71+
TagFinder = new (require('./tagfinder.js').TagFinder)(
72+
TheMatron,
73+
"/usr/bin/find_tags_unifile",
74+
[
75+
"/data/config/SG_tag_database.sqlite", // new preferred location
76+
"/data/config/SG_tag_database.csv"],
77+
Deployment.module_options.find_tags.params
78+
);
8879

8980
DataSaver = new (require('./datasaver.js').DataSaver) (TheMatron);
9081

9182
SafeStream = require('./safestream.js').SafeStream;
9283

9384
AllOut = new SafeStream(TheMatron, "all", ".txt", 1000000, 3600);
9485

86+
LifetagOut = new SafeStream(TheMatron, "ctt", ".txt", 1000000, 3600);
87+
9588
Uploader = new (require('./uploader.js').Uploader) (TheMatron);
9689

9790
Relay = new (require('./relay.js').Relay) (TheMatron, 59000);
9891

92+
9993
var clockNotSet = true;
10094

10195
function do_nothing(err, stdout, stderr) {
10296
};
10397

10498
TheMatron.on("gotGPSFix", function(fix) {
10599
AllOut.write("G," + fix.time + "," + fix.lat + "," + fix.lon + "," + fix.alt + "\n" );
106-
//ugly hack to set date from gps if gps has fix but system clock not set
100+
LifetagOut.write("G," + fix.time + "," + fix.lat + "," + fix.lon + "," + fix.alt + "\n" );
101+
//ugly hack to set date from gps if gps has fix but system clock not set
107102
if (clockNotSet && (new Date()).getFullYear() < 2013) {
108103
console.log("Trying to set time to " + fix.time + "\n");
109104
ChildProcess.exec("date --utc -s @" + fix.time, do_nothing);

src/matron.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,22 @@ Matron = function() {
2424
Util.inherits(Matron, Events.EventEmitter);
2525

2626
Matron.prototype.devAdded = function(dev) {
27-
//DEBUG: console.log("Got devAdded " + JSON.stringify(dev) + "\n");
27+
console.log("New device found: " + dev.path);
28+
2829
var devPlan = Deployment.lookup(dev.attr.port, dev.attr.type);
2930
if (devPlan) {
30-
//DEBUG: console.log("Got plan " + JSON.stringify(devPlan));
31+
//console.log("Got plan " + JSON.stringify(devPlan));
3132
this.devices[dev.attr.port] = Sensor.getSensor(this, dev, devPlan);
3233
}
34+
35+
// for CornellTagXCVR, we don't require or use a plan
36+
if (dev.attr.type == "CornellTagXCVR") {
37+
this.devices[dev.attr.port] = new CornellTagXCVR.CornellTagXCVR(this, dev, null);
38+
}
3339
};
3440

3541
Matron.prototype.devRemoved = function(dev) {
36-
//DEBUG: console.log("Got devRemoved " + JSON.stringify(dev) + "\n");
42+
console.log("Device removed: " + dev.path);
3743
if (this.devices[dev.attr.port]) {
3844
this.devices[dev.attr.port] = null;
3945
};

src/public/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,16 @@ <h3> What I'm doing now:</h3>
165165
<div id="softwareupdate">
166166
<b> SensorGnome Software Update</b>
167167
<br>
168-
<p><b>Not currently functioning...</b></p>
169-
<form id="updateUploadForm" action="/upload_software_update", method="POST", enctype="multipart/form-data">
168+
<p><i>Not currently functional...</i></p>
169+
<!-- form id="updateUploadForm" action="/upload_software_update", method="POST", enctype="multipart/form-data">
170170
<p> Please browse to the update file (called <i>TBD...</i>):
171171
<input type="file" name="update_archive" size=60></input>
172172
<br>
173173
<br>
174174
<input type="submit" value="Upload this software update to your SensorGnome..."></input>
175175
<br>
176176
<span id="softwareUpdateResults"></span>
177-
</form>
177+
</form -->
178178
<br>
179179
</div>
180180
<div id="rebootSystem">

src/public/javascripts/sensorgnome.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,42 @@ function onNewVahData (data) {
146146
elt[0].scrollTop = top;
147147
};
148148

149-
function onGotTag (data) {
150-
tagBuf = tagBuf.concat(data.split(/\n/));
151-
var line="";
149+
function onGotLifetag(data) {
150+
alert('got lifetag '+data);
151+
}
152+
153+
function checkLifeTag(data) {
154+
var vals = data.split(',');
155+
if (vals.length == 4) {
156+
return {
157+
port: vals[0],
158+
received_at: vals[1].replace(/[ZT]/g, " "),
159+
tag: vals[2],
160+
rssi: parseInt(vals[3])
161+
}
162+
}
163+
return null;
164+
}
165+
166+
function onGotTag(data, lifetag = false) {
152167
var elt = $("#taglog");
153168
var top = elt[0].scrollTop;
154-
while (tagBuf.length) {
155-
var hit = tagBuf.shift().split(/,/);
156-
if (hit.length == 13) {
157-
line += (new Date(parseFloat(hit[1]) * 1000)).toISOString().replace(/[ZT]/g," ").substr(11);
158-
line += "ant " + hit[0] + " " + hit[2] + " + " + hit[3] + " kHz " + hit[5] + " / " + hit[7] + " dB\n";
169+
var line = "";
170+
171+
var lifetag = checkLifeTag(data);
172+
if (lifetag) {
173+
line = lifetag.received_at.replace(/^\S+\s/, "") + 'port ' + lifetag.port.substr(1) +
174+
' ' + lifetag.tag + '@434Mhz ' + lifetag.rssi + 'dB\n';
175+
} else {
176+
tagBuf = tagBuf.concat(data.split(/\n/));
177+
while (tagBuf.length) {
178+
var hit = tagBuf.shift().split(/,/);
179+
if (hit.length == 13) {
180+
let d = new Date(parseFloat(hit[1]) * 1000);
181+
line += d.toISOString().replace(/(^.*T)|Z/g, "") +
182+
" ant " + hit[0] + " " + hit[2] + " + " + hit[3] + "kHz " +
183+
hit[5] + "/" + hit[7] + "dB\n";
184+
}
159185
}
160186
}
161187
elt.append(line);
@@ -277,6 +303,11 @@ function onDevinfo (data) {
277303
txt = "USB audio device: " + d["name"];
278304
txt += '<span id="raw_audio_span' + slot + '"><audio id="raw_audio' + slot + '" src="/raw_audio?dev=' + slot + '&fm=0&random=' + Math.random() +'" preload="none"></audio></span> <button id="raw_audio_button' + slot + '" type="button" onclick="listenToRaw(' + slot + ')">Listen</button>';
279305
break;
306+
307+
case "CornellTagXCVR":
308+
txt = devList[slot]["name"];
309+
break;
310+
280311
default:
281312
txt = "unknown";
282313
}

src/tagfinder.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
emitting gotTag messages.
44
*/
55

6-
function TagFinder(matron, prog, tagFile, params) {
7-
this.matron = matron;
8-
this.prog = prog;
9-
this.tagFile = tagFile;
10-
this.params = params || [];
11-
this.child = null;
12-
this.quitting = false;
13-
this.inDieHandler = false;
6+
function TagFinder(matron, prog, tagFiles, params) {
7+
this.matron = matron;
8+
this.prog = prog;
9+
this.tagFiles = tagFiles;
10+
this.params = params || [];
11+
this.noTagFile = false; // used to print no-tag-file error once
12+
this.child = null;
13+
this.quitting = false;
14+
this.inDieHandler = false;
1415
this.this_spawnChild = this.spawnChild.bind(this);
1516
this.this_quit = this.quit.bind(this);
1617
this.this_childDied = this.childDied.bind(this);
@@ -53,7 +54,29 @@ TagFinder.prototype.spawnChild = function() {
5354
if (this.quitting)
5455
return;
5556

56-
var p = this.params.concat("-c", "8", this.tagFile);
57+
// see whether we can find a tag file
58+
this.matron.tagDBFile = null;
59+
for (let tf of this.tagFiles) {
60+
if (Fs.existsSync(tf)) {
61+
this.matron.tagDBFile = tf;
62+
break;
63+
}
64+
}
65+
66+
// if we have no tag file, print an error once, and then sleep for a bit a retry
67+
if (! this.matron.tagDBFile) {
68+
if (! this.noTagFile) {
69+
console.log("No tag database for tag finder, looking at " + this.tagFiles.join(", "));
70+
this.noTagFile = true;
71+
}
72+
73+
setTimeout(this.this_spawnChild, 10000);
74+
return;
75+
}
76+
this.noTagFile = false;
77+
78+
// launch the tag finder process
79+
var p = this.params.concat("-c", "8", this.matron.tagDBFile);
5780
console.log("Starting ", this.prog, " ", p.join(" "));
5881
var child = ChildProcess.spawn(this.prog, p)
5982
.on("exit", this.this_childDied)

src/webserver.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,15 +179,18 @@ WebServer.prototype.pushPlan = function () {
179179
};
180180

181181
WebServer.prototype.requestedTagDB = function() {
182-
if (this.matron.tagDBFile.match(/sqlite$/))
182+
if (! this.matron.tagDBFile)
183+
this.this_pushTagDB("No tag database file found");
184+
else if (this.matron.tagDBFile.match(/sqlite$/))
183185
ChildProcess.exec("/usr/bin/sqlite3 " + this.matron.tagDBFile + " 'select proj,id,tagFreq,dfreq-1000*(tagFreq-fcdFreq) as dfreq,bi from tags order by proj,tagFreq,id,bi'", this.this_pushTagDB);
184186
else
185187
Fs.readFile(this.matron.tagDBFile, this.this_pushTagDB);
186188
};
187189

188190
WebServer.prototype.pushTagDB = function(err, data) {
189191
if (this.sock) {
190-
var obj = {err: err, file:Path.basename(this.matron.tagDBFile), data:data ? data.toString(): "ERROR"};
192+
let f = this.matron.tagDBFile ? Path.basename(this.matron.tagDBFile) : "";
193+
var obj = {err: err, file: f, data:data ? data.toString(): "ERROR"};
191194
this.sock.emit('tagDB', obj);
192195
}
193196
};
@@ -267,7 +270,7 @@ WebServer.prototype.sendMachineInfo = function () {
267270
};
268271

269272
WebServer.prototype.clientDisconnected = function () {
270-
console.log("Got to client disconnected.\n");
273+
console.log("WS client disconnected");
271274
if (this.sock) {
272275
this.matron.removeListener('gotGPSFix' , this.this_pushGPSFix);
273276
this.matron.removeListener('gotTag' , this.this_pushTag);
@@ -302,7 +305,7 @@ WebServer.prototype.setParamError = function (data) {
302305
// Web Sockets
303306

304307
WebServer.prototype.handleWebConnection = function (socket) {
305-
console.log("host: received connection to push socket");
308+
console.log("WS client connected", socket.handshake.address);
306309
this.sock = socket;
307310
socket.on('disconnect' , this.this_clientDisconnected);
308311
socket.on('error' , this.this_clientDisconnected);

0 commit comments

Comments
 (0)