From 7d93e8a3adad5d0c96c96af38779a3ec5fbda683 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Sat, 10 Jan 2026 18:10:56 -0500 Subject: [PATCH 01/10] implement simple file upload --- notecard/main.go | 27 +++++ notecard/upload.go | 297 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 notecard/upload.go diff --git a/notecard/main.go b/notecard/main.go index d905539..c784915 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -97,6 +97,15 @@ func getFlagGroups() []lib.FlagGroup { lib.GetFlagByName("pcap"), }, }, + { + Name: "upload", + Description: "File Upload", + Flags: []*flag.Flag{ + lib.GetFlagByName("upload"), + lib.GetFlagByName("route"), + lib.GetFlagByName("topic"), + }, + }, { Name: "notefile", Description: "Notefile Management", @@ -236,6 +245,14 @@ func main() { var actionPcap string flag.StringVar(&actionPcap, "pcap", "", "enable PCAP mode and stream packets to output file (required: 'usb' or 'aux')") + // Upload flags - for efficient binary file upload via web.post + var actionUpload string + flag.StringVar(&actionUpload, "upload", "", "upload a file to Notehub via a proxy route using efficient binary transfer") + var actionRoute string + flag.StringVar(&actionRoute, "route", "", "Notehub proxy route alias for upload (required with -upload)") + var actionTopic string + flag.StringVar(&actionTopic, "topic", "", "optional URL path appended to the route (becomes 'name' in web.post)") + // Parse these flags and also the note tool config flags err := lib.FlagParse(true, false) if err != nil { @@ -254,6 +271,12 @@ func main() { lib.PrintGroupedFlags(getFlagGroups(), "notecard") config.Print() fmt.Printf("\n") + fmt.Printf("Upload Usage:\n") + fmt.Printf(" notecard -upload -route \n") + fmt.Printf(" notecard -upload -route -topic \n") + fmt.Printf(" Example: notecard -upload ./data.bin -route MyRoute\n") + fmt.Printf(" Example: notecard -upload ./firmware.bin -route Upload -topic /devices/dev123\n") + fmt.Printf("\n") fmt.Printf("PCAP Usage:\n") fmt.Printf(" notecard -port -pcap -output \n") fmt.Printf(" notecard -port -pcap -portconfig -output \n") @@ -719,6 +742,10 @@ func main() { err = dfuSideload(actionSideload, actionVerbose) } + if err == nil && actionUpload != "" { + err = uploadFile(actionUpload, actionRoute, actionTopic) + } + if err == nil && actionDFUPackage != "" { err = dfuPackage(actionVerbose, actionOutput, actionDFUPackage, flag.Args()) actionRequest = "" diff --git a/notecard/upload.go b/notecard/upload.go new file mode 100644 index 0000000..0dde5c7 --- /dev/null +++ b/notecard/upload.go @@ -0,0 +1,297 @@ +// Copyright 2026 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// upload.go implements efficient binary file uploads to a Notehub proxy route +// using the Notecard's binary buffer (card.binary) and web.post API. +// +// This module serves as a reference implementation for high-performance file +// uploads through the Notecard. It demonstrates best practices for: +// - Querying the Notecard's binary buffer capacity +// - Chunking large files to fit within buffer constraints +// - Using COBS encoding for reliable binary transfer +// - MD5 verification for data integrity +// - Tracking upload progress with offset/total fields +// - Performance monitoring and statistics +// +// The upload process works as follows: +// 1. Query card.binary to determine the Notecard's maximum buffer size +// 2. Read the source file and calculate its total size +// 3. For each chunk that fits in the binary buffer: +// a. COBS-encode the chunk for safe serial transmission +// b. Stage the chunk in the Notecard's binary buffer via card.binary.put +// c. Verify the chunk was received correctly via card.binary +// d. Issue web.post with binary:true to send the chunk to Notehub +// 4. Report per-chunk and cumulative performance statistics to stderr +// +// The content type used is application/octet-stream for binary uploads. + +package main + +import ( + "crypto/md5" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/blues/note-go/notecard" +) + +// uploadFile performs a binary file upload to a Notehub proxy route. +// +// Parameters: +// - filename: Path to the file to upload +// - route: The Notehub proxy route alias (required) +// - topic: Optional URL path appended to the route (becomes "name" in web.post) +// +// The function uploads the file in chunks sized to the Notecard's binary buffer +// capacity. Each chunk is verified via MD5 checksum before transmission to Notehub. +// Progress statistics are written to stderr after each chunk. +// +// Returns an error if the upload fails at any stage. +func uploadFile(filename string, route string, topic string) error { + + // ========================================================================= + // STEP 1: Validate required parameters + // ========================================================================= + // The route parameter is mandatory as it specifies the Notehub proxy route + // that will receive the uploaded data. + if route == "" { + return fmt.Errorf("upload requires -route to be specified") + } + + // ========================================================================= + // STEP 2: Read the file into memory + // ========================================================================= + // We read the entire file upfront to: + // - Fail early if the file doesn't exist or isn't readable + // - Know the total size for progress calculations and offset/total fields + // - Simplify chunk extraction during the upload loop + fileData, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read file '%s': %w", filename, err) + } + + totalSize := len(fileData) + if totalSize == 0 { + return fmt.Errorf("file '%s' is empty", filename) + } + + // Extract just the filename for display purposes (strip directory path) + displayName := filepath.Base(filename) + + fmt.Fprintf(os.Stderr, "uploading '%s' (%d bytes) to route '%s'\n", displayName, totalSize, route) + + // ========================================================================= + // STEP 3: Query the Notecard's binary buffer capacity + // ========================================================================= + // The card.binary request returns information about the Notecard's binary + // buffer, including the maximum size it can accept. This value is fixed + // for a given Notecard type and doesn't change, so we only query it once. + // + // The response includes: + // - max: Maximum number of bytes the binary buffer can hold + // - length: Current number of bytes in the buffer (should be 0) + rsp, err := card.TransactionRequest(notecard.Request{Req: "card.binary"}) + if err != nil { + return fmt.Errorf("failed to query card.binary capacity: %w", err) + } + + binaryMax := int(rsp.Max) + if binaryMax == 0 { + return fmt.Errorf("notecard does not support binary transfers (card.binary returned max=0)") + } + + fmt.Fprintf(os.Stderr, "notecard binary buffer capacity: %d bytes\n", binaryMax) + + // ========================================================================= + // STEP 4: Set content type for binary upload + // ========================================================================= + // The content type application/octet-stream indicates raw binary data. + contentType := "application/octet-stream" + + // ========================================================================= + // STEP 5: Initialize upload state and statistics + // ========================================================================= + offset := 0 // Current byte offset in the file + chunkNumber := 0 // Current chunk number (1-based for display) + totalChunks := (totalSize + binaryMax - 1) / binaryMax // Ceiling division + uploadStartTime := time.Now() + + // ========================================================================= + // STEP 6: Upload loop - process file in chunks + // ========================================================================= + for offset < totalSize { + chunkNumber++ + chunkStartTime := time.Now() + + // --------------------------------------------------------------------- + // 6a: Calculate chunk boundaries + // --------------------------------------------------------------------- + // Determine how many bytes to send in this chunk. The last chunk may + // be smaller than binaryMax if the file size isn't evenly divisible. + chunkSize := binaryMax + remaining := totalSize - offset + if remaining < chunkSize { + chunkSize = remaining + } + + // Extract the chunk data from the file buffer + chunkData := fileData[offset : offset+chunkSize] + + // --------------------------------------------------------------------- + // 6b: Calculate MD5 checksum for this chunk + // --------------------------------------------------------------------- + // The MD5 checksum serves two purposes: + // 1. Verify the chunk was correctly staged in the Notecard's buffer + // 2. Allow Notehub to verify the chunk wasn't corrupted in transit + chunkMD5 := fmt.Sprintf("%x", md5.Sum(chunkData)) + + // --------------------------------------------------------------------- + // 6c: COBS-encode the chunk for serial transmission + // --------------------------------------------------------------------- + // COBS (Consistent Overhead Byte Stuffing) encoding ensures the binary + // data can be safely transmitted over the serial connection without + // conflicting with the newline character used as a packet delimiter. + encodedData, err := notecard.CobsEncode(chunkData, byte('\n')) + if err != nil { + return fmt.Errorf("chunk %d: COBS encoding failed: %w", chunkNumber, err) + } + + // --------------------------------------------------------------------- + // 6d: Stage the chunk in the Notecard's binary buffer + // --------------------------------------------------------------------- + // The card.binary.put request prepares the Notecard to receive binary + // data. The 'cobs' field indicates the size of the COBS-encoded data + // that will follow. + req := notecard.Request{Req: "card.binary.put"} + req.Cobs = int32(len(encodedData)) + + _, err = card.TransactionRequest(req) + if err != nil { + return fmt.Errorf("chunk %d: card.binary.put failed: %w", chunkNumber, err) + } + + // Send the COBS-encoded data followed by a newline delimiter + // The newline signals the end of the binary data to the Notecard + encodedData = append(encodedData, byte('\n')) + err = card.SendBytes(encodedData) + if err != nil { + return fmt.Errorf("chunk %d: SendBytes failed: %w", chunkNumber, err) + } + + // --------------------------------------------------------------------- + // 6e: Verify the chunk was received correctly by the Notecard + // --------------------------------------------------------------------- + // Query card.binary to confirm the Notecard received the expected + // number of bytes. This catches any serial transmission errors before + // we attempt to send to Notehub. + verifyRsp, err := card.TransactionRequest(notecard.Request{Req: "card.binary"}) + if err != nil { + return fmt.Errorf("chunk %d: card.binary verification failed: %w", chunkNumber, err) + } + + if int(verifyRsp.Length) != chunkSize { + return fmt.Errorf("chunk %d: size mismatch - sent %d bytes, notecard received %d bytes", + chunkNumber, chunkSize, verifyRsp.Length) + } + + // --------------------------------------------------------------------- + // 6f: Send the chunk to Notehub via web.post + // --------------------------------------------------------------------- + // Now that the chunk is staged in the Notecard's binary buffer, we + // issue a web.post request to send it to Notehub. Key fields: + // + // - route: The proxy route alias configured in Notehub + // - name: Optional URL path (from -topic flag) + // - binary: true indicates data should come from the binary buffer + // - content: MIME type for the request + // - offset: Byte offset of this chunk within the complete file + // - total: Total size of the complete file + // - status: MD5 checksum of this chunk for verification + // + // The offset/total fields allow the server to reassemble chunks in + // the correct order, regardless of network issues or retries. + webReq := notecard.Request{Req: "web.post"} + webReq.RouteUID = route + webReq.Binary = true + webReq.Content = contentType + webReq.Offset = int32(offset) + webReq.Total = int32(totalSize) + webReq.Status = chunkMD5 + + // Set the 'name' field (URL path appended to the route) + webReq.Name = topic + + // Execute the web.post request (synchronous - waits for response) + webRsp, err := card.TransactionRequest(webReq) + if err != nil { + return fmt.Errorf("chunk %d: web.post failed: %w", chunkNumber, err) + } + + // Check for HTTP-level errors in the response + // The 'result' field contains the HTTP status code from the server + if webRsp.Result != 0 && (webRsp.Result < 200 || webRsp.Result >= 300) { + return fmt.Errorf("chunk %d: server returned HTTP %d", chunkNumber, webRsp.Result) + } + + // --------------------------------------------------------------------- + // 6g: Calculate and display performance statistics + // --------------------------------------------------------------------- + chunkDuration := time.Since(chunkStartTime) + totalDuration := time.Since(uploadStartTime) + + // Calculate throughput for this chunk (bytes per second) + chunkBytesPerSec := float64(chunkSize) / chunkDuration.Seconds() + + // Calculate cumulative progress + bytesCompleted := offset + chunkSize + percentComplete := float64(bytesCompleted) * 100.0 / float64(totalSize) + + // Calculate overall throughput (bytes per second) + overallBytesPerSec := float64(bytesCompleted) / totalDuration.Seconds() + + // Estimate time remaining based on current throughput + bytesRemaining := totalSize - bytesCompleted + var etaStr string + if overallBytesPerSec > 0 && bytesRemaining > 0 { + etaSeconds := float64(bytesRemaining) / overallBytesPerSec + etaStr = fmt.Sprintf("ETA %s", (time.Duration(etaSeconds) * time.Second).Round(time.Second)) + } else { + etaStr = "complete" + } + + // Output one line per chunk to stderr with comprehensive statistics + // Format: chunk X/Y: BYTES bytes (XX.X%) @ XX.X KB/s (avg XX.X KB/s) ETA Xm Xs + fmt.Fprintf(os.Stderr, "chunk %d/%d: %d/%d bytes (%.1f%%) @ %.1f KB/s (avg %.1f KB/s) %s\n", + chunkNumber, + totalChunks, + bytesCompleted, + totalSize, + percentComplete, + chunkBytesPerSec/1024.0, + overallBytesPerSec/1024.0, + etaStr, + ) + + // --------------------------------------------------------------------- + // 6h: Advance to the next chunk + // --------------------------------------------------------------------- + offset += chunkSize + } + + // ========================================================================= + // STEP 7: Upload complete - display summary + // ========================================================================= + totalDuration := time.Since(uploadStartTime) + overallBytesPerSec := float64(totalSize) / totalDuration.Seconds() + + fmt.Fprintf(os.Stderr, "upload complete: %d bytes in %s (%.1f KB/s average)\n", + totalSize, + totalDuration.Round(time.Second), + overallBytesPerSec/1024.0, + ) + + return nil +} From 81947eb1e188d2c7a761fdd5773436804045c02d Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Sat, 10 Jan 2026 23:15:50 -0500 Subject: [PATCH 02/10] add file upload support --- go.mod | 1 + notecard/main.go | 14 +++++++------- notecard/upload.go | 42 +++++++++++++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index f514ac1..a167d6e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/note-cli go 1.15 replace github.com/blues/note-cli/lib => ./lib +replace github.com/blues/note-go => ../hub/note-go // uncomment this for easier testing locally // replace github.com/blues/note-go => ../hub/note-go diff --git a/notecard/main.go b/notecard/main.go index c784915..ce66293 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -103,7 +103,7 @@ func getFlagGroups() []lib.FlagGroup { Flags: []*flag.Flag{ lib.GetFlagByName("upload"), lib.GetFlagByName("route"), - lib.GetFlagByName("topic"), + lib.GetFlagByName("target"), }, }, { @@ -250,8 +250,8 @@ func main() { flag.StringVar(&actionUpload, "upload", "", "upload a file to Notehub via a proxy route using efficient binary transfer") var actionRoute string flag.StringVar(&actionRoute, "route", "", "Notehub proxy route alias for upload (required with -upload)") - var actionTopic string - flag.StringVar(&actionTopic, "topic", "", "optional URL path appended to the route (becomes 'name' in web.post)") + var actionTarget string + flag.StringVar(&actionTarget, "target", "", "optional URL path appended to the route (becomes 'name' in web.post); use [filename] for the uploaded filename") // Parse these flags and also the note tool config flags err := lib.FlagParse(true, false) @@ -273,9 +273,9 @@ func main() { fmt.Printf("\n") fmt.Printf("Upload Usage:\n") fmt.Printf(" notecard -upload -route \n") - fmt.Printf(" notecard -upload -route -topic \n") + fmt.Printf(" notecard -upload -route -target \n") fmt.Printf(" Example: notecard -upload ./data.bin -route MyRoute\n") - fmt.Printf(" Example: notecard -upload ./firmware.bin -route Upload -topic /devices/dev123\n") + fmt.Printf(" Example: notecard -upload ./firmware.bin -route Upload -target /devices/[filename]\n") fmt.Printf("\n") fmt.Printf("PCAP Usage:\n") fmt.Printf(" notecard -port -pcap -output \n") @@ -377,7 +377,7 @@ func main() { fmt.Printf("%s\n", err) break } - if strings.Contains(rsp.Status, note.ErrTransportDisconnected) { + if strings.Contains(rsp.Status, note.StatusTransportDisconnected) { break } fmt.Printf("%s\n", rsp.Status) @@ -743,7 +743,7 @@ func main() { } if err == nil && actionUpload != "" { - err = uploadFile(actionUpload, actionRoute, actionTopic) + err = uploadFile(actionUpload, actionRoute, actionTarget) } if err == nil && actionDFUPackage != "" { diff --git a/notecard/upload.go b/notecard/upload.go index 0dde5c7..6e38c3c 100644 --- a/notecard/upload.go +++ b/notecard/upload.go @@ -33,24 +33,30 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/blues/note-go/notecard" ) +// maxUploadChunkBytes is the maximum chunk size we'll use for uploads, +// regardless of what the Notecard reports as its buffer capacity. +const maxUploadChunkBytes = 131072 + // uploadFile performs a binary file upload to a Notehub proxy route. // // Parameters: // - filename: Path to the file to upload // - route: The Notehub proxy route alias (required) -// - topic: Optional URL path appended to the route (becomes "name" in web.post) +// - target: Optional URL path appended to the route (becomes "name" in web.post); +// if it contains "[filename]", that substring is replaced with the uploaded filename // // The function uploads the file in chunks sized to the Notecard's binary buffer // capacity. Each chunk is verified via MD5 checksum before transmission to Notehub. // Progress statistics are written to stderr after each chunk. // // Returns an error if the upload fails at any stage. -func uploadFile(filename string, route string, topic string) error { +func uploadFile(filename string, route string, target string) error { // ========================================================================= // STEP 1: Validate required parameters @@ -81,6 +87,11 @@ func uploadFile(filename string, route string, topic string) error { // Extract just the filename for display purposes (strip directory path) displayName := filepath.Base(filename) + // Substitute [filename] placeholder in target with the actual filename + if strings.Contains(target, "[filename]") { + target = strings.ReplaceAll(target, "[filename]", displayName) + } + fmt.Fprintf(os.Stderr, "uploading '%s' (%d bytes) to route '%s'\n", displayName, totalSize, route) // ========================================================================= @@ -89,11 +100,13 @@ func uploadFile(filename string, route string, topic string) error { // The card.binary request returns information about the Notecard's binary // buffer, including the maximum size it can accept. This value is fixed // for a given Notecard type and doesn't change, so we only query it once. + // Note that the "reset" is essential so that it terminates any previous + // binary upload that may still be in progress from the notecard's perspective. // // The response includes: // - max: Maximum number of bytes the binary buffer can hold // - length: Current number of bytes in the buffer (should be 0) - rsp, err := card.TransactionRequest(notecard.Request{Req: "card.binary"}) + rsp, err := card.TransactionRequest(notecard.Request{Req: "card.binary", Reset: true}) if err != nil { return fmt.Errorf("failed to query card.binary capacity: %w", err) } @@ -103,7 +116,13 @@ func uploadFile(filename string, route string, topic string) error { return fmt.Errorf("notecard does not support binary transfers (card.binary returned max=0)") } - fmt.Fprintf(os.Stderr, "notecard binary buffer capacity: %d bytes\n", binaryMax) + // Use the smaller of the notecard's buffer capacity or our configured max + chunkMax := binaryMax + if maxUploadChunkBytes < chunkMax { + chunkMax = maxUploadChunkBytes + } + + fmt.Fprintf(os.Stderr, "notecard binary buffer capacity: %d bytes, using chunk size: %d bytes\n", binaryMax, chunkMax) // ========================================================================= // STEP 4: Set content type for binary upload @@ -114,9 +133,9 @@ func uploadFile(filename string, route string, topic string) error { // ========================================================================= // STEP 5: Initialize upload state and statistics // ========================================================================= - offset := 0 // Current byte offset in the file - chunkNumber := 0 // Current chunk number (1-based for display) - totalChunks := (totalSize + binaryMax - 1) / binaryMax // Ceiling division + offset := 0 // Current byte offset in the file + chunkNumber := 0 // Current chunk number (1-based for display) + totalChunks := (totalSize + chunkMax - 1) / chunkMax // Ceiling division uploadStartTime := time.Now() // ========================================================================= @@ -130,8 +149,8 @@ func uploadFile(filename string, route string, topic string) error { // 6a: Calculate chunk boundaries // --------------------------------------------------------------------- // Determine how many bytes to send in this chunk. The last chunk may - // be smaller than binaryMax if the file size isn't evenly divisible. - chunkSize := binaryMax + // be smaller than chunkMax if the file size isn't evenly divisible. + chunkSize := chunkMax remaining := totalSize - offset if remaining < chunkSize { chunkSize = remaining @@ -222,7 +241,7 @@ func uploadFile(filename string, route string, topic string) error { webReq.Status = chunkMD5 // Set the 'name' field (URL path appended to the route) - webReq.Name = topic + webReq.Name = target // Execute the web.post request (synchronous - waits for response) webRsp, err := card.TransactionRequest(webReq) @@ -232,7 +251,8 @@ func uploadFile(filename string, route string, topic string) error { // Check for HTTP-level errors in the response // The 'result' field contains the HTTP status code from the server - if webRsp.Result != 0 && (webRsp.Result < 200 || webRsp.Result >= 300) { + // Note: 1xx (informational) and 2xx (success) responses are acceptable + if webRsp.Result >= 300 { return fmt.Errorf("chunk %d: server returned HTTP %d", chunkNumber, webRsp.Result) } From d709f41a88ffa3e73232b83c75e1d12ea882beab Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Sun, 11 Jan 2026 19:07:04 -0500 Subject: [PATCH 03/10] make max bytes optional --- notecard/upload.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/notecard/upload.go b/notecard/upload.go index 6e38c3c..a1597d7 100644 --- a/notecard/upload.go +++ b/notecard/upload.go @@ -41,7 +41,11 @@ import ( // maxUploadChunkBytes is the maximum chunk size we'll use for uploads, // regardless of what the Notecard reports as its buffer capacity. -const maxUploadChunkBytes = 131072 +// The only reason to lower this below the Notecard's capacity (which +// is about 250KB for a v2 (black) Notecard) is if the communications +// between the host and notecard might be slow and you want to reduce +// the size of each individual COBS transfer for host responsiveness. +const maxUploadChunkBytes = 0 // uploadFile performs a binary file upload to a Notehub proxy route. // @@ -118,7 +122,7 @@ func uploadFile(filename string, route string, target string) error { // Use the smaller of the notecard's buffer capacity or our configured max chunkMax := binaryMax - if maxUploadChunkBytes < chunkMax { + if maxUploadChunkBytes != 0 && maxUploadChunkBytes < chunkMax { chunkMax = maxUploadChunkBytes } From 85ff283ebe1e14f2e07553a462eb0e481a563e83 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Mon, 12 Jan 2026 17:11:41 -0500 Subject: [PATCH 04/10] retry errors --- notecard/upload.go | 171 ++++++++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 73 deletions(-) diff --git a/notecard/upload.go b/notecard/upload.go index a1597d7..a61f7b0 100644 --- a/notecard/upload.go +++ b/notecard/upload.go @@ -36,6 +36,7 @@ import ( "strings" "time" + "github.com/blues/note-go/note" "github.com/blues/note-go/notecard" ) @@ -183,81 +184,105 @@ func uploadFile(filename string, route string, target string) error { } // --------------------------------------------------------------------- - // 6d: Stage the chunk in the Notecard's binary buffer + // 6d-6f: Transfer binary and send via web.post with retry logic // --------------------------------------------------------------------- - // The card.binary.put request prepares the Notecard to receive binary - // data. The 'cobs' field indicates the size of the COBS-encoded data - // that will follow. - req := notecard.Request{Req: "card.binary.put"} - req.Cobs = int32(len(encodedData)) - - _, err = card.TransactionRequest(req) - if err != nil { - return fmt.Errorf("chunk %d: card.binary.put failed: %w", chunkNumber, err) - } - - // Send the COBS-encoded data followed by a newline delimiter - // The newline signals the end of the binary data to the Notecard - encodedData = append(encodedData, byte('\n')) - err = card.SendBytes(encodedData) - if err != nil { - return fmt.Errorf("chunk %d: SendBytes failed: %w", chunkNumber, err) - } - - // --------------------------------------------------------------------- - // 6e: Verify the chunk was received correctly by the Notecard - // --------------------------------------------------------------------- - // Query card.binary to confirm the Notecard received the expected - // number of bytes. This catches any serial transmission errors before - // we attempt to send to Notehub. - verifyRsp, err := card.TransactionRequest(notecard.Request{Req: "card.binary"}) - if err != nil { - return fmt.Errorf("chunk %d: card.binary verification failed: %w", chunkNumber, err) - } - - if int(verifyRsp.Length) != chunkSize { - return fmt.Errorf("chunk %d: size mismatch - sent %d bytes, notecard received %d bytes", - chunkNumber, chunkSize, verifyRsp.Length) - } - - // --------------------------------------------------------------------- - // 6f: Send the chunk to Notehub via web.post - // --------------------------------------------------------------------- - // Now that the chunk is staged in the Notecard's binary buffer, we - // issue a web.post request to send it to Notehub. Key fields: - // - // - route: The proxy route alias configured in Notehub - // - name: Optional URL path (from -topic flag) - // - binary: true indicates data should come from the binary buffer - // - content: MIME type for the request - // - offset: Byte offset of this chunk within the complete file - // - total: Total size of the complete file - // - status: MD5 checksum of this chunk for verification + // This section handles both binary transfer to the Notecard and the + // subsequent web.post to Notehub. Both operations have retry logic: + // - Binary transfer errors ({bad-bin}, {io}) retry indefinitely + // - web.post errors wait 15 seconds, re-transfer binary, and retry // - // The offset/total fields allow the server to reassemble chunks in - // the correct order, regardless of network issues or retries. - webReq := notecard.Request{Req: "web.post"} - webReq.RouteUID = route - webReq.Binary = true - webReq.Content = contentType - webReq.Offset = int32(offset) - webReq.Total = int32(totalSize) - webReq.Status = chunkMD5 - - // Set the 'name' field (URL path appended to the route) - webReq.Name = target - - // Execute the web.post request (synchronous - waits for response) - webRsp, err := card.TransactionRequest(webReq) - if err != nil { - return fmt.Errorf("chunk %d: web.post failed: %w", chunkNumber, err) - } - - // Check for HTTP-level errors in the response - // The 'result' field contains the HTTP status code from the server - // Note: 1xx (informational) and 2xx (success) responses are acceptable - if webRsp.Result >= 300 { - return fmt.Errorf("chunk %d: server returned HTTP %d", chunkNumber, webRsp.Result) + // We use a labeled loop so web.post failures can restart the entire + // chunk upload process (binary transfer + web.post). + + encodedDataWithNewline := append(encodedData, byte('\n')) + + chunkRetry: + for { + // ----------------------------------------------------------------- + // Binary transfer with retry on {bad-bin}/{io} errors + // ----------------------------------------------------------------- + for { + // Stage the chunk in the Notecard's binary buffer + req := notecard.Request{Req: "card.binary.put"} + req.Cobs = int32(len(encodedData)) + + _, err = card.TransactionRequest(req) + if err != nil { + if note.ErrorContains(err, note.ErrCardIo) { + fmt.Fprintf(os.Stderr, "binary transfer error, retrying: %s\n", err) + continue + } + return fmt.Errorf("chunk %d: card.binary.put failed: %w", chunkNumber, err) + } + + // Send the COBS-encoded data followed by a newline delimiter + err = card.SendBytes(encodedDataWithNewline) + if err != nil { + if note.ErrorContains(err, note.ErrCardIo) { + fmt.Fprintf(os.Stderr, "binary transfer error, retrying: %s\n", err) + continue + } + return fmt.Errorf("chunk %d: SendBytes failed: %w", chunkNumber, err) + } + + // Verify the chunk was received correctly by the Notecard + verifyRsp, err := card.TransactionRequest(notecard.Request{Req: "card.binary"}) + if err != nil { + if note.ErrorContains(err, note.ErrCardIo) { + fmt.Fprintf(os.Stderr, "binary transfer error, retrying: %s\n", err) + continue + } + return fmt.Errorf("chunk %d: card.binary verification failed: %w", chunkNumber, err) + } + + // Check for error in response (e.g., "binary receive prematurely terminated {bad-bin}{io}") + if verifyRsp.Err != "" { + if strings.Contains(verifyRsp.Err, "{bad-bin}") || strings.Contains(verifyRsp.Err, "{io}") { + fmt.Fprintf(os.Stderr, "binary transfer error, retrying: %s\n", verifyRsp.Err) + continue + } + return fmt.Errorf("chunk %d: card.binary error: %s", chunkNumber, verifyRsp.Err) + } + + // Verify size matches + if int(verifyRsp.Length) != chunkSize { + fmt.Fprintf(os.Stderr, "chunk %d: size mismatch (sent %d, received %d), retrying\n", + chunkNumber, chunkSize, verifyRsp.Length) + continue + } + + break // Binary transfer successful + } + + // ----------------------------------------------------------------- + // Send the chunk to Notehub via web.post + // ----------------------------------------------------------------- + // Now that the chunk is staged in the Notecard's binary buffer, we + // issue a web.post request to send it to Notehub. + webReq := notecard.Request{Req: "web.post"} + webReq.RouteUID = route + webReq.Binary = true + webReq.Content = contentType + webReq.Offset = int32(offset) + webReq.Total = int32(totalSize) + webReq.Status = chunkMD5 + webReq.Name = target + + webRsp, err := card.TransactionRequest(webReq) + if err != nil { + fmt.Fprintf(os.Stderr, "web.post failed: %s, waiting 15s then retrying\n", err) + time.Sleep(15 * time.Second) + continue chunkRetry // Re-transfer binary and retry web.post + } + + // Check for HTTP-level errors (3xx, 4xx, 5xx) + if webRsp.Result >= 300 { + fmt.Fprintf(os.Stderr, "server returned HTTP %d, waiting 15s then retrying\n", webRsp.Result) + time.Sleep(15 * time.Second) + continue chunkRetry // Re-transfer binary and retry web.post + } + + break chunkRetry // Chunk upload successful } // --------------------------------------------------------------------- From 12e356e1c832258aa1d2f2afdcd05059bd279421 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Tue, 13 Jan 2026 07:59:22 -0500 Subject: [PATCH 05/10] filename handling --- notecard/upload.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/notecard/upload.go b/notecard/upload.go index a61f7b0..1e98689 100644 --- a/notecard/upload.go +++ b/notecard/upload.go @@ -53,8 +53,7 @@ const maxUploadChunkBytes = 0 // Parameters: // - filename: Path to the file to upload // - route: The Notehub proxy route alias (required) -// - target: Optional URL path appended to the route (becomes "name" in web.post); -// if it contains "[filename]", that substring is replaced with the uploaded filename +// - target: Optional URL path appended to the route (becomes "name" in web.post) // // The function uploads the file in chunks sized to the Notecard's binary buffer // capacity. Each chunk is verified via MD5 checksum before transmission to Notehub. @@ -92,11 +91,6 @@ func uploadFile(filename string, route string, target string) error { // Extract just the filename for display purposes (strip directory path) displayName := filepath.Base(filename) - // Substitute [filename] placeholder in target with the actual filename - if strings.Contains(target, "[filename]") { - target = strings.ReplaceAll(target, "[filename]", displayName) - } - fmt.Fprintf(os.Stderr, "uploading '%s' (%d bytes) to route '%s'\n", displayName, totalSize, route) // ========================================================================= @@ -267,6 +261,7 @@ func uploadFile(filename string, route string, target string) error { webReq.Total = int32(totalSize) webReq.Status = chunkMD5 webReq.Name = target + webReq.Label = displayName webRsp, err := card.TransactionRequest(webReq) if err != nil { From ce81d765eb86708cf59778b969dfa529c1580eeb Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Tue, 13 Jan 2026 11:44:47 -0500 Subject: [PATCH 06/10] error reporting --- notecard/upload.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/notecard/upload.go b/notecard/upload.go index 1e98689..d0c9647 100644 --- a/notecard/upload.go +++ b/notecard/upload.go @@ -272,7 +272,20 @@ func uploadFile(filename string, route string, target string) error { // Check for HTTP-level errors (3xx, 4xx, 5xx) if webRsp.Result >= 300 { - fmt.Fprintf(os.Stderr, "server returned HTTP %d, waiting 15s then retrying\n", webRsp.Result) + errMsg := webRsp.Err + if errMsg == "" { + // Try to extract error from response body + if webRsp.Body != nil { + if errField, ok := (*webRsp.Body)["err"].(string); ok && errField != "" { + errMsg = errField + } + } + } + if errMsg != "" { + fmt.Fprintf(os.Stderr, "server returned HTTP %d: \"%s\"\nwaiting 15s then retrying\n", webRsp.Result, errMsg) + } else { + fmt.Fprintf(os.Stderr, "server returned HTTP %d, waiting 15s then retrying\n", webRsp.Result) + } time.Sleep(15 * time.Second) continue chunkRetry // Re-transfer binary and retry web.post } From 432106bde8ce8ff0218ea3f07bcc02fa09ddf406 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Tue, 13 Jan 2026 16:35:23 -0500 Subject: [PATCH 07/10] better error reporting --- notecard/upload.go | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/notecard/upload.go b/notecard/upload.go index d0c9647..bfc335c 100644 --- a/notecard/upload.go +++ b/notecard/upload.go @@ -258,11 +258,16 @@ func uploadFile(filename string, route string, target string) error { webReq.Binary = true webReq.Content = contentType webReq.Offset = int32(offset) - webReq.Total = int32(totalSize) webReq.Status = chunkMD5 webReq.Name = target webReq.Label = displayName + // If (and only if) we require more than one chunk, set the total field which + // indicates to the notehub that we want to do segmented upload. + if totalChunks > 1 { + webReq.Total = int32(totalSize) + } + webRsp, err := card.TransactionRequest(webReq) if err != nil { fmt.Fprintf(os.Stderr, "web.post failed: %s, waiting 15s then retrying\n", err) @@ -321,16 +326,20 @@ func uploadFile(filename string, route string, target string) error { // Output one line per chunk to stderr with comprehensive statistics // Format: chunk X/Y: BYTES bytes (XX.X%) @ XX.X KB/s (avg XX.X KB/s) ETA Xm Xs - fmt.Fprintf(os.Stderr, "chunk %d/%d: %d/%d bytes (%.1f%%) @ %.1f KB/s (avg %.1f KB/s) %s\n", - chunkNumber, - totalChunks, - bytesCompleted, - totalSize, - percentComplete, - chunkBytesPerSec/1024.0, - overallBytesPerSec/1024.0, - etaStr, - ) + if totalChunks == 1 { + fmt.Fprintf(os.Stderr, "%d bytes @ %.1f KB/s\n", bytesCompleted, overallBytesPerSec/1024.0) + } else { + fmt.Fprintf(os.Stderr, "chunk %d/%d: %d/%d bytes (%.1f%%) @ %.1f KB/s (avg %.1f KB/s) %s\n", + chunkNumber, + totalChunks, + bytesCompleted, + totalSize, + percentComplete, + chunkBytesPerSec/1024.0, + overallBytesPerSec/1024.0, + etaStr, + ) + } // --------------------------------------------------------------------- // 6h: Advance to the next chunk From e3fdcb3575c1ac915bfc7ab4ef90587132507de4 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Wed, 14 Jan 2026 12:00:43 -0500 Subject: [PATCH 08/10] temporary copy of note-go --- go.mod | 2 +- note-go/.circleci/config.yml | 90 + note-go/.gitignore | 36 + note-go/.vscode/launch.json | 18 + note-go/CODE_OF_CONDUCT.md | 7 + note-go/CONTRIBUTING.md | 64 + note-go/LICENSE | 19 + note-go/README.md | 32 + note-go/build.sh | 38 + note-go/go.mod | 17 + note-go/go.sum | 39 + note-go/note/access.go | 96 + note-go/note/contacts.go | 25 + note-go/note/dfu.go | 55 + note-go/note/errors.go | 349 +++ note-go/note/event.go | 267 +++ note-go/note/message.go | 67 + note-go/note/note.go | 259 +++ note-go/note/notefile.go | 99 + note-go/note/session.go | 161 ++ note-go/note/usage.go | 21 + note-go/note/words.go | 2196 ++++++++++++++++++ note-go/note/words_test.go | 19 + note-go/notecard/cobs.go | 68 + note-go/notecard/cobs_test.go | 29 + note-go/notecard/i2c-unix.go | 176 ++ note-go/notecard/i2c-windows.go | 49 + note-go/notecard/lease.go | 196 ++ note-go/notecard/net.go | 82 + note-go/notecard/notecard.go | 1625 +++++++++++++ note-go/notecard/play.go | 223 ++ note-go/notecard/request.go | 536 +++++ note-go/notecard/serial-default.go | 87 + note-go/notecard/serial-unix.go | 17 + note-go/notecard/serial-windows.go | 24 + note-go/notecard/test.go | 98 + note-go/notecard/trace.go | 106 + note-go/notecard/ua.go | 49 + note-go/notehub/api/billing.go | 15 + note-go/notehub/api/devices.go | 165 ++ note-go/notehub/api/environment_variables.go | 206 ++ note-go/notehub/api/error_defaults.go | 88 + note-go/notehub/api/errors.go | 80 + note-go/notehub/api/events.go | 30 + note-go/notehub/api/fleet.go | 78 + note-go/notehub/api/job.go | 45 + note-go/notehub/api/job_reconciliation.go | 94 + note-go/notehub/api/products.go | 30 + note-go/notehub/api/project.go | 40 + note-go/notehub/api/session.go | 21 + note-go/notehub/auth.go | 340 +++ note-go/notehub/config.go | 8 + note-go/notehub/dbquery.go | 19 + note-go/notehub/request.go | 257 ++ note-go/package.sh | 47 + 55 files changed, 8903 insertions(+), 1 deletion(-) create mode 100644 note-go/.circleci/config.yml create mode 100644 note-go/.gitignore create mode 100644 note-go/.vscode/launch.json create mode 100644 note-go/CODE_OF_CONDUCT.md create mode 100644 note-go/CONTRIBUTING.md create mode 100644 note-go/LICENSE create mode 100644 note-go/README.md create mode 100755 note-go/build.sh create mode 100644 note-go/go.mod create mode 100644 note-go/go.sum create mode 100644 note-go/note/access.go create mode 100644 note-go/note/contacts.go create mode 100644 note-go/note/dfu.go create mode 100644 note-go/note/errors.go create mode 100644 note-go/note/event.go create mode 100644 note-go/note/message.go create mode 100644 note-go/note/note.go create mode 100644 note-go/note/notefile.go create mode 100644 note-go/note/session.go create mode 100644 note-go/note/usage.go create mode 100644 note-go/note/words.go create mode 100644 note-go/note/words_test.go create mode 100644 note-go/notecard/cobs.go create mode 100644 note-go/notecard/cobs_test.go create mode 100644 note-go/notecard/i2c-unix.go create mode 100644 note-go/notecard/i2c-windows.go create mode 100644 note-go/notecard/lease.go create mode 100644 note-go/notecard/net.go create mode 100644 note-go/notecard/notecard.go create mode 100644 note-go/notecard/play.go create mode 100644 note-go/notecard/request.go create mode 100644 note-go/notecard/serial-default.go create mode 100644 note-go/notecard/serial-unix.go create mode 100644 note-go/notecard/serial-windows.go create mode 100644 note-go/notecard/test.go create mode 100644 note-go/notecard/trace.go create mode 100644 note-go/notecard/ua.go create mode 100644 note-go/notehub/api/billing.go create mode 100644 note-go/notehub/api/devices.go create mode 100644 note-go/notehub/api/environment_variables.go create mode 100644 note-go/notehub/api/error_defaults.go create mode 100644 note-go/notehub/api/errors.go create mode 100644 note-go/notehub/api/events.go create mode 100644 note-go/notehub/api/fleet.go create mode 100644 note-go/notehub/api/job.go create mode 100644 note-go/notehub/api/job_reconciliation.go create mode 100644 note-go/notehub/api/products.go create mode 100644 note-go/notehub/api/project.go create mode 100644 note-go/notehub/api/session.go create mode 100644 note-go/notehub/auth.go create mode 100644 note-go/notehub/config.go create mode 100644 note-go/notehub/dbquery.go create mode 100644 note-go/notehub/request.go create mode 100755 note-go/package.sh diff --git a/go.mod b/go.mod index a167d6e..8a6e4f5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/note-cli go 1.15 replace github.com/blues/note-cli/lib => ./lib -replace github.com/blues/note-go => ../hub/note-go +replace github.com/blues/note-go => ./note-go // uncomment this for easier testing locally // replace github.com/blues/note-go => ../hub/note-go diff --git a/note-go/.circleci/config.yml b/note-go/.circleci/config.yml new file mode 100644 index 0000000..2d15f72 --- /dev/null +++ b/note-go/.circleci/config.yml @@ -0,0 +1,90 @@ +# Golang CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-go/ for more details +version: 2 +jobs: + build-unix-and-windows: + docker: + - image: circleci/golang:1.14.4 + working_directory: /go/src/github.com/blues/note-go + steps: + - checkout + - run: export GOOS=linux GOARCH=amd64 ; ./build.sh && ./package.sh + - run: export GOOS=linux GOARCH=arm ; ./build.sh && ./package.sh + - run: export GOOS=windows GOARCH=386 ; ./build.sh && ./package.sh + - run: export GOOS=windows GOARCH=amd64 ; ./build.sh && ./package.sh + - run: find ./build/ -type f + - store_artifacts: + path: ./build/packages/ + - persist_to_workspace: + root: . + paths: + - ./build/packages/ + build-macos: + macos: + xcode: 11.3.0 + steps: + - checkout + - run: pwd + - run: echo $PATH + - run: + name: install go + command: | + curl https://dl.google.com/go/go1.14.4.darwin-amd64.tar.gz | + tar -C "$HOME" -xz # install go to $HOME/go/ + - run: + name: build and package + command: | + export PATH="$PATH:$HOME/go/bin" + ./build.sh + ./package.sh + - store_artifacts: + path: ./build/packages/ + - persist_to_workspace: + root: . + paths: + - ./build/packages/ + publish-github-release: + docker: + - image: cibuilds/github:0.10 + steps: + # We need to do a checkout so the CIRCLE_PROJECT_REPONAME and CIRCLE_SHA1 vars are populated + # for the command below. + - checkout + - attach_workspace: + at: . + - run: ls -l ./build/packages/ + - run: + name: "Publish Release on GitHub" + command: | + ghr -t "${GITHUB_TOKEN}" -u "${CIRCLE_PROJECT_USERNAME}" -r "${CIRCLE_PROJECT_REPONAME}" \ + -c "${CIRCLE_SHA1}" -delete "${CIRCLE_TAG}" ./build/packages/ + # The GITHUB_TOKEN is generated here: https://github.com/settings/tokens for the + # notebot-ci user and securely set here: + # https://app.circleci.com/settings/project/github/blues/note-go/environment-variables + +workflows: + version: 2 + build-and-publish: + jobs: + - build-macos: + filters: + # Because we don't filter out certain branches this code implicitly says `build-macos` + # will run for all builds triggered by a branch push or PR. But in the circle-ci ui we + # chose to only build for PRs here: + # https://app.circleci.com/settings/project/github/blues/note-go/advanced + tags: &PUBLISH_TAG_FILTER_REGEX + # Unlike branch-triggered builds, we do filter down to certain tags. Match v1.2.3 etc. + # i.e. the only tags which can trigger must look like they're tagging a release. + only: /^v\d+\.\d+\.\d+$/ + - build-unix-and-windows: + filters: + tags: *PUBLISH_TAG_FILTER_REGEX + - publish-github-release: + requires: + - build-macos + - build-unix-and-windows + filters: + branches: + ignore: /.*/ + tags: *PUBLISH_TAG_FILTER_REGEX diff --git a/note-go/.gitignore b/note-go/.gitignore new file mode 100644 index 0000000..23930f1 --- /dev/null +++ b/note-go/.gitignore @@ -0,0 +1,36 @@ +# jetbrains IDEs config files +**/.idea +build + +# Production build proucts +./build/ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# auto- generated files # +###################### +*~ +\#*\# +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# VS Code configuration files +.vscode/* +# whitelist +!.vscode/launch.json diff --git a/note-go/.vscode/launch.json b/note-go/.vscode/launch.json new file mode 100644 index 0000000..aee041d --- /dev/null +++ b/note-go/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to ./notecard", + "type": "go", + "request": "attach", + "mode":"local", + "processId": 6968, // Fill this in with the PID of your notecard process. + "remotePath": "/go/src/github.com/blues/note-go", + // Sadly this doesn't work. Even if you set isBackground on the task. + //"preLaunchTask": "Compose w/ Debuggable Noteboard", + }, + ], +} \ No newline at end of file diff --git a/note-go/CODE_OF_CONDUCT.md b/note-go/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..50838cf --- /dev/null +++ b/note-go/CODE_OF_CONDUCT.md @@ -0,0 +1,7 @@ +# Code of conduct + +By participating in this project, you agree to abide by the +[Blues Inc code of conduct][1]. + +[1]: https://blues.github.io/opensource/code-of-conduct + diff --git a/note-go/CONTRIBUTING.md b/note-go/CONTRIBUTING.md new file mode 100644 index 0000000..53f322f --- /dev/null +++ b/note-go/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing to blues/note-go + +We love pull requests from everyone. By participating in this project, you +agree to abide by the Blues Inc [code of conduct]. + +[code of conduct]: https://blues.github.io/opensource/code-of-conduct + +Here are some ways *you* can contribute: + +* by using alpha, beta, and prerelease versions +* by reporting bugs +* by suggesting new features +* by writing or editing documentation +* by writing specifications +* by writing code ( **no patch is too small** : fix typos, add comments, +clean up inconsistent whitespace ) +* by refactoring code +* by closing [issues][] +* by reviewing patches + +[issues]: https://github.com/blues/note-go/issues + +## Submitting an Issue + +* We use the [GitHub issue tracker][issues] to track bugs and features. +* Before submitting a bug report or feature request, check to make sure it + hasn't + already been submitted. +* When submitting a bug report, please include a [Gist][] that includes a stack + trace and any details that may be necessary to reproduce the bug, including + your release version, Go version, and operating system. Ideally, a bug report + should include a pull request with failing specs. + +[gist]: https://gist.github.com/ + +## Cleaning up issues + +* Issues that have no response from the submitter will be closed after 30 days. +* Issues will be closed once they're assumed to be fixed or answered. If the + maintainer is wrong, it can be opened again. +* If your issue is closed by mistake, please understand and explain the issue. + We will happily reopen the issue. + +## Submitting a Pull Request +1. [Fork][fork] the [official repository][repo]. +2. [Create a topic branch.][branch] +3. Implement your feature or bug fix. +4. Add, commit, and push your changes. +5. [Submit a pull request.][pr] + +## Notes +* Please add tests if you changed code. Contributions without tests won't be +* accepted. If you don't know how to add tests, please put in a PR and leave a +* comment asking for help. We love helping! + +[repo]: https://github.com/blues/note-go/tree/master +[fork]: https://help.github.com/articles/fork-a-repo/ +[branch]: +https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/ +[pr]: https://help.github.com/articles/creating-a-pull-request-from-a-fork/ + +Inspired by +https://github.com/thoughtbot/factory_bot/blob/master/CONTRIBUTING.md + diff --git a/note-go/LICENSE b/note-go/LICENSE new file mode 100644 index 0000000..aed1af8 --- /dev/null +++ b/note-go/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Blues Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/note-go/README.md b/note-go/README.md new file mode 100644 index 0000000..83492ff --- /dev/null +++ b/note-go/README.md @@ -0,0 +1,32 @@ +# [Blues Wireless][blues] + +The note-go Go library for communicating with Blues Wireless Notecard via serial or I²C. + +This library allows you to control a Notecard by coding in Go. +Your program may configure Notecard and send Notes to [Notehub.io][notehub]. + +See also: +* [note-c][note-c] for C bindings +* [note-python][note-python] for Python bindings + +## Installing +For all releases, we have compiled the notecard utility for different OS and architectures [here](https://github.com/blues/note-go/releases). +If you don't see your OS and architecture supported, please file an issue and we'll add it to new releases. + +[blues]: https://blues.com +[notehub]: https://notehub.io +[note-arduino]: https://github.com/blues/note-arduino +[note-c]: https://github.com/blues/note-c +[note-go]: https://github.com/blues/note-go +[note-python]: https://github.com/blues/note-python + +## Dependencies +- Install Go and the Go tools [(here)](https://golang.org/doc/install) + +## Compiling the notecard utility +If you want to build the latest, follow the directions below. +```bash +$ cd tools/notecard +$ go get -u . +$ go build . +``` diff --git a/note-go/build.sh b/note-go/build.sh new file mode 100755 index 0000000..6a50482 --- /dev/null +++ b/note-go/build.sh @@ -0,0 +1,38 @@ +#! /usr/bin/env bash +# +# Copyright 2020 Blues Inc. All rights reserved. +# Use of this source code is governed by licenses granted by the +# copyright holder including that found in the LICENSE file. +# +######### Bash Boilerplate ########## +set -euo pipefail # strict mode +readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "$SCRIPT_DIR" # cd to this script's dir +######### End Bash Boilerplate ########## + +# +# note-go build.sh +# +# This script builds all the note-go executables (note, notecard, notehub) by +# looking for any folder containing a main.go and running `go build`. +# +# Parameters: Set $GOOS and $GOARCH to cross compile for different platforms. +# +# Output: Executables are saved in "./build/$GOOS/$GOARCH/" +# + +# Add GOOS and GOARCH to our environment. (and other GO vars we don't need) +eval "$(go env)" + +readonly BUILD_EXE_DIR="$SCRIPT_DIR/build/$GOOS/$GOARCH/" +mkdir -p "$BUILD_EXE_DIR" + +# Build each executable binary +# build_dirs is an array of all the folders which contain a main.go +IFS=$'\r\n' GLOBIGNORE='*' command eval 'build_dirs=($(find . -name main.go -print0 | xargs -0n1 dirname))' +for dir in "${build_dirs[@]}"; do + ( + cd "$dir" + go build ${GO_BUILD_FLAGS:-} -o "$BUILD_EXE_DIR" + ) +done diff --git a/note-go/go.mod b/note-go/go.mod new file mode 100644 index 0000000..7295be5 --- /dev/null +++ b/note-go/go.mod @@ -0,0 +1,17 @@ +module github.com/blues/note-go + +// 2023-02-26 Raspberry Pi apt-get only is updated to 1.15 +go 1.15 + +require ( + github.com/gofrs/flock v0.7.1 + github.com/shirou/gopsutil/v3 v3.21.6 + github.com/stretchr/testify v1.7.0 + go.bug.st/serial v1.6.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + periph.io/x/conn/v3 v3.7.0 + periph.io/x/host/v3 v3.8.0 +) diff --git a/note-go/go.sum b/note-go/go.sum new file mode 100644 index 0000000..b835d1f --- /dev/null +++ b/note-go/go.sum @@ -0,0 +1,39 @@ +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc= +github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shirou/gopsutil/v3 v3.21.6 h1:vU7jrp1Ic/2sHB7w6UNs7MIkn7ebVtTb5D9j45o9VYE= +github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tklauser/go-sysconf v0.3.6 h1:oc1sJWvKkmvIxhDHeKWvZS4f6AW+YcoguSfRF2/Hmo4= +github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY= +go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA= +periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg= +periph.io/x/d2xx v0.1.0/go.mod h1:OflHQcWZ4LDP/2opGYbdXSP/yvWSnHVFO90KRoyobWY= +periph.io/x/host/v3 v3.8.0 h1:T5ojZ2wvnZHGPS4h95N2ZpcCyHnsvH3YRZ1UUUiv5CQ= +periph.io/x/host/v3 v3.8.0/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= diff --git a/note-go/note/access.go b/note-go/note/access.go new file mode 100644 index 0000000..1aec57c --- /dev/null +++ b/note-go/note/access.go @@ -0,0 +1,96 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +// The full URN of a resource for permissioning purposes is: +// app:xxx-xxxx-xxxx-xxxx:dev:xxxxxxxxxxx:file:xxxx + +// ACResourceApp is the app (project) resource, which is the appUID that always begins with this string +const ACResourceApp = "app:" + +// ACResourceApps is the resource for all apps +const ACResourceApps = "app:*" + +// ACResourceDevice is the device resource, which is the deviceUID that always begins with this string +const ACResourceDevice = "dev:" + +// ACResourceDevices is the resource for all devices +const ACResourceDevices = "dev:*" + +// ACResourceNotefile is the notefile resource and its note-level actions, +// which is the notefileID prefixed with this string +const ACResourceNotefile = "file:" + +// ACResourceNotefiles is the resource for all notefiles and all meta-notefile-level actions +const ACResourceNotefiles = "file:*" + +// ACResourceAccount is an account resource, which is the accountUID that always begins with this string +const ACResourceAccount = "account:" + +// ACResourceAccounts is the resource for all accounts and all meta-account-level actions +const ACResourceAccounts = "account:*" + +// ACResourceRoute is an route resource, which is the routeUID that always begins with this string +const ACResourceRoute = "route:" + +// ACResourceRoutes is the resource for all routes and all meta-route-level actions +const ACResourceRoutes = "route:*" + +// ACResourceNotecardFirmwares is the resource for all notecard firmware +const ACResourceNotecardFirmwares = "notecard:*" + +// ACResourceUserFirmwares is the resource for all user firmware +const ACResourceUserFirmwares = "firmware:*" + +// ACResourceSep is the separator for building compound resource names +const ACResourceSep = ":" + +// Entire vocabulary of allowed actions on resources + +// ACActionRead (golint) +const ACActionRead = "read" + +// ACActionUpdate (golint) +const ACActionUpdate = "update" + +// ACActionCreate (golint) +const ACActionCreate = "create" + +// ACActionDelete (golint) +const ACActionDelete = "delete" + +// ACActionMonitor (golint) +const ACActionMonitor = "monitor" + +// Ways of combining actions into one + +// ACActionAnd ensures that all of these actions are allowed +const ACActionAnd = "&" + +// ACActionOr ensures that any of these actions are allowed +const ACActionOr = "|" + +// The entire palette of valid actions, as a comma-separated list + +// ACValidActionsApp are actions allowed on apps +const ACValidActionsApp = "app:create,app:read,app:update,app:delete,app:monitor" + +// ACValidActionsDev are actions allowed on devices +const ACValidActionsDev = "dev:read,dev:update,dev:delete,dev:monitor" + +// ACValidActionsFile are actions allowed on notefiles +const ACValidActionsFile = "file:create,file:read,file:update,file:delete" + +// ACValidActionsAccount are actions allowed on accounts +const ACValidActionsAccount = "account:create,account:read,account:update,account:delete" + +// ACValidActionsRoute are actions allowed on routes +const ACValidActionsRoute = "route:create,route:read,route:update,route:delete" + +// ACValidActionsNotecard are actions allowed on notecard firmware +const ACValidActionsNotecard = "notecard:create,notecard:read,notecard:update,notecard:delete" + +// ACValidActionsFirmware are actions allowed on user firmware +const ACValidActionsFirmware = "firmware:create,firmware:read,firmware:update,firmware:delete" diff --git a/note-go/note/contacts.go b/note-go/note/contacts.go new file mode 100644 index 0000000..5be7645 --- /dev/null +++ b/note-go/note/contacts.go @@ -0,0 +1,25 @@ +package note + +// Contact has the basic contact info structure +// +// NOTE: This structure's underlying storage has been decoupled from the use of +// the structure in business logic. As such, please share any changes to these +// structures with cloud services to ensure that storage and testing frameworks +// are kept in sync with these structures used for business logic +type Contact struct { + Name string `json:"name,omitempty"` + Affiliation string `json:"org,omitempty"` + Role string `json:"role,omitempty"` + Email string `json:"email,omitempty"` +} + +// Contacts has contact info for this app +// +// NOTE: This structure's underlying storage has been decoupled from the use of +// the structure in business logic. As such, please share any changes to these +// structures with cloud services to ensure that storage and testing frameworks +// are kept in sync with these structures used for business logic +type Contacts struct { + Admin *Contact `json:"admin,omitempty"` + Tech *Contact `json:"tech,omitempty"` +} diff --git a/note-go/note/dfu.go b/note-go/note/dfu.go new file mode 100644 index 0000000..0e57a2e --- /dev/null +++ b/note-go/note/dfu.go @@ -0,0 +1,55 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// Package note dfu.go contains DFU-related structures generated/parsed by the notecard +package note + +// DFUState is the state of the DFU in progress +type DFUState struct { + Type string `json:"type,omitempty"` + File string `json:"file,omitempty"` + Length uint32 `json:"length,omitempty"` + CRC32 uint32 `json:"crc32,omitempty"` + MD5 string `json:"md5,omitempty"` + Phase string `json:"mode,omitempty"` + Status string `json:"status,omitempty"` + BeganSecs uint32 `json:"began,omitempty"` + RetryCount uint32 `json:"retry,omitempty"` + ConsecutiveErrors uint32 `json:"errors,omitempty"` + BinaryRetries uint32 `json:"binretry,omitempty"` + DFUStartCount uint32 `json:"dfu_started,omitempty"` + DFUCompletedCount uint32 `json:"dfu_completed,omitempty"` + ODFUStartedCount uint32 `json:"odfu_started,omitempty"` + ODFUTarget string `json:"odfu_target,omitempty"` + ReadFromService uint32 `json:"read,omitempty"` + UpdatedSecs uint32 `json:"updated,omitempty"` + DownloadComplete bool `json:"dl_complete,omitempty"` + DisabledReason string `json:"disabled,omitempty"` + MinNotecardVersion string `json:"min_card_version,omitempty"` + + // This will always point to the current running version + Version string `json:"version,omitempty"` +} + +// DFUEnv is the data structure passed to Notehub when DFU info changes +type DFUEnv struct { + Card *DFUState `json:"card,omitempty"` + User *DFUState `json:"user,omitempty"` + Modem *DFUState `json:"modem,omitempty"` + Star *DFUState `json:"star,omitempty"` +} + +type DfuPhase string + +const ( + DfuPhaseUnknown DfuPhase = "" + DfuPhaseIdle DfuPhase = "idle" + DfuPhaseError DfuPhase = "error" + DfuPhaseDownloading DfuPhase = "downloading" + DfuPhaseSideloading DfuPhase = "sideloading" + DfuPhaseReady DfuPhase = "ready" + DfuPhaseReadyRetry DfuPhase = "ready-retry" + DfuPhaseUpdating DfuPhase = "updating" + DfuPhaseCompleted DfuPhase = "completed" +) diff --git a/note-go/note/errors.go b/note-go/note/errors.go new file mode 100644 index 0000000..87bba25 --- /dev/null +++ b/note-go/note/errors.go @@ -0,0 +1,349 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// Package note errors.go contains programmatically-testable error strings +package note + +import ( + "fmt" + "net/http" + "strings" +) + +// ErrTimeout (golint) +const ErrTimeout = "{timeout}" + +var _ = defineError(ErrTimeout, http.StatusRequestTimeout) + +// ErrInternalTimeout of a notehub-to-notehub transaction (golint) +const ErrInternalTimeout = "{internal-timeout}" + +var _ = defineError(ErrInternalTimeout, http.StatusGatewayTimeout) + +// ErrRouteTimeout of a notehub-to-customer-service transaction (golint) +const ErrRouteTimeout = "{route-timeout}" + +var _ = defineError(ErrRouteTimeout, http.StatusRequestTimeout) + +// ErrClosed (golint) +const ErrClosed = "{closed}" + +var _ = defineError(ErrClosed, http.StatusGone) + +// ErrFileNoExist (golint) +const ErrFileNoExist = "{file-noexist}" + +var _ = defineError(ErrFileNoExist, http.StatusNotFound) + +// ErrNotefileName (golint) +const ErrNotefileName = "{notefile-bad-name}" + +var _ = defineError(ErrNotefileName, http.StatusBadRequest) + +// ErrNotefileInUse (golint) +const ErrNotefileInUse = "{notefile-in-use}" + +var _ = defineError(ErrNotefileInUse, http.StatusConflict) + +// ErrNotefileExists (golint) +const ErrNotefileExists = "{notefile-exists}" + +var _ = defineError(ErrNotefileExists, http.StatusConflict) + +// ErrNotefileNoExist (golint) +const ErrNotefileNoExist = "{notefile-noexist}" + +var _ = defineError(ErrNotefileNoExist, http.StatusNotFound) + +// ErrNotefileQueueDisallowed (golint) +const ErrNotefileQueueDisallowed = "{notefile-queue-disallowed}" + +var _ = defineError(ErrNotefileQueueDisallowed, http.StatusBadRequest) + +// ErrNoteNoExist (golint) +const ErrNoteNoExist = "{note-noexist}" + +var _ = defineError(ErrNoteNoExist, http.StatusNotFound) + +// ErrNoteExists (golint) +const ErrNoteExists = "{note-exists}" + +var _ = defineError(ErrNoteExists, http.StatusConflict) + +// ErrTooManyNotes (golint) +const ErrTooManyNotes = "{too-many-notes}" + +var _ = defineError(ErrTooManyNotes, http.StatusBadRequest) + +// ErrTrackerNoExist (golint) +const ErrTrackerNoExist = "{tracker-noexist}" + +var _ = defineError(ErrTrackerNoExist, http.StatusNotFound) + +// ErrTrackerExists (golint) +const ErrTrackerExists = "{tracker-exists}" + +var _ = defineError(ErrTrackerExists, http.StatusConflict) + +// ErrNetwork (golint) +const ErrNetwork = "{network}" + +var _ = defineError(ErrNetwork, http.StatusServiceUnavailable) + +// ErrRegistrationFailure (golint) +const ErrRegistrationFailure = "{registration-failure}" + +var _ = defineError(ErrRegistrationFailure, http.StatusServiceUnavailable) + +// ErrExtendedNetworkFailure (golint) +const ErrExtendedNetworkFailure = "{extended-network-failure}" + +var _ = defineError(ErrExtendedNetworkFailure, http.StatusServiceUnavailable) + +// ErrExtendedServiceFailure (golint) +const ErrExtendedServiceFailure = "{extended-service-failure}" + +var _ = defineError(ErrExtendedServiceFailure, http.StatusServiceUnavailable) + +// ErrHostUnreachable (golint) +const ErrHostUnreachable = "{host-unreachable}" + +var _ = defineError(ErrHostUnreachable, http.StatusServiceUnavailable) + +// ErrDFUNotReady (golint) +const ErrDFUNotReady = "{dfu-not-ready}" + +var _ = defineError(ErrDFUNotReady, http.StatusServiceUnavailable) + +// ErrDFUInProgress (golint) +const ErrDFUInProgress = "{dfu-in-progress}" + +var _ = defineError(ErrDFUInProgress, http.StatusServiceUnavailable) + +// ErrAuth (golint) +const ErrAuth = "{auth}" + +var _ = defineError(ErrAuth, http.StatusUnauthorized) + +// ErrTicket (golint) +const ErrTicket = "{ticket}" + +var _ = defineError(ErrTicket, http.StatusUnauthorized) + +// ErrHubNoHandler (golint) +const ErrHubNoHandler = "{no-handler}" + +var _ = defineError(ErrHubNoHandler, http.StatusInternalServerError) + +// ErrDeviceNotFound (golint) +const ErrDeviceNotFound = "{device-noexist}" + +var _ = defineError(ErrDeviceNotFound, http.StatusNotFound) + +// ErrDeviceNotSpecified (golint) +const ErrDeviceNotSpecified = "{device-none}" + +var _ = defineError(ErrDeviceNotSpecified, http.StatusBadRequest) + +// ErrDeviceId (golint) +const ErrDeviceId = "{device-id-invalid}" + +var _ = defineError(ErrDeviceId, http.StatusBadRequest) + +// ErrDeviceDisabled (golint) +const ErrDeviceDisabled = "{device-disabled}" + +var _ = defineError(ErrDeviceDisabled, http.StatusBadRequest) + +// ErrProductNotFound (golint) +const ErrProductNotFound = "{product-noexist}" + +var _ = defineError(ErrProductNotFound, http.StatusNotFound) + +// ErrProductNotSpecified (golint) +const ErrProductNotSpecified = "{product-none}" + +var _ = defineError(ErrProductNotSpecified, http.StatusBadRequest) + +// ErrAppNotFound (golint) +const ErrAppNotFound = "{app-noexist}" + +var _ = defineError(ErrAppNotFound, http.StatusNotFound) + +// ErrAppNotSpecified (golint) +const ErrAppNotSpecified = "{app-none}" + +var _ = defineError(ErrAppNotSpecified, http.StatusBadRequest) + +// ErrAppDeleted (golint) +const ErrAppDeleted = "{app-deleted}" + +var _ = defineError(ErrAppDeleted, http.StatusGone) + +// ErrAppExists (golint) +const ErrAppExists = "{app-exists}" + +var _ = defineError(ErrAppExists, http.StatusConflict) + +// ErrFleetNotFound (golint) +const ErrFleetNotFound = "{fleet-noexist}" + +var _ = defineError(ErrFleetNotFound, http.StatusNotFound) + +// ErrCardIo (golint) +const ErrCardIo = "{io}" + +var _ = defineError(ErrCardIo, http.StatusBadGateway) + +// ErrCardHeartbeat (golint) Doesn't seem to be used as a request error +const ErrCardHeartbeat = "{heartbeat}" + +// ErrAccessDenied (golint) +const ErrAccessDenied = "{access-denied}" + +var _ = defineError(ErrAccessDenied, http.StatusForbidden) + +// ErrWebPayload (golint) +const ErrWebPayload = "{web-payload}" + +var _ = defineError(ErrWebPayload, http.StatusBadRequest) + +// ErrHubMode (golint) Unused +const ErrHubMode = "{hub-mode}" + +// ErrTemplateIncompatible (golint) +const ErrTemplateIncompatible = "{template-incompatible}" + +var _ = defineError(ErrTemplateIncompatible, http.StatusBadRequest) + +// ErrSyntax (golint) +const ErrSyntax = "{syntax}" + +var _ = defineError(ErrSyntax, http.StatusBadRequest) + +// ErrIncompatible (golint) +const ErrIncompatible = "{incompatible}" + +var _ = defineError(ErrIncompatible, http.StatusNotAcceptable) + +// ErrReqNotSupported (golint) +const ErrReqNotSupported = "{not-supported}" + +var _ = defineError(ErrReqNotSupported, http.StatusNotImplemented) + +// ErrTooBig (golint) +const ErrTooBig = "{too-big}" + +var _ = defineError(ErrTooBig, http.StatusRequestEntityTooLarge) + +// ErrJson (golint) +const ErrJson = "{not-json}" + +var _ = defineError(ErrJson, http.StatusBadRequest) + +// Status messages returned by the notecard in request.Status +const StatusIdle = "{idle}" +const StatusNtnIdle = "{ntn-idle}" +const StatusTransportConnected = "{connected}" +const StatusTransportDisconnected = "{disconnected}" +const StatusTransportConnecting = "{connecting}" +const StatusTransportConnectFailure = "{connect-failure}" +const StatusTransportConnectedClosed = "{connected-closed}" +const StatusTransportWaitService = "{wait-service}" +const StatusTransportWaitData = "{wait-data}" +const StatusTransportWaitGateway = "{wait-gateway}" +const StatusTransportWaitModule = "{wait-module}" +const StatusGPSInactive = "{gps-inactive}" + +// These are returned from JSONata transforms as special strings to indicate the given behavior +// Used by Smart Fleets and during routing +const ErrAddToFleet = "{add-to-fleet}" +const ErrRemoveFromFleet = "{remove-from-fleet}" +const ErrLeaveFleetAlone = "{leave-fleet-alone}" +const ErrDoNotRoute = "{do-not-route}" + +// These can be sent from Notehub to the notecard to indicate it should pause before reconnecting +// Currently unused +const ErrDeviceDelay5 = "{device-delay-5}" +const ErrDeviceDelay10 = "{device-delay-10}" +const ErrDeviceDelay15 = "{device-delay-15}" +const ErrDeviceDelay20 = "{device-delay-20}" +const ErrDeviceDelay30 = "{device-delay-30}" +const ErrDeviceDelay60 = "{device-delay-60}" + +// ErrorContains tests to see if an error contains an error keyword that we might expect +func ErrorContains(err error, errKeyword string) bool { + if err == nil { + return false + } + return strings.Contains(fmt.Sprintf("%s", err), errKeyword) +} + +var errToHttpStatusMap map[string]int + +func defineError(errKeyword string, httpStatus int) string { + if errToHttpStatusMap == nil { + errToHttpStatusMap = make(map[string]int) + } + errToHttpStatusMap[errKeyword] = httpStatus + return errKeyword +} + +// This scans a response.Err string for known error keywords and returns the appropriate HTTP status code +// If there are multiple error keywords, the first one found is used as the source for the code. +// We choose the first one because that should be the most relevant to the specific failure. +// If no known error keywords are found, we return HTTP 500 Internal Server Error. +func ErrorHttpStatus(errstr string) int { + if errstr == "" { + return http.StatusOK + } + start := strings.Index(errstr, "{") + end := strings.Index(errstr, "}") + if start == -1 || end < start { + // Error message without a keyword. Assume it's an internal server error + return http.StatusInternalServerError + } + errKeyword := errstr[start : end+1] + if status, present := errToHttpStatusMap[errKeyword]; present { + return status + } + return http.StatusInternalServerError +} + +// ErrorClean removes all error keywords from an error string +func ErrorClean(err error) error { + errstr := fmt.Sprintf("%s", err) + for { + left := strings.SplitN(errstr, "{", 2) + if len(left) == 1 { + break + } + errstr = left[0] + b := strings.SplitN(left[1], "}", 2) + if len(b) > 1 { + errstr += strings.TrimPrefix(b[1], " ") + } + } + return fmt.Errorf("%s", errstr) +} + +// ErrorString safely returns a string from any error, returning "" for nil +func ErrorString(err error) string { + if err == nil { + return "" + } + return fmt.Sprintf("%s", err) +} + +// ErrorJSON returns a JSON object with nothing but an error code, and with an optional message +func ErrorJSON(message string, err error) (rspJSON []byte) { + if message == "" { + rspJSON = []byte(fmt.Sprintf("{\"err\":\"%q\"}", err)) + } else if err == nil { + rspJSON = []byte(fmt.Sprintf("{\"err\":\"%q\"}", message)) + } else { + rspJSON = []byte(fmt.Sprintf("{\"err\":\"%q: %q\"}", message, err)) + } + return +} diff --git a/note-go/note/event.go b/note-go/note/event.go new file mode 100644 index 0000000..96c1c48 --- /dev/null +++ b/note-go/note/event.go @@ -0,0 +1,267 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +import ( + "time" +) + +// EventAdd (golint) +const EventAdd = "note.add" + +// EventUpdate (golint) +const EventUpdate = "note.update" + +// EventDelete (golint) +const EventDelete = "note.delete" + +// EventTest (golint) +const EventTest = "test" + +// EventPost (golint) +const EventPost = "post" + +// EventPut (golint) +const EventPut = "put" + +// EventGet (golint) +const EventGet = "get" + +// EventNoAction (golint) +const EventNoAction = "" + +// EventSessionBegin (golint) +const EventSessionBegin = "session.begin" + +// EventSessionEndNotehub (golint) +const EventSessionEnd = "session.end" + +// EventGeolocation (golint) +const EventGeolocation = "device.geolocation" + +// EventTower (golint) +const EventTower = "device.tower" + +// EventSocket (golint) +const EventSocket = "web.socket" + +// EventWebhook (golint) +const EventWebhook = "webhook" + +// Event is the request structure passed to the Notification proc +// +// NOTE: This structure's underlying storage has been decoupled from the use of +// the structure in business logic. As such, please share any changes to these +// structures with cloud services to ensure that storage and testing frameworks +// are kept in sync with these structures used for business logic +type Event struct { + EventUID string `json:"event,omitempty"` + // Indicates whether or not this event is a "platform event" - that is, an event generated automatically + // somewhere in the notecard or notehub largely for administrative purposes that doesn't pertain to either + // implicit or explicit user data. + Platform bool `json:"platform,omitempty"` + // These fields, and only these fields, are regarded as "user data". All + // the rest of the fields are regarded as "metadata". + When int64 `json:"when,omitempty"` + NotefileID string `json:"file,omitempty"` + NoteID string `json:"note,omitempty"` + Body *map[string]interface{} `json:"body,omitempty"` + Payload []byte `json:"payload,omitempty"` + Details *map[string]interface{} `json:"details,omitempty"` + // Metadata + SessionUID string `json:"session,omitempty"` + SessionBegan int64 `json:"session_began,omitempty"` + TLS bool `json:"tls,omitempty"` + Transport string `json:"transport,omitempty"` + Continuous bool `json:"continuous,omitempty"` + BestID string `json:"best_id,omitempty"` + DeviceUID string `json:"device,omitempty"` + DeviceSN string `json:"sn,omitempty"` + ProductUID string `json:"product,omitempty"` + AppUID string `json:"app,omitempty"` + Received float64 `json:"received,omitempty"` + Req string `json:"req,omitempty"` + Error string `json:"err,omitempty"` + Updates int32 `json:"updates,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Sent bool `json:"queued,omitempty"` + Bulk bool `json:"bulk,omitempty"` + BulkReceived float64 `json:"batch_received,omitempty"` + BulkNumber uint32 `json:"batch_number,omitempty"` + BulkTotal uint32 `json:"batch_total,omitempty"` + FirmwareHost string `json:"firmware_host,omitempty"` + FirmwareNotecard string `json:"firmware_notecard,omitempty"` + // This field is ONLY used when we remove the payload for storage reasons, to show the app how large it was + MissingPayloadLength int64 `json:"payload_length,omitempty"` + // Location + BestLocationType string `json:"best_location_type,omitempty"` + BestLocationWhen int64 `json:"best_location_when,omitempty"` + BestLat float64 `json:"best_lat,omitempty"` + BestLon float64 `json:"best_lon,omitempty"` + BestLocation string `json:"best_location,omitempty"` + BestCountry string `json:"best_country,omitempty"` + BestTimeZone string `json:"best_timezone,omitempty"` + Where string `json:"where_olc,omitempty"` + WhereWhen int64 `json:"where_when,omitempty"` + WhereLat float64 `json:"where_lat,omitempty"` + WhereLon float64 `json:"where_lon,omitempty"` + WhereLocation string `json:"where_location,omitempty"` + WhereCountry string `json:"where_country,omitempty"` + WhereTimeZone string `json:"where_timezone,omitempty"` + TowerWhen int64 `json:"tower_when,omitempty"` + TowerLat float64 `json:"tower_lat,omitempty"` + TowerLon float64 `json:"tower_lon,omitempty"` + TowerCountry string `json:"tower_country,omitempty"` + TowerLocation string `json:"tower_location,omitempty"` + TowerTimeZone string `json:"tower_timezone,omitempty"` + TowerID string `json:"tower_id,omitempty"` + TriWhen int64 `json:"tri_when,omitempty"` + TriLat float64 `json:"tri_lat,omitempty"` + TriLon float64 `json:"tri_lon,omitempty"` + TriLocation string `json:"tri_location,omitempty"` + TriCountry string `json:"tri_country,omitempty"` + TriTimeZone string `json:"tri_timezone,omitempty"` + TriPoints int32 `json:"tri_points,omitempty"` + + // Triangulation + Triangulate *map[string]interface{} `json:"triangulate,omitempty"` + // "Routed" environment variables beginning with a "$" prefix + Env *map[string]string `json:"environment,omitempty"` + Status EventRoutingStatus `json:"status,omitempty"` + FleetUIDs *[]string `json:"fleets,omitempty"` + + // ONLY POPULATED FOR EventSessionBegin with info both from notecard and notehub + DeviceSKU string `json:"sku,omitempty"` + DeviceOrderingCode string `json:"ordering_code,omitempty"` + DeviceFirmware int64 `json:"firmware,omitempty"` + Bearer string `json:"bearer,omitempty"` + CellID string `json:"cellid,omitempty"` + Bssid string `json:"bssid,omitempty"` + Ssid string `json:"ssid,omitempty"` + Iccid string `json:"iccid,omitempty"` + Apn string `json:"apn,omitempty"` + Rssi int `json:"rssi,omitempty"` + Sinr int `json:"sinr,omitempty"` + Rsrp int `json:"rsrp,omitempty"` + Rsrq int `json:"rsrq,omitempty"` + Rat string `json:"rat,omitempty"` + Bars uint32 `json:"bars,omitempty"` + Voltage float64 `json:"voltage,omitempty"` + Temp float64 `json:"temp,omitempty"` + Moved int64 `json:"moved,omitempty"` + Orientation string `json:"orientation,omitempty"` + PowerCharging bool `json:"power_charging,omitempty"` + PowerUsb bool `json:"power_usb,omitempty"` + PowerPrimary bool `json:"power_primary,omitempty"` + PowerMahUsed float64 `json:"power_mah,omitempty"` + + // ONLY POPULATED FOR EventSessionEnd because it comes from the notehub + NotehubLastWorkDone int64 `json:"hub_last_work_done,omitempty"` + NotehubDurationSecs int64 `json:"hub_duration_secs,omitempty"` + NotehubEventCount int64 `json:"hub_events_routed,omitempty"` + NotehubRcvdBytes uint32 `json:"hub_rcvd_bytes,omitempty"` + NotehubSentBytes uint32 `json:"hub_sent_bytes,omitempty"` + NotehubTCPSessions uint32 `json:"hub_tcp_sessions,omitempty"` + NotehubTLSSessions uint32 `json:"hub_tls_sessions,omitempty"` + NotehubRcvdNotes uint32 `json:"hub_rcvd_notes,omitempty"` + NotehubSentNotes uint32 `json:"hub_sent_notes,omitempty"` + + // ONLY POPULATED for EventSessionEndNotecard because it comes from the notecard + NotecardRcvdBytes uint32 `json:"card_rcvd_bytes,omitempty"` + NotecardSentBytes uint32 `json:"card_sent_bytes,omitempty"` + NotecardRcvdBytesSecondary uint32 `json:"card_rcvd_bytes_secondary,omitempty"` + NotecardSentBytesSecondary uint32 `json:"card_sent_bytes_secondary,omitempty"` + NotecardTCPSessions uint32 `json:"card_tcp_sessions,omitempty"` + NotecardTLSSessions uint32 `json:"card_tls_sessions,omitempty"` + NotecardRcvdNotes uint32 `json:"card_rcvd_notes,omitempty"` + NotecardSentNotes uint32 `json:"card_sent_notes,omitempty"` +} + +type EventRoutingStatus string + +const ( + EventStatusEmpty EventRoutingStatus = "" + EventStatusSuccess EventRoutingStatus = "success" + EventStatusFailure EventRoutingStatus = "failure" + EventStatusInProgress EventRoutingStatus = "in_progress" +) + +// RouteLogEntry is the log entry used by notification processing +type RouteLogEntry struct { + EventSerial int64 `json:"event,omitempty"` + RouteSerial int64 `json:"route,omitempty"` + Date time.Time `json:"date,omitempty"` + Attn bool `json:"attn,omitempty"` + Status string `json:"status,omitempty"` + Text string `json:"text,omitempty"` + URL string `json:"url,omitempty"` + Source RoutingSource `json:"source,omitempty"` + + // Time in milliseconds that the route took to process + // We're making a simplifying assumption that the route will always + // take at least 1ms. So 0 means we didn't record the duration. + Duration int64 `json:"duration,omitempty"` +} + +type RoutingSource uint8 + +const ( + RoutingSourceUnknown RoutingSource = iota + RoutingSourceNormal + RoutingSourceProxy + RoutingSourceRetry + RoutingSourceManual + RoutingSourceDirect + RoutingSourceTest +) + +// String returns a string representation of the routing source +func (s RoutingSource) String() string { + switch s { + case RoutingSourceUnknown: + return "" // display nothing if no entry/default + case RoutingSourceNormal: + return "Normal Routing" + case RoutingSourceProxy: + return "Web Proxy Request" + case RoutingSourceRetry: + return "Auto-Retry" + case RoutingSourceManual: + return "Manual Reroute" + case RoutingSourceDirect: + return "Direct Routing" //only used for test events, should never show in route logs + case RoutingSourceTest: + return "Test" // only used for tests + default: + return "invalid" + } +} + +// GetAggregateEventStatus returns the status of the event given all +// of the route logs for the event. +// +// The aggregate status is determined by taking the most recent status +// for each route. If any of these are failures then the overall status +// is EventStatusFailure, otherwise it's EventStatusSuccess +func GetAggregateEventStatus(logs []RouteLogEntry) EventRoutingStatus { + if len(logs) == 0 { + return EventStatusEmpty + } + + latest := make(map[int64]RouteLogEntry) + for _, log := range logs { + if val, ok := latest[log.RouteSerial]; !ok || log.Date.After(val.Date) { + latest[log.RouteSerial] = log + } + } + + for _, latestLogEntry := range latest { + if latestLogEntry.Attn { + return EventStatusFailure + } + } + + return EventStatusSuccess +} diff --git a/note-go/note/message.go b/note-go/note/message.go new file mode 100644 index 0000000..70e3aaa --- /dev/null +++ b/note-go/note/message.go @@ -0,0 +1,67 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +// MessageAddress is the network routing information for a message +type MessageAddress struct { + Hub string `json:"hub,omitempty"` + ProductUID string `json:"product,omitempty"` + DeviceUID string `json:"device,omitempty"` + DeviceSN string `json:"sn,omitempty"` + Active uint32 `json:"active,omitempty"` +} + +// MessageContact is the entity sending a message, who may have multiple devices/addresses +type MessageContact struct { + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + StoreTags []string `json:"stags,omitempty"` + Addresses []MessageAddress `json:"addresses,omitempty"` +} + +// Message is the core message data structure. Note that when stored in a map or a note, +// the UID is not present but rather is the map key or noteID. +type Message struct { + UID string `json:"id,omitempty"` + Sent uint32 `json:"sent,omitempty"` + Received uint32 `json:"received,omitempty"` + From MessageContact `json:"from,omitempty"` + To []MessageContact `json:"to,omitempty"` + Tags []string `json:"tags,omitempty"` + StoreTags []string `json:"stags,omitempty"` + ContentType string `json:"type,omitempty"` + Content string `json:"content,omitempty"` + Body *map[string]interface{} `json:"body,omitempty"` +} + +// MessageOutbox is the place from which messages are sent +const MessageOutbox = "messages.qo" + +// MessageInbox is the place into which messages are received +const MessageInbox = "messages.qi" + +// MessageStore is the place where the user retains messages +const MessageStore = "messages.db" + +// ContactStore is the place where the user retains contact info +const ContactStore = "contacts.db" + +// MessageContentASCII is just simple ASCII text +const MessageContentASCII = "" + +// MessageTagImportant indicates that the sender feels that this is an important message +const MessageTagImportant = "important" + +// MessageTagUrgent indicates that the sender feels that this is an urgent message +const MessageTagUrgent = "urgent" + +// MessageSTagSent indicates that this was a sent message +const MessageSTagSent = "sent" + +// MessageSTagReceived indicates that this was a received message +const MessageSTagReceived = "received" + +// ContactOwnerNoteID indicates that this is my contact +const ContactOwnerNoteID = "owner" diff --git a/note-go/note/note.go b/note-go/note/note.go new file mode 100644 index 0000000..6e7689f --- /dev/null +++ b/note-go/note/note.go @@ -0,0 +1,259 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +import ( + "bytes" + "encoding/json" + "math" + "strings" + "time" +) + +// DefaultDeviceEndpointID is the default endpoint name of the edge, chosen for its length in protocol messages +const DefaultDeviceEndpointID = "" + +// DefaultHubEndpointID is the default endpoint name of the hub, chosen for its length in protocol messages +const DefaultHubEndpointID = "1" + +// HubDefaultInboundNotefile is the hard-wired default notefile for user data +const HubDefaultInboundNotefile = "data.qi" + +// HubDefaultOutboundNotefile is the hard-wired default notefile for user data +const HubDefaultOutboundNotefile = "data.qo" + +// Note is the most fundamental data structure, containing +// user data referred to as its "body" and its "payload". All +// access to these fields, and changes to these fields, must +// be done indirectly through the note API. +type Note struct { + Body map[string]interface{} `json:"b,omitempty"` + Payload []byte `json:"p,omitempty"` + Change int64 `json:"c,omitempty"` + Histories *[]History `json:"h,omitempty"` + Conflicts *[]Note `json:"x,omitempty"` + Updates int32 `json:"u,omitempty"` + Deleted bool `json:"d,omitempty"` + Sent bool `json:"s,omitempty"` + Bulk bool `json:"k,omitempty"` + XPOff uint32 `json:"O,omitempty"` + XPLen uint32 `json:"L,omitempty"` + Tower *TowerLocation `json:"T,omitempty"` +} + +// History records the update history, optimized so that if the most recent entry +// is by the same endpoint as an update/delete, that entry is re-used. The primary use +// of History is for conflict detection, and you don't need to detect conflicts +// against yourself. +type History struct { + When int64 `json:"w,omitempty"` + Where string `json:"l,omitempty"` + WhereWhen int64 `json:"m,omitempty"` + EndpointID string `json:"e,omitempty"` + Sequence int32 `json:"s,omitempty"` +} + +// Info is a general "content" structure +type Info struct { + NoteID string `json:"id,omitempty"` + When int64 `json:"time,omitempty"` + WhereLat float64 `json:"lat,omitempty"` + WhereLon float64 `json:"lon,omitempty"` + WhereWhen int64 `json:"ltime,omitempty"` + Body *map[string]interface{} `json:"body,omitempty"` + Payload *[]byte `json:"payload,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Edge bool `json:"edge,omitempty"` + Pending bool `json:"pending,omitempty"` +} + +// CreateNote creates the core data structure for an object, given a JSON body +func CreateNote(body []byte, payload []byte) (newNote Note, err error) { + newNote.Payload = payload + err = newNote.SetBody(body) + return +} + +// SetBody sets the application-supplied Body field of a given Note given some JSON +func (note *Note) SetBody(body []byte) (err error) { + if len(body) == 0 { + note.Body = nil + } else { + note.Body = map[string]interface{}{} + err = JSONUnmarshal(body, ¬e.Body) + if err != nil { + return + } + } + return +} + +// JSONToBody unmarshals the specified object and returns it as a map[string]interface{} +func JSONToBody(bodyJSON []byte) (body map[string]interface{}, err error) { + err = JSONUnmarshal(bodyJSON, &body) + return +} + +// ObjectToJSON Marshals the specified object and returns it as a []byte +func ObjectToJSON(object interface{}) (bodyJSON []byte, err error) { + bodyJSON, err = JSONMarshal(object) + return +} + +// ObjectToBody Marshals the specified object and returns it as map +func ObjectToBody(object interface{}) (body map[string]interface{}, err error) { + var bodyJSON []byte + bodyJSON, err = JSONMarshal(object) + if err == nil { + err = JSONUnmarshal(bodyJSON, &body) + } + return +} + +// BodyToObject Unmarshals the specified map into an object +func BodyToObject(body *map[string]interface{}, object interface{}) (err error) { + if body == nil { + return + } + var bodyJSON []byte + bodyJSON, err = JSONMarshal(body) + if err == nil { + err = JSONUnmarshal(bodyJSON, object) + } + return +} + +// SetPayload sets the application-supplied Payload field of a given Note, +// which must be binary bytes that will ultimately be rendered as base64 in JSON +func (note *Note) SetPayload(payload []byte) { + note.Payload = payload +} + +// Close closes and frees the object on a note { +func (note *Note) Close() { +} + +// Dup duplicates the note +func (note *Note) Dup() Note { + newNote := *note + return newNote +} + +// GetBody retrieves the application-specific Body of a given Note +func (note *Note) GetBody() []byte { + if note.Body == nil { + return []byte("{}") + } + data, err := JSONMarshal(note.Body) + if err != nil { + return []byte("{}") + } + return data +} + +// GetPayload retrieves the Payload from a given Note +func (note *Note) GetPayload() []byte { + return note.Payload +} + +// EndpointID determines the endpoint that last modified the note +func (note *Note) EndpointID() string { + if note.Histories == nil { + return "" + } + histories := *note.Histories + if len(histories) == 0 { + return "" + } + return histories[0].EndpointID +} + +// HasConflicts determines whether or not a given Note has conflicts +func (note *Note) HasConflicts() bool { + if note.Conflicts == nil { + return false + } + return len(*note.Conflicts) != 0 +} + +// GetConflicts fetches the conflicts, so that they may be displayed +func (note *Note) GetConflicts() []Note { + if note.Conflicts == nil { + return []Note{} + } + return *note.Conflicts +} + +// GetWhen retrieves the epoch modification time +func (note *Note) When() (when int64) { + if note.Histories == nil || len(*note.Histories) == 0 { + return 0 + } + h := (*note.Histories)[0] + if h.When < 1483228800 || h.When > math.MaxUint32 { + // Before 1/1/2017 or can't fit into a uint32 + h.When = 0 + } + return h.When +} + +// GetEndpointID retrieves the endpoint that last modified the note +func (note *Note) GetEndpointID() (endpointID string) { + if note.Histories == nil || len(*note.Histories) == 0 { + return "" + } + h := (*note.Histories)[0] + return h.EndpointID +} + +// GetModified retrieves information about the note's modification +func (note *Note) GetModified() (isAvailable bool, endpointID string, when string, where string, updates int32) { + if note.Histories == nil || len(*note.Histories) == 0 { + return + } + histories := *note.Histories + endpointID = histories[0].EndpointID + when = time.Unix(0, histories[0].When*1000000000).UTC().Format("2006-01-02T15:04:05Z") + where = histories[0].Where + updates = histories[0].Sequence + isAvailable = true + return +} + +// JSONUnmarshal uses JSON Numbers, rather than assuming Floats. This fixes an issue +// in which, when decoding to an arbitrary interface, the JSON package decodes +// large numbers (like Unix epoch) into floats. +func JSONUnmarshal(data []byte, v interface{}) (err error) { + d := json.NewDecoder(strings.NewReader(string(data))) + d.UseNumber() + return d.Decode(v) +} + +// JSONMarshal is the equivalent to the json package's Marshal, however it does not escape HTML +// sitting inside JSON strings. +func JSONMarshal(v interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + err := encoder.Encode(v) + clean := bytes.TrimSuffix(buffer.Bytes(), []byte("\n")) + return clean, err +} + +// JSONMarshalIndent is like Marshal but applies Indent to format the output. +// Each JSON element in the output will begin on a new line beginning with prefix +// followed by one or more copies of indent according to the indentation nesting. +func JSONMarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { + b, err := JSONMarshal(v) + if err != nil { + return nil, err + } + var buf bytes.Buffer + err = json.Indent(&buf, b, prefix, indent) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/note-go/note/notefile.go b/note-go/note/notefile.go new file mode 100644 index 0000000..536da93 --- /dev/null +++ b/note-go/note/notefile.go @@ -0,0 +1,99 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +// TrackNotefile is the hard-wired notefile that the notecard can use for tracking the device +const TrackNotefile = "_track.qo" + +// NotecardRequestNotefile is a special notefile for sending notecard requests +const NotecardRequestNotefile = "_req.qis" + +// NotecardResponseNotefile is a special notefile for sending notecard responses +const NotecardResponseNotefile = "_rsp.qos" + +// LogNotefile is the hard-wired notefile that the notecard uses for debug logging +const LogNotefile = "_log.qo" + +// EnvNotefile is the hard-wired notefile that the notecard uses for env vars +const EnvNotefile = "_env.dbs" + +// SessionNotefile is the hard-wired notefile that the notehub uses when starting a session +const SessionNotefile = "_session.qo" + +// HealthNotefile is the hard-wired notefile that the notecard uses for health-related info +const HealthNotefile = "_health.qo" + +// HealthHostNotefile is the hard-wired notefile that the host uses for health-related info +const HealthHostNotefile = "_health_host.qo" + +// GeolocationNotefile is the hard-wired notefile that the notehub uses when performing a geolocation +const GeolocationNotefile = "_geolocate.qo" + +// TowerNotefile is the hard-wired notefile that the notehub uses when performing tower updates +const TowerNotefile = "_tower.qo" + +// SocketNotefile is the hard-wired notefile that the notehub uses when doing websocket I/O +const SocketNotefile = "_socket.qo" + +// WebNotefile is the hard-wired notefile that the notehub uses when performing web requests +const WebNotefile = "_web.qo" + +// WatchdogNotefile is the hard-wired notefile that the notehub uses when adding watchdog messages +const WatchdogNotefile = "_watchdog.qo" + +// SyncPriorityLowest (golint) +const SyncPriorityLowest = -3 + +// SyncPriorityLower (golint) +const SyncPriorityLower = -2 + +// SyncPriorityLow (golint) +const SyncPriorityLow = -1 + +// SyncPriorityNormal (golint) +const SyncPriorityNormal = 0 + +// SyncPriorityHigh (golint) +const SyncPriorityHigh = 1 + +// SyncPriorityHigher (golint) +const SyncPriorityHigher = 2 + +// SyncPriorityHighest (golint) +const SyncPriorityHighest = 3 + +// NotefileInfo has parameters about the Notefile +type NotefileInfo struct { + // The count of modified notes in this notefile. This is used in the Req API, but not in the Notebox info + Changes int `json:"changes,omitempty"` + // The count of total notes in this notefile. This is used in the Req API, but not in the Notebox info + Total int `json:"total,omitempty"` + // This is a unidirectional "to-hub" or "from-hub" endpoint + SyncHubEndpointID string `json:"sync_hub_endpoint,omitempty"` + // Relative positive/negative priority of data, with 0 being normal + SyncPriority int `json:"sync_priority,omitempty"` + // Timed: Target for sync period, if modified and if the value hasn't been synced sooner + SyncPeriodSecs int `json:"sync_secs,omitempty"` + // ReqTime is specified if notes stored in this notefile must have a valid time associated with them + ReqTime bool `json:"req_time,omitempty"` + // ReqLoc is specified if notes stored in this notefile must have a valid location associated with them + ReqLoc bool `json:"req_loc,omitempty"` + // AnonAddAllowed is specified if anyone is allowed to drop into this notefile without authentication + AnonAddAllowed bool `json:"anon_add,omitempty"` + // ImportTime is the epoch time of when an external data source (such as a feed) last sync'ed data inbound + ImportTime int64 `json:"import_time,omitempty"` + // ExportTime is the epoch time of when an external data source (such as a feed) last sync'ed data outbound + ExportTime int64 `json:"export_time,omitempty"` +} + +// Information about notefiles and their templates +type NotefileDesc struct { + NotefileID string `json:"file,omitempty"` + Info NotefileInfo `json:"info,omitempty"` + BodyTemplate string `json:"body_template,omitempty"` + PayloadTemplate uint32 `json:"payload_template,omitempty"` + TemplateFormat uint32 `json:"template_format,omitempty"` + TemplatePort uint16 `json:"template_port,omitempty"` +} diff --git a/note-go/note/session.go b/note-go/note/session.go new file mode 100644 index 0000000..ded8c58 --- /dev/null +++ b/note-go/note/session.go @@ -0,0 +1,161 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +// DeviceSession is the basic unit of recorded device usage history +type DeviceSession struct { + // Session ID that can be mapped to the events created during that session + SessionUID string `json:"session,omitempty"` + // When the session was initially opened + SessionBegan int64 `json:"session_began,omitempty"` + // When a persistent session was last updated + SessionUpdated int64 `json:"session_updated,omitempty"` + // Why a session was opened + WhySessionOpened string `json:"why_session_opened,omitempty"` + // When the session was initially opened + SessionEnded int64 `json:"session_ended,omitempty"` + // Why the session was closed + WhySessionClosed string `json:"why_session_closed,omitempty"` + // Log key for this session + SessionLogKey string `json:"session_log_key,omitempty"` + // Info from the device structure + DeviceUID string `json:"device,omitempty"` + DeviceSN string `json:"sn,omitempty"` + ProductUID string `json:"product,omitempty"` + FleetUIDs []string `json:"fleets,omitempty"` + // Protocol:IP:port address of the handler serving the session + Handler string `json:"handler,omitempty"` + // Cell ID where the session originated and quality ("mcc,mnc,lac,cellid") + CellID string `json:"cell,omitempty"` + // Elevation of cell tower if known + Elevation float64 `json:"elevation,omitempty"` + // Parameters passed by device as a result of scanning towers/APs + ScanResults *[]byte `json:"scan,omitempty"` + Triangulate *map[string]interface{} `json:"triangulate,omitempty"` + // Network connection information sent by the notecard + Rssi int `json:"rssi,omitempty"` + Sinr int `json:"sinr,omitempty"` + Rsrp int `json:"rsrp,omitempty"` + Rsrq int `json:"rsrq,omitempty"` + Bars int `json:"bars,omitempty"` + Rat string `json:"rat,omitempty"` + Bearer string `json:"bearer,omitempty"` + Ip string `json:"ip,omitempty"` + Bssid string `json:"bssid,omitempty"` + Ssid string `json:"ssid,omitempty"` + Iccid string `json:"iccid,omitempty"` + Apn string `json:"apn,omitempty"` + // Composed by wire.go for use in Request.Transport && Event.Transport + Transport string `json:"transport,omitempty"` + // Last known tower and triangulated location as determined at the start of session + Tower TowerLocation `json:"tower,omitempty"` + Tri TowerLocation `json:"tri,omitempty"` + // Last known capture time of a note routed through this session + When int64 `json:"when,omitempty"` + // Last known GPS location of a note routed through this session + WhereWhen int64 `json:"where_when,omitempty"` + WhereOLC string `json:"where,omitempty"` + WhereLat float64 `json:"where_lat,omitempty"` + WhereLon float64 `json:"where_lon,omitempty"` + WhereLocation string `json:"where_location,omitempty"` + WhereCountry string `json:"where_country,omitempty"` + WhereTimeZone string `json:"where_timezone,omitempty"` + // Flag indicating whether the usage data is based on actual stats from the device + IsUsageActual bool `json:"usage_actual,omitempty"` + // Physical device info + Voltage float64 `json:"voltage,omitempty"` + Temp float64 `json:"temp,omitempty"` + // Type of session + ContinuousSession bool `json:"continuous,omitempty"` + TLSSession bool `json:"tls,omitempty"` + // For keeping track of when the last work was done for a session + LastWorkDone int64 `json:"work,omitempty"` + // Number of Events routed + EventCount int64 `json:"events,omitempty"` + // Motion of the notecard + Moved int64 `json:"moved,omitempty"` + Orientation string `json:"orientation,omitempty"` + // Last known power stats at start of session + HighPowerSecsTotal uint32 `json:"hp_secs_total,omitempty"` + HighPowerSecsData uint32 `json:"hp_secs_data,omitempty"` + HighPowerSecsGPS uint32 `json:"hp_secs_gps,omitempty"` + HighPowerCyclesTotal uint32 `json:"hp_cycles_total,omitempty"` + HighPowerCyclesData uint32 `json:"hp_cycles_data,omitempty"` + HighPowerCyclesGPS uint32 `json:"hp_cycles_gps,omitempty"` + // Amount of packet usage within the session, keyed by PSID + PacketUsage map[string]PacketUsage `json:"packet_usage,omitempty"` + // Total device usage at the beginning of the period + ThisPtr *DeviceUsage `json:"this,omitempty"` + // Total device usage at the beginning of the next period, whenever it happens to occur + NextPtr *DeviceUsage `json:"next,omitempty"` + // Usage during the period - initially estimated, but then corrected when we get to the next period + PeriodPtr *DeviceUsage `json:"period,omitempty"` + // NotecardPowerSource flags + PowerCharging bool `json:"power_charging,omitempty"` + PowerUsb bool `json:"power_usb,omitempty"` + PowerPrimary bool `json:"power_primary,omitempty"` + // Mojo power usage + PowerMahUsed float64 `json:"power_mah,omitempty"` + // Information about failed connections PRIOR to this one + PenaltySecs uint32 `json:"penalty_secs,omitempty"` + FailedConnects uint32 `json:"failed_connects,omitempty"` + // Socket-relate + SocketAlias string `json:"socket_alias,omitempty"` + SocketConnectError string `json:"socket_connect_error,omitempty"` + SocketBytesSent int64 `json:"socket_bytes_sent,omitempty"` + SocketBytesRcvd int64 `json:"socket_bytes_rcvd,omitempty"` +} + +func (s *DeviceSession) This() *DeviceUsage { + if s.ThisPtr == nil { + s.ThisPtr = &DeviceUsage{} + } + return s.ThisPtr +} + +func (s *DeviceSession) Next() *DeviceUsage { + if s.NextPtr == nil { + s.NextPtr = &DeviceUsage{} + } + return s.NextPtr +} + +func (s *DeviceSession) Period() *DeviceUsage { + if s.PeriodPtr == nil { + s.PeriodPtr = &DeviceUsage{} + } + return s.PeriodPtr +} + +// Indication of the packet usage within a session +type PacketUsage struct { + Updated int64 `json:"updated,omitempty"` + DownlinkPackets int64 `json:"dl_p,omitempty"` + DownlinkBytes int64 `json:"dl_b,omitempty"` + DownlinkBytesBillable int64 `json:"dl_bb,omitempty"` + UplinkPackets int64 `json:"ul_p,omitempty"` + UplinkBytes int64 `json:"ul_b,omitempty"` + UplinkBytesBillable int64 `json:"ul_bb,omitempty"` + BillableMinBytesPerPacket int64 `json:"bmbpp,omitempty"` +} + +// TowerLocation is a location structure generated by a lookup +type TowerLocation struct { + Source string `json:"source,omitempty"` // source of this location + When int64 `json:"time,omitempty"` // time when this location was ascertained + Name string `json:"n,omitempty"` // name of the location + CountryCode string `json:"c,omitempty"` // country code + Lat float64 `json:"lat,omitempty"` // latitude + Lon float64 `json:"lon,omitempty"` // longitude + TimeZone string `json:"zone,omitempty"` // timezone name + MCC int `json:"mcc,omitempty"` + MNC int `json:"mnc,omitempty"` + LAC int `json:"lac,omitempty"` + CID int `json:"cid,omitempty"` + OLC string `json:"l,omitempty"` // open location code + TimeZoneID int `json:"z,omitempty"` // timezone id (see tz.go) + Deprecated int64 `json:"count,omitempty"` // (no longer used or supported) + Towers int `json:"towers,omitempty"` // number of triangulation points +} diff --git a/note-go/note/usage.go b/note-go/note/usage.go new file mode 100644 index 0000000..8c2ff82 --- /dev/null +++ b/note-go/note/usage.go @@ -0,0 +1,21 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +// DeviceUsage is the device usage metric representing values from the beginning of time, since Provisioned +type DeviceUsage struct { + Since int64 `json:"since,omitempty"` + DurationSecs uint32 `json:"duration,omitempty"` + RcvdBytes uint32 `json:"bytes_rcvd,omitempty"` + SentBytes uint32 `json:"bytes_sent,omitempty"` + RcvdBytesSecondary uint32 `json:"bytes_rcvd_secondary,omitempty"` + SentBytesSecondary uint32 `json:"bytes_sent_secondary,omitempty"` + TCPSessions uint32 `json:"sessions_tcp,omitempty"` + TLSSessions uint32 `json:"sessions_tls,omitempty"` + PacketSessions uint32 `json:"sessions_packet,omitempty"` + WebhookSessions uint32 `json:"sessions_webhook,omitempty"` + RcvdNotes uint32 `json:"notes_rcvd,omitempty"` + SentNotes uint32 `json:"notes_sent,omitempty"` +} diff --git a/note-go/note/words.go b/note-go/note/words.go new file mode 100644 index 0000000..5747e96 --- /dev/null +++ b/note-go/note/words.go @@ -0,0 +1,2196 @@ +// Copyright 2020 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package note + +import ( + "hash/fnv" + "sort" + "strconv" + "strings" + "sync" +) + +// Word index data structure +type Word struct { + WordIndex uint +} + +var ( + sortedWords []Word + sortedWordsInitialized = false + sortedWordsInitLock sync.RWMutex +) + +// Class used to sort an index of words +type byWord []Word + +func (a byWord) Len() int { return len(a) } +func (a byWord) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byWord) Less(i, j int) bool { return words2048[a[i].WordIndex] < words2048[a[j].WordIndex] } + +// WordToNumber converts a single word to a number +func WordToNumber(word string) (num uint, success bool) { + // Initialize sorted words array if necessary + if !sortedWordsInitialized { + sortedWordsInitLock.Lock() + if !sortedWordsInitialized { + + // Init the index array + sortedWords = make([]Word, 2048) + for i := 0; i < 2048; i++ { + sortedWords[i].WordIndex = uint(i) + } + + // Sort the array + sort.Sort(byWord(sortedWords)) + + // We're now initialized + sortedWordsInitialized = true + } + sortedWordsInitLock.Unlock() + } + + // First normalize the word + word = strings.ToLower(word) + + // Do a binary chop to find the word or its insertion slot + i := sort.Search(2048, func(i int) bool { return words2048[sortedWords[i].WordIndex] >= word }) + + // Exit if found. (If we failed to match the result, it's an insertion slot.) + if i < 2048 && words2048[sortedWords[i].WordIndex] == word { + return sortedWords[i].WordIndex, true + } + + return 0, false +} + +// WordsToNumber looks up a number from two or three simple words +func WordsToNumber(words string) (num uint32, found bool) { + var left, middle, right uint + var success bool + + // For convenience, if a number is supplied just return that number. I do this so + // that you can use this same method to parse either a number or the words to get that number. + word := strings.Split(words, "-") + if len(word) == 1 { + + // See if this parses cleanly as a number + i64, err := strconv.ParseUint(words, 10, 32) + if err == nil { + return uint32(i64), true + } + return 0, false + } + + // Convert two or three words to numbers, msb to lsb + if len(word) == 2 { + middle, success = WordToNumber(word[0]) + if !success { + return 0, false + } + right, success = WordToNumber(word[1]) + if !success { + return 0, false + } + } else { + left, success = WordToNumber(word[0]) + if !success { + return 0, false + } + middle, success = WordToNumber(word[1]) + if !success { + return 0, false + } + right, success = WordToNumber(word[2]) + if !success { + return 0, false + } + } + + // Map back to bit fields + result := uint32(left) << 22 + result |= uint32(middle) << 11 + result |= uint32(right) + + return result, true +} + +// WordsFromString hashes a string with a 32-bit function and converts it to three simple words +func WordsFromString(in string) (out string) { + hash := fnv.New32a() + inbytes := []byte(in) + hash.Write(inbytes) + hashval := hash.Sum32() + out = WordsFromNumber(hashval) + return +} + +// WordsFromNumber converts a number to three simple words +func WordsFromNumber(number uint32) string { + // Break the 32-bit uint down into 3 bit fields + left := (number >> 22) & 0x000003ff + middle := (number >> 11) & 0x000007ff + right := number & 0x000007ff + + // If the high order is 0, which is frequently the case, just use two words + if left == 0 { + return words2048[middle] + "-" + words2048[right] + } + return words2048[left] + "-" + words2048[middle] + "-" + words2048[right] +} + +// 2048 words, ORDERED but alphabetically unsorted +var words2048 = []string{ + "act", + "add", + "age", + "ago", + "point", + "big", + "all", + "and", + "any", + "arm", + "art", + "ash", + "ask", + "bad", + "bag", + "ban", + "bar", + "bat", + "bay", + "bed", + "bee", + "beg", + "bet", + "bid", + "air", + "bit", + "bow", + "box", + "boy", + "bug", + "bus", + "buy", + "cab", + "can", + "cap", + "car", + "cat", + "cop", + "cow", + "cry", + "cue", + "cup", + "cut", + "dad", + "day", + "die", + "dig", + "dip", + "dog", + "dot", + "dry", + "due", + "ear", + "eat", + "egg", + "ego", + "end", + "era", + "etc", + "eye", + "fan", + "far", + "fat", + "fee", + "few", + "fit", + "fix", + "fly", + "fog", + "for", + "fun", + "fur", + "gap", + "gas", + "get", + "gun", + "gut", + "guy", + "gym", + "hat", + "hay", + "her", + "hey", + "him", + "hip", + "his", + "hit", + "hot", + "how", + "hug", + "huh", + "ice", + "its", + "jar", + "jaw", + "jet", + "job", + "joy", + "key", + "kid", + "kit", + "lab", + "lap", + "law", + "leg", + "let", + "lid", + "lie", + "lip", + "log", + "lot", + "low", + "mars", + "mango", + "map", + "may", + "mix", + "mom", + "mud", + "net", + "new", + "nod", + "not", + "now", + "nut", + "oak", + "odd", + "off", + "oil", + "old", + "one", + "our", + "out", + "owe", + "own", + "pad", + "pan", + "pat", + "pay", + "pen", + "pet", + "pie", + "pig", + "pin", + "pit", + "pop", + "pot", + "put", + "rat", + "raw", + "red", + "rib", + "rid", + "rip", + "row", + "run", + "say", + "see", + "set", + "she", + "shy", + "sir", + "sit", + "six", + "ski", + "sky", + "son", + "spy", + "sum", + "sun", + "tag", + "tap", + "tax", + "tea", + "ten", + "the", + "tie", + "tip", + "toe", + "top", + "toy", + "try", + "two", + "use", + "van", + "war", + "way", + "web", + "who", + "why", + "win", + "wow", + "yes", + "yet", + "you", + "able", + "acid", + "aide", + "ally", + "also", + "amid", + "area", + "army", + "atop", + "aunt", + "auto", + "away", + "baby", + "back", + "bake", + "ball", + "band", + "bank", + "bare", + "barn", + "base", + "bath", + "beam", + "bean", + "bear", + "beat", + "beef", + "beer", + "bell", + "belt", + "bend", + "best", + "bias", + "bike", + "bill", + "bind", + "bird", + "bite", + "blue", + "boat", + "body", + "boil", + "bold", + "bolt", + "bomb", + "bond", + "bone", + "book", + "boom", + "boot", + "born", + "boss", + "both", + "bowl", + "buck", + "bulb", + "bulk", + "bull", + "burn", + "bury", + "bush", + "busy", + "cage", + "cake", + "call", + "calm", + "camp", + "card", + "care", + "cart", + "case", + "cash", + "cast", + "cave", + "cell", + "chef", + "chew", + "chin", + "chip", + "chop", + "cite", + "city", + "clay", + "clip", + "club", + "clue", + "coal", + "coat", + "code", + "coin", + "cold", + "come", + "cook", + "cool", + "cope", + "copy", + "cord", + "core", + "corn", + "cost", + "coup", + "crew", + "crop", + "cure", + "cute", + "dare", + "dark", + "data", + "date", + "dawn", + "dead", + "deal", + "dear", + "debt", + "deck", + "deem", + "deep", + "deer", + "deny", + "desk", + "diet", + "dirt", + "dish", + "dock", + "doll", + "door", + "dose", + "down", + "drag", + "draw", + "drop", + "drug", + "drum", + "duck", + "dumb", + "dump", + "dust", + "duty", + "each", + "earn", + "ease", + "east", + "easy", + "echo", + "edge", + "edit", + "else", + "even", + "ever", + "evil", + "exam", + "exit", + "face", + "fact", + "fade", + "fail", + "fair", + "fall", + "fame", + "fare", + "farm", + "fast", + "fate", + "feed", + "feel", + "file", + "fill", + "film", + "find", + "fine", + "fire", + "firm", + "fish", + "five", + "flag", + "flat", + "flee", + "flip", + "flow", + "fold", + "folk", + "food", + "foot", + "fork", + "form", + "four", + "free", + "from", + "fuel", + "full", + "fund", + "gain", + "game", + "gang", + "gate", + "gaze", + "gear", + "gene", + "gift", + "girl", + "give", + "glad", + "goal", + "goat", + "gold", + "golf", + "good", + "grab", + "gray", + "grin", + "grip", + "grow", + "half", + "hall", + "hand", + "hang", + "hard", + "harm", + "hate", + "haul", + "have", + "head", + "heal", + "hear", + "heat", + "heel", + "help", + "herb", + "here", + "hero", + "hers", + "hide", + "high", + "hike", + "hill", + "hint", + "hire", + "hold", + "home", + "hook", + "hope", + "horn", + "host", + "hour", + "huge", + "hunt", + "hurt", + "icon", + "idea", + "into", + "iron", + "item", + "jail", + "jazz", + "join", + "joke", + "jump", + "jury", + "just", + "keep", + "kick", + "kilt", + "kind", + "king", + "kiss", + "knee", + "know", + "lack", + "lake", + "lamp", + "land", + "lane", + "last", + "late", + "lawn", + "lead", + "leaf", + "lean", + "leap", + "left", + "lend", + "lens", + "less", + "life", + "lift", + "like", + "limb", + "line", + "link", + "lion", + "list", + "live", + "load", + "loan", + "lock", + "long", + "look", + "loop", + "loss", + "lost", + "lots", + "loud", + "love", + "luck", + "lung", + "mail", + "main", + "make", + "mall", + "many", + "mark", + "mask", + "mass", + "mate", + "math", + "meal", + "mean", + "meat", + "meet", + "melt", + "menu", + "mere", + "mild", + "milk", + "mill", + "mind", + "mine", + "miss", + "mode", + "mood", + "moon", + "more", + "most", + "move", + "much", + "must", + "myth", + "nail", + "name", + "near", + "neat", + "neck", + "need", + "nest", + "news", + "next", + "nice", + "nine", + "none", + "noon", + "norm", + "nose", + "note", + "odds", + "okay", + "once", + "only", + "onto", + "open", + "ours", + "oven", + "over", + "pace", + "pack", + "page", + "pain", + "pair", + "pale", + "palm", + "pant", + "park", + "part", + "pass", + "past", + "path", + "peak", + "peel", + "peer", + "pick", + "pile", + "pill", + "pine", + "pink", + "pipe", + "plan", + "play", + "plea", + "plot", + "plus", + "poem", + "poet", + "poke", + "pole", + "poll", + "pond", + "pool", + "poor", + "pork", + "port", + "pose", + "post", + "pour", + "pray", + "pull", + "pump", + "pure", + "push", + "quit", + "race", + "rack", + "rage", + "rail", + "rain", + "rank", + "rare", + "rate", + "read", + "real", + "rear", + "rely", + "rent", + "rest", + "rice", + "rich", + "ride", + "ring", + "riot", + "rise", + "risk", + "road", + "rock", + "role", + "roll", + "roof", + "room", + "root", + "rope", + "rose", + "ruin", + "rule", + "rush", + "sack", + "safe", + "sail", + "sake", + "sale", + "salt", + "same", + "sand", + "save", + "scan", + "seal", + "seat", + "seed", + "seek", + "seem", + "self", + "sell", + "send", + "sexy", + "shed", + "ship", + "shoe", + "shop", + "shot", + "show", + "shut", + "side", + "sign", + "silk", + "sing", + "sink", + "site", + "size", + "skip", + "slam", + "slip", + "slot", + "slow", + "snap", + "snow", + "soak", + "soap", + "soar", + "sock", + "sofa", + "soft", + "soil", + "sole", + "some", + "song", + "soon", + "sort", + "soul", + "soup", + "spin", + "spit", + "spot", + "star", + "stay", + "stem", + "step", + "stir", + "stop", + "such", + "suck", + "suit", + "sure", + "swim", + "tail", + "take", + "tale", + "talk", + "tall", + "tank", + "tape", + "task", + "team", + "tear", + "teen", + "tell", + "tend", + "tent", + "term", + "test", + "text", + "than", + "that", + "them", + "then", + "they", + "thin", + "this", + "thus", + "tide", + "tile", + "till", + "time", + "tiny", + "tire", + "toll", + "tone", + "tool", + "toss", + "tour", + "town", + "trap", + "tray", + "tree", + "trim", + "trip", + "tube", + "tuck", + "tune", + "turn", + "twin", + "type", + "unit", + "upon", + "urge", + "used", + "user", + "vary", + "vast", + "very", + "view", + "vote", + "wage", + "wait", + "wake", + "walk", + "wall", + "want", + "warn", + "wash", + "wave", + "weak", + "wear", + "weed", + "week", + "well", + "west", + "what", + "when", + "whip", + "whom", + "wide", + "wink", + "wild", + "will", + "wind", + "wine", + "wing", + "wipe", + "wire", + "wise", + "wish", + "with", + "wolf", + "word", + "work", + "wrap", + "yard", + "yeah", + "year", + "yell", + "your", + "zone", + "true", + "about", + "above", + "actor", + "adapt", + "added", + "admit", + "adopt", + "after", + "again", + "agent", + "agree", + "ahead", + "aisle", + "alarm", + "album", + "alien", + "alike", + "alive", + "alley", + "allow", + "alone", + "along", + "alter", + "among", + "angle", + "ankle", + "apart", + "apple", + "apply", + "arena", + "argue", + "arise", + "armed", + "array", + "arrow", + "aside", + "asset", + "avoid", + "await", + "awake", + "award", + "aware", + "basic", + "beach", + "beast", + "begin", + "being", + "belly", + "below", + "bench", + "birth", + "blare", + "blade", + "bling", + "blank", + "blast", + "blend", + "bless", + "blind", + "blink", + "block", + "blond", + "blotter", + "board", + "boast", + "bonus", + "boost", + "booth", + "brain", + "brake", + "brand", + "brave", + "bread", + "break", + "brick", + "bride", + "brief", + "bring", + "broad", + "brood", + "brush", + "buddy", + "build", + "bunch", + "burst", + "buyer", + "cabin", + "cable", + "candy", + "cargo", + "carry", + "carve", + "catch", + "cause", + "cease", + "chain", + "chair", + "chaos", + "charm", + "chart", + "chase", + "cheat", + "check", + "cheek", + "cheer", + "chest", + "chief", + "child", + "chill", + "chunk", + "claim", + "class", + "clean", + "clear", + "clerk", + "click", + "cliff", + "climb", + "cling", + "clock", + "close", + "cloth", + "cloud", + "coach", + "coast", + "color", + "couch", + "could", + "count", + "court", + "cover", + "crave", + "craft", + "crash", + "crawl", + "crater", + "creek", + "crime", + "cross", + "crowd", + "crown", + "crush", + "curve", + "cycle", + "daily", + "dance", + "death", + "debut", + "delay", + "dense", + "depth", + "diary", + "dirty", + "donor", + "doubt", + "dough", + "dozen", + "draft", + "drain", + "drama", + "dream", + "dress", + "dried", + "drift", + "drill", + "drink", + "drive", + "drown", + "drunk", + "dying", + "eager", + "early", + "earth", + "salty", + "elbow", + "elder", + "elect", + "elite", + "empty", + "enact", + "enemy", + "enjoy", + "enter", + "entry", + "equal", + "equip", + "erase", + "essay", + "event", + "every", + "exact", + "exist", + "extra", + "faint", + "faith", + "fatal", + "fault", + "favor", + "fence", + "fever", + "fewer", + "fiber", + "field", + "fifth", + "fifty", + "fight", + "final", + "first", + "fixed", + "flame", + "flash", + "fleet", + "flesh", + "float", + "flood", + "floor", + "flour", + "fluid", + "focus", + "force", + "forth", + "forty", + "forum", + "found", + "frame", + "fraud", + "fresh", + "front", + "frown", + "fruit", + "fully", + "funny", + "genre", + "ghost", + "giant", + "given", + "glass", + "globe", + "glory", + "glove", + "grace", + "grade", + "grain", + "grand", + "grant", + "grape", + "grasp", + "grass", + "gravel", + "great", + "green", + "greet", + "grief", + "gross", + "group", + "guard", + "guess", + "guest", + "guide", + "guilt", + "habit", + "happy", + "harsh", + "heart", + "heavy", + "hello", + "hence", + "honey", + "honor", + "horse", + "hotel", + "house", + "human", + "humor", + "hurry", + "ideal", + "image", + "imply", + "index", + "inner", + "input", + "irony", + "issue", + "jeans", + "joint", + "judge", + "juice", + "juror", + "kneel", + "kayak", + "knock", + "known", + "label", + "labor", + "large", + "laser", + "later", + "laugh", + "layer", + "learn", + "least", + "leave", + "legal", + "lemon", + "level", + "light", + "limit", + "liver", + "lobby", + "local", + "logic", + "loose", + "lover", + "lower", + "loyal", + "lucky", + "lunch", + "magic", + "major", + "maker", + "march", + "match", + "maybe", + "mayor", + "medal", + "media", + "merit", + "metal", + "meter", + "midst", + "might", + "minor", + "mixed", + "model", + "month", + "moral", + "motor", + "mount", + "mouse", + "mouth", + "movie", + "music", + "naked", + "olive", + "cricket", + "nerve", + "never", + "jade", + "night", + "noise", + "north", + "novel", + "nurse", + "occur", + "ocean", + "offer", + "often", + "onion", + "opera", + "orbit", + "order", + "other", + "ought", + "outer", + "owner", + "paint", + "panel", + "panic", + "paper", + "party", + "pasta", + "patch", + "pause", + "phase", + "phone", + "photo", + "piano", + "piece", + "pilot", + "pitch", + "pizza", + "place", + "plain", + "plant", + "plate", + "plead", + "aim", + "porch", + "pound", + "power", + "press", + "price", + "pride", + "prime", + "print", + "prior", + "prize", + "proof", + "proud", + "prove", + "pulse", + "punch", + "purse", + "quest", + "quick", + "quiet", + "quite", + "quote", + "radar", + "radio", + "raise", + "rally", + "ranch", + "range", + "rapid", + "ratio", + "reach", + "react", + "ready", + "realm", + "rebel", + "refer", + "relax", + "reply", + "rider", + "ridge", + "rifle", + "right", + "risky", + "rival", + "river", + "robot", + "round", + "route", + "royal", + "rumor", + "rural", + "salad", + "sales", + "sauce", + "scale", + "scare", + "scene", + "scent", + "scope", + "score", + "screw", + "seize", + "sense", + "serve", + "seven", + "shade", + "shake", + "shall", + "shame", + "shape", + "share", + "shark", + "sharp", + "sheep", + "sheer", + "sheet", + "shelf", + "shell", + "shift", + "shirt", + "shock", + "shoot", + "shore", + "short", + "shout", + "shove", + "shrug", + "sight", + "silly", + "since", + "sixth", + "skill", + "skirt", + "skull", + "slave", + "sleep", + "slice", + "slide", + "slope", + "small", + "smart", + "smell", + "smile", + "smoke", + "snake", + "sneak", + "solar", + "solid", + "solve", + "sorry", + "sound", + "south", + "space", + "spare", + "spark", + "speak", + "speed", + "spell", + "spend", + "spill", + "spine", + "spite", + "split", + "spoon", + "sport", + "spray", + "squad", + "stack", + "staff", + "stage", + "stair", + "stake", + "stand", + "stare", + "start", + "state", + "steak", + "steam", + "steel", + "steep", + "steer", + "stick", + "stiff", + "still", + "stock", + "stone", + "store", + "storm", + "story", + "stove", + "straw", + "strip", + "study", + "stuff", + "style", + "sugar", + "suite", + "sunny", + "super", + "sweat", + "sweep", + "sweet", + "swell", + "swing", + "sword", + "table", + "taste", + "teach", + "thank", + "their", + "theme", + "there", + "these", + "thick", + "thigh", + "thing", + "think", + "third", + "those", + "three", + "throw", + "thumb", + "tight", + "tired", + "title", + "today", + "tooth", + "topic", + "total", + "touch", + "tough", + "towel", + "tower", + "trace", + "track", + "trade", + "trail", + "train", + "trait", + "treat", + "trend", + "trial", + "tribe", + "trick", + "troop", + "truck", + "truly", + "trunk", + "trust", + "truth", + "tumor", + "twice", + "twist", + "uncle", + "under", + "union", + "unite", + "unity", + "until", + "upper", + "upset", + "urban", + "usual", + "valid", + "value", + "video", + "virus", + "visit", + "vital", + "vocal", + "voice", + "voter", + "wagon", + "waist", + "waste", + "watch", + "water", + "weave", + "weigh", + "weird", + "whale", + "wheat", + "wheel", + "where", + "which", + "while", + "whoop", + "whole", + "whose", + "wider", + "worm", + "works", + "world", + "worry", + "worth", + "would", + "wound", + "wrist", + "write", + "wrong", + "yield", + "young", + "yours", + "youth", + "false", + "abroad", + "absorb", + "accent", + "accept", + "access", + "accuse", + "across", + "action", + "active", + "actual", + "adjust", + "admire", + "affect", + "afford", + "agency", + "agenda", + "almost", + "always", + "amount", + "animal", + "annual", + "answer", + "anyone", + "anyway", + "appear", + "around", + "arrest", + "arrive", + "artist", + "aspect", + "assert", + "assess", + "assign", + "assist", + "assume", + "assure", + "attach", + "attack", + "attend", + "author", + "ballot", + "banana", + "banker", + "barrel", + "basket", + "battle", + "beauty", + "become", + "before", + "behalf", + "behave", + "behind", + "belief", + "belong", + "beside", + "better", + "beyond", + "bitter", + "bloody", + "border", + "borrow", + "bottle", + "bounce", + "branch", + "breath", + "breeze", + "bridge", + "bright", + "broken", + "broker", + "bronze", + "brutal", + "bubble", + "bucket", + "bullet", + "bureau", + "butter", + "button", + "camera", + "campus", + "candle", + "canvas", + "carbon", + "career", + "carpet", + "carrot", + "casino", + "casual", + "cattle", + "center", + "change", + "charge", + "cheese", + "choice", + "choose", + "circle", + "client", + "clinic", + "closed", + "closet", + "coffee", + "collar", + "combat", + "comedy", + "commit", + "comply", + "cookie", + "corner", + "cotton", + "county", + "cousin", + "create", + "credit", + "crisis", + "cruise", + "custom", + "dancer", + "danger", + "deadly", + "dealer", + "debate", + "debris", + "decade", + "deeply", + "defeat", + "defend", + "define", + "degree", + "depart", + "depend", + "depict", + "deploy", + "deputy", + "derive", + "desert", + "design", + "desire", + "detail", + "detect", + "device", + "devote", + "differ", + "dining", + "dinner", + "direct", + "divide", + "doctor", + "domain", + "donate", + "double", + "drawer", + "driver", + "during", + "easily", + "eating", + "editor", + "effect", + "effort", + "either", + "eleven", + "emerge", + "empire", + "employ", + "enable", + "endure", + "energy", + "engage", + "engine", + "enough", + "enroll", + "ensure", + "entire", + "entity", + "equity", + "escape", + "estate", + "evolve", + "exceed", + "except", + "expand", + "expect", + "expert", + "export", + "expose", + "extend", + "extent", + "fabric", + "factor", + "fairly", + "family", + "famous", + "farmer", + "faster", + "father", + "fellow", + "fierce", + "figure", + "filter", + "fishy", + "finish", + "firmly", + "fiscal", + "flavor", + "flight", + "flower", + "flying", + "follow", + "forest", + "forget", + "formal", + "format", + "former", + "foster", + "fourth", + "freely", + "freeze", + "friend", + "frozen", + "future", + "galaxy", + "garage", + "garden", + "garlic", + "gather", + "gender", + "genius", + "gifted", + "glance", + "global", + "golden", + "ground", + "growth", + "guitar", + "handle", + "happen", + "hardly", + "hazard", + "health", + "heaven", + "height", + "hidden", + "highly", + "hockey", + "honest", + "hunger", + "hungry", + "hunter", + "ignore", + "immune", + "impact", + "import", + "impose", + "income", + "indeed", + "infant", + "inform", + "injure", + "injury", + "inmate", + "insect", + "inside", + "insist", + "intact", + "intend", + "intent", + "invent", + "invest", + "invite", + "island", + "itself", + "jacket", + "jungle", + "junior", + "ladder", + "lately", + "latter", + "launch", + "lawyer", + "leader", + "league", + "legacy", + "legend", + "length", + "lesson", + "letter", + "likely", + "liquid", + "listen", + "little", + "living", + "locate", + "lovely", + "mainly", + "makeup", + "manage", + "manual", + "marble", + "margin", + "marine", + "market", + "master", + "matter", + "medium", + "member", + "memory", + "mentor", + "merely", + "method", + "middle", + "minute", + "mirror", + "mobile", + "modern", + "modest", + "modify", + "moment", + "monkey", + "mostly", + "mother", + "motion", + "motive", + "museum", + "mutter", + "mutual", + "myself", + "narrow", + "nation", + "native", + "nature", + "nearby", + "nearly", + "needle", + "nobody", + "normal", + "notice", + "notion", + "number", + "object", + "obtain", + "occupy", + "office", + "online", + "oppose", + "option", + "orange", + "origin", + "others", + "outfit", + "outlet", + "output", + "oxygen", + "palace", + "parade", + "parent", + "parish", + "partly", + "patent", + "patrol", + "patron", + "pencil", + "people", + "pepper", + "period", + "permit", + "person", + "phrase", + "pickup", + "pillow", + "planet", + "player", + "please", + "plenty", + "plunge", + "pocket", + "poetry", + "policy", + "poster", + "potato", + "powder", + "prefer", + "pretty", + "priest", + "profit", + "prompt", + "proper", + "public", + "purple", + "pursue", + "puzzle", + "rabbit", + "random", + "rarely", + "rather", + "rating", + "reader", + "really", + "reason", + "recall", + "recent", + "recipe", + "record", + "reduce", + "reform", + "refuse", + "regain", + "regard", + "regime", + "region", + "reject", + "relate", + "relief", + "remain", + "remark", + "remind", + "remote", + "remove", + "rental", + "repair", + "repeat", + "report", + "rescue", + "resign", + "resist", + "resort", + "result", + "resume", + "retail", + "retain", + "retire", + "return", + "reveal", + "review", + "reward", + "rhythm", + "ribbon", + "ritual", + "rocket", + "rubber", + "ruling", + "runner", + "safely", + "safety", + "salary", + "salmon", + "sample", + "saving", + "scared", + "scheme", + "school", + "scream", + "screen", + "script", + "search", + "season", + "second", + "secret", + "sector", + "secure", + "seldom", + "select", + "seller", + "senior", + "sensor", + "series", + "settle", + "severe", + "shadow", + "shorts", + "should", + "shrimp", + "signal", + "silent", + "silver", + "simple", + "simply", + "singer", + "single", + "sister", + "sleeve", + "slight", + "slowly", + "smooth", + "soccer", + "social", + "sodium", + "soften", + "softly", + "solely", + "source", + "speech", + "sphere", + "spirit", + "spread", + "spring", + "square", + "stable", + "stance", + "statue", + "status", + "steady", + "strain", + "streak", + "stream", + "street", + "stress", + "strict", + "strike", + "string", + "stroke", + "strong", + "studio", + "stupid", + "submit", + "subtle", + "suburb", + "sudden", + "suffer", + "summer", + "summit", + "supply", + "surely", + "survey", + "switch", + "symbol", + "system", + "tackle", + "tactic", + "talent", + "target", + "temple", + "tender", + "tennis", + "thanks", + "theory", + "thirty", + "though", + "thread", + "thrive", + "throat", + "ticket", + "timber", + "timing", + "tissue", + "toilet", + "tomato", + "tonic", + "toward", + "tragic", + "trauma", + "travel", + "treaty", + "tribal", + "tunnel", + "turkey", + "twelve", + "twenty", + "unfair", + "unfold", + "unique", + "unless", + "unlike", + "update", + "useful", + "vacuum", + "valley", + "vanish", + "vendor", + "verbal", + "versus", + "vessel", + "viewer", + "virtue", + "vision", + "visual", + "volume", + "voting", + "wander", + "warmth", + "wealth", + "weapon", + "weekly", + "weight", + "widely", + "window", + "winner", + "winter", + "wisdom", + "within", + "wonder", + "wooden", + "worker", + "writer", + "yellow", +} + +// end diff --git a/note-go/note/words_test.go b/note-go/note/words_test.go new file mode 100644 index 0000000..202f685 --- /dev/null +++ b/note-go/note/words_test.go @@ -0,0 +1,19 @@ +package note + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWordsFromString(t *testing.T) { + cases := map[string]string{ + "dev:foobar": "near-eat-read", + "dev:123456778": "farm-quiet-dumb", + "dev:qwerty": "flour-water-stock", + } + + for k, v := range cases { + require.Equal(t, v, WordsFromString(k)) + } +} diff --git a/note-go/notecard/cobs.go b/note-go/notecard/cobs.go new file mode 100644 index 0000000..a379d18 --- /dev/null +++ b/note-go/notecard/cobs.go @@ -0,0 +1,68 @@ +// Copyright 2023 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +// Decode with optional XOR +func CobsDecode(input []byte, xor byte) ([]byte, error) { + output := make([]byte, len(input)) + length := len(output) + inOffset := 0 + outOffset := inOffset + startOffset, endOffset := outOffset, inOffset+length + var code, copy uint8 = 0xFF, 0 + for ; inOffset < endOffset; copy-- { + if copy != 0 { + output[outOffset] = input[inOffset] ^ xor + outOffset, inOffset = outOffset+1, inOffset+1 + } else { + if code != 0xFF { + output[outOffset] = 0 + outOffset = outOffset + 1 + } + code = input[inOffset] ^ xor + copy, inOffset = code, inOffset+1 + if code == 0 { + break + } + } + } + return output[startOffset:outOffset], nil +} + +// Get the maximum size of the cobs-encoded buffer +func CobsEncodedLength(length int) int { + return length + (1 + (length / 254)) +} + +// Encode with optional XOR +func CobsEncode(input []byte, xor byte) ([]byte, error) { + length := len(input) + inOffset := 0 + output := make([]byte, CobsEncodedLength(len(input))) + outOffset := 0 + outStartOffset := outOffset + var ch, code uint8 + code = 1 + outCodeOffset := outOffset + outOffset = outOffset + 1 + for length > 0 { + ch = input[inOffset] + inOffset = inOffset + 1 + length = length - 1 + if ch != 0 { + output[outOffset] = ch ^ xor + outOffset = outOffset + 1 + code = code + 1 + } + if ch == 0 || code == 0xFF { + output[outCodeOffset] = code ^ xor + code = 1 + outCodeOffset = outOffset + outOffset = outOffset + 1 + } + } + output[outCodeOffset] = code ^ xor + return output[outStartOffset:outOffset], nil +} diff --git a/note-go/notecard/cobs_test.go b/note-go/notecard/cobs_test.go new file mode 100644 index 0000000..8d7fbcc --- /dev/null +++ b/note-go/notecard/cobs_test.go @@ -0,0 +1,29 @@ +package notecard + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestCob(t *testing.T) { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + min := 100 + max := 1000 + len := rng.Intn(max-min+1) + min + buf := make([]byte, len) + xor := byte(rng.Int()) + + _, err := rng.Read(buf) + require.NoError(t, err) + + encoded, err := CobsEncode(buf, xor) + require.NoError(t, err) + + decoded, err := CobsDecode(encoded, xor) + require.NoError(t, err) + + require.Equal(t, buf, decoded) +} diff --git a/note-go/notecard/i2c-unix.go b/note-go/notecard/i2c-unix.go new file mode 100644 index 0000000..7820c6a --- /dev/null +++ b/note-go/notecard/i2c-unix.go @@ -0,0 +1,176 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. +// Forked from github.com/d2r2/go-i2c +// Forked from github.com/davecheney/i2c + +//go:build !windows + +// Before usage you must load the i2c-dev kernel module. +// Each i2c bus can address 127 independent i2c devices, and most +// linux systems contain several buses. + +// Note: I2C Device Interface is accessed through periph.io library +// Example: https://github.com/google/periph/blob/master/devices/bmxx80/bmx280.go + +package notecard + +import ( + "fmt" + "sync" + "time" + + "periph.io/x/conn/v3/driver/driverreg" + "periph.io/x/conn/v3/i2c" + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/host/v3" +) + +const ( + // I2CSlave is the slave device address + I2CSlave = 0x0703 +) + +// I2C is the handle to the I2C subsystem +type I2C struct { + host *driverreg.State + bus i2c.BusCloser + device *i2c.Dev +} + +// The open I2C port +var ( + hostInitialized bool + openI2CPort *I2C + i2cLock sync.RWMutex +) + +// Our default I2C address +const notecardDefaultI2CAddress = 0x17 + +// Get the default i2c device +func i2cDefault() (port string, portConfig int) { + port = "" // Null string opens first available bus + portConfig = notecardDefaultI2CAddress + return +} + +// Open the i2c port +func i2cOpen(port string, portConfig int) (err error) { + // Open the periph.io host + if !hostInitialized { + openI2CPort = &I2C{} + openI2CPort.host, err = host.Init() + if err != nil { + return + } + } + + // Open the I2C instance + i2cLock.Lock() + openI2CPort.bus, err = i2creg.Open(port) + i2cLock.Unlock() + if err != nil { + return + } + + return nil +} + +// WriteBytes writes a buffer to I2C +func i2cWriteBytes(buf []byte, i2cAddr int) (err error) { + if i2cAddr == 0 { + i2cAddr = notecardDefaultI2CAddress + } + time.Sleep(1 * time.Millisecond) // By design, must not send more than once every 1Ms + reg := make([]byte, 1) + reg[0] = byte(len(buf)) + reg = append(reg, buf...) + i2cLock.Lock() + openI2CPort.device = &i2c.Dev{Bus: openI2CPort.bus, Addr: uint16(i2cAddr)} + err = openI2CPort.device.Tx(reg, nil) + i2cLock.Unlock() + if err != nil { + err = fmt.Errorf("wb: %s", err) + } + return +} + +// ReadBytes reads a buffer from I2C and returns how many are still pending +func i2cReadBytes(datalen int, i2cAddr int) (outbuf []byte, available int, err error) { + if i2cAddr == 0 { + i2cAddr = notecardDefaultI2CAddress + } + time.Sleep(1 * time.Millisecond) // By design, must not send more than once every 1Ms + readbuf := make([]byte, datalen+2) + for i := 0; ; i++ { // Retry just for robustness + reg := make([]byte, 2) + reg[0] = byte(0) + reg[1] = byte(datalen) + i2cLock.Lock() + openI2CPort.device = &i2c.Dev{Bus: openI2CPort.bus, Addr: uint16(i2cAddr)} + err = openI2CPort.device.Tx(reg, readbuf) + i2cLock.Unlock() + if err == nil { + break + } + if i >= 10 { + err = fmt.Errorf("rb: %s", err) + return + } + time.Sleep(2 * time.Millisecond) + } + if len(readbuf) < 2 { + err = fmt.Errorf("rb: not enough data (%d < 2)", len(readbuf)) + return + } + available = int(readbuf[0]) + if available > 253 { + err = fmt.Errorf("rb: available too large (%d >253)", available) + return + } + good := readbuf[1] + if len(readbuf) < int(2+good) { + err = fmt.Errorf("rb: insufficient data (%d < %d)", len(readbuf), 2+good) + return + } + if 2 > 2+good { + if false { + fmt.Printf("i2cReadBytes(%d): %v\n", datalen, readbuf) + } + err = fmt.Errorf("rb: %d bytes returned while expecting %d", good, datalen) + return + } + outbuf = readbuf[2 : 2+good] + return +} + +// Close I2C +func i2cClose() (err error) { + i2cLock.Lock() + err = openI2CPort.bus.Close() + i2cLock.Unlock() + return +} + +// Enum I2C ports +func i2cPortEnum() (allports []string, usbports []string, notecardports []string, err error) { + // Open the periph.io host + if !hostInitialized { + openI2CPort = &I2C{} + openI2CPort.host, err = host.Init() + if err != nil { + return + } + } + + // Enum + for _, ref := range i2creg.All() { + port := ref.Name + if ref.Number != -1 { + allports = append(allports, port) + notecardports = append(notecardports, port) + } + } + return +} diff --git a/note-go/notecard/i2c-windows.go b/note-go/notecard/i2c-windows.go new file mode 100644 index 0000000..3cea9dc --- /dev/null +++ b/note-go/notecard/i2c-windows.go @@ -0,0 +1,49 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +//go:build windows + +package notecard + +import ( + "fmt" +) + +// Get the default i2c device +func i2cDefault() (port string, portConfig int) { + port = "???" + portConfig = 0x17 + return +} + +// Set the port config of the open port +func i2cSetConfig(portConfig int) (err error) { + return fmt.Errorf("i2c not yet implemented") +} + +// Open the i2c port +func i2cOpen(port string, portConfig int) (err error) { + return fmt.Errorf("i2c not yet implemented") +} + +// WriteBytes writes a buffer to I2C +func i2cWriteBytes(buf []byte, i2cAddr int) (err error) { + return fmt.Errorf("i2c not yet implemented") +} + +// ReadBytes reads a buffer from I2C and returns how many are still pending +func i2cReadBytes(datalen int, i2cAddr int) (outbuf []byte, available int, err error) { + err = fmt.Errorf("i2c not yet implemented") + return +} + +// Close I2C +func i2cClose() error { + return fmt.Errorf("i2c not yet implemented") +} + +// Enum I2C ports +func i2cPortEnum() (allports []string, usbports []string, notecardports []string, err error) { + return +} diff --git a/note-go/notecard/lease.go b/note-go/notecard/lease.go new file mode 100644 index 0000000..9887d47 --- /dev/null +++ b/note-go/notecard/lease.go @@ -0,0 +1,196 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/blues/note-go/note" +) + +// Leaseing service parameters +const leaseTransactionService = "https://notepod.io:8123" +const leaseTraceService = "proxy.notepod.io:123" + +// Lease transaction +type LeaseTransaction struct { + Request string `json:"req,omitempty"` + Lessor string `json:"lessor,omitempty"` + Scope string `json:"scope,omitempty"` + Expires int64 `json:"expires,omitempty"` + Error string `json:"err,omitempty"` + DeviceUID string `json:"device,omitempty"` + NoResponse bool `json:"no_response,omitempty"` + ReqJSON string `json:"request_json,omitempty"` + RspJSON string `json:"response_json,omitempty"` +} + +// Request types +const ( + ReqReserve = "reserve" + ReqTransaction = "transaction" +) + +// Perform an HTTP transaction to the lease service +func leaseService(req LeaseTransaction, promoteError bool) (rsp LeaseTransaction, err error) { + + reqj, err := json.Marshal(req) + if err != nil { + return rsp, err + } + + // Send the transaction + hreq, err := http.NewRequest("POST", leaseTransactionService, bytes.NewBuffer(reqj)) + if err != nil { + return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) + } + hcli := &http.Client{Timeout: time.Second * 90} + hrsp, err := hcli.Do(hreq) + if err != nil { + return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) + } + defer hrsp.Body.Close() + + // Read the response + var rspjb bytes.Buffer + _, err = io.Copy(&rspjb, hrsp.Body) + if err != nil { + return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) + } + rspj := rspjb.Bytes() + + err = note.JSONUnmarshal(rspj, &rsp) + if err != nil { + return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) + } + + if promoteError && rsp.Error != "" { + return rsp, fmt.Errorf("%s", rsp.Error) + } + + return rsp, nil + +} + +// Open or reopen the remote card by taking out a lease, or by renewing the lease. +func leaseReopen(context *Context, portConfig int) (err error) { + + context.portIsOpen = false + + // Don't reopen if tracing + if InitialTraceMode { + context.portIsOpen = true + return + } + + // Find out our unique ID + context.leaseLessor = callerID() + + // Perform the lease transaction + req := LeaseTransaction{} + req.Request = ReqReserve + req.Lessor = context.leaseLessor + req.Scope = context.leaseScope + req.Expires = context.leaseExpires + rsp, err := leaseService(req, true) + if err != nil { + return err + } + + // Trace so that we can find out when + if context.leaseExpires == 0 { + fmt.Printf("%s reserved until %s\n", rsp.DeviceUID, time.Unix(rsp.Expires, 0).Local().Format("03:04:05 PM MST")) + } + + // Save the deviceUID to the allocated device + context.leaseScope = rsp.Scope + context.leaseExpires = rsp.Expires + context.leaseDeviceUID = rsp.DeviceUID + + // Open + context.portIsOpen = true + + return +} + +// Close a remote notecard +func leaseClose(context *Context) { + context.portIsOpen = false +} + +// Perform a remote transaction +func leaseTransaction(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) { + + // Perform the lease transaction + req := LeaseTransaction{} + req.Request = ReqTransaction + req.Lessor = context.leaseLessor + req.DeviceUID = context.leaseDeviceUID + req.ReqJSON = string(reqJSON) + req.NoResponse = noResponse + rsp, err := leaseService(req, true) + if err != nil { + return rspJSON, err + } + + // Done + return []byte(rsp.RspJSON), nil + +} + +// Lease trace open +func leaseTraceOpen(context *Context) (err error) { + + // Scope must be a specific device UID for trace + if !strings.HasPrefix(context.port, "dev:") { + return fmt.Errorf("trace is only allowed when a deviceUID is specified") + } + + // Open the service connection + tcpServer, err := net.ResolveTCPAddr("tcp", leaseTraceService) + if err != nil { + return + } + context.leaseTraceConn, err = net.DialTCP("tcp", nil, tcpServer) + if err != nil { + return + } + + // Write an initial non-json line containing scope, to signal to the service that this is a trace connection + leaseTraceWrite(context, []byte(context.port+"\n")) + + // Done + return + +} + +// Lease trace read function +func leaseTraceRead(context *Context) (data []byte, err error) { + + buf := make([]byte, 2048) + length, err := context.leaseTraceConn.Read(buf) + if err != nil { + if err == io.EOF { + // Just a read timeout + return data, nil + } + return data, err + } + + return buf[:length], nil + +} + +// Lease trace write function +func leaseTraceWrite(context *Context, data []byte) { + context.leaseTraceConn.Write(data) +} diff --git a/note-go/notecard/net.go b/note-go/notecard/net.go new file mode 100644 index 0000000..deca28e --- /dev/null +++ b/note-go/notecard/net.go @@ -0,0 +1,82 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +const ( + NetworkBearerUnknown = -1 + NetworkBearerGsm = 0 + NetworkBearerTdScdma = 1 + NetworkBearerWcdma = 2 + NetworkBearerCdma2000 = 3 + NetworkBearerWiMax = 4 + NetworkBearerLteTdd = 5 + NetworkBearerLteFdd = 6 + NetworkBearerNBIot = 7 + NetworkBearerWLan = 21 + NetworkBearerBluetooth = 22 + NetworkBearerIeee802p15p4 = 23 + NetworkBearerEthernet = 41 + NetworkBearerDsl = 42 + NetworkBearerPlc = 43 +) + +// NetInfo is the composite structure with all networking connection info +type NetInfo struct { + Iccid string `json:"iccid,omitempty"` + Iccid2 string `json:"iccid2,omitempty"` + IccidExternal string `json:"iccid_external,omitempty"` + Imsi string `json:"imsi,omitempty"` + Imsi2 string `json:"imsi2,omitempty"` + ImsiExternal string `json:"imsi_external,omitempty"` + Imei string `json:"imei,omitempty"` + ModemFirmware string `json:"modem,omitempty"` + Band string `json:"band,omitempty"` + AccessTechnology string `json:"rat,omitempty"` + AccessTechnologyFilter string `json:"ratf,omitempty"` + ReportedAccessTechnology string `json:"ratr,omitempty"` + ReportedCarrier string `json:"carrier,omitempty"` + Bssid string `json:"bssid,omitempty"` + Ssid string `json:"ssid,omitempty"` + // Internal vs external SIM used at any given moment + InternalSIMSelected bool `json:"internal,omitempty"` + // Radio signal strength in dBm, or ModemValueUnknown if it is not + // available from the modem. + RssiRange int32 `json:"rssir,omitempty"` + // GSM RxQual, or ModemValueUnknown if it is not available from the modem. + Rxqual int32 `json:"rxqual,omitempty"` + // General received signal strength, in dBm + Rssi int32 `json:"rssi,omitempty"` + // An integer indicating the reference signal received power (RSRP) + Rsrp int32 `json:"rsrp,omitempty"` + // An integer indicating the signal to interference plus noise ratio (SINR). + // Logarithmic value of SINR. Values are in 1/5th of a dB. The range is 0-250 + // which translates to -20dB - +30dB + Sinr int32 `json:"sinr,omitempty"` + // An integer indicating the reference signal received quality (RSRQ) + Rsrq int32 `json:"rsrq,omitempty"` + // An integer indicating relative signal strength in a human-readable way + Bars uint32 `json:"bars,omitempty"` + // IP address assigned to the device + IP string `json:"ip,omitempty"` + // IP address that the device is talking to (if known) + Gw string `json:"gateway,omitempty"` + // Device APN name + Apn string `json:"apn,omitempty"` + // Location area code (16 bits) or ModemValueUnknown if it is not avail from modem + Lac uint32 `json:"lac,omitempty"` + // Cell ID (28 bits) or ModemValueUnknown if it is not available from the modem. + Cellid uint32 `json:"cid,omitempty"` + // Network info + NetworkBearer int32 `json:"bearer,omitempty"` + Mcc uint32 `json:"mcc,omitempty"` + Mnc uint32 `json:"mnc,omitempty"` + // Modem debug + ModemDebugEvents int32 `json:"modem_test_events,omitempty"` + // Overcurrent events + OvercurrentEvents int32 `json:"oc_events,omitempty"` + OvercurrentEventSecs int32 `json:"oc_event_time,omitempty"` + // When the signal strength fields were last updated + Modified int64 `json:"updated,omitempty"` +} diff --git a/note-go/notecard/notecard.go b/note-go/notecard/notecard.go new file mode 100644 index 0000000..189cea6 --- /dev/null +++ b/note-go/notecard/notecard.go @@ -0,0 +1,1625 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +import ( + "bytes" + "encoding/json" + "fmt" + "hash/crc32" + "io" + "net" + "os" + "os/user" + "strconv" + "strings" + "sync" + "time" + + "github.com/blues/note-go/note" + "go.bug.st/serial" +) + +// Debug serial I/O +var debugSerialIO = false + +// InitialDebugMode is the debug mode that the context is initialized with +var InitialDebugMode = false + +// InitialTraceMode is whether or not we will be entering trace mode, to prevent reservationsa +var InitialTraceMode = false + +// InitialResetMode says whether or not we should reset the port on entry +var InitialResetMode = true + +// Protect against multiple concurrent callers, because across different operating systems it is +// not at all clear that concurrency is allowed on a single I/O device. An exception is made +// for the I2C 'multiport' case (exposed by TransactionRequestToPort) where we allow multiple +// concurrent I2C transactions on a single device. (This capability was needed for the +// Notefarm, but it's unclear if anyone uses this multi-notecard concurrency capability anymore +// now that it's deprecated.) +var ( + transLock sync.RWMutex + multiportTransLock [128]sync.RWMutex +) + +// Default transaction timeout (before receiving anything from the notecard) +const transactionTimeoutMsDefault = 30000 + +// IgnoreWindowsHWErrSecs is the amount of time to ignore a Windows serial communiction error. +var IgnoreWindowsHWErrSecs = 2 + +// Module communication interfaces +const ( + NotecardInterfaceSerial = "serial" + NotecardInterfaceI2C = "i2c" + NotecardInterfaceLease = "lease" +) + +// The number of minutes that we'll round up so that notecard reservations don't thrash +const reservationModulusMinutes = 5 + +// CardI2CMax controls chunk size that's socially appropriate on the I2C bus. +// It must be 1-253 bytes as per spec (which allows space for the 2-byte header in a 255-byte read) +const CardI2CMax = 253 + +// The notecard is a real-time device that has a fixed size interrupt buffer. We can push data +// at it far, far faster than it can process it, therefore we push it in segments with a pause +// between each segment. + +// CardRequestSerialSegmentMaxLen (golint) +const CardRequestSerialSegmentMaxLen = 250 + +// CardRequestSerialSegmentDelayMs (golint) +const CardRequestSerialSegmentDelayMs = 250 + +// CardRequestI2CSegmentMaxLen (golint) +const CardRequestI2CSegmentMaxLen = 250 + +// CardRequestI2CSegmentDelayMs (golint) +const CardRequestI2CSegmentDelayMs = 250 + +// RequestSegmentMaxLen (golint) +var RequestSegmentMaxLen = -1 + +// RequestSegmentDelayMs (golint) +var RequestSegmentDelayMs = -1 + +var DoNotReterminateJSON = false + +// Transaction retry logic +const requestRetriesAllowed = 5 + +// IoErrorIsRecoverable is a configuration parameter describing library capabilities. +// Set this to true if the error recovery of the implementation supports re-open. On all implementations +// tested to date, I can't yet get the close/reopen working the way it does on microcontrollers. For +// example, on the go serial, I get a nil pointer dereference within the go library. This MAY have +// soemthing to do with the fact that we don't cleanly implement the shutdown/restart of the inputHandler +// in trace, in which case that should be fixed. In the meantime, this is disabled. +const IoErrorIsRecoverable = true + +// Context for the port that is open +type Context struct { + // True to emit trace output + Debug bool + + // Pretty-print trace output JSON + Pretty bool + + // Disable generation of User Agent object + DisableUA bool + + // Reset should be done on next transaction + resetRequired bool + reopenRequired bool + reopenBecauseOfOpen bool + + // Sequence number + lastRequestSeqno int + + // Class functions + PortEnumFn func() (allports []string, usbports []string, notecardports []string, err error) + PortDefaultsFn func() (port string, portConfig int) + CloseFn func(context *Context) + ReopenFn func(context *Context, portConfig int) (err error) + ResetFn func(context *Context, portConfig int) (err error) + TransactionFn func(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) + + // Transaction timeout (0 for default) + transactionTimeoutMs int + + // User-specified heartbeat function + HeartbeatCtx interface{} + HeartbeatFn func(context *Context, userCtx interface{}, response []byte) bool + + // Trace functions + traceOpenFn func(context *Context) (err error) + traceReadFn func(context *Context) (data []byte, err error) + traceWriteFn func(context *Context, data []byte) + + // Port data + iface string + isLocal bool + port string + portConfig int + portIsOpen bool + + // Serial instance state + isSerial bool + serialPort serial.Port + serialUseDefault bool + serialName string + serialConfig serial.Mode + + // Serial I/O timeout helpers + ioStartSignal chan int + ioCompleteSignal chan bool + ioTimeoutSignal chan bool + + // I2C + i2cMultiport bool + + // Lease state + leaseScope string + leaseExpires int64 + leaseLessor string + leaseDeviceUID string + leaseTraceConn net.Conn +} + +// Report a critical card error +func cardReportError(context *Context, err error) { + if context == nil { + return + } + if context.Debug { + fmt.Printf("*** %s\n", err) + } + if IoErrorIsRecoverable { + time.Sleep(500 * time.Millisecond) + context.reopenRequired = true + } +} + +// Set the transaction function +func (context *Context) GetTransactionTimeoutMs() int { + if context.transactionTimeoutMs == 0 { + return transactionTimeoutMsDefault + } + return context.transactionTimeoutMs +} + +// Set the request timeout (0 to restore for default) +func (context *Context) SetTransactionTimeoutMs(msec int) { + context.transactionTimeoutMs = msec +} + +// Set or clear the heartbeat function +func (context *Context) SetTransactionHeartbeatFn(userFn func(context *Context, userCtx interface{}, rsp []byte) bool, userCtx interface{}) { + context.HeartbeatFn = userFn + context.HeartbeatCtx = userCtx +} + +// DebugOutput enables/disables debug output +func (context *Context) DebugOutput(enabled bool, pretty bool) { + context.Debug = enabled + context.Pretty = pretty +} + +// EnumPorts returns the list of all available ports on the specified interface +func (context *Context) EnumPorts() (allports []string, usbports []string, notecardports []string, err error) { + if context.PortEnumFn == nil { + return + } + return context.PortEnumFn() +} + +// PortDefaults gets the defaults for the specified port +func (context *Context) PortDefaults() (port string, portConfig int) { + if context.PortDefaultsFn == nil { + return + } + return context.PortDefaultsFn() +} + +// Identify this Notecard connection +func (context *Context) Identify() (protocol string, port string, portConfig int) { + if context.isSerial { + return "serial", context.serialName, context.serialConfig.BaudRate + } + return "I2C", context.port, context.portConfig +} + +// Defaults gets the default interface, port, and config +func Defaults() (moduleInterface string, port string, portConfig int) { + moduleInterface = NotecardInterfaceSerial + port, portConfig = serialDefault() + return +} + +// Open the card to establish communications +func Open(moduleInterface string, port string, portConfig int) (context *Context, err error) { + if moduleInterface == "" { + moduleInterface, _, _ = Defaults() + } + + switch moduleInterface { + case NotecardInterfaceSerial: + context, err = OpenSerial(port, portConfig) + context.isLocal = true + case NotecardInterfaceI2C: + context, err = OpenI2C(port, portConfig) + context.isLocal = true + case NotecardInterfaceLease: + context, err = OpenLease(port, portConfig) + default: + err = fmt.Errorf("unknown interface: %s", moduleInterface) + } + if err != nil { + cardReportError(nil, err) + err = fmt.Errorf("error opening port: %s %s", err, note.ErrCardIo) + return + } + context.iface = moduleInterface + return +} + +// Reset serial to a known state. Note that this is performed by sending +// a newline and then draining the input buffer. If a newline is not +// received, it is NOT a bug because, for example, Starnote does not +// perform the echo'ing of \n *by design*. +func cardResetSerial(context *Context, portConfig int) (err error) { + // Exit if not open + if !context.portIsOpen { + err = fmt.Errorf("port not open " + note.ErrCardIo) + cardReportError(context, err) + return + } + + // In order to ensure that we're not getting the reply to a failed + // transaction from a prior session, drain any pending input prior + // to transmitting a command. Note that we use this technique of + // looking for a known reply to \n, rather than just "draining + // anything pending on serial", because the nature of read() is + // that it blocks (until timeout) if there's nothing available. + var length int + buf := make([]byte, 2048) + for { + if debugSerialIO { + fmt.Printf("cardResetSerial: about to write newline\n") + } + serialIOBegin(context, context.GetTransactionTimeoutMs()) + _, err = context.serialPort.Write([]byte("\n")) + err = serialIOEnd(context, err) + if debugSerialIO { + fmt.Printf(" back with err = %v\n", err) + } + if err != nil { + err = fmt.Errorf("error transmitting to module: %s %s", err, note.ErrCardIo) + cardReportError(context, err) + return + } + time.Sleep(250 * time.Millisecond) + if debugSerialIO { + fmt.Printf("cardResetSerial: about to read up to %d bytes\n", len(buf)) + } + readBeganMs := int(time.Now().UnixNano() / 1000000) + serialIOBegin(context, 750) + length, err = context.serialPort.Read(buf) + err = serialIOEnd(context, err) + readElapsedMs := int(time.Now().UnixNano()/1000000) - readBeganMs + if debugSerialIO { + fmt.Printf(" back after %d ms with len = %d err = %v\n", readElapsedMs, length, err) + } + if readElapsedMs == 0 && length == 0 && err == io.EOF { + // On Linux, hardware port failures come back simply as immediate EOF + err = fmt.Errorf("hardware failure") + } + if err != nil { + // Ignore errors after reset, as the only purpose of reset is to drain the input buffer + err = CardReopenSerial(context, portConfig) + return err + } + somethingFound := false + nonCRLFFound := false + for i := 0; i < length && !nonCRLFFound; i++ { + if false { + fmt.Printf("chr: 0x%02x '%c'\n", buf[i], buf[i]) + } + if buf[i] != '\r' { + somethingFound = true + if buf[i] != '\n' { + nonCRLFFound = true + } + } + } + if somethingFound && !nonCRLFFound { + break + } + } + + // Done + return +} + +// Serial I/O timeout helper function for Windows +func serialTimeoutHelper(context *Context, portConfig int) { + for { + timeoutMs := <-context.ioStartSignal + timeout := false + select { + case <-context.ioCompleteSignal: + case <-time.After(time.Duration(timeoutMs) * time.Millisecond): + timeout = true + if debugSerialIO { + fmt.Printf("serialTimeoutHelper: timeout\n") + } + cardCloseSerial(context) + } + context.ioTimeoutSignal <- timeout + } +} + +// Begin a serial I/O +func serialIOBegin(context *Context, timeoutMs int) { + context.ioStartSignal <- timeoutMs + if debugSerialIO { + if !context.portIsOpen { + fmt.Printf("serialIoBegin: WARNING: PORT NOT OPEN\n") + } + fmt.Printf("serialIOBegin: begin timeout of %d ms\n", timeoutMs) + } +} + +// End a serial I/O +func serialIOEnd(context *Context, errIn error) (errOut error) { + errOut = errIn + context.ioCompleteSignal <- true + timeout := <-context.ioTimeoutSignal + select { + case <-context.ioCompleteSignal: + if debugSerialIO { + fmt.Printf("serialIOEnd: ioComplete ate the completed signal (timeout: %v)\n", timeout) + } + default: + if debugSerialIO { + fmt.Printf("serialIOEnd: ioComplete nothing to eat (timeout: %v)\n", timeout) + } + } + if timeout { + errOut = fmt.Errorf("serial I/O timeout %s", note.ErrCardIo) + } + return +} + +// OpenSerial opens the card on serial +func OpenSerial(port string, portConfig int) (context *Context, err error) { + // Create the context structure + context = &Context{} + context.Debug = InitialDebugMode + context.port = port + context.portConfig = portConfig + context.lastRequestSeqno = 0 + + // Set up class functions + context.PortEnumFn = serialPortEnum + context.PortDefaultsFn = serialDefault + context.CloseFn = cardCloseSerial + context.ReopenFn = CardReopenSerial + context.ResetFn = cardResetSerial + context.TransactionFn = cardTransactionSerial + context.traceOpenFn = serialTraceOpen + context.traceReadFn = serialTraceRead + context.traceWriteFn = serialTraceWrite + + // Record serial configuration, and whether or not we are using the default + context.isSerial = true + context.serialName, context.serialConfig.BaudRate = serialDefault() + if port == "" { + context.serialUseDefault = true + } else { + context.serialName = port + + } + if portConfig != 0 { + context.serialConfig.BaudRate = portConfig + } + + // Set up I/O port close channels, because Windows needs a bit of help in timing out I/O's. + context.ioStartSignal = make(chan int, 1) + context.ioCompleteSignal = make(chan bool, 1) + context.ioTimeoutSignal = make(chan bool, 1) + go serialTimeoutHelper(context, portConfig) + + // For serial, we defer the port open until the first transaction so that we can + // support the concept of dynamically inserted devices, as in "notecard -scan" mode. + context.reopenBecauseOfOpen = true + context.reopenRequired = true + + // All set + return +} + +// Reset I2C to a known good state +func cardResetI2C(context *Context, portConfig int) (err error) { + // Synchronize by guaranteeing not only that I2C works, but that we drain the remainder of any + // pending partial reply from a previously-aborted session. + chunklen := 0 + for { + + // Read the next chunk of available data + _, available, err2 := i2cReadBytes(chunklen, portConfig) + if err2 != nil { + err = fmt.Errorf("error reading chunk: %s %s", err2, note.ErrCardIo) + return + } + + // If nothing left, we're ready to transmit a command to receive the data + if available == 0 { + break + } + + // For the next iteration, reaad the min of what's available and what we're permitted to read + chunklen = available + if chunklen > CardI2CMax { + chunklen = CardI2CMax + } + + } + + // Done + return +} + +// OpenI2C opens the card on I2C +func OpenI2C(port string, portConfig int) (context *Context, err error) { + + // Create the context structure + context = &Context{} + context.Debug = InitialDebugMode + context.lastRequestSeqno = 0 + + // Open + context.portIsOpen = false + + // Use default if not specified + if port == "" { + port, portConfig = i2cDefault() + } + context.port = port + context.portConfig = portConfig + + // Set up class functions + context.PortEnumFn = i2cPortEnum + context.PortDefaultsFn = i2cDefault + context.CloseFn = cardCloseI2C + context.ReopenFn = cardReopenI2C + context.ResetFn = cardResetI2C + context.TransactionFn = cardTransactionI2C + + // Open the I2C port + err = i2cOpen(port, portConfig) + if err != nil { + if false { + ports, _, _, _ := I2CPorts() + fmt.Printf("Available ports: %v\n", ports) + } + err = fmt.Errorf("i2c init error: %s", err) + return + } + + // Open + context.portIsOpen = true + + // Done + return +} + +// Reset the port +func (context *Context) Reset(portConfig int) (err error) { + context.resetRequired = false + if context.ResetFn == nil { + return + } + return context.ResetFn(context, portConfig) +} + +// Close the port +func (context *Context) Close() { + context.CloseFn(context) +} + +// Close serial +func cardCloseSerial(context *Context) { + if !context.portIsOpen { + if debugSerialIO { + fmt.Printf("cardCloseSerial: port not open\n") + } + } else { + if debugSerialIO { + fmt.Printf("cardCloseSerial: closed\n") + } + context.serialPort.Close() + context.portIsOpen = false + } +} + +// Close I2C +func cardCloseI2C(context *Context) { + _ = i2cClose() + context.portIsOpen = false +} + +// ReopenIfRequired reopens the port but only if required +func (context *Context) ReopenIfRequired(portConfig int) (err error) { + if context.reopenRequired { + err = context.ReopenFn(context, portConfig) + } + return +} + +// Reopen the port +func (context *Context) Reopen(portConfig int) (err error) { + context.reopenRequired = false + err = context.ReopenFn(context, portConfig) + return +} + +// Reopen serial +func CardReopenSerial(context *Context, portConfig int) (err error) { + + // Close if open + cardCloseSerial(context) + + // Handle deferred insertion + if context.serialUseDefault { + context.serialName, context.serialConfig.BaudRate = serialDefault() + } + if context.serialName == "" { + return fmt.Errorf("error opening serial port: serial device not available %s", note.ErrCardIo) + } + + if portConfig != 0 { + context.serialConfig.BaudRate = portConfig + } + // Set default speed if not set + if context.serialConfig.BaudRate == 0 { + _, context.serialConfig.BaudRate = serialDefault() + } + + // Open the serial port + if debugSerialIO { + fmt.Printf("CardReopenSerial: about to open '%s'\n", context.serialName) + } + context.serialPort, err = serial.Open(context.serialName, &context.serialConfig) + if debugSerialIO { + fmt.Printf(" back with err = %v\n", err) + } + if err != nil { + return fmt.Errorf("error opening serial port %s at %d: %s %s", context.serialName, context.serialConfig.BaudRate, err, note.ErrCardIo) + } + + context.portIsOpen = true + + // Done with the reopen + context.reopenRequired = false + + // Unless we've been instructed not to reset on open, reset serial to a known good state + if context.reopenBecauseOfOpen { + context.reopenBecauseOfOpen = false + if InitialResetMode { + err = cardResetSerial(context, portConfig) + } + } + + // Done + return err +} + +// Reopen I2C +func cardReopenI2C(context *Context, portConfig int) (err error) { + fmt.Printf("error i2c reopen not yet supported since I can't test it yet\n") + return +} + +// SerialDefaults returns the default serial parameters +func SerialDefaults() (port string, portConfig int) { + return serialDefault() +} + +// I2CDefaults returns the default serial parameters +func I2CDefaults() (port string, portConfig int) { + return i2cDefault() +} + +// SerialPorts returns the list of available serial ports +func SerialPorts() (allports []string, usbports []string, notecardports []string, err error) { + return serialPortEnum() +} + +// I2CPorts returns the list of available I2C ports +func I2CPorts() (allports []string, usbports []string, notecardports []string, err error) { + return i2cPortEnum() +} + +// TransactionRequest performs a card transaction with a Req structure +func (context *Context) TransactionRequest(req Request) (rsp Request, err error) { + return context.transactionRequest(req, false, 0) +} + +// TransactionRequestToPort performs a card transaction with a Req structure, to a specified port +func (context *Context) TransactionRequestToPort(req Request, portConfig int) (rsp Request, err error) { + return context.transactionRequest(req, true, portConfig) +} + +// transactionRequest performs a card transaction with a Req structure, to the current or specified port +func (context *Context) transactionRequest(req Request, multiport bool, portConfig int) (rsp Request, err error) { + reqJSON, err2 := note.JSONMarshal(req) + if err2 != nil { + err = fmt.Errorf("error marshaling request for module: %s", err2) + return + } + var rspJSON []byte + rspJSON, err = context.transactionJSON(reqJSON, multiport, portConfig) + if err != nil { + // Give transaction's error precedence, except that if we get an error unmarshaling + // we want to make sure that we indicate to the caller that there was an I/O error (corruption) + err2 := note.JSONUnmarshal(rspJSON, &rsp) + if err2 != nil { + err = fmt.Errorf("%s %s", err, note.ErrCardIo) + } + return + } + err = note.JSONUnmarshal(rspJSON, &rsp) + if err != nil { + err = fmt.Errorf("error unmarshaling reply from module: %s %s: %s", err, note.ErrCardIo, rspJSON) + } + return +} + +// NewRequest creates a new request that is guaranteed to get a response +// from the Notecard. Note that this method is provided merely as syntactic sugar, as of the form +// req := notecard.NewRequest("note.add") +func NewRequest(reqType string) (req map[string]interface{}) { + return map[string]interface{}{ + "req": reqType, + } +} + +// NewCommand creates a new command that requires no response from the notecard. +func NewCommand(reqType string) (cmd map[string]interface{}) { + return map[string]interface{}{ + "cmd": reqType, + } +} + +// NewBody creates a new body. Note that this method is provided +// merely as syntactic sugar, as of the form +// body := note.NewBody() +func NewBody() (body map[string]interface{}) { + return make(map[string]interface{}) +} + +// Request performs a card transaction with a JSON structure and doesn't return a response +// (This is for semantic compatibility with other languages.) +func (context *Context) Request(req map[string]interface{}) (err error) { + _, err = context.Transaction(req) + return +} + +// RequestResponse performs a card transaction with a JSON structure and doesn't return a response +// (This is for semantic compatibility with other languages.) +func (context *Context) RequestResponse(req map[string]interface{}) (rsp map[string]interface{}, err error) { + return context.Transaction(req) +} + +// Response is used in rare cases where there is a transaction that returns multiple responses +func (context *Context) Response() (rsp map[string]interface{}, err error) { + return context.Transaction(nil) +} + +// Transaction performs a card transaction with a JSON structure +func (context *Context) Transaction(req map[string]interface{}) (rsp map[string]interface{}, err error) { + // Handle the special case where we are just processing a response + var reqJSON []byte + if req == nil { + reqJSON = []byte("") + } else { + + // Marshal the request to JSON + reqJSON, err = note.JSONMarshal(req) + if err != nil { + err = fmt.Errorf("error marshaling request for module: %s", err) + return + } + + } + + // Perform the transaction + rspJSON, err2 := context.TransactionJSON(reqJSON) + if err2 != nil { + err = fmt.Errorf("error from TransactionJSON: %s", err2) + return + } + + // Unmarshal for convenience of the caller + err = note.JSONUnmarshal(rspJSON, &rsp) + if err != nil { + err = fmt.Errorf("error unmarshaling reply from module: %s %s: %s", err, note.ErrCardIo, rspJSON) + return + } + + // Done + return +} + +// ReceiveBytes receives arbitrary Bytes from the Notecard +func (context *Context) ReceiveBytes() (rspBytes []byte, err error) { + return context.receiveBytes(0) +} + +// SendBytes sends arbitrary Bytes to the Notecard +func (context *Context) SendBytes(reqBytes []byte) (err error) { + + // Only operate on port 0 + portConfig := 0 + + // Only one caller at a time accessing the I/O port + lockTrans(false, portConfig) + + // Reopen if error + err = context.ReopenIfRequired(portConfig) + if err != nil { + unlockTrans(false, portConfig) + return + } + + // Do a reset if one was pending + if context.resetRequired { + _ = context.Reset(portConfig) + } + + // Do the send, with no response requested and no delays (binary transfer) + _, err = context.TransactionFn(context, portConfig, true, reqBytes, false) + + // Done + unlockTrans(false, portConfig) + return + +} + +// receiveBytes receives arbitrary Bytes from the Notecard, using the current or specified port +func (context *Context) receiveBytes(portConfig int) (rspBytes []byte, err error) { + // Only one caller at a time accessing the I/O port + lockTrans(false, portConfig) + + // Reopen if error + err = context.ReopenIfRequired(portConfig) + if err != nil { + unlockTrans(false, portConfig) + if context.Debug { + fmt.Printf("%s\n", err) + } + return + } + + // Do a reset if one was pending + if context.resetRequired { + _ = context.Reset(portConfig) + } + + // Request is empty + var reqBytes []byte + // Perform the transaction with no delays (binary transfer) + rspBytes, err = context.TransactionFn(context, portConfig, false, reqBytes, false) + + unlockTrans(false, portConfig) + + // Done + return +} + +// TransactionJSON performs a card transaction using raw JSON []bytes +func (context *Context) TransactionJSON(reqJSON []byte) (rspJSON []byte, err error) { + return context.transactionJSON(reqJSON, false, 0) +} + +// TransactionJSONToPort performs a card transaction using raw JSON []bytes to a specified port +func (context *Context) TransactionJSONToPort(reqJSON []byte, portConfig int) (rspJSON []byte, err error) { + return context.transactionJSON(reqJSON, true, portConfig) +} + +// transactionJSON performs a card transaction using raw JSON []bytes, to the current or specified port +func (context *Context) transactionJSON(reqJSON []byte, multiport bool, portConfig int) (rspJSON []byte, err error) { + // Remember in the context if we've ever seen multiport I/O, for timeout computation + if multiport { + context.i2cMultiport = true + } + + // Unmarshal the request to peek inside it. Also, accept a zero-length request as a valid case + // because we use this in the test fixture where we just accept pure responses w/o requests. + var req Request + var noResponseRequested bool + if len(reqJSON) > 0 { + + // Make sure that it is valid JSON, because the transports won't validate this + // and they may misbehave if they do not get a valid JSON response back. + err = note.JSONUnmarshal(reqJSON, &req) + if err != nil { + return + } + + // If this is a hub.set, generate a user agent object if one hasn't already been supplied + if !context.DisableUA && (req.Req == ReqHubSet || req.Cmd == ReqHubSet) && req.Body == nil { + ua := context.UserAgent() + if ua != nil { + req.Body = &ua + reqJSON, _ = note.JSONMarshal(req) + } + } + + // Determine whether or not a response will be expected from the notecard by + // examining the req and cmd fields + noResponseRequested = req.Req == "" && req.Cmd != "" + + if !DoNotReterminateJSON { + // Make sure that the JSON has a single \n terminator + for { + if strings.HasSuffix(string(reqJSON), "\n") { + reqJSON = []byte(strings.TrimSuffix(string(reqJSON), "\n")) + continue + } + if strings.HasSuffix(string(reqJSON), "\r") { + reqJSON = []byte(strings.TrimSuffix(string(reqJSON), "\r")) + continue + } + break + } + reqJSON = []byte(string(reqJSON) + "\n") + } + } + + // Debug + if context.Debug { + var j []byte + if context.Pretty { + j, _ = note.JSONMarshalIndent(req, "", " ") + } else { + j, _ = note.JSONMarshal(req) + } + fmt.Printf("%s\n", string(j)) + } + + // If it is a request (as opposed to a command), include a CRC so that the + // request might be retried if it is received in a corrupted state. (We can + // only do this on requests because for cmd's there is no 'response channel' + // where we can find out that the cmd failed. Note that a Seqno is included + // as part of the CRC data so that two identical requests occurring within the + // modulus of seqno never are mistaken as being the same request being retried. + lastRequestRetries := 0 + lastRequestCrcAdded := false + if !noResponseRequested { + reqJSON = crcAdd(reqJSON, context.lastRequestSeqno) + lastRequestCrcAdded = true + } + + // Only one caller at a time accessing the I/O port + lockTrans(multiport, portConfig) + + // Transaction retry loop. Note that "err" must be set before breaking out of loop + err = nil + for lastRequestRetries <= requestRetriesAllowed { + + // Only do reopen/reset in the single-port case, because we may not be talking to the port in error + if !multiport { + + // Reopen if error + err = context.ReopenIfRequired(portConfig) + if err != nil { + unlockTrans(multiport, portConfig) + if context.Debug { + fmt.Printf("%s\n", err) + } + return + } + + // Do a reset if one was pending + if context.resetRequired { + _ = context.Reset(portConfig) + } + + } + + // Perform the transaction with delays (JSON requires pacing for the Notecard) + rspJSON, err = context.TransactionFn(context, portConfig, noResponseRequested, reqJSON, true) + if err != nil { + // We can defer the error if a single port, but we need to reset it NOW if multiport + if multiport { + if context.ResetFn != nil { + _ = context.ResetFn(context, portConfig) + } + } else { + context.resetRequired = true + } + } + + // If no response expected, we won't be retrying + if noResponseRequested { + break + } + + // Decode the response to create an error if the response JSON was badly formatted. + // do this because it's SUPER inconvenient to always be checking for a response error + // vs an error on the transaction itself + if err == nil { + var rsp Request + err = note.JSONUnmarshal(rspJSON, &rsp) + if err != nil { + err = fmt.Errorf("error unmarshaling reply from module: %s %s: %s", err, note.ErrCardIo, rspJSON) + } else { + if rsp.Err != "" { + if req.Req == "" { + err = fmt.Errorf("%s", rsp.Err) + } else { + err = fmt.Errorf("%s: %s", req.Req, rsp.Err) + } + } + } + } + + // Don't retry these transactions for obvious reasons + if req.Req == ReqCardRestore || req.Req == ReqCardRestart { + break + } + + // If an I/O error, retry + if note.ErrorContains(err, note.ErrCardIo) && !note.ErrorContains(err, note.ErrReqNotSupported) { + // We can defer the error if a single port, but we need to reset it NOW if multiport + if multiport { + if context.ResetFn != nil { + _ = context.ResetFn(context, portConfig) + } + } else { + context.resetRequired = true + } + lastRequestRetries++ + if context.Debug { + fmt.Printf("retrying I/O error detected by host: %s\n", err) + } + time.Sleep(500 * time.Millisecond) + continue + } + + // If an error, stop transaction processing here + if err != nil { + break + } + + // If we sent a CRC in the request, examine the response JSON to see if + // it has a CRC error. Note that the CRC is stripped from the + // rspJSON as a side-effect of this method. + if lastRequestCrcAdded { + rspJSON, err = crcError(rspJSON, context.lastRequestSeqno) + if err != nil { + lastRequestRetries++ + if context.Debug { + fmt.Printf("retrying: %s\n", err) + } + time.Sleep(500 * time.Millisecond) + continue + } + + } + + // Transaction completed + break + + } + + // Bump the request sequence number now that we've processed this request, success or error + context.lastRequestSeqno++ + + // If this was a card restore, we want to hold everyone back if we reset the card if it + // isn't a multiport case. But in multiport, we only want to hold this caller back. + if (req.Req == ReqCardRestore) && req.Reset { + // Special case card.restore, reset:true does not cause a reboot. + unlockTrans(multiport, portConfig) + } else if context.isLocal && (req.Req == ReqCardRestore || req.Req == ReqCardRestart) { + if multiport { + unlockTrans(multiport, portConfig) + time.Sleep(12 * time.Second) + } else { + context.reopenRequired = true + time.Sleep(8 * time.Second) + unlockTrans(multiport, portConfig) + } + } else { + unlockTrans(multiport, portConfig) + } + + // If no response, we're done + if noResponseRequested { + rspJSON = []byte("{}") + return + } + + // Debug + if context.Debug { + responseJSON := rspJSON + if context.Pretty { + var rsp Request + e := note.JSONUnmarshal(responseJSON, &rsp) + if e == nil { + prettyJSON, e := note.JSONMarshalIndent(rsp, " ", " ") + if e == nil { + fmt.Printf("==> ") + responseJSON = append(prettyJSON, byte('\n')) + } + } + } + fmt.Printf("%s", string(responseJSON)) + } + + // Done + return +} + +// Perform a card transaction over serial under the assumption that request already has '\n' terminator +func cardTransactionSerial(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) { + // Exit if not open + if !context.portIsOpen { + err = fmt.Errorf("port not open " + note.ErrCardIo) + cardReportError(context, err) + return + } + + // Initialize timing parameters + if RequestSegmentMaxLen < 0 { + RequestSegmentMaxLen = CardRequestSerialSegmentMaxLen + } + if RequestSegmentDelayMs < 0 { + RequestSegmentDelayMs = CardRequestSerialSegmentDelayMs + } + + // Set the serial read timeout to 30 seconds, preventing reads under windows from stalling indefinitely on a serial error. + _ = context.serialPort.SetReadTimeout(30 * time.Second) + + // Handle the special case where we are looking only for a reply + if len(reqJSON) > 0 { + + // Transmit the request in segments so as not to overwhelm the notecard's interrupt buffers + segOff := 0 + segLeft := len(reqJSON) + for { + segLen := segLeft + if segLen > RequestSegmentMaxLen { + segLen = RequestSegmentMaxLen + } + if debugSerialIO { + fmt.Printf("cardTransactionSerial: about to write %d bytes\n", segLen) + } + serialIOBegin(context, context.GetTransactionTimeoutMs()) + _, err = context.serialPort.Write(reqJSON[segOff : segOff+segLen]) + err = serialIOEnd(context, err) + if debugSerialIO { + fmt.Printf(" back with err = %v\n", err) + } + if err != nil { + err = fmt.Errorf("error transmitting to module: %s %s", err, note.ErrCardIo) + cardReportError(context, err) + return + } + segOff += segLen + segLeft -= segLen + if segLeft == 0 { + break + } + if delay { + time.Sleep(time.Duration(RequestSegmentDelayMs) * time.Millisecond) + } + } + + } + + // If no response, we're done + if noResponse { + return + } + + // Read the reply until we get '\n' at the end + waitBegan := time.Now() + waitExpires := waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) + for { + var length int + buf := make([]byte, 2048) + if debugSerialIO { + fmt.Printf("cardTransactionSerial: about to read up to %d bytes\n", len(buf)) + } + readBeganMs := int(time.Now().UnixNano() / 1000000) + waitRemainingMs := int(time.Until(waitExpires).Milliseconds()) + serialIOBegin(context, waitRemainingMs) + length, err = context.serialPort.Read(buf) + err = serialIOEnd(context, err) + readElapsedMs := int(time.Now().UnixNano()/1000000) - readBeganMs + if debugSerialIO { + fmt.Printf(" back after %d ms with len = %d err = %v\n", readElapsedMs, length, err) + } + if false { + err2 := err + if err2 == nil { + err2 = fmt.Errorf("none") + } + fmt.Printf("req: elapsed:%d len:%d err:%s '%s'\n", readElapsedMs, length, err2, string(buf[:length])) + } + if readElapsedMs == 0 && length == 0 && err == io.EOF { + // On Linux, hardware port failures come back simply as immediate EOF + err = fmt.Errorf("hardware failure") + } + if err != nil { + if err == io.EOF { + // Just a read timeout + continue + } + // Ignore [flaky, rare, Windows] hardware errors for up to several seconds + if (time.Now().Unix() - waitBegan.Unix()) > int64(IgnoreWindowsHWErrSecs) { + err = fmt.Errorf("error reading from module: %s %s", err, note.ErrCardIo) + cardReportError(context, err) + return + } + time.Sleep(1 * time.Second) + continue + } + rspJSON = append(rspJSON, buf[:length]...) + if !strings.Contains(string(rspJSON), "\n") { + continue + } + + // We now have at least one whole line. If we're just gathering a reply, we're done. + if len(reqJSON) == 0 { + break + } + + // At this point, if we split the string at \n its len must be >= 2 + // If the json didn't END in \n, we are still collecting a partial line + lines := strings.Split(string(rspJSON), "\n") + lastLine := lines[len(lines)-1] + secondToLastLine := lines[len(lines)-2] + if lastLine != "" { + // The reply should be only a single line. However, if the user had been + // in trace mode (likely on USB) we may be receiving trace lines that + // were sent to us and inserted into the serial buffer prior to the JSON reply. + rspJSON = []byte(lastLine) + continue + } + + // Skip the line if it's empty or doesn't look like JSON + if len(secondToLastLine) == 0 || secondToLastLine[0] != '{' { + rspJSON = []byte{} + continue + } + + // ** We now have a clean response in rspJSON ** + + // We're done if it's not a heartbeat + fn := context.HeartbeatFn + if fn == nil { + break + } + m := make(map[string]string) + if json.Unmarshal(rspJSON, &m) != nil { + break + } + v, errPresent := m["err"] + if !errPresent { + break + } + if !strings.Contains(v, note.ErrCardHeartbeat) { + break + } + + // Call the heartbeat function, and abort if it requests that we do so + if fn(context, context.HeartbeatCtx, rspJSON) { + err = fmt.Errorf("aborted by heartbeat function") + cardReportError(context, err) + return + } + + // Reset the JSON and timeout and start again + rspJSON = []byte{} + waitBegan = time.Now() + waitExpires = waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) + } + + // Done + return +} + +// Perform a card transaction over I2C under the assumption that request already has '\n' terminator +func cardTransactionI2C(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) { + // Initialize timing parameters + if RequestSegmentMaxLen < 0 { + RequestSegmentMaxLen = CardRequestI2CSegmentMaxLen + } + if RequestSegmentDelayMs < 0 { + RequestSegmentDelayMs = CardRequestI2CSegmentDelayMs + } + + // Transmit the request in chunks, but also in segments so as not to overwhelm the notecard's interrupt buffers + chunkoffset := 0 + jsonbufLen := len(reqJSON) + sentInSegment := 0 + for jsonbufLen > 0 { + chunklen := CardI2CMax + if jsonbufLen < chunklen { + chunklen = jsonbufLen + } + err = i2cWriteBytes(reqJSON[chunkoffset:chunkoffset+chunklen], portConfig) + if err != nil { + err = fmt.Errorf("write error: %s %s", err, note.ErrCardIo) + return + } + chunkoffset += chunklen + jsonbufLen -= chunklen + sentInSegment += chunklen + if delay { + if sentInSegment > RequestSegmentMaxLen { + sentInSegment = 0 + time.Sleep(time.Duration(RequestSegmentDelayMs) * time.Millisecond) + } + time.Sleep(time.Duration(RequestSegmentDelayMs) * time.Millisecond) + } + } + + // If no response, we're done + if noResponse { + return + } + + // Loop, building a reply buffer out of received chunks. We'll build the reply in the same + // buffer we used to transmit, and will grow it as necessary. + jsonbufLen = 0 + receivedNewline := false + chunklen := 0 + waitBegan := time.Now() + waitExpires := waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) + for { + + // Read the next chunk + readbuf, available, err2 := i2cReadBytes(chunklen, portConfig) + if err2 != nil { + err = fmt.Errorf("read error: %s %s", err2, note.ErrCardIo) + return + } + + // Append to the JSON being accumulated + rspJSON = append(rspJSON, readbuf...) + readlen := len(readbuf) + jsonbufLen += readlen + + // If we received something, reset the expiration to what we'd expect for just the + // I/O portion of a transaction. + if readlen > 0 { + waitExpires = time.Now().Add(time.Duration(5) * time.Second) + } + + // If the last byte of the chunk is \n, chances are that we're done. However, just so + // that we pull everything pending from the module, we only exit when we've received + // a newline AND there's nothing left available from the module. + if readlen > 0 && readbuf[readlen-1] == '\n' { + receivedNewline = true + } + + // For the next iteration, reaad the min of what's available and what we're permitted to read + chunklen = available + if chunklen > CardI2CMax { + chunklen = CardI2CMax + } + + // If there's something available on the notecard for us to receive, do it + if chunklen > 0 { + continue + } + + // See if we're done + if !receivedNewline { + + // If we've timed out and nothing's available, exit + expired := time.Now().After(waitExpires) + if context.i2cMultiport { + expired = time.Now().After(waitBegan.Add(time.Duration(90) * time.Second)) + } + if expired { + err = fmt.Errorf("transaction timeout (%d bytes received before timeout) %s", jsonbufLen, note.ErrCardIo+note.ErrTimeout) + return + } + + // Continue receiving + continue + } + + // ** We now have a clean response in rspJSON ** + + // We're done if it's not a heartbeat + fn := context.HeartbeatFn + if fn == nil { + break + } + m := make(map[string]string) + if json.Unmarshal(rspJSON, &m) != nil { + break + } + v, errPresent := m["err"] + if !errPresent { + break + } + if !strings.Contains(v, note.ErrCardHeartbeat) { + break + } + + // Call the heartbeat function, and abort if it requests that we do so + if fn(context, context.HeartbeatCtx, rspJSON) { + err = fmt.Errorf("aborted by heartbeat function") + cardReportError(context, err) + return + } + + // Reset the JSON and timeout and start again + rspJSON = []byte{} + waitBegan = time.Now() + waitExpires = waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) + } + + // Done + return +} + +// OpenLease opens a remote card with a lease +func OpenLease(leaseScope string, leaseMins int) (context *Context, err error) { + + // Create the context structure + context = &Context{} + context.Debug = InitialDebugMode + context.port = leaseScope + context.portConfig = 0 + context.lastRequestSeqno = 0 + + // Prevent accidental reservation for excessive durations e.g. 115200 minutes + if leaseMins > 120 { + err = fmt.Errorf("leasing a notecard has a 120 minute limit, but got %d", leaseMins) + return + } + + // Set up class functions + context.CloseFn = leaseClose + context.ReopenFn = leaseReopen + context.TransactionFn = leaseTransaction + context.traceOpenFn = leaseTraceOpen + context.traceReadFn = leaseTraceRead + context.traceWriteFn = leaseTraceWrite + + // Record serial configuration + context.leaseScope = leaseScope + if leaseMins == 0 { + leaseMins = 1 + } + leaseMins = (((leaseMins - 1) / reservationModulusMinutes) + 1) * reservationModulusMinutes + context.leaseExpires = time.Now().Unix() + int64(leaseMins*60) + + // Open the port + err = context.ReopenFn(context, context.portConfig) + if err != nil { + err = fmt.Errorf("error taking out lease: %s %s", err, note.ErrCardIo) + return + } + + // All set + return +} + +// Lock the appropriate mutex for the transaction +func lockTrans(multiport bool, portConfig int) { + if multiport && portConfig >= 0 && portConfig < 128 { + multiportTransLock[portConfig].Lock() + } else { + transLock.Lock() + } +} + +// Unlock the appropriate mutex for the transaction +func unlockTrans(multiport bool, portConfig int) { + if multiport && portConfig >= 0 && portConfig < 128 { + multiportTransLock[portConfig].Unlock() + } else { + transLock.Unlock() + } +} + +// Get the CallerID for this requestor, increasing the likelihood of getting the same +// reservation between tests which may be run across different machines and across +// different processes on the same machine. +func callerID() (id string) { + + // See if it's specified in the environment + id = os.Getenv("NOTEFARM_CALLERID") + if id != "" { + return + } + + user, err := user.Current() + if user != nil && err == nil { + id = user.Username + } + + hostname, err := os.Hostname() + if hostname != "" && err == nil { + id += "@" + hostname + } + + if id == "" { + // Use the mac address if we have no other name + interfaces, err := net.Interfaces() + if err == nil { + for _, i := range interfaces { + if i.Flags&net.FlagUp != 0 && !bytes.Equal(i.HardwareAddr, nil) { + // Don't use random as we have a real address + id = i.HardwareAddr.String() + break + } + } + } + } + + return +} + +// Serial trace open +func serialTraceOpen(context *Context) (err error) { + return +} + +// Serial trace read function +func serialTraceRead(context *Context) (data []byte, err error) { + + // Exit if not open + if !context.portIsOpen { + return data, fmt.Errorf("port not open " + note.ErrCardIo) + } + + // Do the read + var length int + buf := make([]byte, 2048) + readBeganMs = int(time.Now().UnixNano() / 1000000) + length, err = context.serialPort.Read(buf) + readElapsedMs := int(time.Now().UnixNano()/1000000) - readBeganMs + if false { + fmt.Printf("mon: elapsed:%d len:%d err:%s '%s'\n", readElapsedMs, length, err, string(buf[:length])) + } + if readElapsedMs == 0 && length == 0 && err == io.EOF { + // On Linux, hardware port failures come back simply as immediate EOF + err = fmt.Errorf("hardware failure") + } + if readElapsedMs == 0 && length == 0 { + // On Linux, sudden unplug comes back simply as immediate '' + err = fmt.Errorf("hardware unplugged or rebooted probably") + } + if err != nil { + if err == io.EOF { + // Just a read timeout + return data, nil + } + return data, fmt.Errorf("%s %s", err, note.ErrCardIo) + } + + return buf[:length], nil +} + +// Serial trace write function +func serialTraceWrite(context *Context, data []byte) { + context.serialPort.Write(data) +} + +// Add a crc to the JSON transaction +func crcAdd(reqJson []byte, seqno int) []byte { + + // Exit if invalid + if len(reqJson) < 2 { + return reqJson + } + + // Extract any terminator present so it isn't included in the checksum + reqJsonStr := string(reqJson) + terminator := "" + temp := strings.Split(reqJsonStr, "}") + if len(temp) > 1 { + terminator = temp[len(temp)-1] + reqJsonStr = strings.Join(temp[0:len(temp)-1], "}") + "}" + } + + // Compute the CRC of the JSON + crc := crc32.ChecksumIEEE([]byte(reqJsonStr)) + + // Strip the suffix and prepare for crc concatenation. Note that + // the decode side assumes that either a space or comma was added + reqJsonStr = strings.TrimSuffix(reqJsonStr, "}") + if !strings.Contains(reqJsonStr, ":") { + reqJsonStr += " " + } else { + reqJsonStr += "," + } + + // Append the CRC + reqJsonStr += fmt.Sprintf("\"crc\":\"%04X:%08X\"}", seqno, crc) + + // Done + return []byte(reqJsonStr + terminator) +} + +// Test and remove CRC from transaction JSON +// Note that if a CRC field is not present in the JSON, it is considered +// a valid transaction because old Notecards do not have the code +// with which to calculate and piggyback a CRC field. Note that the +// CRC is stripped from the input JSON regardless of whether or not +// there was an error. +func crcError(rspJson []byte, shouldBeSeqno int) (retJson []byte, err error) { + + // Exit silently if invalid + if len(rspJson) < 2 { + return rspJson, nil + } + + // Extract any terminator present so it isn't included in the checksum + rspJsonStr := string(rspJson) + terminator := "" + temp := strings.Split(rspJsonStr, "}") + if len(temp) > 1 { + terminator = temp[len(temp)-1] + rspJsonStr = strings.Join(temp[0:len(temp)-1], "}") + "}" + } + + // Minimum valid JSON is "{}" (2 bytes) and must end with a closing "}". If + // it's not there, it's ok because it could be an old notecard + crcFieldLength := 22 // ,"crc":"SSSS:CCCCCCCC" + if len(rspJsonStr) < crcFieldLength+2 || !strings.HasSuffix(rspJsonStr, "}") { + return rspJson, nil + } + + // Split the string into its json and non-json components + t1 := strings.Split(rspJsonStr, " \"crc\":\"") + if len(t1) != 2 { + t1 = strings.Split(rspJsonStr, ",\"crc\":\"") + } + if len(t1) != 2 { + return rspJson, nil + } + stripped := t1[0] + "}" + t2 := strings.Split(t1[1], ":") + if len(t2) != 2 { + return rspJson, fmt.Errorf("badly formatted CRC seqno") + } + seqnoHex := t2[0] + seqno, err := strconv.ParseInt(seqnoHex, 16, 64) + if err != nil { + return rspJson, fmt.Errorf("badly formatted hex CRC seqno") + } + shouldBeCrcHex := strings.TrimSuffix(t2[1], "\"}") + shouldBeCrc, err := strconv.ParseInt(shouldBeCrcHex, 16, 64) + if err != nil { + return rspJson, fmt.Errorf("badly formatted hex CRC") + } + + // Compute the CRC of the JSON + crc := crc32.ChecksumIEEE([]byte(stripped)) + + // Test values + if shouldBeSeqno != int(seqno) { + return rspJson, fmt.Errorf("sequence number mismatch (%d != %d)", seqno, shouldBeSeqno) + } + if uint32(shouldBeCrc) != crc { + return rspJson, fmt.Errorf("CRC mismatch") + } + + // Done + return []byte(stripped + terminator), nil +} diff --git a/note-go/notecard/play.go b/note-go/notecard/play.go new file mode 100644 index 0000000..87767df --- /dev/null +++ b/note-go/notecard/play.go @@ -0,0 +1,223 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +import ( + "bufio" + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/blues/note-go/note" +) + +// Interactive I/O +var ( + iInputHandlerActive = false + iWatch = false + uiLock sync.RWMutex +) + +// Interactive enters interactive request/response mode, disabling trace in case +// that was the last mode entered +func (context *Context) Interactive(watch bool, watchLevel int, prompt bool, watchCommand string, quitCommand string) (err error) { + var rsp Request + var colWidth int + var cols int + var subsystem []string + var subsystemDisplayName []string + + // Set the watch on/off based upon whether there is a command + iWatch = watch + + // Initialize for watching + linesDisplayed := 0 + + // Get the template for the trace log results. We need to get this regardless of whether watch + // is initially on because it might be turned on later + rsp, err = context.TransactionRequest(Request{Req: "note.get", NotefileID: "_synclog.qi", Start: true}) + if err != nil { + return + } + for _, entry := range strings.Split(rsp.Status, ",") { + str := strings.Split(entry, ":") + if len(str) >= 2 { + cols++ + subsystem = append(subsystem, str[0]) + subsystemDisplayName = append(subsystemDisplayName, str[1]) + if len(str[1]) > colWidth { + colWidth = len(str[1]) + } + } + } + colWidth += 4 + + if iWatch { + + // Print an opening banner if necessary + now := time.Now().Local().Format("03:04:05 PM MST") + rsp, err = context.TransactionRequest(Request{Req: "note.get", NotefileID: "_synclog.qi"}) + if err == nil && rsp.Body == nil { + fmt.Printf("%s waiting for sync activity\n", now) + } + + } + + // Now that we know we can speak to the notecard, spawn the input handlers + if !iInputHandlerActive { + go interactiveInputHandler(context, prompt, watchCommand, quitCommand) + for !iInputHandlerActive { + time.Sleep(100 * time.Millisecond) + } + } + + // Loop, printing data + prevTimeSecs := int64(0) + for err == nil || note.ErrorContains(err, note.ErrNoteNoExist) { + + // Exit if the handler exited + if !iInputHandlerActive { + err = nil + break + } + + // Loop if not watching + if !iWatch { + time.Sleep(500 * time.Millisecond) + continue + } + + // Get the next entry + rsp, err = context.TransactionRequest(Request{Req: "note.get", NotefileID: "_synclog.qi", Delete: true}) + if err != nil { + if !note.ErrorContains(err, note.ErrNoteNoExist) { + uiLock.Lock() + fmt.Printf("\r%s\n", err) + uiLock.Unlock() + } + time.Sleep(1000 * time.Millisecond) + continue + } + if rsp.Body == nil { + time.Sleep(1000 * time.Millisecond) + continue + } + var bodyJSON []byte + bodyJSON, err = note.ObjectToJSON(rsp.Body) + if err != nil { + break + } + var body SyncLogBody + err = note.JSONUnmarshal(bodyJSON, &body) + if err != nil { + break + } + if body.DetailLevel > uint32(watchLevel) { + continue + } + + // Lock output for a moment + uiLock.Lock() + + // Output a header if it will help readability + if linesDisplayed%250 == 0 { + fmt.Printf("\n%s ", strings.Repeat(" ", len(time.Now().Local().Format("03:04:05 PM MST")))) + for i := 0; i < cols; i++ { + fmt.Printf("%s%s", + subsystemDisplayName[i], + strings.Repeat(" ", colWidth-len(subsystemDisplayName[i]))) + } + fmt.Printf("\n\n") + } else { + // Output a spacer if there is a distance in time + if body.TimeSecs != 0 && body.TimeSecs > prevTimeSecs+30 { + fmt.Printf("\n") + } + } + linesDisplayed++ + + // Display either the time OR the 'secs since boot' if time isn't available + prevTimeSecs = body.TimeSecs + timebuf := time.Unix(int64(body.TimeSecs), 0).Local().Format("03:04:05 PM MST") + if body.TimeSecs == 0 { + str := fmt.Sprintf("%d", body.BootMs) + timebuf = fmt.Sprintf("%s%s", str, strings.Repeat(" ", len(timebuf)-len(str))) + } + + // Display indentation + fmt.Printf("\r%s ", timebuf) + indentstr := "." + strings.Repeat(" ", colWidth-1) + for _, ss := range subsystem { + if ss == body.Subsystem { + break + } + fmt.Printf("%s", indentstr) + } + + // Display the message + if watchLevel < SyncLogLevelProg { + fmt.Printf("%s\n", note.ErrorClean(fmt.Errorf(body.Text))) + } else { + fmt.Printf("%s\n", body.Text) + } + + // Release the UI + uiLock.Unlock() + + } + + // Done + iInputHandlerActive = false + return +} + +// Watch for console input +func interactiveInputHandler(context *Context, prompt bool, watchCommand string, quitCommand string) { + // Mark as active, in case we invoke this multiple times + iInputHandlerActive = true + + // Create a scanner to watch stdin + scanner := bufio.NewScanner(os.Stdin) + var message string + + // Send the command to the module + for iInputHandlerActive { + if prompt { + uiLock.Lock() + fmt.Printf("> ") + uiLock.Unlock() + } + scanner.Scan() + message = scanner.Text() + if message == quitCommand { + iInputHandlerActive = false + break + } + if message == "" { + continue + } + uiLock.Lock() + if watchCommand != "" && message == watchCommand { + if iWatch { + iWatch = false + fmt.Printf("watch off\n") + } else { + iWatch = true + fmt.Printf("watch ON\n") + } + uiLock.Unlock() + continue + } + rspJSON, err := context.TransactionJSON([]byte(message)) + if err != nil { + fmt.Printf("error: %s\n", err) + } else { + fmt.Printf("%s", rspJSON) + } + uiLock.Unlock() + } +} diff --git a/note-go/notecard/request.go b/note-go/notecard/request.go new file mode 100644 index 0000000..1515aa2 --- /dev/null +++ b/note-go/notecard/request.go @@ -0,0 +1,536 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +import ( + "errors" + + "github.com/blues/note-go/note" +) + +// Request Types (L suffix means Legacy as of 2019-11-18, and can be removed after we ship) + +// ReqFileAdd (golint) +const ReqFileAdd = "file.add" + +// ReqFileSet (golint) +const ReqFileSet = "file.set" + +// ReqFileDelete (golint) +const ReqFileDelete = "file.delete" + +// ReqFileClear (golint) +const ReqFileClear = "file.clear" + +// ReqFileGetL (golint) +const ReqFileGetL = "file.get" + +// ReqFileChanges (golint) +const ReqFileChanges = "file.changes" + +// ReqFileChangesPending (golint) +const ReqFileChangesPending = "file.changes.pending" + +// ReqFileSync (golint) +const ReqFileSync = "file.sync" + +// ReqFileStats (golint) +const ReqFileStats = "file.stats" + +// ReqNotesGetL (golint) +const ReqNotesGetL = "notes.get" + +// ReqNoteChanges (golint) +const ReqNoteChanges = "note.changes" + +// ReqNoteAdd (golint) +const ReqNoteAdd = "note.add" + +// ReqNoteTemplate (golint) +const ReqNoteTemplate = "note.template" + +// ReqNoteGet (golint) +const ReqNoteGet = "note.get" + +// ReqNoteUpdate (golint) +const ReqNoteUpdate = "note.update" + +// ReqNoteDelete (golint) +const ReqNoteDelete = "note.delete" + +// ReqNoteEncrypt (golint) +const ReqNoteEncrypt = "note.encrypt" + +// ReqNoteDecrypt (golint) +const ReqNoteDecrypt = "note.decrypt" + +// ReqCardTime (golint) +const ReqCardTime = "card.time" + +// ReqCardRandom (golint) +const ReqCardRandom = "card.random" + +// ReqCardSleep (golint) +const ReqCardSleep = "card.sleep" + +// ReqCardContact (golint) +const ReqCardContact = "card.contact" + +// ReqCardAttn (golint) +const ReqCardAttn = "card.attn" + +// ReqCardStatus (golint) +const ReqCardStatus = "card.status" + +// ReqCardRestart (golint) +const ReqCardRestart = "card.restart" + +// ReqCardCheckpoint (golint) +const ReqCardCheckpoint = "card.checkpoint" + +// ReqCardRestore (golint) +const ReqCardRestore = "card.restore" + +// ReqCardLocation (golint) +const ReqCardLocation = "card.location" + +// ReqCardLocationMode (golint) +const ReqCardLocationMode = "card.location.mode" + +// ReqCardLocationTrack (golint) +const ReqCardLocationTrack = "card.location.track" + +// ReqCardTriangulate (golint) +const ReqCardTriangulate = "card.triangulate" + +// ReqCardTemp (golint) +const ReqCardTemp = "card.temp" + +// ReqCardIllumination (golint) +const ReqCardIllumination = "card.illumination" + +// ReqCardVoltage (golint) +const ReqCardVoltage = "card.voltage" + +// ReqCardPower (golint) +const ReqCardPower = "card.power" + +// ReqCardMotion (golint) +const ReqCardMotion = "card.motion" + +// ReqCardMotionMode (golint) +const ReqCardMotionMode = "card.motion.mode" + +// ReqCardMotionSync (golint) +const ReqCardMotionSync = "card.motion.sync" + +// ReqCardMotionTrack (golint) +const ReqCardMotionTrack = "card.motion.track" + +// ReqCardIO (golint) +const ReqCardIO = "card.io" + +// ReqCardAUX (golint) +const ReqCardAUX = "card.aux" + +// ReqCardAUXSerial (golint) +const ReqCardAUXSerial = "card.aux.serial" + +// ReqCardMonitor (golint) +const ReqCardMonitor = "card.monitor" + +// ReqCardCarrier (golint) +const ReqCardCarrier = "card.carrier" + +// ReqCardTrace (golint) +const ReqCardTrace = "card.trace" + +// ReqCardUsageGet (golint) +const ReqCardUsageGet = "card.usage.get" + +// ReqCardUsageTest (golint) +const ReqCardUsageTest = "card.usage.test" + +// ReqEnvModified (golint) +const ReqEnvModified = "env.modified" + +// ReqEnvGet (golint) +const ReqEnvGet = "env.get" + +// ReqEnvSet (golint) +const ReqEnvSet = "env.set" + +// ReqVarSet (golint) +const ReqVarSet = "var.set" + +// ReqVarGet (golint) +const ReqVarGet = "var.get" + +// ReqVarDelete (golint) +const ReqVarDelete = "var.delete" + +// ReqEnvTemplate (golint) +const ReqEnvTemplate = "env.template" + +// ReqEnvDefault (golint) +const ReqEnvDefault = "env.default" + +// ReqEnvTime (golint) +const ReqEnvTime = "env.time" + +// ReqEnvLocation (golint) +const ReqEnvLocation = "env.location" + +// ReqEnvSync (golint) +const ReqEnvSync = "env.sync" + +// ReqWeb (golint) +const ReqWeb = "web" + +// ReqCardBinary (golint) +const ReqCardBinary = "card.binary" + +// ReqCardBinaryGet (golint) +const ReqCardBinaryGet = "card.binary.get" + +// ReqCardBinaryPut (golint) +const ReqCardBinaryPut = "card.binary.put" + +// ReqWebGet (golint) +const ReqWebGet = "web.get" + +// ReqWebPut (golint) +const ReqWebPut = "web.put" + +// ReqWebPost (golint) +const ReqWebPost = "web.post" + +// ReqWebDelete (golint) +const ReqWebDelete = "web.delete" + +// ReqDFUStatus (golint) +const ReqDFUStatus = "dfu.status" + +// ReqDFUGet (golint) +const ReqDFUGet = "dfu.get" + +// ReqDFUPut (golint) +const ReqDFUPut = "dfu.put" + +// ReqCardDFU (golint) +const ReqCardDFU = "card.dfu" + +// ReqEnvVersion (golint) +const ReqEnvVersion = "env.version" + +// ReqCardVersion (golint) +const ReqCardVersion = "card.version" + +// ReqCardBootloader (golint) +const ReqCardBootloader = "card.bootloader" + +// ReqCardTest (golint) +const ReqCardTest = "card.test" + +// ReqCardSetup (golint) +const ReqCardSetup = "card.setup" + +// ReqCardWireless (golint) +const ReqCardWireless = "card.wireless" + +// ReqCardTransport (golint) +const ReqCardTransport = "card.transport" + +// ReqCardWirelessPenalty (golint) +const ReqCardWirelessPenalty = "card.wireless.penalty" + +// ReqCardWirelessSignal (golint) +const ReqCardWirelessSignal = "card.wireless.signal" + +// ReqCardWiFi (golint) +const ReqCardWiFi = "card.wifi" + +// ReqCardLog (golint) +const ReqCardLog = "card.log" + +// ReqHubSync (golint) +const ReqHubSync = "hub.sync" + +// ReqHubSyncL (golint) +const ReqHubSyncL = "service.sync" + +// ReqHubLog (golint) +const ReqHubLog = "hub.log" + +// ReqHubLogL (golint) +const ReqHubLogL = "service.log" + +// ReqHubEnvL (golint) +const ReqHubEnvL = "hub.env" + +// ReqHubEnvLL (golint) +const ReqHubEnvLL = "service.env" + +// ReqHubSet (golint) +const ReqHubSet = "hub.set" + +// ReqHubSetL (golint) +const ReqHubSetL = "service.set" + +// ReqHubGet (golint) +const ReqHubGet = "hub.get" + +// ReqHubGetL (golint) +const ReqHubGetL = "service.get" + +// ReqHubStatus (golint) +const ReqHubStatus = "hub.status" + +// ReqHubStatusL (golint) +const ReqHubStatusL = "service.status" + +// ReqHubSignal (golint) +const ReqHubSignal = "hub.signal" + +// ReqHubSignalL (golint) +const ReqHubSignalL = "service.signal" + +// ReqHubSyncStatus (golint) +const ReqHubSyncStatus = "hub.sync.status" + +// ReqHubSyncStatusL (golint) +const ReqHubSyncStatusL = "service.sync.status" + +// ReqHubDFUGet (golint) +const ReqHubDFUGet = "hub.dfu.get" + +// ReqHubHubDFUGetL (golint) +const ReqHubDFUGetL = "dfu.service.get" + +// ReqHubFileGet (golint) +const ReqHubFileGet = "hub.file.get" + +// Request is the core API request/response data structure +type Request struct { + Req string `json:"req,omitempty"` + Cmd string `json:"cmd,omitempty"` + Err string `json:"err,omitempty"` + RequestID uint32 `json:"id,omitempty"` + Transport string `json:"transport,omitempty"` + NotefileID string `json:"file,omitempty"` + TrackerID string `json:"tracker,omitempty"` + NoteID string `json:"note,omitempty"` + Body *map[string]interface{} `json:"body,omitempty"` + Payload *[]byte `json:"payload,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Start bool `json:"start,omitempty"` + Stop bool `json:"stop,omitempty"` + Delete bool `json:"delete,omitempty"` + USB bool `json:"usb,omitempty"` + Primary bool `json:"primary,omitempty"` + Edge bool `json:"edge,omitempty"` + Connected bool `json:"connected,omitempty"` + Secure bool `json:"secure,omitempty"` + Unsecure bool `json:"unsecure,omitempty"` + Alert bool `json:"alert,omitempty"` + Retry bool `json:"retry,omitempty"` + Signals int32 `json:"signals,omitempty"` + Max int32 `json:"max,omitempty"` + Changes int32 `json:"changes,omitempty"` + Seconds int32 `json:"seconds,omitempty"` + SecondsV string `json:"vseconds,omitempty"` + Minutes int32 `json:"minutes,omitempty"` + MinutesV string `json:"vminutes,omitempty"` + Hours int32 `json:"hours,omitempty"` + HoursV string `json:"vhours,omitempty"` + Days int32 `json:"days,omitempty"` + Result int32 `json:"result,omitempty"` + I2C int32 `json:"i2c,omitempty"` + Status string `json:"status,omitempty"` + Version string `json:"version,omitempty"` + Name string `json:"name,omitempty"` + Label string `json:"label,omitempty"` + Org string `json:"org,omitempty"` + Role string `json:"role,omitempty"` + Email string `json:"email,omitempty"` + Area string `json:"area,omitempty"` + Country string `json:"country,omitempty"` + Zone string `json:"zone,omitempty"` + Mode string `json:"mode,omitempty"` + Host string `json:"host,omitempty"` + Movements string `json:"movements,omitempty"` + ProductUID string `json:"product,omitempty"` + DeviceUID string `json:"device,omitempty"` + RouteUID string `json:"route,omitempty"` + Files *[]string `json:"files,omitempty"` + Names *[]string `json:"names,omitempty"` + FileInfo *map[string]note.NotefileInfo `json:"info,omitempty"` + FileDesc *[]note.NotefileDesc `json:"desc,omitempty"` + Notes *map[string]note.Info `json:"notes,omitempty"` + Pad int32 `json:"pad,omitempty"` + Storage int32 `json:"storage,omitempty"` + LocationOLC string `json:"olc,omitempty"` + Latitude float64 `json:"lat,omitempty"` + Longitude float64 `json:"lon,omitempty"` + LocationTime int64 `json:"ltime,omitempty"` + Value float64 `json:"value,omitempty"` + ValueV string `json:"vvalue,omitempty"` + SN string `json:"sn,omitempty"` + APN string `json:"apn,omitempty"` + Text string `json:"text,omitempty"` + Base int32 `json:"base,omitempty"` + Offset int32 `json:"offset,omitempty"` + Length int32 `json:"length,omitempty"` + Total int32 `json:"total,omitempty"` + BytesSent uint32 `json:"bytes_sent,omitempty"` + BytesReceived uint32 `json:"bytes_received,omitempty"` + BytesSentSecondary uint32 `json:"bytes_sent_secondary,omitempty"` + BytesReceivedSecondary uint32 `json:"bytes_received_secondary,omitempty"` + NotesSent uint32 `json:"notes_sent,omitempty"` + NotesReceived uint32 `json:"notes_received,omitempty"` + SessionsStandard uint32 `json:"sessions_standard,omitempty"` + SessionsSecure uint32 `json:"sessions_secure,omitempty"` + Megabytes uint32 `json:"megabytes,omitempty"` + BytesPerDay int32 `json:"bytes_per_day,omitempty"` + DataRate float64 `json:"rate,omitempty"` + NumBytes int32 `json:"bytes,omitempty"` + Template bool `json:"template,omitempty"` + Allow bool `json:"allow,omitempty"` + Align bool `json:"align,omitempty"` + Limit bool `json:"limit,omitempty"` + Pending bool `json:"pending,omitempty"` + Charging bool `json:"charging,omitempty"` + On bool `json:"on,omitempty"` + Off bool `json:"off,omitempty"` + ReqTime bool `json:"reqtime,omitempty"` + ReqLoc bool `json:"reqloc,omitempty"` + Trace string `json:"trace,omitempty"` + Usage *[]string `json:"usage,omitempty"` + State *[]PinState `json:"state,omitempty"` + Time int64 `json:"time,omitempty"` // Time is defined as uint32 on Notecard and int64 on Notehub. See the rationale below. + Motion uint32 `json:"motion,omitempty"` + VMin float64 `json:"vmin,omitempty"` + VMax float64 `json:"vmax,omitempty"` + VAvg float64 `json:"vavg,omitempty"` + Daily float64 `json:"daily,omitempty"` + Weekly float64 `json:"weekly,omitempty"` + Montly float64 `json:"monthly,omitempty"` + Verify bool `json:"verify,omitempty"` + Confirm bool `json:"confirm,omitempty"` + Port int32 `json:"port,omitempty"` + Set bool `json:"set,omitempty"` + Reset bool `json:"reset,omitempty"` + Flag bool `json:"flag,omitempty"` + Calibration float64 `json:"calibration,omitempty"` + Heartbeat bool `json:"heartbeat,omitempty"` + Threshold int32 `json:"threshold,omitempty"` + Count uint32 `json:"count,omitempty"` + Sync bool `json:"sync,omitempty"` + Live bool `json:"live,omitempty"` + Now bool `json:"now,omitempty"` + Type int32 `json:"type,omitempty"` + Number int64 `json:"number,omitempty"` + SKU string `json:"sku,omitempty"` + OrderingCode string `json:"ordering_code,omitempty"` + Board string `json:"board,omitempty"` + Net *NetInfo `json:"net,omitempty"` + Sensitivity int32 `json:"sensitivity,omitempty"` + Requested int32 `json:"requested,omitempty"` + Completed int32 `json:"completed,omitempty"` + WiFi bool `json:"wifi,omitempty"` + Cell bool `json:"cell,omitempty"` + GPS bool `json:"gps,omitempty"` + LoRa bool `json:"lora,omitempty"` + NTN bool `json:"ntn,omitempty"` + Inbound int32 `json:"inbound,omitempty"` + InboundV string `json:"vinbound,omitempty"` + Outbound int32 `json:"outbound,omitempty"` + OutboundV string `json:"voutbound,omitempty"` + Duration int32 `json:"duration,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + Pressure float64 `json:"pressure,omitempty"` + Humidity float64 `json:"humidity,omitempty"` + MinVersion string `json:"minver,omitempty"` + SSID string `json:"ssid,omitempty"` + Password string `json:"password,omitempty"` + Security string `json:"security,omitempty"` + Key string `json:"key,omitempty"` + Method string `json:"method,omitempty"` + Content string `json:"content,omitempty"` + Min int32 `json:"min,omitempty"` + Add int32 `json:"add,omitempty"` + Encrypt bool `json:"encrypt,omitempty"` + Decrypt bool `json:"decrypt,omitempty"` + Alt bool `json:"alt,omitempty"` + Scan bool `json:"scan,omitempty"` + Journey bool `json:"journey,omitempty"` + UOff bool `json:"uoff,omitempty"` + UMin bool `json:"umin,omitempty"` + UPeriodic bool `json:"uperiodic,omitempty"` + Milliseconds int32 `json:"ms,omitempty"` + Full bool `json:"full,omitempty"` + Async bool `json:"async,omitempty"` + Binary bool `json:"binary,omitempty"` + Cobs int32 `json:"cobs,omitempty"` + Append bool `json:"append,omitempty"` + Details *map[string]interface{} `json:"details,omitempty"` + Tower *note.TowerLocation `json:"tower,omitempty"` + Change float64 `json:"change,omitempty"` + Format string `json:"format,omitempty"` + Voltage float64 `json:"voltage,omitempty"` + MilliampHours float64 `json:"milliamp_hours,omitempty"` + Default bool `json:"default,omitempty"` + In bool `json:"in,omitempty"` +} + +func (req *Request) Error() error { + if req.Err != "" { + return errors.New(req.Err) + } + return nil +} + +// A Note on Time +// The Notecard protocol communicates the Time value as a uint32. However, this is non-standard and problematic for Notehub which would have to +// constantly cast it to the modern Unix standard of int64 (i.e., the time_t type in Posix C libraries.) +// In older OSes, Unix time (time_t) was int32. It was never defined as a uint32. +// Thus by converting the uint32 to int64, it may allow us to delay the 2038 problem to 2106. + +// PinState describes the state of an AUX pin for hardware-related Notecard requests +type PinState struct { + High bool `json:"high,omitempty"` + Low bool `json:"low,omitempty"` + Count []uint32 `json:"count,omitempty"` +} + +// SyncLogLevelMajor is just major events +const SyncLogLevelMajor = 0 + +// SyncLogLevelMinor is just major and minor events +const SyncLogLevelMinor = 1 + +// SyncLogLevelDetail is major, minor, and detailed events +const SyncLogLevelDetail = 2 + +// SyncLogLevelProg is everything plus programmatically-targeted +const SyncLogLevelProg = 3 + +// SyncLogLevelAll is all events +const SyncLogLevelAll = SyncLogLevelProg + +// SyncLogLevelNone is no events +const SyncLogLevelNone = -1 + +// SyncLogNotefile is the special notefile containing sync log info +const SyncLogNotefile = "_synclog.qi" + +// SyncLogBody is the data structure used in the SyncLogNotefile +type SyncLogBody struct { + TimeSecs int64 `json:"time,omitempty"` + BootMs int64 `json:"sequence,omitempty"` + DetailLevel uint32 `json:"level,omitempty"` + Subsystem string `json:"subsystem,omitempty"` + Text string `json:"text,omitempty"` +} diff --git a/note-go/notecard/serial-default.go b/note-go/notecard/serial-default.go new file mode 100644 index 0000000..4690c47 --- /dev/null +++ b/note-go/notecard/serial-default.go @@ -0,0 +1,87 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +import ( + "strings" + + "go.bug.st/serial/enumerator" +) + +// Notecard's USB VID/PID +const ( + bluesincVID = "30A4" + notecardPID = "0001" +) + +// Get the default serial device +func defaultSerialDefault() (device string, speed int) { + // Enum all ports + speed = 115200 + ports, err2 := enumerator.GetDetailedPortsList() + if err2 != nil { + return + } + if len(ports) == 0 { + return + } + + // First, look for the notecard + for _, port := range ports { + if port.IsUSB { + if strings.EqualFold(port.VID, bluesincVID) && strings.EqualFold(port.PID, notecardPID) { + device = port.Name + return + } + } + } + + // Otherwise, look for anything from Blues + for _, port := range ports { + if port.IsUSB && strings.EqualFold(port.VID, bluesincVID) { + device = port.Name + return + } + } + + // Not found + return +} + +// Set or display the serial port +func defaultSerialPortEnum() (allports []string, usbports []string, notecardports []string, err error) { + // Enum all ports + ports, err2 := enumerator.GetDetailedPortsList() + if err2 != nil { + err = err2 + return + } + if len(ports) == 0 { + return + } + + // First, look for the notecard + for _, port := range ports { + allports = append(allports, port.Name) + if port.IsUSB { + usbports = append(usbports, port.Name) + if strings.EqualFold(port.VID, bluesincVID) && strings.EqualFold(port.PID, notecardPID) { + notecardports = append(notecardports, port.Name) + } + } + } + + // Otherwise, look for anything from Blues + if len(notecardports) == 0 { + for _, port := range ports { + if port.IsUSB && strings.EqualFold(port.VID, bluesincVID) { + notecardports = append(notecardports, port.Name) + } + } + } + + // Done + return +} diff --git a/note-go/notecard/serial-unix.go b/note-go/notecard/serial-unix.go new file mode 100644 index 0000000..c1c7de7 --- /dev/null +++ b/note-go/notecard/serial-unix.go @@ -0,0 +1,17 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +//go:build !windows + +package notecard + +// Get the default serial device +func serialDefault() (device string, speed int) { + return defaultSerialDefault() +} + +// Set or display the serial port +func serialPortEnum() (allports []string, usbports []string, notecardports []string, err error) { + return defaultSerialPortEnum() +} diff --git a/note-go/notecard/serial-windows.go b/note-go/notecard/serial-windows.go new file mode 100644 index 0000000..770871c --- /dev/null +++ b/note-go/notecard/serial-windows.go @@ -0,0 +1,24 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +//go:build windows + +// If you have odd serial port behavior (where responses are apparently lost or delayed), try this: +// 1) open Control Panel -> Device Manager -> Ports (COM & LPT) +// 2) right-click for USB Serial Device Properties on the appropriate port +// 3) Port Settings tab +// 4) Click Advanced... button +// 5) UN-CHECK "Use FIFO buffers" + +package notecard + +// Get the default serial device +func serialDefault() (device string, speed int) { + return defaultSerialDefault() +} + +// Set or display the serial port +func serialPortEnum() (allports []string, usbports []string, notecardports []string, err error) { + return defaultSerialPortEnum() +} diff --git a/note-go/notecard/test.go b/note-go/notecard/test.go new file mode 100644 index 0000000..d2a4f61 --- /dev/null +++ b/note-go/notecard/test.go @@ -0,0 +1,98 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +// CardTest is a structure that is returned by the notecard after completing its self-test +type CardTest struct { + DeviceUID string `json:"device,omitempty"` + Error string `json:"err,omitempty"` + Status string `json:"status,omitempty"` + Tests string `json:"tests,omitempty"` + FailTest string `json:"fail_test,omitempty"` + FailReason string `json:"fail_reason,omitempty"` + Info string `json:"info,omitempty"` + BoardVersion uint32 `json:"board,omitempty"` + BoardType uint32 `json:"board_type,omitempty"` + Modem string `json:"modem,omitempty"` + ICCID string `json:"iccid,omitempty"` + ICCID2 string `json:"iccid2,omitempty"` + IMSI string `json:"imsi,omitempty"` + IMSI2 string `json:"imsi2,omitempty"` + IMEI string `json:"imei,omitempty"` + Apn string `json:"apn,omitempty"` + Band string `json:"band,omitempty"` + Channel string `json:"channel,omitempty"` + When uint32 `json:"when,omitempty"` + SKU string `json:"sku,omitempty"` + OrderingCode string `json:"ordering_code,omitempty"` + DefaultProductUID string `json:"default_product,omitempty"` + SIMActivationKey string `json:"key,omitempty"` + SIMless bool `json:"simless,omitempty"` + Station string `json:"station,omitempty"` + Operator string `json:"operator,omitempty"` + Check uint32 `json:"check,omitempty"` + CellUsageBytes uint32 `json:"cell_used,omitempty"` + CellProvisionedTime uint32 `json:"cell_provisioned,omitempty"` + // Firmware info + FirmwareOrg string `json:"org,omitempty"` + FirmwareProduct string `json:"product,omitempty"` + FirmwareVersion string `json:"version,omitempty"` + FirmwareMajor uint32 `json:"ver_major,omitempty"` + FirmwareMinor uint32 `json:"ver_minor,omitempty"` + FirmwarePatch uint32 `json:"ver_patch,omitempty"` + FirmwareBuild uint32 `json:"ver_build,omitempty"` + FirmwareBuilt string `json:"built,omitempty"` + // Certificate and cert info + CertSN string `json:"certsn,omitempty"` + Cert string `json:"cert,omitempty"` + // Card initialization requests + SetupRequests string `json:"setup,omitempty"` + // Detailed information about LSE stability + LSEStability string `json:"lse,omitempty"` + // LoRa notecard provisioning info + DevEui string `json:"deveui,omitempty"` + AppEui string `json:"appeui,omitempty"` + AppKey string `json:"appkey,omitempty"` + FreqPlan string `json:"freqplan,omitempty"` + LWVersion string `json:"lorawan,omitempty"` + PHVersion string `json:"regional,omitempty"` + // For manufacturing + CPN string `json:"cpn,omitempty"` + // For Iridium + IriSku string `json:"iri_sku,omitempty"` + IriSn string `json:"iri_sn,omitempty"` + IriImei string `json:"iri_imei,omitempty"` + IriIccid string `json:"iri_iccid,omitempty"` + // For Starnote + Hardware string `json:"hardware,omitempty"` + Mtu uint16 `json:"mtu,omitempty"` + DownMtu uint16 `json:"down_mtu,omitempty"` + UpMtu uint16 `json:"up_mtu,omitempty"` + Policy string `json:"policy,omitempty"` + Cid uint32 `json:"cid,omitempty"` +} + +// Remove fields that are not useful or are sensitive when externalizing for public consumption +func CardTestExternalized(ct CardTest) CardTest { + ct.BoardVersion = 0 // distracting + ct.BoardType = 0 // distracting + ct.SIMActivationKey = "" // security + ct.Station = "" // privacy + ct.Operator = "" // privacy + ct.Check = 0 // invalid after externalizing + ct.FirmwareOrg = "" // distracting + ct.FirmwareProduct = "" // distracting + ct.FirmwareMajor = 0 // distracting + ct.FirmwareMinor = 0 // distracting + ct.FirmwarePatch = 0 // distracting + ct.FirmwareBuild = 0 // distracting + ct.FirmwareBuilt = "" // distracting + ct.CertSN = "" // security + ct.Cert = "" // security + ct.LSEStability = "" // distracting + ct.SetupRequests = "" // security + ct.LSEStability = "" // distracting + return ct +} diff --git a/note-go/notecard/trace.go b/note-go/notecard/trace.go new file mode 100644 index 0000000..24ffe36 --- /dev/null +++ b/note-go/notecard/trace.go @@ -0,0 +1,106 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +import ( + "bufio" + "fmt" + "os" + "strings" + "time" +) + +// The time when the last read began +var ( + readBeganMs = 0 + inputHandlerActive = false +) + +// Trace the incoming serial output AND connect the input handler +func (context *Context) Trace() (err error) { + + // Tracing only works for USB and AUX ports + if context.traceOpenFn == nil { + return fmt.Errorf("tracing is not available on this port") + } + + // Ensure that we have a reservation + err = context.ReopenIfRequired(context.portConfig) + if err != nil { + return err + } + + // Open the trace port + err = context.traceOpenFn(context) + if err != nil { + cardReportError(context, err) + return + } + + // Spawn the input handler + if !inputHandlerActive { + go inputHandler(context) + } + + // Loop, echoing to the console + for { + + buf, err := context.traceReadFn(context) + if err != nil { + cardReportError(context, err) + time.Sleep(2 * time.Second) + continue + } + + if len(buf) > 0 { + fmt.Printf("%s", buf) + } + + } + +} + +// Watch for console input +func inputHandler(context *Context) { + // Mark as active, in case we invoke this multiple times + inputHandlerActive = true + + // Create a scanner to watch stdin + scanner := bufio.NewScanner(os.Stdin) + var message string + + for { + + scanner.Scan() + message = scanner.Text() + + if strings.HasPrefix(message, "^") { + if !context.portIsOpen { + for _, r := range message[1:] { + switch { + // 'a' - 'z' + case 97 <= r && r <= 122: + ba := make([]byte, 1) + ba[0] = byte(r - 96) + context.traceWriteFn(context, ba) + // 'A' - 'Z' + case 65 <= r && r <= 90: + ba := make([]byte, 1) + ba[0] = byte(r - 64) + context.traceWriteFn(context, ba) + } + } + } + } else { + // Send the command to the module + if !context.portIsOpen { + time.Sleep(250 * time.Millisecond) + } else { + context.traceWriteFn(context, []byte(message)) + context.traceWriteFn(context, []byte("\n")) + } + } + } +} diff --git a/note-go/notecard/ua.go b/note-go/notecard/ua.go new file mode 100644 index 0000000..20bfeda --- /dev/null +++ b/note-go/notecard/ua.go @@ -0,0 +1,49 @@ +// Copyright 2017 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notecard + +import ( + "fmt" + "runtime" + + "github.com/shirou/gopsutil/v3/cpu" + /* "github.com/shirou/gopsutil/v3/host" // Deprecated */ + "github.com/shirou/gopsutil/v3/mem" +) + +// UserAgent generates a User Agent object for a given interface +func (context *Context) UserAgent() (ua map[string]interface{}) { + ua = map[string]interface{}{} + ua["agent"] = "note-go" + ua["compiler"] = fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH) + + ua["req_interface"] = context.iface + if context.isSerial { + ua["req_port"] = context.serialName + } else { + ua["req_port"] = context.port + } + + m, _ := mem.VirtualMemory() + ua["cpu_mem"] = m.Total + + c, _ := cpu.Info() + if len(c) >= 1 { + ua["cpu_mhz"] = int(c[0].Mhz) + ua["cpu_cores"] = int(c[0].Cores) + ua["cpu_vendor"] = c[0].VendorID + ua["cpu_name"] = c[0].ModelName + } + + /* Deprecated + h, _ := host.Info() + ua["os_name"] = h.OS // freebsd, linux + ua["os_platform"] = h.Platform // ubuntu, linuxmint + ua["os_family"] = h.PlatformFamily // debian, rhel + ua["os_version"] = h.PlatformVersion // complete OS version + */ + + return +} diff --git a/note-go/notehub/api/billing.go b/note-go/notehub/api/billing.go new file mode 100644 index 0000000..bd4f2c2 --- /dev/null +++ b/note-go/notehub/api/billing.go @@ -0,0 +1,15 @@ +package api + +// GetBillingAccountResponse v1 +// +// The response object for getting a billing account. +type GetBillingAccountResponse struct { + UID string `json:"uid"` + Name string `json:"name"` + // "billing_admin", "billing_manager", or "project_creator" + Role string `json:"role"` +} + +type GetBillingAccountsResponse struct { + BillingAccounts []GetBillingAccountResponse `json:"billing_accounts"` +} diff --git a/note-go/notehub/api/devices.go b/note-go/notehub/api/devices.go new file mode 100644 index 0000000..8abd504 --- /dev/null +++ b/note-go/notehub/api/devices.go @@ -0,0 +1,165 @@ +package api + +import ( + "strings" + + "github.com/blues/note-go/note" +) + +// GetDevicesResponse v1 +// +// The response object for getting devices. +type GetDevicesResponse struct { + Devices []GetDeviceResponse `json:"devices"` + HasMore bool `json:"has_more"` +} + +// Part of the response object for a device. +type DeviceHealthLogEntry struct { + When string `json:"when"` + Text string `json:"text"` + Alert bool `json:"alert"` +} + +// DeviceResponse v1 +// +// The response object for a device. +type GetDeviceResponse struct { + UID string `json:"uid"` + SerialNumber string `json:"serial_number,omitempty"` + SKU string `json:"sku,omitempty"` + + // RFC3339 timestamps, in UTC. + Provisioned string `json:"provisioned"` + LastActivity *string `json:"last_activity"` + + FirmwareHost string `json:"firmware_host,omitempty"` + FirmwareNotecard string `json:"firmware_notecard,omitempty"` + + Contact *ContactResponse `json:"contact,omitempty"` + + ProductUID string `json:"product_uid"` + FleetUIDs []string `json:"fleet_uids"` + + TowerInfo *TowerInformation `json:"tower_info,omitempty"` + TowerLocation *Location `json:"tower_location,omitempty"` + GPSLocation *Location `json:"gps_location,omitempty"` + TriangulatedLocation *Location `json:"triangulated_location,omitempty"` + BestLocation *Location `json:"best_location,omitempty"` + + Voltage float64 `json:"voltage"` + Temperature float64 `json:"temperature"` + DFUEnv *note.DFUEnv `json:"dfu,omitempty"` + Disabled bool `json:"disabled,omitempty"` + Tags string `json:"tags,omitempty"` + + // Activity + RecentActivityBase string `json:"recent_event_base,omitempty"` + RecentEventCount []int `json:"recent_event_count,omitempty"` + RecentSessionCount []int `json:"recent_session_count,omitempty"` + RecentSessionSeconds []int `json:"recent_session_seconds,omitempty"` + + // Health + HealthLog []DeviceHealthLogEntry `json:"health_log,omitempty"` +} + +// GetDevicesPublicKeysResponse v1 +// +// The response object for retrieving a collection of devices' public keys +type GetDevicesPublicKeysResponse struct { + DevicePublicKeys []DevicePublicKey `json:"device_public_keys"` + HasMore bool `json:"has_more"` +} + +// DevicePublicKey v1 +// +// A structure representing the public key for a specific device +type DevicePublicKey struct { + UID string `json:"uid"` + PublicKey string `json:"key"` +} + +// ProvisionDeviceRequest v1 +// +// The request object for provisioning a device +type ProvisionDeviceRequest struct { + ProductUID string `json:"product_uid"` + DeviceSN string `json:"device_sn"` + FleetUIDs *[]string `json:"fleet_uids,omitempty"` +} + +// GetDeviceLatestResponse v1 +// +// The response object for retrieving the latest notefile values for a device +type GetDeviceLatestResponse struct { + LatestEvents []note.Event `json:"latest_events"` +} + +// Location v1 +// +// The response object for a location. +type Location struct { + When string `json:"when"` + Name string `json:"name"` + Country string `json:"country"` + Timezone string `json:"timezone"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +// TowerInformation v1 +// +// The response object for tower information. +type TowerInformation struct { + // Mobile Country Code + MCC int `json:"mcc"` + // Mobile Network Code + MNC int `json:"mnc"` + // Location Area Code + LAC int `json:"lac"` + CellID int `json:"cell_id"` +} + +// GetDeviceHealthLogResponse v1 +// +// The response object for getting a device's health log. +type GetDeviceHealthLogResponse struct { + HealthLog []HealthLogEntry `json:"health_log"` +} + +// HealthLogEntry v1 +// +// The response object for a health log entry. +type HealthLogEntry struct { + When string `json:"when"` + Alert bool `json:"alert"` + Text string `json:"text"` +} + +var allDfuPhases = []note.DfuPhase{ + note.DfuPhaseUnknown, + note.DfuPhaseIdle, + note.DfuPhaseError, + note.DfuPhaseDownloading, + note.DfuPhaseSideloading, + note.DfuPhaseReady, + note.DfuPhaseReadyRetry, + note.DfuPhaseUpdating, + note.DfuPhaseCompleted, +} + +func ParseDfuPhase(phase string) note.DfuPhase { + phase = strings.ToLower(phase) + for _, validPhase := range allDfuPhases { + if phase == string(validPhase) { + return validPhase + } + } + return note.DfuPhaseUnknown +} + +func IsDfuTerminal(phase note.DfuPhase) bool { + return phase == note.DfuPhaseError || + phase == note.DfuPhaseCompleted || + phase == note.DfuPhaseIdle +} diff --git a/note-go/notehub/api/environment_variables.go b/note-go/notehub/api/environment_variables.go new file mode 100644 index 0000000..484bd09 --- /dev/null +++ b/note-go/notehub/api/environment_variables.go @@ -0,0 +1,206 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package api + +// GetAppEnvironmentVariablesResponse v1 +// +// The response object for getting app environment variables. +type GetAppEnvironmentVariablesResponse struct { + // EnvironmentVariables + // + // The environment variables for this app. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// PutAppEnvironmentVariablesRequest v1 +// +// The request object for setting app environment variables. +type PutAppEnvironmentVariablesRequest struct { + // EnvironmentVariables + // + // The environment variables scoped at the app level + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// PutAppEnvironmentVariablesResponse v1 +// +// The response object for setting app environment variables. +type PutAppEnvironmentVariablesResponse struct { + // EnvironmentVariables + // + // The environment variables for this app. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// DeleteAppEnvironmentVariableResponse v1 +// +// The response object for deleting an app environment variable. +type DeleteAppEnvironmentVariableResponse struct { + // EnvironmentVariables + // + // The environment variables for this app. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// GetFleetEnvironmentVariablesResponse v1 +// +// The response object for getting fleet environment variables. +type GetFleetEnvironmentVariablesResponse struct { + // EnvironmentVariables + // + // The environment variables for this fleet. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// PutFleetEnvironmentVariablesRequest v1 +// +// The request object for setting fleet environment variables. +type PutFleetEnvironmentVariablesRequest struct { + // EnvironmentVariables + // + // The environment variables scoped at the fleet level + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// PutFleetEnvironmentVariablesResponse v1 +// +// The response object for setting fleet environment variables. +type PutFleetEnvironmentVariablesResponse struct { + // EnvironmentVariables + // + // The environment variables for this fleet. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// DeleteFleetEnvironmentVariableResponse v1 +// +// The response object for deleting an fleet environment variable. +type DeleteFleetEnvironmentVariableResponse struct { + // EnvironmentVariables + // + // The environment variables for this fleet. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// GetDeviceEnvironmentVariablesResponse v1 +// +// The response object for getting device environment variables. +type GetDeviceEnvironmentVariablesResponse struct { + // EnvironmentVariables + // + // The environment variables for this device that have been set using host firmware or the Notehub API or UI. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` + + // EnvironmentVariablesEnvDefault + // + // The environment variables that have been set using the env.default request through the Notecard API. + // + // required: true + EnvironmentVariablesEnvDefault map[string]string `json:"environment_variables_env_default"` + + // EnvironmentVariablesEffective + // + // The environment variables for the device as though they were fully resolved by resolution rules + // + // required: true + EnvironmentVariablesEffective map[string]string `json:"environment_variables_effective"` +} + +// PutDeviceEnvironmentVariablesRequest v1 +// +// The request object for setting device environment variables. +type PutDeviceEnvironmentVariablesRequest struct { + // EnvironmentVariables + // + // The environment variables scoped at the device level + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// PutDeviceEnvironmentVariablesResponse v1 +// +// The response object for setting device environment variables. +type PutDeviceEnvironmentVariablesResponse struct { + // EnvironmentVariables + // + // The environment variables for this device that have been set using host firmware or the Notehub API or UI. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// DeleteDeviceEnvironmentVariableResponse v1 +// +// The response object for deleting a device environment variable. +type DeleteDeviceEnvironmentVariableResponse struct { + // EnvironmentVariables + // + // The environment variables for this device that have been set using host firmware or the Notehub API or UI. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// GetDeviceEnvironmentVariablesWithPINResponse v1 +// +// The response object for getting device environment variables with a PIN. +type GetDeviceEnvironmentVariablesWithPINResponse struct { + // EnvironmentVariables + // + // The environment variables for this device that have been set using host firmware or the Notehub API or UI. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` + + // EnvironmentVariablesEnvDefault + // + // The environment variables that have been set using the env.default request through the Notecard API. + // + // required: true + EnvironmentVariablesEnvDefault map[string]string `json:"environment_variables_env_default"` +} + +// PutDeviceEnvironmentVariablesWithPINRequest v1 +// +// The request object for setting device environment variables with a PIN. (The PIN comes in via a header) +type PutDeviceEnvironmentVariablesWithPINRequest struct { + // EnvironmentVariables + // + // The environment variables scoped at the device level + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} + +// PutDeviceEnvironmentVariablesWithPINResponse v1 +// +// The response object for setting device environment variables with a PIN. +type PutDeviceEnvironmentVariablesWithPINResponse struct { + // EnvironmentVariables + // + // The environment variables for this device that have been set using host firmware or the Notehub API or UI. + // + // required: true + EnvironmentVariables map[string]string `json:"environment_variables"` +} diff --git a/note-go/notehub/api/error_defaults.go b/note-go/notehub/api/error_defaults.go new file mode 100644 index 0000000..18172f6 --- /dev/null +++ b/note-go/notehub/api/error_defaults.go @@ -0,0 +1,88 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package api + +import "net/http" + +// ErrNotFound returns the default for an HTTP 404 NotFound +func ErrNotFound() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusNotFound), + Error: "The requested resource could not be found", + Code: http.StatusNotFound, + } +} + +// ErrUnauthorized returns the default for an HTTP 401 Unauthorized +func ErrUnauthorized() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusUnauthorized), + Error: "The request could not be authorized", + Code: http.StatusUnauthorized, + } +} + +// ErrForbidden returns the default for an HTTP 403 Forbidden +func ErrForbidden() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusForbidden), + Error: "The requested action was forbidden", + Code: http.StatusForbidden, + } +} + +// ErrMethodNotAllowed returns the default for an HTTP 405 Method Not Allowed +func ErrMethodNotAllowed() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusMethodNotAllowed), + Error: "Method not allowed on this endpoint", + Code: http.StatusMethodNotAllowed, + } +} + +// ErrInternalServerError returns the default for an HTTP 500 InternalServerError +func ErrInternalServerError() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusInternalServerError), + Error: "An internal server error occurred", + Code: http.StatusInternalServerError, + } +} + +// ErrEventsQueryTimeout returns the default for a GetEvents (and related) request that took too long +func ErrEventsQueryTimeout() ErrorResponse { + return ErrorResponse{ + Status: "Took too long", + Error: "Events query took too long to complete", + Code: http.StatusGatewayTimeout, + } +} + +// ErrBadRequest returns the default for an HTTP 400 BadRequest +func ErrBadRequest() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusBadRequest), + Error: "The request was malformed or contained invalid parameters", + Code: http.StatusBadRequest, + } +} + +// ErrUnsupportedMediaType returns the default for an HTTP 415 UnsupportedMediaType +func ErrUnsupportedMediaType() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusUnsupportedMediaType), + Error: "The request is using an unknown content type", + Code: http.StatusUnsupportedMediaType, + } +} + +// ErrConflict returns the default for an HTTP 409 Conflict +func ErrConflict() ErrorResponse { + return ErrorResponse{ + Status: http.StatusText(http.StatusConflict), + Error: "The resource could not be created due to a conflict", + Code: http.StatusConflict, + } +} diff --git a/note-go/notehub/api/errors.go b/note-go/notehub/api/errors.go new file mode 100644 index 0000000..7c36908 --- /dev/null +++ b/note-go/notehub/api/errors.go @@ -0,0 +1,80 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package api + +import ( + "net/http" +) + +// ErrorResponse v1 +// +// The structure returned from HTTPS API calls when there is an error. +type ErrorResponse struct { + // Error represents the human readable error message. + // + // required: true + // type: string + Error string `json:"err"` + + // Code represents the standard status code + // + // required: true + // type: int + Code int `json:"code"` + + // Status is the machine readable string representation of the error code. + // + // required: true + // type: string + Status string `json:"status"` + + // Request is the request that was made that resulted in error. The url path would be sufficient. + // + // required: false + // type: string + Request string `json:"request,omitempty"` + + // Details are any additional information about the request that would be nice to in the response. + // The request body would be nice especially if there are a lot of parameters. + // + // required: false + // type: object + Details map[string]interface{} `json:"details,omitempty"` + + // Debug is any customer-facing information to aid in debugging. + // + // required: false + // type: string + Debug string `json:"debug,omitempty"` +} + +var SuspendedBillingAccountResponse = ErrorResponse{ + Code: http.StatusForbidden, + Status: "Forbidden", + Error: "this billing account is suspended", +} + +// WithRequest is a an easy way to add http.Request information to an error. +// It takes a http.Request object, parses the URI string into response.Request +// and adds the request Body (if it exists) into the response.Details["body"] as a string +func (e ErrorResponse) WithRequest(r *http.Request) ErrorResponse { + e.Request = r.RequestURI + if len(e.Details) == 0 { + e.Details = make(map[string]interface{}) + } + return e +} + +// WithError adds an error string from an error object into the response. +func (e ErrorResponse) WithError(err error) ErrorResponse { + e.Error = err.Error() + return e +} + +// WithDebug adds a debug string onto the error response object +func (e ErrorResponse) WithDebug(msg string) ErrorResponse { + e.Debug = msg + return e +} diff --git a/note-go/notehub/api/events.go b/note-go/notehub/api/events.go new file mode 100644 index 0000000..79dde42 --- /dev/null +++ b/note-go/notehub/api/events.go @@ -0,0 +1,30 @@ +package api + +import "github.com/blues/note-go/note" + +// GetEventsResponse v1 +// +// The response object for getting events. +type GetEventsResponse struct { + Events []note.Event `json:"events"` + Through string `json:"through,omitempty"` + HasMore bool `json:"has_more"` +} + +// GetEventsResponseSelectedFields v1 +// +// The response object for getting events with selected fields. +type GetEventsResponseSelectedFields struct { + Events []note.Event `json:"events"` + Through string `json:"through,omitempty"` + HasMore bool `json:"has_more"` +} + +// GetEventsByCursorResponse v1 +// +// The response object for getting events by cursor. +type GetEventsByCursorResponse struct { + Events []note.Event `json:"events"` + NextCursor string `json:"next_cursor"` + HasMore bool `json:"has_more"` +} diff --git a/note-go/notehub/api/fleet.go b/note-go/notehub/api/fleet.go new file mode 100644 index 0000000..df389ac --- /dev/null +++ b/note-go/notehub/api/fleet.go @@ -0,0 +1,78 @@ +package api + +// GetFleetsResponse v1 +// +// The response object for getting fleets. +type GetFleetsResponse struct { + Fleets []FleetResponse `json:"fleets"` +} + +// FleetResponse v1 +// +// The response object for a fleet. +type FleetResponse struct { + UID string `json:"uid"` + Label string `json:"label"` + // RFC3339 timestamp, in UTC. + Created string `json:"created"` + + EnvironmentVariables map[string]string `json:"environment_variables"` + + SmartRule string `json:"smart_rule,omitempty"` + WatchdogMins int64 `json:"watchdog_mins,omitempty"` + ConnectivityAssurance FleetConnectivityAssurance `json:"connectivity_assurance,omitempty"` +} + +// PutDeviceFleetsRequest v1 +// +// The request object for adding a device to fleets +type PutDeviceFleetsRequest struct { + // FleetUIDs + // + // The fleets the device belong to + // + // required: true + FleetUIDs []string `json:"fleet_uids"` +} + +// DeleteDeviceFleetsRequest v1 +// +// The request object for removing a device from fleets +type DeleteDeviceFleetsRequest struct { + // FleetUIDs + // + // The fleets the device should be disassociated from + // + // required: true + FleetUIDs []string `json:"fleet_uids"` +} + +// PostFleetRequest v1 +// +// The request object for adding a fleet for a project +type PostFleetRequest struct { + Label string `json:"label"` + + SmartRule string `json:"smart_rule,omitempty"` + WatchdogMins int64 `json:"watchdog_mins,omitempty"` +} + +// PutFleetRequest v1 +// +// The request object for updating a fleet within a project +type PutFleetRequest struct { + Label string `json:"label"` + AddDevices []string `json:"addDevices,omitempty"` + RemoveDevices []string `json:"removeDevices,omitempty"` + + SmartRule string `json:"smart_rule,omitempty"` + WatchdogMins int64 `json:"watchdog_mins,omitempty"` +} + +// FleetConnectivityAssurance v1 +// +// Includes, Enabled = Whether Connectivity Assurance is enabled for this fleet +// With flexibility to add more information in the future +type FleetConnectivityAssurance struct { + Enabled bool `json:"enabled"` +} diff --git a/note-go/notehub/api/job.go b/note-go/notehub/api/job.go new file mode 100644 index 0000000..f52aefa --- /dev/null +++ b/note-go/notehub/api/job.go @@ -0,0 +1,45 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package api + +// This header is present for every type of job +type HubJob struct { + Type HubJobType `json:"type,omitempty"` + Version string `json:"version,omitempty"` + Name string `json:"name,omitempty"` + Comment string `json:"comment,omitempty"` + Created int64 `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` +} + +// This header is present for every type of report +type HubJobReport struct { + Type HubJobType `json:"type,omitempty"` + Version string `json:"version,omitempty"` + Comment string `json:"comment,omitempty"` + JobId string `json:"job_id"` + JobName string `json:"job_name"` + Status string `json:"status,omitempty"` + DryRun bool `json:"dry_run,omitempty"` + Cancel bool `json:"cancel,omitempty"` + SubmittedBy string `json:"who_submitted,omitempty"` + Submitted int64 `json:"when_submitted,omitempty"` + Started int64 `json:"when_started,omitempty"` + Updated int64 `json:"when_updated,omitempty"` + Completed int64 `json:"when_completed,omitempty"` +} + +// Types of jobs +type HubJobType string + +const ( + HubJobTypeUnspecified HubJobType = "" + HubJobTypeReconciliation HubJobType = "reconciliation" +) + +const ( + HubJobStatusCancelled = "cancelled" + HubJobStatusSubmitted = "submitted" +) diff --git a/note-go/notehub/api/job_reconciliation.go b/note-go/notehub/api/job_reconciliation.go new file mode 100644 index 0000000..b14c7f8 --- /dev/null +++ b/note-go/notehub/api/job_reconciliation.go @@ -0,0 +1,94 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package api + +// Current data format of the reconciliation job type. Note that major types +// require conversion, while minor types can be handled by the same code. +const HubJobReconciliationMajorVersion = 1 +const HubJobReconciliationMinorVersion = 1 + +// HubJobReconciliation is the format of a batch request file +type HubJobReconciliation struct { + Header HubJob `json:"job,omitempty"` + Comment string `json:"comment,omitempty"` + Select struct { + Comment string `json:"comment,omitempty"` + AllDevices bool `json:"all_devices,omitempty"` + DevicesInFleets []string `json:"devices_in_fleets,omitempty"` + Devices []string `json:"devices,omitempty"` + DevicesBySn []string `json:"devices_by_sn,omitempty"` + } `json:"select,omitempty"` + DefaultRequests HubJobReconciliationRequests `json:"default_requests,omitempty"` + DeviceRequests map[string]HubJobReconciliationRequests `json:"device_requests,omitempty"` + ReportOptions HubJobReconciliationReportOptions `json:"report_options,omitempty"` +} + +// HubReportReconciliation is the format of the report generated by a reconciliation job. +type HubReportReconciliation struct { + Comment string `json:"comment,omitempty"` + Header HubJobReport `json:"job,omitempty"` + Status HubJobReconciliationReportStatus `json:"status,omitempty"` + Report *HubJobReconciliationReport `json:"output,omitempty"` +} + +// HubJobReconciliationRequests is a structure defining requests to apply to a set of selected devices. +// Note that if ProvisionProductUID is specified, the device will be provisioned if it isn't already provisioned, +// else it will fail if not provisioned. Also note that sn_to_set and vars_to_set use the same syntax +// as our standard API calls in that the value '-' means to clear the value. +type HubJobReconciliationRequests struct { + Comment string `json:"comment,omitempty"` + ProvisionProductUID string `json:"provision_product,omitempty"` + Disable bool `json:"disable,omitempty"` + Enable bool `json:"enable,omitempty"` + CaDisable bool `json:"connectivity_assurance_disable,omitempty"` + CaEnable bool `json:"connectivity_assurance_enable,omitempty"` + SnToDefault string `json:"sn_to_default,omitempty"` + SnToSet string `json:"sn_to_set,omitempty"` + VarsToDefault map[string]string `json:"vars_to_default,omitempty"` + VarsToSet map[string]string `json:"vars_to_set,omitempty"` + FleetsToDefault []string `json:"fleets_to_default,omitempty"` + FleetsToJoin []string `json:"fleets_to_join,omitempty"` + FleetsToLeave []string `json:"fleets_to_leave,omitempty"` +} + +// HubJobReconciliationReportStatus is the status portion of a batch report file +type HubJobReconciliationReportStatus struct { + Error string `json:"error,omitempty"` + Errors map[string]string `json:"errors,omitempty"` + Actions map[string]string `json:"actions,omitempty"` + DeviceCount int `json:"device_count,omitempty"` + Provisioned []string `json:"provisioned,omitempty"` +} + +// HubJobReconciliationReport is the format of a batch report file +type HubJobReconciliationReport struct { + App *HubJobReconciliationAppReport `json:"project,omitempty"` + Devices map[string]HubJobReconciliationDeviceReport `json:"devices,omitempty"` +} + +// HubJobReconciliationReportOptions is a structure defining options for the report +type HubJobReconciliationReportOptions struct { + Comment string `json:"comment,omitempty"` + AppInfo bool `json:"app_info,omitempty"` + AppVars bool `json:"app_vars,omitempty"` + AppFleets bool `json:"app_fleets,omitempty"` + DeviceInfo bool `json:"device_info,omitempty"` + DeviceActivity bool `json:"device_activity,omitempty"` + DeviceHealth bool `json:"device_health,omitempty"` + DeviceVars bool `json:"device_vars,omitempty"` +} + +// HubJobReconciliationAppReport is a structure defining the app report +type HubJobReconciliationAppReport struct { + Info *GetAppResponse `json:"project_info,omitempty"` + Vars *GetAppEnvironmentVariablesResponse `json:"project_vars,omitempty"` + Fleets *GetFleetsResponse `json:"project_fleets,omitempty"` +} + +// HubJobReconciliationDeviceReport is a structure defining the device report +type HubJobReconciliationDeviceReport struct { + Info *GetDeviceResponse `json:"device_info,omitempty"` + Vars *GetDeviceEnvironmentVariablesResponse `json:"device_vars,omitempty"` +} diff --git a/note-go/notehub/api/products.go b/note-go/notehub/api/products.go new file mode 100644 index 0000000..b2b142b --- /dev/null +++ b/note-go/notehub/api/products.go @@ -0,0 +1,30 @@ +package api + +// GetProductsResponse v1 +// +// The response object for getting products. +type GetProductsResponse struct { + Products []ProductResponse `json:"products"` +} + +// ProductResponse v1 +// +// The response object for a product. +type ProductResponse struct { + UID string `json:"uid"` + Label string `json:"label"` + AutoProvisionFleets *[]string `json:"auto_provision_fleets"` + DisableDevicesByDefault bool `json:"disable_devices_by_default"` +} + +// PostProductRequest v1 +// +// The request object for adding a product. +type PostProductRequest struct { + ProductUID string `json:"product_uid"` + Label string `json:"label"` + // Not required + AutoProvisionFleets []string `json:"auto_provision_fleets"` + // Not required + DisableDevicesByDefault bool `json:"disable_devices_by_default"` +} diff --git a/note-go/notehub/api/project.go b/note-go/notehub/api/project.go new file mode 100644 index 0000000..fe003c3 --- /dev/null +++ b/note-go/notehub/api/project.go @@ -0,0 +1,40 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package api + +// GetAppResponse v1 +// +// The response object for getting an app. +type GetAppResponse struct { + UID string `json:"uid"` + Label string `json:"label"` + // RFC3339 timestamp, in UTC. + Created string `json:"created"` + + AdministrativeContact *ContactResponse `json:"administrative_contact"` + TechnicalContact *ContactResponse `json:"technical_contact"` + + // "owner", "developer", or "viewer" + Role *string `json:"role"` +} + +// ContactResponse v1 +// +// The response object for an app contact. +type ContactResponse struct { + Name string `json:"name"` + Email string `json:"email"` + Role string `json:"role"` + Organization string `json:"organization"` +} + +// GenerateClientAppResponse v1 +// +// The response object for generating a new client app for +// a specific app +type GenerateClientAppResponse struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} diff --git a/note-go/notehub/api/session.go b/note-go/notehub/api/session.go new file mode 100644 index 0000000..302e7f0 --- /dev/null +++ b/note-go/notehub/api/session.go @@ -0,0 +1,21 @@ +package api + +import "github.com/blues/note-go/note" + +// GetDeviceSessionsResponse is the structure returned from a GetDeviceSessions call +type GetDeviceSessionsResponse struct { + // Sessions + // + // The requested page of session logs for the device + // + // required: true + Sessions []note.DeviceSession `json:"sessions"` + + // HasMore + // + // A boolean indicating whether there is at least one more + // page of data available after this page + // + // required: true + HasMore bool `json:"has_more"` +} diff --git a/note-go/notehub/auth.go b/note-go/notehub/auth.go new file mode 100644 index 0000000..18dea81 --- /dev/null +++ b/note-go/notehub/auth.go @@ -0,0 +1,340 @@ +package notehub + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "math/rand" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "time" +) + +type AccessToken struct { + Host string + Email string + AccessToken string + ExpiresAt time.Time +} + +// open opens the specified URL in the default browser of the user. +func open(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} + +// listenOnAny tries each port in order and returns a bound net.Listener for the first available one. +func listenOnAny(ports []int) (net.Listener, int, error) { + for _, p := range ports { + ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", p)) + if err == nil { + return ln, p, nil + } + } + return nil, 0, errors.New("no ports available") +} + +func randString(n int) string { + letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} + +func RevokeAccessToken(hub, token string) error { + form := url.Values{ + "token": {token}, + "token_type_hint": {"access_token"}, + "client_id": {"notehub_cli"}, + } + + req, err := http.NewRequestWithContext( + context.Background(), + http.MethodPost, + fmt.Sprintf("https://%s/oauth2/revoke", hub), + strings.NewReader(form.Encode()), + ) + + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("making request: %w", err) + } + defer resp.Body.Close() + + // Per RFC 7009: 200 OK is returned even if the token is already revoked + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil +} + +// InitiateBrowserBasedLogin starts the OAuth2 login flow by opening the user's browser. +// the `hub` parameter is the hostname of Notehub where it is assumed that an OAuth2 client +// with client ID `notehub_cli` is configured for authorization code flow. +func InitiateBrowserBasedLogin(notehubApiHost string) (*AccessToken, error) { + // this is the hard-coded OAuth client ID that's persisted in Hydra + clientId := "notehub_cli" + + if !strings.HasPrefix(notehubApiHost, "api.") { + notehubApiHost = "api." + notehubApiHost + } + + var notehubUiHost string + if notehubApiHost == "api.notefile.net" { + notehubUiHost = "notehub.io" + } else { + notehubUiHost = strings.TrimPrefix(notehubApiHost, "api.") + } + + // Try these ports in order until one is available: + // + // these ports are randomly chosen and hard-coded into + // the OAuth client in Hydra within Notehub (in the redirect_uris field) + ports := []int{58766, 58767, 58768, 58769, 42100, 42101, 42102, 42103} + + // Return values + var accessToken *AccessToken + var accessTokenErr error + + state := randString(16) + codeVerifier := randString(50) // must be at least 43 characters + hash := sha256.Sum256([]byte(codeVerifier)) + codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:]) + + done := make(chan bool, 1) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + defer signal.Reset(os.Interrupt) + + router := http.NewServeMux() + + // We'll fill this after we pick a port but declare it now so the handler can close over it. + chosenPort := 0 + + // The browser will be redirected to this endpoint with an authorization code + // and then this endpoint will exchange that authorization code for an access token + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + authorizationCode := r.URL.Query().Get("code") + callbackState := r.URL.Query().Get("state") + + errHandler := func(msg string) { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "error: %s", msg) + fmt.Printf("error: %s\n", msg) + accessTokenErr = errors.New(msg) + } + + if callbackState != state { + errHandler("state mismatch") + return + } + + /////////////////////////////////////////// + // Exchange code for access token + /////////////////////////////////////////// + + tokenResp, err := http.Post( + (&url.URL{ + Scheme: "https", + Host: notehubUiHost, + Path: "/oauth2/token", + }).String(), + "application/x-www-form-urlencoded", + strings.NewReader(url.Values{ + "client_id": {clientId}, + "code": {authorizationCode}, + "code_verifier": {codeVerifier}, + "grant_type": {"authorization_code"}, + "redirect_uri": {fmt.Sprintf("http://localhost:%d", chosenPort)}, + }.Encode()), + ) + if err != nil { + errHandler("error on /oauth2/token: " + err.Error()) + return + } + defer tokenResp.Body.Close() + + body, err := io.ReadAll(tokenResp.Body) + if err != nil { + errHandler("could not read body from /oauth2/token: " + err.Error()) + return + } + + var tokenData map[string]interface{} + if err := json.Unmarshal(body, &tokenData); err != nil { + errHandler("could not unmarshal body from /oauth2/token: " + err.Error()) + return + } + + if errCode, ok := tokenData["error"].(string); ok { + if errDescription, ok2 := tokenData["error_description"].(string); ok2 { + errHandler(fmt.Sprintf("%s: %s", errCode, errDescription)) + } else { + errHandler(errCode) + } + return + } + + accessTokenString, ok := tokenData["access_token"].(string) + if !ok { + errHandler("unexpected error: no access token returned") + return + } + + // be defensive about type + var expiresIn time.Duration + switch v := tokenData["expires_in"].(type) { + case float64: + expiresIn = time.Duration(v) * time.Second + case int: + expiresIn = time.Duration(v) * time.Second + default: + expiresIn = 0 + } + + /////////////////////////////////////////// + // Get user's information (specifically email) + /////////////////////////////////////////// + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/userinfo", notehubApiHost), nil) + if err != nil { + errHandler("could not create request for /userinfo: " + err.Error()) + return + } + req.Header.Set("Authorization", "Bearer "+accessTokenString) + userinfoResp, err := http.DefaultClient.Do(req) + if err != nil { + errHandler("could not get userinfo: " + err.Error()) + return + } + defer userinfoResp.Body.Close() + + userinfoBody, err := io.ReadAll(userinfoResp.Body) + if err != nil { + errHandler("could not read body from /userinfo: " + err.Error()) + return + } + + var userinfoData map[string]interface{} + if err := json.Unmarshal(userinfoBody, &userinfoData); err != nil { + errHandler("could not unmarshal body from /userinfo: " + err.Error()) + return + } + + email, ok := userinfoData["email"].(string) + if !ok { + errHandler("could not retrieve email") + return + } + + /////////////////////////////////////////// + // Build the access token response + /////////////////////////////////////////// + + accessToken = &AccessToken{ + Host: notehubApiHost, + Email: email, + AccessToken: accessTokenString, + ExpiresAt: time.Now().Add(expiresIn), + } + + /////////////////////////////////////////// + // respond to the browser and quit + /////////////////////////////////////////// + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "

Token exchange completed successfully

You may now close this window and return to the CLI application

") + + quit <- os.Interrupt + }) + + // Pick first available port and get a listener + listener, port, err := listenOnAny(ports) + if err != nil { + return nil, fmt.Errorf("could not bind any callback port: %w", err) + } + chosenPort = port + + server := &http.Server{ + Addr: fmt.Sprintf(":%d", chosenPort), + Handler: router, + } + + // Wait for OAuth callback to be hit, then shutdown HTTP server + go func() { + <-quit + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + server.SetKeepAlivesEnabled(false) + if err := server.Shutdown(ctx); err != nil { + log.Printf("error: %v", err) + } + close(done) + }() + + // Start HTTP server waiting for OAuth callback + go func() { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + log.Printf("error: %v", err) + } + }() + + // Build the authorize URL using the chosen port + authorizeUrl := url.URL{ + Scheme: "https", + Host: notehubUiHost, + Path: "/oauth2/auth", + RawQuery: url.Values{ + "client_id": {clientId}, + "code_challenge": {codeChallenge}, + "code_challenge_method": {"S256"}, + "redirect_uri": {fmt.Sprintf("http://localhost:%d", chosenPort)}, + "response_type": {"code"}, + "scope": {"openid email"}, + "state": {state}, + }.Encode(), + } + + // Open web browser to authorize + fmt.Printf("Opening web browser to initiate authentication (redirect port %d)...\n", chosenPort) + if err := open(authorizeUrl.String()); err != nil { + fmt.Printf("error opening web browser: %v", err) + } + + // Wait for exchange to finish + <-done + return accessToken, accessTokenErr +} diff --git a/note-go/notehub/config.go b/note-go/notehub/config.go new file mode 100644 index 0000000..4bb21bb --- /dev/null +++ b/note-go/notehub/config.go @@ -0,0 +1,8 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notehub + +// DefaultAPIService (golint) +const DefaultAPIService = "api.notefile.net" diff --git a/note-go/notehub/dbquery.go b/note-go/notehub/dbquery.go new file mode 100644 index 0000000..81e613a --- /dev/null +++ b/note-go/notehub/dbquery.go @@ -0,0 +1,19 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notehub + +// DbQuery is the structure for a database query +type DbQuery struct { + Columns string `json:"columns,omitempty"` + Format string `json:"format,omitempty"` + Count bool `json:"count,omitempty"` + Offset int `json:"offset,omitempty"` + Limit int `json:"limit,omitempty"` + NoHeader bool `json:"noheader,omitempty"` + Where string `json:"where,omitempty"` + Last string `json:"last,omitempty"` + Order string `json:"order,omitempty"` + Descending bool `json:"descending,omitempty"` +} diff --git a/note-go/notehub/request.go b/note-go/notehub/request.go new file mode 100644 index 0000000..e3b43ea --- /dev/null +++ b/note-go/notehub/request.go @@ -0,0 +1,257 @@ +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package notehub + +import ( + "fmt" + "strings" + + "github.com/blues/note-go/note" + "github.com/blues/note-go/notecard" +) + +// Supported requests + +// HubDeviceContact (golint) +const HubDeviceContact = "hub.device.contact" + +// HubDeviceSessionBegin (golint) +const HubDeviceSessionBegin = "hub.device.session.begin" + +// HubDeviceSessionUsage (golint) +const HubDeviceSessionUsage = "hub.device.session.usage" + +// HubDeviceSessionEnd (golint) +const HubDeviceSessionEnd = "hub.device.session.end" + +// HubAppGetSchemas (golint) +const HubAppGetSchemas = "hub.app.schemas.get" + +// HubQuery (golint) +const HubQuery = "hub.app.data.query" + +// HubEventQuery (golint) +const HubEventQuery = "hub.app.event.query" + +// HubSessionQuery (golint) +const HubSessionQuery = "hub.app.session.query" + +// HubAppUpload (golint) +const HubAppUpload = "hub.app.upload.add" + +// HubUpload (golint) +const HubUpload = "hub.upload.add" + +// HubAppUploads (golint) +const HubAppUploads = "hub.app.upload.query" + +// HubAppJobSubmit (golint) +const HubAppJobSubmit = "hub.app.job.submit" + +// HubAppJobGet (golint) +const HubAppJobGet = "hub.app.job.get" + +// HubAppJobPut (golint) +const HubAppJobPut = "hub.app.job.put" + +// HubAppJobDelete (golint) +const HubAppJobDelete = "hub.app.job.delete" + +// HubAppJobsGet (golint) +const HubAppJobsGet = "hub.app.jobs.get" + +// HubAppReportGet (golint) +const HubAppReportGet = "hub.app.report.get" + +// HubAppReportDelete (golint) +const HubAppReportDelete = "hub.app.report.delete" + +// HubAppReportCancel (golint) +const HubAppReportCancel = "hub.app.report.cancel" + +// HubAppReportsGet (golint) +const HubAppReportsGet = "hub.app.reports.get" + +// HubUploads (golint) +const HubUploads = "hub.upload.query" + +// HubAppUploadSet (golint) +const HubAppUploadSet = "hub.app.upload.set" + +// HubUploadSet (golint) +const HubUploadSet = "hub.upload.set" + +// HubAppUploadDelete (golint) +const HubAppUploadDelete = "hub.app.upload.delete" + +// HubUploadDelete (golint) +const HubUploadDelete = "hub.upload.delete" + +// HubAppUploadRead (golint) +const HubAppUploadRead = "hub.app.upload.get" + +// HubUploadRead (golint) +const HubUploadRead = "hub.upload.get" + +// HubAppSetTransform (golint) +const HubAppSetTransform = "hub.app.transform.set" + +// HubAppGetTransform (golint) +const HubAppGetTransform = "hub.app.transform.get" + +// HubEnvSet (golint) +const HubEnvSet = "hub.env.set" + +// HubEnvGet (golint) +const HubEnvGet = "hub.env.get" + +// HubEnvScopeApp (golint) +const HubEnvScopeApp = "app" + +// HubEnvScopeProject (golint) +const HubEnvScopeProject = "project" + +// HubEnvScopeFleet (golint) +const HubEnvScopeFleet = "fleet" + +// HubEnvScopeFleets (golint) +const HubEnvScopeFleets = "fleets" + +// HubEnvScopeDevice (golint) +const HubEnvScopeDevice = "device" + +// HubCompressModeSnappy (golint) +const HubCompressModeSnappy = "snappy" + +// HubCompressModeCobs (golint) +const HubCompressModeCobs = "cobs" + +// HubRequest is is the core data structure for notehub-specific requests +type HubRequest struct { + notecard.Request `json:",omitempty"` + Contact *note.Contact `json:"contact,omitempty"` + AppUID string `json:"app,omitempty"` + FleetUID string `json:"fleet,omitempty"` + EventSerials []string `json:"events,omitempty"` + DbQuery *DbQuery `json:"query,omitempty"` + Uploads []UploadMetadata `json:"uploads,omitempty"` + Contains string `json:"contains,omitempty"` + Handlers *[]string `json:"handlers,omitempty"` + FileType UploadType `json:"type,omitempty"` + FileTags string `json:"tags,omitempty"` + FileNotes string `json:"filenotes,omitempty"` + Provision bool `json:"provision,omitempty"` + Scope string `json:"scope,omitempty"` + Env *map[string]string `json:"env,omitempty"` + FleetEnv *map[string]map[string]string `json:"fleet_env,omitempty"` + PIN string `json:"pin,omitempty"` + Compress string `json:"compress,omitempty"` + MD5 string `json:"md5,omitempty"` + DeviceEndpoint bool `json:"device_endpoint,omitempty"` + DryRun bool `json:"dry_run,omitempty"` +} + +type UploadType string + +const ( + UploadTypeUnknown UploadType = "" + UploadTypeHostFirmware UploadType = "firmware" + UploadTypeNotecardFirmware UploadType = "notecard" + UploadTypeModemFirmware UploadType = "modem" + UploadTypeStarnoteFirmware UploadType = "starnote" + UploadTypeUserData UploadType = "data" + UploadTypeJob UploadType = "job" +) + +var allFileTypes = []UploadType{ + UploadTypeUnknown, + UploadTypeHostFirmware, + UploadTypeNotecardFirmware, + UploadTypeModemFirmware, + UploadTypeStarnoteFirmware, + UploadTypeUserData, + UploadTypeJob, +} + +func ParseUploadType(fileType string) UploadType { + if fileType == "host" { + return UploadTypeHostFirmware + } + for _, validType := range allFileTypes { + if string(validType) == fileType { + return validType + } + } + return UploadTypeUnknown +} + +const TestFirmwareString = "(test firmware)" + +// HubRequestFileFirmware is firmware-specific metadata +type HubRequestFileFirmware struct { + // The organization accountable for the firmware - a display string + Organization string `json:"org,omitempty"` + // A description of the firmware - a display string + Description string `json:"description,omitempty"` + // The name and model number of the product containing the firmware - a display string + Product string `json:"product,omitempty"` + // The identifier of the only firmware that will be acceptable and downloaded to this device + Firmware string `json:"firmware,omitempty"` + // The composite version number of the firmware, generally major.minor.patch as a string + Version string `json:"version,omitempty"` + // The target CPU of the firmware (see notecard/src/board.h) + Target string `json:"target,omitempty"` + // The build number of the firmware, for numeric comparison + Major uint32 `json:"ver_major,omitempty"` + Minor uint32 `json:"ver_minor,omitempty"` + Patch uint32 `json:"ver_patch,omitempty"` + Build uint32 `json:"ver_build,omitempty"` + // The build number of the firmware, generally just a date and time + Built string `json:"built,omitempty"` + // The entity who built or is responsible for the firmware - a display string + Builder string `json:"builder,omitempty"` +} + +func (metadata HubRequestFileFirmware) VersionString() string { + return fmt.Sprintf("%d.%d.%d.%d", metadata.Major, metadata.Minor, metadata.Patch, metadata.Build) +} + +// UploadMetadata is the body of the object uploaded for each file +type UploadMetadata struct { + Name string `json:"name,omitempty"` + Length int `json:"length,omitempty"` + MD5 string `json:"md5,omitempty"` + CRC32 uint32 `json:"crc32,omitempty"` + Created int64 `json:"created,omitempty"` + Modified int64 `json:"modified,omitempty"` + Source string `json:"source,omitempty"` + Contains string `json:"contains,omitempty"` + Found string `json:"found,omitempty"` + FileType UploadType `json:"type,omitempty"` + Tags string `json:"tags,omitempty"` // comma-separated, no spaces, case-insensitive + Notes string `json:"notes,omitempty"` // Should be simple text + Firmware *HubRequestFileFirmware `json:"firmware,omitempty"` // This value is pulled out of the firmware binary itself + Version string `json:"version,omitempty"` // User-specified version string provided at time of upload + // Arbitrary metadata that the user may define - we don't interpret the schema at all + Info map[string]interface{} `json:"info,omitempty"` +} + +func (upload UploadMetadata) IsArchSpecificNotecardFirmware() bool { + return upload.FileType == UploadTypeNotecardFirmware && (strings.Contains(upload.Name, "-s3-") || + strings.Contains(upload.Name, "-u5-") || + strings.Contains(upload.Name, "-wl-")) +} + +func (upload UploadMetadata) IsPublished() bool { + for _, tag := range strings.Split(upload.Tags, ",") { + if strings.TrimSpace(strings.ToLower(tag)) == "publish" { + return true + } + } + return false +} + +// HubRequestFileTagPublish indicates that this should be published in the UI +const HubRequestFileTagPublish = "publish" diff --git a/note-go/package.sh b/note-go/package.sh new file mode 100755 index 0000000..a79d6f4 --- /dev/null +++ b/note-go/package.sh @@ -0,0 +1,47 @@ +#! /usr/bin/env bash +# +# Copyright 2020 Blues Inc. All rights reserved. +# Use of this source code is governed by licenses granted by the +# copyright holder including that found in the LICENSE file. +# +######### Bash Boilerplate ########## +set -euo pipefail # strict mode +readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "$SCRIPT_DIR" # cd to this script's dir +######### End Bash Boilerplate ########## + +# +# note-go package.sh +# +# This script packages the best note-go executables (notecard, notehub) into +# archives named note{card,hub}cli_${GOOS}_${GOARCH}.tar.gz or .zip in the case of +# ${GOOS}=windows. To be more user-friendly we call darwin 'macos' in the archive +# names. +# +# Parameters: This script uses ${GOOS} and ${GOARCH} determine where to look for the +# executables. +# +# Output: Archives are saved in "./build/packages/" +# + +# Add GOOS and GOARCH to our environment. (and other GO vars we don't need) +eval "$(go env)" + +readonly BUILD_EXE_DIR="$SCRIPT_DIR/build/${GOOS}/${GOARCH}/" +mkdir -p "$BUILD_EXE_DIR" +readonly BUILD_PACKAGE_DIR="$SCRIPT_DIR/build/packages/" +mkdir -p "$BUILD_PACKAGE_DIR" + +# compress the build products into an archive +cd "$BUILD_EXE_DIR" +if [ "${GOOS}" = "windows" ]; then + # -j means don't store directory names, just file names. Basically flattens everything into the root of the zip. + zip -j "$BUILD_PACKAGE_DIR/notecardcli_${GOOS}_${GOARCH}.zip" ./notecard.exe "$SCRIPT_DIR/notecard-driver-windows7.inf" + zip -j "$BUILD_PACKAGE_DIR/notehubcli_${GOOS}_${GOARCH}.zip" ./notehub.exe +elif [ "${GOOS}" = "darwin" ]; then + tar -czvf "$BUILD_PACKAGE_DIR/notecardcli_macos_${GOARCH}.tar.gz" ./notecard + tar -czvf "$BUILD_PACKAGE_DIR/notehubcli_macos_${GOARCH}.tar.gz" ./notehub +else + tar -czvf "$BUILD_PACKAGE_DIR/notecardcli_${GOOS}_${GOARCH}.tar.gz" ./notecard + tar -czvf "$BUILD_PACKAGE_DIR/notehubcli_${GOOS}_${GOARCH}.tar.gz" ./notehub +fi; From af064f7bb52a526348cbd2e220339786cd037df1 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Thu, 15 Jan 2026 06:10:55 -0500 Subject: [PATCH 09/10] significant COBS performance optimizations --- note-go/notecard/cobs.go | 15 ++- note-go/notecard/cobs_test.go | 219 ++++++++++++++++++++++++++++++++++ note-go/notecard/i2c-unix.go | 16 ++- note-go/notecard/notecard.go | 75 ++++++++---- notecard/upload.go | 12 +- 5 files changed, 305 insertions(+), 32 deletions(-) diff --git a/note-go/notecard/cobs.go b/note-go/notecard/cobs.go index a379d18..0b203a6 100644 --- a/note-go/notecard/cobs.go +++ b/note-go/notecard/cobs.go @@ -40,7 +40,9 @@ func CobsEncodedLength(length int) int { func CobsEncode(input []byte, xor byte) ([]byte, error) { length := len(input) inOffset := 0 - output := make([]byte, CobsEncodedLength(len(input))) + // Allocate with +1 capacity so append(result, '\n') won't reallocate + maxLen := CobsEncodedLength(len(input)) + output := make([]byte, maxLen, maxLen+1) outOffset := 0 outStartOffset := outOffset var ch, code uint8 @@ -66,3 +68,14 @@ func CobsEncode(input []byte, xor byte) ([]byte, error) { output[outCodeOffset] = code ^ xor return output[outStartOffset:outOffset], nil } + +// CobsEncodeAppend encodes data and appends a delimiter in one operation. +// This avoids the reallocation that would occur with append(CobsEncode(...), delim). +func CobsEncodeAppend(input []byte, xor byte, delimiter byte) ([]byte, error) { + encoded, err := CobsEncode(input, xor) + if err != nil { + return nil, err + } + // Since CobsEncode allocates with +1 capacity, this append won't reallocate + return append(encoded, delimiter), nil +} diff --git a/note-go/notecard/cobs_test.go b/note-go/notecard/cobs_test.go index 8d7fbcc..f085a3f 100644 --- a/note-go/notecard/cobs_test.go +++ b/note-go/notecard/cobs_test.go @@ -27,3 +27,222 @@ func TestCob(t *testing.T) { require.Equal(t, buf, decoded) } + +func TestCobsEdgeCases(t *testing.T) { + tests := []struct { + name string + input []byte + xor byte + }{ + {"empty", []byte{}, 0}, + {"single zero", []byte{0}, 0}, + {"single nonzero", []byte{1}, 0}, + {"two zeros", []byte{0, 0}, 0}, + {"trailing zero", []byte{1, 2, 0}, 0}, + {"leading zero", []byte{0, 1, 2}, 0}, + {"middle zero", []byte{1, 0, 2}, 0}, + {"no zeros", []byte{1, 2, 3}, 0}, + {"all zeros 3", []byte{0, 0, 0}, 0}, + {"with xor", []byte{1, 2, 0, 3}, '\n'}, + {"254 bytes no zero", make254NonZero(), 0}, + {"255 bytes no zero", make255NonZero(), 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encoded, err := CobsEncode(tt.input, tt.xor) + require.NoError(t, err, "encode failed") + + decoded, err := CobsDecode(encoded, tt.xor) + require.NoError(t, err, "decode failed") + + require.Equal(t, tt.input, decoded, "roundtrip failed: encoded=%v", encoded) + }) + } +} + +func make254NonZero() []byte { + b := make([]byte, 254) + for i := range b { + b[i] = byte(i%255) + 1 + } + return b +} + +func make255NonZero() []byte { + b := make([]byte, 255) + for i := range b { + b[i] = byte(i%255) + 1 + } + return b +} + +// TestCobsKnownValues tests encoding against known expected output values. +// These are the canonical COBS encodings per the specification. +// This catches regressions where encode/decode are broken in compatible but wrong ways. +func TestCobsKnownValues(t *testing.T) { + tests := []struct { + name string + input []byte + xor byte + expected []byte + }{ + // Standard COBS test vectors (xor=0) + { + name: "single zero", + input: []byte{0x00}, + xor: 0, + expected: []byte{0x01, 0x01}, + }, + { + name: "single nonzero", + input: []byte{0x01}, + xor: 0, + expected: []byte{0x02, 0x01}, + }, + { + name: "two zeros", + input: []byte{0x00, 0x00}, + xor: 0, + expected: []byte{0x01, 0x01, 0x01}, + }, + { + name: "three nonzero bytes", + input: []byte{0x01, 0x02, 0x03}, + xor: 0, + expected: []byte{0x04, 0x01, 0x02, 0x03}, + }, + { + name: "zero in middle", + input: []byte{0x01, 0x00, 0x02}, + xor: 0, + expected: []byte{0x02, 0x01, 0x02, 0x02}, + }, + { + name: "leading zero", + input: []byte{0x00, 0x01, 0x02}, + xor: 0, + expected: []byte{0x01, 0x03, 0x01, 0x02}, + }, + { + name: "trailing zero", + input: []byte{0x01, 0x02, 0x00}, + xor: 0, + expected: []byte{0x03, 0x01, 0x02, 0x01}, + }, + { + name: "Hello", + input: []byte{'H', 'e', 'l', 'l', 'o'}, + xor: 0, + expected: []byte{0x06, 'H', 'e', 'l', 'l', 'o'}, + }, + } + + for _, tt := range tests { + t.Run(tt.name+" encode", func(t *testing.T) { + encoded, err := CobsEncode(tt.input, tt.xor) + require.NoError(t, err) + require.Equal(t, tt.expected, encoded, "encoded output mismatch") + }) + + t.Run(tt.name+" decode", func(t *testing.T) { + decoded, err := CobsDecode(tt.expected, tt.xor) + require.NoError(t, err) + require.Equal(t, tt.input, decoded, "decoded output mismatch") + }) + } +} + +// TestCobsXORRoundtrip tests that XOR mode (used to eliminate newlines) roundtrips correctly. +// XOR mode is Blues-specific; there's no external standard, so we only test roundtrip. +func TestCobsXORRoundtrip(t *testing.T) { + xor := byte('\n') // 0x0A - what note-c/notecard uses + + tests := []struct { + name string + input []byte + }{ + {"single zero", []byte{0x00}}, + {"single nonzero", []byte{0x01}}, + {"contains newline", []byte{0x01, '\n', 0x02}}, + {"multiple newlines", []byte{'\n', 0x01, '\n', '\n', 0x02, '\n'}}, + {"all newlines", []byte{'\n', '\n', '\n'}}, + {"binary with newlines", func() []byte { + b := make([]byte, 256) + for i := range b { + b[i] = byte(i) + } + return b + }()}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encoded, err := CobsEncode(tt.input, xor) + require.NoError(t, err) + + // Verify no newlines in encoded output (the whole point of XOR mode) + for i, b := range encoded { + require.NotEqual(t, byte('\n'), b, "found newline at position %d in encoded output", i) + } + + decoded, err := CobsDecode(encoded, xor) + require.NoError(t, err) + require.Equal(t, tt.input, decoded, "roundtrip failed") + }) + } +} + +// TestCobs254ByteBoundary tests the critical 254-byte boundary where COBS +// must insert an extra code byte. This is a common source of bugs. +func TestCobs254ByteBoundary(t *testing.T) { + // 254 non-zero bytes: [0xFF, 254 data bytes, 0x01] + // The 0xFF means "254 data bytes follow, no implicit zero after" + // The trailing 0x01 terminates the stream (0 more data bytes) + data254 := make([]byte, 254) + for i := range data254 { + data254[i] = byte(i) + 1 // 1, 2, 3, ..., 254 + } + + encoded254, err := CobsEncode(data254, 0) + require.NoError(t, err) + require.Len(t, encoded254, 256, "254 non-zero bytes encode to 256 bytes") + require.Equal(t, byte(0xFF), encoded254[0], "first code byte should be 0xFF") + require.Equal(t, byte(0x01), encoded254[255], "trailing code byte should be 0x01") + + decoded254, err := CobsDecode(encoded254, 0) + require.NoError(t, err) + require.Equal(t, data254, decoded254) + + // 255 non-zero bytes: [0xFF, 254 data bytes, 0x02, 1 data byte] + data255 := make([]byte, 255) + for i := range data255 { + data255[i] = byte(i) + 1 + } + data255[254] = 1 // Last byte wraps to 1 + + encoded255, err := CobsEncode(data255, 0) + require.NoError(t, err) + require.Len(t, encoded255, 257, "255 non-zero bytes encode to 257 bytes") + require.Equal(t, byte(0xFF), encoded255[0], "first code byte should be 0xFF") + require.Equal(t, byte(0x02), encoded255[255], "second code byte should be 0x02") + + decoded255, err := CobsDecode(encoded255, 0) + require.NoError(t, err) + require.Equal(t, data255, decoded255) + + // 253 non-zero bytes: [0xFE, 253 data bytes] - no extra code byte needed + data253 := make([]byte, 253) + for i := range data253 { + data253[i] = byte(i) + 1 + } + + encoded253, err := CobsEncode(data253, 0) + require.NoError(t, err) + require.Len(t, encoded253, 254, "253 non-zero bytes encode to 254 bytes") + require.Equal(t, byte(0xFE), encoded253[0], "code byte should be 0xFE for 253 data bytes") + + decoded253, err := CobsDecode(encoded253, 0) + require.NoError(t, err) + require.Equal(t, data253, decoded253) +} diff --git a/note-go/notecard/i2c-unix.go b/note-go/notecard/i2c-unix.go index 7820c6a..790eed8 100644 --- a/note-go/notecard/i2c-unix.go +++ b/note-go/notecard/i2c-unix.go @@ -83,9 +83,12 @@ func i2cWriteBytes(buf []byte, i2cAddr int) (err error) { i2cAddr = notecardDefaultI2CAddress } time.Sleep(1 * time.Millisecond) // By design, must not send more than once every 1Ms - reg := make([]byte, 1) + + // Single allocation for header + payload (avoids make + append pattern) + reg := make([]byte, 1+len(buf)) reg[0] = byte(len(buf)) - reg = append(reg, buf...) + copy(reg[1:], buf) + i2cLock.Lock() openI2CPort.device = &i2c.Dev{Bus: openI2CPort.bus, Addr: uint16(i2cAddr)} err = openI2CPort.device.Tx(reg, nil) @@ -103,13 +106,14 @@ func i2cReadBytes(datalen int, i2cAddr int) (outbuf []byte, available int, err e } time.Sleep(1 * time.Millisecond) // By design, must not send more than once every 1Ms readbuf := make([]byte, datalen+2) + + // Pre-allocate register buffer once outside retry loop + reg := [2]byte{0, byte(datalen)} + for i := 0; ; i++ { // Retry just for robustness - reg := make([]byte, 2) - reg[0] = byte(0) - reg[1] = byte(datalen) i2cLock.Lock() openI2CPort.device = &i2c.Dev{Bus: openI2CPort.bus, Addr: uint16(i2cAddr)} - err = openI2CPort.device.Tx(reg, readbuf) + err = openI2CPort.device.Tx(reg[:], readbuf) i2cLock.Unlock() if err == nil { break diff --git a/note-go/notecard/notecard.go b/note-go/notecard/notecard.go index 189cea6..b263e4f 100644 --- a/note-go/notecard/notecard.go +++ b/note-go/notecard/notecard.go @@ -45,6 +45,14 @@ var ( multiportTransLock [128]sync.RWMutex ) +// Buffer pool for serial read operations to reduce GC pressure +var serialReadBufPool = sync.Pool{ + New: func() interface{} { + buf := make([]byte, 2048) + return &buf + }, +} + // Default transaction timeout (before receiving anything from the notecard) const transactionTimeoutMsDefault = 30000 @@ -285,7 +293,10 @@ func cardResetSerial(context *Context, portConfig int) (err error) { // anything pending on serial", because the nature of read() is // that it blocks (until timeout) if there's nothing available. var length int - buf := make([]byte, 2048) + bufPtr := serialReadBufPool.Get().(*[]byte) + buf := *bufPtr + defer serialReadBufPool.Put(bufPtr) + for { if debugSerialIO { fmt.Printf("cardResetSerial: about to write newline\n") @@ -866,18 +877,15 @@ func (context *Context) transactionJSON(reqJSON []byte, multiport bool, portConf if !DoNotReterminateJSON { // Make sure that the JSON has a single \n terminator - for { - if strings.HasSuffix(string(reqJSON), "\n") { - reqJSON = []byte(strings.TrimSuffix(string(reqJSON), "\n")) - continue - } - if strings.HasSuffix(string(reqJSON), "\r") { - reqJSON = []byte(strings.TrimSuffix(string(reqJSON), "\r")) - continue + // Use byte operations instead of string conversions + for len(reqJSON) > 0 { + last := reqJSON[len(reqJSON)-1] + if last != '\n' && last != '\r' { + break } - break + reqJSON = reqJSON[:len(reqJSON)-1] } - reqJSON = []byte(string(reqJSON) + "\n") + reqJSON = append(reqJSON, '\n') } } @@ -1131,9 +1139,17 @@ func cardTransactionSerial(context *Context, portConfig int, noResponse bool, re // Read the reply until we get '\n' at the end waitBegan := time.Now() waitExpires := waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) + + // Get pooled buffer for reading to reduce allocations + bufPtr := serialReadBufPool.Get().(*[]byte) + buf := *bufPtr + defer serialReadBufPool.Put(bufPtr) + + // Pre-allocate response buffer + rspJSON = make([]byte, 0, 4096) + for { var length int - buf := make([]byte, 2048) if debugSerialIO { fmt.Printf("cardTransactionSerial: about to read up to %d bytes\n", len(buf)) } @@ -1172,7 +1188,9 @@ func cardTransactionSerial(context *Context, portConfig int, noResponse bool, re continue } rspJSON = append(rspJSON, buf[:length]...) - if !strings.Contains(string(rspJSON), "\n") { + + // Use bytes.IndexByte instead of strings.Contains + if bytes.IndexByte(rspJSON, '\n') == -1 { continue } @@ -1181,22 +1199,37 @@ func cardTransactionSerial(context *Context, portConfig int, noResponse bool, re break } - // At this point, if we split the string at \n its len must be >= 2 - // If the json didn't END in \n, we are still collecting a partial line - lines := strings.Split(string(rspJSON), "\n") - lastLine := lines[len(lines)-1] - secondToLastLine := lines[len(lines)-2] - if lastLine != "" { + // Find the last newline position + lastNewline := bytes.LastIndexByte(rspJSON, '\n') + if lastNewline == -1 { + continue + } + + // Check if there's a partial line after the last newline + if lastNewline < len(rspJSON)-1 { // The reply should be only a single line. However, if the user had been // in trace mode (likely on USB) we may be receiving trace lines that // were sent to us and inserted into the serial buffer prior to the JSON reply. - rspJSON = []byte(lastLine) + rspJSON = rspJSON[lastNewline+1:] continue } + // Find the second-to-last line + prevNewline := -1 + if lastNewline > 0 { + prevNewline = bytes.LastIndexByte(rspJSON[:lastNewline], '\n') + } + + var secondToLastLine []byte + if prevNewline == -1 { + secondToLastLine = rspJSON[:lastNewline] + } else { + secondToLastLine = rspJSON[prevNewline+1 : lastNewline] + } + // Skip the line if it's empty or doesn't look like JSON if len(secondToLastLine) == 0 || secondToLastLine[0] != '{' { - rspJSON = []byte{} + rspJSON = rspJSON[:0] continue } diff --git a/notecard/upload.go b/notecard/upload.go index bfc335c..f343f5c 100644 --- a/notecard/upload.go +++ b/notecard/upload.go @@ -172,11 +172,17 @@ func uploadFile(filename string, route string, target string) error { // COBS (Consistent Overhead Byte Stuffing) encoding ensures the binary // data can be safely transmitted over the serial connection without // conflicting with the newline character used as a packet delimiter. - encodedData, err := notecard.CobsEncode(chunkData, byte('\n')) + // + // Using CobsEncodeAppend to encode and append newline in one operation, + // which avoids a reallocation that would occur with separate encode + append. + encodedDataWithNewline, err := notecard.CobsEncodeAppend(chunkData, byte('\n'), byte('\n')) if err != nil { return fmt.Errorf("chunk %d: COBS encoding failed: %w", chunkNumber, err) } + // Length of encoded data without the trailing newline (for card.binary.put) + encodedLen := len(encodedDataWithNewline) - 1 + // --------------------------------------------------------------------- // 6d-6f: Transfer binary and send via web.post with retry logic // --------------------------------------------------------------------- @@ -188,8 +194,6 @@ func uploadFile(filename string, route string, target string) error { // We use a labeled loop so web.post failures can restart the entire // chunk upload process (binary transfer + web.post). - encodedDataWithNewline := append(encodedData, byte('\n')) - chunkRetry: for { // ----------------------------------------------------------------- @@ -198,7 +202,7 @@ func uploadFile(filename string, route string, target string) error { for { // Stage the chunk in the Notecard's binary buffer req := notecard.Request{Req: "card.binary.put"} - req.Cobs = int32(len(encodedData)) + req.Cobs = int32(encodedLen) _, err = card.TransactionRequest(req) if err != nil { From 1b015a87e566de2f72949b022ab41a385feb4310 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Mon, 2 Feb 2026 20:07:47 -0500 Subject: [PATCH 10/10] m --- go.mod | 41 +- go.sum | 107 +- note-go/.circleci/config.yml | 90 - note-go/.gitignore | 36 - note-go/.vscode/launch.json | 18 - note-go/CODE_OF_CONDUCT.md | 7 - note-go/CONTRIBUTING.md | 64 - note-go/LICENSE | 19 - note-go/README.md | 32 - note-go/build.sh | 38 - note-go/go.mod | 17 - note-go/go.sum | 39 - note-go/note/access.go | 96 - note-go/note/contacts.go | 25 - note-go/note/dfu.go | 55 - note-go/note/errors.go | 349 --- note-go/note/event.go | 267 --- note-go/note/message.go | 67 - note-go/note/note.go | 259 --- note-go/note/notefile.go | 99 - note-go/note/session.go | 161 -- note-go/note/usage.go | 21 - note-go/note/words.go | 2196 ------------------ note-go/note/words_test.go | 19 - note-go/notecard/cobs.go | 81 - note-go/notecard/cobs_test.go | 248 -- note-go/notecard/i2c-unix.go | 180 -- note-go/notecard/i2c-windows.go | 49 - note-go/notecard/lease.go | 196 -- note-go/notecard/net.go | 82 - note-go/notecard/notecard.go | 1658 ------------- note-go/notecard/play.go | 223 -- note-go/notecard/request.go | 536 ----- note-go/notecard/serial-default.go | 87 - note-go/notecard/serial-unix.go | 17 - note-go/notecard/serial-windows.go | 24 - note-go/notecard/test.go | 98 - note-go/notecard/trace.go | 106 - note-go/notecard/ua.go | 49 - note-go/notehub/api/billing.go | 15 - note-go/notehub/api/devices.go | 165 -- note-go/notehub/api/environment_variables.go | 206 -- note-go/notehub/api/error_defaults.go | 88 - note-go/notehub/api/errors.go | 80 - note-go/notehub/api/events.go | 30 - note-go/notehub/api/fleet.go | 78 - note-go/notehub/api/job.go | 45 - note-go/notehub/api/job_reconciliation.go | 94 - note-go/notehub/api/products.go | 30 - note-go/notehub/api/project.go | 40 - note-go/notehub/api/session.go | 21 - note-go/notehub/auth.go | 340 --- note-go/notehub/config.go | 8 - note-go/notehub/dbquery.go | 19 - note-go/notehub/request.go | 257 -- note-go/package.sh | 47 - notecard/echo.go | 2 +- 57 files changed, 65 insertions(+), 9256 deletions(-) delete mode 100644 note-go/.circleci/config.yml delete mode 100644 note-go/.gitignore delete mode 100644 note-go/.vscode/launch.json delete mode 100644 note-go/CODE_OF_CONDUCT.md delete mode 100644 note-go/CONTRIBUTING.md delete mode 100644 note-go/LICENSE delete mode 100644 note-go/README.md delete mode 100755 note-go/build.sh delete mode 100644 note-go/go.mod delete mode 100644 note-go/go.sum delete mode 100644 note-go/note/access.go delete mode 100644 note-go/note/contacts.go delete mode 100644 note-go/note/dfu.go delete mode 100644 note-go/note/errors.go delete mode 100644 note-go/note/event.go delete mode 100644 note-go/note/message.go delete mode 100644 note-go/note/note.go delete mode 100644 note-go/note/notefile.go delete mode 100644 note-go/note/session.go delete mode 100644 note-go/note/usage.go delete mode 100644 note-go/note/words.go delete mode 100644 note-go/note/words_test.go delete mode 100644 note-go/notecard/cobs.go delete mode 100644 note-go/notecard/cobs_test.go delete mode 100644 note-go/notecard/i2c-unix.go delete mode 100644 note-go/notecard/i2c-windows.go delete mode 100644 note-go/notecard/lease.go delete mode 100644 note-go/notecard/net.go delete mode 100644 note-go/notecard/notecard.go delete mode 100644 note-go/notecard/play.go delete mode 100644 note-go/notecard/request.go delete mode 100644 note-go/notecard/serial-default.go delete mode 100644 note-go/notecard/serial-unix.go delete mode 100644 note-go/notecard/serial-windows.go delete mode 100644 note-go/notecard/test.go delete mode 100644 note-go/notecard/trace.go delete mode 100644 note-go/notecard/ua.go delete mode 100644 note-go/notehub/api/billing.go delete mode 100644 note-go/notehub/api/devices.go delete mode 100644 note-go/notehub/api/environment_variables.go delete mode 100644 note-go/notehub/api/error_defaults.go delete mode 100644 note-go/notehub/api/errors.go delete mode 100644 note-go/notehub/api/events.go delete mode 100644 note-go/notehub/api/fleet.go delete mode 100644 note-go/notehub/api/job.go delete mode 100644 note-go/notehub/api/job_reconciliation.go delete mode 100644 note-go/notehub/api/products.go delete mode 100644 note-go/notehub/api/project.go delete mode 100644 note-go/notehub/api/session.go delete mode 100644 note-go/notehub/auth.go delete mode 100644 note-go/notehub/config.go delete mode 100644 note-go/notehub/dbquery.go delete mode 100644 note-go/notehub/request.go delete mode 100755 note-go/package.sh diff --git a/go.mod b/go.mod index 8a6e4f5..121badc 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,39 @@ module github.com/note-cli -go 1.15 +go 1.24.0 replace github.com/blues/note-cli/lib => ./lib -replace github.com/blues/note-go => ./note-go - -// uncomment this for easier testing locally -// replace github.com/blues/note-go => ../hub/note-go require ( - github.com/blues/note-cli/lib v0.0.0-20240515194341-6ba45582741d - github.com/blues/note-go v1.7.4 - github.com/fatih/color v1.17.0 + github.com/blues/note-cli/lib v0.0.0-20251120160051-d509bdf52531 + github.com/blues/note-go v1.7.5 + github.com/fatih/color v1.18.0 github.com/peterh/liner v1.2.2 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 ) +require ( + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/creack/goselect v0.1.3 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + periph.io/x/conn/v3 v3.7.2 // indirect +) + require ( github.com/go-ole/go-ole v1.3.0 // indirect - github.com/golang/snappy v0.0.4 - github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/golang/snappy v1.0.0 + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/shirou/gopsutil/v3 v3.24.4 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.7 // indirect - github.com/tklauser/go-sysconf v0.3.14 // indirect - go.bug.st/serial v1.6.2 - golang.org/x/sys v0.20.0 // indirect - periph.io/x/host/v3 v3.8.2 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + go.bug.st/serial v1.6.4 + golang.org/x/sys v0.40.0 // indirect + periph.io/x/host/v3 v3.8.5 // indirect ) diff --git a/go.sum b/go.sum index 54c87ec..79a5838 100644 --- a/go.sum +++ b/go.sum @@ -1,116 +1,89 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/blues/note-go v1.5.0/go.mod h1:F66ZqObdOhxRRXIwn9+YhVGqB93jMAnqlO2ibwMa998= -github.com/blues/note-go v1.7.4 h1:AqeU6HXkCa7FwDsAao49H6DdTTtNNGJYjGwevZi4Shc= -github.com/blues/note-go v1.7.4/go.mod h1:GfslvbmFus7z05P1YykcbMedTKTuDNTf8ryBb1Qjq/4= -github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/blues/note-go v1.7.5 h1:Vzx//F4haI6XPkYDqadN61NkXKwBfsODynjjf8YODF0= +github.com/blues/note-go v1.7.5/go.mod h1:GfslvbmFus7z05P1YykcbMedTKTuDNTf8ryBb1Qjq/4= +github.com/blues/note-go v1.8.0 h1:7h9xVXREnFK0bT7xcYyXq19s1yPcFhZjrkerqFV0TLg= +github.com/blues/note-go v1.8.0/go.mod h1:GfslvbmFus7z05P1YykcbMedTKTuDNTf8ryBb1Qjq/4= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc= +github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= -github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= -github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= -github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0= github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= -github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= -github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.bug.st/serial v1.3.4/go.mod h1:z8CesKorE90Qr/oRSJiEuvzYRKol9r/anJZEb5kt304= go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= -go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= -go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA= periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg= +periph.io/x/conn/v3 v3.7.2 h1:qt9dE6XGP5ljbFnCKRJ9OOCoiOyBGlw7JZgoi72zZ1s= +periph.io/x/conn/v3 v3.7.2/go.mod h1:Ao0b4sFRo4QOx6c1tROJU1fLJN1hUIYggjOrkIVnpGg= periph.io/x/d2xx v0.1.0/go.mod h1:OflHQcWZ4LDP/2opGYbdXSP/yvWSnHVFO90KRoyobWY= periph.io/x/host/v3 v3.8.0/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= -periph.io/x/host/v3 v3.8.2 h1:ayKUDzgUCN0g8+/xM9GTkWaOBhSLVcVHGTfjAOi8OsQ= -periph.io/x/host/v3 v3.8.2/go.mod h1:yFL76AesNHR68PboofSWYaQTKmvPXsQH2Apvp/ls/K4= -periph.io/x/periph v3.6.2+incompatible/go.mod h1:EWr+FCIU2dBWz5/wSWeiIUJTriYv9v2j2ENBmgYyy7Y= +periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII= +periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc= diff --git a/note-go/.circleci/config.yml b/note-go/.circleci/config.yml deleted file mode 100644 index 2d15f72..0000000 --- a/note-go/.circleci/config.yml +++ /dev/null @@ -1,90 +0,0 @@ -# Golang CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-go/ for more details -version: 2 -jobs: - build-unix-and-windows: - docker: - - image: circleci/golang:1.14.4 - working_directory: /go/src/github.com/blues/note-go - steps: - - checkout - - run: export GOOS=linux GOARCH=amd64 ; ./build.sh && ./package.sh - - run: export GOOS=linux GOARCH=arm ; ./build.sh && ./package.sh - - run: export GOOS=windows GOARCH=386 ; ./build.sh && ./package.sh - - run: export GOOS=windows GOARCH=amd64 ; ./build.sh && ./package.sh - - run: find ./build/ -type f - - store_artifacts: - path: ./build/packages/ - - persist_to_workspace: - root: . - paths: - - ./build/packages/ - build-macos: - macos: - xcode: 11.3.0 - steps: - - checkout - - run: pwd - - run: echo $PATH - - run: - name: install go - command: | - curl https://dl.google.com/go/go1.14.4.darwin-amd64.tar.gz | - tar -C "$HOME" -xz # install go to $HOME/go/ - - run: - name: build and package - command: | - export PATH="$PATH:$HOME/go/bin" - ./build.sh - ./package.sh - - store_artifacts: - path: ./build/packages/ - - persist_to_workspace: - root: . - paths: - - ./build/packages/ - publish-github-release: - docker: - - image: cibuilds/github:0.10 - steps: - # We need to do a checkout so the CIRCLE_PROJECT_REPONAME and CIRCLE_SHA1 vars are populated - # for the command below. - - checkout - - attach_workspace: - at: . - - run: ls -l ./build/packages/ - - run: - name: "Publish Release on GitHub" - command: | - ghr -t "${GITHUB_TOKEN}" -u "${CIRCLE_PROJECT_USERNAME}" -r "${CIRCLE_PROJECT_REPONAME}" \ - -c "${CIRCLE_SHA1}" -delete "${CIRCLE_TAG}" ./build/packages/ - # The GITHUB_TOKEN is generated here: https://github.com/settings/tokens for the - # notebot-ci user and securely set here: - # https://app.circleci.com/settings/project/github/blues/note-go/environment-variables - -workflows: - version: 2 - build-and-publish: - jobs: - - build-macos: - filters: - # Because we don't filter out certain branches this code implicitly says `build-macos` - # will run for all builds triggered by a branch push or PR. But in the circle-ci ui we - # chose to only build for PRs here: - # https://app.circleci.com/settings/project/github/blues/note-go/advanced - tags: &PUBLISH_TAG_FILTER_REGEX - # Unlike branch-triggered builds, we do filter down to certain tags. Match v1.2.3 etc. - # i.e. the only tags which can trigger must look like they're tagging a release. - only: /^v\d+\.\d+\.\d+$/ - - build-unix-and-windows: - filters: - tags: *PUBLISH_TAG_FILTER_REGEX - - publish-github-release: - requires: - - build-macos - - build-unix-and-windows - filters: - branches: - ignore: /.*/ - tags: *PUBLISH_TAG_FILTER_REGEX diff --git a/note-go/.gitignore b/note-go/.gitignore deleted file mode 100644 index 23930f1..0000000 --- a/note-go/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# jetbrains IDEs config files -**/.idea -build - -# Production build proucts -./build/ - -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# auto- generated files # -###################### -*~ -\#*\# -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# VS Code configuration files -.vscode/* -# whitelist -!.vscode/launch.json diff --git a/note-go/.vscode/launch.json b/note-go/.vscode/launch.json deleted file mode 100644 index aee041d..0000000 --- a/note-go/.vscode/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to ./notecard", - "type": "go", - "request": "attach", - "mode":"local", - "processId": 6968, // Fill this in with the PID of your notecard process. - "remotePath": "/go/src/github.com/blues/note-go", - // Sadly this doesn't work. Even if you set isBackground on the task. - //"preLaunchTask": "Compose w/ Debuggable Noteboard", - }, - ], -} \ No newline at end of file diff --git a/note-go/CODE_OF_CONDUCT.md b/note-go/CODE_OF_CONDUCT.md deleted file mode 100644 index 50838cf..0000000 --- a/note-go/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,7 +0,0 @@ -# Code of conduct - -By participating in this project, you agree to abide by the -[Blues Inc code of conduct][1]. - -[1]: https://blues.github.io/opensource/code-of-conduct - diff --git a/note-go/CONTRIBUTING.md b/note-go/CONTRIBUTING.md deleted file mode 100644 index 53f322f..0000000 --- a/note-go/CONTRIBUTING.md +++ /dev/null @@ -1,64 +0,0 @@ -# Contributing to blues/note-go - -We love pull requests from everyone. By participating in this project, you -agree to abide by the Blues Inc [code of conduct]. - -[code of conduct]: https://blues.github.io/opensource/code-of-conduct - -Here are some ways *you* can contribute: - -* by using alpha, beta, and prerelease versions -* by reporting bugs -* by suggesting new features -* by writing or editing documentation -* by writing specifications -* by writing code ( **no patch is too small** : fix typos, add comments, -clean up inconsistent whitespace ) -* by refactoring code -* by closing [issues][] -* by reviewing patches - -[issues]: https://github.com/blues/note-go/issues - -## Submitting an Issue - -* We use the [GitHub issue tracker][issues] to track bugs and features. -* Before submitting a bug report or feature request, check to make sure it - hasn't - already been submitted. -* When submitting a bug report, please include a [Gist][] that includes a stack - trace and any details that may be necessary to reproduce the bug, including - your release version, Go version, and operating system. Ideally, a bug report - should include a pull request with failing specs. - -[gist]: https://gist.github.com/ - -## Cleaning up issues - -* Issues that have no response from the submitter will be closed after 30 days. -* Issues will be closed once they're assumed to be fixed or answered. If the - maintainer is wrong, it can be opened again. -* If your issue is closed by mistake, please understand and explain the issue. - We will happily reopen the issue. - -## Submitting a Pull Request -1. [Fork][fork] the [official repository][repo]. -2. [Create a topic branch.][branch] -3. Implement your feature or bug fix. -4. Add, commit, and push your changes. -5. [Submit a pull request.][pr] - -## Notes -* Please add tests if you changed code. Contributions without tests won't be -* accepted. If you don't know how to add tests, please put in a PR and leave a -* comment asking for help. We love helping! - -[repo]: https://github.com/blues/note-go/tree/master -[fork]: https://help.github.com/articles/fork-a-repo/ -[branch]: -https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/ -[pr]: https://help.github.com/articles/creating-a-pull-request-from-a-fork/ - -Inspired by -https://github.com/thoughtbot/factory_bot/blob/master/CONTRIBUTING.md - diff --git a/note-go/LICENSE b/note-go/LICENSE deleted file mode 100644 index aed1af8..0000000 --- a/note-go/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2019 Blues Inc - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/note-go/README.md b/note-go/README.md deleted file mode 100644 index 83492ff..0000000 --- a/note-go/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# [Blues Wireless][blues] - -The note-go Go library for communicating with Blues Wireless Notecard via serial or I²C. - -This library allows you to control a Notecard by coding in Go. -Your program may configure Notecard and send Notes to [Notehub.io][notehub]. - -See also: -* [note-c][note-c] for C bindings -* [note-python][note-python] for Python bindings - -## Installing -For all releases, we have compiled the notecard utility for different OS and architectures [here](https://github.com/blues/note-go/releases). -If you don't see your OS and architecture supported, please file an issue and we'll add it to new releases. - -[blues]: https://blues.com -[notehub]: https://notehub.io -[note-arduino]: https://github.com/blues/note-arduino -[note-c]: https://github.com/blues/note-c -[note-go]: https://github.com/blues/note-go -[note-python]: https://github.com/blues/note-python - -## Dependencies -- Install Go and the Go tools [(here)](https://golang.org/doc/install) - -## Compiling the notecard utility -If you want to build the latest, follow the directions below. -```bash -$ cd tools/notecard -$ go get -u . -$ go build . -``` diff --git a/note-go/build.sh b/note-go/build.sh deleted file mode 100755 index 6a50482..0000000 --- a/note-go/build.sh +++ /dev/null @@ -1,38 +0,0 @@ -#! /usr/bin/env bash -# -# Copyright 2020 Blues Inc. All rights reserved. -# Use of this source code is governed by licenses granted by the -# copyright holder including that found in the LICENSE file. -# -######### Bash Boilerplate ########## -set -euo pipefail # strict mode -readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -cd "$SCRIPT_DIR" # cd to this script's dir -######### End Bash Boilerplate ########## - -# -# note-go build.sh -# -# This script builds all the note-go executables (note, notecard, notehub) by -# looking for any folder containing a main.go and running `go build`. -# -# Parameters: Set $GOOS and $GOARCH to cross compile for different platforms. -# -# Output: Executables are saved in "./build/$GOOS/$GOARCH/" -# - -# Add GOOS and GOARCH to our environment. (and other GO vars we don't need) -eval "$(go env)" - -readonly BUILD_EXE_DIR="$SCRIPT_DIR/build/$GOOS/$GOARCH/" -mkdir -p "$BUILD_EXE_DIR" - -# Build each executable binary -# build_dirs is an array of all the folders which contain a main.go -IFS=$'\r\n' GLOBIGNORE='*' command eval 'build_dirs=($(find . -name main.go -print0 | xargs -0n1 dirname))' -for dir in "${build_dirs[@]}"; do - ( - cd "$dir" - go build ${GO_BUILD_FLAGS:-} -o "$BUILD_EXE_DIR" - ) -done diff --git a/note-go/go.mod b/note-go/go.mod deleted file mode 100644 index 7295be5..0000000 --- a/note-go/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module github.com/blues/note-go - -// 2023-02-26 Raspberry Pi apt-get only is updated to 1.15 -go 1.15 - -require ( - github.com/gofrs/flock v0.7.1 - github.com/shirou/gopsutil/v3 v3.21.6 - github.com/stretchr/testify v1.7.0 - go.bug.st/serial v1.6.1 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - periph.io/x/conn/v3 v3.7.0 - periph.io/x/host/v3 v3.8.0 -) diff --git a/note-go/go.sum b/note-go/go.sum deleted file mode 100644 index b835d1f..0000000 --- a/note-go/go.sum +++ /dev/null @@ -1,39 +0,0 @@ -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= -github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= -github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= -github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc= -github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= -github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/shirou/gopsutil/v3 v3.21.6 h1:vU7jrp1Ic/2sHB7w6UNs7MIkn7ebVtTb5D9j45o9VYE= -github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tklauser/go-sysconf v0.3.6 h1:oc1sJWvKkmvIxhDHeKWvZS4f6AW+YcoguSfRF2/Hmo4= -github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= -github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= -github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= -go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY= -go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= -golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA= -periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg= -periph.io/x/d2xx v0.1.0/go.mod h1:OflHQcWZ4LDP/2opGYbdXSP/yvWSnHVFO90KRoyobWY= -periph.io/x/host/v3 v3.8.0 h1:T5ojZ2wvnZHGPS4h95N2ZpcCyHnsvH3YRZ1UUUiv5CQ= -periph.io/x/host/v3 v3.8.0/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= diff --git a/note-go/note/access.go b/note-go/note/access.go deleted file mode 100644 index 1aec57c..0000000 --- a/note-go/note/access.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -// The full URN of a resource for permissioning purposes is: -// app:xxx-xxxx-xxxx-xxxx:dev:xxxxxxxxxxx:file:xxxx - -// ACResourceApp is the app (project) resource, which is the appUID that always begins with this string -const ACResourceApp = "app:" - -// ACResourceApps is the resource for all apps -const ACResourceApps = "app:*" - -// ACResourceDevice is the device resource, which is the deviceUID that always begins with this string -const ACResourceDevice = "dev:" - -// ACResourceDevices is the resource for all devices -const ACResourceDevices = "dev:*" - -// ACResourceNotefile is the notefile resource and its note-level actions, -// which is the notefileID prefixed with this string -const ACResourceNotefile = "file:" - -// ACResourceNotefiles is the resource for all notefiles and all meta-notefile-level actions -const ACResourceNotefiles = "file:*" - -// ACResourceAccount is an account resource, which is the accountUID that always begins with this string -const ACResourceAccount = "account:" - -// ACResourceAccounts is the resource for all accounts and all meta-account-level actions -const ACResourceAccounts = "account:*" - -// ACResourceRoute is an route resource, which is the routeUID that always begins with this string -const ACResourceRoute = "route:" - -// ACResourceRoutes is the resource for all routes and all meta-route-level actions -const ACResourceRoutes = "route:*" - -// ACResourceNotecardFirmwares is the resource for all notecard firmware -const ACResourceNotecardFirmwares = "notecard:*" - -// ACResourceUserFirmwares is the resource for all user firmware -const ACResourceUserFirmwares = "firmware:*" - -// ACResourceSep is the separator for building compound resource names -const ACResourceSep = ":" - -// Entire vocabulary of allowed actions on resources - -// ACActionRead (golint) -const ACActionRead = "read" - -// ACActionUpdate (golint) -const ACActionUpdate = "update" - -// ACActionCreate (golint) -const ACActionCreate = "create" - -// ACActionDelete (golint) -const ACActionDelete = "delete" - -// ACActionMonitor (golint) -const ACActionMonitor = "monitor" - -// Ways of combining actions into one - -// ACActionAnd ensures that all of these actions are allowed -const ACActionAnd = "&" - -// ACActionOr ensures that any of these actions are allowed -const ACActionOr = "|" - -// The entire palette of valid actions, as a comma-separated list - -// ACValidActionsApp are actions allowed on apps -const ACValidActionsApp = "app:create,app:read,app:update,app:delete,app:monitor" - -// ACValidActionsDev are actions allowed on devices -const ACValidActionsDev = "dev:read,dev:update,dev:delete,dev:monitor" - -// ACValidActionsFile are actions allowed on notefiles -const ACValidActionsFile = "file:create,file:read,file:update,file:delete" - -// ACValidActionsAccount are actions allowed on accounts -const ACValidActionsAccount = "account:create,account:read,account:update,account:delete" - -// ACValidActionsRoute are actions allowed on routes -const ACValidActionsRoute = "route:create,route:read,route:update,route:delete" - -// ACValidActionsNotecard are actions allowed on notecard firmware -const ACValidActionsNotecard = "notecard:create,notecard:read,notecard:update,notecard:delete" - -// ACValidActionsFirmware are actions allowed on user firmware -const ACValidActionsFirmware = "firmware:create,firmware:read,firmware:update,firmware:delete" diff --git a/note-go/note/contacts.go b/note-go/note/contacts.go deleted file mode 100644 index 5be7645..0000000 --- a/note-go/note/contacts.go +++ /dev/null @@ -1,25 +0,0 @@ -package note - -// Contact has the basic contact info structure -// -// NOTE: This structure's underlying storage has been decoupled from the use of -// the structure in business logic. As such, please share any changes to these -// structures with cloud services to ensure that storage and testing frameworks -// are kept in sync with these structures used for business logic -type Contact struct { - Name string `json:"name,omitempty"` - Affiliation string `json:"org,omitempty"` - Role string `json:"role,omitempty"` - Email string `json:"email,omitempty"` -} - -// Contacts has contact info for this app -// -// NOTE: This structure's underlying storage has been decoupled from the use of -// the structure in business logic. As such, please share any changes to these -// structures with cloud services to ensure that storage and testing frameworks -// are kept in sync with these structures used for business logic -type Contacts struct { - Admin *Contact `json:"admin,omitempty"` - Tech *Contact `json:"tech,omitempty"` -} diff --git a/note-go/note/dfu.go b/note-go/note/dfu.go deleted file mode 100644 index 0e57a2e..0000000 --- a/note-go/note/dfu.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2025 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -// Package note dfu.go contains DFU-related structures generated/parsed by the notecard -package note - -// DFUState is the state of the DFU in progress -type DFUState struct { - Type string `json:"type,omitempty"` - File string `json:"file,omitempty"` - Length uint32 `json:"length,omitempty"` - CRC32 uint32 `json:"crc32,omitempty"` - MD5 string `json:"md5,omitempty"` - Phase string `json:"mode,omitempty"` - Status string `json:"status,omitempty"` - BeganSecs uint32 `json:"began,omitempty"` - RetryCount uint32 `json:"retry,omitempty"` - ConsecutiveErrors uint32 `json:"errors,omitempty"` - BinaryRetries uint32 `json:"binretry,omitempty"` - DFUStartCount uint32 `json:"dfu_started,omitempty"` - DFUCompletedCount uint32 `json:"dfu_completed,omitempty"` - ODFUStartedCount uint32 `json:"odfu_started,omitempty"` - ODFUTarget string `json:"odfu_target,omitempty"` - ReadFromService uint32 `json:"read,omitempty"` - UpdatedSecs uint32 `json:"updated,omitempty"` - DownloadComplete bool `json:"dl_complete,omitempty"` - DisabledReason string `json:"disabled,omitempty"` - MinNotecardVersion string `json:"min_card_version,omitempty"` - - // This will always point to the current running version - Version string `json:"version,omitempty"` -} - -// DFUEnv is the data structure passed to Notehub when DFU info changes -type DFUEnv struct { - Card *DFUState `json:"card,omitempty"` - User *DFUState `json:"user,omitempty"` - Modem *DFUState `json:"modem,omitempty"` - Star *DFUState `json:"star,omitempty"` -} - -type DfuPhase string - -const ( - DfuPhaseUnknown DfuPhase = "" - DfuPhaseIdle DfuPhase = "idle" - DfuPhaseError DfuPhase = "error" - DfuPhaseDownloading DfuPhase = "downloading" - DfuPhaseSideloading DfuPhase = "sideloading" - DfuPhaseReady DfuPhase = "ready" - DfuPhaseReadyRetry DfuPhase = "ready-retry" - DfuPhaseUpdating DfuPhase = "updating" - DfuPhaseCompleted DfuPhase = "completed" -) diff --git a/note-go/note/errors.go b/note-go/note/errors.go deleted file mode 100644 index 87bba25..0000000 --- a/note-go/note/errors.go +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -// Package note errors.go contains programmatically-testable error strings -package note - -import ( - "fmt" - "net/http" - "strings" -) - -// ErrTimeout (golint) -const ErrTimeout = "{timeout}" - -var _ = defineError(ErrTimeout, http.StatusRequestTimeout) - -// ErrInternalTimeout of a notehub-to-notehub transaction (golint) -const ErrInternalTimeout = "{internal-timeout}" - -var _ = defineError(ErrInternalTimeout, http.StatusGatewayTimeout) - -// ErrRouteTimeout of a notehub-to-customer-service transaction (golint) -const ErrRouteTimeout = "{route-timeout}" - -var _ = defineError(ErrRouteTimeout, http.StatusRequestTimeout) - -// ErrClosed (golint) -const ErrClosed = "{closed}" - -var _ = defineError(ErrClosed, http.StatusGone) - -// ErrFileNoExist (golint) -const ErrFileNoExist = "{file-noexist}" - -var _ = defineError(ErrFileNoExist, http.StatusNotFound) - -// ErrNotefileName (golint) -const ErrNotefileName = "{notefile-bad-name}" - -var _ = defineError(ErrNotefileName, http.StatusBadRequest) - -// ErrNotefileInUse (golint) -const ErrNotefileInUse = "{notefile-in-use}" - -var _ = defineError(ErrNotefileInUse, http.StatusConflict) - -// ErrNotefileExists (golint) -const ErrNotefileExists = "{notefile-exists}" - -var _ = defineError(ErrNotefileExists, http.StatusConflict) - -// ErrNotefileNoExist (golint) -const ErrNotefileNoExist = "{notefile-noexist}" - -var _ = defineError(ErrNotefileNoExist, http.StatusNotFound) - -// ErrNotefileQueueDisallowed (golint) -const ErrNotefileQueueDisallowed = "{notefile-queue-disallowed}" - -var _ = defineError(ErrNotefileQueueDisallowed, http.StatusBadRequest) - -// ErrNoteNoExist (golint) -const ErrNoteNoExist = "{note-noexist}" - -var _ = defineError(ErrNoteNoExist, http.StatusNotFound) - -// ErrNoteExists (golint) -const ErrNoteExists = "{note-exists}" - -var _ = defineError(ErrNoteExists, http.StatusConflict) - -// ErrTooManyNotes (golint) -const ErrTooManyNotes = "{too-many-notes}" - -var _ = defineError(ErrTooManyNotes, http.StatusBadRequest) - -// ErrTrackerNoExist (golint) -const ErrTrackerNoExist = "{tracker-noexist}" - -var _ = defineError(ErrTrackerNoExist, http.StatusNotFound) - -// ErrTrackerExists (golint) -const ErrTrackerExists = "{tracker-exists}" - -var _ = defineError(ErrTrackerExists, http.StatusConflict) - -// ErrNetwork (golint) -const ErrNetwork = "{network}" - -var _ = defineError(ErrNetwork, http.StatusServiceUnavailable) - -// ErrRegistrationFailure (golint) -const ErrRegistrationFailure = "{registration-failure}" - -var _ = defineError(ErrRegistrationFailure, http.StatusServiceUnavailable) - -// ErrExtendedNetworkFailure (golint) -const ErrExtendedNetworkFailure = "{extended-network-failure}" - -var _ = defineError(ErrExtendedNetworkFailure, http.StatusServiceUnavailable) - -// ErrExtendedServiceFailure (golint) -const ErrExtendedServiceFailure = "{extended-service-failure}" - -var _ = defineError(ErrExtendedServiceFailure, http.StatusServiceUnavailable) - -// ErrHostUnreachable (golint) -const ErrHostUnreachable = "{host-unreachable}" - -var _ = defineError(ErrHostUnreachable, http.StatusServiceUnavailable) - -// ErrDFUNotReady (golint) -const ErrDFUNotReady = "{dfu-not-ready}" - -var _ = defineError(ErrDFUNotReady, http.StatusServiceUnavailable) - -// ErrDFUInProgress (golint) -const ErrDFUInProgress = "{dfu-in-progress}" - -var _ = defineError(ErrDFUInProgress, http.StatusServiceUnavailable) - -// ErrAuth (golint) -const ErrAuth = "{auth}" - -var _ = defineError(ErrAuth, http.StatusUnauthorized) - -// ErrTicket (golint) -const ErrTicket = "{ticket}" - -var _ = defineError(ErrTicket, http.StatusUnauthorized) - -// ErrHubNoHandler (golint) -const ErrHubNoHandler = "{no-handler}" - -var _ = defineError(ErrHubNoHandler, http.StatusInternalServerError) - -// ErrDeviceNotFound (golint) -const ErrDeviceNotFound = "{device-noexist}" - -var _ = defineError(ErrDeviceNotFound, http.StatusNotFound) - -// ErrDeviceNotSpecified (golint) -const ErrDeviceNotSpecified = "{device-none}" - -var _ = defineError(ErrDeviceNotSpecified, http.StatusBadRequest) - -// ErrDeviceId (golint) -const ErrDeviceId = "{device-id-invalid}" - -var _ = defineError(ErrDeviceId, http.StatusBadRequest) - -// ErrDeviceDisabled (golint) -const ErrDeviceDisabled = "{device-disabled}" - -var _ = defineError(ErrDeviceDisabled, http.StatusBadRequest) - -// ErrProductNotFound (golint) -const ErrProductNotFound = "{product-noexist}" - -var _ = defineError(ErrProductNotFound, http.StatusNotFound) - -// ErrProductNotSpecified (golint) -const ErrProductNotSpecified = "{product-none}" - -var _ = defineError(ErrProductNotSpecified, http.StatusBadRequest) - -// ErrAppNotFound (golint) -const ErrAppNotFound = "{app-noexist}" - -var _ = defineError(ErrAppNotFound, http.StatusNotFound) - -// ErrAppNotSpecified (golint) -const ErrAppNotSpecified = "{app-none}" - -var _ = defineError(ErrAppNotSpecified, http.StatusBadRequest) - -// ErrAppDeleted (golint) -const ErrAppDeleted = "{app-deleted}" - -var _ = defineError(ErrAppDeleted, http.StatusGone) - -// ErrAppExists (golint) -const ErrAppExists = "{app-exists}" - -var _ = defineError(ErrAppExists, http.StatusConflict) - -// ErrFleetNotFound (golint) -const ErrFleetNotFound = "{fleet-noexist}" - -var _ = defineError(ErrFleetNotFound, http.StatusNotFound) - -// ErrCardIo (golint) -const ErrCardIo = "{io}" - -var _ = defineError(ErrCardIo, http.StatusBadGateway) - -// ErrCardHeartbeat (golint) Doesn't seem to be used as a request error -const ErrCardHeartbeat = "{heartbeat}" - -// ErrAccessDenied (golint) -const ErrAccessDenied = "{access-denied}" - -var _ = defineError(ErrAccessDenied, http.StatusForbidden) - -// ErrWebPayload (golint) -const ErrWebPayload = "{web-payload}" - -var _ = defineError(ErrWebPayload, http.StatusBadRequest) - -// ErrHubMode (golint) Unused -const ErrHubMode = "{hub-mode}" - -// ErrTemplateIncompatible (golint) -const ErrTemplateIncompatible = "{template-incompatible}" - -var _ = defineError(ErrTemplateIncompatible, http.StatusBadRequest) - -// ErrSyntax (golint) -const ErrSyntax = "{syntax}" - -var _ = defineError(ErrSyntax, http.StatusBadRequest) - -// ErrIncompatible (golint) -const ErrIncompatible = "{incompatible}" - -var _ = defineError(ErrIncompatible, http.StatusNotAcceptable) - -// ErrReqNotSupported (golint) -const ErrReqNotSupported = "{not-supported}" - -var _ = defineError(ErrReqNotSupported, http.StatusNotImplemented) - -// ErrTooBig (golint) -const ErrTooBig = "{too-big}" - -var _ = defineError(ErrTooBig, http.StatusRequestEntityTooLarge) - -// ErrJson (golint) -const ErrJson = "{not-json}" - -var _ = defineError(ErrJson, http.StatusBadRequest) - -// Status messages returned by the notecard in request.Status -const StatusIdle = "{idle}" -const StatusNtnIdle = "{ntn-idle}" -const StatusTransportConnected = "{connected}" -const StatusTransportDisconnected = "{disconnected}" -const StatusTransportConnecting = "{connecting}" -const StatusTransportConnectFailure = "{connect-failure}" -const StatusTransportConnectedClosed = "{connected-closed}" -const StatusTransportWaitService = "{wait-service}" -const StatusTransportWaitData = "{wait-data}" -const StatusTransportWaitGateway = "{wait-gateway}" -const StatusTransportWaitModule = "{wait-module}" -const StatusGPSInactive = "{gps-inactive}" - -// These are returned from JSONata transforms as special strings to indicate the given behavior -// Used by Smart Fleets and during routing -const ErrAddToFleet = "{add-to-fleet}" -const ErrRemoveFromFleet = "{remove-from-fleet}" -const ErrLeaveFleetAlone = "{leave-fleet-alone}" -const ErrDoNotRoute = "{do-not-route}" - -// These can be sent from Notehub to the notecard to indicate it should pause before reconnecting -// Currently unused -const ErrDeviceDelay5 = "{device-delay-5}" -const ErrDeviceDelay10 = "{device-delay-10}" -const ErrDeviceDelay15 = "{device-delay-15}" -const ErrDeviceDelay20 = "{device-delay-20}" -const ErrDeviceDelay30 = "{device-delay-30}" -const ErrDeviceDelay60 = "{device-delay-60}" - -// ErrorContains tests to see if an error contains an error keyword that we might expect -func ErrorContains(err error, errKeyword string) bool { - if err == nil { - return false - } - return strings.Contains(fmt.Sprintf("%s", err), errKeyword) -} - -var errToHttpStatusMap map[string]int - -func defineError(errKeyword string, httpStatus int) string { - if errToHttpStatusMap == nil { - errToHttpStatusMap = make(map[string]int) - } - errToHttpStatusMap[errKeyword] = httpStatus - return errKeyword -} - -// This scans a response.Err string for known error keywords and returns the appropriate HTTP status code -// If there are multiple error keywords, the first one found is used as the source for the code. -// We choose the first one because that should be the most relevant to the specific failure. -// If no known error keywords are found, we return HTTP 500 Internal Server Error. -func ErrorHttpStatus(errstr string) int { - if errstr == "" { - return http.StatusOK - } - start := strings.Index(errstr, "{") - end := strings.Index(errstr, "}") - if start == -1 || end < start { - // Error message without a keyword. Assume it's an internal server error - return http.StatusInternalServerError - } - errKeyword := errstr[start : end+1] - if status, present := errToHttpStatusMap[errKeyword]; present { - return status - } - return http.StatusInternalServerError -} - -// ErrorClean removes all error keywords from an error string -func ErrorClean(err error) error { - errstr := fmt.Sprintf("%s", err) - for { - left := strings.SplitN(errstr, "{", 2) - if len(left) == 1 { - break - } - errstr = left[0] - b := strings.SplitN(left[1], "}", 2) - if len(b) > 1 { - errstr += strings.TrimPrefix(b[1], " ") - } - } - return fmt.Errorf("%s", errstr) -} - -// ErrorString safely returns a string from any error, returning "" for nil -func ErrorString(err error) string { - if err == nil { - return "" - } - return fmt.Sprintf("%s", err) -} - -// ErrorJSON returns a JSON object with nothing but an error code, and with an optional message -func ErrorJSON(message string, err error) (rspJSON []byte) { - if message == "" { - rspJSON = []byte(fmt.Sprintf("{\"err\":\"%q\"}", err)) - } else if err == nil { - rspJSON = []byte(fmt.Sprintf("{\"err\":\"%q\"}", message)) - } else { - rspJSON = []byte(fmt.Sprintf("{\"err\":\"%q: %q\"}", message, err)) - } - return -} diff --git a/note-go/note/event.go b/note-go/note/event.go deleted file mode 100644 index 96c1c48..0000000 --- a/note-go/note/event.go +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -import ( - "time" -) - -// EventAdd (golint) -const EventAdd = "note.add" - -// EventUpdate (golint) -const EventUpdate = "note.update" - -// EventDelete (golint) -const EventDelete = "note.delete" - -// EventTest (golint) -const EventTest = "test" - -// EventPost (golint) -const EventPost = "post" - -// EventPut (golint) -const EventPut = "put" - -// EventGet (golint) -const EventGet = "get" - -// EventNoAction (golint) -const EventNoAction = "" - -// EventSessionBegin (golint) -const EventSessionBegin = "session.begin" - -// EventSessionEndNotehub (golint) -const EventSessionEnd = "session.end" - -// EventGeolocation (golint) -const EventGeolocation = "device.geolocation" - -// EventTower (golint) -const EventTower = "device.tower" - -// EventSocket (golint) -const EventSocket = "web.socket" - -// EventWebhook (golint) -const EventWebhook = "webhook" - -// Event is the request structure passed to the Notification proc -// -// NOTE: This structure's underlying storage has been decoupled from the use of -// the structure in business logic. As such, please share any changes to these -// structures with cloud services to ensure that storage and testing frameworks -// are kept in sync with these structures used for business logic -type Event struct { - EventUID string `json:"event,omitempty"` - // Indicates whether or not this event is a "platform event" - that is, an event generated automatically - // somewhere in the notecard or notehub largely for administrative purposes that doesn't pertain to either - // implicit or explicit user data. - Platform bool `json:"platform,omitempty"` - // These fields, and only these fields, are regarded as "user data". All - // the rest of the fields are regarded as "metadata". - When int64 `json:"when,omitempty"` - NotefileID string `json:"file,omitempty"` - NoteID string `json:"note,omitempty"` - Body *map[string]interface{} `json:"body,omitempty"` - Payload []byte `json:"payload,omitempty"` - Details *map[string]interface{} `json:"details,omitempty"` - // Metadata - SessionUID string `json:"session,omitempty"` - SessionBegan int64 `json:"session_began,omitempty"` - TLS bool `json:"tls,omitempty"` - Transport string `json:"transport,omitempty"` - Continuous bool `json:"continuous,omitempty"` - BestID string `json:"best_id,omitempty"` - DeviceUID string `json:"device,omitempty"` - DeviceSN string `json:"sn,omitempty"` - ProductUID string `json:"product,omitempty"` - AppUID string `json:"app,omitempty"` - Received float64 `json:"received,omitempty"` - Req string `json:"req,omitempty"` - Error string `json:"err,omitempty"` - Updates int32 `json:"updates,omitempty"` - Deleted bool `json:"deleted,omitempty"` - Sent bool `json:"queued,omitempty"` - Bulk bool `json:"bulk,omitempty"` - BulkReceived float64 `json:"batch_received,omitempty"` - BulkNumber uint32 `json:"batch_number,omitempty"` - BulkTotal uint32 `json:"batch_total,omitempty"` - FirmwareHost string `json:"firmware_host,omitempty"` - FirmwareNotecard string `json:"firmware_notecard,omitempty"` - // This field is ONLY used when we remove the payload for storage reasons, to show the app how large it was - MissingPayloadLength int64 `json:"payload_length,omitempty"` - // Location - BestLocationType string `json:"best_location_type,omitempty"` - BestLocationWhen int64 `json:"best_location_when,omitempty"` - BestLat float64 `json:"best_lat,omitempty"` - BestLon float64 `json:"best_lon,omitempty"` - BestLocation string `json:"best_location,omitempty"` - BestCountry string `json:"best_country,omitempty"` - BestTimeZone string `json:"best_timezone,omitempty"` - Where string `json:"where_olc,omitempty"` - WhereWhen int64 `json:"where_when,omitempty"` - WhereLat float64 `json:"where_lat,omitempty"` - WhereLon float64 `json:"where_lon,omitempty"` - WhereLocation string `json:"where_location,omitempty"` - WhereCountry string `json:"where_country,omitempty"` - WhereTimeZone string `json:"where_timezone,omitempty"` - TowerWhen int64 `json:"tower_when,omitempty"` - TowerLat float64 `json:"tower_lat,omitempty"` - TowerLon float64 `json:"tower_lon,omitempty"` - TowerCountry string `json:"tower_country,omitempty"` - TowerLocation string `json:"tower_location,omitempty"` - TowerTimeZone string `json:"tower_timezone,omitempty"` - TowerID string `json:"tower_id,omitempty"` - TriWhen int64 `json:"tri_when,omitempty"` - TriLat float64 `json:"tri_lat,omitempty"` - TriLon float64 `json:"tri_lon,omitempty"` - TriLocation string `json:"tri_location,omitempty"` - TriCountry string `json:"tri_country,omitempty"` - TriTimeZone string `json:"tri_timezone,omitempty"` - TriPoints int32 `json:"tri_points,omitempty"` - - // Triangulation - Triangulate *map[string]interface{} `json:"triangulate,omitempty"` - // "Routed" environment variables beginning with a "$" prefix - Env *map[string]string `json:"environment,omitempty"` - Status EventRoutingStatus `json:"status,omitempty"` - FleetUIDs *[]string `json:"fleets,omitempty"` - - // ONLY POPULATED FOR EventSessionBegin with info both from notecard and notehub - DeviceSKU string `json:"sku,omitempty"` - DeviceOrderingCode string `json:"ordering_code,omitempty"` - DeviceFirmware int64 `json:"firmware,omitempty"` - Bearer string `json:"bearer,omitempty"` - CellID string `json:"cellid,omitempty"` - Bssid string `json:"bssid,omitempty"` - Ssid string `json:"ssid,omitempty"` - Iccid string `json:"iccid,omitempty"` - Apn string `json:"apn,omitempty"` - Rssi int `json:"rssi,omitempty"` - Sinr int `json:"sinr,omitempty"` - Rsrp int `json:"rsrp,omitempty"` - Rsrq int `json:"rsrq,omitempty"` - Rat string `json:"rat,omitempty"` - Bars uint32 `json:"bars,omitempty"` - Voltage float64 `json:"voltage,omitempty"` - Temp float64 `json:"temp,omitempty"` - Moved int64 `json:"moved,omitempty"` - Orientation string `json:"orientation,omitempty"` - PowerCharging bool `json:"power_charging,omitempty"` - PowerUsb bool `json:"power_usb,omitempty"` - PowerPrimary bool `json:"power_primary,omitempty"` - PowerMahUsed float64 `json:"power_mah,omitempty"` - - // ONLY POPULATED FOR EventSessionEnd because it comes from the notehub - NotehubLastWorkDone int64 `json:"hub_last_work_done,omitempty"` - NotehubDurationSecs int64 `json:"hub_duration_secs,omitempty"` - NotehubEventCount int64 `json:"hub_events_routed,omitempty"` - NotehubRcvdBytes uint32 `json:"hub_rcvd_bytes,omitempty"` - NotehubSentBytes uint32 `json:"hub_sent_bytes,omitempty"` - NotehubTCPSessions uint32 `json:"hub_tcp_sessions,omitempty"` - NotehubTLSSessions uint32 `json:"hub_tls_sessions,omitempty"` - NotehubRcvdNotes uint32 `json:"hub_rcvd_notes,omitempty"` - NotehubSentNotes uint32 `json:"hub_sent_notes,omitempty"` - - // ONLY POPULATED for EventSessionEndNotecard because it comes from the notecard - NotecardRcvdBytes uint32 `json:"card_rcvd_bytes,omitempty"` - NotecardSentBytes uint32 `json:"card_sent_bytes,omitempty"` - NotecardRcvdBytesSecondary uint32 `json:"card_rcvd_bytes_secondary,omitempty"` - NotecardSentBytesSecondary uint32 `json:"card_sent_bytes_secondary,omitempty"` - NotecardTCPSessions uint32 `json:"card_tcp_sessions,omitempty"` - NotecardTLSSessions uint32 `json:"card_tls_sessions,omitempty"` - NotecardRcvdNotes uint32 `json:"card_rcvd_notes,omitempty"` - NotecardSentNotes uint32 `json:"card_sent_notes,omitempty"` -} - -type EventRoutingStatus string - -const ( - EventStatusEmpty EventRoutingStatus = "" - EventStatusSuccess EventRoutingStatus = "success" - EventStatusFailure EventRoutingStatus = "failure" - EventStatusInProgress EventRoutingStatus = "in_progress" -) - -// RouteLogEntry is the log entry used by notification processing -type RouteLogEntry struct { - EventSerial int64 `json:"event,omitempty"` - RouteSerial int64 `json:"route,omitempty"` - Date time.Time `json:"date,omitempty"` - Attn bool `json:"attn,omitempty"` - Status string `json:"status,omitempty"` - Text string `json:"text,omitempty"` - URL string `json:"url,omitempty"` - Source RoutingSource `json:"source,omitempty"` - - // Time in milliseconds that the route took to process - // We're making a simplifying assumption that the route will always - // take at least 1ms. So 0 means we didn't record the duration. - Duration int64 `json:"duration,omitempty"` -} - -type RoutingSource uint8 - -const ( - RoutingSourceUnknown RoutingSource = iota - RoutingSourceNormal - RoutingSourceProxy - RoutingSourceRetry - RoutingSourceManual - RoutingSourceDirect - RoutingSourceTest -) - -// String returns a string representation of the routing source -func (s RoutingSource) String() string { - switch s { - case RoutingSourceUnknown: - return "" // display nothing if no entry/default - case RoutingSourceNormal: - return "Normal Routing" - case RoutingSourceProxy: - return "Web Proxy Request" - case RoutingSourceRetry: - return "Auto-Retry" - case RoutingSourceManual: - return "Manual Reroute" - case RoutingSourceDirect: - return "Direct Routing" //only used for test events, should never show in route logs - case RoutingSourceTest: - return "Test" // only used for tests - default: - return "invalid" - } -} - -// GetAggregateEventStatus returns the status of the event given all -// of the route logs for the event. -// -// The aggregate status is determined by taking the most recent status -// for each route. If any of these are failures then the overall status -// is EventStatusFailure, otherwise it's EventStatusSuccess -func GetAggregateEventStatus(logs []RouteLogEntry) EventRoutingStatus { - if len(logs) == 0 { - return EventStatusEmpty - } - - latest := make(map[int64]RouteLogEntry) - for _, log := range logs { - if val, ok := latest[log.RouteSerial]; !ok || log.Date.After(val.Date) { - latest[log.RouteSerial] = log - } - } - - for _, latestLogEntry := range latest { - if latestLogEntry.Attn { - return EventStatusFailure - } - } - - return EventStatusSuccess -} diff --git a/note-go/note/message.go b/note-go/note/message.go deleted file mode 100644 index 70e3aaa..0000000 --- a/note-go/note/message.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -// MessageAddress is the network routing information for a message -type MessageAddress struct { - Hub string `json:"hub,omitempty"` - ProductUID string `json:"product,omitempty"` - DeviceUID string `json:"device,omitempty"` - DeviceSN string `json:"sn,omitempty"` - Active uint32 `json:"active,omitempty"` -} - -// MessageContact is the entity sending a message, who may have multiple devices/addresses -type MessageContact struct { - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - StoreTags []string `json:"stags,omitempty"` - Addresses []MessageAddress `json:"addresses,omitempty"` -} - -// Message is the core message data structure. Note that when stored in a map or a note, -// the UID is not present but rather is the map key or noteID. -type Message struct { - UID string `json:"id,omitempty"` - Sent uint32 `json:"sent,omitempty"` - Received uint32 `json:"received,omitempty"` - From MessageContact `json:"from,omitempty"` - To []MessageContact `json:"to,omitempty"` - Tags []string `json:"tags,omitempty"` - StoreTags []string `json:"stags,omitempty"` - ContentType string `json:"type,omitempty"` - Content string `json:"content,omitempty"` - Body *map[string]interface{} `json:"body,omitempty"` -} - -// MessageOutbox is the place from which messages are sent -const MessageOutbox = "messages.qo" - -// MessageInbox is the place into which messages are received -const MessageInbox = "messages.qi" - -// MessageStore is the place where the user retains messages -const MessageStore = "messages.db" - -// ContactStore is the place where the user retains contact info -const ContactStore = "contacts.db" - -// MessageContentASCII is just simple ASCII text -const MessageContentASCII = "" - -// MessageTagImportant indicates that the sender feels that this is an important message -const MessageTagImportant = "important" - -// MessageTagUrgent indicates that the sender feels that this is an urgent message -const MessageTagUrgent = "urgent" - -// MessageSTagSent indicates that this was a sent message -const MessageSTagSent = "sent" - -// MessageSTagReceived indicates that this was a received message -const MessageSTagReceived = "received" - -// ContactOwnerNoteID indicates that this is my contact -const ContactOwnerNoteID = "owner" diff --git a/note-go/note/note.go b/note-go/note/note.go deleted file mode 100644 index 6e7689f..0000000 --- a/note-go/note/note.go +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -import ( - "bytes" - "encoding/json" - "math" - "strings" - "time" -) - -// DefaultDeviceEndpointID is the default endpoint name of the edge, chosen for its length in protocol messages -const DefaultDeviceEndpointID = "" - -// DefaultHubEndpointID is the default endpoint name of the hub, chosen for its length in protocol messages -const DefaultHubEndpointID = "1" - -// HubDefaultInboundNotefile is the hard-wired default notefile for user data -const HubDefaultInboundNotefile = "data.qi" - -// HubDefaultOutboundNotefile is the hard-wired default notefile for user data -const HubDefaultOutboundNotefile = "data.qo" - -// Note is the most fundamental data structure, containing -// user data referred to as its "body" and its "payload". All -// access to these fields, and changes to these fields, must -// be done indirectly through the note API. -type Note struct { - Body map[string]interface{} `json:"b,omitempty"` - Payload []byte `json:"p,omitempty"` - Change int64 `json:"c,omitempty"` - Histories *[]History `json:"h,omitempty"` - Conflicts *[]Note `json:"x,omitempty"` - Updates int32 `json:"u,omitempty"` - Deleted bool `json:"d,omitempty"` - Sent bool `json:"s,omitempty"` - Bulk bool `json:"k,omitempty"` - XPOff uint32 `json:"O,omitempty"` - XPLen uint32 `json:"L,omitempty"` - Tower *TowerLocation `json:"T,omitempty"` -} - -// History records the update history, optimized so that if the most recent entry -// is by the same endpoint as an update/delete, that entry is re-used. The primary use -// of History is for conflict detection, and you don't need to detect conflicts -// against yourself. -type History struct { - When int64 `json:"w,omitempty"` - Where string `json:"l,omitempty"` - WhereWhen int64 `json:"m,omitempty"` - EndpointID string `json:"e,omitempty"` - Sequence int32 `json:"s,omitempty"` -} - -// Info is a general "content" structure -type Info struct { - NoteID string `json:"id,omitempty"` - When int64 `json:"time,omitempty"` - WhereLat float64 `json:"lat,omitempty"` - WhereLon float64 `json:"lon,omitempty"` - WhereWhen int64 `json:"ltime,omitempty"` - Body *map[string]interface{} `json:"body,omitempty"` - Payload *[]byte `json:"payload,omitempty"` - Deleted bool `json:"deleted,omitempty"` - Edge bool `json:"edge,omitempty"` - Pending bool `json:"pending,omitempty"` -} - -// CreateNote creates the core data structure for an object, given a JSON body -func CreateNote(body []byte, payload []byte) (newNote Note, err error) { - newNote.Payload = payload - err = newNote.SetBody(body) - return -} - -// SetBody sets the application-supplied Body field of a given Note given some JSON -func (note *Note) SetBody(body []byte) (err error) { - if len(body) == 0 { - note.Body = nil - } else { - note.Body = map[string]interface{}{} - err = JSONUnmarshal(body, ¬e.Body) - if err != nil { - return - } - } - return -} - -// JSONToBody unmarshals the specified object and returns it as a map[string]interface{} -func JSONToBody(bodyJSON []byte) (body map[string]interface{}, err error) { - err = JSONUnmarshal(bodyJSON, &body) - return -} - -// ObjectToJSON Marshals the specified object and returns it as a []byte -func ObjectToJSON(object interface{}) (bodyJSON []byte, err error) { - bodyJSON, err = JSONMarshal(object) - return -} - -// ObjectToBody Marshals the specified object and returns it as map -func ObjectToBody(object interface{}) (body map[string]interface{}, err error) { - var bodyJSON []byte - bodyJSON, err = JSONMarshal(object) - if err == nil { - err = JSONUnmarshal(bodyJSON, &body) - } - return -} - -// BodyToObject Unmarshals the specified map into an object -func BodyToObject(body *map[string]interface{}, object interface{}) (err error) { - if body == nil { - return - } - var bodyJSON []byte - bodyJSON, err = JSONMarshal(body) - if err == nil { - err = JSONUnmarshal(bodyJSON, object) - } - return -} - -// SetPayload sets the application-supplied Payload field of a given Note, -// which must be binary bytes that will ultimately be rendered as base64 in JSON -func (note *Note) SetPayload(payload []byte) { - note.Payload = payload -} - -// Close closes and frees the object on a note { -func (note *Note) Close() { -} - -// Dup duplicates the note -func (note *Note) Dup() Note { - newNote := *note - return newNote -} - -// GetBody retrieves the application-specific Body of a given Note -func (note *Note) GetBody() []byte { - if note.Body == nil { - return []byte("{}") - } - data, err := JSONMarshal(note.Body) - if err != nil { - return []byte("{}") - } - return data -} - -// GetPayload retrieves the Payload from a given Note -func (note *Note) GetPayload() []byte { - return note.Payload -} - -// EndpointID determines the endpoint that last modified the note -func (note *Note) EndpointID() string { - if note.Histories == nil { - return "" - } - histories := *note.Histories - if len(histories) == 0 { - return "" - } - return histories[0].EndpointID -} - -// HasConflicts determines whether or not a given Note has conflicts -func (note *Note) HasConflicts() bool { - if note.Conflicts == nil { - return false - } - return len(*note.Conflicts) != 0 -} - -// GetConflicts fetches the conflicts, so that they may be displayed -func (note *Note) GetConflicts() []Note { - if note.Conflicts == nil { - return []Note{} - } - return *note.Conflicts -} - -// GetWhen retrieves the epoch modification time -func (note *Note) When() (when int64) { - if note.Histories == nil || len(*note.Histories) == 0 { - return 0 - } - h := (*note.Histories)[0] - if h.When < 1483228800 || h.When > math.MaxUint32 { - // Before 1/1/2017 or can't fit into a uint32 - h.When = 0 - } - return h.When -} - -// GetEndpointID retrieves the endpoint that last modified the note -func (note *Note) GetEndpointID() (endpointID string) { - if note.Histories == nil || len(*note.Histories) == 0 { - return "" - } - h := (*note.Histories)[0] - return h.EndpointID -} - -// GetModified retrieves information about the note's modification -func (note *Note) GetModified() (isAvailable bool, endpointID string, when string, where string, updates int32) { - if note.Histories == nil || len(*note.Histories) == 0 { - return - } - histories := *note.Histories - endpointID = histories[0].EndpointID - when = time.Unix(0, histories[0].When*1000000000).UTC().Format("2006-01-02T15:04:05Z") - where = histories[0].Where - updates = histories[0].Sequence - isAvailable = true - return -} - -// JSONUnmarshal uses JSON Numbers, rather than assuming Floats. This fixes an issue -// in which, when decoding to an arbitrary interface, the JSON package decodes -// large numbers (like Unix epoch) into floats. -func JSONUnmarshal(data []byte, v interface{}) (err error) { - d := json.NewDecoder(strings.NewReader(string(data))) - d.UseNumber() - return d.Decode(v) -} - -// JSONMarshal is the equivalent to the json package's Marshal, however it does not escape HTML -// sitting inside JSON strings. -func JSONMarshal(v interface{}) ([]byte, error) { - buffer := &bytes.Buffer{} - encoder := json.NewEncoder(buffer) - encoder.SetEscapeHTML(false) - err := encoder.Encode(v) - clean := bytes.TrimSuffix(buffer.Bytes(), []byte("\n")) - return clean, err -} - -// JSONMarshalIndent is like Marshal but applies Indent to format the output. -// Each JSON element in the output will begin on a new line beginning with prefix -// followed by one or more copies of indent according to the indentation nesting. -func JSONMarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { - b, err := JSONMarshal(v) - if err != nil { - return nil, err - } - var buf bytes.Buffer - err = json.Indent(&buf, b, prefix, indent) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} diff --git a/note-go/note/notefile.go b/note-go/note/notefile.go deleted file mode 100644 index 536da93..0000000 --- a/note-go/note/notefile.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -// TrackNotefile is the hard-wired notefile that the notecard can use for tracking the device -const TrackNotefile = "_track.qo" - -// NotecardRequestNotefile is a special notefile for sending notecard requests -const NotecardRequestNotefile = "_req.qis" - -// NotecardResponseNotefile is a special notefile for sending notecard responses -const NotecardResponseNotefile = "_rsp.qos" - -// LogNotefile is the hard-wired notefile that the notecard uses for debug logging -const LogNotefile = "_log.qo" - -// EnvNotefile is the hard-wired notefile that the notecard uses for env vars -const EnvNotefile = "_env.dbs" - -// SessionNotefile is the hard-wired notefile that the notehub uses when starting a session -const SessionNotefile = "_session.qo" - -// HealthNotefile is the hard-wired notefile that the notecard uses for health-related info -const HealthNotefile = "_health.qo" - -// HealthHostNotefile is the hard-wired notefile that the host uses for health-related info -const HealthHostNotefile = "_health_host.qo" - -// GeolocationNotefile is the hard-wired notefile that the notehub uses when performing a geolocation -const GeolocationNotefile = "_geolocate.qo" - -// TowerNotefile is the hard-wired notefile that the notehub uses when performing tower updates -const TowerNotefile = "_tower.qo" - -// SocketNotefile is the hard-wired notefile that the notehub uses when doing websocket I/O -const SocketNotefile = "_socket.qo" - -// WebNotefile is the hard-wired notefile that the notehub uses when performing web requests -const WebNotefile = "_web.qo" - -// WatchdogNotefile is the hard-wired notefile that the notehub uses when adding watchdog messages -const WatchdogNotefile = "_watchdog.qo" - -// SyncPriorityLowest (golint) -const SyncPriorityLowest = -3 - -// SyncPriorityLower (golint) -const SyncPriorityLower = -2 - -// SyncPriorityLow (golint) -const SyncPriorityLow = -1 - -// SyncPriorityNormal (golint) -const SyncPriorityNormal = 0 - -// SyncPriorityHigh (golint) -const SyncPriorityHigh = 1 - -// SyncPriorityHigher (golint) -const SyncPriorityHigher = 2 - -// SyncPriorityHighest (golint) -const SyncPriorityHighest = 3 - -// NotefileInfo has parameters about the Notefile -type NotefileInfo struct { - // The count of modified notes in this notefile. This is used in the Req API, but not in the Notebox info - Changes int `json:"changes,omitempty"` - // The count of total notes in this notefile. This is used in the Req API, but not in the Notebox info - Total int `json:"total,omitempty"` - // This is a unidirectional "to-hub" or "from-hub" endpoint - SyncHubEndpointID string `json:"sync_hub_endpoint,omitempty"` - // Relative positive/negative priority of data, with 0 being normal - SyncPriority int `json:"sync_priority,omitempty"` - // Timed: Target for sync period, if modified and if the value hasn't been synced sooner - SyncPeriodSecs int `json:"sync_secs,omitempty"` - // ReqTime is specified if notes stored in this notefile must have a valid time associated with them - ReqTime bool `json:"req_time,omitempty"` - // ReqLoc is specified if notes stored in this notefile must have a valid location associated with them - ReqLoc bool `json:"req_loc,omitempty"` - // AnonAddAllowed is specified if anyone is allowed to drop into this notefile without authentication - AnonAddAllowed bool `json:"anon_add,omitempty"` - // ImportTime is the epoch time of when an external data source (such as a feed) last sync'ed data inbound - ImportTime int64 `json:"import_time,omitempty"` - // ExportTime is the epoch time of when an external data source (such as a feed) last sync'ed data outbound - ExportTime int64 `json:"export_time,omitempty"` -} - -// Information about notefiles and their templates -type NotefileDesc struct { - NotefileID string `json:"file,omitempty"` - Info NotefileInfo `json:"info,omitempty"` - BodyTemplate string `json:"body_template,omitempty"` - PayloadTemplate uint32 `json:"payload_template,omitempty"` - TemplateFormat uint32 `json:"template_format,omitempty"` - TemplatePort uint16 `json:"template_port,omitempty"` -} diff --git a/note-go/note/session.go b/note-go/note/session.go deleted file mode 100644 index ded8c58..0000000 --- a/note-go/note/session.go +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -// DeviceSession is the basic unit of recorded device usage history -type DeviceSession struct { - // Session ID that can be mapped to the events created during that session - SessionUID string `json:"session,omitempty"` - // When the session was initially opened - SessionBegan int64 `json:"session_began,omitempty"` - // When a persistent session was last updated - SessionUpdated int64 `json:"session_updated,omitempty"` - // Why a session was opened - WhySessionOpened string `json:"why_session_opened,omitempty"` - // When the session was initially opened - SessionEnded int64 `json:"session_ended,omitempty"` - // Why the session was closed - WhySessionClosed string `json:"why_session_closed,omitempty"` - // Log key for this session - SessionLogKey string `json:"session_log_key,omitempty"` - // Info from the device structure - DeviceUID string `json:"device,omitempty"` - DeviceSN string `json:"sn,omitempty"` - ProductUID string `json:"product,omitempty"` - FleetUIDs []string `json:"fleets,omitempty"` - // Protocol:IP:port address of the handler serving the session - Handler string `json:"handler,omitempty"` - // Cell ID where the session originated and quality ("mcc,mnc,lac,cellid") - CellID string `json:"cell,omitempty"` - // Elevation of cell tower if known - Elevation float64 `json:"elevation,omitempty"` - // Parameters passed by device as a result of scanning towers/APs - ScanResults *[]byte `json:"scan,omitempty"` - Triangulate *map[string]interface{} `json:"triangulate,omitempty"` - // Network connection information sent by the notecard - Rssi int `json:"rssi,omitempty"` - Sinr int `json:"sinr,omitempty"` - Rsrp int `json:"rsrp,omitempty"` - Rsrq int `json:"rsrq,omitempty"` - Bars int `json:"bars,omitempty"` - Rat string `json:"rat,omitempty"` - Bearer string `json:"bearer,omitempty"` - Ip string `json:"ip,omitempty"` - Bssid string `json:"bssid,omitempty"` - Ssid string `json:"ssid,omitempty"` - Iccid string `json:"iccid,omitempty"` - Apn string `json:"apn,omitempty"` - // Composed by wire.go for use in Request.Transport && Event.Transport - Transport string `json:"transport,omitempty"` - // Last known tower and triangulated location as determined at the start of session - Tower TowerLocation `json:"tower,omitempty"` - Tri TowerLocation `json:"tri,omitempty"` - // Last known capture time of a note routed through this session - When int64 `json:"when,omitempty"` - // Last known GPS location of a note routed through this session - WhereWhen int64 `json:"where_when,omitempty"` - WhereOLC string `json:"where,omitempty"` - WhereLat float64 `json:"where_lat,omitempty"` - WhereLon float64 `json:"where_lon,omitempty"` - WhereLocation string `json:"where_location,omitempty"` - WhereCountry string `json:"where_country,omitempty"` - WhereTimeZone string `json:"where_timezone,omitempty"` - // Flag indicating whether the usage data is based on actual stats from the device - IsUsageActual bool `json:"usage_actual,omitempty"` - // Physical device info - Voltage float64 `json:"voltage,omitempty"` - Temp float64 `json:"temp,omitempty"` - // Type of session - ContinuousSession bool `json:"continuous,omitempty"` - TLSSession bool `json:"tls,omitempty"` - // For keeping track of when the last work was done for a session - LastWorkDone int64 `json:"work,omitempty"` - // Number of Events routed - EventCount int64 `json:"events,omitempty"` - // Motion of the notecard - Moved int64 `json:"moved,omitempty"` - Orientation string `json:"orientation,omitempty"` - // Last known power stats at start of session - HighPowerSecsTotal uint32 `json:"hp_secs_total,omitempty"` - HighPowerSecsData uint32 `json:"hp_secs_data,omitempty"` - HighPowerSecsGPS uint32 `json:"hp_secs_gps,omitempty"` - HighPowerCyclesTotal uint32 `json:"hp_cycles_total,omitempty"` - HighPowerCyclesData uint32 `json:"hp_cycles_data,omitempty"` - HighPowerCyclesGPS uint32 `json:"hp_cycles_gps,omitempty"` - // Amount of packet usage within the session, keyed by PSID - PacketUsage map[string]PacketUsage `json:"packet_usage,omitempty"` - // Total device usage at the beginning of the period - ThisPtr *DeviceUsage `json:"this,omitempty"` - // Total device usage at the beginning of the next period, whenever it happens to occur - NextPtr *DeviceUsage `json:"next,omitempty"` - // Usage during the period - initially estimated, but then corrected when we get to the next period - PeriodPtr *DeviceUsage `json:"period,omitempty"` - // NotecardPowerSource flags - PowerCharging bool `json:"power_charging,omitempty"` - PowerUsb bool `json:"power_usb,omitempty"` - PowerPrimary bool `json:"power_primary,omitempty"` - // Mojo power usage - PowerMahUsed float64 `json:"power_mah,omitempty"` - // Information about failed connections PRIOR to this one - PenaltySecs uint32 `json:"penalty_secs,omitempty"` - FailedConnects uint32 `json:"failed_connects,omitempty"` - // Socket-relate - SocketAlias string `json:"socket_alias,omitempty"` - SocketConnectError string `json:"socket_connect_error,omitempty"` - SocketBytesSent int64 `json:"socket_bytes_sent,omitempty"` - SocketBytesRcvd int64 `json:"socket_bytes_rcvd,omitempty"` -} - -func (s *DeviceSession) This() *DeviceUsage { - if s.ThisPtr == nil { - s.ThisPtr = &DeviceUsage{} - } - return s.ThisPtr -} - -func (s *DeviceSession) Next() *DeviceUsage { - if s.NextPtr == nil { - s.NextPtr = &DeviceUsage{} - } - return s.NextPtr -} - -func (s *DeviceSession) Period() *DeviceUsage { - if s.PeriodPtr == nil { - s.PeriodPtr = &DeviceUsage{} - } - return s.PeriodPtr -} - -// Indication of the packet usage within a session -type PacketUsage struct { - Updated int64 `json:"updated,omitempty"` - DownlinkPackets int64 `json:"dl_p,omitempty"` - DownlinkBytes int64 `json:"dl_b,omitempty"` - DownlinkBytesBillable int64 `json:"dl_bb,omitempty"` - UplinkPackets int64 `json:"ul_p,omitempty"` - UplinkBytes int64 `json:"ul_b,omitempty"` - UplinkBytesBillable int64 `json:"ul_bb,omitempty"` - BillableMinBytesPerPacket int64 `json:"bmbpp,omitempty"` -} - -// TowerLocation is a location structure generated by a lookup -type TowerLocation struct { - Source string `json:"source,omitempty"` // source of this location - When int64 `json:"time,omitempty"` // time when this location was ascertained - Name string `json:"n,omitempty"` // name of the location - CountryCode string `json:"c,omitempty"` // country code - Lat float64 `json:"lat,omitempty"` // latitude - Lon float64 `json:"lon,omitempty"` // longitude - TimeZone string `json:"zone,omitempty"` // timezone name - MCC int `json:"mcc,omitempty"` - MNC int `json:"mnc,omitempty"` - LAC int `json:"lac,omitempty"` - CID int `json:"cid,omitempty"` - OLC string `json:"l,omitempty"` // open location code - TimeZoneID int `json:"z,omitempty"` // timezone id (see tz.go) - Deprecated int64 `json:"count,omitempty"` // (no longer used or supported) - Towers int `json:"towers,omitempty"` // number of triangulation points -} diff --git a/note-go/note/usage.go b/note-go/note/usage.go deleted file mode 100644 index 8c2ff82..0000000 --- a/note-go/note/usage.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -// DeviceUsage is the device usage metric representing values from the beginning of time, since Provisioned -type DeviceUsage struct { - Since int64 `json:"since,omitempty"` - DurationSecs uint32 `json:"duration,omitempty"` - RcvdBytes uint32 `json:"bytes_rcvd,omitempty"` - SentBytes uint32 `json:"bytes_sent,omitempty"` - RcvdBytesSecondary uint32 `json:"bytes_rcvd_secondary,omitempty"` - SentBytesSecondary uint32 `json:"bytes_sent_secondary,omitempty"` - TCPSessions uint32 `json:"sessions_tcp,omitempty"` - TLSSessions uint32 `json:"sessions_tls,omitempty"` - PacketSessions uint32 `json:"sessions_packet,omitempty"` - WebhookSessions uint32 `json:"sessions_webhook,omitempty"` - RcvdNotes uint32 `json:"notes_rcvd,omitempty"` - SentNotes uint32 `json:"notes_sent,omitempty"` -} diff --git a/note-go/note/words.go b/note-go/note/words.go deleted file mode 100644 index 5747e96..0000000 --- a/note-go/note/words.go +++ /dev/null @@ -1,2196 +0,0 @@ -// Copyright 2020 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package note - -import ( - "hash/fnv" - "sort" - "strconv" - "strings" - "sync" -) - -// Word index data structure -type Word struct { - WordIndex uint -} - -var ( - sortedWords []Word - sortedWordsInitialized = false - sortedWordsInitLock sync.RWMutex -) - -// Class used to sort an index of words -type byWord []Word - -func (a byWord) Len() int { return len(a) } -func (a byWord) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a byWord) Less(i, j int) bool { return words2048[a[i].WordIndex] < words2048[a[j].WordIndex] } - -// WordToNumber converts a single word to a number -func WordToNumber(word string) (num uint, success bool) { - // Initialize sorted words array if necessary - if !sortedWordsInitialized { - sortedWordsInitLock.Lock() - if !sortedWordsInitialized { - - // Init the index array - sortedWords = make([]Word, 2048) - for i := 0; i < 2048; i++ { - sortedWords[i].WordIndex = uint(i) - } - - // Sort the array - sort.Sort(byWord(sortedWords)) - - // We're now initialized - sortedWordsInitialized = true - } - sortedWordsInitLock.Unlock() - } - - // First normalize the word - word = strings.ToLower(word) - - // Do a binary chop to find the word or its insertion slot - i := sort.Search(2048, func(i int) bool { return words2048[sortedWords[i].WordIndex] >= word }) - - // Exit if found. (If we failed to match the result, it's an insertion slot.) - if i < 2048 && words2048[sortedWords[i].WordIndex] == word { - return sortedWords[i].WordIndex, true - } - - return 0, false -} - -// WordsToNumber looks up a number from two or three simple words -func WordsToNumber(words string) (num uint32, found bool) { - var left, middle, right uint - var success bool - - // For convenience, if a number is supplied just return that number. I do this so - // that you can use this same method to parse either a number or the words to get that number. - word := strings.Split(words, "-") - if len(word) == 1 { - - // See if this parses cleanly as a number - i64, err := strconv.ParseUint(words, 10, 32) - if err == nil { - return uint32(i64), true - } - return 0, false - } - - // Convert two or three words to numbers, msb to lsb - if len(word) == 2 { - middle, success = WordToNumber(word[0]) - if !success { - return 0, false - } - right, success = WordToNumber(word[1]) - if !success { - return 0, false - } - } else { - left, success = WordToNumber(word[0]) - if !success { - return 0, false - } - middle, success = WordToNumber(word[1]) - if !success { - return 0, false - } - right, success = WordToNumber(word[2]) - if !success { - return 0, false - } - } - - // Map back to bit fields - result := uint32(left) << 22 - result |= uint32(middle) << 11 - result |= uint32(right) - - return result, true -} - -// WordsFromString hashes a string with a 32-bit function and converts it to three simple words -func WordsFromString(in string) (out string) { - hash := fnv.New32a() - inbytes := []byte(in) - hash.Write(inbytes) - hashval := hash.Sum32() - out = WordsFromNumber(hashval) - return -} - -// WordsFromNumber converts a number to three simple words -func WordsFromNumber(number uint32) string { - // Break the 32-bit uint down into 3 bit fields - left := (number >> 22) & 0x000003ff - middle := (number >> 11) & 0x000007ff - right := number & 0x000007ff - - // If the high order is 0, which is frequently the case, just use two words - if left == 0 { - return words2048[middle] + "-" + words2048[right] - } - return words2048[left] + "-" + words2048[middle] + "-" + words2048[right] -} - -// 2048 words, ORDERED but alphabetically unsorted -var words2048 = []string{ - "act", - "add", - "age", - "ago", - "point", - "big", - "all", - "and", - "any", - "arm", - "art", - "ash", - "ask", - "bad", - "bag", - "ban", - "bar", - "bat", - "bay", - "bed", - "bee", - "beg", - "bet", - "bid", - "air", - "bit", - "bow", - "box", - "boy", - "bug", - "bus", - "buy", - "cab", - "can", - "cap", - "car", - "cat", - "cop", - "cow", - "cry", - "cue", - "cup", - "cut", - "dad", - "day", - "die", - "dig", - "dip", - "dog", - "dot", - "dry", - "due", - "ear", - "eat", - "egg", - "ego", - "end", - "era", - "etc", - "eye", - "fan", - "far", - "fat", - "fee", - "few", - "fit", - "fix", - "fly", - "fog", - "for", - "fun", - "fur", - "gap", - "gas", - "get", - "gun", - "gut", - "guy", - "gym", - "hat", - "hay", - "her", - "hey", - "him", - "hip", - "his", - "hit", - "hot", - "how", - "hug", - "huh", - "ice", - "its", - "jar", - "jaw", - "jet", - "job", - "joy", - "key", - "kid", - "kit", - "lab", - "lap", - "law", - "leg", - "let", - "lid", - "lie", - "lip", - "log", - "lot", - "low", - "mars", - "mango", - "map", - "may", - "mix", - "mom", - "mud", - "net", - "new", - "nod", - "not", - "now", - "nut", - "oak", - "odd", - "off", - "oil", - "old", - "one", - "our", - "out", - "owe", - "own", - "pad", - "pan", - "pat", - "pay", - "pen", - "pet", - "pie", - "pig", - "pin", - "pit", - "pop", - "pot", - "put", - "rat", - "raw", - "red", - "rib", - "rid", - "rip", - "row", - "run", - "say", - "see", - "set", - "she", - "shy", - "sir", - "sit", - "six", - "ski", - "sky", - "son", - "spy", - "sum", - "sun", - "tag", - "tap", - "tax", - "tea", - "ten", - "the", - "tie", - "tip", - "toe", - "top", - "toy", - "try", - "two", - "use", - "van", - "war", - "way", - "web", - "who", - "why", - "win", - "wow", - "yes", - "yet", - "you", - "able", - "acid", - "aide", - "ally", - "also", - "amid", - "area", - "army", - "atop", - "aunt", - "auto", - "away", - "baby", - "back", - "bake", - "ball", - "band", - "bank", - "bare", - "barn", - "base", - "bath", - "beam", - "bean", - "bear", - "beat", - "beef", - "beer", - "bell", - "belt", - "bend", - "best", - "bias", - "bike", - "bill", - "bind", - "bird", - "bite", - "blue", - "boat", - "body", - "boil", - "bold", - "bolt", - "bomb", - "bond", - "bone", - "book", - "boom", - "boot", - "born", - "boss", - "both", - "bowl", - "buck", - "bulb", - "bulk", - "bull", - "burn", - "bury", - "bush", - "busy", - "cage", - "cake", - "call", - "calm", - "camp", - "card", - "care", - "cart", - "case", - "cash", - "cast", - "cave", - "cell", - "chef", - "chew", - "chin", - "chip", - "chop", - "cite", - "city", - "clay", - "clip", - "club", - "clue", - "coal", - "coat", - "code", - "coin", - "cold", - "come", - "cook", - "cool", - "cope", - "copy", - "cord", - "core", - "corn", - "cost", - "coup", - "crew", - "crop", - "cure", - "cute", - "dare", - "dark", - "data", - "date", - "dawn", - "dead", - "deal", - "dear", - "debt", - "deck", - "deem", - "deep", - "deer", - "deny", - "desk", - "diet", - "dirt", - "dish", - "dock", - "doll", - "door", - "dose", - "down", - "drag", - "draw", - "drop", - "drug", - "drum", - "duck", - "dumb", - "dump", - "dust", - "duty", - "each", - "earn", - "ease", - "east", - "easy", - "echo", - "edge", - "edit", - "else", - "even", - "ever", - "evil", - "exam", - "exit", - "face", - "fact", - "fade", - "fail", - "fair", - "fall", - "fame", - "fare", - "farm", - "fast", - "fate", - "feed", - "feel", - "file", - "fill", - "film", - "find", - "fine", - "fire", - "firm", - "fish", - "five", - "flag", - "flat", - "flee", - "flip", - "flow", - "fold", - "folk", - "food", - "foot", - "fork", - "form", - "four", - "free", - "from", - "fuel", - "full", - "fund", - "gain", - "game", - "gang", - "gate", - "gaze", - "gear", - "gene", - "gift", - "girl", - "give", - "glad", - "goal", - "goat", - "gold", - "golf", - "good", - "grab", - "gray", - "grin", - "grip", - "grow", - "half", - "hall", - "hand", - "hang", - "hard", - "harm", - "hate", - "haul", - "have", - "head", - "heal", - "hear", - "heat", - "heel", - "help", - "herb", - "here", - "hero", - "hers", - "hide", - "high", - "hike", - "hill", - "hint", - "hire", - "hold", - "home", - "hook", - "hope", - "horn", - "host", - "hour", - "huge", - "hunt", - "hurt", - "icon", - "idea", - "into", - "iron", - "item", - "jail", - "jazz", - "join", - "joke", - "jump", - "jury", - "just", - "keep", - "kick", - "kilt", - "kind", - "king", - "kiss", - "knee", - "know", - "lack", - "lake", - "lamp", - "land", - "lane", - "last", - "late", - "lawn", - "lead", - "leaf", - "lean", - "leap", - "left", - "lend", - "lens", - "less", - "life", - "lift", - "like", - "limb", - "line", - "link", - "lion", - "list", - "live", - "load", - "loan", - "lock", - "long", - "look", - "loop", - "loss", - "lost", - "lots", - "loud", - "love", - "luck", - "lung", - "mail", - "main", - "make", - "mall", - "many", - "mark", - "mask", - "mass", - "mate", - "math", - "meal", - "mean", - "meat", - "meet", - "melt", - "menu", - "mere", - "mild", - "milk", - "mill", - "mind", - "mine", - "miss", - "mode", - "mood", - "moon", - "more", - "most", - "move", - "much", - "must", - "myth", - "nail", - "name", - "near", - "neat", - "neck", - "need", - "nest", - "news", - "next", - "nice", - "nine", - "none", - "noon", - "norm", - "nose", - "note", - "odds", - "okay", - "once", - "only", - "onto", - "open", - "ours", - "oven", - "over", - "pace", - "pack", - "page", - "pain", - "pair", - "pale", - "palm", - "pant", - "park", - "part", - "pass", - "past", - "path", - "peak", - "peel", - "peer", - "pick", - "pile", - "pill", - "pine", - "pink", - "pipe", - "plan", - "play", - "plea", - "plot", - "plus", - "poem", - "poet", - "poke", - "pole", - "poll", - "pond", - "pool", - "poor", - "pork", - "port", - "pose", - "post", - "pour", - "pray", - "pull", - "pump", - "pure", - "push", - "quit", - "race", - "rack", - "rage", - "rail", - "rain", - "rank", - "rare", - "rate", - "read", - "real", - "rear", - "rely", - "rent", - "rest", - "rice", - "rich", - "ride", - "ring", - "riot", - "rise", - "risk", - "road", - "rock", - "role", - "roll", - "roof", - "room", - "root", - "rope", - "rose", - "ruin", - "rule", - "rush", - "sack", - "safe", - "sail", - "sake", - "sale", - "salt", - "same", - "sand", - "save", - "scan", - "seal", - "seat", - "seed", - "seek", - "seem", - "self", - "sell", - "send", - "sexy", - "shed", - "ship", - "shoe", - "shop", - "shot", - "show", - "shut", - "side", - "sign", - "silk", - "sing", - "sink", - "site", - "size", - "skip", - "slam", - "slip", - "slot", - "slow", - "snap", - "snow", - "soak", - "soap", - "soar", - "sock", - "sofa", - "soft", - "soil", - "sole", - "some", - "song", - "soon", - "sort", - "soul", - "soup", - "spin", - "spit", - "spot", - "star", - "stay", - "stem", - "step", - "stir", - "stop", - "such", - "suck", - "suit", - "sure", - "swim", - "tail", - "take", - "tale", - "talk", - "tall", - "tank", - "tape", - "task", - "team", - "tear", - "teen", - "tell", - "tend", - "tent", - "term", - "test", - "text", - "than", - "that", - "them", - "then", - "they", - "thin", - "this", - "thus", - "tide", - "tile", - "till", - "time", - "tiny", - "tire", - "toll", - "tone", - "tool", - "toss", - "tour", - "town", - "trap", - "tray", - "tree", - "trim", - "trip", - "tube", - "tuck", - "tune", - "turn", - "twin", - "type", - "unit", - "upon", - "urge", - "used", - "user", - "vary", - "vast", - "very", - "view", - "vote", - "wage", - "wait", - "wake", - "walk", - "wall", - "want", - "warn", - "wash", - "wave", - "weak", - "wear", - "weed", - "week", - "well", - "west", - "what", - "when", - "whip", - "whom", - "wide", - "wink", - "wild", - "will", - "wind", - "wine", - "wing", - "wipe", - "wire", - "wise", - "wish", - "with", - "wolf", - "word", - "work", - "wrap", - "yard", - "yeah", - "year", - "yell", - "your", - "zone", - "true", - "about", - "above", - "actor", - "adapt", - "added", - "admit", - "adopt", - "after", - "again", - "agent", - "agree", - "ahead", - "aisle", - "alarm", - "album", - "alien", - "alike", - "alive", - "alley", - "allow", - "alone", - "along", - "alter", - "among", - "angle", - "ankle", - "apart", - "apple", - "apply", - "arena", - "argue", - "arise", - "armed", - "array", - "arrow", - "aside", - "asset", - "avoid", - "await", - "awake", - "award", - "aware", - "basic", - "beach", - "beast", - "begin", - "being", - "belly", - "below", - "bench", - "birth", - "blare", - "blade", - "bling", - "blank", - "blast", - "blend", - "bless", - "blind", - "blink", - "block", - "blond", - "blotter", - "board", - "boast", - "bonus", - "boost", - "booth", - "brain", - "brake", - "brand", - "brave", - "bread", - "break", - "brick", - "bride", - "brief", - "bring", - "broad", - "brood", - "brush", - "buddy", - "build", - "bunch", - "burst", - "buyer", - "cabin", - "cable", - "candy", - "cargo", - "carry", - "carve", - "catch", - "cause", - "cease", - "chain", - "chair", - "chaos", - "charm", - "chart", - "chase", - "cheat", - "check", - "cheek", - "cheer", - "chest", - "chief", - "child", - "chill", - "chunk", - "claim", - "class", - "clean", - "clear", - "clerk", - "click", - "cliff", - "climb", - "cling", - "clock", - "close", - "cloth", - "cloud", - "coach", - "coast", - "color", - "couch", - "could", - "count", - "court", - "cover", - "crave", - "craft", - "crash", - "crawl", - "crater", - "creek", - "crime", - "cross", - "crowd", - "crown", - "crush", - "curve", - "cycle", - "daily", - "dance", - "death", - "debut", - "delay", - "dense", - "depth", - "diary", - "dirty", - "donor", - "doubt", - "dough", - "dozen", - "draft", - "drain", - "drama", - "dream", - "dress", - "dried", - "drift", - "drill", - "drink", - "drive", - "drown", - "drunk", - "dying", - "eager", - "early", - "earth", - "salty", - "elbow", - "elder", - "elect", - "elite", - "empty", - "enact", - "enemy", - "enjoy", - "enter", - "entry", - "equal", - "equip", - "erase", - "essay", - "event", - "every", - "exact", - "exist", - "extra", - "faint", - "faith", - "fatal", - "fault", - "favor", - "fence", - "fever", - "fewer", - "fiber", - "field", - "fifth", - "fifty", - "fight", - "final", - "first", - "fixed", - "flame", - "flash", - "fleet", - "flesh", - "float", - "flood", - "floor", - "flour", - "fluid", - "focus", - "force", - "forth", - "forty", - "forum", - "found", - "frame", - "fraud", - "fresh", - "front", - "frown", - "fruit", - "fully", - "funny", - "genre", - "ghost", - "giant", - "given", - "glass", - "globe", - "glory", - "glove", - "grace", - "grade", - "grain", - "grand", - "grant", - "grape", - "grasp", - "grass", - "gravel", - "great", - "green", - "greet", - "grief", - "gross", - "group", - "guard", - "guess", - "guest", - "guide", - "guilt", - "habit", - "happy", - "harsh", - "heart", - "heavy", - "hello", - "hence", - "honey", - "honor", - "horse", - "hotel", - "house", - "human", - "humor", - "hurry", - "ideal", - "image", - "imply", - "index", - "inner", - "input", - "irony", - "issue", - "jeans", - "joint", - "judge", - "juice", - "juror", - "kneel", - "kayak", - "knock", - "known", - "label", - "labor", - "large", - "laser", - "later", - "laugh", - "layer", - "learn", - "least", - "leave", - "legal", - "lemon", - "level", - "light", - "limit", - "liver", - "lobby", - "local", - "logic", - "loose", - "lover", - "lower", - "loyal", - "lucky", - "lunch", - "magic", - "major", - "maker", - "march", - "match", - "maybe", - "mayor", - "medal", - "media", - "merit", - "metal", - "meter", - "midst", - "might", - "minor", - "mixed", - "model", - "month", - "moral", - "motor", - "mount", - "mouse", - "mouth", - "movie", - "music", - "naked", - "olive", - "cricket", - "nerve", - "never", - "jade", - "night", - "noise", - "north", - "novel", - "nurse", - "occur", - "ocean", - "offer", - "often", - "onion", - "opera", - "orbit", - "order", - "other", - "ought", - "outer", - "owner", - "paint", - "panel", - "panic", - "paper", - "party", - "pasta", - "patch", - "pause", - "phase", - "phone", - "photo", - "piano", - "piece", - "pilot", - "pitch", - "pizza", - "place", - "plain", - "plant", - "plate", - "plead", - "aim", - "porch", - "pound", - "power", - "press", - "price", - "pride", - "prime", - "print", - "prior", - "prize", - "proof", - "proud", - "prove", - "pulse", - "punch", - "purse", - "quest", - "quick", - "quiet", - "quite", - "quote", - "radar", - "radio", - "raise", - "rally", - "ranch", - "range", - "rapid", - "ratio", - "reach", - "react", - "ready", - "realm", - "rebel", - "refer", - "relax", - "reply", - "rider", - "ridge", - "rifle", - "right", - "risky", - "rival", - "river", - "robot", - "round", - "route", - "royal", - "rumor", - "rural", - "salad", - "sales", - "sauce", - "scale", - "scare", - "scene", - "scent", - "scope", - "score", - "screw", - "seize", - "sense", - "serve", - "seven", - "shade", - "shake", - "shall", - "shame", - "shape", - "share", - "shark", - "sharp", - "sheep", - "sheer", - "sheet", - "shelf", - "shell", - "shift", - "shirt", - "shock", - "shoot", - "shore", - "short", - "shout", - "shove", - "shrug", - "sight", - "silly", - "since", - "sixth", - "skill", - "skirt", - "skull", - "slave", - "sleep", - "slice", - "slide", - "slope", - "small", - "smart", - "smell", - "smile", - "smoke", - "snake", - "sneak", - "solar", - "solid", - "solve", - "sorry", - "sound", - "south", - "space", - "spare", - "spark", - "speak", - "speed", - "spell", - "spend", - "spill", - "spine", - "spite", - "split", - "spoon", - "sport", - "spray", - "squad", - "stack", - "staff", - "stage", - "stair", - "stake", - "stand", - "stare", - "start", - "state", - "steak", - "steam", - "steel", - "steep", - "steer", - "stick", - "stiff", - "still", - "stock", - "stone", - "store", - "storm", - "story", - "stove", - "straw", - "strip", - "study", - "stuff", - "style", - "sugar", - "suite", - "sunny", - "super", - "sweat", - "sweep", - "sweet", - "swell", - "swing", - "sword", - "table", - "taste", - "teach", - "thank", - "their", - "theme", - "there", - "these", - "thick", - "thigh", - "thing", - "think", - "third", - "those", - "three", - "throw", - "thumb", - "tight", - "tired", - "title", - "today", - "tooth", - "topic", - "total", - "touch", - "tough", - "towel", - "tower", - "trace", - "track", - "trade", - "trail", - "train", - "trait", - "treat", - "trend", - "trial", - "tribe", - "trick", - "troop", - "truck", - "truly", - "trunk", - "trust", - "truth", - "tumor", - "twice", - "twist", - "uncle", - "under", - "union", - "unite", - "unity", - "until", - "upper", - "upset", - "urban", - "usual", - "valid", - "value", - "video", - "virus", - "visit", - "vital", - "vocal", - "voice", - "voter", - "wagon", - "waist", - "waste", - "watch", - "water", - "weave", - "weigh", - "weird", - "whale", - "wheat", - "wheel", - "where", - "which", - "while", - "whoop", - "whole", - "whose", - "wider", - "worm", - "works", - "world", - "worry", - "worth", - "would", - "wound", - "wrist", - "write", - "wrong", - "yield", - "young", - "yours", - "youth", - "false", - "abroad", - "absorb", - "accent", - "accept", - "access", - "accuse", - "across", - "action", - "active", - "actual", - "adjust", - "admire", - "affect", - "afford", - "agency", - "agenda", - "almost", - "always", - "amount", - "animal", - "annual", - "answer", - "anyone", - "anyway", - "appear", - "around", - "arrest", - "arrive", - "artist", - "aspect", - "assert", - "assess", - "assign", - "assist", - "assume", - "assure", - "attach", - "attack", - "attend", - "author", - "ballot", - "banana", - "banker", - "barrel", - "basket", - "battle", - "beauty", - "become", - "before", - "behalf", - "behave", - "behind", - "belief", - "belong", - "beside", - "better", - "beyond", - "bitter", - "bloody", - "border", - "borrow", - "bottle", - "bounce", - "branch", - "breath", - "breeze", - "bridge", - "bright", - "broken", - "broker", - "bronze", - "brutal", - "bubble", - "bucket", - "bullet", - "bureau", - "butter", - "button", - "camera", - "campus", - "candle", - "canvas", - "carbon", - "career", - "carpet", - "carrot", - "casino", - "casual", - "cattle", - "center", - "change", - "charge", - "cheese", - "choice", - "choose", - "circle", - "client", - "clinic", - "closed", - "closet", - "coffee", - "collar", - "combat", - "comedy", - "commit", - "comply", - "cookie", - "corner", - "cotton", - "county", - "cousin", - "create", - "credit", - "crisis", - "cruise", - "custom", - "dancer", - "danger", - "deadly", - "dealer", - "debate", - "debris", - "decade", - "deeply", - "defeat", - "defend", - "define", - "degree", - "depart", - "depend", - "depict", - "deploy", - "deputy", - "derive", - "desert", - "design", - "desire", - "detail", - "detect", - "device", - "devote", - "differ", - "dining", - "dinner", - "direct", - "divide", - "doctor", - "domain", - "donate", - "double", - "drawer", - "driver", - "during", - "easily", - "eating", - "editor", - "effect", - "effort", - "either", - "eleven", - "emerge", - "empire", - "employ", - "enable", - "endure", - "energy", - "engage", - "engine", - "enough", - "enroll", - "ensure", - "entire", - "entity", - "equity", - "escape", - "estate", - "evolve", - "exceed", - "except", - "expand", - "expect", - "expert", - "export", - "expose", - "extend", - "extent", - "fabric", - "factor", - "fairly", - "family", - "famous", - "farmer", - "faster", - "father", - "fellow", - "fierce", - "figure", - "filter", - "fishy", - "finish", - "firmly", - "fiscal", - "flavor", - "flight", - "flower", - "flying", - "follow", - "forest", - "forget", - "formal", - "format", - "former", - "foster", - "fourth", - "freely", - "freeze", - "friend", - "frozen", - "future", - "galaxy", - "garage", - "garden", - "garlic", - "gather", - "gender", - "genius", - "gifted", - "glance", - "global", - "golden", - "ground", - "growth", - "guitar", - "handle", - "happen", - "hardly", - "hazard", - "health", - "heaven", - "height", - "hidden", - "highly", - "hockey", - "honest", - "hunger", - "hungry", - "hunter", - "ignore", - "immune", - "impact", - "import", - "impose", - "income", - "indeed", - "infant", - "inform", - "injure", - "injury", - "inmate", - "insect", - "inside", - "insist", - "intact", - "intend", - "intent", - "invent", - "invest", - "invite", - "island", - "itself", - "jacket", - "jungle", - "junior", - "ladder", - "lately", - "latter", - "launch", - "lawyer", - "leader", - "league", - "legacy", - "legend", - "length", - "lesson", - "letter", - "likely", - "liquid", - "listen", - "little", - "living", - "locate", - "lovely", - "mainly", - "makeup", - "manage", - "manual", - "marble", - "margin", - "marine", - "market", - "master", - "matter", - "medium", - "member", - "memory", - "mentor", - "merely", - "method", - "middle", - "minute", - "mirror", - "mobile", - "modern", - "modest", - "modify", - "moment", - "monkey", - "mostly", - "mother", - "motion", - "motive", - "museum", - "mutter", - "mutual", - "myself", - "narrow", - "nation", - "native", - "nature", - "nearby", - "nearly", - "needle", - "nobody", - "normal", - "notice", - "notion", - "number", - "object", - "obtain", - "occupy", - "office", - "online", - "oppose", - "option", - "orange", - "origin", - "others", - "outfit", - "outlet", - "output", - "oxygen", - "palace", - "parade", - "parent", - "parish", - "partly", - "patent", - "patrol", - "patron", - "pencil", - "people", - "pepper", - "period", - "permit", - "person", - "phrase", - "pickup", - "pillow", - "planet", - "player", - "please", - "plenty", - "plunge", - "pocket", - "poetry", - "policy", - "poster", - "potato", - "powder", - "prefer", - "pretty", - "priest", - "profit", - "prompt", - "proper", - "public", - "purple", - "pursue", - "puzzle", - "rabbit", - "random", - "rarely", - "rather", - "rating", - "reader", - "really", - "reason", - "recall", - "recent", - "recipe", - "record", - "reduce", - "reform", - "refuse", - "regain", - "regard", - "regime", - "region", - "reject", - "relate", - "relief", - "remain", - "remark", - "remind", - "remote", - "remove", - "rental", - "repair", - "repeat", - "report", - "rescue", - "resign", - "resist", - "resort", - "result", - "resume", - "retail", - "retain", - "retire", - "return", - "reveal", - "review", - "reward", - "rhythm", - "ribbon", - "ritual", - "rocket", - "rubber", - "ruling", - "runner", - "safely", - "safety", - "salary", - "salmon", - "sample", - "saving", - "scared", - "scheme", - "school", - "scream", - "screen", - "script", - "search", - "season", - "second", - "secret", - "sector", - "secure", - "seldom", - "select", - "seller", - "senior", - "sensor", - "series", - "settle", - "severe", - "shadow", - "shorts", - "should", - "shrimp", - "signal", - "silent", - "silver", - "simple", - "simply", - "singer", - "single", - "sister", - "sleeve", - "slight", - "slowly", - "smooth", - "soccer", - "social", - "sodium", - "soften", - "softly", - "solely", - "source", - "speech", - "sphere", - "spirit", - "spread", - "spring", - "square", - "stable", - "stance", - "statue", - "status", - "steady", - "strain", - "streak", - "stream", - "street", - "stress", - "strict", - "strike", - "string", - "stroke", - "strong", - "studio", - "stupid", - "submit", - "subtle", - "suburb", - "sudden", - "suffer", - "summer", - "summit", - "supply", - "surely", - "survey", - "switch", - "symbol", - "system", - "tackle", - "tactic", - "talent", - "target", - "temple", - "tender", - "tennis", - "thanks", - "theory", - "thirty", - "though", - "thread", - "thrive", - "throat", - "ticket", - "timber", - "timing", - "tissue", - "toilet", - "tomato", - "tonic", - "toward", - "tragic", - "trauma", - "travel", - "treaty", - "tribal", - "tunnel", - "turkey", - "twelve", - "twenty", - "unfair", - "unfold", - "unique", - "unless", - "unlike", - "update", - "useful", - "vacuum", - "valley", - "vanish", - "vendor", - "verbal", - "versus", - "vessel", - "viewer", - "virtue", - "vision", - "visual", - "volume", - "voting", - "wander", - "warmth", - "wealth", - "weapon", - "weekly", - "weight", - "widely", - "window", - "winner", - "winter", - "wisdom", - "within", - "wonder", - "wooden", - "worker", - "writer", - "yellow", -} - -// end diff --git a/note-go/note/words_test.go b/note-go/note/words_test.go deleted file mode 100644 index 202f685..0000000 --- a/note-go/note/words_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package note - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestWordsFromString(t *testing.T) { - cases := map[string]string{ - "dev:foobar": "near-eat-read", - "dev:123456778": "farm-quiet-dumb", - "dev:qwerty": "flour-water-stock", - } - - for k, v := range cases { - require.Equal(t, v, WordsFromString(k)) - } -} diff --git a/note-go/notecard/cobs.go b/note-go/notecard/cobs.go deleted file mode 100644 index 0b203a6..0000000 --- a/note-go/notecard/cobs.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2023 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -// Decode with optional XOR -func CobsDecode(input []byte, xor byte) ([]byte, error) { - output := make([]byte, len(input)) - length := len(output) - inOffset := 0 - outOffset := inOffset - startOffset, endOffset := outOffset, inOffset+length - var code, copy uint8 = 0xFF, 0 - for ; inOffset < endOffset; copy-- { - if copy != 0 { - output[outOffset] = input[inOffset] ^ xor - outOffset, inOffset = outOffset+1, inOffset+1 - } else { - if code != 0xFF { - output[outOffset] = 0 - outOffset = outOffset + 1 - } - code = input[inOffset] ^ xor - copy, inOffset = code, inOffset+1 - if code == 0 { - break - } - } - } - return output[startOffset:outOffset], nil -} - -// Get the maximum size of the cobs-encoded buffer -func CobsEncodedLength(length int) int { - return length + (1 + (length / 254)) -} - -// Encode with optional XOR -func CobsEncode(input []byte, xor byte) ([]byte, error) { - length := len(input) - inOffset := 0 - // Allocate with +1 capacity so append(result, '\n') won't reallocate - maxLen := CobsEncodedLength(len(input)) - output := make([]byte, maxLen, maxLen+1) - outOffset := 0 - outStartOffset := outOffset - var ch, code uint8 - code = 1 - outCodeOffset := outOffset - outOffset = outOffset + 1 - for length > 0 { - ch = input[inOffset] - inOffset = inOffset + 1 - length = length - 1 - if ch != 0 { - output[outOffset] = ch ^ xor - outOffset = outOffset + 1 - code = code + 1 - } - if ch == 0 || code == 0xFF { - output[outCodeOffset] = code ^ xor - code = 1 - outCodeOffset = outOffset - outOffset = outOffset + 1 - } - } - output[outCodeOffset] = code ^ xor - return output[outStartOffset:outOffset], nil -} - -// CobsEncodeAppend encodes data and appends a delimiter in one operation. -// This avoids the reallocation that would occur with append(CobsEncode(...), delim). -func CobsEncodeAppend(input []byte, xor byte, delimiter byte) ([]byte, error) { - encoded, err := CobsEncode(input, xor) - if err != nil { - return nil, err - } - // Since CobsEncode allocates with +1 capacity, this append won't reallocate - return append(encoded, delimiter), nil -} diff --git a/note-go/notecard/cobs_test.go b/note-go/notecard/cobs_test.go deleted file mode 100644 index f085a3f..0000000 --- a/note-go/notecard/cobs_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package notecard - -import ( - "math/rand" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestCob(t *testing.T) { - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - min := 100 - max := 1000 - len := rng.Intn(max-min+1) + min - buf := make([]byte, len) - xor := byte(rng.Int()) - - _, err := rng.Read(buf) - require.NoError(t, err) - - encoded, err := CobsEncode(buf, xor) - require.NoError(t, err) - - decoded, err := CobsDecode(encoded, xor) - require.NoError(t, err) - - require.Equal(t, buf, decoded) -} - -func TestCobsEdgeCases(t *testing.T) { - tests := []struct { - name string - input []byte - xor byte - }{ - {"empty", []byte{}, 0}, - {"single zero", []byte{0}, 0}, - {"single nonzero", []byte{1}, 0}, - {"two zeros", []byte{0, 0}, 0}, - {"trailing zero", []byte{1, 2, 0}, 0}, - {"leading zero", []byte{0, 1, 2}, 0}, - {"middle zero", []byte{1, 0, 2}, 0}, - {"no zeros", []byte{1, 2, 3}, 0}, - {"all zeros 3", []byte{0, 0, 0}, 0}, - {"with xor", []byte{1, 2, 0, 3}, '\n'}, - {"254 bytes no zero", make254NonZero(), 0}, - {"255 bytes no zero", make255NonZero(), 0}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - encoded, err := CobsEncode(tt.input, tt.xor) - require.NoError(t, err, "encode failed") - - decoded, err := CobsDecode(encoded, tt.xor) - require.NoError(t, err, "decode failed") - - require.Equal(t, tt.input, decoded, "roundtrip failed: encoded=%v", encoded) - }) - } -} - -func make254NonZero() []byte { - b := make([]byte, 254) - for i := range b { - b[i] = byte(i%255) + 1 - } - return b -} - -func make255NonZero() []byte { - b := make([]byte, 255) - for i := range b { - b[i] = byte(i%255) + 1 - } - return b -} - -// TestCobsKnownValues tests encoding against known expected output values. -// These are the canonical COBS encodings per the specification. -// This catches regressions where encode/decode are broken in compatible but wrong ways. -func TestCobsKnownValues(t *testing.T) { - tests := []struct { - name string - input []byte - xor byte - expected []byte - }{ - // Standard COBS test vectors (xor=0) - { - name: "single zero", - input: []byte{0x00}, - xor: 0, - expected: []byte{0x01, 0x01}, - }, - { - name: "single nonzero", - input: []byte{0x01}, - xor: 0, - expected: []byte{0x02, 0x01}, - }, - { - name: "two zeros", - input: []byte{0x00, 0x00}, - xor: 0, - expected: []byte{0x01, 0x01, 0x01}, - }, - { - name: "three nonzero bytes", - input: []byte{0x01, 0x02, 0x03}, - xor: 0, - expected: []byte{0x04, 0x01, 0x02, 0x03}, - }, - { - name: "zero in middle", - input: []byte{0x01, 0x00, 0x02}, - xor: 0, - expected: []byte{0x02, 0x01, 0x02, 0x02}, - }, - { - name: "leading zero", - input: []byte{0x00, 0x01, 0x02}, - xor: 0, - expected: []byte{0x01, 0x03, 0x01, 0x02}, - }, - { - name: "trailing zero", - input: []byte{0x01, 0x02, 0x00}, - xor: 0, - expected: []byte{0x03, 0x01, 0x02, 0x01}, - }, - { - name: "Hello", - input: []byte{'H', 'e', 'l', 'l', 'o'}, - xor: 0, - expected: []byte{0x06, 'H', 'e', 'l', 'l', 'o'}, - }, - } - - for _, tt := range tests { - t.Run(tt.name+" encode", func(t *testing.T) { - encoded, err := CobsEncode(tt.input, tt.xor) - require.NoError(t, err) - require.Equal(t, tt.expected, encoded, "encoded output mismatch") - }) - - t.Run(tt.name+" decode", func(t *testing.T) { - decoded, err := CobsDecode(tt.expected, tt.xor) - require.NoError(t, err) - require.Equal(t, tt.input, decoded, "decoded output mismatch") - }) - } -} - -// TestCobsXORRoundtrip tests that XOR mode (used to eliminate newlines) roundtrips correctly. -// XOR mode is Blues-specific; there's no external standard, so we only test roundtrip. -func TestCobsXORRoundtrip(t *testing.T) { - xor := byte('\n') // 0x0A - what note-c/notecard uses - - tests := []struct { - name string - input []byte - }{ - {"single zero", []byte{0x00}}, - {"single nonzero", []byte{0x01}}, - {"contains newline", []byte{0x01, '\n', 0x02}}, - {"multiple newlines", []byte{'\n', 0x01, '\n', '\n', 0x02, '\n'}}, - {"all newlines", []byte{'\n', '\n', '\n'}}, - {"binary with newlines", func() []byte { - b := make([]byte, 256) - for i := range b { - b[i] = byte(i) - } - return b - }()}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - encoded, err := CobsEncode(tt.input, xor) - require.NoError(t, err) - - // Verify no newlines in encoded output (the whole point of XOR mode) - for i, b := range encoded { - require.NotEqual(t, byte('\n'), b, "found newline at position %d in encoded output", i) - } - - decoded, err := CobsDecode(encoded, xor) - require.NoError(t, err) - require.Equal(t, tt.input, decoded, "roundtrip failed") - }) - } -} - -// TestCobs254ByteBoundary tests the critical 254-byte boundary where COBS -// must insert an extra code byte. This is a common source of bugs. -func TestCobs254ByteBoundary(t *testing.T) { - // 254 non-zero bytes: [0xFF, 254 data bytes, 0x01] - // The 0xFF means "254 data bytes follow, no implicit zero after" - // The trailing 0x01 terminates the stream (0 more data bytes) - data254 := make([]byte, 254) - for i := range data254 { - data254[i] = byte(i) + 1 // 1, 2, 3, ..., 254 - } - - encoded254, err := CobsEncode(data254, 0) - require.NoError(t, err) - require.Len(t, encoded254, 256, "254 non-zero bytes encode to 256 bytes") - require.Equal(t, byte(0xFF), encoded254[0], "first code byte should be 0xFF") - require.Equal(t, byte(0x01), encoded254[255], "trailing code byte should be 0x01") - - decoded254, err := CobsDecode(encoded254, 0) - require.NoError(t, err) - require.Equal(t, data254, decoded254) - - // 255 non-zero bytes: [0xFF, 254 data bytes, 0x02, 1 data byte] - data255 := make([]byte, 255) - for i := range data255 { - data255[i] = byte(i) + 1 - } - data255[254] = 1 // Last byte wraps to 1 - - encoded255, err := CobsEncode(data255, 0) - require.NoError(t, err) - require.Len(t, encoded255, 257, "255 non-zero bytes encode to 257 bytes") - require.Equal(t, byte(0xFF), encoded255[0], "first code byte should be 0xFF") - require.Equal(t, byte(0x02), encoded255[255], "second code byte should be 0x02") - - decoded255, err := CobsDecode(encoded255, 0) - require.NoError(t, err) - require.Equal(t, data255, decoded255) - - // 253 non-zero bytes: [0xFE, 253 data bytes] - no extra code byte needed - data253 := make([]byte, 253) - for i := range data253 { - data253[i] = byte(i) + 1 - } - - encoded253, err := CobsEncode(data253, 0) - require.NoError(t, err) - require.Len(t, encoded253, 254, "253 non-zero bytes encode to 254 bytes") - require.Equal(t, byte(0xFE), encoded253[0], "code byte should be 0xFE for 253 data bytes") - - decoded253, err := CobsDecode(encoded253, 0) - require.NoError(t, err) - require.Equal(t, data253, decoded253) -} diff --git a/note-go/notecard/i2c-unix.go b/note-go/notecard/i2c-unix.go deleted file mode 100644 index 790eed8..0000000 --- a/note-go/notecard/i2c-unix.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. -// Forked from github.com/d2r2/go-i2c -// Forked from github.com/davecheney/i2c - -//go:build !windows - -// Before usage you must load the i2c-dev kernel module. -// Each i2c bus can address 127 independent i2c devices, and most -// linux systems contain several buses. - -// Note: I2C Device Interface is accessed through periph.io library -// Example: https://github.com/google/periph/blob/master/devices/bmxx80/bmx280.go - -package notecard - -import ( - "fmt" - "sync" - "time" - - "periph.io/x/conn/v3/driver/driverreg" - "periph.io/x/conn/v3/i2c" - "periph.io/x/conn/v3/i2c/i2creg" - "periph.io/x/host/v3" -) - -const ( - // I2CSlave is the slave device address - I2CSlave = 0x0703 -) - -// I2C is the handle to the I2C subsystem -type I2C struct { - host *driverreg.State - bus i2c.BusCloser - device *i2c.Dev -} - -// The open I2C port -var ( - hostInitialized bool - openI2CPort *I2C - i2cLock sync.RWMutex -) - -// Our default I2C address -const notecardDefaultI2CAddress = 0x17 - -// Get the default i2c device -func i2cDefault() (port string, portConfig int) { - port = "" // Null string opens first available bus - portConfig = notecardDefaultI2CAddress - return -} - -// Open the i2c port -func i2cOpen(port string, portConfig int) (err error) { - // Open the periph.io host - if !hostInitialized { - openI2CPort = &I2C{} - openI2CPort.host, err = host.Init() - if err != nil { - return - } - } - - // Open the I2C instance - i2cLock.Lock() - openI2CPort.bus, err = i2creg.Open(port) - i2cLock.Unlock() - if err != nil { - return - } - - return nil -} - -// WriteBytes writes a buffer to I2C -func i2cWriteBytes(buf []byte, i2cAddr int) (err error) { - if i2cAddr == 0 { - i2cAddr = notecardDefaultI2CAddress - } - time.Sleep(1 * time.Millisecond) // By design, must not send more than once every 1Ms - - // Single allocation for header + payload (avoids make + append pattern) - reg := make([]byte, 1+len(buf)) - reg[0] = byte(len(buf)) - copy(reg[1:], buf) - - i2cLock.Lock() - openI2CPort.device = &i2c.Dev{Bus: openI2CPort.bus, Addr: uint16(i2cAddr)} - err = openI2CPort.device.Tx(reg, nil) - i2cLock.Unlock() - if err != nil { - err = fmt.Errorf("wb: %s", err) - } - return -} - -// ReadBytes reads a buffer from I2C and returns how many are still pending -func i2cReadBytes(datalen int, i2cAddr int) (outbuf []byte, available int, err error) { - if i2cAddr == 0 { - i2cAddr = notecardDefaultI2CAddress - } - time.Sleep(1 * time.Millisecond) // By design, must not send more than once every 1Ms - readbuf := make([]byte, datalen+2) - - // Pre-allocate register buffer once outside retry loop - reg := [2]byte{0, byte(datalen)} - - for i := 0; ; i++ { // Retry just for robustness - i2cLock.Lock() - openI2CPort.device = &i2c.Dev{Bus: openI2CPort.bus, Addr: uint16(i2cAddr)} - err = openI2CPort.device.Tx(reg[:], readbuf) - i2cLock.Unlock() - if err == nil { - break - } - if i >= 10 { - err = fmt.Errorf("rb: %s", err) - return - } - time.Sleep(2 * time.Millisecond) - } - if len(readbuf) < 2 { - err = fmt.Errorf("rb: not enough data (%d < 2)", len(readbuf)) - return - } - available = int(readbuf[0]) - if available > 253 { - err = fmt.Errorf("rb: available too large (%d >253)", available) - return - } - good := readbuf[1] - if len(readbuf) < int(2+good) { - err = fmt.Errorf("rb: insufficient data (%d < %d)", len(readbuf), 2+good) - return - } - if 2 > 2+good { - if false { - fmt.Printf("i2cReadBytes(%d): %v\n", datalen, readbuf) - } - err = fmt.Errorf("rb: %d bytes returned while expecting %d", good, datalen) - return - } - outbuf = readbuf[2 : 2+good] - return -} - -// Close I2C -func i2cClose() (err error) { - i2cLock.Lock() - err = openI2CPort.bus.Close() - i2cLock.Unlock() - return -} - -// Enum I2C ports -func i2cPortEnum() (allports []string, usbports []string, notecardports []string, err error) { - // Open the periph.io host - if !hostInitialized { - openI2CPort = &I2C{} - openI2CPort.host, err = host.Init() - if err != nil { - return - } - } - - // Enum - for _, ref := range i2creg.All() { - port := ref.Name - if ref.Number != -1 { - allports = append(allports, port) - notecardports = append(notecardports, port) - } - } - return -} diff --git a/note-go/notecard/i2c-windows.go b/note-go/notecard/i2c-windows.go deleted file mode 100644 index 3cea9dc..0000000 --- a/note-go/notecard/i2c-windows.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -//go:build windows - -package notecard - -import ( - "fmt" -) - -// Get the default i2c device -func i2cDefault() (port string, portConfig int) { - port = "???" - portConfig = 0x17 - return -} - -// Set the port config of the open port -func i2cSetConfig(portConfig int) (err error) { - return fmt.Errorf("i2c not yet implemented") -} - -// Open the i2c port -func i2cOpen(port string, portConfig int) (err error) { - return fmt.Errorf("i2c not yet implemented") -} - -// WriteBytes writes a buffer to I2C -func i2cWriteBytes(buf []byte, i2cAddr int) (err error) { - return fmt.Errorf("i2c not yet implemented") -} - -// ReadBytes reads a buffer from I2C and returns how many are still pending -func i2cReadBytes(datalen int, i2cAddr int) (outbuf []byte, available int, err error) { - err = fmt.Errorf("i2c not yet implemented") - return -} - -// Close I2C -func i2cClose() error { - return fmt.Errorf("i2c not yet implemented") -} - -// Enum I2C ports -func i2cPortEnum() (allports []string, usbports []string, notecardports []string, err error) { - return -} diff --git a/note-go/notecard/lease.go b/note-go/notecard/lease.go deleted file mode 100644 index 9887d47..0000000 --- a/note-go/notecard/lease.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "strings" - "time" - - "github.com/blues/note-go/note" -) - -// Leaseing service parameters -const leaseTransactionService = "https://notepod.io:8123" -const leaseTraceService = "proxy.notepod.io:123" - -// Lease transaction -type LeaseTransaction struct { - Request string `json:"req,omitempty"` - Lessor string `json:"lessor,omitempty"` - Scope string `json:"scope,omitempty"` - Expires int64 `json:"expires,omitempty"` - Error string `json:"err,omitempty"` - DeviceUID string `json:"device,omitempty"` - NoResponse bool `json:"no_response,omitempty"` - ReqJSON string `json:"request_json,omitempty"` - RspJSON string `json:"response_json,omitempty"` -} - -// Request types -const ( - ReqReserve = "reserve" - ReqTransaction = "transaction" -) - -// Perform an HTTP transaction to the lease service -func leaseService(req LeaseTransaction, promoteError bool) (rsp LeaseTransaction, err error) { - - reqj, err := json.Marshal(req) - if err != nil { - return rsp, err - } - - // Send the transaction - hreq, err := http.NewRequest("POST", leaseTransactionService, bytes.NewBuffer(reqj)) - if err != nil { - return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) - } - hcli := &http.Client{Timeout: time.Second * 90} - hrsp, err := hcli.Do(hreq) - if err != nil { - return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) - } - defer hrsp.Body.Close() - - // Read the response - var rspjb bytes.Buffer - _, err = io.Copy(&rspjb, hrsp.Body) - if err != nil { - return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) - } - rspj := rspjb.Bytes() - - err = note.JSONUnmarshal(rspj, &rsp) - if err != nil { - return rsp, fmt.Errorf("%s %s", err, note.ErrCardIo) - } - - if promoteError && rsp.Error != "" { - return rsp, fmt.Errorf("%s", rsp.Error) - } - - return rsp, nil - -} - -// Open or reopen the remote card by taking out a lease, or by renewing the lease. -func leaseReopen(context *Context, portConfig int) (err error) { - - context.portIsOpen = false - - // Don't reopen if tracing - if InitialTraceMode { - context.portIsOpen = true - return - } - - // Find out our unique ID - context.leaseLessor = callerID() - - // Perform the lease transaction - req := LeaseTransaction{} - req.Request = ReqReserve - req.Lessor = context.leaseLessor - req.Scope = context.leaseScope - req.Expires = context.leaseExpires - rsp, err := leaseService(req, true) - if err != nil { - return err - } - - // Trace so that we can find out when - if context.leaseExpires == 0 { - fmt.Printf("%s reserved until %s\n", rsp.DeviceUID, time.Unix(rsp.Expires, 0).Local().Format("03:04:05 PM MST")) - } - - // Save the deviceUID to the allocated device - context.leaseScope = rsp.Scope - context.leaseExpires = rsp.Expires - context.leaseDeviceUID = rsp.DeviceUID - - // Open - context.portIsOpen = true - - return -} - -// Close a remote notecard -func leaseClose(context *Context) { - context.portIsOpen = false -} - -// Perform a remote transaction -func leaseTransaction(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) { - - // Perform the lease transaction - req := LeaseTransaction{} - req.Request = ReqTransaction - req.Lessor = context.leaseLessor - req.DeviceUID = context.leaseDeviceUID - req.ReqJSON = string(reqJSON) - req.NoResponse = noResponse - rsp, err := leaseService(req, true) - if err != nil { - return rspJSON, err - } - - // Done - return []byte(rsp.RspJSON), nil - -} - -// Lease trace open -func leaseTraceOpen(context *Context) (err error) { - - // Scope must be a specific device UID for trace - if !strings.HasPrefix(context.port, "dev:") { - return fmt.Errorf("trace is only allowed when a deviceUID is specified") - } - - // Open the service connection - tcpServer, err := net.ResolveTCPAddr("tcp", leaseTraceService) - if err != nil { - return - } - context.leaseTraceConn, err = net.DialTCP("tcp", nil, tcpServer) - if err != nil { - return - } - - // Write an initial non-json line containing scope, to signal to the service that this is a trace connection - leaseTraceWrite(context, []byte(context.port+"\n")) - - // Done - return - -} - -// Lease trace read function -func leaseTraceRead(context *Context) (data []byte, err error) { - - buf := make([]byte, 2048) - length, err := context.leaseTraceConn.Read(buf) - if err != nil { - if err == io.EOF { - // Just a read timeout - return data, nil - } - return data, err - } - - return buf[:length], nil - -} - -// Lease trace write function -func leaseTraceWrite(context *Context, data []byte) { - context.leaseTraceConn.Write(data) -} diff --git a/note-go/notecard/net.go b/note-go/notecard/net.go deleted file mode 100644 index deca28e..0000000 --- a/note-go/notecard/net.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -const ( - NetworkBearerUnknown = -1 - NetworkBearerGsm = 0 - NetworkBearerTdScdma = 1 - NetworkBearerWcdma = 2 - NetworkBearerCdma2000 = 3 - NetworkBearerWiMax = 4 - NetworkBearerLteTdd = 5 - NetworkBearerLteFdd = 6 - NetworkBearerNBIot = 7 - NetworkBearerWLan = 21 - NetworkBearerBluetooth = 22 - NetworkBearerIeee802p15p4 = 23 - NetworkBearerEthernet = 41 - NetworkBearerDsl = 42 - NetworkBearerPlc = 43 -) - -// NetInfo is the composite structure with all networking connection info -type NetInfo struct { - Iccid string `json:"iccid,omitempty"` - Iccid2 string `json:"iccid2,omitempty"` - IccidExternal string `json:"iccid_external,omitempty"` - Imsi string `json:"imsi,omitempty"` - Imsi2 string `json:"imsi2,omitempty"` - ImsiExternal string `json:"imsi_external,omitempty"` - Imei string `json:"imei,omitempty"` - ModemFirmware string `json:"modem,omitempty"` - Band string `json:"band,omitempty"` - AccessTechnology string `json:"rat,omitempty"` - AccessTechnologyFilter string `json:"ratf,omitempty"` - ReportedAccessTechnology string `json:"ratr,omitempty"` - ReportedCarrier string `json:"carrier,omitempty"` - Bssid string `json:"bssid,omitempty"` - Ssid string `json:"ssid,omitempty"` - // Internal vs external SIM used at any given moment - InternalSIMSelected bool `json:"internal,omitempty"` - // Radio signal strength in dBm, or ModemValueUnknown if it is not - // available from the modem. - RssiRange int32 `json:"rssir,omitempty"` - // GSM RxQual, or ModemValueUnknown if it is not available from the modem. - Rxqual int32 `json:"rxqual,omitempty"` - // General received signal strength, in dBm - Rssi int32 `json:"rssi,omitempty"` - // An integer indicating the reference signal received power (RSRP) - Rsrp int32 `json:"rsrp,omitempty"` - // An integer indicating the signal to interference plus noise ratio (SINR). - // Logarithmic value of SINR. Values are in 1/5th of a dB. The range is 0-250 - // which translates to -20dB - +30dB - Sinr int32 `json:"sinr,omitempty"` - // An integer indicating the reference signal received quality (RSRQ) - Rsrq int32 `json:"rsrq,omitempty"` - // An integer indicating relative signal strength in a human-readable way - Bars uint32 `json:"bars,omitempty"` - // IP address assigned to the device - IP string `json:"ip,omitempty"` - // IP address that the device is talking to (if known) - Gw string `json:"gateway,omitempty"` - // Device APN name - Apn string `json:"apn,omitempty"` - // Location area code (16 bits) or ModemValueUnknown if it is not avail from modem - Lac uint32 `json:"lac,omitempty"` - // Cell ID (28 bits) or ModemValueUnknown if it is not available from the modem. - Cellid uint32 `json:"cid,omitempty"` - // Network info - NetworkBearer int32 `json:"bearer,omitempty"` - Mcc uint32 `json:"mcc,omitempty"` - Mnc uint32 `json:"mnc,omitempty"` - // Modem debug - ModemDebugEvents int32 `json:"modem_test_events,omitempty"` - // Overcurrent events - OvercurrentEvents int32 `json:"oc_events,omitempty"` - OvercurrentEventSecs int32 `json:"oc_event_time,omitempty"` - // When the signal strength fields were last updated - Modified int64 `json:"updated,omitempty"` -} diff --git a/note-go/notecard/notecard.go b/note-go/notecard/notecard.go deleted file mode 100644 index b263e4f..0000000 --- a/note-go/notecard/notecard.go +++ /dev/null @@ -1,1658 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -import ( - "bytes" - "encoding/json" - "fmt" - "hash/crc32" - "io" - "net" - "os" - "os/user" - "strconv" - "strings" - "sync" - "time" - - "github.com/blues/note-go/note" - "go.bug.st/serial" -) - -// Debug serial I/O -var debugSerialIO = false - -// InitialDebugMode is the debug mode that the context is initialized with -var InitialDebugMode = false - -// InitialTraceMode is whether or not we will be entering trace mode, to prevent reservationsa -var InitialTraceMode = false - -// InitialResetMode says whether or not we should reset the port on entry -var InitialResetMode = true - -// Protect against multiple concurrent callers, because across different operating systems it is -// not at all clear that concurrency is allowed on a single I/O device. An exception is made -// for the I2C 'multiport' case (exposed by TransactionRequestToPort) where we allow multiple -// concurrent I2C transactions on a single device. (This capability was needed for the -// Notefarm, but it's unclear if anyone uses this multi-notecard concurrency capability anymore -// now that it's deprecated.) -var ( - transLock sync.RWMutex - multiportTransLock [128]sync.RWMutex -) - -// Buffer pool for serial read operations to reduce GC pressure -var serialReadBufPool = sync.Pool{ - New: func() interface{} { - buf := make([]byte, 2048) - return &buf - }, -} - -// Default transaction timeout (before receiving anything from the notecard) -const transactionTimeoutMsDefault = 30000 - -// IgnoreWindowsHWErrSecs is the amount of time to ignore a Windows serial communiction error. -var IgnoreWindowsHWErrSecs = 2 - -// Module communication interfaces -const ( - NotecardInterfaceSerial = "serial" - NotecardInterfaceI2C = "i2c" - NotecardInterfaceLease = "lease" -) - -// The number of minutes that we'll round up so that notecard reservations don't thrash -const reservationModulusMinutes = 5 - -// CardI2CMax controls chunk size that's socially appropriate on the I2C bus. -// It must be 1-253 bytes as per spec (which allows space for the 2-byte header in a 255-byte read) -const CardI2CMax = 253 - -// The notecard is a real-time device that has a fixed size interrupt buffer. We can push data -// at it far, far faster than it can process it, therefore we push it in segments with a pause -// between each segment. - -// CardRequestSerialSegmentMaxLen (golint) -const CardRequestSerialSegmentMaxLen = 250 - -// CardRequestSerialSegmentDelayMs (golint) -const CardRequestSerialSegmentDelayMs = 250 - -// CardRequestI2CSegmentMaxLen (golint) -const CardRequestI2CSegmentMaxLen = 250 - -// CardRequestI2CSegmentDelayMs (golint) -const CardRequestI2CSegmentDelayMs = 250 - -// RequestSegmentMaxLen (golint) -var RequestSegmentMaxLen = -1 - -// RequestSegmentDelayMs (golint) -var RequestSegmentDelayMs = -1 - -var DoNotReterminateJSON = false - -// Transaction retry logic -const requestRetriesAllowed = 5 - -// IoErrorIsRecoverable is a configuration parameter describing library capabilities. -// Set this to true if the error recovery of the implementation supports re-open. On all implementations -// tested to date, I can't yet get the close/reopen working the way it does on microcontrollers. For -// example, on the go serial, I get a nil pointer dereference within the go library. This MAY have -// soemthing to do with the fact that we don't cleanly implement the shutdown/restart of the inputHandler -// in trace, in which case that should be fixed. In the meantime, this is disabled. -const IoErrorIsRecoverable = true - -// Context for the port that is open -type Context struct { - // True to emit trace output - Debug bool - - // Pretty-print trace output JSON - Pretty bool - - // Disable generation of User Agent object - DisableUA bool - - // Reset should be done on next transaction - resetRequired bool - reopenRequired bool - reopenBecauseOfOpen bool - - // Sequence number - lastRequestSeqno int - - // Class functions - PortEnumFn func() (allports []string, usbports []string, notecardports []string, err error) - PortDefaultsFn func() (port string, portConfig int) - CloseFn func(context *Context) - ReopenFn func(context *Context, portConfig int) (err error) - ResetFn func(context *Context, portConfig int) (err error) - TransactionFn func(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) - - // Transaction timeout (0 for default) - transactionTimeoutMs int - - // User-specified heartbeat function - HeartbeatCtx interface{} - HeartbeatFn func(context *Context, userCtx interface{}, response []byte) bool - - // Trace functions - traceOpenFn func(context *Context) (err error) - traceReadFn func(context *Context) (data []byte, err error) - traceWriteFn func(context *Context, data []byte) - - // Port data - iface string - isLocal bool - port string - portConfig int - portIsOpen bool - - // Serial instance state - isSerial bool - serialPort serial.Port - serialUseDefault bool - serialName string - serialConfig serial.Mode - - // Serial I/O timeout helpers - ioStartSignal chan int - ioCompleteSignal chan bool - ioTimeoutSignal chan bool - - // I2C - i2cMultiport bool - - // Lease state - leaseScope string - leaseExpires int64 - leaseLessor string - leaseDeviceUID string - leaseTraceConn net.Conn -} - -// Report a critical card error -func cardReportError(context *Context, err error) { - if context == nil { - return - } - if context.Debug { - fmt.Printf("*** %s\n", err) - } - if IoErrorIsRecoverable { - time.Sleep(500 * time.Millisecond) - context.reopenRequired = true - } -} - -// Set the transaction function -func (context *Context) GetTransactionTimeoutMs() int { - if context.transactionTimeoutMs == 0 { - return transactionTimeoutMsDefault - } - return context.transactionTimeoutMs -} - -// Set the request timeout (0 to restore for default) -func (context *Context) SetTransactionTimeoutMs(msec int) { - context.transactionTimeoutMs = msec -} - -// Set or clear the heartbeat function -func (context *Context) SetTransactionHeartbeatFn(userFn func(context *Context, userCtx interface{}, rsp []byte) bool, userCtx interface{}) { - context.HeartbeatFn = userFn - context.HeartbeatCtx = userCtx -} - -// DebugOutput enables/disables debug output -func (context *Context) DebugOutput(enabled bool, pretty bool) { - context.Debug = enabled - context.Pretty = pretty -} - -// EnumPorts returns the list of all available ports on the specified interface -func (context *Context) EnumPorts() (allports []string, usbports []string, notecardports []string, err error) { - if context.PortEnumFn == nil { - return - } - return context.PortEnumFn() -} - -// PortDefaults gets the defaults for the specified port -func (context *Context) PortDefaults() (port string, portConfig int) { - if context.PortDefaultsFn == nil { - return - } - return context.PortDefaultsFn() -} - -// Identify this Notecard connection -func (context *Context) Identify() (protocol string, port string, portConfig int) { - if context.isSerial { - return "serial", context.serialName, context.serialConfig.BaudRate - } - return "I2C", context.port, context.portConfig -} - -// Defaults gets the default interface, port, and config -func Defaults() (moduleInterface string, port string, portConfig int) { - moduleInterface = NotecardInterfaceSerial - port, portConfig = serialDefault() - return -} - -// Open the card to establish communications -func Open(moduleInterface string, port string, portConfig int) (context *Context, err error) { - if moduleInterface == "" { - moduleInterface, _, _ = Defaults() - } - - switch moduleInterface { - case NotecardInterfaceSerial: - context, err = OpenSerial(port, portConfig) - context.isLocal = true - case NotecardInterfaceI2C: - context, err = OpenI2C(port, portConfig) - context.isLocal = true - case NotecardInterfaceLease: - context, err = OpenLease(port, portConfig) - default: - err = fmt.Errorf("unknown interface: %s", moduleInterface) - } - if err != nil { - cardReportError(nil, err) - err = fmt.Errorf("error opening port: %s %s", err, note.ErrCardIo) - return - } - context.iface = moduleInterface - return -} - -// Reset serial to a known state. Note that this is performed by sending -// a newline and then draining the input buffer. If a newline is not -// received, it is NOT a bug because, for example, Starnote does not -// perform the echo'ing of \n *by design*. -func cardResetSerial(context *Context, portConfig int) (err error) { - // Exit if not open - if !context.portIsOpen { - err = fmt.Errorf("port not open " + note.ErrCardIo) - cardReportError(context, err) - return - } - - // In order to ensure that we're not getting the reply to a failed - // transaction from a prior session, drain any pending input prior - // to transmitting a command. Note that we use this technique of - // looking for a known reply to \n, rather than just "draining - // anything pending on serial", because the nature of read() is - // that it blocks (until timeout) if there's nothing available. - var length int - bufPtr := serialReadBufPool.Get().(*[]byte) - buf := *bufPtr - defer serialReadBufPool.Put(bufPtr) - - for { - if debugSerialIO { - fmt.Printf("cardResetSerial: about to write newline\n") - } - serialIOBegin(context, context.GetTransactionTimeoutMs()) - _, err = context.serialPort.Write([]byte("\n")) - err = serialIOEnd(context, err) - if debugSerialIO { - fmt.Printf(" back with err = %v\n", err) - } - if err != nil { - err = fmt.Errorf("error transmitting to module: %s %s", err, note.ErrCardIo) - cardReportError(context, err) - return - } - time.Sleep(250 * time.Millisecond) - if debugSerialIO { - fmt.Printf("cardResetSerial: about to read up to %d bytes\n", len(buf)) - } - readBeganMs := int(time.Now().UnixNano() / 1000000) - serialIOBegin(context, 750) - length, err = context.serialPort.Read(buf) - err = serialIOEnd(context, err) - readElapsedMs := int(time.Now().UnixNano()/1000000) - readBeganMs - if debugSerialIO { - fmt.Printf(" back after %d ms with len = %d err = %v\n", readElapsedMs, length, err) - } - if readElapsedMs == 0 && length == 0 && err == io.EOF { - // On Linux, hardware port failures come back simply as immediate EOF - err = fmt.Errorf("hardware failure") - } - if err != nil { - // Ignore errors after reset, as the only purpose of reset is to drain the input buffer - err = CardReopenSerial(context, portConfig) - return err - } - somethingFound := false - nonCRLFFound := false - for i := 0; i < length && !nonCRLFFound; i++ { - if false { - fmt.Printf("chr: 0x%02x '%c'\n", buf[i], buf[i]) - } - if buf[i] != '\r' { - somethingFound = true - if buf[i] != '\n' { - nonCRLFFound = true - } - } - } - if somethingFound && !nonCRLFFound { - break - } - } - - // Done - return -} - -// Serial I/O timeout helper function for Windows -func serialTimeoutHelper(context *Context, portConfig int) { - for { - timeoutMs := <-context.ioStartSignal - timeout := false - select { - case <-context.ioCompleteSignal: - case <-time.After(time.Duration(timeoutMs) * time.Millisecond): - timeout = true - if debugSerialIO { - fmt.Printf("serialTimeoutHelper: timeout\n") - } - cardCloseSerial(context) - } - context.ioTimeoutSignal <- timeout - } -} - -// Begin a serial I/O -func serialIOBegin(context *Context, timeoutMs int) { - context.ioStartSignal <- timeoutMs - if debugSerialIO { - if !context.portIsOpen { - fmt.Printf("serialIoBegin: WARNING: PORT NOT OPEN\n") - } - fmt.Printf("serialIOBegin: begin timeout of %d ms\n", timeoutMs) - } -} - -// End a serial I/O -func serialIOEnd(context *Context, errIn error) (errOut error) { - errOut = errIn - context.ioCompleteSignal <- true - timeout := <-context.ioTimeoutSignal - select { - case <-context.ioCompleteSignal: - if debugSerialIO { - fmt.Printf("serialIOEnd: ioComplete ate the completed signal (timeout: %v)\n", timeout) - } - default: - if debugSerialIO { - fmt.Printf("serialIOEnd: ioComplete nothing to eat (timeout: %v)\n", timeout) - } - } - if timeout { - errOut = fmt.Errorf("serial I/O timeout %s", note.ErrCardIo) - } - return -} - -// OpenSerial opens the card on serial -func OpenSerial(port string, portConfig int) (context *Context, err error) { - // Create the context structure - context = &Context{} - context.Debug = InitialDebugMode - context.port = port - context.portConfig = portConfig - context.lastRequestSeqno = 0 - - // Set up class functions - context.PortEnumFn = serialPortEnum - context.PortDefaultsFn = serialDefault - context.CloseFn = cardCloseSerial - context.ReopenFn = CardReopenSerial - context.ResetFn = cardResetSerial - context.TransactionFn = cardTransactionSerial - context.traceOpenFn = serialTraceOpen - context.traceReadFn = serialTraceRead - context.traceWriteFn = serialTraceWrite - - // Record serial configuration, and whether or not we are using the default - context.isSerial = true - context.serialName, context.serialConfig.BaudRate = serialDefault() - if port == "" { - context.serialUseDefault = true - } else { - context.serialName = port - - } - if portConfig != 0 { - context.serialConfig.BaudRate = portConfig - } - - // Set up I/O port close channels, because Windows needs a bit of help in timing out I/O's. - context.ioStartSignal = make(chan int, 1) - context.ioCompleteSignal = make(chan bool, 1) - context.ioTimeoutSignal = make(chan bool, 1) - go serialTimeoutHelper(context, portConfig) - - // For serial, we defer the port open until the first transaction so that we can - // support the concept of dynamically inserted devices, as in "notecard -scan" mode. - context.reopenBecauseOfOpen = true - context.reopenRequired = true - - // All set - return -} - -// Reset I2C to a known good state -func cardResetI2C(context *Context, portConfig int) (err error) { - // Synchronize by guaranteeing not only that I2C works, but that we drain the remainder of any - // pending partial reply from a previously-aborted session. - chunklen := 0 - for { - - // Read the next chunk of available data - _, available, err2 := i2cReadBytes(chunklen, portConfig) - if err2 != nil { - err = fmt.Errorf("error reading chunk: %s %s", err2, note.ErrCardIo) - return - } - - // If nothing left, we're ready to transmit a command to receive the data - if available == 0 { - break - } - - // For the next iteration, reaad the min of what's available and what we're permitted to read - chunklen = available - if chunklen > CardI2CMax { - chunklen = CardI2CMax - } - - } - - // Done - return -} - -// OpenI2C opens the card on I2C -func OpenI2C(port string, portConfig int) (context *Context, err error) { - - // Create the context structure - context = &Context{} - context.Debug = InitialDebugMode - context.lastRequestSeqno = 0 - - // Open - context.portIsOpen = false - - // Use default if not specified - if port == "" { - port, portConfig = i2cDefault() - } - context.port = port - context.portConfig = portConfig - - // Set up class functions - context.PortEnumFn = i2cPortEnum - context.PortDefaultsFn = i2cDefault - context.CloseFn = cardCloseI2C - context.ReopenFn = cardReopenI2C - context.ResetFn = cardResetI2C - context.TransactionFn = cardTransactionI2C - - // Open the I2C port - err = i2cOpen(port, portConfig) - if err != nil { - if false { - ports, _, _, _ := I2CPorts() - fmt.Printf("Available ports: %v\n", ports) - } - err = fmt.Errorf("i2c init error: %s", err) - return - } - - // Open - context.portIsOpen = true - - // Done - return -} - -// Reset the port -func (context *Context) Reset(portConfig int) (err error) { - context.resetRequired = false - if context.ResetFn == nil { - return - } - return context.ResetFn(context, portConfig) -} - -// Close the port -func (context *Context) Close() { - context.CloseFn(context) -} - -// Close serial -func cardCloseSerial(context *Context) { - if !context.portIsOpen { - if debugSerialIO { - fmt.Printf("cardCloseSerial: port not open\n") - } - } else { - if debugSerialIO { - fmt.Printf("cardCloseSerial: closed\n") - } - context.serialPort.Close() - context.portIsOpen = false - } -} - -// Close I2C -func cardCloseI2C(context *Context) { - _ = i2cClose() - context.portIsOpen = false -} - -// ReopenIfRequired reopens the port but only if required -func (context *Context) ReopenIfRequired(portConfig int) (err error) { - if context.reopenRequired { - err = context.ReopenFn(context, portConfig) - } - return -} - -// Reopen the port -func (context *Context) Reopen(portConfig int) (err error) { - context.reopenRequired = false - err = context.ReopenFn(context, portConfig) - return -} - -// Reopen serial -func CardReopenSerial(context *Context, portConfig int) (err error) { - - // Close if open - cardCloseSerial(context) - - // Handle deferred insertion - if context.serialUseDefault { - context.serialName, context.serialConfig.BaudRate = serialDefault() - } - if context.serialName == "" { - return fmt.Errorf("error opening serial port: serial device not available %s", note.ErrCardIo) - } - - if portConfig != 0 { - context.serialConfig.BaudRate = portConfig - } - // Set default speed if not set - if context.serialConfig.BaudRate == 0 { - _, context.serialConfig.BaudRate = serialDefault() - } - - // Open the serial port - if debugSerialIO { - fmt.Printf("CardReopenSerial: about to open '%s'\n", context.serialName) - } - context.serialPort, err = serial.Open(context.serialName, &context.serialConfig) - if debugSerialIO { - fmt.Printf(" back with err = %v\n", err) - } - if err != nil { - return fmt.Errorf("error opening serial port %s at %d: %s %s", context.serialName, context.serialConfig.BaudRate, err, note.ErrCardIo) - } - - context.portIsOpen = true - - // Done with the reopen - context.reopenRequired = false - - // Unless we've been instructed not to reset on open, reset serial to a known good state - if context.reopenBecauseOfOpen { - context.reopenBecauseOfOpen = false - if InitialResetMode { - err = cardResetSerial(context, portConfig) - } - } - - // Done - return err -} - -// Reopen I2C -func cardReopenI2C(context *Context, portConfig int) (err error) { - fmt.Printf("error i2c reopen not yet supported since I can't test it yet\n") - return -} - -// SerialDefaults returns the default serial parameters -func SerialDefaults() (port string, portConfig int) { - return serialDefault() -} - -// I2CDefaults returns the default serial parameters -func I2CDefaults() (port string, portConfig int) { - return i2cDefault() -} - -// SerialPorts returns the list of available serial ports -func SerialPorts() (allports []string, usbports []string, notecardports []string, err error) { - return serialPortEnum() -} - -// I2CPorts returns the list of available I2C ports -func I2CPorts() (allports []string, usbports []string, notecardports []string, err error) { - return i2cPortEnum() -} - -// TransactionRequest performs a card transaction with a Req structure -func (context *Context) TransactionRequest(req Request) (rsp Request, err error) { - return context.transactionRequest(req, false, 0) -} - -// TransactionRequestToPort performs a card transaction with a Req structure, to a specified port -func (context *Context) TransactionRequestToPort(req Request, portConfig int) (rsp Request, err error) { - return context.transactionRequest(req, true, portConfig) -} - -// transactionRequest performs a card transaction with a Req structure, to the current or specified port -func (context *Context) transactionRequest(req Request, multiport bool, portConfig int) (rsp Request, err error) { - reqJSON, err2 := note.JSONMarshal(req) - if err2 != nil { - err = fmt.Errorf("error marshaling request for module: %s", err2) - return - } - var rspJSON []byte - rspJSON, err = context.transactionJSON(reqJSON, multiport, portConfig) - if err != nil { - // Give transaction's error precedence, except that if we get an error unmarshaling - // we want to make sure that we indicate to the caller that there was an I/O error (corruption) - err2 := note.JSONUnmarshal(rspJSON, &rsp) - if err2 != nil { - err = fmt.Errorf("%s %s", err, note.ErrCardIo) - } - return - } - err = note.JSONUnmarshal(rspJSON, &rsp) - if err != nil { - err = fmt.Errorf("error unmarshaling reply from module: %s %s: %s", err, note.ErrCardIo, rspJSON) - } - return -} - -// NewRequest creates a new request that is guaranteed to get a response -// from the Notecard. Note that this method is provided merely as syntactic sugar, as of the form -// req := notecard.NewRequest("note.add") -func NewRequest(reqType string) (req map[string]interface{}) { - return map[string]interface{}{ - "req": reqType, - } -} - -// NewCommand creates a new command that requires no response from the notecard. -func NewCommand(reqType string) (cmd map[string]interface{}) { - return map[string]interface{}{ - "cmd": reqType, - } -} - -// NewBody creates a new body. Note that this method is provided -// merely as syntactic sugar, as of the form -// body := note.NewBody() -func NewBody() (body map[string]interface{}) { - return make(map[string]interface{}) -} - -// Request performs a card transaction with a JSON structure and doesn't return a response -// (This is for semantic compatibility with other languages.) -func (context *Context) Request(req map[string]interface{}) (err error) { - _, err = context.Transaction(req) - return -} - -// RequestResponse performs a card transaction with a JSON structure and doesn't return a response -// (This is for semantic compatibility with other languages.) -func (context *Context) RequestResponse(req map[string]interface{}) (rsp map[string]interface{}, err error) { - return context.Transaction(req) -} - -// Response is used in rare cases where there is a transaction that returns multiple responses -func (context *Context) Response() (rsp map[string]interface{}, err error) { - return context.Transaction(nil) -} - -// Transaction performs a card transaction with a JSON structure -func (context *Context) Transaction(req map[string]interface{}) (rsp map[string]interface{}, err error) { - // Handle the special case where we are just processing a response - var reqJSON []byte - if req == nil { - reqJSON = []byte("") - } else { - - // Marshal the request to JSON - reqJSON, err = note.JSONMarshal(req) - if err != nil { - err = fmt.Errorf("error marshaling request for module: %s", err) - return - } - - } - - // Perform the transaction - rspJSON, err2 := context.TransactionJSON(reqJSON) - if err2 != nil { - err = fmt.Errorf("error from TransactionJSON: %s", err2) - return - } - - // Unmarshal for convenience of the caller - err = note.JSONUnmarshal(rspJSON, &rsp) - if err != nil { - err = fmt.Errorf("error unmarshaling reply from module: %s %s: %s", err, note.ErrCardIo, rspJSON) - return - } - - // Done - return -} - -// ReceiveBytes receives arbitrary Bytes from the Notecard -func (context *Context) ReceiveBytes() (rspBytes []byte, err error) { - return context.receiveBytes(0) -} - -// SendBytes sends arbitrary Bytes to the Notecard -func (context *Context) SendBytes(reqBytes []byte) (err error) { - - // Only operate on port 0 - portConfig := 0 - - // Only one caller at a time accessing the I/O port - lockTrans(false, portConfig) - - // Reopen if error - err = context.ReopenIfRequired(portConfig) - if err != nil { - unlockTrans(false, portConfig) - return - } - - // Do a reset if one was pending - if context.resetRequired { - _ = context.Reset(portConfig) - } - - // Do the send, with no response requested and no delays (binary transfer) - _, err = context.TransactionFn(context, portConfig, true, reqBytes, false) - - // Done - unlockTrans(false, portConfig) - return - -} - -// receiveBytes receives arbitrary Bytes from the Notecard, using the current or specified port -func (context *Context) receiveBytes(portConfig int) (rspBytes []byte, err error) { - // Only one caller at a time accessing the I/O port - lockTrans(false, portConfig) - - // Reopen if error - err = context.ReopenIfRequired(portConfig) - if err != nil { - unlockTrans(false, portConfig) - if context.Debug { - fmt.Printf("%s\n", err) - } - return - } - - // Do a reset if one was pending - if context.resetRequired { - _ = context.Reset(portConfig) - } - - // Request is empty - var reqBytes []byte - // Perform the transaction with no delays (binary transfer) - rspBytes, err = context.TransactionFn(context, portConfig, false, reqBytes, false) - - unlockTrans(false, portConfig) - - // Done - return -} - -// TransactionJSON performs a card transaction using raw JSON []bytes -func (context *Context) TransactionJSON(reqJSON []byte) (rspJSON []byte, err error) { - return context.transactionJSON(reqJSON, false, 0) -} - -// TransactionJSONToPort performs a card transaction using raw JSON []bytes to a specified port -func (context *Context) TransactionJSONToPort(reqJSON []byte, portConfig int) (rspJSON []byte, err error) { - return context.transactionJSON(reqJSON, true, portConfig) -} - -// transactionJSON performs a card transaction using raw JSON []bytes, to the current or specified port -func (context *Context) transactionJSON(reqJSON []byte, multiport bool, portConfig int) (rspJSON []byte, err error) { - // Remember in the context if we've ever seen multiport I/O, for timeout computation - if multiport { - context.i2cMultiport = true - } - - // Unmarshal the request to peek inside it. Also, accept a zero-length request as a valid case - // because we use this in the test fixture where we just accept pure responses w/o requests. - var req Request - var noResponseRequested bool - if len(reqJSON) > 0 { - - // Make sure that it is valid JSON, because the transports won't validate this - // and they may misbehave if they do not get a valid JSON response back. - err = note.JSONUnmarshal(reqJSON, &req) - if err != nil { - return - } - - // If this is a hub.set, generate a user agent object if one hasn't already been supplied - if !context.DisableUA && (req.Req == ReqHubSet || req.Cmd == ReqHubSet) && req.Body == nil { - ua := context.UserAgent() - if ua != nil { - req.Body = &ua - reqJSON, _ = note.JSONMarshal(req) - } - } - - // Determine whether or not a response will be expected from the notecard by - // examining the req and cmd fields - noResponseRequested = req.Req == "" && req.Cmd != "" - - if !DoNotReterminateJSON { - // Make sure that the JSON has a single \n terminator - // Use byte operations instead of string conversions - for len(reqJSON) > 0 { - last := reqJSON[len(reqJSON)-1] - if last != '\n' && last != '\r' { - break - } - reqJSON = reqJSON[:len(reqJSON)-1] - } - reqJSON = append(reqJSON, '\n') - } - } - - // Debug - if context.Debug { - var j []byte - if context.Pretty { - j, _ = note.JSONMarshalIndent(req, "", " ") - } else { - j, _ = note.JSONMarshal(req) - } - fmt.Printf("%s\n", string(j)) - } - - // If it is a request (as opposed to a command), include a CRC so that the - // request might be retried if it is received in a corrupted state. (We can - // only do this on requests because for cmd's there is no 'response channel' - // where we can find out that the cmd failed. Note that a Seqno is included - // as part of the CRC data so that two identical requests occurring within the - // modulus of seqno never are mistaken as being the same request being retried. - lastRequestRetries := 0 - lastRequestCrcAdded := false - if !noResponseRequested { - reqJSON = crcAdd(reqJSON, context.lastRequestSeqno) - lastRequestCrcAdded = true - } - - // Only one caller at a time accessing the I/O port - lockTrans(multiport, portConfig) - - // Transaction retry loop. Note that "err" must be set before breaking out of loop - err = nil - for lastRequestRetries <= requestRetriesAllowed { - - // Only do reopen/reset in the single-port case, because we may not be talking to the port in error - if !multiport { - - // Reopen if error - err = context.ReopenIfRequired(portConfig) - if err != nil { - unlockTrans(multiport, portConfig) - if context.Debug { - fmt.Printf("%s\n", err) - } - return - } - - // Do a reset if one was pending - if context.resetRequired { - _ = context.Reset(portConfig) - } - - } - - // Perform the transaction with delays (JSON requires pacing for the Notecard) - rspJSON, err = context.TransactionFn(context, portConfig, noResponseRequested, reqJSON, true) - if err != nil { - // We can defer the error if a single port, but we need to reset it NOW if multiport - if multiport { - if context.ResetFn != nil { - _ = context.ResetFn(context, portConfig) - } - } else { - context.resetRequired = true - } - } - - // If no response expected, we won't be retrying - if noResponseRequested { - break - } - - // Decode the response to create an error if the response JSON was badly formatted. - // do this because it's SUPER inconvenient to always be checking for a response error - // vs an error on the transaction itself - if err == nil { - var rsp Request - err = note.JSONUnmarshal(rspJSON, &rsp) - if err != nil { - err = fmt.Errorf("error unmarshaling reply from module: %s %s: %s", err, note.ErrCardIo, rspJSON) - } else { - if rsp.Err != "" { - if req.Req == "" { - err = fmt.Errorf("%s", rsp.Err) - } else { - err = fmt.Errorf("%s: %s", req.Req, rsp.Err) - } - } - } - } - - // Don't retry these transactions for obvious reasons - if req.Req == ReqCardRestore || req.Req == ReqCardRestart { - break - } - - // If an I/O error, retry - if note.ErrorContains(err, note.ErrCardIo) && !note.ErrorContains(err, note.ErrReqNotSupported) { - // We can defer the error if a single port, but we need to reset it NOW if multiport - if multiport { - if context.ResetFn != nil { - _ = context.ResetFn(context, portConfig) - } - } else { - context.resetRequired = true - } - lastRequestRetries++ - if context.Debug { - fmt.Printf("retrying I/O error detected by host: %s\n", err) - } - time.Sleep(500 * time.Millisecond) - continue - } - - // If an error, stop transaction processing here - if err != nil { - break - } - - // If we sent a CRC in the request, examine the response JSON to see if - // it has a CRC error. Note that the CRC is stripped from the - // rspJSON as a side-effect of this method. - if lastRequestCrcAdded { - rspJSON, err = crcError(rspJSON, context.lastRequestSeqno) - if err != nil { - lastRequestRetries++ - if context.Debug { - fmt.Printf("retrying: %s\n", err) - } - time.Sleep(500 * time.Millisecond) - continue - } - - } - - // Transaction completed - break - - } - - // Bump the request sequence number now that we've processed this request, success or error - context.lastRequestSeqno++ - - // If this was a card restore, we want to hold everyone back if we reset the card if it - // isn't a multiport case. But in multiport, we only want to hold this caller back. - if (req.Req == ReqCardRestore) && req.Reset { - // Special case card.restore, reset:true does not cause a reboot. - unlockTrans(multiport, portConfig) - } else if context.isLocal && (req.Req == ReqCardRestore || req.Req == ReqCardRestart) { - if multiport { - unlockTrans(multiport, portConfig) - time.Sleep(12 * time.Second) - } else { - context.reopenRequired = true - time.Sleep(8 * time.Second) - unlockTrans(multiport, portConfig) - } - } else { - unlockTrans(multiport, portConfig) - } - - // If no response, we're done - if noResponseRequested { - rspJSON = []byte("{}") - return - } - - // Debug - if context.Debug { - responseJSON := rspJSON - if context.Pretty { - var rsp Request - e := note.JSONUnmarshal(responseJSON, &rsp) - if e == nil { - prettyJSON, e := note.JSONMarshalIndent(rsp, " ", " ") - if e == nil { - fmt.Printf("==> ") - responseJSON = append(prettyJSON, byte('\n')) - } - } - } - fmt.Printf("%s", string(responseJSON)) - } - - // Done - return -} - -// Perform a card transaction over serial under the assumption that request already has '\n' terminator -func cardTransactionSerial(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) { - // Exit if not open - if !context.portIsOpen { - err = fmt.Errorf("port not open " + note.ErrCardIo) - cardReportError(context, err) - return - } - - // Initialize timing parameters - if RequestSegmentMaxLen < 0 { - RequestSegmentMaxLen = CardRequestSerialSegmentMaxLen - } - if RequestSegmentDelayMs < 0 { - RequestSegmentDelayMs = CardRequestSerialSegmentDelayMs - } - - // Set the serial read timeout to 30 seconds, preventing reads under windows from stalling indefinitely on a serial error. - _ = context.serialPort.SetReadTimeout(30 * time.Second) - - // Handle the special case where we are looking only for a reply - if len(reqJSON) > 0 { - - // Transmit the request in segments so as not to overwhelm the notecard's interrupt buffers - segOff := 0 - segLeft := len(reqJSON) - for { - segLen := segLeft - if segLen > RequestSegmentMaxLen { - segLen = RequestSegmentMaxLen - } - if debugSerialIO { - fmt.Printf("cardTransactionSerial: about to write %d bytes\n", segLen) - } - serialIOBegin(context, context.GetTransactionTimeoutMs()) - _, err = context.serialPort.Write(reqJSON[segOff : segOff+segLen]) - err = serialIOEnd(context, err) - if debugSerialIO { - fmt.Printf(" back with err = %v\n", err) - } - if err != nil { - err = fmt.Errorf("error transmitting to module: %s %s", err, note.ErrCardIo) - cardReportError(context, err) - return - } - segOff += segLen - segLeft -= segLen - if segLeft == 0 { - break - } - if delay { - time.Sleep(time.Duration(RequestSegmentDelayMs) * time.Millisecond) - } - } - - } - - // If no response, we're done - if noResponse { - return - } - - // Read the reply until we get '\n' at the end - waitBegan := time.Now() - waitExpires := waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) - - // Get pooled buffer for reading to reduce allocations - bufPtr := serialReadBufPool.Get().(*[]byte) - buf := *bufPtr - defer serialReadBufPool.Put(bufPtr) - - // Pre-allocate response buffer - rspJSON = make([]byte, 0, 4096) - - for { - var length int - if debugSerialIO { - fmt.Printf("cardTransactionSerial: about to read up to %d bytes\n", len(buf)) - } - readBeganMs := int(time.Now().UnixNano() / 1000000) - waitRemainingMs := int(time.Until(waitExpires).Milliseconds()) - serialIOBegin(context, waitRemainingMs) - length, err = context.serialPort.Read(buf) - err = serialIOEnd(context, err) - readElapsedMs := int(time.Now().UnixNano()/1000000) - readBeganMs - if debugSerialIO { - fmt.Printf(" back after %d ms with len = %d err = %v\n", readElapsedMs, length, err) - } - if false { - err2 := err - if err2 == nil { - err2 = fmt.Errorf("none") - } - fmt.Printf("req: elapsed:%d len:%d err:%s '%s'\n", readElapsedMs, length, err2, string(buf[:length])) - } - if readElapsedMs == 0 && length == 0 && err == io.EOF { - // On Linux, hardware port failures come back simply as immediate EOF - err = fmt.Errorf("hardware failure") - } - if err != nil { - if err == io.EOF { - // Just a read timeout - continue - } - // Ignore [flaky, rare, Windows] hardware errors for up to several seconds - if (time.Now().Unix() - waitBegan.Unix()) > int64(IgnoreWindowsHWErrSecs) { - err = fmt.Errorf("error reading from module: %s %s", err, note.ErrCardIo) - cardReportError(context, err) - return - } - time.Sleep(1 * time.Second) - continue - } - rspJSON = append(rspJSON, buf[:length]...) - - // Use bytes.IndexByte instead of strings.Contains - if bytes.IndexByte(rspJSON, '\n') == -1 { - continue - } - - // We now have at least one whole line. If we're just gathering a reply, we're done. - if len(reqJSON) == 0 { - break - } - - // Find the last newline position - lastNewline := bytes.LastIndexByte(rspJSON, '\n') - if lastNewline == -1 { - continue - } - - // Check if there's a partial line after the last newline - if lastNewline < len(rspJSON)-1 { - // The reply should be only a single line. However, if the user had been - // in trace mode (likely on USB) we may be receiving trace lines that - // were sent to us and inserted into the serial buffer prior to the JSON reply. - rspJSON = rspJSON[lastNewline+1:] - continue - } - - // Find the second-to-last line - prevNewline := -1 - if lastNewline > 0 { - prevNewline = bytes.LastIndexByte(rspJSON[:lastNewline], '\n') - } - - var secondToLastLine []byte - if prevNewline == -1 { - secondToLastLine = rspJSON[:lastNewline] - } else { - secondToLastLine = rspJSON[prevNewline+1 : lastNewline] - } - - // Skip the line if it's empty or doesn't look like JSON - if len(secondToLastLine) == 0 || secondToLastLine[0] != '{' { - rspJSON = rspJSON[:0] - continue - } - - // ** We now have a clean response in rspJSON ** - - // We're done if it's not a heartbeat - fn := context.HeartbeatFn - if fn == nil { - break - } - m := make(map[string]string) - if json.Unmarshal(rspJSON, &m) != nil { - break - } - v, errPresent := m["err"] - if !errPresent { - break - } - if !strings.Contains(v, note.ErrCardHeartbeat) { - break - } - - // Call the heartbeat function, and abort if it requests that we do so - if fn(context, context.HeartbeatCtx, rspJSON) { - err = fmt.Errorf("aborted by heartbeat function") - cardReportError(context, err) - return - } - - // Reset the JSON and timeout and start again - rspJSON = []byte{} - waitBegan = time.Now() - waitExpires = waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) - } - - // Done - return -} - -// Perform a card transaction over I2C under the assumption that request already has '\n' terminator -func cardTransactionI2C(context *Context, portConfig int, noResponse bool, reqJSON []byte, delay bool) (rspJSON []byte, err error) { - // Initialize timing parameters - if RequestSegmentMaxLen < 0 { - RequestSegmentMaxLen = CardRequestI2CSegmentMaxLen - } - if RequestSegmentDelayMs < 0 { - RequestSegmentDelayMs = CardRequestI2CSegmentDelayMs - } - - // Transmit the request in chunks, but also in segments so as not to overwhelm the notecard's interrupt buffers - chunkoffset := 0 - jsonbufLen := len(reqJSON) - sentInSegment := 0 - for jsonbufLen > 0 { - chunklen := CardI2CMax - if jsonbufLen < chunklen { - chunklen = jsonbufLen - } - err = i2cWriteBytes(reqJSON[chunkoffset:chunkoffset+chunklen], portConfig) - if err != nil { - err = fmt.Errorf("write error: %s %s", err, note.ErrCardIo) - return - } - chunkoffset += chunklen - jsonbufLen -= chunklen - sentInSegment += chunklen - if delay { - if sentInSegment > RequestSegmentMaxLen { - sentInSegment = 0 - time.Sleep(time.Duration(RequestSegmentDelayMs) * time.Millisecond) - } - time.Sleep(time.Duration(RequestSegmentDelayMs) * time.Millisecond) - } - } - - // If no response, we're done - if noResponse { - return - } - - // Loop, building a reply buffer out of received chunks. We'll build the reply in the same - // buffer we used to transmit, and will grow it as necessary. - jsonbufLen = 0 - receivedNewline := false - chunklen := 0 - waitBegan := time.Now() - waitExpires := waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) - for { - - // Read the next chunk - readbuf, available, err2 := i2cReadBytes(chunklen, portConfig) - if err2 != nil { - err = fmt.Errorf("read error: %s %s", err2, note.ErrCardIo) - return - } - - // Append to the JSON being accumulated - rspJSON = append(rspJSON, readbuf...) - readlen := len(readbuf) - jsonbufLen += readlen - - // If we received something, reset the expiration to what we'd expect for just the - // I/O portion of a transaction. - if readlen > 0 { - waitExpires = time.Now().Add(time.Duration(5) * time.Second) - } - - // If the last byte of the chunk is \n, chances are that we're done. However, just so - // that we pull everything pending from the module, we only exit when we've received - // a newline AND there's nothing left available from the module. - if readlen > 0 && readbuf[readlen-1] == '\n' { - receivedNewline = true - } - - // For the next iteration, reaad the min of what's available and what we're permitted to read - chunklen = available - if chunklen > CardI2CMax { - chunklen = CardI2CMax - } - - // If there's something available on the notecard for us to receive, do it - if chunklen > 0 { - continue - } - - // See if we're done - if !receivedNewline { - - // If we've timed out and nothing's available, exit - expired := time.Now().After(waitExpires) - if context.i2cMultiport { - expired = time.Now().After(waitBegan.Add(time.Duration(90) * time.Second)) - } - if expired { - err = fmt.Errorf("transaction timeout (%d bytes received before timeout) %s", jsonbufLen, note.ErrCardIo+note.ErrTimeout) - return - } - - // Continue receiving - continue - } - - // ** We now have a clean response in rspJSON ** - - // We're done if it's not a heartbeat - fn := context.HeartbeatFn - if fn == nil { - break - } - m := make(map[string]string) - if json.Unmarshal(rspJSON, &m) != nil { - break - } - v, errPresent := m["err"] - if !errPresent { - break - } - if !strings.Contains(v, note.ErrCardHeartbeat) { - break - } - - // Call the heartbeat function, and abort if it requests that we do so - if fn(context, context.HeartbeatCtx, rspJSON) { - err = fmt.Errorf("aborted by heartbeat function") - cardReportError(context, err) - return - } - - // Reset the JSON and timeout and start again - rspJSON = []byte{} - waitBegan = time.Now() - waitExpires = waitBegan.Add(time.Duration(context.GetTransactionTimeoutMs()) * time.Millisecond) - } - - // Done - return -} - -// OpenLease opens a remote card with a lease -func OpenLease(leaseScope string, leaseMins int) (context *Context, err error) { - - // Create the context structure - context = &Context{} - context.Debug = InitialDebugMode - context.port = leaseScope - context.portConfig = 0 - context.lastRequestSeqno = 0 - - // Prevent accidental reservation for excessive durations e.g. 115200 minutes - if leaseMins > 120 { - err = fmt.Errorf("leasing a notecard has a 120 minute limit, but got %d", leaseMins) - return - } - - // Set up class functions - context.CloseFn = leaseClose - context.ReopenFn = leaseReopen - context.TransactionFn = leaseTransaction - context.traceOpenFn = leaseTraceOpen - context.traceReadFn = leaseTraceRead - context.traceWriteFn = leaseTraceWrite - - // Record serial configuration - context.leaseScope = leaseScope - if leaseMins == 0 { - leaseMins = 1 - } - leaseMins = (((leaseMins - 1) / reservationModulusMinutes) + 1) * reservationModulusMinutes - context.leaseExpires = time.Now().Unix() + int64(leaseMins*60) - - // Open the port - err = context.ReopenFn(context, context.portConfig) - if err != nil { - err = fmt.Errorf("error taking out lease: %s %s", err, note.ErrCardIo) - return - } - - // All set - return -} - -// Lock the appropriate mutex for the transaction -func lockTrans(multiport bool, portConfig int) { - if multiport && portConfig >= 0 && portConfig < 128 { - multiportTransLock[portConfig].Lock() - } else { - transLock.Lock() - } -} - -// Unlock the appropriate mutex for the transaction -func unlockTrans(multiport bool, portConfig int) { - if multiport && portConfig >= 0 && portConfig < 128 { - multiportTransLock[portConfig].Unlock() - } else { - transLock.Unlock() - } -} - -// Get the CallerID for this requestor, increasing the likelihood of getting the same -// reservation between tests which may be run across different machines and across -// different processes on the same machine. -func callerID() (id string) { - - // See if it's specified in the environment - id = os.Getenv("NOTEFARM_CALLERID") - if id != "" { - return - } - - user, err := user.Current() - if user != nil && err == nil { - id = user.Username - } - - hostname, err := os.Hostname() - if hostname != "" && err == nil { - id += "@" + hostname - } - - if id == "" { - // Use the mac address if we have no other name - interfaces, err := net.Interfaces() - if err == nil { - for _, i := range interfaces { - if i.Flags&net.FlagUp != 0 && !bytes.Equal(i.HardwareAddr, nil) { - // Don't use random as we have a real address - id = i.HardwareAddr.String() - break - } - } - } - } - - return -} - -// Serial trace open -func serialTraceOpen(context *Context) (err error) { - return -} - -// Serial trace read function -func serialTraceRead(context *Context) (data []byte, err error) { - - // Exit if not open - if !context.portIsOpen { - return data, fmt.Errorf("port not open " + note.ErrCardIo) - } - - // Do the read - var length int - buf := make([]byte, 2048) - readBeganMs = int(time.Now().UnixNano() / 1000000) - length, err = context.serialPort.Read(buf) - readElapsedMs := int(time.Now().UnixNano()/1000000) - readBeganMs - if false { - fmt.Printf("mon: elapsed:%d len:%d err:%s '%s'\n", readElapsedMs, length, err, string(buf[:length])) - } - if readElapsedMs == 0 && length == 0 && err == io.EOF { - // On Linux, hardware port failures come back simply as immediate EOF - err = fmt.Errorf("hardware failure") - } - if readElapsedMs == 0 && length == 0 { - // On Linux, sudden unplug comes back simply as immediate '' - err = fmt.Errorf("hardware unplugged or rebooted probably") - } - if err != nil { - if err == io.EOF { - // Just a read timeout - return data, nil - } - return data, fmt.Errorf("%s %s", err, note.ErrCardIo) - } - - return buf[:length], nil -} - -// Serial trace write function -func serialTraceWrite(context *Context, data []byte) { - context.serialPort.Write(data) -} - -// Add a crc to the JSON transaction -func crcAdd(reqJson []byte, seqno int) []byte { - - // Exit if invalid - if len(reqJson) < 2 { - return reqJson - } - - // Extract any terminator present so it isn't included in the checksum - reqJsonStr := string(reqJson) - terminator := "" - temp := strings.Split(reqJsonStr, "}") - if len(temp) > 1 { - terminator = temp[len(temp)-1] - reqJsonStr = strings.Join(temp[0:len(temp)-1], "}") + "}" - } - - // Compute the CRC of the JSON - crc := crc32.ChecksumIEEE([]byte(reqJsonStr)) - - // Strip the suffix and prepare for crc concatenation. Note that - // the decode side assumes that either a space or comma was added - reqJsonStr = strings.TrimSuffix(reqJsonStr, "}") - if !strings.Contains(reqJsonStr, ":") { - reqJsonStr += " " - } else { - reqJsonStr += "," - } - - // Append the CRC - reqJsonStr += fmt.Sprintf("\"crc\":\"%04X:%08X\"}", seqno, crc) - - // Done - return []byte(reqJsonStr + terminator) -} - -// Test and remove CRC from transaction JSON -// Note that if a CRC field is not present in the JSON, it is considered -// a valid transaction because old Notecards do not have the code -// with which to calculate and piggyback a CRC field. Note that the -// CRC is stripped from the input JSON regardless of whether or not -// there was an error. -func crcError(rspJson []byte, shouldBeSeqno int) (retJson []byte, err error) { - - // Exit silently if invalid - if len(rspJson) < 2 { - return rspJson, nil - } - - // Extract any terminator present so it isn't included in the checksum - rspJsonStr := string(rspJson) - terminator := "" - temp := strings.Split(rspJsonStr, "}") - if len(temp) > 1 { - terminator = temp[len(temp)-1] - rspJsonStr = strings.Join(temp[0:len(temp)-1], "}") + "}" - } - - // Minimum valid JSON is "{}" (2 bytes) and must end with a closing "}". If - // it's not there, it's ok because it could be an old notecard - crcFieldLength := 22 // ,"crc":"SSSS:CCCCCCCC" - if len(rspJsonStr) < crcFieldLength+2 || !strings.HasSuffix(rspJsonStr, "}") { - return rspJson, nil - } - - // Split the string into its json and non-json components - t1 := strings.Split(rspJsonStr, " \"crc\":\"") - if len(t1) != 2 { - t1 = strings.Split(rspJsonStr, ",\"crc\":\"") - } - if len(t1) != 2 { - return rspJson, nil - } - stripped := t1[0] + "}" - t2 := strings.Split(t1[1], ":") - if len(t2) != 2 { - return rspJson, fmt.Errorf("badly formatted CRC seqno") - } - seqnoHex := t2[0] - seqno, err := strconv.ParseInt(seqnoHex, 16, 64) - if err != nil { - return rspJson, fmt.Errorf("badly formatted hex CRC seqno") - } - shouldBeCrcHex := strings.TrimSuffix(t2[1], "\"}") - shouldBeCrc, err := strconv.ParseInt(shouldBeCrcHex, 16, 64) - if err != nil { - return rspJson, fmt.Errorf("badly formatted hex CRC") - } - - // Compute the CRC of the JSON - crc := crc32.ChecksumIEEE([]byte(stripped)) - - // Test values - if shouldBeSeqno != int(seqno) { - return rspJson, fmt.Errorf("sequence number mismatch (%d != %d)", seqno, shouldBeSeqno) - } - if uint32(shouldBeCrc) != crc { - return rspJson, fmt.Errorf("CRC mismatch") - } - - // Done - return []byte(stripped + terminator), nil -} diff --git a/note-go/notecard/play.go b/note-go/notecard/play.go deleted file mode 100644 index 87767df..0000000 --- a/note-go/notecard/play.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -import ( - "bufio" - "fmt" - "os" - "strings" - "sync" - "time" - - "github.com/blues/note-go/note" -) - -// Interactive I/O -var ( - iInputHandlerActive = false - iWatch = false - uiLock sync.RWMutex -) - -// Interactive enters interactive request/response mode, disabling trace in case -// that was the last mode entered -func (context *Context) Interactive(watch bool, watchLevel int, prompt bool, watchCommand string, quitCommand string) (err error) { - var rsp Request - var colWidth int - var cols int - var subsystem []string - var subsystemDisplayName []string - - // Set the watch on/off based upon whether there is a command - iWatch = watch - - // Initialize for watching - linesDisplayed := 0 - - // Get the template for the trace log results. We need to get this regardless of whether watch - // is initially on because it might be turned on later - rsp, err = context.TransactionRequest(Request{Req: "note.get", NotefileID: "_synclog.qi", Start: true}) - if err != nil { - return - } - for _, entry := range strings.Split(rsp.Status, ",") { - str := strings.Split(entry, ":") - if len(str) >= 2 { - cols++ - subsystem = append(subsystem, str[0]) - subsystemDisplayName = append(subsystemDisplayName, str[1]) - if len(str[1]) > colWidth { - colWidth = len(str[1]) - } - } - } - colWidth += 4 - - if iWatch { - - // Print an opening banner if necessary - now := time.Now().Local().Format("03:04:05 PM MST") - rsp, err = context.TransactionRequest(Request{Req: "note.get", NotefileID: "_synclog.qi"}) - if err == nil && rsp.Body == nil { - fmt.Printf("%s waiting for sync activity\n", now) - } - - } - - // Now that we know we can speak to the notecard, spawn the input handlers - if !iInputHandlerActive { - go interactiveInputHandler(context, prompt, watchCommand, quitCommand) - for !iInputHandlerActive { - time.Sleep(100 * time.Millisecond) - } - } - - // Loop, printing data - prevTimeSecs := int64(0) - for err == nil || note.ErrorContains(err, note.ErrNoteNoExist) { - - // Exit if the handler exited - if !iInputHandlerActive { - err = nil - break - } - - // Loop if not watching - if !iWatch { - time.Sleep(500 * time.Millisecond) - continue - } - - // Get the next entry - rsp, err = context.TransactionRequest(Request{Req: "note.get", NotefileID: "_synclog.qi", Delete: true}) - if err != nil { - if !note.ErrorContains(err, note.ErrNoteNoExist) { - uiLock.Lock() - fmt.Printf("\r%s\n", err) - uiLock.Unlock() - } - time.Sleep(1000 * time.Millisecond) - continue - } - if rsp.Body == nil { - time.Sleep(1000 * time.Millisecond) - continue - } - var bodyJSON []byte - bodyJSON, err = note.ObjectToJSON(rsp.Body) - if err != nil { - break - } - var body SyncLogBody - err = note.JSONUnmarshal(bodyJSON, &body) - if err != nil { - break - } - if body.DetailLevel > uint32(watchLevel) { - continue - } - - // Lock output for a moment - uiLock.Lock() - - // Output a header if it will help readability - if linesDisplayed%250 == 0 { - fmt.Printf("\n%s ", strings.Repeat(" ", len(time.Now().Local().Format("03:04:05 PM MST")))) - for i := 0; i < cols; i++ { - fmt.Printf("%s%s", - subsystemDisplayName[i], - strings.Repeat(" ", colWidth-len(subsystemDisplayName[i]))) - } - fmt.Printf("\n\n") - } else { - // Output a spacer if there is a distance in time - if body.TimeSecs != 0 && body.TimeSecs > prevTimeSecs+30 { - fmt.Printf("\n") - } - } - linesDisplayed++ - - // Display either the time OR the 'secs since boot' if time isn't available - prevTimeSecs = body.TimeSecs - timebuf := time.Unix(int64(body.TimeSecs), 0).Local().Format("03:04:05 PM MST") - if body.TimeSecs == 0 { - str := fmt.Sprintf("%d", body.BootMs) - timebuf = fmt.Sprintf("%s%s", str, strings.Repeat(" ", len(timebuf)-len(str))) - } - - // Display indentation - fmt.Printf("\r%s ", timebuf) - indentstr := "." + strings.Repeat(" ", colWidth-1) - for _, ss := range subsystem { - if ss == body.Subsystem { - break - } - fmt.Printf("%s", indentstr) - } - - // Display the message - if watchLevel < SyncLogLevelProg { - fmt.Printf("%s\n", note.ErrorClean(fmt.Errorf(body.Text))) - } else { - fmt.Printf("%s\n", body.Text) - } - - // Release the UI - uiLock.Unlock() - - } - - // Done - iInputHandlerActive = false - return -} - -// Watch for console input -func interactiveInputHandler(context *Context, prompt bool, watchCommand string, quitCommand string) { - // Mark as active, in case we invoke this multiple times - iInputHandlerActive = true - - // Create a scanner to watch stdin - scanner := bufio.NewScanner(os.Stdin) - var message string - - // Send the command to the module - for iInputHandlerActive { - if prompt { - uiLock.Lock() - fmt.Printf("> ") - uiLock.Unlock() - } - scanner.Scan() - message = scanner.Text() - if message == quitCommand { - iInputHandlerActive = false - break - } - if message == "" { - continue - } - uiLock.Lock() - if watchCommand != "" && message == watchCommand { - if iWatch { - iWatch = false - fmt.Printf("watch off\n") - } else { - iWatch = true - fmt.Printf("watch ON\n") - } - uiLock.Unlock() - continue - } - rspJSON, err := context.TransactionJSON([]byte(message)) - if err != nil { - fmt.Printf("error: %s\n", err) - } else { - fmt.Printf("%s", rspJSON) - } - uiLock.Unlock() - } -} diff --git a/note-go/notecard/request.go b/note-go/notecard/request.go deleted file mode 100644 index 1515aa2..0000000 --- a/note-go/notecard/request.go +++ /dev/null @@ -1,536 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -import ( - "errors" - - "github.com/blues/note-go/note" -) - -// Request Types (L suffix means Legacy as of 2019-11-18, and can be removed after we ship) - -// ReqFileAdd (golint) -const ReqFileAdd = "file.add" - -// ReqFileSet (golint) -const ReqFileSet = "file.set" - -// ReqFileDelete (golint) -const ReqFileDelete = "file.delete" - -// ReqFileClear (golint) -const ReqFileClear = "file.clear" - -// ReqFileGetL (golint) -const ReqFileGetL = "file.get" - -// ReqFileChanges (golint) -const ReqFileChanges = "file.changes" - -// ReqFileChangesPending (golint) -const ReqFileChangesPending = "file.changes.pending" - -// ReqFileSync (golint) -const ReqFileSync = "file.sync" - -// ReqFileStats (golint) -const ReqFileStats = "file.stats" - -// ReqNotesGetL (golint) -const ReqNotesGetL = "notes.get" - -// ReqNoteChanges (golint) -const ReqNoteChanges = "note.changes" - -// ReqNoteAdd (golint) -const ReqNoteAdd = "note.add" - -// ReqNoteTemplate (golint) -const ReqNoteTemplate = "note.template" - -// ReqNoteGet (golint) -const ReqNoteGet = "note.get" - -// ReqNoteUpdate (golint) -const ReqNoteUpdate = "note.update" - -// ReqNoteDelete (golint) -const ReqNoteDelete = "note.delete" - -// ReqNoteEncrypt (golint) -const ReqNoteEncrypt = "note.encrypt" - -// ReqNoteDecrypt (golint) -const ReqNoteDecrypt = "note.decrypt" - -// ReqCardTime (golint) -const ReqCardTime = "card.time" - -// ReqCardRandom (golint) -const ReqCardRandom = "card.random" - -// ReqCardSleep (golint) -const ReqCardSleep = "card.sleep" - -// ReqCardContact (golint) -const ReqCardContact = "card.contact" - -// ReqCardAttn (golint) -const ReqCardAttn = "card.attn" - -// ReqCardStatus (golint) -const ReqCardStatus = "card.status" - -// ReqCardRestart (golint) -const ReqCardRestart = "card.restart" - -// ReqCardCheckpoint (golint) -const ReqCardCheckpoint = "card.checkpoint" - -// ReqCardRestore (golint) -const ReqCardRestore = "card.restore" - -// ReqCardLocation (golint) -const ReqCardLocation = "card.location" - -// ReqCardLocationMode (golint) -const ReqCardLocationMode = "card.location.mode" - -// ReqCardLocationTrack (golint) -const ReqCardLocationTrack = "card.location.track" - -// ReqCardTriangulate (golint) -const ReqCardTriangulate = "card.triangulate" - -// ReqCardTemp (golint) -const ReqCardTemp = "card.temp" - -// ReqCardIllumination (golint) -const ReqCardIllumination = "card.illumination" - -// ReqCardVoltage (golint) -const ReqCardVoltage = "card.voltage" - -// ReqCardPower (golint) -const ReqCardPower = "card.power" - -// ReqCardMotion (golint) -const ReqCardMotion = "card.motion" - -// ReqCardMotionMode (golint) -const ReqCardMotionMode = "card.motion.mode" - -// ReqCardMotionSync (golint) -const ReqCardMotionSync = "card.motion.sync" - -// ReqCardMotionTrack (golint) -const ReqCardMotionTrack = "card.motion.track" - -// ReqCardIO (golint) -const ReqCardIO = "card.io" - -// ReqCardAUX (golint) -const ReqCardAUX = "card.aux" - -// ReqCardAUXSerial (golint) -const ReqCardAUXSerial = "card.aux.serial" - -// ReqCardMonitor (golint) -const ReqCardMonitor = "card.monitor" - -// ReqCardCarrier (golint) -const ReqCardCarrier = "card.carrier" - -// ReqCardTrace (golint) -const ReqCardTrace = "card.trace" - -// ReqCardUsageGet (golint) -const ReqCardUsageGet = "card.usage.get" - -// ReqCardUsageTest (golint) -const ReqCardUsageTest = "card.usage.test" - -// ReqEnvModified (golint) -const ReqEnvModified = "env.modified" - -// ReqEnvGet (golint) -const ReqEnvGet = "env.get" - -// ReqEnvSet (golint) -const ReqEnvSet = "env.set" - -// ReqVarSet (golint) -const ReqVarSet = "var.set" - -// ReqVarGet (golint) -const ReqVarGet = "var.get" - -// ReqVarDelete (golint) -const ReqVarDelete = "var.delete" - -// ReqEnvTemplate (golint) -const ReqEnvTemplate = "env.template" - -// ReqEnvDefault (golint) -const ReqEnvDefault = "env.default" - -// ReqEnvTime (golint) -const ReqEnvTime = "env.time" - -// ReqEnvLocation (golint) -const ReqEnvLocation = "env.location" - -// ReqEnvSync (golint) -const ReqEnvSync = "env.sync" - -// ReqWeb (golint) -const ReqWeb = "web" - -// ReqCardBinary (golint) -const ReqCardBinary = "card.binary" - -// ReqCardBinaryGet (golint) -const ReqCardBinaryGet = "card.binary.get" - -// ReqCardBinaryPut (golint) -const ReqCardBinaryPut = "card.binary.put" - -// ReqWebGet (golint) -const ReqWebGet = "web.get" - -// ReqWebPut (golint) -const ReqWebPut = "web.put" - -// ReqWebPost (golint) -const ReqWebPost = "web.post" - -// ReqWebDelete (golint) -const ReqWebDelete = "web.delete" - -// ReqDFUStatus (golint) -const ReqDFUStatus = "dfu.status" - -// ReqDFUGet (golint) -const ReqDFUGet = "dfu.get" - -// ReqDFUPut (golint) -const ReqDFUPut = "dfu.put" - -// ReqCardDFU (golint) -const ReqCardDFU = "card.dfu" - -// ReqEnvVersion (golint) -const ReqEnvVersion = "env.version" - -// ReqCardVersion (golint) -const ReqCardVersion = "card.version" - -// ReqCardBootloader (golint) -const ReqCardBootloader = "card.bootloader" - -// ReqCardTest (golint) -const ReqCardTest = "card.test" - -// ReqCardSetup (golint) -const ReqCardSetup = "card.setup" - -// ReqCardWireless (golint) -const ReqCardWireless = "card.wireless" - -// ReqCardTransport (golint) -const ReqCardTransport = "card.transport" - -// ReqCardWirelessPenalty (golint) -const ReqCardWirelessPenalty = "card.wireless.penalty" - -// ReqCardWirelessSignal (golint) -const ReqCardWirelessSignal = "card.wireless.signal" - -// ReqCardWiFi (golint) -const ReqCardWiFi = "card.wifi" - -// ReqCardLog (golint) -const ReqCardLog = "card.log" - -// ReqHubSync (golint) -const ReqHubSync = "hub.sync" - -// ReqHubSyncL (golint) -const ReqHubSyncL = "service.sync" - -// ReqHubLog (golint) -const ReqHubLog = "hub.log" - -// ReqHubLogL (golint) -const ReqHubLogL = "service.log" - -// ReqHubEnvL (golint) -const ReqHubEnvL = "hub.env" - -// ReqHubEnvLL (golint) -const ReqHubEnvLL = "service.env" - -// ReqHubSet (golint) -const ReqHubSet = "hub.set" - -// ReqHubSetL (golint) -const ReqHubSetL = "service.set" - -// ReqHubGet (golint) -const ReqHubGet = "hub.get" - -// ReqHubGetL (golint) -const ReqHubGetL = "service.get" - -// ReqHubStatus (golint) -const ReqHubStatus = "hub.status" - -// ReqHubStatusL (golint) -const ReqHubStatusL = "service.status" - -// ReqHubSignal (golint) -const ReqHubSignal = "hub.signal" - -// ReqHubSignalL (golint) -const ReqHubSignalL = "service.signal" - -// ReqHubSyncStatus (golint) -const ReqHubSyncStatus = "hub.sync.status" - -// ReqHubSyncStatusL (golint) -const ReqHubSyncStatusL = "service.sync.status" - -// ReqHubDFUGet (golint) -const ReqHubDFUGet = "hub.dfu.get" - -// ReqHubHubDFUGetL (golint) -const ReqHubDFUGetL = "dfu.service.get" - -// ReqHubFileGet (golint) -const ReqHubFileGet = "hub.file.get" - -// Request is the core API request/response data structure -type Request struct { - Req string `json:"req,omitempty"` - Cmd string `json:"cmd,omitempty"` - Err string `json:"err,omitempty"` - RequestID uint32 `json:"id,omitempty"` - Transport string `json:"transport,omitempty"` - NotefileID string `json:"file,omitempty"` - TrackerID string `json:"tracker,omitempty"` - NoteID string `json:"note,omitempty"` - Body *map[string]interface{} `json:"body,omitempty"` - Payload *[]byte `json:"payload,omitempty"` - Deleted bool `json:"deleted,omitempty"` - Start bool `json:"start,omitempty"` - Stop bool `json:"stop,omitempty"` - Delete bool `json:"delete,omitempty"` - USB bool `json:"usb,omitempty"` - Primary bool `json:"primary,omitempty"` - Edge bool `json:"edge,omitempty"` - Connected bool `json:"connected,omitempty"` - Secure bool `json:"secure,omitempty"` - Unsecure bool `json:"unsecure,omitempty"` - Alert bool `json:"alert,omitempty"` - Retry bool `json:"retry,omitempty"` - Signals int32 `json:"signals,omitempty"` - Max int32 `json:"max,omitempty"` - Changes int32 `json:"changes,omitempty"` - Seconds int32 `json:"seconds,omitempty"` - SecondsV string `json:"vseconds,omitempty"` - Minutes int32 `json:"minutes,omitempty"` - MinutesV string `json:"vminutes,omitempty"` - Hours int32 `json:"hours,omitempty"` - HoursV string `json:"vhours,omitempty"` - Days int32 `json:"days,omitempty"` - Result int32 `json:"result,omitempty"` - I2C int32 `json:"i2c,omitempty"` - Status string `json:"status,omitempty"` - Version string `json:"version,omitempty"` - Name string `json:"name,omitempty"` - Label string `json:"label,omitempty"` - Org string `json:"org,omitempty"` - Role string `json:"role,omitempty"` - Email string `json:"email,omitempty"` - Area string `json:"area,omitempty"` - Country string `json:"country,omitempty"` - Zone string `json:"zone,omitempty"` - Mode string `json:"mode,omitempty"` - Host string `json:"host,omitempty"` - Movements string `json:"movements,omitempty"` - ProductUID string `json:"product,omitempty"` - DeviceUID string `json:"device,omitempty"` - RouteUID string `json:"route,omitempty"` - Files *[]string `json:"files,omitempty"` - Names *[]string `json:"names,omitempty"` - FileInfo *map[string]note.NotefileInfo `json:"info,omitempty"` - FileDesc *[]note.NotefileDesc `json:"desc,omitempty"` - Notes *map[string]note.Info `json:"notes,omitempty"` - Pad int32 `json:"pad,omitempty"` - Storage int32 `json:"storage,omitempty"` - LocationOLC string `json:"olc,omitempty"` - Latitude float64 `json:"lat,omitempty"` - Longitude float64 `json:"lon,omitempty"` - LocationTime int64 `json:"ltime,omitempty"` - Value float64 `json:"value,omitempty"` - ValueV string `json:"vvalue,omitempty"` - SN string `json:"sn,omitempty"` - APN string `json:"apn,omitempty"` - Text string `json:"text,omitempty"` - Base int32 `json:"base,omitempty"` - Offset int32 `json:"offset,omitempty"` - Length int32 `json:"length,omitempty"` - Total int32 `json:"total,omitempty"` - BytesSent uint32 `json:"bytes_sent,omitempty"` - BytesReceived uint32 `json:"bytes_received,omitempty"` - BytesSentSecondary uint32 `json:"bytes_sent_secondary,omitempty"` - BytesReceivedSecondary uint32 `json:"bytes_received_secondary,omitempty"` - NotesSent uint32 `json:"notes_sent,omitempty"` - NotesReceived uint32 `json:"notes_received,omitempty"` - SessionsStandard uint32 `json:"sessions_standard,omitempty"` - SessionsSecure uint32 `json:"sessions_secure,omitempty"` - Megabytes uint32 `json:"megabytes,omitempty"` - BytesPerDay int32 `json:"bytes_per_day,omitempty"` - DataRate float64 `json:"rate,omitempty"` - NumBytes int32 `json:"bytes,omitempty"` - Template bool `json:"template,omitempty"` - Allow bool `json:"allow,omitempty"` - Align bool `json:"align,omitempty"` - Limit bool `json:"limit,omitempty"` - Pending bool `json:"pending,omitempty"` - Charging bool `json:"charging,omitempty"` - On bool `json:"on,omitempty"` - Off bool `json:"off,omitempty"` - ReqTime bool `json:"reqtime,omitempty"` - ReqLoc bool `json:"reqloc,omitempty"` - Trace string `json:"trace,omitempty"` - Usage *[]string `json:"usage,omitempty"` - State *[]PinState `json:"state,omitempty"` - Time int64 `json:"time,omitempty"` // Time is defined as uint32 on Notecard and int64 on Notehub. See the rationale below. - Motion uint32 `json:"motion,omitempty"` - VMin float64 `json:"vmin,omitempty"` - VMax float64 `json:"vmax,omitempty"` - VAvg float64 `json:"vavg,omitempty"` - Daily float64 `json:"daily,omitempty"` - Weekly float64 `json:"weekly,omitempty"` - Montly float64 `json:"monthly,omitempty"` - Verify bool `json:"verify,omitempty"` - Confirm bool `json:"confirm,omitempty"` - Port int32 `json:"port,omitempty"` - Set bool `json:"set,omitempty"` - Reset bool `json:"reset,omitempty"` - Flag bool `json:"flag,omitempty"` - Calibration float64 `json:"calibration,omitempty"` - Heartbeat bool `json:"heartbeat,omitempty"` - Threshold int32 `json:"threshold,omitempty"` - Count uint32 `json:"count,omitempty"` - Sync bool `json:"sync,omitempty"` - Live bool `json:"live,omitempty"` - Now bool `json:"now,omitempty"` - Type int32 `json:"type,omitempty"` - Number int64 `json:"number,omitempty"` - SKU string `json:"sku,omitempty"` - OrderingCode string `json:"ordering_code,omitempty"` - Board string `json:"board,omitempty"` - Net *NetInfo `json:"net,omitempty"` - Sensitivity int32 `json:"sensitivity,omitempty"` - Requested int32 `json:"requested,omitempty"` - Completed int32 `json:"completed,omitempty"` - WiFi bool `json:"wifi,omitempty"` - Cell bool `json:"cell,omitempty"` - GPS bool `json:"gps,omitempty"` - LoRa bool `json:"lora,omitempty"` - NTN bool `json:"ntn,omitempty"` - Inbound int32 `json:"inbound,omitempty"` - InboundV string `json:"vinbound,omitempty"` - Outbound int32 `json:"outbound,omitempty"` - OutboundV string `json:"voutbound,omitempty"` - Duration int32 `json:"duration,omitempty"` - Temperature float64 `json:"temperature,omitempty"` - Pressure float64 `json:"pressure,omitempty"` - Humidity float64 `json:"humidity,omitempty"` - MinVersion string `json:"minver,omitempty"` - SSID string `json:"ssid,omitempty"` - Password string `json:"password,omitempty"` - Security string `json:"security,omitempty"` - Key string `json:"key,omitempty"` - Method string `json:"method,omitempty"` - Content string `json:"content,omitempty"` - Min int32 `json:"min,omitempty"` - Add int32 `json:"add,omitempty"` - Encrypt bool `json:"encrypt,omitempty"` - Decrypt bool `json:"decrypt,omitempty"` - Alt bool `json:"alt,omitempty"` - Scan bool `json:"scan,omitempty"` - Journey bool `json:"journey,omitempty"` - UOff bool `json:"uoff,omitempty"` - UMin bool `json:"umin,omitempty"` - UPeriodic bool `json:"uperiodic,omitempty"` - Milliseconds int32 `json:"ms,omitempty"` - Full bool `json:"full,omitempty"` - Async bool `json:"async,omitempty"` - Binary bool `json:"binary,omitempty"` - Cobs int32 `json:"cobs,omitempty"` - Append bool `json:"append,omitempty"` - Details *map[string]interface{} `json:"details,omitempty"` - Tower *note.TowerLocation `json:"tower,omitempty"` - Change float64 `json:"change,omitempty"` - Format string `json:"format,omitempty"` - Voltage float64 `json:"voltage,omitempty"` - MilliampHours float64 `json:"milliamp_hours,omitempty"` - Default bool `json:"default,omitempty"` - In bool `json:"in,omitempty"` -} - -func (req *Request) Error() error { - if req.Err != "" { - return errors.New(req.Err) - } - return nil -} - -// A Note on Time -// The Notecard protocol communicates the Time value as a uint32. However, this is non-standard and problematic for Notehub which would have to -// constantly cast it to the modern Unix standard of int64 (i.e., the time_t type in Posix C libraries.) -// In older OSes, Unix time (time_t) was int32. It was never defined as a uint32. -// Thus by converting the uint32 to int64, it may allow us to delay the 2038 problem to 2106. - -// PinState describes the state of an AUX pin for hardware-related Notecard requests -type PinState struct { - High bool `json:"high,omitempty"` - Low bool `json:"low,omitempty"` - Count []uint32 `json:"count,omitempty"` -} - -// SyncLogLevelMajor is just major events -const SyncLogLevelMajor = 0 - -// SyncLogLevelMinor is just major and minor events -const SyncLogLevelMinor = 1 - -// SyncLogLevelDetail is major, minor, and detailed events -const SyncLogLevelDetail = 2 - -// SyncLogLevelProg is everything plus programmatically-targeted -const SyncLogLevelProg = 3 - -// SyncLogLevelAll is all events -const SyncLogLevelAll = SyncLogLevelProg - -// SyncLogLevelNone is no events -const SyncLogLevelNone = -1 - -// SyncLogNotefile is the special notefile containing sync log info -const SyncLogNotefile = "_synclog.qi" - -// SyncLogBody is the data structure used in the SyncLogNotefile -type SyncLogBody struct { - TimeSecs int64 `json:"time,omitempty"` - BootMs int64 `json:"sequence,omitempty"` - DetailLevel uint32 `json:"level,omitempty"` - Subsystem string `json:"subsystem,omitempty"` - Text string `json:"text,omitempty"` -} diff --git a/note-go/notecard/serial-default.go b/note-go/notecard/serial-default.go deleted file mode 100644 index 4690c47..0000000 --- a/note-go/notecard/serial-default.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -import ( - "strings" - - "go.bug.st/serial/enumerator" -) - -// Notecard's USB VID/PID -const ( - bluesincVID = "30A4" - notecardPID = "0001" -) - -// Get the default serial device -func defaultSerialDefault() (device string, speed int) { - // Enum all ports - speed = 115200 - ports, err2 := enumerator.GetDetailedPortsList() - if err2 != nil { - return - } - if len(ports) == 0 { - return - } - - // First, look for the notecard - for _, port := range ports { - if port.IsUSB { - if strings.EqualFold(port.VID, bluesincVID) && strings.EqualFold(port.PID, notecardPID) { - device = port.Name - return - } - } - } - - // Otherwise, look for anything from Blues - for _, port := range ports { - if port.IsUSB && strings.EqualFold(port.VID, bluesincVID) { - device = port.Name - return - } - } - - // Not found - return -} - -// Set or display the serial port -func defaultSerialPortEnum() (allports []string, usbports []string, notecardports []string, err error) { - // Enum all ports - ports, err2 := enumerator.GetDetailedPortsList() - if err2 != nil { - err = err2 - return - } - if len(ports) == 0 { - return - } - - // First, look for the notecard - for _, port := range ports { - allports = append(allports, port.Name) - if port.IsUSB { - usbports = append(usbports, port.Name) - if strings.EqualFold(port.VID, bluesincVID) && strings.EqualFold(port.PID, notecardPID) { - notecardports = append(notecardports, port.Name) - } - } - } - - // Otherwise, look for anything from Blues - if len(notecardports) == 0 { - for _, port := range ports { - if port.IsUSB && strings.EqualFold(port.VID, bluesincVID) { - notecardports = append(notecardports, port.Name) - } - } - } - - // Done - return -} diff --git a/note-go/notecard/serial-unix.go b/note-go/notecard/serial-unix.go deleted file mode 100644 index c1c7de7..0000000 --- a/note-go/notecard/serial-unix.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -//go:build !windows - -package notecard - -// Get the default serial device -func serialDefault() (device string, speed int) { - return defaultSerialDefault() -} - -// Set or display the serial port -func serialPortEnum() (allports []string, usbports []string, notecardports []string, err error) { - return defaultSerialPortEnum() -} diff --git a/note-go/notecard/serial-windows.go b/note-go/notecard/serial-windows.go deleted file mode 100644 index 770871c..0000000 --- a/note-go/notecard/serial-windows.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -//go:build windows - -// If you have odd serial port behavior (where responses are apparently lost or delayed), try this: -// 1) open Control Panel -> Device Manager -> Ports (COM & LPT) -// 2) right-click for USB Serial Device Properties on the appropriate port -// 3) Port Settings tab -// 4) Click Advanced... button -// 5) UN-CHECK "Use FIFO buffers" - -package notecard - -// Get the default serial device -func serialDefault() (device string, speed int) { - return defaultSerialDefault() -} - -// Set or display the serial port -func serialPortEnum() (allports []string, usbports []string, notecardports []string, err error) { - return defaultSerialPortEnum() -} diff --git a/note-go/notecard/test.go b/note-go/notecard/test.go deleted file mode 100644 index d2a4f61..0000000 --- a/note-go/notecard/test.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -// CardTest is a structure that is returned by the notecard after completing its self-test -type CardTest struct { - DeviceUID string `json:"device,omitempty"` - Error string `json:"err,omitempty"` - Status string `json:"status,omitempty"` - Tests string `json:"tests,omitempty"` - FailTest string `json:"fail_test,omitempty"` - FailReason string `json:"fail_reason,omitempty"` - Info string `json:"info,omitempty"` - BoardVersion uint32 `json:"board,omitempty"` - BoardType uint32 `json:"board_type,omitempty"` - Modem string `json:"modem,omitempty"` - ICCID string `json:"iccid,omitempty"` - ICCID2 string `json:"iccid2,omitempty"` - IMSI string `json:"imsi,omitempty"` - IMSI2 string `json:"imsi2,omitempty"` - IMEI string `json:"imei,omitempty"` - Apn string `json:"apn,omitempty"` - Band string `json:"band,omitempty"` - Channel string `json:"channel,omitempty"` - When uint32 `json:"when,omitempty"` - SKU string `json:"sku,omitempty"` - OrderingCode string `json:"ordering_code,omitempty"` - DefaultProductUID string `json:"default_product,omitempty"` - SIMActivationKey string `json:"key,omitempty"` - SIMless bool `json:"simless,omitempty"` - Station string `json:"station,omitempty"` - Operator string `json:"operator,omitempty"` - Check uint32 `json:"check,omitempty"` - CellUsageBytes uint32 `json:"cell_used,omitempty"` - CellProvisionedTime uint32 `json:"cell_provisioned,omitempty"` - // Firmware info - FirmwareOrg string `json:"org,omitempty"` - FirmwareProduct string `json:"product,omitempty"` - FirmwareVersion string `json:"version,omitempty"` - FirmwareMajor uint32 `json:"ver_major,omitempty"` - FirmwareMinor uint32 `json:"ver_minor,omitempty"` - FirmwarePatch uint32 `json:"ver_patch,omitempty"` - FirmwareBuild uint32 `json:"ver_build,omitempty"` - FirmwareBuilt string `json:"built,omitempty"` - // Certificate and cert info - CertSN string `json:"certsn,omitempty"` - Cert string `json:"cert,omitempty"` - // Card initialization requests - SetupRequests string `json:"setup,omitempty"` - // Detailed information about LSE stability - LSEStability string `json:"lse,omitempty"` - // LoRa notecard provisioning info - DevEui string `json:"deveui,omitempty"` - AppEui string `json:"appeui,omitempty"` - AppKey string `json:"appkey,omitempty"` - FreqPlan string `json:"freqplan,omitempty"` - LWVersion string `json:"lorawan,omitempty"` - PHVersion string `json:"regional,omitempty"` - // For manufacturing - CPN string `json:"cpn,omitempty"` - // For Iridium - IriSku string `json:"iri_sku,omitempty"` - IriSn string `json:"iri_sn,omitempty"` - IriImei string `json:"iri_imei,omitempty"` - IriIccid string `json:"iri_iccid,omitempty"` - // For Starnote - Hardware string `json:"hardware,omitempty"` - Mtu uint16 `json:"mtu,omitempty"` - DownMtu uint16 `json:"down_mtu,omitempty"` - UpMtu uint16 `json:"up_mtu,omitempty"` - Policy string `json:"policy,omitempty"` - Cid uint32 `json:"cid,omitempty"` -} - -// Remove fields that are not useful or are sensitive when externalizing for public consumption -func CardTestExternalized(ct CardTest) CardTest { - ct.BoardVersion = 0 // distracting - ct.BoardType = 0 // distracting - ct.SIMActivationKey = "" // security - ct.Station = "" // privacy - ct.Operator = "" // privacy - ct.Check = 0 // invalid after externalizing - ct.FirmwareOrg = "" // distracting - ct.FirmwareProduct = "" // distracting - ct.FirmwareMajor = 0 // distracting - ct.FirmwareMinor = 0 // distracting - ct.FirmwarePatch = 0 // distracting - ct.FirmwareBuild = 0 // distracting - ct.FirmwareBuilt = "" // distracting - ct.CertSN = "" // security - ct.Cert = "" // security - ct.LSEStability = "" // distracting - ct.SetupRequests = "" // security - ct.LSEStability = "" // distracting - return ct -} diff --git a/note-go/notecard/trace.go b/note-go/notecard/trace.go deleted file mode 100644 index 24ffe36..0000000 --- a/note-go/notecard/trace.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -import ( - "bufio" - "fmt" - "os" - "strings" - "time" -) - -// The time when the last read began -var ( - readBeganMs = 0 - inputHandlerActive = false -) - -// Trace the incoming serial output AND connect the input handler -func (context *Context) Trace() (err error) { - - // Tracing only works for USB and AUX ports - if context.traceOpenFn == nil { - return fmt.Errorf("tracing is not available on this port") - } - - // Ensure that we have a reservation - err = context.ReopenIfRequired(context.portConfig) - if err != nil { - return err - } - - // Open the trace port - err = context.traceOpenFn(context) - if err != nil { - cardReportError(context, err) - return - } - - // Spawn the input handler - if !inputHandlerActive { - go inputHandler(context) - } - - // Loop, echoing to the console - for { - - buf, err := context.traceReadFn(context) - if err != nil { - cardReportError(context, err) - time.Sleep(2 * time.Second) - continue - } - - if len(buf) > 0 { - fmt.Printf("%s", buf) - } - - } - -} - -// Watch for console input -func inputHandler(context *Context) { - // Mark as active, in case we invoke this multiple times - inputHandlerActive = true - - // Create a scanner to watch stdin - scanner := bufio.NewScanner(os.Stdin) - var message string - - for { - - scanner.Scan() - message = scanner.Text() - - if strings.HasPrefix(message, "^") { - if !context.portIsOpen { - for _, r := range message[1:] { - switch { - // 'a' - 'z' - case 97 <= r && r <= 122: - ba := make([]byte, 1) - ba[0] = byte(r - 96) - context.traceWriteFn(context, ba) - // 'A' - 'Z' - case 65 <= r && r <= 90: - ba := make([]byte, 1) - ba[0] = byte(r - 64) - context.traceWriteFn(context, ba) - } - } - } - } else { - // Send the command to the module - if !context.portIsOpen { - time.Sleep(250 * time.Millisecond) - } else { - context.traceWriteFn(context, []byte(message)) - context.traceWriteFn(context, []byte("\n")) - } - } - } -} diff --git a/note-go/notecard/ua.go b/note-go/notecard/ua.go deleted file mode 100644 index 20bfeda..0000000 --- a/note-go/notecard/ua.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notecard - -import ( - "fmt" - "runtime" - - "github.com/shirou/gopsutil/v3/cpu" - /* "github.com/shirou/gopsutil/v3/host" // Deprecated */ - "github.com/shirou/gopsutil/v3/mem" -) - -// UserAgent generates a User Agent object for a given interface -func (context *Context) UserAgent() (ua map[string]interface{}) { - ua = map[string]interface{}{} - ua["agent"] = "note-go" - ua["compiler"] = fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH) - - ua["req_interface"] = context.iface - if context.isSerial { - ua["req_port"] = context.serialName - } else { - ua["req_port"] = context.port - } - - m, _ := mem.VirtualMemory() - ua["cpu_mem"] = m.Total - - c, _ := cpu.Info() - if len(c) >= 1 { - ua["cpu_mhz"] = int(c[0].Mhz) - ua["cpu_cores"] = int(c[0].Cores) - ua["cpu_vendor"] = c[0].VendorID - ua["cpu_name"] = c[0].ModelName - } - - /* Deprecated - h, _ := host.Info() - ua["os_name"] = h.OS // freebsd, linux - ua["os_platform"] = h.Platform // ubuntu, linuxmint - ua["os_family"] = h.PlatformFamily // debian, rhel - ua["os_version"] = h.PlatformVersion // complete OS version - */ - - return -} diff --git a/note-go/notehub/api/billing.go b/note-go/notehub/api/billing.go deleted file mode 100644 index bd4f2c2..0000000 --- a/note-go/notehub/api/billing.go +++ /dev/null @@ -1,15 +0,0 @@ -package api - -// GetBillingAccountResponse v1 -// -// The response object for getting a billing account. -type GetBillingAccountResponse struct { - UID string `json:"uid"` - Name string `json:"name"` - // "billing_admin", "billing_manager", or "project_creator" - Role string `json:"role"` -} - -type GetBillingAccountsResponse struct { - BillingAccounts []GetBillingAccountResponse `json:"billing_accounts"` -} diff --git a/note-go/notehub/api/devices.go b/note-go/notehub/api/devices.go deleted file mode 100644 index 8abd504..0000000 --- a/note-go/notehub/api/devices.go +++ /dev/null @@ -1,165 +0,0 @@ -package api - -import ( - "strings" - - "github.com/blues/note-go/note" -) - -// GetDevicesResponse v1 -// -// The response object for getting devices. -type GetDevicesResponse struct { - Devices []GetDeviceResponse `json:"devices"` - HasMore bool `json:"has_more"` -} - -// Part of the response object for a device. -type DeviceHealthLogEntry struct { - When string `json:"when"` - Text string `json:"text"` - Alert bool `json:"alert"` -} - -// DeviceResponse v1 -// -// The response object for a device. -type GetDeviceResponse struct { - UID string `json:"uid"` - SerialNumber string `json:"serial_number,omitempty"` - SKU string `json:"sku,omitempty"` - - // RFC3339 timestamps, in UTC. - Provisioned string `json:"provisioned"` - LastActivity *string `json:"last_activity"` - - FirmwareHost string `json:"firmware_host,omitempty"` - FirmwareNotecard string `json:"firmware_notecard,omitempty"` - - Contact *ContactResponse `json:"contact,omitempty"` - - ProductUID string `json:"product_uid"` - FleetUIDs []string `json:"fleet_uids"` - - TowerInfo *TowerInformation `json:"tower_info,omitempty"` - TowerLocation *Location `json:"tower_location,omitempty"` - GPSLocation *Location `json:"gps_location,omitempty"` - TriangulatedLocation *Location `json:"triangulated_location,omitempty"` - BestLocation *Location `json:"best_location,omitempty"` - - Voltage float64 `json:"voltage"` - Temperature float64 `json:"temperature"` - DFUEnv *note.DFUEnv `json:"dfu,omitempty"` - Disabled bool `json:"disabled,omitempty"` - Tags string `json:"tags,omitempty"` - - // Activity - RecentActivityBase string `json:"recent_event_base,omitempty"` - RecentEventCount []int `json:"recent_event_count,omitempty"` - RecentSessionCount []int `json:"recent_session_count,omitempty"` - RecentSessionSeconds []int `json:"recent_session_seconds,omitempty"` - - // Health - HealthLog []DeviceHealthLogEntry `json:"health_log,omitempty"` -} - -// GetDevicesPublicKeysResponse v1 -// -// The response object for retrieving a collection of devices' public keys -type GetDevicesPublicKeysResponse struct { - DevicePublicKeys []DevicePublicKey `json:"device_public_keys"` - HasMore bool `json:"has_more"` -} - -// DevicePublicKey v1 -// -// A structure representing the public key for a specific device -type DevicePublicKey struct { - UID string `json:"uid"` - PublicKey string `json:"key"` -} - -// ProvisionDeviceRequest v1 -// -// The request object for provisioning a device -type ProvisionDeviceRequest struct { - ProductUID string `json:"product_uid"` - DeviceSN string `json:"device_sn"` - FleetUIDs *[]string `json:"fleet_uids,omitempty"` -} - -// GetDeviceLatestResponse v1 -// -// The response object for retrieving the latest notefile values for a device -type GetDeviceLatestResponse struct { - LatestEvents []note.Event `json:"latest_events"` -} - -// Location v1 -// -// The response object for a location. -type Location struct { - When string `json:"when"` - Name string `json:"name"` - Country string `json:"country"` - Timezone string `json:"timezone"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` -} - -// TowerInformation v1 -// -// The response object for tower information. -type TowerInformation struct { - // Mobile Country Code - MCC int `json:"mcc"` - // Mobile Network Code - MNC int `json:"mnc"` - // Location Area Code - LAC int `json:"lac"` - CellID int `json:"cell_id"` -} - -// GetDeviceHealthLogResponse v1 -// -// The response object for getting a device's health log. -type GetDeviceHealthLogResponse struct { - HealthLog []HealthLogEntry `json:"health_log"` -} - -// HealthLogEntry v1 -// -// The response object for a health log entry. -type HealthLogEntry struct { - When string `json:"when"` - Alert bool `json:"alert"` - Text string `json:"text"` -} - -var allDfuPhases = []note.DfuPhase{ - note.DfuPhaseUnknown, - note.DfuPhaseIdle, - note.DfuPhaseError, - note.DfuPhaseDownloading, - note.DfuPhaseSideloading, - note.DfuPhaseReady, - note.DfuPhaseReadyRetry, - note.DfuPhaseUpdating, - note.DfuPhaseCompleted, -} - -func ParseDfuPhase(phase string) note.DfuPhase { - phase = strings.ToLower(phase) - for _, validPhase := range allDfuPhases { - if phase == string(validPhase) { - return validPhase - } - } - return note.DfuPhaseUnknown -} - -func IsDfuTerminal(phase note.DfuPhase) bool { - return phase == note.DfuPhaseError || - phase == note.DfuPhaseCompleted || - phase == note.DfuPhaseIdle -} diff --git a/note-go/notehub/api/environment_variables.go b/note-go/notehub/api/environment_variables.go deleted file mode 100644 index 484bd09..0000000 --- a/note-go/notehub/api/environment_variables.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package api - -// GetAppEnvironmentVariablesResponse v1 -// -// The response object for getting app environment variables. -type GetAppEnvironmentVariablesResponse struct { - // EnvironmentVariables - // - // The environment variables for this app. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// PutAppEnvironmentVariablesRequest v1 -// -// The request object for setting app environment variables. -type PutAppEnvironmentVariablesRequest struct { - // EnvironmentVariables - // - // The environment variables scoped at the app level - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// PutAppEnvironmentVariablesResponse v1 -// -// The response object for setting app environment variables. -type PutAppEnvironmentVariablesResponse struct { - // EnvironmentVariables - // - // The environment variables for this app. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// DeleteAppEnvironmentVariableResponse v1 -// -// The response object for deleting an app environment variable. -type DeleteAppEnvironmentVariableResponse struct { - // EnvironmentVariables - // - // The environment variables for this app. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// GetFleetEnvironmentVariablesResponse v1 -// -// The response object for getting fleet environment variables. -type GetFleetEnvironmentVariablesResponse struct { - // EnvironmentVariables - // - // The environment variables for this fleet. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// PutFleetEnvironmentVariablesRequest v1 -// -// The request object for setting fleet environment variables. -type PutFleetEnvironmentVariablesRequest struct { - // EnvironmentVariables - // - // The environment variables scoped at the fleet level - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// PutFleetEnvironmentVariablesResponse v1 -// -// The response object for setting fleet environment variables. -type PutFleetEnvironmentVariablesResponse struct { - // EnvironmentVariables - // - // The environment variables for this fleet. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// DeleteFleetEnvironmentVariableResponse v1 -// -// The response object for deleting an fleet environment variable. -type DeleteFleetEnvironmentVariableResponse struct { - // EnvironmentVariables - // - // The environment variables for this fleet. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// GetDeviceEnvironmentVariablesResponse v1 -// -// The response object for getting device environment variables. -type GetDeviceEnvironmentVariablesResponse struct { - // EnvironmentVariables - // - // The environment variables for this device that have been set using host firmware or the Notehub API or UI. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` - - // EnvironmentVariablesEnvDefault - // - // The environment variables that have been set using the env.default request through the Notecard API. - // - // required: true - EnvironmentVariablesEnvDefault map[string]string `json:"environment_variables_env_default"` - - // EnvironmentVariablesEffective - // - // The environment variables for the device as though they were fully resolved by resolution rules - // - // required: true - EnvironmentVariablesEffective map[string]string `json:"environment_variables_effective"` -} - -// PutDeviceEnvironmentVariablesRequest v1 -// -// The request object for setting device environment variables. -type PutDeviceEnvironmentVariablesRequest struct { - // EnvironmentVariables - // - // The environment variables scoped at the device level - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// PutDeviceEnvironmentVariablesResponse v1 -// -// The response object for setting device environment variables. -type PutDeviceEnvironmentVariablesResponse struct { - // EnvironmentVariables - // - // The environment variables for this device that have been set using host firmware or the Notehub API or UI. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// DeleteDeviceEnvironmentVariableResponse v1 -// -// The response object for deleting a device environment variable. -type DeleteDeviceEnvironmentVariableResponse struct { - // EnvironmentVariables - // - // The environment variables for this device that have been set using host firmware or the Notehub API or UI. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// GetDeviceEnvironmentVariablesWithPINResponse v1 -// -// The response object for getting device environment variables with a PIN. -type GetDeviceEnvironmentVariablesWithPINResponse struct { - // EnvironmentVariables - // - // The environment variables for this device that have been set using host firmware or the Notehub API or UI. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` - - // EnvironmentVariablesEnvDefault - // - // The environment variables that have been set using the env.default request through the Notecard API. - // - // required: true - EnvironmentVariablesEnvDefault map[string]string `json:"environment_variables_env_default"` -} - -// PutDeviceEnvironmentVariablesWithPINRequest v1 -// -// The request object for setting device environment variables with a PIN. (The PIN comes in via a header) -type PutDeviceEnvironmentVariablesWithPINRequest struct { - // EnvironmentVariables - // - // The environment variables scoped at the device level - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} - -// PutDeviceEnvironmentVariablesWithPINResponse v1 -// -// The response object for setting device environment variables with a PIN. -type PutDeviceEnvironmentVariablesWithPINResponse struct { - // EnvironmentVariables - // - // The environment variables for this device that have been set using host firmware or the Notehub API or UI. - // - // required: true - EnvironmentVariables map[string]string `json:"environment_variables"` -} diff --git a/note-go/notehub/api/error_defaults.go b/note-go/notehub/api/error_defaults.go deleted file mode 100644 index 18172f6..0000000 --- a/note-go/notehub/api/error_defaults.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package api - -import "net/http" - -// ErrNotFound returns the default for an HTTP 404 NotFound -func ErrNotFound() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusNotFound), - Error: "The requested resource could not be found", - Code: http.StatusNotFound, - } -} - -// ErrUnauthorized returns the default for an HTTP 401 Unauthorized -func ErrUnauthorized() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusUnauthorized), - Error: "The request could not be authorized", - Code: http.StatusUnauthorized, - } -} - -// ErrForbidden returns the default for an HTTP 403 Forbidden -func ErrForbidden() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusForbidden), - Error: "The requested action was forbidden", - Code: http.StatusForbidden, - } -} - -// ErrMethodNotAllowed returns the default for an HTTP 405 Method Not Allowed -func ErrMethodNotAllowed() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusMethodNotAllowed), - Error: "Method not allowed on this endpoint", - Code: http.StatusMethodNotAllowed, - } -} - -// ErrInternalServerError returns the default for an HTTP 500 InternalServerError -func ErrInternalServerError() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusInternalServerError), - Error: "An internal server error occurred", - Code: http.StatusInternalServerError, - } -} - -// ErrEventsQueryTimeout returns the default for a GetEvents (and related) request that took too long -func ErrEventsQueryTimeout() ErrorResponse { - return ErrorResponse{ - Status: "Took too long", - Error: "Events query took too long to complete", - Code: http.StatusGatewayTimeout, - } -} - -// ErrBadRequest returns the default for an HTTP 400 BadRequest -func ErrBadRequest() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusBadRequest), - Error: "The request was malformed or contained invalid parameters", - Code: http.StatusBadRequest, - } -} - -// ErrUnsupportedMediaType returns the default for an HTTP 415 UnsupportedMediaType -func ErrUnsupportedMediaType() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusUnsupportedMediaType), - Error: "The request is using an unknown content type", - Code: http.StatusUnsupportedMediaType, - } -} - -// ErrConflict returns the default for an HTTP 409 Conflict -func ErrConflict() ErrorResponse { - return ErrorResponse{ - Status: http.StatusText(http.StatusConflict), - Error: "The resource could not be created due to a conflict", - Code: http.StatusConflict, - } -} diff --git a/note-go/notehub/api/errors.go b/note-go/notehub/api/errors.go deleted file mode 100644 index 7c36908..0000000 --- a/note-go/notehub/api/errors.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package api - -import ( - "net/http" -) - -// ErrorResponse v1 -// -// The structure returned from HTTPS API calls when there is an error. -type ErrorResponse struct { - // Error represents the human readable error message. - // - // required: true - // type: string - Error string `json:"err"` - - // Code represents the standard status code - // - // required: true - // type: int - Code int `json:"code"` - - // Status is the machine readable string representation of the error code. - // - // required: true - // type: string - Status string `json:"status"` - - // Request is the request that was made that resulted in error. The url path would be sufficient. - // - // required: false - // type: string - Request string `json:"request,omitempty"` - - // Details are any additional information about the request that would be nice to in the response. - // The request body would be nice especially if there are a lot of parameters. - // - // required: false - // type: object - Details map[string]interface{} `json:"details,omitempty"` - - // Debug is any customer-facing information to aid in debugging. - // - // required: false - // type: string - Debug string `json:"debug,omitempty"` -} - -var SuspendedBillingAccountResponse = ErrorResponse{ - Code: http.StatusForbidden, - Status: "Forbidden", - Error: "this billing account is suspended", -} - -// WithRequest is a an easy way to add http.Request information to an error. -// It takes a http.Request object, parses the URI string into response.Request -// and adds the request Body (if it exists) into the response.Details["body"] as a string -func (e ErrorResponse) WithRequest(r *http.Request) ErrorResponse { - e.Request = r.RequestURI - if len(e.Details) == 0 { - e.Details = make(map[string]interface{}) - } - return e -} - -// WithError adds an error string from an error object into the response. -func (e ErrorResponse) WithError(err error) ErrorResponse { - e.Error = err.Error() - return e -} - -// WithDebug adds a debug string onto the error response object -func (e ErrorResponse) WithDebug(msg string) ErrorResponse { - e.Debug = msg - return e -} diff --git a/note-go/notehub/api/events.go b/note-go/notehub/api/events.go deleted file mode 100644 index 79dde42..0000000 --- a/note-go/notehub/api/events.go +++ /dev/null @@ -1,30 +0,0 @@ -package api - -import "github.com/blues/note-go/note" - -// GetEventsResponse v1 -// -// The response object for getting events. -type GetEventsResponse struct { - Events []note.Event `json:"events"` - Through string `json:"through,omitempty"` - HasMore bool `json:"has_more"` -} - -// GetEventsResponseSelectedFields v1 -// -// The response object for getting events with selected fields. -type GetEventsResponseSelectedFields struct { - Events []note.Event `json:"events"` - Through string `json:"through,omitempty"` - HasMore bool `json:"has_more"` -} - -// GetEventsByCursorResponse v1 -// -// The response object for getting events by cursor. -type GetEventsByCursorResponse struct { - Events []note.Event `json:"events"` - NextCursor string `json:"next_cursor"` - HasMore bool `json:"has_more"` -} diff --git a/note-go/notehub/api/fleet.go b/note-go/notehub/api/fleet.go deleted file mode 100644 index df389ac..0000000 --- a/note-go/notehub/api/fleet.go +++ /dev/null @@ -1,78 +0,0 @@ -package api - -// GetFleetsResponse v1 -// -// The response object for getting fleets. -type GetFleetsResponse struct { - Fleets []FleetResponse `json:"fleets"` -} - -// FleetResponse v1 -// -// The response object for a fleet. -type FleetResponse struct { - UID string `json:"uid"` - Label string `json:"label"` - // RFC3339 timestamp, in UTC. - Created string `json:"created"` - - EnvironmentVariables map[string]string `json:"environment_variables"` - - SmartRule string `json:"smart_rule,omitempty"` - WatchdogMins int64 `json:"watchdog_mins,omitempty"` - ConnectivityAssurance FleetConnectivityAssurance `json:"connectivity_assurance,omitempty"` -} - -// PutDeviceFleetsRequest v1 -// -// The request object for adding a device to fleets -type PutDeviceFleetsRequest struct { - // FleetUIDs - // - // The fleets the device belong to - // - // required: true - FleetUIDs []string `json:"fleet_uids"` -} - -// DeleteDeviceFleetsRequest v1 -// -// The request object for removing a device from fleets -type DeleteDeviceFleetsRequest struct { - // FleetUIDs - // - // The fleets the device should be disassociated from - // - // required: true - FleetUIDs []string `json:"fleet_uids"` -} - -// PostFleetRequest v1 -// -// The request object for adding a fleet for a project -type PostFleetRequest struct { - Label string `json:"label"` - - SmartRule string `json:"smart_rule,omitempty"` - WatchdogMins int64 `json:"watchdog_mins,omitempty"` -} - -// PutFleetRequest v1 -// -// The request object for updating a fleet within a project -type PutFleetRequest struct { - Label string `json:"label"` - AddDevices []string `json:"addDevices,omitempty"` - RemoveDevices []string `json:"removeDevices,omitempty"` - - SmartRule string `json:"smart_rule,omitempty"` - WatchdogMins int64 `json:"watchdog_mins,omitempty"` -} - -// FleetConnectivityAssurance v1 -// -// Includes, Enabled = Whether Connectivity Assurance is enabled for this fleet -// With flexibility to add more information in the future -type FleetConnectivityAssurance struct { - Enabled bool `json:"enabled"` -} diff --git a/note-go/notehub/api/job.go b/note-go/notehub/api/job.go deleted file mode 100644 index f52aefa..0000000 --- a/note-go/notehub/api/job.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2025 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package api - -// This header is present for every type of job -type HubJob struct { - Type HubJobType `json:"type,omitempty"` - Version string `json:"version,omitempty"` - Name string `json:"name,omitempty"` - Comment string `json:"comment,omitempty"` - Created int64 `json:"created,omitempty"` - CreatedBy string `json:"created_by,omitempty"` -} - -// This header is present for every type of report -type HubJobReport struct { - Type HubJobType `json:"type,omitempty"` - Version string `json:"version,omitempty"` - Comment string `json:"comment,omitempty"` - JobId string `json:"job_id"` - JobName string `json:"job_name"` - Status string `json:"status,omitempty"` - DryRun bool `json:"dry_run,omitempty"` - Cancel bool `json:"cancel,omitempty"` - SubmittedBy string `json:"who_submitted,omitempty"` - Submitted int64 `json:"when_submitted,omitempty"` - Started int64 `json:"when_started,omitempty"` - Updated int64 `json:"when_updated,omitempty"` - Completed int64 `json:"when_completed,omitempty"` -} - -// Types of jobs -type HubJobType string - -const ( - HubJobTypeUnspecified HubJobType = "" - HubJobTypeReconciliation HubJobType = "reconciliation" -) - -const ( - HubJobStatusCancelled = "cancelled" - HubJobStatusSubmitted = "submitted" -) diff --git a/note-go/notehub/api/job_reconciliation.go b/note-go/notehub/api/job_reconciliation.go deleted file mode 100644 index b14c7f8..0000000 --- a/note-go/notehub/api/job_reconciliation.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2025 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package api - -// Current data format of the reconciliation job type. Note that major types -// require conversion, while minor types can be handled by the same code. -const HubJobReconciliationMajorVersion = 1 -const HubJobReconciliationMinorVersion = 1 - -// HubJobReconciliation is the format of a batch request file -type HubJobReconciliation struct { - Header HubJob `json:"job,omitempty"` - Comment string `json:"comment,omitempty"` - Select struct { - Comment string `json:"comment,omitempty"` - AllDevices bool `json:"all_devices,omitempty"` - DevicesInFleets []string `json:"devices_in_fleets,omitempty"` - Devices []string `json:"devices,omitempty"` - DevicesBySn []string `json:"devices_by_sn,omitempty"` - } `json:"select,omitempty"` - DefaultRequests HubJobReconciliationRequests `json:"default_requests,omitempty"` - DeviceRequests map[string]HubJobReconciliationRequests `json:"device_requests,omitempty"` - ReportOptions HubJobReconciliationReportOptions `json:"report_options,omitempty"` -} - -// HubReportReconciliation is the format of the report generated by a reconciliation job. -type HubReportReconciliation struct { - Comment string `json:"comment,omitempty"` - Header HubJobReport `json:"job,omitempty"` - Status HubJobReconciliationReportStatus `json:"status,omitempty"` - Report *HubJobReconciliationReport `json:"output,omitempty"` -} - -// HubJobReconciliationRequests is a structure defining requests to apply to a set of selected devices. -// Note that if ProvisionProductUID is specified, the device will be provisioned if it isn't already provisioned, -// else it will fail if not provisioned. Also note that sn_to_set and vars_to_set use the same syntax -// as our standard API calls in that the value '-' means to clear the value. -type HubJobReconciliationRequests struct { - Comment string `json:"comment,omitempty"` - ProvisionProductUID string `json:"provision_product,omitempty"` - Disable bool `json:"disable,omitempty"` - Enable bool `json:"enable,omitempty"` - CaDisable bool `json:"connectivity_assurance_disable,omitempty"` - CaEnable bool `json:"connectivity_assurance_enable,omitempty"` - SnToDefault string `json:"sn_to_default,omitempty"` - SnToSet string `json:"sn_to_set,omitempty"` - VarsToDefault map[string]string `json:"vars_to_default,omitempty"` - VarsToSet map[string]string `json:"vars_to_set,omitempty"` - FleetsToDefault []string `json:"fleets_to_default,omitempty"` - FleetsToJoin []string `json:"fleets_to_join,omitempty"` - FleetsToLeave []string `json:"fleets_to_leave,omitempty"` -} - -// HubJobReconciliationReportStatus is the status portion of a batch report file -type HubJobReconciliationReportStatus struct { - Error string `json:"error,omitempty"` - Errors map[string]string `json:"errors,omitempty"` - Actions map[string]string `json:"actions,omitempty"` - DeviceCount int `json:"device_count,omitempty"` - Provisioned []string `json:"provisioned,omitempty"` -} - -// HubJobReconciliationReport is the format of a batch report file -type HubJobReconciliationReport struct { - App *HubJobReconciliationAppReport `json:"project,omitempty"` - Devices map[string]HubJobReconciliationDeviceReport `json:"devices,omitempty"` -} - -// HubJobReconciliationReportOptions is a structure defining options for the report -type HubJobReconciliationReportOptions struct { - Comment string `json:"comment,omitempty"` - AppInfo bool `json:"app_info,omitempty"` - AppVars bool `json:"app_vars,omitempty"` - AppFleets bool `json:"app_fleets,omitempty"` - DeviceInfo bool `json:"device_info,omitempty"` - DeviceActivity bool `json:"device_activity,omitempty"` - DeviceHealth bool `json:"device_health,omitempty"` - DeviceVars bool `json:"device_vars,omitempty"` -} - -// HubJobReconciliationAppReport is a structure defining the app report -type HubJobReconciliationAppReport struct { - Info *GetAppResponse `json:"project_info,omitempty"` - Vars *GetAppEnvironmentVariablesResponse `json:"project_vars,omitempty"` - Fleets *GetFleetsResponse `json:"project_fleets,omitempty"` -} - -// HubJobReconciliationDeviceReport is a structure defining the device report -type HubJobReconciliationDeviceReport struct { - Info *GetDeviceResponse `json:"device_info,omitempty"` - Vars *GetDeviceEnvironmentVariablesResponse `json:"device_vars,omitempty"` -} diff --git a/note-go/notehub/api/products.go b/note-go/notehub/api/products.go deleted file mode 100644 index b2b142b..0000000 --- a/note-go/notehub/api/products.go +++ /dev/null @@ -1,30 +0,0 @@ -package api - -// GetProductsResponse v1 -// -// The response object for getting products. -type GetProductsResponse struct { - Products []ProductResponse `json:"products"` -} - -// ProductResponse v1 -// -// The response object for a product. -type ProductResponse struct { - UID string `json:"uid"` - Label string `json:"label"` - AutoProvisionFleets *[]string `json:"auto_provision_fleets"` - DisableDevicesByDefault bool `json:"disable_devices_by_default"` -} - -// PostProductRequest v1 -// -// The request object for adding a product. -type PostProductRequest struct { - ProductUID string `json:"product_uid"` - Label string `json:"label"` - // Not required - AutoProvisionFleets []string `json:"auto_provision_fleets"` - // Not required - DisableDevicesByDefault bool `json:"disable_devices_by_default"` -} diff --git a/note-go/notehub/api/project.go b/note-go/notehub/api/project.go deleted file mode 100644 index fe003c3..0000000 --- a/note-go/notehub/api/project.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package api - -// GetAppResponse v1 -// -// The response object for getting an app. -type GetAppResponse struct { - UID string `json:"uid"` - Label string `json:"label"` - // RFC3339 timestamp, in UTC. - Created string `json:"created"` - - AdministrativeContact *ContactResponse `json:"administrative_contact"` - TechnicalContact *ContactResponse `json:"technical_contact"` - - // "owner", "developer", or "viewer" - Role *string `json:"role"` -} - -// ContactResponse v1 -// -// The response object for an app contact. -type ContactResponse struct { - Name string `json:"name"` - Email string `json:"email"` - Role string `json:"role"` - Organization string `json:"organization"` -} - -// GenerateClientAppResponse v1 -// -// The response object for generating a new client app for -// a specific app -type GenerateClientAppResponse struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` -} diff --git a/note-go/notehub/api/session.go b/note-go/notehub/api/session.go deleted file mode 100644 index 302e7f0..0000000 --- a/note-go/notehub/api/session.go +++ /dev/null @@ -1,21 +0,0 @@ -package api - -import "github.com/blues/note-go/note" - -// GetDeviceSessionsResponse is the structure returned from a GetDeviceSessions call -type GetDeviceSessionsResponse struct { - // Sessions - // - // The requested page of session logs for the device - // - // required: true - Sessions []note.DeviceSession `json:"sessions"` - - // HasMore - // - // A boolean indicating whether there is at least one more - // page of data available after this page - // - // required: true - HasMore bool `json:"has_more"` -} diff --git a/note-go/notehub/auth.go b/note-go/notehub/auth.go deleted file mode 100644 index 18dea81..0000000 --- a/note-go/notehub/auth.go +++ /dev/null @@ -1,340 +0,0 @@ -package notehub - -import ( - "context" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "math/rand" - "net" - "net/http" - "net/url" - "os" - "os/exec" - "os/signal" - "runtime" - "strings" - "time" -) - -type AccessToken struct { - Host string - Email string - AccessToken string - ExpiresAt time.Time -} - -// open opens the specified URL in the default browser of the user. -func open(url string) error { - var cmd string - var args []string - - switch runtime.GOOS { - case "windows": - cmd = "cmd" - args = []string{"/c", "start"} - case "darwin": - cmd = "open" - default: // "linux", "freebsd", "openbsd", "netbsd" - cmd = "xdg-open" - } - args = append(args, url) - return exec.Command(cmd, args...).Start() -} - -// listenOnAny tries each port in order and returns a bound net.Listener for the first available one. -func listenOnAny(ports []int) (net.Listener, int, error) { - for _, p := range ports { - ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", p)) - if err == nil { - return ln, p, nil - } - } - return nil, 0, errors.New("no ports available") -} - -func randString(n int) string { - letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] - } - return string(b) -} - -func RevokeAccessToken(hub, token string) error { - form := url.Values{ - "token": {token}, - "token_type_hint": {"access_token"}, - "client_id": {"notehub_cli"}, - } - - req, err := http.NewRequestWithContext( - context.Background(), - http.MethodPost, - fmt.Sprintf("https://%s/oauth2/revoke", hub), - strings.NewReader(form.Encode()), - ) - - if err != nil { - return fmt.Errorf("creating request: %w", err) - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("making request: %w", err) - } - defer resp.Body.Close() - - // Per RFC 7009: 200 OK is returned even if the token is already revoked - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - return nil -} - -// InitiateBrowserBasedLogin starts the OAuth2 login flow by opening the user's browser. -// the `hub` parameter is the hostname of Notehub where it is assumed that an OAuth2 client -// with client ID `notehub_cli` is configured for authorization code flow. -func InitiateBrowserBasedLogin(notehubApiHost string) (*AccessToken, error) { - // this is the hard-coded OAuth client ID that's persisted in Hydra - clientId := "notehub_cli" - - if !strings.HasPrefix(notehubApiHost, "api.") { - notehubApiHost = "api." + notehubApiHost - } - - var notehubUiHost string - if notehubApiHost == "api.notefile.net" { - notehubUiHost = "notehub.io" - } else { - notehubUiHost = strings.TrimPrefix(notehubApiHost, "api.") - } - - // Try these ports in order until one is available: - // - // these ports are randomly chosen and hard-coded into - // the OAuth client in Hydra within Notehub (in the redirect_uris field) - ports := []int{58766, 58767, 58768, 58769, 42100, 42101, 42102, 42103} - - // Return values - var accessToken *AccessToken - var accessTokenErr error - - state := randString(16) - codeVerifier := randString(50) // must be at least 43 characters - hash := sha256.Sum256([]byte(codeVerifier)) - codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:]) - - done := make(chan bool, 1) - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt) - defer signal.Reset(os.Interrupt) - - router := http.NewServeMux() - - // We'll fill this after we pick a port but declare it now so the handler can close over it. - chosenPort := 0 - - // The browser will be redirected to this endpoint with an authorization code - // and then this endpoint will exchange that authorization code for an access token - router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - authorizationCode := r.URL.Query().Get("code") - callbackState := r.URL.Query().Get("state") - - errHandler := func(msg string) { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "error: %s", msg) - fmt.Printf("error: %s\n", msg) - accessTokenErr = errors.New(msg) - } - - if callbackState != state { - errHandler("state mismatch") - return - } - - /////////////////////////////////////////// - // Exchange code for access token - /////////////////////////////////////////// - - tokenResp, err := http.Post( - (&url.URL{ - Scheme: "https", - Host: notehubUiHost, - Path: "/oauth2/token", - }).String(), - "application/x-www-form-urlencoded", - strings.NewReader(url.Values{ - "client_id": {clientId}, - "code": {authorizationCode}, - "code_verifier": {codeVerifier}, - "grant_type": {"authorization_code"}, - "redirect_uri": {fmt.Sprintf("http://localhost:%d", chosenPort)}, - }.Encode()), - ) - if err != nil { - errHandler("error on /oauth2/token: " + err.Error()) - return - } - defer tokenResp.Body.Close() - - body, err := io.ReadAll(tokenResp.Body) - if err != nil { - errHandler("could not read body from /oauth2/token: " + err.Error()) - return - } - - var tokenData map[string]interface{} - if err := json.Unmarshal(body, &tokenData); err != nil { - errHandler("could not unmarshal body from /oauth2/token: " + err.Error()) - return - } - - if errCode, ok := tokenData["error"].(string); ok { - if errDescription, ok2 := tokenData["error_description"].(string); ok2 { - errHandler(fmt.Sprintf("%s: %s", errCode, errDescription)) - } else { - errHandler(errCode) - } - return - } - - accessTokenString, ok := tokenData["access_token"].(string) - if !ok { - errHandler("unexpected error: no access token returned") - return - } - - // be defensive about type - var expiresIn time.Duration - switch v := tokenData["expires_in"].(type) { - case float64: - expiresIn = time.Duration(v) * time.Second - case int: - expiresIn = time.Duration(v) * time.Second - default: - expiresIn = 0 - } - - /////////////////////////////////////////// - // Get user's information (specifically email) - /////////////////////////////////////////// - - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/userinfo", notehubApiHost), nil) - if err != nil { - errHandler("could not create request for /userinfo: " + err.Error()) - return - } - req.Header.Set("Authorization", "Bearer "+accessTokenString) - userinfoResp, err := http.DefaultClient.Do(req) - if err != nil { - errHandler("could not get userinfo: " + err.Error()) - return - } - defer userinfoResp.Body.Close() - - userinfoBody, err := io.ReadAll(userinfoResp.Body) - if err != nil { - errHandler("could not read body from /userinfo: " + err.Error()) - return - } - - var userinfoData map[string]interface{} - if err := json.Unmarshal(userinfoBody, &userinfoData); err != nil { - errHandler("could not unmarshal body from /userinfo: " + err.Error()) - return - } - - email, ok := userinfoData["email"].(string) - if !ok { - errHandler("could not retrieve email") - return - } - - /////////////////////////////////////////// - // Build the access token response - /////////////////////////////////////////// - - accessToken = &AccessToken{ - Host: notehubApiHost, - Email: email, - AccessToken: accessTokenString, - ExpiresAt: time.Now().Add(expiresIn), - } - - /////////////////////////////////////////// - // respond to the browser and quit - /////////////////////////////////////////// - - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "

Token exchange completed successfully

You may now close this window and return to the CLI application

") - - quit <- os.Interrupt - }) - - // Pick first available port and get a listener - listener, port, err := listenOnAny(ports) - if err != nil { - return nil, fmt.Errorf("could not bind any callback port: %w", err) - } - chosenPort = port - - server := &http.Server{ - Addr: fmt.Sprintf(":%d", chosenPort), - Handler: router, - } - - // Wait for OAuth callback to be hit, then shutdown HTTP server - go func() { - <-quit - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - server.SetKeepAlivesEnabled(false) - if err := server.Shutdown(ctx); err != nil { - log.Printf("error: %v", err) - } - close(done) - }() - - // Start HTTP server waiting for OAuth callback - go func() { - if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { - log.Printf("error: %v", err) - } - }() - - // Build the authorize URL using the chosen port - authorizeUrl := url.URL{ - Scheme: "https", - Host: notehubUiHost, - Path: "/oauth2/auth", - RawQuery: url.Values{ - "client_id": {clientId}, - "code_challenge": {codeChallenge}, - "code_challenge_method": {"S256"}, - "redirect_uri": {fmt.Sprintf("http://localhost:%d", chosenPort)}, - "response_type": {"code"}, - "scope": {"openid email"}, - "state": {state}, - }.Encode(), - } - - // Open web browser to authorize - fmt.Printf("Opening web browser to initiate authentication (redirect port %d)...\n", chosenPort) - if err := open(authorizeUrl.String()); err != nil { - fmt.Printf("error opening web browser: %v", err) - } - - // Wait for exchange to finish - <-done - return accessToken, accessTokenErr -} diff --git a/note-go/notehub/config.go b/note-go/notehub/config.go deleted file mode 100644 index 4bb21bb..0000000 --- a/note-go/notehub/config.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notehub - -// DefaultAPIService (golint) -const DefaultAPIService = "api.notefile.net" diff --git a/note-go/notehub/dbquery.go b/note-go/notehub/dbquery.go deleted file mode 100644 index 81e613a..0000000 --- a/note-go/notehub/dbquery.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notehub - -// DbQuery is the structure for a database query -type DbQuery struct { - Columns string `json:"columns,omitempty"` - Format string `json:"format,omitempty"` - Count bool `json:"count,omitempty"` - Offset int `json:"offset,omitempty"` - Limit int `json:"limit,omitempty"` - NoHeader bool `json:"noheader,omitempty"` - Where string `json:"where,omitempty"` - Last string `json:"last,omitempty"` - Order string `json:"order,omitempty"` - Descending bool `json:"descending,omitempty"` -} diff --git a/note-go/notehub/request.go b/note-go/notehub/request.go deleted file mode 100644 index e3b43ea..0000000 --- a/note-go/notehub/request.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2019 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package notehub - -import ( - "fmt" - "strings" - - "github.com/blues/note-go/note" - "github.com/blues/note-go/notecard" -) - -// Supported requests - -// HubDeviceContact (golint) -const HubDeviceContact = "hub.device.contact" - -// HubDeviceSessionBegin (golint) -const HubDeviceSessionBegin = "hub.device.session.begin" - -// HubDeviceSessionUsage (golint) -const HubDeviceSessionUsage = "hub.device.session.usage" - -// HubDeviceSessionEnd (golint) -const HubDeviceSessionEnd = "hub.device.session.end" - -// HubAppGetSchemas (golint) -const HubAppGetSchemas = "hub.app.schemas.get" - -// HubQuery (golint) -const HubQuery = "hub.app.data.query" - -// HubEventQuery (golint) -const HubEventQuery = "hub.app.event.query" - -// HubSessionQuery (golint) -const HubSessionQuery = "hub.app.session.query" - -// HubAppUpload (golint) -const HubAppUpload = "hub.app.upload.add" - -// HubUpload (golint) -const HubUpload = "hub.upload.add" - -// HubAppUploads (golint) -const HubAppUploads = "hub.app.upload.query" - -// HubAppJobSubmit (golint) -const HubAppJobSubmit = "hub.app.job.submit" - -// HubAppJobGet (golint) -const HubAppJobGet = "hub.app.job.get" - -// HubAppJobPut (golint) -const HubAppJobPut = "hub.app.job.put" - -// HubAppJobDelete (golint) -const HubAppJobDelete = "hub.app.job.delete" - -// HubAppJobsGet (golint) -const HubAppJobsGet = "hub.app.jobs.get" - -// HubAppReportGet (golint) -const HubAppReportGet = "hub.app.report.get" - -// HubAppReportDelete (golint) -const HubAppReportDelete = "hub.app.report.delete" - -// HubAppReportCancel (golint) -const HubAppReportCancel = "hub.app.report.cancel" - -// HubAppReportsGet (golint) -const HubAppReportsGet = "hub.app.reports.get" - -// HubUploads (golint) -const HubUploads = "hub.upload.query" - -// HubAppUploadSet (golint) -const HubAppUploadSet = "hub.app.upload.set" - -// HubUploadSet (golint) -const HubUploadSet = "hub.upload.set" - -// HubAppUploadDelete (golint) -const HubAppUploadDelete = "hub.app.upload.delete" - -// HubUploadDelete (golint) -const HubUploadDelete = "hub.upload.delete" - -// HubAppUploadRead (golint) -const HubAppUploadRead = "hub.app.upload.get" - -// HubUploadRead (golint) -const HubUploadRead = "hub.upload.get" - -// HubAppSetTransform (golint) -const HubAppSetTransform = "hub.app.transform.set" - -// HubAppGetTransform (golint) -const HubAppGetTransform = "hub.app.transform.get" - -// HubEnvSet (golint) -const HubEnvSet = "hub.env.set" - -// HubEnvGet (golint) -const HubEnvGet = "hub.env.get" - -// HubEnvScopeApp (golint) -const HubEnvScopeApp = "app" - -// HubEnvScopeProject (golint) -const HubEnvScopeProject = "project" - -// HubEnvScopeFleet (golint) -const HubEnvScopeFleet = "fleet" - -// HubEnvScopeFleets (golint) -const HubEnvScopeFleets = "fleets" - -// HubEnvScopeDevice (golint) -const HubEnvScopeDevice = "device" - -// HubCompressModeSnappy (golint) -const HubCompressModeSnappy = "snappy" - -// HubCompressModeCobs (golint) -const HubCompressModeCobs = "cobs" - -// HubRequest is is the core data structure for notehub-specific requests -type HubRequest struct { - notecard.Request `json:",omitempty"` - Contact *note.Contact `json:"contact,omitempty"` - AppUID string `json:"app,omitempty"` - FleetUID string `json:"fleet,omitempty"` - EventSerials []string `json:"events,omitempty"` - DbQuery *DbQuery `json:"query,omitempty"` - Uploads []UploadMetadata `json:"uploads,omitempty"` - Contains string `json:"contains,omitempty"` - Handlers *[]string `json:"handlers,omitempty"` - FileType UploadType `json:"type,omitempty"` - FileTags string `json:"tags,omitempty"` - FileNotes string `json:"filenotes,omitempty"` - Provision bool `json:"provision,omitempty"` - Scope string `json:"scope,omitempty"` - Env *map[string]string `json:"env,omitempty"` - FleetEnv *map[string]map[string]string `json:"fleet_env,omitempty"` - PIN string `json:"pin,omitempty"` - Compress string `json:"compress,omitempty"` - MD5 string `json:"md5,omitempty"` - DeviceEndpoint bool `json:"device_endpoint,omitempty"` - DryRun bool `json:"dry_run,omitempty"` -} - -type UploadType string - -const ( - UploadTypeUnknown UploadType = "" - UploadTypeHostFirmware UploadType = "firmware" - UploadTypeNotecardFirmware UploadType = "notecard" - UploadTypeModemFirmware UploadType = "modem" - UploadTypeStarnoteFirmware UploadType = "starnote" - UploadTypeUserData UploadType = "data" - UploadTypeJob UploadType = "job" -) - -var allFileTypes = []UploadType{ - UploadTypeUnknown, - UploadTypeHostFirmware, - UploadTypeNotecardFirmware, - UploadTypeModemFirmware, - UploadTypeStarnoteFirmware, - UploadTypeUserData, - UploadTypeJob, -} - -func ParseUploadType(fileType string) UploadType { - if fileType == "host" { - return UploadTypeHostFirmware - } - for _, validType := range allFileTypes { - if string(validType) == fileType { - return validType - } - } - return UploadTypeUnknown -} - -const TestFirmwareString = "(test firmware)" - -// HubRequestFileFirmware is firmware-specific metadata -type HubRequestFileFirmware struct { - // The organization accountable for the firmware - a display string - Organization string `json:"org,omitempty"` - // A description of the firmware - a display string - Description string `json:"description,omitempty"` - // The name and model number of the product containing the firmware - a display string - Product string `json:"product,omitempty"` - // The identifier of the only firmware that will be acceptable and downloaded to this device - Firmware string `json:"firmware,omitempty"` - // The composite version number of the firmware, generally major.minor.patch as a string - Version string `json:"version,omitempty"` - // The target CPU of the firmware (see notecard/src/board.h) - Target string `json:"target,omitempty"` - // The build number of the firmware, for numeric comparison - Major uint32 `json:"ver_major,omitempty"` - Minor uint32 `json:"ver_minor,omitempty"` - Patch uint32 `json:"ver_patch,omitempty"` - Build uint32 `json:"ver_build,omitempty"` - // The build number of the firmware, generally just a date and time - Built string `json:"built,omitempty"` - // The entity who built or is responsible for the firmware - a display string - Builder string `json:"builder,omitempty"` -} - -func (metadata HubRequestFileFirmware) VersionString() string { - return fmt.Sprintf("%d.%d.%d.%d", metadata.Major, metadata.Minor, metadata.Patch, metadata.Build) -} - -// UploadMetadata is the body of the object uploaded for each file -type UploadMetadata struct { - Name string `json:"name,omitempty"` - Length int `json:"length,omitempty"` - MD5 string `json:"md5,omitempty"` - CRC32 uint32 `json:"crc32,omitempty"` - Created int64 `json:"created,omitempty"` - Modified int64 `json:"modified,omitempty"` - Source string `json:"source,omitempty"` - Contains string `json:"contains,omitempty"` - Found string `json:"found,omitempty"` - FileType UploadType `json:"type,omitempty"` - Tags string `json:"tags,omitempty"` // comma-separated, no spaces, case-insensitive - Notes string `json:"notes,omitempty"` // Should be simple text - Firmware *HubRequestFileFirmware `json:"firmware,omitempty"` // This value is pulled out of the firmware binary itself - Version string `json:"version,omitempty"` // User-specified version string provided at time of upload - // Arbitrary metadata that the user may define - we don't interpret the schema at all - Info map[string]interface{} `json:"info,omitempty"` -} - -func (upload UploadMetadata) IsArchSpecificNotecardFirmware() bool { - return upload.FileType == UploadTypeNotecardFirmware && (strings.Contains(upload.Name, "-s3-") || - strings.Contains(upload.Name, "-u5-") || - strings.Contains(upload.Name, "-wl-")) -} - -func (upload UploadMetadata) IsPublished() bool { - for _, tag := range strings.Split(upload.Tags, ",") { - if strings.TrimSpace(strings.ToLower(tag)) == "publish" { - return true - } - } - return false -} - -// HubRequestFileTagPublish indicates that this should be published in the UI -const HubRequestFileTagPublish = "publish" diff --git a/note-go/package.sh b/note-go/package.sh deleted file mode 100755 index a79d6f4..0000000 --- a/note-go/package.sh +++ /dev/null @@ -1,47 +0,0 @@ -#! /usr/bin/env bash -# -# Copyright 2020 Blues Inc. All rights reserved. -# Use of this source code is governed by licenses granted by the -# copyright holder including that found in the LICENSE file. -# -######### Bash Boilerplate ########## -set -euo pipefail # strict mode -readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -cd "$SCRIPT_DIR" # cd to this script's dir -######### End Bash Boilerplate ########## - -# -# note-go package.sh -# -# This script packages the best note-go executables (notecard, notehub) into -# archives named note{card,hub}cli_${GOOS}_${GOARCH}.tar.gz or .zip in the case of -# ${GOOS}=windows. To be more user-friendly we call darwin 'macos' in the archive -# names. -# -# Parameters: This script uses ${GOOS} and ${GOARCH} determine where to look for the -# executables. -# -# Output: Archives are saved in "./build/packages/" -# - -# Add GOOS and GOARCH to our environment. (and other GO vars we don't need) -eval "$(go env)" - -readonly BUILD_EXE_DIR="$SCRIPT_DIR/build/${GOOS}/${GOARCH}/" -mkdir -p "$BUILD_EXE_DIR" -readonly BUILD_PACKAGE_DIR="$SCRIPT_DIR/build/packages/" -mkdir -p "$BUILD_PACKAGE_DIR" - -# compress the build products into an archive -cd "$BUILD_EXE_DIR" -if [ "${GOOS}" = "windows" ]; then - # -j means don't store directory names, just file names. Basically flattens everything into the root of the zip. - zip -j "$BUILD_PACKAGE_DIR/notecardcli_${GOOS}_${GOARCH}.zip" ./notecard.exe "$SCRIPT_DIR/notecard-driver-windows7.inf" - zip -j "$BUILD_PACKAGE_DIR/notehubcli_${GOOS}_${GOARCH}.zip" ./notehub.exe -elif [ "${GOOS}" = "darwin" ]; then - tar -czvf "$BUILD_PACKAGE_DIR/notecardcli_macos_${GOARCH}.tar.gz" ./notecard - tar -czvf "$BUILD_PACKAGE_DIR/notehubcli_macos_${GOARCH}.tar.gz" ./notehub -else - tar -czvf "$BUILD_PACKAGE_DIR/notecardcli_${GOOS}_${GOARCH}.tar.gz" ./notecard - tar -czvf "$BUILD_PACKAGE_DIR/notehubcli_${GOOS}_${GOARCH}.tar.gz" ./notehub -fi; diff --git a/notecard/echo.go b/notecard/echo.go index aac00f3..5f5d41a 100644 --- a/notecard/echo.go +++ b/notecard/echo.go @@ -6,8 +6,8 @@ package main import ( "bytes" + "crypto/rand" "fmt" - "math/rand" "github.com/blues/note-go/notecard" )