From 796d9f4488df0f66197d01f96f353659ec65734b Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Wed, 29 Oct 2025 15:16:05 +0000 Subject: [PATCH 1/5] feat: add firmware best practices tool and enhance firmware entrypoint tool description --- blues-expert/lib/docs/arduino/index.md | 49 ++++++++++++++++++++-- blues-expert/lib/docs/arduino/templates.md | 9 ---- blues-expert/lib/handlers.go | 49 ++++++++++++++++++++++ blues-expert/main.go | 2 + blues-expert/tools.go | 29 ++++++++++++- 5 files changed, 125 insertions(+), 13 deletions(-) diff --git a/blues-expert/lib/docs/arduino/index.md b/blues-expert/lib/docs/arduino/index.md index 2518990..b4fede4 100644 --- a/blues-expert/lib/docs/arduino/index.md +++ b/blues-expert/lib/docs/arduino/index.md @@ -7,18 +7,61 @@ This library is recommended for Arduino-based devices, in particular those that ## Installing the note-arduino library +If required, the user may wish to install the note-arduino library using the Arduino CLI. + ### Arduino CLI ```bash arduino-cli lib install "Blues Wireless Notecard" ``` -### PlatformIO +## Code Layout -```bash -pio lib install "Blues Wireless Notecard" +When either creating a new project or retrofitting existing projects to use the Notecard, ensure that the following structures are present: + +- ALL Notecard code should be in a separate library file from the main sketch (VERY IMPORTANT). +- Limit modifications to the user's Arduino code; prefer to add new functions to the library file. +- A 'init' function that initializes the Notecard, along with `hub.set` commands to configure the Notecard. + - Before optimising the code, set the `mode` to `continuous` for easy debugging. +- Once the initial pass has been made, go to [power management](#power-management) to ensure that the Notecard is power efficient.. + +## Design Patterns + +Work through the following design patterns to ensure that target projects are easy to maintain and extend. + +ALWAYS: + +- use templates for notes and ensure that the data types are correct. ALWAYS use the `firmware_best_practices` tool with the `templates` document type for more information. +- use the I2C interface for Notecard communication, unless instructed otherwise by the user. +- use Blues Expert tools to check and validate Notecard requests and responses. +- generate a header comment in bother the `.c` and the `.h` files that contains the following information: + +```c +/*************************************************************************** + - Library for + + This library encapsulates all Notecard functionality for the . + This is specific to your project and is NOT A GENERAL PURPOSE LIBRARY. + + THIS FILE SHOULD BE EDITED AFTER GENERATION. + IT IS PROVIDED AS A STARTING POINT FOR THE USER TO EDIT AND EXTEND. +***************************************************************************/ ``` +TRY TO: + +- use the Serial interface for debugging, if possible. This should be easily disabled by the user, if not needed. + +NEVER: + +- layer note-c calls within the user's application code. If the Notecard is used, it should be handled by a function in the newly created library file. + +## Power Management + +Implement the initial changes following this guide and then ask the user if they would like to optimise the code for power management. + +- use the `firmware_best_practices` tool with the `best_practices` document type. + ## Further Reading Queries about note-arduino specifics should be made to the `docs_search` or `docs_search_expert` tool. diff --git a/blues-expert/lib/docs/arduino/templates.md b/blues-expert/lib/docs/arduino/templates.md index 75ec244..1663c02 100644 --- a/blues-expert/lib/docs/arduino/templates.md +++ b/blues-expert/lib/docs/arduino/templates.md @@ -4,17 +4,8 @@ When building an application that is expected to operate over a long period of t ## Working with Note Templates -By default, the Notecard allows for maximum developer flexibility in the structure and content of Notes. As such, individual Notes in a Notefile do not share structure or schema. You can add JSON structures and payloads of any type and format to a Notefile, adding and removing fields as required by your application. - -In order to provide this simplicity to developers, the design of the Notefile system is primarily memory based and designed to support no more than 100 Notes per Notefile. As long as your data needs and sync periods ensure regular uploads of data to Notehub, this limit is adequate for most applications. - -Some applications, however, will need to track and stage bursts of data that may eclipse the 100 Note limit in a short period of time, and before a sync can occur. For these types of use cases, the Notecard supports using a flash-based storage system based on Note templates. - Using the `note.template` request with any `.qo` or `.qos` Notefile, developers can provide the Notecard with a schema of sorts to apply to future Notes added to the Notefile. This template acts as a hint to the Notecard that allows it to internally store data as fixed-length records rather than as flexible JSON objects, which tend to be much larger. -> Note: -> Note Templates are required for both inbound and outbound Notefiles when using Notecard LoRa or NTN mode with Starnote. - ## Creating a Template To create a template, use the file argument to specify the Notefile to which the template should be applied. Then, use the body argument to specify a template body, similar to the way you'd make a note.add request. That body must contain the name of each field expected in each note.add request, and a value that serves as the hint indicating the data type to the Notecard. Each field can be a boolean, integer, float, or string. The port argument is required on Notecard LoRa and Starnote, and is a unique integer in the range 1-100. diff --git a/blues-expert/lib/handlers.go b/blues-expert/lib/handlers.go index 9a97854..be89546 100644 --- a/blues-expert/lib/handlers.go +++ b/blues-expert/lib/handlers.go @@ -15,6 +15,12 @@ type FirmwareEntrypointArgs struct { Sdk string `json:"sdk" jsonschema:"The sdk to use for the firmware project. Must be one of: Arduino, C, Zephyr, Python"` } +// FirmwareBestPracticesArgs defines the arguments for the firmware best practices tool +type FirmwareBestPracticesArgs struct { + Sdk string `json:"sdk" jsonschema:"The sdk to use for the firmware project. Must be one of: arduino, c, zephyr, python"` + DocumentType string `json:"document_type" jsonschema:"The type of documentation to retrieve (e.g., 'power_management', 'best_practices', 'sensors', 'templates')"` +} + // RequestValidateArgs defines the arguments for the notecard request validation tool type RequestValidateArgs struct { Request string `json:"request" jsonschema:"The JSON string of the request to validate (e.g., '{\"req\":\"card.version\"}', '{\"req\":\"card.temp\",\"minutes\":60}')"` @@ -73,6 +79,49 @@ func HandleFirmwareEntrypointTool(ctx context.Context, request *mcp.CallToolRequ }, nil, nil } +func HandleFirmwareBestPracticesTool(ctx context.Context, request *mcp.CallToolRequest, args FirmwareBestPracticesArgs) (*mcp.CallToolResult, any, error) { + TrackSession(request, "firmware_best_practices") + + if args.Sdk == "" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "Error: SDK parameter is required and cannot be empty. Valid values are: arduino, c, zephyr, python"}, + }, + IsError: true, + }, nil, nil + } + + if args.DocumentType == "" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "Error: document_type parameter is required and cannot be empty. Examples: 'power_management', 'best_practices', 'sensors', 'templates'"}, + }, + IsError: true, + }, nil, nil + } + + // Convert SDK to lowercase for directory name + sdk := strings.ToLower(args.Sdk) + docFile := fmt.Sprintf("docs/%s/%s.md", sdk, args.DocumentType) + + // Get the docs + docContent, err := docs.ReadFile(docFile) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: fmt.Sprintf("Error reading documentation: %v. Make sure the SDK ('%s') and document_type ('%s') are valid.", err, sdk, args.DocumentType)}, + }, + IsError: true, + }, nil, nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(docContent)}, + }, + }, nil, nil +} + // Notecard API Tools func HandleAPIValidateTool(ctx context.Context, request *mcp.CallToolRequest, args RequestValidateArgs) (*mcp.CallToolResult, any, error) { TrackSession(request, "api_validate") diff --git a/blues-expert/main.go b/blues-expert/main.go index f4c9c50..e4d1612 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -50,6 +50,7 @@ func main() { // Add tools firmwareEntrypointTool := CreateFirmwareEntrypointTool() + firmwareBestPracticesTool := CreateFirmwareBestPracticesTool() apiValidateTool := CreateAPIValidateTool() apiDocsTool := CreateAPIDocsTool() docsSearchTool := CreateDocsSearchTool() @@ -57,6 +58,7 @@ func main() { // Add tool handlers mcp.AddTool(s, firmwareEntrypointTool, lib.HandleFirmwareEntrypointTool) + mcp.AddTool(s, firmwareBestPracticesTool, lib.HandleFirmwareBestPracticesTool) mcp.AddTool(s, apiValidateTool, lib.HandleAPIValidateTool) mcp.AddTool(s, apiDocsTool, lib.HandleAPIDocsTool) mcp.AddTool(s, docsSearchTool, lib.HandleDocsSearchTool) diff --git a/blues-expert/tools.go b/blues-expert/tools.go index 67c2178..14a4080 100644 --- a/blues-expert/tools.go +++ b/blues-expert/tools.go @@ -9,7 +9,7 @@ import ( func CreateFirmwareEntrypointTool() *mcp.Tool { return &mcp.Tool{ Name: "firmware_entrypoint", - Description: "Get a starting point for a firmware project. This tool will return information about developing firmware for the Notecard using a specific SDK.", + Description: "Get a starting point for a firmware project. This tool will return information about developing firmware for the Notecard using a specific SDK. ALWAYS use this tool when writing code, before using any other tools as it contains critial information about Notecard implementation.", InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -29,6 +29,33 @@ func CreateFirmwareEntrypointTool() *mcp.Tool { } } +func CreateFirmwareBestPracticesTool() *mcp.Tool { + return &mcp.Tool{ + Name: "firmware_best_practices", + Description: "Get best practices documentation for firmware development with the Notecard. Returns detailed guidance on specific topics like power management, sensors, templates, etc. for a given SDK.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "sdk": { + Type: "string", + Description: "The SDK to use for the firmware project. Must be one of: arduino, c, zephyr, python", + Enum: []any{ + "arduino", + "c", + "zephyr", + "python", + }, + }, + "document_type": { + Type: "string", + Description: "The type of documentation to retrieve (e.g., 'power_management', 'best_practices', 'sensors', 'templates')", + }, + }, + Required: []string{"sdk", "document_type"}, + }, + } +} + // Notecard API Tools func CreateAPIValidateTool() *mcp.Tool { return &mcp.Tool{ From ff2e53bf4f4b52063549f52795327701815c43f0 Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 11 Nov 2025 13:38:52 +0000 Subject: [PATCH 2/5] feat: enhance API tools with schema version metadata --- blues-expert/lib/docs/arduino/index.md | 5 +-- blues-expert/lib/handlers.go | 38 +++++++++++++++++++-- blues-expert/lib/validate.go | 46 ++++++++++++++++++++------ 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/blues-expert/lib/docs/arduino/index.md b/blues-expert/lib/docs/arduino/index.md index b4fede4..731404f 100644 --- a/blues-expert/lib/docs/arduino/index.md +++ b/blues-expert/lib/docs/arduino/index.md @@ -22,8 +22,9 @@ When either creating a new project or retrofitting existing projects to use the - ALL Notecard code should be in a separate library file from the main sketch (VERY IMPORTANT). - Limit modifications to the user's Arduino code; prefer to add new functions to the library file. - A 'init' function that initializes the Notecard, along with `hub.set` commands to configure the Notecard. - - Before optimising the code, set the `mode` to `continuous` for easy debugging. -- Once the initial pass has been made, go to [power management](#power-management) to ensure that the Notecard is power efficient.. + - Before optimising the code, set the `mode` to `continuous` for easy debugging. Use a comment to indicate that the user should change this to `periodic` to fit their application. +- The first `sendRequest()` call should instead be `sendRequestWithRetry()` to ensure that the request is retried if it fails. This is to address a potential race condition on cold boot. +- Once the initial pass has been made, go to [power management](#power-management) to ensure that the Notecard is power efficient. ## Design Patterns diff --git a/blues-expert/lib/handlers.go b/blues-expert/lib/handlers.go index be89546..dc2803a 100644 --- a/blues-expert/lib/handlers.go +++ b/blues-expert/lib/handlers.go @@ -145,9 +145,17 @@ func HandleAPIValidateTool(ctx context.Context, request *mcp.CallToolRequest, ar }, nil, nil } + // Get schema version for metadata + schemaVersion := GetSchemaVersion("") + return &mcp.CallToolResult{ Content: []mcp.Content{ - &mcp.TextContent{Text: "Request validation successful: The JSON request is valid according to the Notecard API schema."}, + &mcp.TextContent{ + Text: "Request validation successful: The JSON request is valid according to the Notecard API schema.", + Meta: mcp.Meta{ + "schema_version": schemaVersion, + }, + }, }, }, nil, nil } @@ -166,6 +174,9 @@ func HandleAPIDocsTool(ctx context.Context, request *mcp.CallToolRequest, args G }, nil, nil } + // Get schema version for metadata + schemaVersion := GetSchemaVersion("") + var response []byte // If specific API requested, return just the API object if args.API != "" && len(apiCategory.APIs) > 0 { @@ -186,7 +197,12 @@ func HandleAPIDocsTool(ctx context.Context, request *mcp.CallToolRequest, args G return &mcp.CallToolResult{ Content: []mcp.Content{ - &mcp.TextContent{Text: string(response)}, + &mcp.TextContent{ + Text: string(response), + Meta: mcp.Meta{ + "schema_version": schemaVersion, + }, + }, }, }, nil, nil } @@ -206,6 +222,16 @@ func HandleDocsSearchTool(ctx context.Context, request *mcp.CallToolRequest, arg }, nil, nil } + // Log the response sent to the client + if request != nil && request.Session != nil && result != nil && !result.IsError { + if len(result.Content) > 0 { + request.Session.Log(ctx, &mcp.LoggingMessageParams{ + Level: "info", + Data: "Search completed successfully", + }) + } + } + return result, nil, nil } @@ -313,6 +339,14 @@ Please provide a comprehensive, expert-level response that goes beyond just summ // Extract the expert response expertResponse := getTextFromContent(result.Content) + // Log the response sent to the client + if request != nil && request.Session != nil { + request.Session.Log(ctx, &mcp.LoggingMessageParams{ + Level: "info", + Data: fmt.Sprintf("Expert analysis completed successfully using model: %s", result.Model), + }) + } + // Return the expert analysis return &mcp.CallToolResult{ Content: []mcp.Content{ diff --git a/blues-expert/lib/validate.go b/blues-expert/lib/validate.go index 108597a..c196558 100644 --- a/blues-expert/lib/validate.go +++ b/blues-expert/lib/validate.go @@ -38,8 +38,9 @@ const cacheExpirationDuration = 24 * time.Hour // CacheMetadata represents metadata for cached schema files type CacheMetadata struct { - FetchTime time.Time `json:"fetch_time"` - URL string `json:"url"` + FetchTime time.Time `json:"fetch_time"` + URL string `json:"url"` + SchemaVersion string `json:"schema_version,omitempty"` } // resetSchemaWithLock safely resets the schema state for re-initialization @@ -114,12 +115,18 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url } log.Println("Processing and validating schema...") - // Verify it's valid JSON before caching - var v interface{} - if err := json.Unmarshal(data, &v); err != nil { + // Verify it's valid JSON before caching and extract version + var schemaMap map[string]interface{} + if err := json.Unmarshal(data, &schemaMap); err != nil { return nil, fmt.Errorf("invalid JSON schema %s: %v", url, err) } + // Extract schema version from the schema if available + var schemaVersion string + if version, ok := schemaMap["version"].(string); ok { + schemaVersion = version + } + // Log caching status if request != nil && request.Session != nil { request.Session.Log(ctx, &mcp.LoggingMessageParams{ @@ -136,8 +143,8 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url // Log error but continue - don't fail if we can't cache fmt.Fprintf(os.Stderr, "warning: failed to cache schema %s: %v\n", url, err) } else { - // Save cache metadata - if err := saveCacheMetadata(url, fetchTime); err != nil { + // Save cache metadata with schema version + if err := saveCacheMetadata(url, fetchTime, schemaVersion); err != nil { fmt.Fprintf(os.Stderr, "warning: failed to save cache metadata for %s: %v\n", url, err) } } @@ -230,10 +237,11 @@ func getCacheMetadataPath(url string) string { } // saveCacheMetadata saves metadata for a cached schema file -func saveCacheMetadata(url string, fetchTime time.Time) error { +func saveCacheMetadata(url string, fetchTime time.Time, schemaVersion string) error { metadata := CacheMetadata{ - FetchTime: fetchTime, - URL: url, + FetchTime: fetchTime, + URL: url, + SchemaVersion: schemaVersion, } metadataPath := getCacheMetadataPath(url) @@ -277,6 +285,24 @@ func isCacheExpired(url string) bool { return time.Since(metadata.FetchTime) > cacheExpirationDuration } +// GetSchemaVersion retrieves the schema version from cache metadata +func GetSchemaVersion(schemaURL string) string { + if schemaURL == "" { + schemaURL = defaultSchemaURL + } + + metadata, err := loadCacheMetadata(schemaURL) + if err != nil { + return "unknown" + } + + if metadata.SchemaVersion == "" { + return "unknown" + } + + return metadata.SchemaVersion +} + // initSchema compiles the schema, using cached files if available func initSchema(url string) error { schemaMutex.RLock() From 168cdd7065df90fca9957465c74b5f7bbebd7b81 Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 11 Nov 2025 13:39:12 +0000 Subject: [PATCH 3/5] feat: integrate zerolog for enhanced logging and add log level configuration --- blues-expert/Dockerfile | 2 +- blues-expert/lib/handlers.go | 34 +++++++++++---------- blues-expert/lib/logger.go | 57 ++++++++++++++++++++++++++++++++++++ blues-expert/lib/query.go | 15 +++++++++- blues-expert/lib/session.go | 54 +++++++++++++++++++++++----------- blues-expert/lib/validate.go | 39 ++++++++++++++++-------- blues-expert/main.go | 21 ++++++++----- go.mod | 3 ++ go.sum | 14 +++++++++ 9 files changed, 185 insertions(+), 54 deletions(-) create mode 100644 blues-expert/lib/logger.go diff --git a/blues-expert/Dockerfile b/blues-expert/Dockerfile index 6f4778e..6915ce7 100644 --- a/blues-expert/Dockerfile +++ b/blues-expert/Dockerfile @@ -43,4 +43,4 @@ USER appuser EXPOSE 8080 # Run the binary -CMD ["./main"] +CMD ["./main", "-log-level", "debug"] diff --git a/blues-expert/lib/handlers.go b/blues-expert/lib/handlers.go index dc2803a..24473c5 100644 --- a/blues-expert/lib/handlers.go +++ b/blues-expert/lib/handlers.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/rs/zerolog/log" ) // FirmwareEntrypointArgs defines the arguments for the firmware entrypoint tool @@ -222,13 +223,13 @@ func HandleDocsSearchTool(ctx context.Context, request *mcp.CallToolRequest, arg }, nil, nil } - // Log the response sent to the client - if request != nil && request.Session != nil && result != nil && !result.IsError { - if len(result.Content) > 0 { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: "Search completed successfully", - }) + // Log the response for server debugging + if result != nil && !result.IsError && len(result.Content) > 0 { + if textContent, ok := result.Content[0].(*mcp.TextContent); ok { + log.Debug(). + Str("tool", "docs_search"). + Str("response", textContent.Text). + Msg("Response sent to client") } } @@ -339,18 +340,21 @@ Please provide a comprehensive, expert-level response that goes beyond just summ // Extract the expert response expertResponse := getTextFromContent(result.Content) - // Log the response sent to the client - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: fmt.Sprintf("Expert analysis completed successfully using model: %s", result.Model), - }) - } + // Format the full response that will be sent to the client + fullResponse := fmt.Sprintf("# Notecard Expert Analysis\n\n**Query:** %s\n\n**Expert Response:**\n%s\n\n---\n*Analysis provided by AI model: %s*", args.Query, expertResponse, result.Model) + + // Log the response for server debugging + log.Debug(). + Str("tool", "docs_search_expert"). + Str("query", args.Query). + Str("model", result.Model). + Str("response", fullResponse). + Msg("Response sent to client") // Return the expert analysis return &mcp.CallToolResult{ Content: []mcp.Content{ - &mcp.TextContent{Text: fmt.Sprintf("# Notecard Expert Analysis\n\n**Query:** %s\n\n**Expert Response:**\n%s\n\n---\n*Analysis provided by AI model: %s*", args.Query, expertResponse, result.Model)}, + &mcp.TextContent{Text: fullResponse}, }, }, nil, nil } diff --git a/blues-expert/lib/logger.go b/blues-expert/lib/logger.go new file mode 100644 index 0000000..89cf43a --- /dev/null +++ b/blues-expert/lib/logger.go @@ -0,0 +1,57 @@ +package lib + +import ( + "io" + "os" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// InitLogger initializes the global logger with the specified log level +// Valid levels: trace, debug, info, warn, error, fatal, panic +func InitLogger(level string) { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + var output io.Writer = os.Stderr + + // Parse log level from string + logLevel := parseLogLevel(level) + + // Enable pretty printing for debug and trace levels + if logLevel <= zerolog.TraceLevel { + output = zerolog.ConsoleWriter{Out: os.Stderr} + } + + // Set the global logger + log.Logger = zerolog.New(output). + Level(logLevel). + With(). + Timestamp(). + Logger() + + log.Info().Str("set-level", level).Msg("Logger initialized") +} + +// parseLogLevel converts a string to zerolog.Level +func parseLogLevel(level string) zerolog.Level { + switch strings.ToLower(level) { + case "trace": + return zerolog.TraceLevel + case "debug": + return zerolog.DebugLevel + case "info": + return zerolog.InfoLevel + case "warn", "warning": + return zerolog.WarnLevel + case "error": + return zerolog.ErrorLevel + case "fatal": + return zerolog.FatalLevel + case "panic": + return zerolog.PanicLevel + default: + return zerolog.InfoLevel + } +} diff --git a/blues-expert/lib/query.go b/blues-expert/lib/query.go index 4ee7941..b6bb82e 100644 --- a/blues-expert/lib/query.go +++ b/blues-expert/lib/query.go @@ -77,6 +77,13 @@ type SearchResult struct { // SearchNotecardDocs performs a search against the Blues documentation API func SearchNotecardDocs(ctx context.Context, request *mcp.CallToolRequest, query string) (*mcp.CallToolResult, error) { + // Ensure session exists if request is provided + if request != nil && request.Session != nil { + sessionID := GetSessionIDFromRequest(request) + if sessionID != "" { + GetSessionManager().GetOrCreateSession(sessionID) + } + } // Create HTTP client with timeout client := &http.Client{ @@ -159,9 +166,15 @@ func SearchNotecardDocs(ctx context.Context, request *mcp.CallToolRequest, query // Check response status if resp.StatusCode != http.StatusOK { + // Try to read error response body for more details + body, _ := io.ReadAll(resp.Body) + errorMsg := fmt.Sprintf("Search API returned status %d", resp.StatusCode) + if len(body) > 0 { + errorMsg += fmt.Sprintf(": %s", string(body)) + } return &mcp.CallToolResult{ Content: []mcp.Content{ - &mcp.TextContent{Text: fmt.Sprintf("Search API returned status %d", resp.StatusCode)}, + &mcp.TextContent{Text: errorMsg}, }, IsError: true, }, nil diff --git a/blues-expert/lib/session.go b/blues-expert/lib/session.go index 2ca8ceb..8bc8183 100644 --- a/blues-expert/lib/session.go +++ b/blues-expert/lib/session.go @@ -2,11 +2,11 @@ package lib import ( "encoding/json" - "log" "sync" "time" "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/rs/zerolog/log" ) var globalSessionManager *SessionManager @@ -82,7 +82,7 @@ func (sm *SessionManager) GetOrCreateSession(sessionID string) *SessionData { RequestLog: make([]RequestLog, 0), } sm.sessions[sessionID] = session - log.Printf("Session %s created", sessionID) + log.Info().Str("session_id", sessionID).Msg("Session created") } else { session.LastAccessed = time.Now() } @@ -123,8 +123,11 @@ func (sm *SessionManager) RemoveSession(sessionID string) { if session, exists := sm.sessions[sessionID]; exists { // Log session exit with summary statistics - log.Printf("Session %s exited after %d requests (duration: %v)", - sessionID, session.RequestCount, time.Since(session.CreatedAt).Truncate(time.Second)) + log.Info(). + Str("session_id", sessionID). + Int64("request_count", session.RequestCount). + Dur("duration", time.Since(session.CreatedAt).Truncate(time.Second)). + Msg("Session exited") delete(sm.sessions, sessionID) } } @@ -171,10 +174,12 @@ func (sm *SessionManager) cleanupExpiredSessions() { // Remove expired sessions for _, sessionID := range expiredSessions { if session, exists := sm.sessions[sessionID]; exists { - log.Printf("Session %s expired after %d requests (duration: %v, idle: %v)", - sessionID, session.RequestCount, - time.Since(session.CreatedAt).Truncate(time.Second), - time.Since(session.LastAccessed).Truncate(time.Second)) + log.Info(). + Str("session_id", sessionID). + Int64("request_count", session.RequestCount). + Dur("duration", time.Since(session.CreatedAt).Truncate(time.Second)). + Dur("idle", time.Since(session.LastAccessed).Truncate(time.Second)). + Msg("Session expired") delete(sm.sessions, sessionID) } } @@ -182,7 +187,7 @@ func (sm *SessionManager) cleanupExpiredSessions() { sm.mu.Unlock() if len(expiredSessions) > 0 { - log.Printf("Cleaned up %d expired sessions", len(expiredSessions)) + log.Info().Int("count", len(expiredSessions)).Msg("Cleaned up expired sessions") } } } @@ -198,10 +203,13 @@ func GetSessionIDFromRequest(request *mcp.CallToolRequest) string { // LogSessionActivity logs session activity for monitoring func LogSessionActivity(sessionID, toolName string, sessionData *SessionData) { if sessionID == "" || sessionID == "stateless" { - log.Printf("Tool %s called (stateless session)", toolName) + log.Debug().Str("tool", toolName).Msg("Tool called (stateless session)") } else { - log.Printf("Tool %s called by session %s (requests: %d)", - toolName, sessionID, sessionData.RequestCount) + log.Debug(). + Str("tool", toolName). + Str("session_id", sessionID). + Int64("request_count", sessionData.RequestCount). + Msg("Tool called") } } @@ -219,18 +227,30 @@ func LogSessionActivityWithArgs(sessionID, toolName string, sessionData *Session } if sessionID == "" || sessionID == "stateless" { - log.Printf("Tool %s called (stateless session) with args: %s", toolName, argsStr) + log.Debug(). + Str("tool", toolName). + Str("arguments", argsStr). + Msg("Tool called (stateless session)") } else { historyCount := len(sessionData.RequestLog) totalRequests := sessionData.RequestCount // Show if we've truncated history if totalRequests > int64(historyCount) && historyCount == 50 { - log.Printf("Tool %s called by session %s (total: %d requests, recent: %d stored) with args: %s", - toolName, sessionID, totalRequests, historyCount, argsStr) + log.Debug(). + Str("tool", toolName). + Str("session_id", sessionID). + Int64("total_requests", totalRequests). + Int("stored_requests", historyCount). + Str("arguments", argsStr). + Msg("Tool called") } else { - log.Printf("Tool %s called by session %s (requests: %d) with args: %s", - toolName, sessionID, totalRequests, argsStr) + log.Debug(). + Str("tool", toolName). + Str("session_id", sessionID). + Int64("request_count", totalRequests). + Str("arguments", argsStr). + Msg("Tool called") } } } diff --git a/blues-expert/lib/validate.go b/blues-expert/lib/validate.go index c196558..d61fce5 100644 --- a/blues-expert/lib/validate.go +++ b/blues-expert/lib/validate.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "os" "path/filepath" @@ -15,6 +14,7 @@ import ( "time" "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/rs/zerolog/log" "github.com/santhosh-tekuri/jsonschema/v5" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" // Enable HTTP/HTTPS loading ) @@ -73,7 +73,18 @@ func extractRefs(schemaMap map[string]interface{}, baseURL string) []string { } // fetchAndCacheSchema fetches a schema from the URL and caches it +// If request is provided, it will create or retrieve a session for logging func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url string) (io.Reader, error) { + // Get or create session if request is provided + var sessionID string + if request != nil && request.Session != nil { + sessionID = GetSessionIDFromRequest(request) + if sessionID != "" { + // Ensure session exists in the session manager + GetSessionManager().GetOrCreateSession(sessionID) + } + } + // Log that we're fetching the schema if request != nil && request.Session != nil { request.Session.Log(ctx, &mcp.LoggingMessageParams{ @@ -81,7 +92,7 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url Data: fmt.Sprintf("Fetching Notecard API schema from %s...", url), }) } - log.Printf("Fetching Notecard API schema from %s...", url) + log.Info().Str("url", url).Msg("Fetching Notecard API schema") resp, err := http.Get(url) if err != nil { @@ -96,7 +107,7 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url Data: "Schema download in progress, please wait...", }) } - log.Println("Schema download in progress, please wait...") + log.Info().Msg("Schema download in progress, please wait...") if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to fetch schema %s: status %d", url, resp.StatusCode) @@ -113,7 +124,7 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url Data: "Processing and validating schema...", }) } - log.Println("Processing and validating schema...") + log.Info().Msg("Processing and validating schema...") // Verify it's valid JSON before caching and extract version var schemaMap map[string]interface{} @@ -134,18 +145,18 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url Data: "Caching schema for future use...", }) } - log.Println("Caching schema for future use...") + log.Info().Msg("Caching schema for future use...") // Save to cache cachePath := getCachePath(url) fetchTime := time.Now() if err := os.WriteFile(cachePath, data, 0600); err != nil { // Log error but continue - don't fail if we can't cache - fmt.Fprintf(os.Stderr, "warning: failed to cache schema %s: %v\n", url, err) + log.Warn().Str("url", url).Err(err).Msg("Failed to cache schema") } else { // Save cache metadata with schema version if err := saveCacheMetadata(url, fetchTime, schemaVersion); err != nil { - fmt.Fprintf(os.Stderr, "warning: failed to save cache metadata for %s: %v\n", url, err) + log.Warn().Str("url", url).Err(err).Msg("Failed to save cache metadata") } } @@ -156,7 +167,7 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url Data: "Schema fetch and cache completed successfully", }) } - log.Println("Schema fetch and cache completed successfully") + log.Info().Msg("Schema fetch and cache completed successfully") return bytes.NewReader(data), nil } @@ -353,10 +364,14 @@ func initSchema(url string) error { // Extract and cache referenced schemas refs := extractRefs(mainSchema, url) if len(refs) > 0 { - log.Printf("Processing %d referenced schema files...", len(refs)) + log.Info().Int("count", len(refs)).Msg("Processing referenced schema files") } for i, refURL := range refs { - log.Printf("Loading referenced schema %d/%d: %s", i+1, len(refs), filepath.Base(refURL)) + log.Info(). + Int("current", i+1). + Int("total", len(refs)). + Str("file", filepath.Base(refURL)). + Msg("Loading referenced schema") refReader, err := loadOrFetchSchema(refURL) if err != nil { schemaErr = fmt.Errorf("failed to load referenced schema %s: %v", refURL, err) @@ -536,7 +551,7 @@ func GetNotecardAPIs(ctx context.Context, request *mcp.CallToolRequest, apiName Data: "No cached API schema found, fetching fresh schema from remote...", }) } - log.Println("No cached API schema found, fetching fresh schema from remote...") + log.Info().Msg("No cached API schema found, fetching fresh schema from remote...") // Force a fresh fetch by safely resetting the schema cache schemaMutex.Lock() @@ -572,7 +587,7 @@ func GetNotecardAPIs(ctx context.Context, request *mcp.CallToolRequest, apiName Data: fmt.Sprintf("API '%s' not found in cache, refreshing schema...", apiName), }) } - log.Printf("API '%s' not found in cache, refreshing schema...", apiName) + log.Info().Str("api", apiName).Msg("API not found in cache, refreshing schema...") // Try to refresh the cache in case the API was recently added schemaMutex.Lock() diff --git a/blues-expert/main.go b/blues-expert/main.go index e4d1612..5cee75b 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -2,7 +2,6 @@ package main import ( "flag" - "log" "net/http" "os" @@ -11,26 +10,32 @@ import ( "github.com/joho/godotenv" "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/rs/zerolog/log" ) var ( envFilePath string + logLevel string sessionManager *lib.SessionManager ) func init() { flag.StringVar(&envFilePath, "env", "", "Path to .env file to load environment variables") + flag.StringVar(&logLevel, "log-level", "info", "Log level (trace, debug, info, warn, error, fatal, panic)") } func main() { flag.Parse() + // Initialize logger with specified log level + lib.InitLogger(logLevel) + // Load environment variables from .env file if specified if envFilePath != "" { - log.Printf("Loading environment variables from %s", envFilePath) + log.Info().Str("path", envFilePath).Msg("Loading environment variables") err := godotenv.Load(envFilePath) if err != nil { - log.Printf("Warning: Failed to load .env file '%s': %v", envFilePath, err) + log.Warn().Err(err).Str("path", envFilePath).Msg("Failed to load .env file") } } @@ -46,7 +51,7 @@ func main() { s := mcp.NewServer(impl, opts) // Send initial startup log - log.Println("Blues Expert MCP server starting...") + log.Info().Msg("Blues Expert MCP server starting...") // Add tools firmwareEntrypointTool := CreateFirmwareEntrypointTool() @@ -90,12 +95,12 @@ func main() { httpHandler.ServeHTTP(w, r) }) - log.Printf("Starting HTTP server on port %s", port) - log.Printf("MCP server available at /expert/") - log.Printf("Health check at /expert/health") + log.Info().Str("port", port).Msg("Starting HTTP server") + log.Info().Msg("MCP server available at /expert/") + log.Info().Msg("Health check at /expert/health") // Start HTTP server with our custom multiplexer if err := http.ListenAndServe(":"+port, mux); err != nil { - log.Fatal(err) + log.Fatal().Err(err).Msg("Failed to start HTTP server") } } diff --git a/go.mod b/go.mod index 173ac2a..2f1920d 100644 --- a/go.mod +++ b/go.mod @@ -34,10 +34,13 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rs/zerolog v1.34.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect diff --git a/go.sum b/go.sum index 1c3f0d0..125fadf 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,12 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/google/jsonschema-go v0.2.0 h1:Uh19091iHC56//WOsAd1oRg6yy1P9BpSvpjOL6RcjLQ= @@ -57,10 +59,16 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.38.0 h1:E5tmJiIXkhwlV0pLAwAT0O5ZjUZSISE/2Jxg+6vpq4I= github.com/mark3labs/mcp-go v0.38.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +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/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modelcontextprotocol/go-sdk v0.3.0 h1:/1XC6+PpdKfE4CuFJz8/goo0An31bu8n8G8d3BkeJoY= github.com/modelcontextprotocol/go-sdk v0.3.0/go.mod h1:71VUZVa8LL6WARvSgLJ7DMpDWSeomT4uBv8g97mGBvo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= @@ -73,6 +81,9 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 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/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= @@ -83,6 +94,9 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= From 42ce6d804a2020f324c426f61934b8bf3adf4462 Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 11 Nov 2025 13:42:51 +0000 Subject: [PATCH 4/5] fix: remove mermaid diagram --- blues-expert/lib/docs/arduino/best_practices.md | 1 - 1 file changed, 1 deletion(-) diff --git a/blues-expert/lib/docs/arduino/best_practices.md b/blues-expert/lib/docs/arduino/best_practices.md index 21c1af5..31cdf4e 100644 --- a/blues-expert/lib/docs/arduino/best_practices.md +++ b/blues-expert/lib/docs/arduino/best_practices.md @@ -6,7 +6,6 @@ When creating a new Arduino project, there are a few best practices to follow to - The project or sketch should be in a directory of the same name as the project, e.g. 'app/app.ino' (this is required for the Arduino CLI to work correctly) - Create a 'README.md' in the same directory as the sketch, e.g. 'app/README.md'. This should contain a description of the project, along with instruction for how to connect any sensors to the Notecard. -- Create a 'WORKFLOW.mmd' in the same directory as the sketch, e.g. 'app/WORKFLOW.mmd'. This should contain a diagram of the flow of code in the project. - Always assume the user is using the Blues Feather MCU (e.g. Swan) and Notecarrier-F. Where sensors are concerned, always default to using the I2C interface, if possible. ## Requirements From 737b5cc7c54d24511e95a895dfc0d94c26317d93 Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 11 Nov 2025 13:53:11 +0000 Subject: [PATCH 5/5] feat: add python guidelines --- .../lib/docs/python/best_practices.md | 194 ++++++++++++++++++ blues-expert/lib/docs/python/index.md | 57 +++++ 2 files changed, 251 insertions(+) create mode 100644 blues-expert/lib/docs/python/best_practices.md diff --git a/blues-expert/lib/docs/python/best_practices.md b/blues-expert/lib/docs/python/best_practices.md new file mode 100644 index 0000000..b644ce5 --- /dev/null +++ b/blues-expert/lib/docs/python/best_practices.md @@ -0,0 +1,194 @@ +# Python Notecard Best Practices + +When creating a new Python project with the Notecard, there are a few best practices to follow to ensure that the project is easy to maintain and extend. + +## Project Structure + +- Organize your project with a clear directory structure, typically with a main application file (e.g. 'app.py' or 'main.py') +- Create a 'README.md' in the project directory. This should contain a description of the project, setup instructions, and details for how to connect any sensors to the Notecard. +- Create a 'requirements.txt' file to specify Python dependencies, including the note-python library +- Common hardware configurations include Raspberry Pi, PC/Mac development machines, or Python-capable embedded systems. Where sensors are concerned, always default to using the I2C interface, if possible. + +## Requirements + +- Always use templates for notes when sending data to minimize bandwidth usage. +- Use the note-python library for all Notecard interactions. + +## Suggestions + +- Do not introduce power management features until the user has confirmed that the application is working. Offer this as a follow up change. +- Start with console debugging output to demonstrate that the application is working. After the user has confirmed that the application is working, logging can be adjusted. +- If the user asks for their data to be uploaded at a specific interval, ensure to set the `mode` to `periodic` in the `hub.set` request and the `outbound` to their desired interval. +- Use proper error handling and exception management when communicating with the Notecard. + +## Example Basic Project + +Use this example to get started with building a Python project that uses the Notecard. + +You will need to know the following before starting: + +- REQUIRED: The Product Unique Identifier for your application. This is a unique identifier for your application that is used to identify your Notehub project in the Notecard. +- REQUIRED (if not using I2C): The Notecard's serial port. This is the serial port that the Notecard is connected to (e.g., '/dev/ttyUSB0' on Linux or 'COM3' on Windows). +- REQUIRED (if not using serial): The I2C bus. On Raspberry Pi, this is typically bus 1. + +```python +#!/usr/bin/env python3 +""" +This example demonstrates the basic usage of the note-python library +to communicate with the Blues Notecard, configure it, and send sensor data. +""" + +import notecard +import time +import sys + +# Product UID for your Notehub project +# This should be in the format "com.company.username:productname" +# You must register at notehub.io to claim a product UID +PRODUCT_UID = "com.my-company.my-name:my-project" + +def main(): + """Main application loop""" + print(f"Starting Python application for {PRODUCT_UID}...") + + # Initialize Notecard + # For I2C connection (default on Raspberry Pi): + card = notecard.OpenI2C(0, 0, 0, debug=True) + + # For serial connection, use this instead: + # card = notecard.OpenSerial("/dev/ttyUSB0", debug=True) + + # Configure the Notecard to connect to Notehub + req = {"req": "hub.set"} + req["product"] = PRODUCT_UID + req["mode"] = "continuous" # Use "periodic" for battery-powered devices + + try: + rsp = card.Transaction(req) + print("Notecard configured successfully") + except Exception as e: + print(f"Error configuring Notecard: {e}") + sys.exit(1) + + # Main loop: send data every 15 seconds + event_counter = 0 + max_events = 25 + + while event_counter < max_events: + event_counter += 1 + + # Read temperature from Notecard's built-in sensor + try: + temp_req = {"req": "card.temp"} + temp_rsp = card.Transaction(temp_req) + temperature = temp_rsp.get("value", 0) + except Exception as e: + print(f"Error reading temperature: {e}") + temperature = 0 + + # Read voltage from Notecard + try: + volt_req = {"req": "card.voltage"} + volt_rsp = card.Transaction(volt_req) + voltage = volt_rsp.get("value", 0) + except Exception as e: + print(f"Error reading voltage: {e}") + voltage = 0 + + # Send note to Notehub + try: + note_req = {"req": "note.add"} + note_req["sync"] = True # Upload immediately for demonstration + note_req["body"] = { + "temp": temperature, + "voltage": voltage, + "count": event_counter + } + card.Transaction(note_req) + print(f"Sent event {event_counter}: temp={temperature:.2f}°C, voltage={voltage:.2f}V") + except Exception as e: + print(f"Error sending note: {e}") + + # Wait before next reading + time.sleep(15) + + print("Demo cycle complete. Program finished.") + +if __name__ == "__main__": + main() +``` + +This basic example demonstrates how to use the note-python library to send and receive JSON commands and responses. +It does not make use of the Notecard's templated note feature, see below for more information. + +## Installation + +Install the note-python library: + +```bash +pip install note-python +``` + +Or add it to your `requirements.txt`: + +```txt +note-python>=1.2.0 +``` + +## Use Templates + +When sending data repeatedly with the same structure, always use note templates to reduce bandwidth usage. Templates are defined once and then referenced in subsequent notes, sending only the data values without field names. + +Example of setting up a template: + +```python +# Define the template structure +template_req = {"req": "note.template"} +template_req["file"] = "sensors.qo" +template_req["body"] = { + "temp": 14.1, + "humidity": 12.1, + "pressure": 14.1 +} + +try: + card.Transaction(template_req) + print("Template created successfully") +except Exception as e: + print(f"Error creating template: {e}") +``` + +After defining the template, send notes with just the values: + +```python +# Send note using the template +note_req = {"req": "note.add"} +note_req["file"] = "sensors.qo" +note_req["body"] = { + "temp": 23.5, + "humidity": 65.2, + "pressure": 1013.25 +} + +card.Transaction(note_req) +``` + +## Connection Options + +The note-python library supports both I2C and serial connections: + +### I2C Connection (Recommended for Raspberry Pi) + +```python +card = notecard.OpenI2C(0, 0, 0, debug=True) +``` + +### Serial Connection + +```python +# Linux/Mac +card = notecard.OpenSerial("/dev/ttyUSB0", debug=True) + +# Windows +card = notecard.OpenSerial("COM3", debug=True) +``` diff --git a/blues-expert/lib/docs/python/index.md b/blues-expert/lib/docs/python/index.md index 9964c48..ac33fd0 100644 --- a/blues-expert/lib/docs/python/index.md +++ b/blues-expert/lib/docs/python/index.md @@ -8,10 +8,67 @@ Support for CircuitPython and MicroPython is also available. ## Installing the note-python SDK +If required, the user may wish to install the note-python library using pip. + ```bash pip install note-python ``` +Or add it to `requirements.txt`: + +```txt +note-python>=1.2.0 +``` + +## Code Layout + +When either creating a new project or retrofitting existing projects to use the Notecard, ensure that the following structures are present: + +- ALL Notecard code should be in a separate module/file from the main application (VERY IMPORTANT). +- Limit modifications to the user's main application code; prefer to add new functions to the Notecard module. +- An 'init' or 'setup' function that initializes the Notecard connection, along with `hub.set` commands to configure the Notecard. + - Before optimizing the code, set the `mode` to `continuous` for easy debugging. Use a comment to indicate that the user should change this to `periodic` to fit their application. +- Include proper error handling with try/except blocks for all Notecard transactions. +- Once the initial pass has been made, go to [power management](#power-management) to ensure that the Notecard is power efficient. + +## Design Patterns + +Work through the following design patterns to ensure that target projects are easy to maintain and extend. + +ALWAYS: + +- use templates for notes and ensure that the data types are correct. ALWAYS use the `firmware_best_practices` tool with the `templates` document type for more information. +- use the I2C interface for Notecard communication (default on Raspberry Pi), unless instructed otherwise by the user. +- use Blues Expert tools to check and validate Notecard requests and responses. +- generate a header docstring in the Notecard module that contains the following information: + +```python +""" + - Module for + +This module encapsulates all Notecard functionality for the . +This is specific to your project and is NOT A GENERAL PURPOSE LIBRARY. + +THIS FILE SHOULD BE EDITED AFTER GENERATION. +IT IS PROVIDED AS A STARTING POINT FOR THE USER TO EDIT AND EXTEND. +""" +``` + +TRY TO: + +- use Python's logging module for debugging output. This should be easily configurable by the user. +- follow PEP 8 style guidelines for Python code. + +NEVER: + +- layer note-python calls within the user's main application code. If the Notecard is used, it should be handled by a function in the newly created Notecard module. + +## Power Management + +Implement the initial changes following this guide and then ask the user if they would like to optimize the code for power management. + +- use the `firmware_best_practices` tool with the `best_practices` document type. + ## Further Reading Queries about note-python specifics should be made to the `docs_search` or `docs_search_expert` tool.